crowi-form.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. var pageId = $('#content-main').data('page-id');
  2. var pagePath= $('#content-main').data('path');
  3. var isEnabledLineBreaks = $('#content-main').data('linebreaks-enabled');
  4. // generate options obj
  5. var rendererOptions = {
  6. // see: https://www.npmjs.com/package/marked
  7. marked: {
  8. breaks: isEnabledLineBreaks
  9. }
  10. };
  11. require('bootstrap-sass');
  12. require('inline-attachment/src/inline-attachment');
  13. require('./thirdparty-js/jquery.selection');
  14. // show/hide
  15. function FetchPagesUpdatePostAndInsert(path) {
  16. $.get('/_api/pages.updatePost', {path: path}, function(res) {
  17. if (res.ok) {
  18. var $slackChannels = $('#page-form-slack-channel');
  19. $slackChannels.val(res.updatePost.join(','));
  20. }
  21. });
  22. }
  23. var slackConfigured = $('#page-form-setting').data('slack-configured');
  24. // for new page
  25. if (!pageId) {
  26. if (!pageId && pagePath.match(/(20\d{4}|20\d{6}|20\d{2}_\d{1,2}|20\d{2}_\d{1,2}_\d{1,2})/)) {
  27. $('#page-warning-modal').modal('show');
  28. }
  29. if (slackConfigured) {
  30. FetchPagesUpdatePostAndInsert(pagePath);
  31. }
  32. }
  33. $('a[data-toggle="tab"][href="#edit-form"]').on('show.bs.tab', function() {
  34. $('.content-main').addClass('on-edit');
  35. if (slackConfigured) {
  36. var $slackChannels = $('#page-form-slack-channel');
  37. var slackChannels = $slackChannels.val();
  38. // if slackChannels is empty, then fetch default (admin setting)
  39. // if not empty, it means someone specified this setting for the page.
  40. if (slackChannels === '') {
  41. FetchPagesUpdatePostAndInsert(pagePath);
  42. }
  43. }
  44. });
  45. $('a[data-toggle="tab"][href="#edit-form"]').on('hide.bs.tab', function() {
  46. $('.content-main').removeClass('on-edit');
  47. });
  48. /**
  49. * DOM ready
  50. */
  51. $(function() {
  52. // preview watch
  53. var originalContent = $('#form-body').val();
  54. // restore draft
  55. // とりあえず、originalContent がない場合のみ復元する。(それ以外の場合は後で考える)
  56. var draft = crowi.findDraft(pagePath);
  57. var originalRevision = $('#page-form [name="pageForm[currentRevision]"]').val();
  58. if (!originalRevision && draft) {
  59. // TODO
  60. $('#form-body').val(draft)
  61. }
  62. var prevContent = originalContent;
  63. function renderPreview() {
  64. var markdown = $('#form-body').val();
  65. // create context object
  66. var context = {
  67. markdown,
  68. currentPagePath: decodeURIComponent(location.pathname)
  69. };
  70. crowi.interceptorManager.process('preRenderPreview', context)
  71. .then(() => crowi.interceptorManager.process('prePreProcess', context))
  72. .then(() => {
  73. context.markdown = crowiRenderer.preProcess(context.markdown);
  74. })
  75. .then(() => crowi.interceptorManager.process('postPreProcess', context))
  76. .then(() => {
  77. var parsedHTML = crowiRenderer.render(context.markdown, rendererOptions);
  78. context['parsedHTML'] = parsedHTML;
  79. })
  80. .then(() => crowi.interceptorManager.process('postRenderPreview', context))
  81. .then(() => crowi.interceptorManager.process('preRenderPreviewHtml', context))
  82. // render HTML with jQuery
  83. .then(() => {
  84. $('#preview-body').html(context.parsedHTML);
  85. Promise.resolve($('#preview-body'));
  86. })
  87. // process interceptors for post rendering
  88. .then((bodyElement) => {
  89. context = Object.assign(context, {bodyElement})
  90. return crowi.interceptorManager.process('postRenderPreviewHtml', context);
  91. });
  92. }
  93. // for initialize preview
  94. renderPreview();
  95. var watchTimer = setInterval(function() {
  96. var content = $('#form-body').val();
  97. if (prevContent != content) {
  98. renderPreview();
  99. prevContent = content;
  100. }
  101. }, 500);
  102. // edit detection
  103. var isFormChanged = false;
  104. $(window).on('beforeunload', function(e) {
  105. if (isFormChanged) {
  106. return '編集中の内容があります。内容を破棄してページを移動しますか?';
  107. }
  108. });
  109. $('#form-body').on('keyup change', function(e) {
  110. var content = $('#form-body').val();
  111. if (originalContent != content) {
  112. isFormChanged = true;
  113. crowi.saveDraft(pagePath, content);
  114. } else {
  115. isFormChanged = false;
  116. crowi.clearDraft(pagePath);
  117. }
  118. });
  119. $('#page-form').on('submit', function(e) {
  120. // avoid message
  121. isFormChanged = false;
  122. crowi.clearDraft(pagePath);
  123. });
  124. // This is a temporary implementation until porting to React.
  125. var insertText = function(start, end, newText, mode) {
  126. var editor = document.querySelector('#form-body');
  127. mode = mode || 'after';
  128. switch (mode) {
  129. case 'before':
  130. editor.setSelectionRange(start, start);
  131. break;
  132. case 'replace':
  133. editor.setSelectionRange(start, end);
  134. break;
  135. case 'after':
  136. default:
  137. editor.setSelectionRange(end, end);
  138. }
  139. editor.focus();
  140. var inserted = false;
  141. try {
  142. // Chrome, Safari
  143. inserted = document.execCommand('insertText', false, newText);
  144. } catch (e) {
  145. inserted = false;
  146. }
  147. if (!inserted) {
  148. // Firefox
  149. editor.value = editor.value.substr(0, start) + newText + editor.value.substr(end);
  150. }
  151. };
  152. var getCurrentLine = function(event) {
  153. var $target = $(event.target);
  154. var text = $target.val();
  155. var pos = $target.selection('getPos');
  156. if (text === null || pos.start !== pos.end) {
  157. return null;
  158. }
  159. var startPos = text.lastIndexOf("\n", pos.start - 1) + 1;
  160. var endPos = text.indexOf("\n", pos.start);
  161. if (endPos === -1) {
  162. endPos = text.length;
  163. }
  164. return {
  165. text: text.slice(startPos, endPos),
  166. start: startPos,
  167. end: endPos,
  168. caret: pos.start,
  169. endOfLine: !$.trim(text.slice(pos.start, endPos))
  170. };
  171. };
  172. var getPrevLine = function(event) {
  173. var $target = $(event.target);
  174. var currentLine = getCurrentLine(event);
  175. var text = $target.val().slice(0, currentLine.start);
  176. var startPos = text.lastIndexOf("\n", currentLine.start - 2) + 1;
  177. var endPos = currentLine.start;
  178. return {
  179. text: text.slice(startPos, endPos),
  180. start: startPos,
  181. end: endPos
  182. };
  183. };
  184. var handleTabKey = function(event) {
  185. event.preventDefault();
  186. var $target = $(event.target);
  187. var currentLine = getCurrentLine(event);
  188. var text = $target.val();
  189. var pos = $target.selection('getPos');
  190. if (currentLine) {
  191. $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
  192. }
  193. if (event.shiftKey === true) {
  194. if (currentLine && currentLine.text.charAt(0) === '|') {
  195. // prev cell in table
  196. var newPos = text.lastIndexOf('|', pos.start - 1);
  197. if (newPos > 0) {
  198. $target.selection('setPos', {start: newPos - 1, end: newPos - 1});
  199. }
  200. } else {
  201. // re indent
  202. var reindentedText = $target.selection().replace(/^ {1,4}/gm, '');
  203. var reindentedCount = $target.selection().length - reindentedText.length;
  204. $target.selection('replace', {text: reindentedText, mode: 'before'});
  205. if (currentLine) {
  206. $target.selection('setPos', {start: pos.start - reindentedCount, end: pos.start - reindentedCount});
  207. }
  208. }
  209. } else {
  210. if (currentLine && currentLine.text.charAt(0) === '|') {
  211. // next cell in table
  212. var newPos = text.indexOf('|', pos.start + 1);
  213. if (newPos < 0 || newPos === text.lastIndexOf('|', currentLine.end - 1)) {
  214. $target.selection('setPos', {start: currentLine.end, end: currentLine.end});
  215. } else {
  216. $target.selection('setPos', {start: newPos + 2, end: newPos + 2});
  217. }
  218. } else {
  219. // indent
  220. $target.selection('replace', {
  221. text: ' ' + $target.selection().split("\n").join("\n "),
  222. mode: 'before'
  223. });
  224. if (currentLine) {
  225. $target.selection('setPos', {start: pos.start + 4, end: pos.start + 4});
  226. }
  227. }
  228. }
  229. $target.trigger('input');
  230. };
  231. var handleEnterKey = function(event) {
  232. if (event.metaKey || event.ctrlKey || event.shiftKey) {
  233. return;
  234. }
  235. var currentLine = getCurrentLine(event);
  236. if (!currentLine || currentLine.start === currentLine.caret) {
  237. return;
  238. }
  239. var $target = $(event.target);
  240. var match = currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)\s*\S/);
  241. if (match) {
  242. // smart indent with list
  243. if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] ))\s*$/)) {
  244. // empty task list
  245. $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
  246. return;
  247. }
  248. event.preventDefault();
  249. var listMark = match[1].replace(/\[x\]/, '[ ]');
  250. var listMarkMatch = listMark.match(/^(\s*)(\d+)\./);
  251. if (listMarkMatch) {
  252. var indent = listMarkMatch[1];
  253. var num = parseInt(listMarkMatch[2]);
  254. if (num !== 1) {
  255. listMark = listMark.replace(/\s*\d+/, indent + (num +1));
  256. }
  257. }
  258. //$target.selection('insert', {text: "\n" + listMark, mode: 'before'});
  259. var pos = $target.selection('getPos');
  260. insertText(pos.start, pos.start, "\n" + listMark, 'replace');
  261. var newPosition = pos.start + ("\n" + listMark).length;
  262. $target.selection('setPos', {start: newPosition, end: newPosition});
  263. } else if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) )/)) {
  264. // remove list
  265. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  266. } else if (currentLine.text.match(/^.*\|\s*$/)) {
  267. // new row for table
  268. if (currentLine.text.match(/^[\|\s]+$/)) {
  269. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  270. return;
  271. }
  272. if (!currentLine.endOfLine) {
  273. return;
  274. }
  275. event.preventDefault();
  276. var row = [];
  277. var cellbarMatch = currentLine.text.match(/\|/g);
  278. for (var i = 0; i < cellbarMatch.length; i++) {
  279. row.push('|');
  280. }
  281. var prevLine = getPrevLine(event);
  282. if (!prevLine || (!currentLine.text.match(/---/) && !prevLine.text.match(/\|/g))) {
  283. //$target.selection('insert', {text: "\n" + row.join(' --- ') + "\n" + row.join(' '), mode: 'before'});
  284. var pos = $target.selection('getPos');
  285. insertText(pos.start, pos.start, "\n" + row.join(' --- ') + "\n" + row.join(' '), 'after');
  286. $target.selection('setPos', {start: currentLine.caret + 6 * row.length - 1, end: currentLine.caret + 6 * row.length - 1});
  287. } else {
  288. //$target.selection('insert', {text: "\n" + row.join(' '), mode: 'before'});
  289. var pos = $target.selection('getPos');
  290. insertText(pos.start, pos.end, "\n" + row.join(' '), 'after');
  291. $target.selection('setPos', {start: currentLine.caret + 3, end: currentLine.caret + 3});
  292. }
  293. }
  294. $target.trigger('input');
  295. };
  296. var handleEscapeKey = function(event) {
  297. event.preventDefault();
  298. var $target = $(event.target);
  299. $target.blur();
  300. };
  301. var handleSpaceKey = function(event) {
  302. // keybind: alt + shift + space
  303. if (!(event.shiftKey && event.altKey)) {
  304. return;
  305. }
  306. var currentLine = getCurrentLine(event);
  307. if (!currentLine) {
  308. return;
  309. }
  310. var $target = $(event.target);
  311. var match = currentLine.text.match(/^(\s*)(-|\+|\*|\d+\.) (?:\[(x| )\] )(.*)/);
  312. if (match) {
  313. event.preventDefault();
  314. var checkMark = (match[3] == ' ') ? 'x' : ' ';
  315. var replaceTo = match[1] + match[2] + ' [' + checkMark + '] ' + match[4];
  316. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  317. //$target.selection('replace', {text: replaceTo, mode: 'keep'});
  318. insertText(currentLine.start, currentLine.end, replaceTo, 'replace');
  319. $target.selection('setPos', {start: currentLine.caret, end: currentLine.caret});
  320. $target.trigger('input');
  321. }
  322. };
  323. // markdown helper inspired by 'esarea'.
  324. // see: https://github.com/fukayatsu/esarea
  325. $('textarea#form-body').on('keydown', function(event) {
  326. switch (event.which || event.keyCode) {
  327. case 9:
  328. handleTabKey(event);
  329. break;
  330. case 13:
  331. handleEnterKey(event);
  332. break;
  333. case 27:
  334. handleEscapeKey(event);
  335. break;
  336. case 32:
  337. handleSpaceKey(event);
  338. break;
  339. default:
  340. }
  341. });
  342. var handlePasteEvent = function(event) {
  343. var currentLine = getCurrentLine(event);
  344. if (!currentLine) {
  345. return false;
  346. }
  347. var $target = $(event.target);
  348. var pasteText = event.clipboardData.getData('text');
  349. var match = currentLine.text.match(/^(\s*(?:>|\-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)/);
  350. if (match) {
  351. if (pasteText.match(/(?:\r\n|\r|\n)/)) {
  352. pasteText = pasteText.replace(/(\r\n|\r|\n)/g, "$1" + match[1]);
  353. }
  354. }
  355. //$target.selection('insert', {text: pasteText, mode: 'after'});
  356. insertText(currentLine.caret, currentLine.caret, pasteText, 'replace');
  357. var newPos = currentLine.caret + pasteText.length;
  358. $target.selection('setPos', {start: newPos, end: newPos});
  359. return true;
  360. };
  361. document.getElementById('form-body').addEventListener('paste', function(event) {
  362. if (handlePasteEvent(event)) {
  363. event.preventDefault();
  364. }
  365. });
  366. var unbindInlineAttachment = function($form) {
  367. $form.unbind('.inlineattach');
  368. };
  369. var bindInlineAttachment = function($form, attachmentOption) {
  370. var $this = $form;
  371. var editor = createEditorInstance($form);
  372. var inlineattach = new inlineAttachment(attachmentOption, editor);
  373. $form.bind({
  374. 'paste.inlineattach': function(e) {
  375. inlineattach.onPaste(e.originalEvent);
  376. },
  377. 'drop.inlineattach': function(e) {
  378. e.stopPropagation();
  379. e.preventDefault();
  380. inlineattach.onDrop(e.originalEvent);
  381. },
  382. 'dragenter.inlineattach dragover.inlineattach': function(e) {
  383. e.stopPropagation();
  384. e.preventDefault();
  385. }
  386. });
  387. };
  388. var createEditorInstance = function($form) {
  389. var $this = $form;
  390. return {
  391. getValue: function() {
  392. return $this.val();
  393. },
  394. insertValue: function(val) {
  395. inlineAttachment.util.insertTextAtCursor($this[0], val);
  396. },
  397. setValue: function(val) {
  398. $this.val(val);
  399. }
  400. };
  401. };
  402. var $inputForm = $('form.uploadable textarea#form-body');
  403. if ($inputForm.length > 0) {
  404. var csrfToken = $('form.uploadable input#edit-form-csrf').val();
  405. var pageId = $('#content-main').data('page-id') || 0;
  406. var attachmentOption = {
  407. uploadUrl: '/_api/attachments.add',
  408. extraParams: {
  409. path: location.pathname,
  410. page_id: pageId,
  411. _csrf: csrfToken
  412. },
  413. progressText: '(Uploading file...)',
  414. urlText: "\n![file]({filename})\n"
  415. };
  416. attachmentOption.onFileUploadResponse = function(res) {
  417. var result = JSON.parse(res.response);
  418. console.log(result);
  419. if (result.ok && result.pageCreated) {
  420. var page = result.page,
  421. pageId = page._id;
  422. $('#content-main').data('page-id', page._id);
  423. $('#page-form [name="pageForm[currentRevision]"]').val(page.revision._id)
  424. unbindInlineAttachment($inputForm);
  425. attachmentOption.extraParams.page_id = pageId;
  426. bindInlineAttachment($inputForm, attachmentOption);
  427. }
  428. return true;
  429. };
  430. bindInlineAttachment($inputForm, attachmentOption);
  431. $('textarea#form-body').on('dragenter dragover', function() {
  432. $(this).addClass('dragover');
  433. });
  434. $('textarea#form-body').on('drop dragleave dragend', function() {
  435. $(this).removeClass('dragover');
  436. });
  437. }
  438. var enableScrollSync = function() {
  439. var getMaxScrollTop = function(dom) {
  440. var rect = dom.getBoundingClientRect();
  441. return dom.scrollHeight - rect.height;
  442. };
  443. var getScrollRate = function(dom) {
  444. var maxScrollTop = getMaxScrollTop(dom);
  445. var rate = dom.scrollTop / maxScrollTop;
  446. return rate;
  447. };
  448. var getScrollTop = function(dom, rate) {
  449. var maxScrollTop = getMaxScrollTop(dom);
  450. var top = maxScrollTop * rate;
  451. return top;
  452. };
  453. var editor = document.querySelector('#form-body');
  454. var preview = document.querySelector('#preview-body');
  455. editor.addEventListener('scroll', function(event) {
  456. var rate = getScrollRate(this);
  457. var top = getScrollTop(preview, rate);
  458. preview.scrollTop = top;
  459. });
  460. };
  461. enableScrollSync();
  462. });