crowi-form.js 16 KB

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