crowi-form.js 18 KB

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