crowi-form.js 19 KB

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