crowi-form.js 18 KB

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