crowi-form.js 18 KB

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