crowi-form.js 13 KB

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