mdeditor.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. ((function(){
  2. var editors = [];
  3. var toolbarIdentifiers = [ 'bold', 'italic', 'strike', 'link', 'image', 'blockquote', 'listUl', 'listOl' ];
  4. if (typeof window.customToolbarElements !== 'undefined') {
  5. window.customToolbarElements.forEach(function(customToolbarElement) {
  6. toolbarIdentifiers.push(customToolbarElement.identifier);
  7. });
  8. }
  9. var toolbarButtons = {
  10. fullscreen: {
  11. title : 'Fullscreen',
  12. label : '<i class="fa fa-fw fa-expand"></i>'
  13. },
  14. bold : {
  15. title : 'Bold',
  16. label : '<i class="fa fa-fw fa-bold"></i>'
  17. },
  18. italic : {
  19. title : 'Italic',
  20. label : '<i class="fa fa-fw fa-italic"></i>'
  21. },
  22. strike : {
  23. title : 'Strikethrough',
  24. label : '<i class="fa fa-fw fa-strikethrough"></i>'
  25. },
  26. blockquote : {
  27. title : 'Blockquote',
  28. label : '<i class="fa fa-fw fa-quote-right"></i>'
  29. },
  30. link : {
  31. title : 'Link',
  32. label : '<i class="fa fa-fw fa-link"></i>'
  33. },
  34. image : {
  35. title : 'Image',
  36. label : '<i class="fa fa-fw fa-picture-o"></i>'
  37. },
  38. listUl : {
  39. title : 'Unordered List',
  40. label : '<i class="fa fa-fw fa-list-ul"></i>'
  41. },
  42. listOl : {
  43. title : 'Ordered List',
  44. label : '<i class="fa fa-fw fa-list-ol"></i>'
  45. }
  46. };
  47. if (typeof window.customToolbarElements !== 'undefined') {
  48. window.customToolbarElements.forEach(function(customToolbarElement) {
  49. toolbarButtons[customToolbarElement.identifier] = customToolbarElement.button;
  50. });
  51. }
  52. var debounce = function(func, wait, immediate) {
  53. var timeout;
  54. return function() {
  55. var context = this, args = arguments;
  56. var later = function() {
  57. timeout = null;
  58. if (!immediate) func.apply(context, args);
  59. };
  60. var callNow = immediate && !timeout;
  61. clearTimeout(timeout);
  62. timeout = setTimeout(later, wait);
  63. if (callNow) func.apply(context, args);
  64. };
  65. };
  66. var template = [
  67. '<div class="grav-mdeditor clearfix" data-mode="tab" data-active-tab="code">',
  68. '<div class="grav-mdeditor-navbar">',
  69. '<ul class="grav-mdeditor-navbar-nav grav-mdeditor-toolbar"></ul>',
  70. '<div class="grav-mdeditor-navbar-flip">',
  71. '<ul class="grav-mdeditor-navbar-nav">',
  72. '<li class="grav-mdeditor-button-code mdeditor-active"><a>{:lblCodeview}</a></li>',
  73. '<li class="grav-mdeditor-button-preview"><a>{:lblPreview}</a></li>',
  74. '<li><a data-mdeditor-button="fullscreen"><i class="fa fa-fw fa-expand"></i></a></li>',
  75. '</ul>',
  76. '</div>',
  77. '<p class="grav-mdeditor-preview-text" style="display: none;">Preview</p>',
  78. '</div>',
  79. '<div class="grav-mdeditor-content">',
  80. '<div class="grav-mdeditor-code"></div>',
  81. '<div class="grav-mdeditor-preview"><div></div></div>',
  82. '</div>',
  83. '</div>'
  84. ].join('');
  85. var MDEditor = function(editor, options){
  86. var tpl = template, $this = this,
  87. task = 'task' + GravAdmin.config.param_sep;
  88. this.defaults = {
  89. markdown : false,
  90. autocomplete : true,
  91. height : 500,
  92. codemirror : { mode: 'htmlmixed', theme: 'paper', lineWrapping: true, dragDrop: true, autoCloseTags: true, matchTags: true, autoCloseBrackets: true, matchBrackets: true, indentUnit: 4, indentWithTabs: false, tabSize: 4, hintOptions: {completionSingle:false}, extraKeys: {"Enter": "newlineAndIndentContinueMarkdownList"} },
  93. toolbar : toolbarIdentifiers,
  94. lblPreview : '<i class="fa fa-fw fa-eye"></i>',
  95. lblCodeview : '<i class="fa fa-fw fa-code"></i>',
  96. lblMarkedview: '<i class="fa fa-fw fa-code"></i>'
  97. }
  98. this.element = $(editor);
  99. this.options = $.extend({}, this.defaults, options);
  100. this.CodeMirror = CodeMirror;
  101. this.buttons = {};
  102. tpl = tpl.replace(/\{:lblPreview\}/g, this.options.lblPreview);
  103. tpl = tpl.replace(/\{:lblCodeview\}/g, this.options.lblCodeview);
  104. this.mdeditor = $(tpl);
  105. this.content = this.mdeditor.find('.grav-mdeditor-content');
  106. this.toolbar = this.mdeditor.find('.grav-mdeditor-toolbar');
  107. this.preview = this.mdeditor.find('.grav-mdeditor-preview').children().eq(0);
  108. this.code = this.mdeditor.find('.grav-mdeditor-code');
  109. this.element.before(this.mdeditor).appendTo(this.code);
  110. this.editor = this.CodeMirror.fromTextArea(this.element[0], this.options.codemirror);
  111. this.editor.mdeditor = this;
  112. if (this.options.markdown) {
  113. this.editor.setOption('mode', 'gfm');
  114. }
  115. this.editor.on('change', debounce(function() { $this.render(); }, 150));
  116. this.editor.on('change', function() { $this.editor.save(); });
  117. this.code.find('.CodeMirror').css('height', this.options.height);
  118. var editor = this.editor;
  119. $("#gravDropzone").delegate('[data-dz-insert]', 'click', function(e) {
  120. var target = $(e.currentTarget).parent('.dz-preview').find('.dz-filename');
  121. editor.focus();
  122. var filename = encodeURI(target.text());
  123. filename = filename.replace(/@3x|@2x|@1x/, '');
  124. filename = filename.replace(/\(/g, '%28');
  125. filename = filename.replace(/\)/g, '%29');
  126. if (filename.match(/\.(jpg|jpeg|png|gif)$/)) {
  127. editor.doc.replaceSelection('![](' + filename + ')');
  128. } else {
  129. editor.doc.replaceSelection('[' + decodeURI(filename) + '](' + filename + ')');
  130. }
  131. });
  132. this.preview.container = this.preview;
  133. this.mdeditor.on('click', '.grav-mdeditor-button-code, .grav-mdeditor-button-preview', function(e) {
  134. var task = 'task' + GravAdmin.config.param_sep;
  135. e.preventDefault();
  136. if ($this.mdeditor.attr('data-mode') == 'tab') {
  137. if ($(this).hasClass('grav-mdeditor-button-preview')) {
  138. GravAjax({
  139. dataType: 'JSON',
  140. url: $this.element.data('grav-urlpreview') + '/' + task + 'processmarkdown',
  141. method: 'post',
  142. data: $this.element.parents('form').serialize(),
  143. toastErrors: true,
  144. success: function (response) {
  145. $this.preview.container.html(response.message);
  146. }
  147. });
  148. }
  149. $this.mdeditor.find('.grav-mdeditor-button-code, .grav-mdeditor-button-preview').removeClass('mdeditor-active').filter(this).addClass('mdeditor-active');
  150. $this.activetab = $(this).hasClass('grav-mdeditor-button-code') ? 'code' : 'preview';
  151. $this.mdeditor.attr('data-active-tab', $this.activetab);
  152. $this.editor.refresh();
  153. if ($this.activetab == 'preview') {
  154. $('.grav-mdeditor-toolbar').fadeOut();
  155. setTimeout(function() {
  156. $('.grav-mdeditor-preview-text').fadeIn();
  157. }, 500);
  158. } else {
  159. $('.grav-mdeditor-preview-text').fadeOut();
  160. setTimeout(function() {
  161. $('.grav-mdeditor-toolbar').fadeIn();
  162. }, 500);
  163. }
  164. }
  165. });
  166. this.mdeditor.on('click', 'a[data-mdeditor-button]', function() {
  167. if (!$this.code.is(':visible')) return;
  168. $this.element.trigger('action.' + $(this).data('mdeditor-button'), [$this.editor]);
  169. });
  170. this.preview.parent().css('height', this.code.height());
  171. // autocomplete
  172. if (this.options.autocomplete && this.CodeMirror.showHint && this.CodeMirror.hint && this.CodeMirror.hint.html) {
  173. this.editor.on('inputRead', debounce(function() {
  174. var doc = $this.editor.getDoc(), POS = doc.getCursor(), mode = $this.CodeMirror.innerMode($this.editor.getMode(), $this.editor.getTokenAt(POS).state).mode.name;
  175. if (mode == 'xml') { //html depends on xml
  176. var cur = $this.editor.getCursor(), token = $this.editor.getTokenAt(cur);
  177. if (token.string.charAt(0) == '<' || token.type == 'attribute') {
  178. $this.CodeMirror.showHint($this.editor, $this.CodeMirror.hint.html, { completeSingle: false });
  179. }
  180. }
  181. }, 100));
  182. }
  183. this.debouncedRedraw = debounce(function () { $this.redraw(); }, 5);
  184. /*this.element.attr('data-grav-check-display', 1).on('grav-check-display', function(e) {
  185. if($this.mdeditor.is(":visible")) $this.fit();
  186. });*/
  187. editors.push(this);
  188. // Methods
  189. this.addButton = function(name, button) {
  190. this.buttons[name] = button;
  191. };
  192. this.addButtons = function(buttons) {
  193. $.extend(this.buttons, buttons);
  194. };
  195. this._buildtoolbar = function() {
  196. if (!(this.options.toolbar && this.options.toolbar.length)) return;
  197. var $this = this, bar = [];
  198. this.toolbar.empty();
  199. this.options.toolbar.forEach(function(button) {
  200. if (!$this.buttons[button]) return;
  201. var title = $this.buttons[button].title ? $this.buttons[button].title : button;
  202. var buttonClass = $this.buttons[button].class ? 'class="' + $this.buttons[button].class + '"' : '';
  203. bar.push('<li><a data-mdeditor-button="'+button+'" title="'+title+'" '+buttonClass+' data-uk-tooltip>'+$this.buttons[button].label+'</a></li>');
  204. });
  205. this.toolbar.html(bar.join('\n'));
  206. };
  207. this.fit = function() {
  208. var mode = this.options.mode;
  209. if (mode == 'split' && this.mdeditor.width() < this.options.maxsplitsize) {
  210. mode = 'tab';
  211. }
  212. if (mode == 'tab') {
  213. if (!this.activetab) {
  214. this.activetab = 'code';
  215. this.mdeditor.attr('data-active-tab', this.activetab);
  216. }
  217. this.mdeditor.find('.grav-mdeditor-button-code, .grav-mdeditor-button-preview').removeClass('uk-active')
  218. .filter(this.activetab == 'code' ? '.grav-mdeditor-button-code' : '.grav-mdeditor-button-preview')
  219. .addClass('uk-active');
  220. }
  221. this.editor.refresh();
  222. this.preview.parent().css('height', this.code.height());
  223. this.mdeditor.attr('data-mode', mode);
  224. };
  225. this.redraw = function() {
  226. this._buildtoolbar();
  227. this.render();
  228. this.fit();
  229. };
  230. this.getMode = function() {
  231. return this.editor.getOption('mode');
  232. };
  233. this.getCursorMode = function() {
  234. var param = { mode: 'html'};
  235. this.element.trigger('cursorMode', [param]);
  236. return param.mode;
  237. };
  238. this.render = function() {
  239. this.currentvalue = this.editor.getValue().replace(/^---([\s\S]*?)---\n{1,}/g, '');
  240. // empty code
  241. if (!this.currentvalue) {
  242. this.element.val('');
  243. this.preview.container.html('');
  244. return;
  245. }
  246. this.element.trigger('render', [this]);
  247. this.element.trigger('renderLate', [this]);
  248. this.preview.container.html(this.currentvalue);
  249. };
  250. this.addShortcut = function(name, callback) {
  251. var map = {};
  252. if (!$.isArray(name)) {
  253. name = [name];
  254. }
  255. name.forEach(function(key) {
  256. map[key] = callback;
  257. });
  258. this.editor.addKeyMap(map);
  259. return map;
  260. };
  261. this.addShortcutAction = function(action, shortcuts) {
  262. var editor = this;
  263. this.addShortcut(shortcuts, function() {
  264. editor.element.trigger('action.' + action, [editor.editor]);
  265. });
  266. };
  267. this.replaceSelection = function(replace, action) {
  268. var text = this.editor.getSelection(),
  269. indexOf = -1,
  270. cur = this.editor.getCursor(),
  271. curLine = this.editor.getLine(cur.line),
  272. start = cur.ch,
  273. end = start;
  274. if (!text.length) {
  275. while (end < curLine.length && /[\w$]+/.test(curLine.charAt(end))) ++end;
  276. while (start && /[\w$]+/.test(curLine.charAt(start - 1))) --start;
  277. var curWord = start != end && curLine.slice(start, end);
  278. if (curWord) {
  279. this.editor.setSelection({ line: cur.line, ch: start}, { line: cur.line, ch: end });
  280. text = curWord;
  281. } else {
  282. indexOf = replace.indexOf('$1');
  283. }
  284. }
  285. var html = replace.replace('$1', text);
  286. this.editor.replaceSelection(html, 'end');
  287. if (indexOf !== -1) {
  288. this.editor.setCursor({ line: cur.line, ch: start + indexOf });
  289. } else {
  290. if (action == 'link' || action == 'image') {
  291. this.editor.setCursor({ line: cur.line, ch: html.length -1 });
  292. }
  293. }
  294. this.editor.focus();
  295. };
  296. this.replaceLine = function(replace, action) {
  297. var pos = this.editor.getDoc().getCursor(),
  298. text = this.editor.getLine(pos.line),
  299. html = replace.replace('$1', text);
  300. this.editor.replaceRange(html , { line: pos.line, ch: 0 }, { line: pos.line, ch: text.length });
  301. this.editor.setCursor({ line: pos.line, ch: html.length });
  302. this.editor.focus();
  303. };
  304. this.save = function() {
  305. this.editor.save();
  306. };
  307. this._initToolbar = function(editor) {
  308. editor.addButtons(toolbarButtons);
  309. addAction('bold', '**$1**');
  310. addAction('italic', '_$1_');
  311. addAction('strike', '~~$1~~');
  312. addAction('blockquote', '> $1', 'replaceLine');
  313. addAction('link', '[$1](http://)');
  314. addAction('image', '![$1](http://)');
  315. editor.element.on('action.listUl', function() {
  316. if (editor.getCursorMode() == 'markdown') {
  317. var cm = editor.editor,
  318. pos = cm.getDoc().getCursor(true),
  319. posend = cm.getDoc().getCursor(false);
  320. for (var i=pos.line; i<(posend.line+1);i++) {
  321. cm.replaceRange('* '+cm.getLine(i), { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
  322. }
  323. cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
  324. cm.focus();
  325. }
  326. });
  327. editor.element.on('action.listOl', function() {
  328. if (editor.getCursorMode() == 'markdown') {
  329. var cm = editor.editor,
  330. pos = cm.getDoc().getCursor(true),
  331. posend = cm.getDoc().getCursor(false),
  332. prefix = 1;
  333. if (pos.line > 0) {
  334. var prevline = cm.getLine(pos.line-1), matches;
  335. if(matches = prevline.match(/^(\d+)\./)) {
  336. prefix = Number(matches[1])+1;
  337. }
  338. }
  339. for (var i=pos.line; i<(posend.line+1);i++) {
  340. cm.replaceRange(prefix+'. '+cm.getLine(i), { line: i, ch: 0 }, { line: i, ch: cm.getLine(i).length });
  341. prefix++;
  342. }
  343. cm.setCursor({ line: posend.line, ch: cm.getLine(posend.line).length });
  344. cm.focus();
  345. }
  346. });
  347. if (typeof window.customToolbarElements !== 'undefined') {
  348. window.customToolbarElements.forEach(function(customToolbarElement) {
  349. editor.element.on('action.' + customToolbarElement.identifier, function() {
  350. if (editor.getCursorMode() == 'markdown') {
  351. customToolbarElement.processAction(editor);
  352. }
  353. });
  354. });
  355. }
  356. editor.element.on('cursorMode', function(e, param) {
  357. if (editor.editor.options.mode == 'gfm') {
  358. var pos = editor.editor.getDoc().getCursor();
  359. if (!editor.editor.getTokenAt(pos).state.base.htmlState) {
  360. param.mode = 'markdown';
  361. }
  362. }
  363. });
  364. $.extend(editor, {
  365. enableMarkdown: function() {
  366. enableMarkdown()
  367. this.render();
  368. },
  369. disableMarkdown: function() {
  370. this.editor.setOption('mode', 'htmlmixed');
  371. this.mdeditor.find('.grav-mdeditor-button-code a').html(this.options.lblCodeview);
  372. this.render();
  373. }
  374. });
  375. // switch markdown mode on event
  376. editor.element.on({
  377. enableMarkdown : function() { editor.enableMarkdown(); },
  378. disableMarkdown : function() { editor.disableMarkdown(); }
  379. });
  380. function enableMarkdown() {
  381. editor.editor.setOption('mode', 'gfm');
  382. editor.mdeditor.find('.grav-mdeditor-button-code a').html(editor.options.lblMarkedview);
  383. }
  384. editor.mdeditor.on('click', 'a[data-mdeditor-button="fullscreen"]', function() {
  385. editor.mdeditor.toggleClass('grav-mdeditor-fullscreen');
  386. var wrap = editor.editor.getWrapperElement();
  387. if (editor.mdeditor.hasClass('grav-mdeditor-fullscreen')) {
  388. editor.editor.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, width: wrap.style.width, height: wrap.style.height};
  389. wrap.style.width = '';
  390. wrap.style.height = editor.content.height()+'px';
  391. document.documentElement.style.overflow = 'hidden';
  392. } else {
  393. document.documentElement.style.overflow = '';
  394. var info = editor.editor.state.fullScreenRestore;
  395. wrap.style.width = info.width; wrap.style.height = info.height;
  396. window.scrollTo(info.scrollLeft, info.scrollTop);
  397. }
  398. setTimeout(function() {
  399. editor.fit();
  400. $(window).trigger('resize');
  401. }, 50);
  402. });
  403. editor.addShortcut(['Ctrl-S', 'Cmd-S'], function() { editor.element.trigger('mdeditor-save', [editor]); });
  404. editor.addShortcutAction('bold', ['Ctrl-B', 'Cmd-B']);
  405. editor.addShortcutAction('italic', ['Ctrl-I', 'Cmd-I']);
  406. function addAction(name, replace, mode) {
  407. editor.element.on('action.'+name, function() {
  408. if (editor.getCursorMode() == 'markdown') {
  409. editor[mode == 'replaceLine' ? 'replaceLine' : 'replaceSelection'](replace, name);
  410. }
  411. });
  412. }
  413. }
  414. // toolbar actions
  415. this._initToolbar($this);
  416. this._buildtoolbar();
  417. }
  418. // init
  419. $(function(){
  420. $('textarea[data-grav-mdeditor]').each(function() {
  421. var editor = $(this), obj;
  422. if (!editor.data('mdeditor')) {
  423. obj = MDEditor(editor, JSON.parse(editor.attr('data-grav-mdeditor') || '{}'));
  424. }
  425. });
  426. })
  427. })());