crowi-form.js 13 KB

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