crowi-form.js 18 KB

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