crowi-form.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. var pageId = $('#content-main').data('page-id');
  2. var pagePath= $('#content-main').data('path');
  3. require('bootstrap-select');
  4. // show/hide
  5. function FetchPagesUpdatePostAndInsert(path) {
  6. $.get('/_api/pages.updatePost', {path: path}, function(res) {
  7. if (res.ok) {
  8. var $slackChannels = $('#page-form-slack-channel');
  9. $slackChannels.val(res.updatePost.join(','));
  10. }
  11. });
  12. }
  13. function initSlack() {
  14. if (slackConfigured) {
  15. var $slackChannels = $('#page-form-slack-channel');
  16. var slackChannels = $slackChannels.val();
  17. // if slackChannels is empty, then fetch default (admin setting)
  18. // if not empty, it means someone specified this setting for the page.
  19. if (slackChannels === '') {
  20. FetchPagesUpdatePostAndInsert(pagePath);
  21. }
  22. }
  23. }
  24. var slackConfigured = $('#page-form-setting').data('slack-configured');
  25. // for new page
  26. if (!pageId) {
  27. 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})/)) {
  28. $('#page-warning-modal').modal('show');
  29. }
  30. if (slackConfigured) {
  31. FetchPagesUpdatePostAndInsert(pagePath);
  32. }
  33. }
  34. $('a[data-toggle="tab"][href="#edit-form"]').on('show.bs.tab', function() {
  35. $('body').addClass('on-edit');
  36. $('body').addClass('builtin-editor');
  37. initSlack();
  38. });
  39. $('a[data-toggle="tab"][href="#edit-form"]').on('hide.bs.tab', function() {
  40. $('body').removeClass('on-edit');
  41. $('body').removeClass('builtin-editor');
  42. });
  43. $('a[data-toggle="tab"][href="#hackmd"]').on('show.bs.tab', function() {
  44. $('body').addClass('on-edit');
  45. $('body').addClass('hackmd');
  46. initSlack();
  47. });
  48. $('a[data-toggle="tab"][href="#hackmd"]').on('hide.bs.tab', function() {
  49. $('body').removeClass('on-edit');
  50. $('body').removeClass('hackmd');
  51. });
  52. /**
  53. * DOM ready
  54. */
  55. $(function() {
  56. /*
  57. * DUPRECATED CODES
  58. * using PageEditor React Component -- 2017.01.06 Yuki Takei
  59. *
  60. // preview watch
  61. var originalContent = $('#form-body').val();
  62. // restore draft
  63. // とりあえず、originalContent がない場合のみ復元する。(それ以外の場合は後で考える)
  64. var draft = crowi.findDraft(pagePath);
  65. var originalRevision = $('#page-form [name="pageForm[currentRevision]"]').val();
  66. if (!originalRevision && draft) {
  67. // TODO
  68. $('#form-body').val(draft)
  69. }
  70. var prevContent = originalContent;
  71. function renderPreview() {
  72. var markdown = $('#form-body').val();
  73. var dom = $('#preview-body');
  74. // create context object
  75. var context = {
  76. markdown,
  77. dom,
  78. currentPagePath: decodeURIComponent(location.pathname)
  79. };
  80. crowi.interceptorManager.process('preRenderPreview', context)
  81. .then(() => crowi.interceptorManager.process('prePreProcess', context))
  82. .then(() => {
  83. context.markdown = crowiRenderer.preProcess(context.markdown);
  84. })
  85. .then(() => crowi.interceptorManager.process('postPreProcess', context))
  86. .then(() => {
  87. var parsedHTML = crowiRenderer.render(context.markdown, context.dom);
  88. context['parsedHTML'] = parsedHTML;
  89. })
  90. .then(() => crowi.interceptorManager.process('postRenderPreview', context))
  91. .then(() => crowi.interceptorManager.process('preRenderPreviewHtml', context))
  92. // render HTML with jQuery
  93. .then(() => {
  94. $('#preview-body').html(context.parsedHTML);
  95. Promise.resolve($('#preview-body'));
  96. })
  97. // process interceptors for post rendering
  98. .then((bodyElement) => {
  99. context = Object.assign(context, {bodyElement})
  100. return crowi.interceptorManager.process('postRenderPreviewHtml', context);
  101. });
  102. }
  103. // for initialize preview
  104. renderPreview();
  105. var watchTimer = setInterval(function() {
  106. var content = $('#form-body').val();
  107. if (prevContent != content) {
  108. renderPreview();
  109. prevContent = content;
  110. }
  111. }, 500);
  112. // edit detection
  113. var isFormChanged = false;
  114. $(window).on('beforeunload', function(e) {
  115. if (isFormChanged) {
  116. // TODO i18n
  117. return 'You haven\'t finished your comment yet. Do you want to leave without finishing?';
  118. }
  119. });
  120. $('#form-body').on('keyup change', function(e) {
  121. var content = $('#form-body').val();
  122. if (originalContent != content) {
  123. isFormChanged = true;
  124. crowi.saveDraft(pagePath, content);
  125. } else {
  126. isFormChanged = false;
  127. crowi.clearDraft(pagePath);
  128. }
  129. });
  130. */
  131. $('#page-form').on('submit', function(e) {
  132. // avoid message
  133. // isFormChanged = false;
  134. window.crowi.clearDraft(pagePath);
  135. });
  136. /*
  137. // This is a temporary implementation until porting to React.
  138. var insertText = function(start, end, newText, mode) {
  139. var editor = document.querySelector('#form-body');
  140. mode = mode || 'after';
  141. switch (mode) {
  142. case 'before':
  143. editor.setSelectionRange(start, start);
  144. break;
  145. case 'replace':
  146. editor.setSelectionRange(start, end);
  147. break;
  148. case 'after':
  149. default:
  150. editor.setSelectionRange(end, end);
  151. }
  152. editor.focus();
  153. var inserted = false;
  154. try {
  155. // Chrome, Safari
  156. inserted = document.execCommand('insertText', false, newText);
  157. } catch (e) {
  158. inserted = false;
  159. }
  160. if (!inserted) {
  161. // Firefox
  162. editor.value = editor.value.substr(0, start) + newText + editor.value.substr(end);
  163. }
  164. };
  165. var getCurrentLine = function(event) {
  166. var $target = $(event.target);
  167. var text = $target.val();
  168. var pos = $target.selection('getPos');
  169. if (text === null || pos.start !== pos.end) {
  170. return null;
  171. }
  172. var startPos = text.lastIndexOf("\n", pos.start - 1) + 1;
  173. var endPos = text.indexOf("\n", pos.start);
  174. if (endPos === -1) {
  175. endPos = text.length;
  176. }
  177. return {
  178. text: text.slice(startPos, endPos),
  179. start: startPos,
  180. end: endPos,
  181. caret: pos.start,
  182. endOfLine: !$.trim(text.slice(pos.start, endPos))
  183. };
  184. };
  185. var getPrevLine = function(event) {
  186. var $target = $(event.target);
  187. var currentLine = getCurrentLine(event);
  188. var text = $target.val().slice(0, currentLine.start);
  189. var startPos = text.lastIndexOf("\n", currentLine.start - 2) + 1;
  190. var endPos = currentLine.start;
  191. return {
  192. text: text.slice(startPos, endPos),
  193. start: startPos,
  194. end: endPos
  195. };
  196. };
  197. var handleTabKey = function(event) {
  198. event.preventDefault();
  199. var $target = $(event.target);
  200. var currentLine = getCurrentLine(event);
  201. var text = $target.val();
  202. var pos = $target.selection('getPos');
  203. // When the user presses CTRL + TAB, it is a case to control the tab of the browser
  204. // (for Firefox 54 on Windows)
  205. if (event.ctrlKey === true) {
  206. return;
  207. }
  208. if (currentLine) {
  209. $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
  210. }
  211. if (event.shiftKey === true) {
  212. if (currentLine && currentLine.text.charAt(0) === '|') {
  213. // prev cell in table
  214. var newPos = text.lastIndexOf('|', pos.start - 1);
  215. if (newPos > 0) {
  216. $target.selection('setPos', {start: newPos - 1, end: newPos - 1});
  217. }
  218. } else {
  219. // re indent
  220. var reindentedText = $target.selection().replace(/^ {1,4}/gm, '');
  221. var reindentedCount = $target.selection().length - reindentedText.length;
  222. $target.selection('replace', {text: reindentedText, mode: 'before'});
  223. if (currentLine) {
  224. $target.selection('setPos', {start: pos.start - reindentedCount, end: pos.start - reindentedCount});
  225. }
  226. }
  227. } else {
  228. if (currentLine && currentLine.text.charAt(0) === '|') {
  229. // next cell in table
  230. var newPos = text.indexOf('|', pos.start + 1);
  231. if (newPos < 0 || newPos === text.lastIndexOf('|', currentLine.end - 1)) {
  232. $target.selection('setPos', {start: currentLine.end, end: currentLine.end});
  233. } else {
  234. $target.selection('setPos', {start: newPos + 2, end: newPos + 2});
  235. }
  236. } else {
  237. // indent
  238. $target.selection('replace', {
  239. text: ' ' + $target.selection().split("\n").join("\n "),
  240. mode: 'before'
  241. });
  242. if (currentLine) {
  243. $target.selection('setPos', {start: pos.start + 4, end: pos.start + 4});
  244. }
  245. }
  246. }
  247. $target.trigger('input');
  248. };
  249. var handleEnterKey = function(event) {
  250. if (event.metaKey || event.ctrlKey || event.shiftKey) {
  251. return;
  252. }
  253. var currentLine = getCurrentLine(event);
  254. if (!currentLine || currentLine.start === currentLine.caret) {
  255. return;
  256. }
  257. var $target = $(event.target);
  258. var match = currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)\s*\S/);
  259. if (match) {
  260. // smart indent with list
  261. if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] ))\s*$/)) {
  262. // empty task list
  263. $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
  264. return;
  265. }
  266. event.preventDefault();
  267. var listMark = match[1].replace(/\[x\]/, '[ ]');
  268. var listMarkMatch = listMark.match(/^(\s*)(\d+)\./);
  269. if (listMarkMatch) {
  270. var indent = listMarkMatch[1];
  271. var num = parseInt(listMarkMatch[2]);
  272. if (num !== 1) {
  273. listMark = listMark.replace(/\s*\d+/, indent + (num +1));
  274. }
  275. }
  276. //$target.selection('insert', {text: "\n" + listMark, mode: 'before'});
  277. var pos = $target.selection('getPos');
  278. insertText(pos.start, pos.start, "\n" + listMark, 'replace');
  279. var newPosition = pos.start + ("\n" + listMark).length;
  280. $target.selection('setPos', {start: newPosition, end: newPosition});
  281. } else if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) )/)) {
  282. // remove list
  283. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  284. } else if (currentLine.text.match(/^.*\|\s*$/)) {
  285. // new row for table
  286. if (currentLine.text.match(/^[\|\s]+$/)) {
  287. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  288. return;
  289. }
  290. if (!currentLine.endOfLine) {
  291. return;
  292. }
  293. event.preventDefault();
  294. var row = [];
  295. var cellbarMatch = currentLine.text.match(/\|/g);
  296. for (var i = 0; i < cellbarMatch.length; i++) {
  297. row.push('|');
  298. }
  299. var prevLine = getPrevLine(event);
  300. if (!prevLine || (!currentLine.text.match(/---/) && !prevLine.text.match(/\|/g))) {
  301. //$target.selection('insert', {text: "\n" + row.join(' --- ') + "\n" + row.join(' '), mode: 'before'});
  302. var pos = $target.selection('getPos');
  303. insertText(pos.start, pos.start, "\n" + row.join(' --- ') + "\n" + row.join(' '), 'after');
  304. $target.selection('setPos', {start: currentLine.caret + 6 * row.length - 1, end: currentLine.caret + 6 * row.length - 1});
  305. } else {
  306. //$target.selection('insert', {text: "\n" + row.join(' '), mode: 'before'});
  307. var pos = $target.selection('getPos');
  308. insertText(pos.start, pos.end, "\n" + row.join(' '), 'after');
  309. $target.selection('setPos', {start: currentLine.caret + 3, end: currentLine.caret + 3});
  310. }
  311. }
  312. $target.trigger('input');
  313. };
  314. var handleEscapeKey = function(event) {
  315. event.preventDefault();
  316. var $target = $(event.target);
  317. $target.blur();
  318. };
  319. var handleSpaceKey = function(event) {
  320. // keybind: alt + shift + space
  321. if (!(event.shiftKey && event.altKey)) {
  322. return;
  323. }
  324. var currentLine = getCurrentLine(event);
  325. if (!currentLine) {
  326. return;
  327. }
  328. var $target = $(event.target);
  329. var match = currentLine.text.match(/^(\s*)(-|\+|\*|\d+\.) (?:\[(x| )\] )(.*)/);
  330. if (match) {
  331. event.preventDefault();
  332. var checkMark = (match[3] == ' ') ? 'x' : ' ';
  333. var replaceTo = match[1] + match[2] + ' [' + checkMark + '] ' + match[4];
  334. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  335. //$target.selection('replace', {text: replaceTo, mode: 'keep'});
  336. insertText(currentLine.start, currentLine.end, replaceTo, 'replace');
  337. $target.selection('setPos', {start: currentLine.caret, end: currentLine.caret});
  338. $target.trigger('input');
  339. }
  340. };
  341. var handleSKey = function(event) {
  342. if (!event.ctrlKey && !event.metaKey) {
  343. return;
  344. }
  345. event.preventDefault();
  346. const revisionInput = $('#page-form [name="pageForm[currentRevision]"]');
  347. // generate data to post
  348. const body = $('#form-body').val();
  349. let endpoint;
  350. let data;
  351. // update
  352. if (pageId) {
  353. endpoint = '/pages.update';
  354. data = {
  355. page_id: pageId,
  356. revision_id: revisionInput.val(),
  357. body: body,
  358. };
  359. }
  360. // create
  361. else {
  362. endpoint = '/pages.create';
  363. data = {
  364. path: pagePath,
  365. body: body,
  366. };
  367. }
  368. crowi.apiPost(endpoint, data)
  369. .then((res) => {
  370. let page = res.page;
  371. pageId = page._id
  372. toastr.success(undefined, 'Saved successfully', {
  373. closeButton: true,
  374. progressBar: true,
  375. newestOnTop: false,
  376. showDuration: "100",
  377. hideDuration: "100",
  378. timeOut: "1200",
  379. extendedTimeOut: "150",
  380. });
  381. // update currentRevision input
  382. revisionInput.val(page.revision._id);
  383. // TODO update $('#revision-body-content')
  384. })
  385. .catch((error) => {
  386. console.error(error);
  387. toastr.error(error.message, 'Error occured on saveing', {
  388. closeButton: true,
  389. progressBar: true,
  390. newestOnTop: false,
  391. showDuration: "100",
  392. hideDuration: "100",
  393. timeOut: "3000",
  394. });
  395. });
  396. }
  397. // markdown helper inspired by 'esarea'.
  398. // see: https://github.com/fukayatsu/esarea
  399. $('textarea#form-body').on('keydown', function(event) {
  400. switch (event.which || event.keyCode) {
  401. case 9:
  402. handleTabKey(event);
  403. break;
  404. case 13:
  405. handleEnterKey(event);
  406. break;
  407. case 27:
  408. handleEscapeKey(event);
  409. break;
  410. case 32:
  411. handleSpaceKey(event);
  412. break;
  413. case 83:
  414. handleSKey(event);
  415. break;
  416. default:
  417. }
  418. });
  419. var handlePasteEvent = function(event) {
  420. var currentLine = getCurrentLine(event);
  421. if (!currentLine) {
  422. return false;
  423. }
  424. var $target = $(event.target);
  425. var pasteText = event.clipboardData.getData('text');
  426. var match = currentLine.text.match(/^(\s*(?:>|\-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)/);
  427. if (match) {
  428. if (pasteText.match(/(?:\r\n|\r|\n)/)) {
  429. pasteText = pasteText.replace(/(\r\n|\r|\n)/g, "$1" + match[1]);
  430. }
  431. }
  432. //$target.selection('insert', {text: pasteText, mode: 'after'});
  433. insertText(currentLine.caret, currentLine.caret, pasteText, 'replace');
  434. var newPos = currentLine.caret + pasteText.length;
  435. $target.selection('setPos', {start: newPos, end: newPos});
  436. return true;
  437. };
  438. document.getElementById('form-body').addEventListener('paste', function(event) {
  439. if (handlePasteEvent(event)) {
  440. event.preventDefault();
  441. }
  442. });
  443. var unbindInlineAttachment = function($form) {
  444. $form.unbind('.inlineattach');
  445. };
  446. var bindInlineAttachment = function($form, attachmentOption) {
  447. var $this = $form;
  448. var editor = createEditorInstance($form);
  449. var inlineattach = new inlineAttachment(attachmentOption, editor);
  450. $form.bind({
  451. 'paste.inlineattach': function(e) {
  452. inlineattach.onPaste(e.originalEvent);
  453. },
  454. 'drop.inlineattach': function(e) {
  455. e.stopPropagation();
  456. e.preventDefault();
  457. inlineattach.onDrop(e.originalEvent);
  458. },
  459. 'dragenter.inlineattach dragover.inlineattach': function(e) {
  460. e.stopPropagation();
  461. e.preventDefault();
  462. }
  463. });
  464. };
  465. var createEditorInstance = function($form) {
  466. var $this = $form;
  467. return {
  468. getValue: function() {
  469. return $this.val();
  470. },
  471. insertValue: function(val) {
  472. inlineAttachment.util.insertTextAtCursor($this[0], val);
  473. },
  474. setValue: function(val) {
  475. $this.val(val);
  476. }
  477. };
  478. };
  479. var $inputForm = $('form.uploadable textarea#form-body');
  480. if ($inputForm.length > 0) {
  481. var csrfToken = $('form.uploadable input#edit-form-csrf').val();
  482. var pageId = $('#content-main').data('page-id') || 0;
  483. var attachmentOption = {
  484. uploadUrl: '/_api/attachments.add',
  485. extraParams: {
  486. path: location.pathname,
  487. page_id: pageId,
  488. _csrf: csrfToken
  489. },
  490. progressText: '(Uploading file...)',
  491. jsonFieldName: 'url',
  492. };
  493. // if files upload is set
  494. var config = crowi.getConfig();
  495. if (config.upload.file) {
  496. attachmentOption.allowedTypes = '*';
  497. }
  498. attachmentOption.remoteFilename = function(file) {
  499. return file.name;
  500. };
  501. attachmentOption.onFileReceived = function(file) {
  502. // if not image
  503. if (!file.type.match(/^image\/.+$/)) {
  504. // modify urlText with 'a' tag
  505. this.settings.urlText = `<a href="{filename}">${file.name}</a>\n`;
  506. this.settings.urlText = `[${file.name}]({filename})\n`;
  507. } else {
  508. this.settings.urlText = `![${file.name}]({filename})\n`;
  509. }
  510. }
  511. attachmentOption.onFileUploadResponse = function(res) {
  512. var result = JSON.parse(res.response);
  513. if (result.ok && result.pageCreated) {
  514. var page = result.page,
  515. pageId = page._id;
  516. $('#content-main').data('page-id', page._id);
  517. $('#page-form [name="pageForm[currentRevision]"]').val(page.revision._id)
  518. unbindInlineAttachment($inputForm);
  519. attachmentOption.extraParams.page_id = pageId;
  520. bindInlineAttachment($inputForm, attachmentOption);
  521. }
  522. return true;
  523. };
  524. bindInlineAttachment($inputForm, attachmentOption);
  525. $('textarea#form-body').on('dragenter dragover', function() {
  526. $(this).addClass('dragover');
  527. });
  528. $('textarea#form-body').on('drop dragleave dragend', function() {
  529. $(this).removeClass('dragover');
  530. });
  531. }
  532. var enableScrollSync = function() {
  533. var getMaxScrollTop = function(dom) {
  534. var rect = dom.getBoundingClientRect();
  535. return dom.scrollHeight - rect.height;
  536. };
  537. var getScrollRate = function(dom) {
  538. var maxScrollTop = getMaxScrollTop(dom);
  539. var rate = dom.scrollTop / maxScrollTop;
  540. return rate;
  541. };
  542. var getScrollTop = function(dom, rate) {
  543. var maxScrollTop = getMaxScrollTop(dom);
  544. var top = maxScrollTop * rate;
  545. return top;
  546. };
  547. var editor = document.querySelector('#form-body');
  548. var preview = document.querySelector('#preview-body');
  549. editor.addEventListener('scroll', function(event) {
  550. var rate = getScrollRate(this);
  551. var top = getScrollTop(preview, rate);
  552. preview.scrollTop = top;
  553. });
  554. };
  555. enableScrollSync();
  556. */
  557. });