crowi-form.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. // restore draft
  17. var draft = crowi.findDraft(pagePath);
  18. if (draft) {
  19. $('#form-body').val(draft);
  20. }
  21. var slackConfigured = $('#page-form-setting').data('slack-configured');
  22. // for new page
  23. if (!pageId) {
  24. 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})/)) {
  25. $('#page-warning-modal').modal('show');
  26. }
  27. if (slackConfigured) {
  28. FetchPagesUpdatePostAndInsert(pagePath);
  29. }
  30. }
  31. $('a[data-toggle="tab"][href="#edit-form"]').on('show.bs.tab', function() {
  32. $('.content-main').addClass('on-edit');
  33. if (slackConfigured) {
  34. var $slackChannels = $('#page-form-slack-channel');
  35. var slackChannels = $slackChannels.val();
  36. // if slackChannels is empty, then fetch default (admin setting)
  37. // if not empty, it means someone specified this setting for the page.
  38. if (slackChannels === '') {
  39. FetchPagesUpdatePostAndInsert(pagePath);
  40. }
  41. }
  42. });
  43. $('a[data-toggle="tab"][href="#edit-form"]').on('hide.bs.tab', function() {
  44. $('.content-main').removeClass('on-edit');
  45. });
  46. // preview watch
  47. var originalContent = $('#form-body').val();
  48. var prevContent = "";
  49. var watchTimer = setInterval(function() {
  50. var content = $('#form-body').val();
  51. if (prevContent != content) {
  52. var parsedHTML = crowiRenderer.render(content);
  53. $('#preview-body').html(parsedHTML);
  54. crowi.saveDraft(pagePath, content);
  55. prevContent = content;
  56. }
  57. }, 500);
  58. // edit detection
  59. var isFormChanged = false;
  60. $(window).on('beforeunload', function(e) {
  61. if (isFormChanged) {
  62. return '編集中の内容があります。内容を破棄してページを移動しますか?';
  63. }
  64. });
  65. $('#form-body').on('keyup change', function(e) {
  66. var content = $('#form-body').val();
  67. if (originalContent != content) {
  68. isFormChanged = true;
  69. } else {
  70. isFormChanged = false;
  71. }
  72. });
  73. $('#page-form').on('submit', function(e) {
  74. // avoid message
  75. isFormChanged = false;
  76. });
  77. var getCurrentLine = function(event) {
  78. var $target = $(event.target);
  79. var text = $target.val();
  80. var pos = $target.selection('getPos');
  81. if (text === null || pos.start !== pos.end) {
  82. return null;
  83. }
  84. var startPos = text.lastIndexOf("\n", pos.start - 1) + 1;
  85. var endPos = text.indexOf("\n", pos.start);
  86. if (endPos === -1) {
  87. endPos = text.length;
  88. }
  89. return {
  90. text: text.slice(startPos, endPos),
  91. start: startPos,
  92. end: endPos,
  93. caret: pos.start,
  94. endOfLine: !$.trim(text.slice(pos.start, endPos))
  95. };
  96. };
  97. var getPrevLine = function(event) {
  98. var $target = $(event.target);
  99. var currentLine = getCurrentLine(event);
  100. var text = $target.val().slice(0, currentLine.start);
  101. var startPos = text.lastIndexOf("\n", currentLine.start - 2) + 1;
  102. var endPos = currentLine.start;
  103. return {
  104. text: text.slice(startPos, endPos),
  105. start: startPos,
  106. end: endPos
  107. };
  108. };
  109. var handleTabKey = function(event) {
  110. event.preventDefault();
  111. var $target = $(event.target);
  112. var currentLine = getCurrentLine(event);
  113. var text = $target.val();
  114. var pos = $target.selection('getPos');
  115. if (currentLine) {
  116. $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
  117. }
  118. if (event.shiftKey === true) {
  119. if (currentLine && currentLine.text.charAt(0) === '|') {
  120. // prev cell in table
  121. var newPos = text.lastIndexOf('|', pos.start - 1);
  122. if (newPos > 0) {
  123. $target.selection('setPos', {start: newPos - 1, end: newPos - 1});
  124. }
  125. } else {
  126. // re indent
  127. var reindentedText = $target.selection().replace(/^ {1,4}/gm, '');
  128. var reindentedCount = $target.selection().length - reindentedText.length;
  129. $target.selection('replace', {text: reindentedText, mode: 'before'});
  130. if (currentLine) {
  131. $target.selection('setPos', {start: pos.start - reindentedCount, end: pos.start - reindentedCount});
  132. }
  133. }
  134. } else {
  135. if (currentLine && currentLine.text.charAt(0) === '|') {
  136. // next cell in table
  137. var newPos = text.indexOf('|', pos.start + 1);
  138. if (newPos < 0 || newPos === text.lastIndexOf('|', currentLine.end - 1)) {
  139. $target.selection('setPos', {start: currentLine.end, end: currentLine.end});
  140. } else {
  141. $target.selection('setPos', {start: newPos + 2, end: newPos + 2});
  142. }
  143. } else {
  144. // indent
  145. $target.selection('replace', {
  146. text: ' ' + $target.selection().split("\n").join("\n "),
  147. mode: 'before'
  148. });
  149. if (currentLine) {
  150. $target.selection('setPos', {start: pos.start + 4, end: pos.start + 4});
  151. }
  152. }
  153. }
  154. $target.trigger('input');
  155. };
  156. var handleEnterKey = function(event) {
  157. if (event.metaKey || event.ctrlKey || event.shiftKey) {
  158. return;
  159. }
  160. var currentLine = getCurrentLine(event);
  161. if (!currentLine || currentLine.start === currentLine.caret) {
  162. return;
  163. }
  164. var $target = $(event.target);
  165. var match = currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)\s*\S/);
  166. if (match) {
  167. // smart indent with list
  168. if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) (?:\[(?:x| )\] ))\s*$/)) {
  169. // empty task list
  170. $target.selection('setPos', {start: currentLine.start, end: (currentLine.end - 1)});
  171. return;
  172. }
  173. event.preventDefault();
  174. var listMark = match[1].replace(/\[x\]/, '[ ]');
  175. var listMarkMatch = listMark.match(/^(\s*)(\d+)\./);
  176. if (listMarkMatch) {
  177. var indent = listMarkMatch[1];
  178. var num = parseInt(listMarkMatch[2]);
  179. if (num !== 1) {
  180. listMark = listMark.replace(/\s*\d+/, indent + (num +1));
  181. }
  182. }
  183. $target.selection('insert', {text: "\n" + listMark, mode: 'before'});
  184. } else if (currentLine.text.match(/^(\s*(?:-|\+|\*|\d+\.) )/)) {
  185. // remove list
  186. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  187. } else if (currentLine.text.match(/^.*\|\s*$/)) {
  188. // new row for table
  189. if (currentLine.text.match(/^[\|\s]+$/)) {
  190. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  191. return;
  192. }
  193. if (!currentLine.endOfLine) {
  194. return;
  195. }
  196. event.preventDefault();
  197. var row = [];
  198. var cellbarMatch = currentLine.text.match(/\|/g);
  199. for (var i = 0; i < cellbarMatch.length; i++) {
  200. row.push('|');
  201. }
  202. var prevLine = getPrevLine(event);
  203. if (!prevLine || (!currentLine.text.match(/---/) && !prevLine.text.match(/\|/g))) {
  204. $target.selection('insert', {text: "\n" + row.join(' --- ') + "\n" + row.join(' '), mode: 'before'});
  205. $target.selection('setPos', {start: currentLine.caret + 6 * row.length - 1, end: currentLine.caret + 6 * row.length - 1});
  206. } else {
  207. $target.selection('insert', {text: "\n" + row.join(' '), mode: 'before'});
  208. $target.selection('setPos', {start: currentLine.caret + 3, end: currentLine.caret + 3});
  209. }
  210. }
  211. $target.trigger('input');
  212. };
  213. var handleEscapeKey = function(event) {
  214. event.preventDefault();
  215. var $target = $(event.target);
  216. $target.blur();
  217. };
  218. var handleSpaceKey = function(event) {
  219. // keybind: alt + shift + space
  220. if (!(event.shiftKey && event.altKey)) {
  221. return;
  222. }
  223. var currentLine = getCurrentLine(event);
  224. if (!currentLine) {
  225. return;
  226. }
  227. var $target = $(event.target);
  228. var match = currentLine.text.match(/^(\s*)(-|\+|\*|\d+\.) (?:\[(x| )\] )(.*)/);
  229. if (match) {
  230. event.preventDefault();
  231. var checkMark = (match[3] == ' ') ? 'x' : ' ';
  232. var replaceTo = match[1] + match[2] + ' [' + checkMark + '] ' + match[4];
  233. $target.selection('setPos', {start: currentLine.start, end: currentLine.end});
  234. $target.selection('replace', {text: replaceTo, mode: 'keep'});
  235. $target.selection('setPos', {start: currentLine.caret, end: currentLine.caret});
  236. $target.trigger('input');
  237. }
  238. };
  239. // markdown helper inspired by 'esarea'.
  240. // see: https://github.com/fukayatsu/esarea
  241. $('textarea#form-body').on('keydown', function(event) {
  242. switch (event.which || event.keyCode) {
  243. case 9:
  244. handleTabKey(event);
  245. break;
  246. case 13:
  247. handleEnterKey(event);
  248. break;
  249. case 27:
  250. handleEscapeKey(event);
  251. break;
  252. case 32:
  253. handleSpaceKey(event);
  254. break;
  255. default:
  256. }
  257. });
  258. var handlePasteEvent = function(event) {
  259. var currentLine = getCurrentLine(event);
  260. if (!currentLine) {
  261. return false;
  262. }
  263. var $target = $(event.target);
  264. var pasteText = event.clipboardData.getData('text');
  265. var match = currentLine.text.match(/^(\s*(?:>|\-|\+|\*|\d+\.) (?:\[(?:x| )\] )?)/);
  266. if (match) {
  267. if (pasteText.match(/(?:\r\n|\r|\n)/)) {
  268. pasteText = pasteText.replace(/(\r\n|\r|\n)/g, "$1" + match[1]);
  269. }
  270. }
  271. $target.selection('insert', {text: pasteText, mode: 'after'});
  272. var newPos = currentLine.end + pasteText.length;
  273. $target.selection('setPos', {start: newPos, end: newPos});
  274. return true;
  275. };
  276. document.getElementById('form-body').addEventListener('paste', function(event) {
  277. if (handlePasteEvent(event)) {
  278. event.preventDefault();
  279. }
  280. });
  281. var unbindInlineAttachment = function($form) {
  282. $form.unbind('.inlineattach');
  283. };
  284. var bindInlineAttachment = function($form, attachmentOption) {
  285. var $this = $form;
  286. var editor = createEditorInstance($form);
  287. var inlineattach = new inlineAttachment(attachmentOption, editor);
  288. $form.bind({
  289. 'paste.inlineattach': function(e) {
  290. inlineattach.onPaste(e.originalEvent);
  291. },
  292. 'drop.inlineattach': function(e) {
  293. e.stopPropagation();
  294. e.preventDefault();
  295. inlineattach.onDrop(e.originalEvent);
  296. },
  297. 'dragenter.inlineattach dragover.inlineattach': function(e) {
  298. e.stopPropagation();
  299. e.preventDefault();
  300. }
  301. });
  302. };
  303. var createEditorInstance = function($form) {
  304. var $this = $form;
  305. return {
  306. getValue: function() {
  307. return $this.val();
  308. },
  309. insertValue: function(val) {
  310. inlineAttachment.util.insertTextAtCursor($this[0], val);
  311. },
  312. setValue: function(val) {
  313. $this.val(val);
  314. }
  315. };
  316. };
  317. var $inputForm = $('form.uploadable textarea#form-body');
  318. if ($inputForm.length > 0) {
  319. var pageId = $('#content-main').data('page-id') || 0;
  320. var attachmentOption = {
  321. uploadUrl: '/_api/attachment/page/' + pageId,
  322. extraParams: {
  323. path: location.pathname
  324. },
  325. progressText: '(Uploading file...)',
  326. urlText: "\n![file]({filename})\n"
  327. };
  328. attachmentOption.onFileUploadResponse = function(res) {
  329. var result = JSON.parse(res.response);
  330. if (result.status && result.pageCreated) {
  331. var page = result.page,
  332. pageId = page._id;
  333. $('#content-main').data('page-id', page._id);
  334. $('#page-form [name="pageForm[currentRevision]"]').val(page.revision._id)
  335. unbindInlineAttachment($inputForm);
  336. attachmentOption.uploadUrl = '/_api/attachment/page/' + pageId,
  337. bindInlineAttachment($inputForm, attachmentOption);
  338. }
  339. return true;
  340. };
  341. bindInlineAttachment($inputForm, attachmentOption);
  342. $('textarea#form-body').on('dragenter dragover', function() {
  343. $(this).addClass('dragover');
  344. });
  345. $('textarea#form-body').on('drop dragleave dragend', function() {
  346. $(this).removeClass('dragover');
  347. });
  348. }
  349. var enableScrollSync = function() {
  350. var getMaxScrollTop = function(dom) {
  351. var rect = dom.getBoundingClientRect();
  352. return dom.scrollHeight - rect.height;
  353. };
  354. var getScrollRate = function(dom) {
  355. var maxScrollTop = getMaxScrollTop(dom);
  356. var rate = dom.scrollTop / maxScrollTop;
  357. return rate;
  358. };
  359. var getScrollTop = function(dom, rate) {
  360. var maxScrollTop = getMaxScrollTop(dom);
  361. var top = maxScrollTop * rate;
  362. return top;
  363. };
  364. var editor = document.querySelector('#form-body');
  365. var preview = document.querySelector('#preview-body');
  366. editor.addEventListener('scroll', function(event) {
  367. var rate = getScrollRate(this);
  368. var top = getScrollTop(preview, rate);
  369. preview.scrollTop = top;
  370. });
  371. };
  372. enableScrollSync();
  373. });