crowi-form.js 17 KB

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