/*global require,module*/ 'use strict'; var CodeMirror = require('codemirror'); require('codemirror/addon/edit/continuelist.js'); require('./codemirror/tablist'); require('codemirror/addon/display/fullscreen.js'); require('codemirror/mode/markdown/markdown.js'); require('codemirror/addon/mode/overlay.js'); require('codemirror/addon/display/placeholder.js'); require('codemirror/addon/selection/mark-selection.js'); require('codemirror/addon/search/searchcursor.js'); require('codemirror/mode/gfm/gfm.js'); require('codemirror/mode/xml/xml.js'); var CodeMirrorSpellChecker = require('codemirror-spell-checker'); var marked = require('marked'); // Some variables var isMac = /Mac/.test(navigator.platform); var anchorToExternalRegex = new RegExp(/()+?/g); // Mapping of actions that can be bound to keyboard shortcuts or toolbar buttons var bindings = { 'toggleBold': toggleBold, 'toggleItalic': toggleItalic, 'drawLink': drawLink, 'toggleHeadingSmaller': toggleHeadingSmaller, 'toggleHeadingBigger': toggleHeadingBigger, 'drawImage': drawImage, 'toggleBlockquote': toggleBlockquote, 'toggleOrderedList': toggleOrderedList, 'toggleUnorderedList': toggleUnorderedList, 'toggleCodeBlock': toggleCodeBlock, 'togglePreview': togglePreview, 'toggleStrikethrough': toggleStrikethrough, 'toggleHeading1': toggleHeading1, 'toggleHeading2': toggleHeading2, 'toggleHeading3': toggleHeading3, 'cleanBlock': cleanBlock, 'drawTable': drawTable, 'drawHorizontalRule': drawHorizontalRule, 'undo': undo, 'redo': redo, 'toggleSideBySide': toggleSideBySide, 'toggleFullScreen': toggleFullScreen, }; var shortcuts = { 'toggleBold': 'Cmd-B', 'toggleItalic': 'Cmd-I', 'drawLink': 'Cmd-K', 'toggleHeadingSmaller': 'Cmd-H', 'toggleHeadingBigger': 'Shift-Cmd-H', 'cleanBlock': 'Cmd-E', 'drawImage': 'Cmd-Alt-I', 'toggleBlockquote': 'Cmd-\'', 'toggleOrderedList': 'Cmd-Alt-L', 'toggleUnorderedList': 'Cmd-L', 'toggleCodeBlock': 'Cmd-Alt-C', 'togglePreview': 'Cmd-P', 'toggleSideBySide': 'F9', 'toggleFullScreen': 'F11', }; var getBindingName = function (f) { for (var key in bindings) { if (bindings[key] === f) { return key; } } return null; }; var isMobile = function () { var check = false; (function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(a.substr(0, 4))) check = true; })(navigator.userAgent || navigator.vendor || window.opera); return check; }; /** * Modify HTML to add 'target="_blank"' to links so they open in new tabs by default. * @param {string} htmlText - HTML to be modified. * @return {string} The modified HTML text. */ function addAnchorTargetBlank(htmlText) { var match; while ((match = anchorToExternalRegex.exec(htmlText)) !== null) { // With only one capture group in the RegExp, we can safely take the first index from the match. var linkString = match[0]; if (linkString.indexOf('target=') === -1) { var fixedLinkString = linkString.replace(/>$/, ' target="_blank">'); htmlText = htmlText.replace(linkString, fixedLinkString); } } return htmlText; } /** * Fix shortcut. Mac use Command, others use Ctrl. */ function fixShortcut(name) { if (isMac) { name = name.replace('Ctrl', 'Cmd'); } else { name = name.replace('Cmd', 'Ctrl'); } return name; } /** * Create icon element for toolbar. */ function createIcon(options, enableTooltips, shortcuts) { options = options || {}; var el = document.createElement('button'); el.className = options.name; el.setAttribute('type', 'button'); enableTooltips = (enableTooltips == undefined) ? true : enableTooltips; if (options.title && enableTooltips) { el.title = createTooltip(options.title, options.action, shortcuts); if (isMac) { el.title = el.title.replace('Ctrl', '⌘'); el.title = el.title.replace('Alt', '⌥'); } } if (options.noDisable) { el.classList.add('no-disable'); } if (options.noMobile) { el.classList.add('no-mobile'); } el.tabIndex = -1; // Create icon element and append as a child to the button var icon = document.createElement('i'); icon.className = options.className; el.appendChild(icon); return el; } function createSep() { var el = document.createElement('i'); el.className = 'separator'; el.innerHTML = '|'; return el; } function createTooltip(title, action, shortcuts) { var actionName; var tooltip = title; if (action) { actionName = getBindingName(action); if (shortcuts[actionName]) { tooltip += ' (' + fixShortcut(shortcuts[actionName]) + ')'; } } return tooltip; } /** * The state of CodeMirror at the given position. */ function getState(cm, pos) { pos = pos || cm.getCursor('start'); var stat = cm.getTokenAt(pos); if (!stat.type) return {}; var types = stat.type.split(' '); var ret = {}, data, text; for (var i = 0; i < types.length; i++) { data = types[i]; if (data === 'strong') { ret.bold = true; } else if (data === 'variable-2') { text = cm.getLine(pos.line); if (/^\s*\d+\.\s/.test(text)) { ret['ordered-list'] = true; } else { ret['unordered-list'] = true; } } else if (data === 'atom') { ret.quote = true; } else if (data === 'em') { ret.italic = true; } else if (data === 'quote') { ret.quote = true; } else if (data === 'strikethrough') { ret.strikethrough = true; } else if (data === 'comment') { ret.code = true; } else if (data === 'link') { ret.link = true; } else if (data === 'tag') { ret.image = true; } else if (data.match(/^header(-[1-6])?$/)) { ret[data.replace('header', 'heading')] = true; } } return ret; } // Saved overflow setting var saved_overflow = ''; /** * Toggle full screen of the editor. */ function toggleFullScreen(editor) { // Set fullscreen var cm = editor.codemirror; cm.setOption('fullScreen', !cm.getOption('fullScreen')); // Prevent scrolling on body during fullscreen active if (cm.getOption('fullScreen')) { saved_overflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = saved_overflow; } // Update toolbar class var wrap = cm.getWrapperElement(); if (!/fullscreen/.test(wrap.previousSibling.className)) { wrap.previousSibling.className += ' fullscreen'; } else { wrap.previousSibling.className = wrap.previousSibling.className.replace(/\s*fullscreen\b/, ''); } // Update toolbar button if (editor.toolbarElements && editor.toolbarElements.fullscreen) { var toolbarButton = editor.toolbarElements.fullscreen; if (!/active/.test(toolbarButton.className)) { toolbarButton.className += ' active'; } else { toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, ''); } } // Hide side by side if needed var sidebyside = cm.getWrapperElement().nextSibling; if (/editor-preview-active-side/.test(sidebyside.className)) toggleSideBySide(editor); if (editor.options.onToggleFullScreen) { editor.options.onToggleFullScreen(cm.getOption('fullScreen') || false); } } /** * Action for toggling bold. */ function toggleBold(editor) { _toggleBlock(editor, 'bold', editor.options.blockStyles.bold); } /** * Action for toggling italic. */ function toggleItalic(editor) { _toggleBlock(editor, 'italic', editor.options.blockStyles.italic); } /** * Action for toggling strikethrough. */ function toggleStrikethrough(editor) { _toggleBlock(editor, 'strikethrough', '~~'); } /** * Action for toggling code block. */ function toggleCodeBlock(editor) { var fenceCharsToInsert = editor.options.blockStyles.code; function fencing_line(line) { /* return true, if this is a ``` or ~~~ line */ if (typeof line !== 'object') { throw 'fencing_line() takes a \'line\' object (not a line number, or line text). Got: ' + typeof line + ': ' + line; } return line.styles && line.styles[2] && line.styles[2].indexOf('formatting-code-block') !== -1; } function token_state(token) { // base goes an extra level deep when mode backdrops are used, e.g. spellchecker on return token.state.base.base || token.state.base; } function code_type(cm, line_num, line, firstTok, lastTok) { /* * Return "single", "indented", "fenced" or false * * cm and line_num are required. Others are optional for efficiency * To check in the middle of a line, pass in firstTok yourself. */ line = line || cm.getLineHandle(line_num); firstTok = firstTok || cm.getTokenAt({ line: line_num, ch: 1, }); lastTok = lastTok || (!!line.text && cm.getTokenAt({ line: line_num, ch: line.text.length - 1, })); var types = firstTok.type ? firstTok.type.split(' ') : []; if (lastTok && token_state(lastTok).indentedCode) { // have to check last char, since first chars of first line aren"t marked as indented return 'indented'; } else if (types.indexOf('comment') === -1) { // has to be after "indented" check, since first chars of first indented line aren"t marked as such return false; } else if (token_state(firstTok).fencedChars || token_state(lastTok).fencedChars || fencing_line(line)) { return 'fenced'; } else { return 'single'; } } function insertFencingAtSelection(cm, cur_start, cur_end, fenceCharsToInsert) { var start_line_sel = cur_start.line + 1, end_line_sel = cur_end.line + 1, sel_multi = cur_start.line !== cur_end.line, repl_start = fenceCharsToInsert + '\n', repl_end = '\n' + fenceCharsToInsert; if (sel_multi) { end_line_sel++; } // handle last char including \n or not if (sel_multi && cur_end.ch === 0) { repl_end = fenceCharsToInsert + '\n'; end_line_sel--; } _replaceSelection(cm, false, [repl_start, repl_end]); cm.setSelection({ line: start_line_sel, ch: 0, }, { line: end_line_sel, ch: 0, }); } var cm = editor.codemirror, cur_start = cm.getCursor('start'), cur_end = cm.getCursor('end'), tok = cm.getTokenAt({ line: cur_start.line, ch: cur_start.ch || 1, }), // avoid ch 0 which is a cursor pos but not token line = cm.getLineHandle(cur_start.line), is_code = code_type(cm, cur_start.line, line, tok); var block_start, block_end, lineCount; if (is_code === 'single') { // similar to some EasyMDE _toggleBlock logic var start = line.text.slice(0, cur_start.ch).replace('`', ''), end = line.text.slice(cur_start.ch).replace('`', ''); cm.replaceRange(start + end, { line: cur_start.line, ch: 0, }, { line: cur_start.line, ch: 99999999999999, }); cur_start.ch--; if (cur_start !== cur_end) { cur_end.ch--; } cm.setSelection(cur_start, cur_end); cm.focus(); } else if (is_code === 'fenced') { if (cur_start.line !== cur_end.line || cur_start.ch !== cur_end.ch) { // use selection // find the fenced line so we know what type it is (tilde, backticks, number of them) for (block_start = cur_start.line; block_start >= 0; block_start--) { line = cm.getLineHandle(block_start); if (fencing_line(line)) { break; } } var fencedTok = cm.getTokenAt({ line: block_start, ch: 1, }); var fence_chars = token_state(fencedTok).fencedChars; var start_text, start_line; var end_text, end_line; // check for selection going up against fenced lines, in which case we don't want to add more fencing if (fencing_line(cm.getLineHandle(cur_start.line))) { start_text = ''; start_line = cur_start.line; } else if (fencing_line(cm.getLineHandle(cur_start.line - 1))) { start_text = ''; start_line = cur_start.line - 1; } else { start_text = fence_chars + '\n'; start_line = cur_start.line; } if (fencing_line(cm.getLineHandle(cur_end.line))) { end_text = ''; end_line = cur_end.line; if (cur_end.ch === 0) { end_line += 1; } } else if (cur_end.ch !== 0 && fencing_line(cm.getLineHandle(cur_end.line + 1))) { end_text = ''; end_line = cur_end.line + 1; } else { end_text = fence_chars + '\n'; end_line = cur_end.line + 1; } if (cur_end.ch === 0) { // full last line selected, putting cursor at beginning of next end_line -= 1; } cm.operation(function () { // end line first, so that line numbers don't change cm.replaceRange(end_text, { line: end_line, ch: 0, }, { line: end_line + (end_text ? 0 : 1), ch: 0, }); cm.replaceRange(start_text, { line: start_line, ch: 0, }, { line: start_line + (start_text ? 0 : 1), ch: 0, }); }); cm.setSelection({ line: start_line + (start_text ? 1 : 0), ch: 0, }, { line: end_line + (start_text ? 1 : -1), ch: 0, }); cm.focus(); } else { // no selection, search for ends of this fenced block var search_from = cur_start.line; if (fencing_line(cm.getLineHandle(cur_start.line))) { // gets a little tricky if cursor is right on a fenced line if (code_type(cm, cur_start.line + 1) === 'fenced') { block_start = cur_start.line; search_from = cur_start.line + 1; // for searching for "end" } else { block_end = cur_start.line; search_from = cur_start.line - 1; // for searching for "start" } } if (block_start === undefined) { for (block_start = search_from; block_start >= 0; block_start--) { line = cm.getLineHandle(block_start); if (fencing_line(line)) { break; } } } if (block_end === undefined) { lineCount = cm.lineCount(); for (block_end = search_from; block_end < lineCount; block_end++) { line = cm.getLineHandle(block_end); if (fencing_line(line)) { break; } } } cm.operation(function () { cm.replaceRange('', { line: block_start, ch: 0, }, { line: block_start + 1, ch: 0, }); cm.replaceRange('', { line: block_end - 1, ch: 0, }, { line: block_end, ch: 0, }); }); cm.focus(); } } else if (is_code === 'indented') { if (cur_start.line !== cur_end.line || cur_start.ch !== cur_end.ch) { // use selection block_start = cur_start.line; block_end = cur_end.line; if (cur_end.ch === 0) { block_end--; } } else { // no selection, search for ends of this indented block for (block_start = cur_start.line; block_start >= 0; block_start--) { line = cm.getLineHandle(block_start); if (line.text.match(/^\s*$/)) { // empty or all whitespace - keep going continue; } else { if (code_type(cm, block_start, line) !== 'indented') { block_start += 1; break; } } } lineCount = cm.lineCount(); for (block_end = cur_start.line; block_end < lineCount; block_end++) { line = cm.getLineHandle(block_end); if (line.text.match(/^\s*$/)) { // empty or all whitespace - keep going continue; } else { if (code_type(cm, block_end, line) !== 'indented') { block_end -= 1; break; } } } } // if we are going to un-indent based on a selected set of lines, and the next line is indented too, we need to // insert a blank line so that the next line(s) continue to be indented code var next_line = cm.getLineHandle(block_end + 1), next_line_last_tok = next_line && cm.getTokenAt({ line: block_end + 1, ch: next_line.text.length - 1, }), next_line_indented = next_line_last_tok && token_state(next_line_last_tok).indentedCode; if (next_line_indented) { cm.replaceRange('\n', { line: block_end + 1, ch: 0, }); } for (var i = block_start; i <= block_end; i++) { cm.indentLine(i, 'subtract'); // TODO: this doesn't get tracked in the history, so can't be undone :( } cm.focus(); } else { // insert code formatting var no_sel_and_starting_of_line = (cur_start.line === cur_end.line && cur_start.ch === cur_end.ch && cur_start.ch === 0); var sel_multi = cur_start.line !== cur_end.line; if (no_sel_and_starting_of_line || sel_multi) { insertFencingAtSelection(cm, cur_start, cur_end, fenceCharsToInsert); } else { _replaceSelection(cm, false, ['`', '`']); } } } /** * Action for toggling blockquote. */ function toggleBlockquote(editor) { var cm = editor.codemirror; _toggleLine(cm, 'quote'); } /** * Action for toggling heading size: normal -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> normal */ function toggleHeadingSmaller(editor) { var cm = editor.codemirror; _toggleHeading(cm, 'smaller'); } /** * Action for toggling heading size: normal -> h6 -> h5 -> h4 -> h3 -> h2 -> h1 -> normal */ function toggleHeadingBigger(editor) { var cm = editor.codemirror; _toggleHeading(cm, 'bigger'); } /** * Action for toggling heading size 1 */ function toggleHeading1(editor) { var cm = editor.codemirror; _toggleHeading(cm, undefined, 1); } /** * Action for toggling heading size 2 */ function toggleHeading2(editor) { var cm = editor.codemirror; _toggleHeading(cm, undefined, 2); } /** * Action for toggling heading size 3 */ function toggleHeading3(editor) { var cm = editor.codemirror; _toggleHeading(cm, undefined, 3); } /** * Action for toggling ul. */ function toggleUnorderedList(editor) { var cm = editor.codemirror; _toggleLine(cm, 'unordered-list'); } /** * Action for toggling ol. */ function toggleOrderedList(editor) { var cm = editor.codemirror; _toggleLine(cm, 'ordered-list'); } /** * Action for clean block (remove headline, list, blockquote code, markers) */ function cleanBlock(editor) { var cm = editor.codemirror; _cleanBlock(cm); } /** * Action for drawing a link. */ function drawLink(editor) { var cm = editor.codemirror; var stat = getState(cm); var options = editor.options; var url = 'https://'; if (options.promptURLs) { url = prompt(options.promptTexts.link, 'https://'); if (!url) { return false; } } _replaceSelection(cm, stat.link, options.insertTexts.link, url); } /** * Action for drawing an img. */ function drawImage(editor) { var cm = editor.codemirror; var stat = getState(cm); var options = editor.options; var url = 'https://'; if (options.promptURLs) { url = prompt(options.promptTexts.image, 'https://'); if (!url) { return false; } } _replaceSelection(cm, stat.image, options.insertTexts.image, url); } /** * Action for drawing a table. */ function drawTable(editor) { var cm = editor.codemirror; var stat = getState(cm); var options = editor.options; _replaceSelection(cm, stat.table, options.insertTexts.table); } /** * Action for drawing a horizontal rule. */ function drawHorizontalRule(editor) { var cm = editor.codemirror; var stat = getState(cm); var options = editor.options; _replaceSelection(cm, stat.image, options.insertTexts.horizontalRule); } /** * Undo action. */ function undo(editor) { var cm = editor.codemirror; cm.undo(); cm.focus(); } /** * Redo action. */ function redo(editor) { var cm = editor.codemirror; cm.redo(); cm.focus(); } /** * Toggle side by side preview */ function toggleSideBySide(editor) { var cm = editor.codemirror; var wrapper = cm.getWrapperElement(); var preview = wrapper.nextSibling; var toolbarButton = editor.toolbarElements && editor.toolbarElements['side-by-side']; var useSideBySideListener = false; if (/editor-preview-active-side/.test(preview.className)) { preview.className = preview.className.replace( /\s*editor-preview-active-side\s*/g, '' ); if (toolbarButton) toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, ''); wrapper.className = wrapper.className.replace(/\s*CodeMirror-sided\s*/g, ' '); } else { // When the preview button is clicked for the first time, // give some time for the transition from editor.css to fire and the view to slide from right to left, // instead of just appearing. setTimeout(function () { if (!cm.getOption('fullScreen')) toggleFullScreen(editor); preview.className += ' editor-preview-active-side'; }, 1); if (toolbarButton) toolbarButton.className += ' active'; wrapper.className += ' CodeMirror-sided'; useSideBySideListener = true; } // Hide normal preview if active var previewNormal = wrapper.lastChild; if (/editor-preview-active/.test(previewNormal.className)) { previewNormal.className = previewNormal.className.replace( /\s*editor-preview-active\s*/g, '' ); var toolbar = editor.toolbarElements.preview; var toolbar_div = wrapper.previousSibling; toolbar.className = toolbar.className.replace(/\s*active\s*/g, ''); toolbar_div.className = toolbar_div.className.replace(/\s*disabled-for-preview*/g, ''); } var sideBySideRenderingFunction = function () { preview.innerHTML = editor.options.previewRender(editor.value(), preview); }; if (!cm.sideBySideRenderingFunction) { cm.sideBySideRenderingFunction = sideBySideRenderingFunction; } if (useSideBySideListener) { preview.innerHTML = editor.options.previewRender(editor.value(), preview); cm.on('update', cm.sideBySideRenderingFunction); } else { cm.off('update', cm.sideBySideRenderingFunction); } // Refresh to fix selection being off (#309) cm.refresh(); } /** * Preview action. */ function togglePreview(editor) { var cm = editor.codemirror; var wrapper = cm.getWrapperElement(); var toolbar_div = wrapper.previousSibling; var toolbar = editor.options.toolbar ? editor.toolbarElements.preview : false; var preview = wrapper.lastChild; if (!preview || !/editor-preview/.test(preview.className)) { preview = document.createElement('div'); preview.className = 'editor-preview'; wrapper.appendChild(preview); } if (/editor-preview-active/.test(preview.className)) { preview.className = preview.className.replace( /\s*editor-preview-active\s*/g, '' ); if (toolbar) { toolbar.className = toolbar.className.replace(/\s*active\s*/g, ''); toolbar_div.className = toolbar_div.className.replace(/\s*disabled-for-preview*/g, ''); } } else { // When the preview button is clicked for the first time, // give some time for the transition from editor.css to fire and the view to slide from right to left, // instead of just appearing. setTimeout(function () { preview.className += ' editor-preview-active'; }, 1); if (toolbar) { toolbar.className += ' active'; toolbar_div.className += ' disabled-for-preview'; } } preview.innerHTML = editor.options.previewRender(editor.value(), preview); // Turn off side by side if needed var sidebyside = cm.getWrapperElement().nextSibling; if (/editor-preview-active-side/.test(sidebyside.className)) toggleSideBySide(editor); } function _replaceSelection(cm, active, startEnd, url) { if (/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) return; var text; var start = startEnd[0]; var end = startEnd[1]; var startPoint = {}, endPoint = {}; Object.assign(startPoint, cm.getCursor('start')); Object.assign(endPoint, cm.getCursor('end')); if (url) { end = end.replace('#url#', url); } if (active) { text = cm.getLine(startPoint.line); start = text.slice(0, startPoint.ch); end = text.slice(startPoint.ch); cm.replaceRange(start + end, { line: startPoint.line, ch: 0, }); } else { text = cm.getSelection(); cm.replaceSelection(start + text + end); startPoint.ch += start.length; if (startPoint !== endPoint) { endPoint.ch += start.length; } } cm.setSelection(startPoint, endPoint); cm.focus(); } function _toggleHeading(cm, direction, size) { if (/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) return; var startPoint = cm.getCursor('start'); var endPoint = cm.getCursor('end'); for (var i = startPoint.line; i <= endPoint.line; i++) { (function (i) { var text = cm.getLine(i); var currHeadingLevel = text.search(/[^#]/); if (direction !== undefined) { if (currHeadingLevel <= 0) { if (direction == 'bigger') { text = '###### ' + text; } else { text = '# ' + text; } } else if (currHeadingLevel == 6 && direction == 'smaller') { text = text.substr(7); } else if (currHeadingLevel == 1 && direction == 'bigger') { text = text.substr(2); } else { if (direction == 'bigger') { text = text.substr(1); } else { text = '#' + text; } } } else { if (size == 1) { if (currHeadingLevel <= 0) { text = '# ' + text; } else if (currHeadingLevel == size) { text = text.substr(currHeadingLevel + 1); } else { text = '# ' + text.substr(currHeadingLevel + 1); } } else if (size == 2) { if (currHeadingLevel <= 0) { text = '## ' + text; } else if (currHeadingLevel == size) { text = text.substr(currHeadingLevel + 1); } else { text = '## ' + text.substr(currHeadingLevel + 1); } } else { if (currHeadingLevel <= 0) { text = '### ' + text; } else if (currHeadingLevel == size) { text = text.substr(currHeadingLevel + 1); } else { text = '### ' + text.substr(currHeadingLevel + 1); } } } cm.replaceRange(text, { line: i, ch: 0, }, { line: i, ch: 99999999999999, }); })(i); } cm.focus(); } function _toggleLine(cm, name) { if (/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) return; var listRegexp = /^(\s*)(\*|-|\+|\d*\.)(\s+)/; var whitespacesRegexp = /^\s*/; var stat = getState(cm); var startPoint = cm.getCursor('start'); var endPoint = cm.getCursor('end'); var repl = { 'quote': /^(\s*)>\s+/, 'unordered-list': listRegexp, 'ordered-list': listRegexp, }; var _getChar = function (name, i) { var map = { 'quote': '>', 'unordered-list': '*', 'ordered-list': '%%i.', }; return map[name].replace('%%i', i); }; var _checkChar = function (name, char) { var map = { 'quote': '>', 'unordered-list': '*', 'ordered-list': 'd+.', }; var rt = new RegExp(map[name]); return char && rt.test(char); }; var line = 1; for (var i = startPoint.line; i <= endPoint.line; i++) { (function (i) { var text = cm.getLine(i); if (stat[name]) { text = text.replace(repl[name], '$1'); } else { var arr = listRegexp.exec(text); var char = _getChar(name, line); if (arr !== null) { if (_checkChar(name, arr[2])) { char = ''; } text = arr[1] + char + arr[3] + text.replace(whitespacesRegexp, '').replace(repl[name], '$1'); } else { text = char + ' ' + text; } line += 1; } cm.replaceRange(text, { line: i, ch: 0, }, { line: i, ch: 99999999999999, }); })(i); } cm.focus(); } function _toggleBlock(editor, type, start_chars, end_chars) { if (/editor-preview-active/.test(editor.codemirror.getWrapperElement().lastChild.className)) return; end_chars = (typeof end_chars === 'undefined') ? start_chars : end_chars; var cm = editor.codemirror; var stat = getState(cm); var text; var start = start_chars; var end = end_chars; var startPoint = cm.getCursor('start'); var endPoint = cm.getCursor('end'); if (stat[type]) { text = cm.getLine(startPoint.line); start = text.slice(0, startPoint.ch); end = text.slice(startPoint.ch); if (type == 'bold') { start = start.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/, ''); end = end.replace(/(\*\*|__)/, ''); } else if (type == 'italic') { start = start.replace(/(\*|_)(?![\s\S]*(\*|_))/, ''); end = end.replace(/(\*|_)/, ''); } else if (type == 'strikethrough') { start = start.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/, ''); end = end.replace(/(\*\*|~~)/, ''); } cm.replaceRange(start + end, { line: startPoint.line, ch: 0, }, { line: startPoint.line, ch: 99999999999999, }); if (type == 'bold' || type == 'strikethrough') { startPoint.ch -= 2; if (startPoint !== endPoint) { endPoint.ch -= 2; } } else if (type == 'italic') { startPoint.ch -= 1; if (startPoint !== endPoint) { endPoint.ch -= 1; } } } else { text = cm.getSelection(); if (type == 'bold') { text = text.split('**').join(''); text = text.split('__').join(''); } else if (type == 'italic') { text = text.split('*').join(''); text = text.split('_').join(''); } else if (type == 'strikethrough') { text = text.split('~~').join(''); } cm.replaceSelection(start + text + end); startPoint.ch += start_chars.length; endPoint.ch = startPoint.ch + text.length; } cm.setSelection(startPoint, endPoint); cm.focus(); } function _cleanBlock(cm) { if (/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) return; var startPoint = cm.getCursor('start'); var endPoint = cm.getCursor('end'); var text; for (var line = startPoint.line; line <= endPoint.line; line++) { text = cm.getLine(line); text = text.replace(/^[ ]*([# ]+|\*|-|[> ]+|[0-9]+(.|\)))[ ]*/, ''); cm.replaceRange(text, { line: line, ch: 0, }, { line: line, ch: 99999999999999, }); } } // Merge the properties of one object into another. function _mergeProperties(target, source) { for (var property in source) { if (source.hasOwnProperty(property)) { if (source[property] instanceof Array) { target[property] = source[property].concat(target[property] instanceof Array ? target[property] : []); } else if ( source[property] !== null && typeof source[property] === 'object' && source[property].constructor === Object ) { target[property] = _mergeProperties(target[property] || {}, source[property]); } else { target[property] = source[property]; } } } return target; } // Merge an arbitrary number of objects into one. function extend(target) { for (var i = 1; i < arguments.length; i++) { target = _mergeProperties(target, arguments[i]); } return target; } /* The right word count in respect for CJK. */ function wordCount(data) { var pattern = /[a-zA-Z0-9_\u00A0-\u02AF\u0392-\u03c9\u0410-\u04F9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g; var m = data.match(pattern); var count = 0; if (m === null) return count; for (var i = 0; i < m.length; i++) { if (m[i].charCodeAt(0) >= 0x4E00) { count += m[i].length; } else { count += 1; } } return count; } var toolbarBuiltInButtons = { 'bold': { name: 'bold', action: toggleBold, className: 'fa fa-bold', title: 'Bold', default: true, }, 'italic': { name: 'italic', action: toggleItalic, className: 'fa fa-italic', title: 'Italic', default: true, }, 'strikethrough': { name: 'strikethrough', action: toggleStrikethrough, className: 'fa fa-strikethrough', title: 'Strikethrough', }, 'heading': { name: 'heading', action: toggleHeadingSmaller, className: 'fa fa-header fa-heading', title: 'Heading', default: true, }, 'heading-smaller': { name: 'heading-smaller', action: toggleHeadingSmaller, className: 'fa fa-header fa-heading header-smaller', title: 'Smaller Heading', }, 'heading-bigger': { name: 'heading-bigger', action: toggleHeadingBigger, className: 'fa fa-header fa-heading header-bigger', title: 'Bigger Heading', }, 'heading-1': { name: 'heading-1', action: toggleHeading1, className: 'fa fa-header fa-heading header-1', title: 'Big Heading', }, 'heading-2': { name: 'heading-2', action: toggleHeading2, className: 'fa fa-header fa-heading header-2', title: 'Medium Heading', }, 'heading-3': { name: 'heading-3', action: toggleHeading3, className: 'fa fa-header fa-heading header-3', title: 'Small Heading', }, 'separator-1': { name: 'separator-1', }, 'code': { name: 'code', action: toggleCodeBlock, className: 'fa fa-code', title: 'Code', }, 'quote': { name: 'quote', action: toggleBlockquote, className: 'fa fa-quote-left', title: 'Quote', default: true, }, 'unordered-list': { name: 'unordered-list', action: toggleUnorderedList, className: 'fa fa-list-ul', title: 'Generic List', default: true, }, 'ordered-list': { name: 'ordered-list', action: toggleOrderedList, className: 'fa fa-list-ol', title: 'Numbered List', default: true, }, 'clean-block': { name: 'clean-block', action: cleanBlock, className: 'fa fa-eraser', title: 'Clean block', }, 'separator-2': { name: 'separator-2', }, 'link': { name: 'link', action: drawLink, className: 'fa fa-link', title: 'Create Link', default: true, }, 'image': { name: 'image', action: drawImage, className: 'fa fa-image', title: 'Insert Image', default: true, }, 'table': { name: 'table', action: drawTable, className: 'fa fa-table', title: 'Insert Table', }, 'horizontal-rule': { name: 'horizontal-rule', action: drawHorizontalRule, className: 'fa fa-minus', title: 'Insert Horizontal Line', }, 'separator-3': { name: 'separator-3', }, 'preview': { name: 'preview', action: togglePreview, className: 'fa fa-eye', noDisable: true, title: 'Toggle Preview', default: true, }, 'side-by-side': { name: 'side-by-side', action: toggleSideBySide, className: 'fa fa-columns', noDisable: true, noMobile: true, title: 'Toggle Side by Side', default: true, }, 'fullscreen': { name: 'fullscreen', action: toggleFullScreen, className: 'fa fa-arrows-alt', noDisable: true, noMobile: true, title: 'Toggle Fullscreen', default: true, }, 'separator-4': { name: 'separator-4', }, 'guide': { name: 'guide', action: 'https://www.markdownguide.org/basic-syntax/', className: 'fa fa-question-circle', noDisable: true, title: 'Markdown Guide', default: true, }, 'separator-5': { name: 'separator-5', }, 'undo': { name: 'undo', action: undo, className: 'fa fa-undo', noDisable: true, title: 'Undo', }, 'redo': { name: 'redo', action: redo, className: 'fa fa-repeat fa-redo', noDisable: true, title: 'Redo', }, }; var insertTexts = { link: ['[', '](#url#)'], image: ['![](', '#url#)'], table: ['', '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n'], horizontalRule: ['', '\n\n-----\n\n'], }; var promptTexts = { link: 'URL for the link:', image: 'URL of the image:', }; var blockStyles = { 'bold': '**', 'code': '```', 'italic': '*', }; /** * Interface of EasyMDE. */ function EasyMDE(options) { // Handle options parameter options = options || {}; // Used later to refer to it"s parent options.parent = this; // Check if Font Awesome needs to be auto downloaded var autoDownloadFA = true; if (options.autoDownloadFontAwesome === false) { autoDownloadFA = false; } if (options.autoDownloadFontAwesome !== true) { var styleSheets = document.styleSheets; for (var i = 0; i < styleSheets.length; i++) { if (!styleSheets[i].href) continue; if (styleSheets[i].href.indexOf('//maxcdn.bootstrapcdn.com/font-awesome/') > -1) { autoDownloadFA = false; } } } if (autoDownloadFA) { var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css'; document.getElementsByTagName('head')[0].appendChild(link); } // Find the textarea to use if (options.element) { this.element = options.element; } else if (options.element === null) { // This means that the element option was specified, but no element was found console.log('EasyMDE: Error. No element was found.'); return; } // Handle toolbar if (options.toolbar === undefined) { // Initialize options.toolbar = []; // Loop over the built in buttons, to get the preferred order for (var key in toolbarBuiltInButtons) { if (toolbarBuiltInButtons.hasOwnProperty(key)) { if (key.indexOf('separator-') != -1) { options.toolbar.push('|'); } if (toolbarBuiltInButtons[key].default === true || (options.showIcons && options.showIcons.constructor === Array && options.showIcons.indexOf(key) != -1)) { options.toolbar.push(key); } } } } // Handle status bar if (!options.hasOwnProperty('status')) { options.status = ['autosave', 'lines', 'words', 'cursor']; } // Add default preview rendering function if (!options.previewRender) { options.previewRender = function (plainText) { // Note: "this" refers to the options object return this.parent.markdown(plainText); }; } // Set default options for parsing config options.parsingConfig = extend({ highlightFormatting: true, // needed for toggleCodeBlock to detect types of code }, options.parsingConfig || {}); // Merging the insertTexts, with the given options options.insertTexts = extend({}, insertTexts, options.insertTexts || {}); // Merging the promptTexts, with the given options options.promptTexts = extend({}, promptTexts, options.promptTexts || {}); // Merging the blockStyles, with the given options options.blockStyles = extend({}, blockStyles, options.blockStyles || {}); // Merging the shortcuts, with the given options options.shortcuts = extend({}, shortcuts, options.shortcuts || {}); options.minHeight = options.minHeight || '300px'; // Change unique_id to uniqueId for backwards compatibility if (options.autosave != undefined && options.autosave.unique_id != undefined && options.autosave.unique_id != '') options.autosave.uniqueId = options.autosave.unique_id; // Update this options this.options = options; // Auto render this.render(); // The codemirror component is only available after rendering // so, the setter for the initialValue can only run after // the element has been rendered if (options.initialValue && (!this.options.autosave || this.options.autosave.foundSavedValue !== true)) { this.value(options.initialValue); } } /** * Default markdown render. */ EasyMDE.prototype.markdown = function (text) { if (marked) { // Initialize var markedOptions; if (this.options && this.options.renderingConfig && this.options.renderingConfig.markedOptions) { markedOptions = this.options.renderingConfig.markedOptions; } else { markedOptions = {}; } // Update options if (this.options && this.options.renderingConfig && this.options.renderingConfig.singleLineBreaks === false) { markedOptions.breaks = false; } else { markedOptions.breaks = true; } if (this.options && this.options.renderingConfig && this.options.renderingConfig.codeSyntaxHighlighting === true) { /* Get HLJS from config or window */ var hljs = this.options.renderingConfig.hljs || window.hljs; /* Check if HLJS loaded */ if (hljs) { markedOptions.highlight = function (code) { return hljs.highlightAuto(code).value; }; } } // Set options marked.setOptions(markedOptions); // Convert the markdown to HTML var htmlText = marked(text); // Edit the HTML anchors to add 'target="_blank"' by default. htmlText = addAnchorTargetBlank(htmlText); return htmlText; } }; /** * Render editor to the given element. */ EasyMDE.prototype.render = function (el) { if (!el) { el = this.element || document.getElementsByTagName('textarea')[0]; } if (this._rendered && this._rendered === el) { // Already rendered. return; } this.element = el; var options = this.options; var self = this; var keyMaps = {}; for (var key in options.shortcuts) { // null stands for "do not bind this command" if (options.shortcuts[key] !== null && bindings[key] !== null) { (function (key) { keyMaps[fixShortcut(options.shortcuts[key])] = function () { bindings[key](self); }; })(key); } } keyMaps['Enter'] = 'newlineAndIndentContinueMarkdownList'; keyMaps['Tab'] = 'tabAndIndentMarkdownList'; keyMaps['Shift-Tab'] = 'shiftTabAndUnindentMarkdownList'; keyMaps['Esc'] = function (cm) { if (cm.getOption('fullScreen')) toggleFullScreen(self); }; document.addEventListener('keydown', function (e) { e = e || window.event; if (e.keyCode == 27) { if (self.codemirror.getOption('fullScreen')) toggleFullScreen(self); } }, false); var mode, backdrop; if (options.spellChecker !== false) { mode = 'spell-checker'; backdrop = options.parsingConfig; backdrop.name = 'gfm'; backdrop.gitHubSpice = false; CodeMirrorSpellChecker({ codeMirrorInstance: CodeMirror, }); } else { mode = options.parsingConfig; mode.name = 'gfm'; mode.gitHubSpice = false; } // eslint-disable-next-line no-unused-vars function configureMouse(cm, repeat, event) { return { addNew: false, }; } this.codemirror = CodeMirror.fromTextArea(el, { mode: mode, backdrop: backdrop, theme: (options.theme != undefined) ? options.theme : 'easymde', tabSize: (options.tabSize != undefined) ? options.tabSize : 2, indentUnit: (options.tabSize != undefined) ? options.tabSize : 2, indentWithTabs: (options.indentWithTabs === false) ? false : true, lineNumbers: false, autofocus: (options.autofocus === true) ? true : false, extraKeys: keyMaps, lineWrapping: (options.lineWrapping === false) ? false : true, allowDropFileTypes: ['text/plain'], placeholder: options.placeholder || el.getAttribute('placeholder') || '', styleSelectedText: (options.styleSelectedText != undefined) ? options.styleSelectedText : !isMobile(), configureMouse: configureMouse, }); this.codemirror.getScrollerElement().style.minHeight = options.minHeight; if (options.forceSync === true) { var cm = this.codemirror; cm.on('change', function () { cm.save(); }); } this.gui = {}; if (options.toolbar !== false) { this.gui.toolbar = this.createToolbar(); } if (options.status !== false) { this.gui.statusbar = this.createStatusbar(); } if (options.autosave != undefined && options.autosave.enabled === true) { this.autosave(); } this.gui.sideBySide = this.createSideBySide(); this._rendered = this.element; // Fixes CodeMirror bug (#344) var temp_cm = this.codemirror; setTimeout(function () { temp_cm.refresh(); }.bind(temp_cm), 0); }; // Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem throw QuotaExceededError. We're going to detect this and set a variable accordingly. function isLocalStorageAvailable() { if (typeof localStorage === 'object') { try { localStorage.setItem('smde_localStorage', 1); localStorage.removeItem('smde_localStorage'); } catch (e) { return false; } } else { return false; } return true; } EasyMDE.prototype.autosave = function () { if (isLocalStorageAvailable()) { var easyMDE = this; if (this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == '') { console.log('EasyMDE: You must set a uniqueId to use the autosave feature'); return; } if(this.options.autosave.binded !== true) { if (easyMDE.element.form != null && easyMDE.element.form != undefined) { easyMDE.element.form.addEventListener('submit', function () { clearTimeout(easyMDE.autosaveTimeoutId); easyMDE.autosaveTimeoutId = undefined; localStorage.removeItem('smde_' + easyMDE.options.autosave.uniqueId); // Restart autosaving in case the submit will be cancelled down the line setTimeout(function() { easyMDE.autosave(); }, easyMDE.options.autosave.delay || 10000); }); } this.options.autosave.binded = true; } if (this.options.autosave.loaded !== true) { if (typeof localStorage.getItem('smde_' + this.options.autosave.uniqueId) == 'string' && localStorage.getItem('smde_' + this.options.autosave.uniqueId) != '') { this.codemirror.setValue(localStorage.getItem('smde_' + this.options.autosave.uniqueId)); this.options.autosave.foundSavedValue = true; } this.options.autosave.loaded = true; } localStorage.setItem('smde_' + this.options.autosave.uniqueId, easyMDE.value()); var el = document.getElementById('autosaved'); if (el != null && el != undefined && el != '') { var d = new Date(); var hh = d.getHours(); var m = d.getMinutes(); var dd = 'am'; var h = hh; if (h >= 12) { h = hh - 12; dd = 'pm'; } if (h == 0) { h = 12; } m = m < 10 ? '0' + m : m; el.innerHTML = 'Autosaved: ' + h + ':' + m + ' ' + dd; } this.autosaveTimeoutId = setTimeout(function () { easyMDE.autosave(); }, this.options.autosave.delay || 10000); } else { console.log('EasyMDE: localStorage not available, cannot autosave'); } }; EasyMDE.prototype.clearAutosavedValue = function () { if (isLocalStorageAvailable()) { if (this.options.autosave == undefined || this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == '') { console.log('EasyMDE: You must set a uniqueId to clear the autosave value'); return; } localStorage.removeItem('smde_' + this.options.autosave.uniqueId); } else { console.log('EasyMDE: localStorage not available, cannot autosave'); } }; EasyMDE.prototype.createSideBySide = function () { var cm = this.codemirror; var wrapper = cm.getWrapperElement(); var preview = wrapper.nextSibling; if (!preview || !/editor-preview-side/.test(preview.className)) { preview = document.createElement('div'); preview.className = 'editor-preview-side'; wrapper.parentNode.insertBefore(preview, wrapper.nextSibling); } if (this.options.syncSideBySidePreviewScroll === false) return preview; // Syncs scroll editor -> preview var cScroll = false; var pScroll = false; cm.on('scroll', function (v) { if (cScroll) { cScroll = false; return; } pScroll = true; var height = v.getScrollInfo().height - v.getScrollInfo().clientHeight; var ratio = parseFloat(v.getScrollInfo().top) / height; var move = (preview.scrollHeight - preview.clientHeight) * ratio; preview.scrollTop = move; }); // Syncs scroll preview -> editor preview.onscroll = function () { if (pScroll) { pScroll = false; return; } cScroll = true; var height = preview.scrollHeight - preview.clientHeight; var ratio = parseFloat(preview.scrollTop) / height; var move = (cm.getScrollInfo().height - cm.getScrollInfo().clientHeight) * ratio; cm.scrollTo(0, move); }; return preview; }; EasyMDE.prototype.createToolbar = function (items) { items = items || this.options.toolbar; if (!items || items.length === 0) { return; } var i; for (i = 0; i < items.length; i++) { if (toolbarBuiltInButtons[items[i]] != undefined) { items[i] = toolbarBuiltInButtons[items[i]]; } } var bar = document.createElement('div'); bar.className = 'editor-toolbar'; var self = this; var toolbarData = {}; self.toolbar = items; for (i = 0; i < items.length; i++) { if (items[i].name == 'guide' && self.options.toolbarGuideIcon === false) continue; if (self.options.hideIcons && self.options.hideIcons.indexOf(items[i].name) != -1) continue; // Fullscreen does not work well on mobile devices (even tablets) // In the future, hopefully this can be resolved if ((items[i].name == 'fullscreen' || items[i].name == 'side-by-side') && isMobile()) continue; // Don't include trailing separators if (items[i] === '|') { var nonSeparatorIconsFollow = false; for (var x = (i + 1); x < items.length; x++) { if (items[x] !== '|' && (!self.options.hideIcons || self.options.hideIcons.indexOf(items[x].name) == -1)) { nonSeparatorIconsFollow = true; } } if (!nonSeparatorIconsFollow) continue; } // Create the icon and append to the toolbar (function (item) { var el; if (item === '|') { el = createSep(); } else { el = createIcon(item, self.options.toolbarTips, self.options.shortcuts); } // bind events, special for info if (item.action) { if (typeof item.action === 'function') { el.onclick = function (e) { e.preventDefault(); item.action(self); }; } else if (typeof item.action === 'string') { el.onclick = function (e) { e.preventDefault(); window.open(item.action, '_blank'); }; } } toolbarData[item.name || item] = el; bar.appendChild(el); })(items[i]); } self.toolbarElements = toolbarData; var cm = this.codemirror; cm.on('cursorActivity', function () { var stat = getState(cm); for (var key in toolbarData) { (function (key) { var el = toolbarData[key]; if (stat[key]) { el.className += ' active'; } else if (key != 'fullscreen' && key != 'side-by-side') { el.className = el.className.replace(/\s*active\s*/g, ''); } })(key); } }); var cmWrapper = cm.getWrapperElement(); cmWrapper.parentNode.insertBefore(bar, cmWrapper); return bar; }; EasyMDE.prototype.createStatusbar = function (status) { // Initialize status = status || this.options.status; var options = this.options; var cm = this.codemirror; // Make sure the status variable is valid if (!status || status.length === 0) return; // Set up the built-in items var items = []; var i, onUpdate, defaultValue; for (i = 0; i < status.length; i++) { // Reset some values onUpdate = undefined; defaultValue = undefined; // Handle if custom or not if (typeof status[i] === 'object') { items.push({ className: status[i].className, defaultValue: status[i].defaultValue, onUpdate: status[i].onUpdate, }); } else { var name = status[i]; if (name === 'words') { defaultValue = function (el) { el.innerHTML = wordCount(cm.getValue()); }; onUpdate = function (el) { el.innerHTML = wordCount(cm.getValue()); }; } else if (name === 'lines') { defaultValue = function (el) { el.innerHTML = cm.lineCount(); }; onUpdate = function (el) { el.innerHTML = cm.lineCount(); }; } else if (name === 'cursor') { defaultValue = function (el) { el.innerHTML = '0:0'; }; onUpdate = function (el) { var pos = cm.getCursor(); el.innerHTML = pos.line + ':' + pos.ch; }; } else if (name === 'autosave') { defaultValue = function (el) { if (options.autosave != undefined && options.autosave.enabled === true) { el.setAttribute('id', 'autosaved'); } }; } items.push({ className: name, defaultValue: defaultValue, onUpdate: onUpdate, }); } } // Create element for the status bar var bar = document.createElement('div'); bar.className = 'editor-statusbar'; // Create a new span for each item for (i = 0; i < items.length; i++) { // Store in temporary variable var item = items[i]; // Create span element var el = document.createElement('span'); el.className = item.className; // Ensure the defaultValue is a function if (typeof item.defaultValue === 'function') { item.defaultValue(el); } // Ensure the onUpdate is a function if (typeof item.onUpdate === 'function') { // Create a closure around the span of the current action, then execute the onUpdate handler this.codemirror.on('update', (function (el, item) { return function () { item.onUpdate(el); }; }(el, item))); } // Append the item to the status bar bar.appendChild(el); } // Insert the status bar into the DOM var cmWrapper = this.codemirror.getWrapperElement(); cmWrapper.parentNode.insertBefore(bar, cmWrapper.nextSibling); return bar; }; /** * Get or set the text content. */ EasyMDE.prototype.value = function (val) { var cm = this.codemirror; if (val === undefined) { return cm.getValue(); } else { cm.getDoc().setValue(val); if (this.isPreviewActive()) { var wrapper = cm.getWrapperElement(); var preview = wrapper.lastChild; preview.innerHTML = this.options.previewRender(val, preview); } return this; } }; /** * Bind static methods for exports. */ EasyMDE.toggleBold = toggleBold; EasyMDE.toggleItalic = toggleItalic; EasyMDE.toggleStrikethrough = toggleStrikethrough; EasyMDE.toggleBlockquote = toggleBlockquote; EasyMDE.toggleHeadingSmaller = toggleHeadingSmaller; EasyMDE.toggleHeadingBigger = toggleHeadingBigger; EasyMDE.toggleHeading1 = toggleHeading1; EasyMDE.toggleHeading2 = toggleHeading2; EasyMDE.toggleHeading3 = toggleHeading3; EasyMDE.toggleCodeBlock = toggleCodeBlock; EasyMDE.toggleUnorderedList = toggleUnorderedList; EasyMDE.toggleOrderedList = toggleOrderedList; EasyMDE.cleanBlock = cleanBlock; EasyMDE.drawLink = drawLink; EasyMDE.drawImage = drawImage; EasyMDE.drawTable = drawTable; EasyMDE.drawHorizontalRule = drawHorizontalRule; EasyMDE.undo = undo; EasyMDE.redo = redo; EasyMDE.togglePreview = togglePreview; EasyMDE.toggleSideBySide = toggleSideBySide; EasyMDE.toggleFullScreen = toggleFullScreen; /** * Bind instance methods for exports. */ EasyMDE.prototype.toggleBold = function () { toggleBold(this); }; EasyMDE.prototype.toggleItalic = function () { toggleItalic(this); }; EasyMDE.prototype.toggleStrikethrough = function () { toggleStrikethrough(this); }; EasyMDE.prototype.toggleBlockquote = function () { toggleBlockquote(this); }; EasyMDE.prototype.toggleHeadingSmaller = function () { toggleHeadingSmaller(this); }; EasyMDE.prototype.toggleHeadingBigger = function () { toggleHeadingBigger(this); }; EasyMDE.prototype.toggleHeading1 = function () { toggleHeading1(this); }; EasyMDE.prototype.toggleHeading2 = function () { toggleHeading2(this); }; EasyMDE.prototype.toggleHeading3 = function () { toggleHeading3(this); }; EasyMDE.prototype.toggleCodeBlock = function () { toggleCodeBlock(this); }; EasyMDE.prototype.toggleUnorderedList = function () { toggleUnorderedList(this); }; EasyMDE.prototype.toggleOrderedList = function () { toggleOrderedList(this); }; EasyMDE.prototype.cleanBlock = function () { cleanBlock(this); }; EasyMDE.prototype.drawLink = function () { drawLink(this); }; EasyMDE.prototype.drawImage = function () { drawImage(this); }; EasyMDE.prototype.drawTable = function () { drawTable(this); }; EasyMDE.prototype.drawHorizontalRule = function () { drawHorizontalRule(this); }; EasyMDE.prototype.undo = function () { undo(this); }; EasyMDE.prototype.redo = function () { redo(this); }; EasyMDE.prototype.togglePreview = function () { togglePreview(this); }; EasyMDE.prototype.toggleSideBySide = function () { toggleSideBySide(this); }; EasyMDE.prototype.toggleFullScreen = function () { toggleFullScreen(this); }; EasyMDE.prototype.isPreviewActive = function () { var cm = this.codemirror; var wrapper = cm.getWrapperElement(); var preview = wrapper.lastChild; return /editor-preview-active/.test(preview.className); }; EasyMDE.prototype.isSideBySideActive = function () { var cm = this.codemirror; var wrapper = cm.getWrapperElement(); var preview = wrapper.nextSibling; return /editor-preview-active-side/.test(preview.className); }; EasyMDE.prototype.isFullscreenActive = function () { var cm = this.codemirror; return cm.getOption('fullScreen'); }; EasyMDE.prototype.getState = function () { var cm = this.codemirror; return getState(cm); }; EasyMDE.prototype.toTextArea = function () { var cm = this.codemirror; var wrapper = cm.getWrapperElement(); if (wrapper.parentNode) { if (this.gui.toolbar) { wrapper.parentNode.removeChild(this.gui.toolbar); } if (this.gui.statusbar) { wrapper.parentNode.removeChild(this.gui.statusbar); } if (this.gui.sideBySide) { wrapper.parentNode.removeChild(this.gui.sideBySide); } } cm.toTextArea(); if (this.autosaveTimeoutId) { clearTimeout(this.autosaveTimeoutId); this.autosaveTimeoutId = undefined; this.clearAutosavedValue(); } }; module.exports = EasyMDE;