crowi-form.js 19 KB

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