/** * Source: https://github.com/Ionaru/easy-markdown-editor/blob/master/src/js/easymde.js * Changes: * - Removal of line 1 to 15, awaiting https://github.com/Ionaru/easy-markdown-editor/pull/263 */ // 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, toggleHeading4: toggleHeading4, toggleHeading5: toggleHeading5, toggleHeading6: toggleHeading6, 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', toggleHeading1: 'Ctrl+Alt+1', toggleHeading2: 'Ctrl+Alt+2', toggleHeading3: 'Ctrl+Alt+3', toggleHeading4: 'Ctrl+Alt+4', toggleHeading5: 'Ctrl+Alt+5', toggleHeading6: 'Ctrl+Alt+6', 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 } /** * Modify HTML to remove the list-style when rendering checkboxes. * @param {string} htmlText - HTML to be modified. * @return {string} The modified HTML text. */ function removeListStyleWhenCheckbox(htmlText) { var parser = new DOMParser() var htmlDoc = parser.parseFromString(htmlText, 'text/html') var listItems = htmlDoc.getElementsByTagName('li') for (var i = 0; i < listItems.length; i++) { var listItem = listItems[i] for (var j = 0; j < listItem.children.length; j++) { var listItemChild = listItem.children[j] if ( listItemChild instanceof HTMLInputElement && listItemChild.type === 'checkbox' ) { // From Github: margin: 0 .2em .25em -1.6em; listItem.style.marginLeft = '-1.5em' listItem.style.listStyleType = 'none' } } } return htmlDoc.documentElement.innerHTML } /** * 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 dropdown block */ function createToolbarDropdown(options, enableTooltips, shortcuts, parent) { var el = createToolbarButton( options, false, enableTooltips, shortcuts, 'button', parent, ) el.classList.add('easymde-dropdown') el.onclick = function () { el.focus() } var content = document.createElement('div') content.className = 'easymde-dropdown-content' for ( var childrenIndex = 0; childrenIndex < options.children.length; childrenIndex++ ) { var child = options.children[childrenIndex] var childElement if (typeof child === 'string' && child in toolbarBuiltInButtons) { childElement = createToolbarButton( toolbarBuiltInButtons[child], true, enableTooltips, shortcuts, 'button', parent, ) } else { childElement = createToolbarButton( child, true, enableTooltips, shortcuts, 'button', parent, ) } childElement.addEventListener( 'click', function (e) { e.stopPropagation() }, false, ) content.appendChild(childElement) } el.appendChild(content) return el } /** * Create button element for toolbar. */ function createToolbarButton( options, enableActions, enableTooltips, shortcuts, markup, parent, ) { options = options || {} var el = document.createElement(markup) // Add 'custom' attributes as early as possible, so that 'official' attributes will never be overwritten. if (options.attributes) { for (var attribute in options.attributes) { if ( Object.prototype.hasOwnProperty.call( options.attributes, attribute, ) ) { el.setAttribute(attribute, options.attributes[attribute]) } } } el.className = options.name el.setAttribute('type', markup) enableTooltips = enableTooltips == undefined ? true : enableTooltips if (options.text) { el.innerText = options.text } // Properly handle custom shortcuts if (options.name && options.name in shortcuts) { bindings[options.name] = options.action } 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.title) { el.setAttribute('aria-label', options.title) } if (options.noDisable) { el.classList.add('no-disable') } if (options.noMobile) { el.classList.add('no-mobile') } // Prevent errors if there is no class name in custom options var classNameParts = [] if (typeof options.className !== 'undefined') { classNameParts = options.className.split(' ') } // Provide backwards compatibility with simple-markdown-editor by adding custom classes to the button. var iconClasses = [] for ( var classNameIndex = 0; classNameIndex < classNameParts.length; classNameIndex++ ) { var classNamePart = classNameParts[classNameIndex] // Split icon classes from the button. // Regex will detect "fa", "fas", "fa-something" and "fa-some-icon-1", but not "fanfare". if (classNamePart.match(/^fa([srlb]|(-[\w-]*)|$)/)) { iconClasses.push(classNamePart) } else { el.classList.add(classNamePart) } } el.tabIndex = -1 if (iconClasses.length > 0) { // Create icon element and append as a child to the button var icon = document.createElement('i') for ( var iconClassIndex = 0; iconClassIndex < iconClasses.length; iconClassIndex++ ) { var iconClass = iconClasses[iconClassIndex] icon.classList.add(iconClass) } el.appendChild(icon) } // If there is a custom icon markup set, use that if (typeof options.icon !== 'undefined') { el.innerHTML = options.icon } if (options.action && enableActions) { if (typeof options.action === 'function') { el.onclick = function (e) { e.preventDefault() options.action(parent) } } else if (typeof options.action === 'string') { el.onclick = function (e) { e.preventDefault() window.open(options.action, '_blank') } } } 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.image) { ret.link = true } else if (data === 'image') { 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. * @param {EasyMDE} 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 } var wrapper = cm.getWrapperElement() var sidebyside = wrapper.nextSibling if (sidebyside.classList.contains('editor-preview-active-side')) { if (editor.options.sideBySideFullscreen === false) { // if side-by-side not-fullscreen ok, apply classes as needed var easyMDEContainer = wrapper.parentNode if (cm.getOption('fullScreen')) { easyMDEContainer.classList.remove('sided--no-fullscreen') } else { easyMDEContainer.classList.add('sided--no-fullscreen') } } else { toggleSideBySide(editor) } } if (editor.options.onToggleFullScreen) { editor.options.onToggleFullScreen(cm.getOption('fullScreen') || false) } // Remove or set maxHeight if (typeof editor.options.maxHeight !== 'undefined') { if (cm.getOption('fullScreen')) { cm.getScrollerElement().style.removeProperty('height') sidebyside.style.removeProperty('height') } else { cm.getScrollerElement().style.height = editor.options.maxHeight editor.setPreviewMaxHeight() } } // Update toolbar class editor.toolbar_div.classList.toggle('fullscreen') // Update toolbar button if (editor.toolbarElements && editor.toolbarElements.fullscreen) { var toolbarButton = editor.toolbarElements.fullscreen toolbarButton.classList.toggle('active') } } /** * Action for toggling bold. * @param {EasyMDE} editor */ function toggleBold(editor) { _toggleBlock(editor, 'bold', editor.options.blockStyles.bold) } /** * Action for toggling italic. * @param {EasyMDE} editor */ function toggleItalic(editor) { _toggleBlock(editor, 'italic', editor.options.blockStyles.italic) } /** * Action for toggling strikethrough. * @param {EasyMDE} editor */ function toggleStrikethrough(editor) { _toggleBlock(editor, 'strikethrough', '~~') } /** * Action for toggling code block. * @param {EasyMDE} editor */ 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) { _toggleLine(editor.codemirror, 'quote') } /** * Action for toggling heading size: normal -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> normal */ function toggleHeadingSmaller(editor) { _toggleHeading(editor.codemirror, 'smaller') } /** * Action for toggling heading size: normal -> h6 -> h5 -> h4 -> h3 -> h2 -> h1 -> normal */ function toggleHeadingBigger(editor) { _toggleHeading(editor.codemirror, 'bigger') } /** * Action for toggling heading size 1 */ function toggleHeading1(editor) { _toggleHeading(editor.codemirror, undefined, 1) } /** * Action for toggling heading size 2 */ function toggleHeading2(editor) { _toggleHeading(editor.codemirror, undefined, 2) } /** * Action for toggling heading size 3 */ function toggleHeading3(editor) { _toggleHeading(editor.codemirror, undefined, 3) } /** * Action for toggling heading size 4 */ function toggleHeading4(editor) { _toggleHeading(editor.codemirror, undefined, 4) } /** * Action for toggling heading size 5 */ function toggleHeading5(editor) { _toggleHeading(editor.codemirror, undefined, 5) } /** * Action for toggling heading size 6 */ function toggleHeading6(editor) { _toggleHeading(editor.codemirror, undefined, 6) } /** * Action for toggling ul. */ function toggleUnorderedList(editor) { var cm = editor.codemirror var listStyle = '*' // Default if (['-', '+', '*'].includes(editor.options.unorderedListStyle)) { listStyle = editor.options.unorderedListStyle } _toggleLine(cm, 'unordered-list', listStyle) } /** * Action for toggling ol. */ function toggleOrderedList(editor) { _toggleLine(editor.codemirror, 'ordered-list') } /** * Action for clean block (remove headline, list, blockquote code, markers) */ function cleanBlock(editor) { _cleanBlock(editor.codemirror) } /** * Action for drawing a link. * @param {EasyMDE} editor */ function drawLink(editor) { var options = editor.options var url = 'https://' if (options.promptURLs) { var result = prompt(options.promptTexts.link, url) if (!result) { return false } url = escapePromptURL(result) } _toggleLink(editor, 'link', options.insertTexts.link, url) } /** * Action for drawing an img. * @param {EasyMDE} editor */ function drawImage(editor) { var options = editor.options var url = 'https://' if (options.promptURLs) { var result = prompt(options.promptTexts.image, url) if (!result) { return false } url = escapePromptURL(result) } _toggleLink(editor, 'image', options.insertTexts.image, url) } /** * Encode and escape URLs to prevent breaking up rendered Markdown links. * @param {string} url The url of the link or image */ function escapePromptURL(url) { return encodeURI(url).replace(/([\\()])/g, '\\$1') } /** * Action for opening the browse-file window to upload an image to a server. * @param {EasyMDE} editor The EasyMDE object */ function drawUploadedImage(editor) { // TODO: Draw the image template with a fake url? ie: '![](importing foo.png...)' editor.openBrowseFileWindow() } /** * Action executed after an image have been successfully imported on the server. * @param {EasyMDE} editor The EasyMDE object * @param {string} url The url of the uploaded image */ function afterImageUploaded(editor, url) { var cm = editor.codemirror var stat = getState(cm) var options = editor.options var imageName = url.substr(url.lastIndexOf('/') + 1) var ext = imageName .substring(imageName.lastIndexOf('.') + 1) .replace(/\?.*$/, '') .toLowerCase() // Check if media is an image if ( ['png', 'jpg', 'jpeg', 'gif', 'svg', 'apng', 'avif', 'webp'].includes( ext, ) ) { _replaceSelection( cm, stat.image, options.insertTexts.uploadedImage, url, ) } else { var text_link = options.insertTexts.link text_link[0] = '[' + imageName _replaceSelection(cm, stat.link, text_link, url) } // show uploaded image filename for 1000ms editor.updateStatusBar( 'upload-image', editor.options.imageTexts.sbOnUploaded.replace( '#image_name#', imageName, ), ) setTimeout(function () { editor.updateStatusBar('upload-image', editor.options.imageTexts.sbInit) }, 1000) } /** * Action for drawing a table. * @param {EasyMDE} editor */ 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. * @param {EasyMDE} editor */ function drawHorizontalRule(editor) { var cm = editor.codemirror var stat = getState(cm) var options = editor.options _replaceSelection(cm, stat.image, options.insertTexts.horizontalRule) } /** * Undo action. * @param {EasyMDE} editor */ function undo(editor) { var cm = editor.codemirror cm.undo() cm.focus() } /** * Redo action. * @param {EasyMDE} editor */ function redo(editor) { var cm = editor.codemirror cm.redo() cm.focus() } /** * Toggle side by side preview * @param {EasyMDE} editor */ 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 var easyMDEContainer = wrapper.parentNode if (preview.classList.contains('editor-preview-active-side')) { if (editor.options.sideBySideFullscreen === false) { // if side-by-side not-fullscreen ok, remove classes when hiding side easyMDEContainer.classList.remove('sided--no-fullscreen') } preview.classList.remove('editor-preview-active-side') if (toolbarButton) toolbarButton.classList.remove('active') wrapper.classList.remove('CodeMirror-sided') } 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')) { if (editor.options.sideBySideFullscreen === false) { // if side-by-side not-fullscreen ok, add classes when not fullscreen and showing side easyMDEContainer.classList.add('sided--no-fullscreen') } else { toggleFullScreen(editor) } } preview.classList.add('editor-preview-active-side') }, 1) if (toolbarButton) toolbarButton.classList.add('active') wrapper.classList.add('CodeMirror-sided') useSideBySideListener = true } // Hide normal preview if active var previewNormal = wrapper.lastChild if (previewNormal.classList.contains('editor-preview-active')) { previewNormal.classList.remove('editor-preview-active') var toolbar = editor.toolbarElements.preview var toolbar_div = editor.toolbar_div toolbar.classList.remove('active') toolbar_div.classList.remove('disabled-for-preview') } var sideBySideRenderingFunction = function () { var newValue = editor.options.previewRender(editor.value(), preview) if (newValue != null) { preview.innerHTML = newValue } } if (!cm.sideBySideRenderingFunction) { cm.sideBySideRenderingFunction = sideBySideRenderingFunction } if (useSideBySideListener) { var newValue = editor.options.previewRender(editor.value(), preview) if (newValue != null) { preview.innerHTML = newValue } cm.on('update', cm.sideBySideRenderingFunction) } else { cm.off('update', cm.sideBySideRenderingFunction) } // Refresh to fix selection being off (#309) cm.refresh() } /** * Preview action. * @param {EasyMDE} editor */ function togglePreview(editor) { var cm = editor.codemirror var wrapper = cm.getWrapperElement() var toolbar_div = editor.toolbar_div var toolbar = editor.options.toolbar ? editor.toolbarElements.preview : false var preview = wrapper.lastChild // Turn off side by side if needed var sidebyside = cm.getWrapperElement().nextSibling if (sidebyside.classList.contains('editor-preview-active-side')) toggleSideBySide(editor) if (!preview || !preview.classList.contains('editor-preview-full')) { preview = document.createElement('div') preview.className = 'editor-preview-full' if (editor.options.previewClass) { if (Array.isArray(editor.options.previewClass)) { for (var i = 0; i < editor.options.previewClass.length; i++) { preview.classList.add(editor.options.previewClass[i]) } } else if (typeof editor.options.previewClass === 'string') { preview.classList.add(editor.options.previewClass) } } wrapper.appendChild(preview) } if (preview.classList.contains('editor-preview-active')) { preview.classList.remove('editor-preview-active') if (toolbar) { toolbar.classList.remove('active') toolbar_div.classList.remove('disabled-for-preview') } } 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.classList.add('editor-preview-active') }, 1) if (toolbar) { toolbar.classList.add('active') toolbar_div.classList.add('disabled-for-preview') } } var preview_result = editor.options.previewRender(editor.value(), preview) if (preview_result !== null) { preview.innerHTML = preview_result } } function _replaceSelection(cm, active, startEnd, url) { if ( cm .getWrapperElement() .lastChild.classList.contains('editor-preview-active') ) 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) { start = start.replace('#url#', url) // url is in start for upload-image 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 ( cm .getWrapperElement() .lastChild.classList.contains('editor-preview-active') ) 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 (currHeadingLevel <= 0) { text = '#'.repeat(size) + ' ' + text } else if (currHeadingLevel == size) { text = text.substr(currHeadingLevel + 1) } else { text = '#'.repeat(size) + ' ' + text.substr(currHeadingLevel + 1) } } cm.replaceRange( text, { line: i, ch: 0, }, { line: i, ch: 99999999999999, }, ) })(i) } cm.focus() } function _toggleLine(cm, name, liststyle) { if ( cm .getWrapperElement() .lastChild.classList.contains('editor-preview-active') ) 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': liststyle, 'ordered-list': '%%i.', } return map[name].replace('%%i', i) } var _checkChar = function (name, char) { var map = { quote: '>', 'unordered-list': '\\' + liststyle, 'ordered-list': '\\d+.', } var rt = new RegExp(map[name]) return char && rt.test(char) } var _toggle = function (name, text, untoggleOnly) { 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 if (untoggleOnly == false) { text = char + ' ' + text } return text } 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 { // If we're toggling unordered-list formatting, check if the current line // is part of an ordered-list, and if so, untoggle that first. // Workaround for https://github.com/Ionaru/easy-markdown-editor/issues/92 if (name == 'unordered-list') { text = _toggle('ordered-list', text, true) } text = _toggle(name, text, false) line += 1 } cm.replaceRange( text, { line: i, ch: 0, }, { line: i, ch: 99999999999999, }, ) })(i) } cm.focus() } /** * @param {EasyMDE} editor * @param {'link' | 'image'} type * @param {string} startEnd * @param {string} url */ function _toggleLink(editor, type, startEnd, url) { if (!editor.codemirror || editor.isPreviewActive()) { return } var cm = editor.codemirror var stat = getState(cm) var active = stat[type] if (!active) { _replaceSelection(cm, active, startEnd, url) return } var startPoint = cm.getCursor('start') var endPoint = cm.getCursor('end') var text = cm.getLine(startPoint.line) var start = text.slice(0, startPoint.ch) var end = text.slice(startPoint.ch) if (type == 'link') { start = start.replace(/(.*)[^!]\[/, '$1') } else if (type == 'image') { start = start.replace(/(.*)!\[$/, '$1') } end = end.replace(/]\(.*?\)/, '') cm.replaceRange( start + end, { line: startPoint.line, ch: 0, }, { line: startPoint.line, ch: 99999999999999, }, ) startPoint.ch -= startEnd[0].length if (startPoint !== endPoint) { endPoint.ch -= startEnd[0].length } cm.setSelection(startPoint, endPoint) cm.focus() } /** * @param {EasyMDE} editor */ function _toggleBlock(editor, type, start_chars, end_chars) { if (!editor.codemirror || editor.isPreviewActive()) { 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 ( cm .getWrapperElement() .lastChild.classList.contains('editor-preview-active') ) 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, }, ) } } /** * Convert a number of bytes to a human-readable file size. If you desire * to add a space between the value and the unit, you need to add this space * to the given units. * @param bytes {number} A number of bytes, as integer. Ex: 421137 * @param units {number[]} An array of human-readable units, ie. [' B', ' K', ' MB'] * @returns string A human-readable file size. Ex: '412 KB' */ function humanFileSize(bytes, units) { if (Math.abs(bytes) < 1024) { return '' + bytes + units[0] } var u = 0 do { bytes /= 1024 ++u } while (Math.abs(bytes) >= 1024 && u < units.length) return '' + bytes.toFixed(1) + units[u] } // Merge the properties of one object into another. function _mergeProperties(target, source) { for (var property in source) { if (Object.prototype.hasOwnProperty.call(source, 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 iconClassMap = { bold: 'fa fa-bold', italic: 'fa fa-italic', strikethrough: 'fa fa-strikethrough', heading: 'fa fa-header fa-heading', 'heading-smaller': 'fa fa-header fa-heading header-smaller', 'heading-bigger': 'fa fa-header fa-heading header-bigger', 'heading-1': 'fa fa-header fa-heading header-1', 'heading-2': 'fa fa-header fa-heading header-2', 'heading-3': 'fa fa-header fa-heading header-3', code: 'fa fa-code', quote: 'fa fa-quote-left', 'ordered-list': 'fa fa-list-ol', 'unordered-list': 'fa fa-list-ul', 'clean-block': 'fa fa-eraser', link: 'fa fa-link', image: 'fa fa-image', 'upload-image': 'fa fa-image', table: 'fa fa-table', 'horizontal-rule': 'fa fa-minus', preview: 'fa fa-eye', 'side-by-side': 'fa fa-columns', fullscreen: 'fa fa-arrows-alt', guide: 'fa fa-question-circle', undo: 'fa fa-undo', redo: 'fa fa-repeat fa-redo', } var toolbarBuiltInButtons = { bold: { name: 'bold', action: toggleBold, className: iconClassMap['bold'], title: 'Bold', default: true, }, italic: { name: 'italic', action: toggleItalic, className: iconClassMap['italic'], title: 'Italic', default: true, }, strikethrough: { name: 'strikethrough', action: toggleStrikethrough, className: iconClassMap['strikethrough'], title: 'Strikethrough', }, heading: { name: 'heading', action: toggleHeadingSmaller, className: iconClassMap['heading'], title: 'Heading', default: true, }, 'heading-smaller': { name: 'heading-smaller', action: toggleHeadingSmaller, className: iconClassMap['heading-smaller'], title: 'Smaller Heading', }, 'heading-bigger': { name: 'heading-bigger', action: toggleHeadingBigger, className: iconClassMap['heading-bigger'], title: 'Bigger Heading', }, 'heading-1': { name: 'heading-1', action: toggleHeading1, className: iconClassMap['heading-1'], title: 'Big Heading', }, 'heading-2': { name: 'heading-2', action: toggleHeading2, className: iconClassMap['heading-2'], title: 'Medium Heading', }, 'heading-3': { name: 'heading-3', action: toggleHeading3, className: iconClassMap['heading-3'], title: 'Small Heading', }, 'separator-1': { name: 'separator-1', }, code: { name: 'code', action: toggleCodeBlock, className: iconClassMap['code'], title: 'Code', }, quote: { name: 'quote', action: toggleBlockquote, className: iconClassMap['quote'], title: 'Quote', default: true, }, 'unordered-list': { name: 'unordered-list', action: toggleUnorderedList, className: iconClassMap['unordered-list'], title: 'Generic List', default: true, }, 'ordered-list': { name: 'ordered-list', action: toggleOrderedList, className: iconClassMap['ordered-list'], title: 'Numbered List', default: true, }, 'clean-block': { name: 'clean-block', action: cleanBlock, className: iconClassMap['clean-block'], title: 'Clean block', }, 'separator-2': { name: 'separator-2', }, link: { name: 'link', action: drawLink, className: iconClassMap['link'], title: 'Create Link', default: true, }, image: { name: 'image', action: drawImage, className: iconClassMap['image'], title: 'Insert Image', default: true, }, 'upload-image': { name: 'upload-image', action: drawUploadedImage, className: iconClassMap['upload-image'], title: 'Import an image', }, table: { name: 'table', action: drawTable, className: iconClassMap['table'], title: 'Insert Table', }, 'horizontal-rule': { name: 'horizontal-rule', action: drawHorizontalRule, className: iconClassMap['horizontal-rule'], title: 'Insert Horizontal Line', }, 'separator-3': { name: 'separator-3', }, preview: { name: 'preview', action: togglePreview, className: iconClassMap['preview'], noDisable: true, title: 'Toggle Preview', default: true, }, 'side-by-side': { name: 'side-by-side', action: toggleSideBySide, className: iconClassMap['side-by-side'], noDisable: true, noMobile: true, title: 'Toggle Side by Side', default: true, }, fullscreen: { name: 'fullscreen', action: toggleFullScreen, className: iconClassMap['fullscreen'], 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: iconClassMap['guide'], noDisable: true, title: 'Markdown Guide', default: true, }, 'separator-5': { name: 'separator-5', }, undo: { name: 'undo', action: undo, className: iconClassMap['undo'], noDisable: true, title: 'Undo', }, redo: { name: 'redo', action: redo, className: iconClassMap['redo'], noDisable: true, title: 'Redo', }, } var insertTexts = { link: ['[', '](#url#)'], image: ['![', '](#url#)'], uploadedImage: ['![](#url#)', ''], // uploadedImage: ['![](#url#)\n', ''], // TODO: New line insertion doesn't work here. 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 timeFormat = { locale: 'en-US', format: { hour: '2-digit', minute: '2-digit', }, } var blockStyles = { bold: '**', code: '```', italic: '*', } /** * Texts displayed to the user (mainly on the status bar) for the import image * feature. Can be used for customization or internationalization. */ var imageTexts = { sbInit: 'Attach files by drag and dropping or pasting from clipboard.', sbOnDragEnter: 'Drop image to upload it.', sbOnDrop: 'Uploading image #images_names#...', sbProgress: 'Uploading #file_name#: #progress#%', sbOnUploaded: 'Uploaded #image_name#', sizeUnits: ' B, KB, MB', } /** * Errors displayed to the user, using the `errorCallback` option. Can be used for * customization or internationalization. */ var errorMessages = { noFileGiven: 'You must select a file.', typeNotAllowed: 'This image type is not allowed.', fileTooLarge: 'Image #image_name# is too big (#image_size#).\n' + 'Maximum file size is #image_max_size#.', importError: 'Something went wrong when uploading the image #image_name#.', } /** * 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 ( Object.prototype.hasOwnProperty.call(toolbarBuiltInButtons, 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) } } } } // Editor preview styling class. if (!Object.prototype.hasOwnProperty.call(options, 'previewClass')) { options.previewClass = 'editor-preview' } // Handle status bar if (!Object.prototype.hasOwnProperty.call(options, 'status')) { options.status = ['autosave', 'lines', 'words', 'cursor'] if (options.uploadImage) { options.status.unshift('upload-image') } } // 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 || {}) if (options.autosave != undefined) { // Merging the Autosave timeFormat, with the given options options.autosave.timeFormat = extend( {}, timeFormat, options.autosave.timeFormat || {}, ) } options.iconClassMap = extend({}, iconClassMap, options.iconClassMap || {}) // Merging the shortcuts, with the given options options.shortcuts = extend({}, shortcuts, options.shortcuts || {}) options.maxHeight = options.maxHeight || undefined options.direction = options.direction || 'ltr' if (typeof options.maxHeight !== 'undefined') { // Min and max height are equal if maxHeight is set options.minHeight = options.maxHeight } else { options.minHeight = options.minHeight || '300px' } options.errorCallback = options.errorCallback || function (errorMessage) { alert(errorMessage) } // Import-image default configuration options.uploadImage = options.uploadImage || false options.imageMaxSize = options.imageMaxSize || 2097152 // 1024 * 1024 * 2 options.imageAccept = options.imageAccept || 'image/png, image/jpeg, image/gif, image/avif' options.imageTexts = extend({}, imageTexts, options.imageTexts || {}) options.errorMessages = extend( {}, errorMessages, options.errorMessages || {}, ) options.imagePathAbsolute = options.imagePathAbsolute || false options.imageCSRFName = options.imageCSRFName || 'csrfmiddlewaretoken' options.imageCSRFHeader = options.imageCSRFHeader || false // 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 // If overlay mode is specified and combine is not provided, default it to true if (options.overlayMode && options.overlayMode.combine === undefined) { options.overlayMode.combine = true } // 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) } if (options.uploadImage) { var self = this this.codemirror.on('dragenter', function (cm, event) { self.updateStatusBar( 'upload-image', self.options.imageTexts.sbOnDragEnter, ) event.stopPropagation() event.preventDefault() }) this.codemirror.on('dragend', function (cm, event) { self.updateStatusBar('upload-image', self.options.imageTexts.sbInit) event.stopPropagation() event.preventDefault() }) this.codemirror.on('dragleave', function (cm, event) { self.updateStatusBar('upload-image', self.options.imageTexts.sbInit) event.stopPropagation() event.preventDefault() }) this.codemirror.on('dragover', function (cm, event) { self.updateStatusBar( 'upload-image', self.options.imageTexts.sbOnDragEnter, ) event.stopPropagation() event.preventDefault() }) this.codemirror.on('drop', function (cm, event) { event.stopPropagation() event.preventDefault() if (options.imageUploadFunction) { self.uploadImagesUsingCustomFunction( options.imageUploadFunction, event.dataTransfer.files, ) } else { self.uploadImages(event.dataTransfer.files) } }) this.codemirror.on('paste', function (cm, event) { if (options.imageUploadFunction) { self.uploadImagesUsingCustomFunction( options.imageUploadFunction, event.clipboardData.files, ) } else { self.uploadImages(event.clipboardData.files) } }) } } /** * Upload asynchronously a list of images to a server. * * Can be triggered by: * - drag&drop; * - copy-paste; * - the browse-file window (opened when the user clicks on the *upload-image* icon). * @param {FileList} files The files to upload the the server. * @param [onSuccess] {function} see EasyMDE.prototype.uploadImage * @param [onError] {function} see EasyMDE.prototype.uploadImage */ EasyMDE.prototype.uploadImages = function (files, onSuccess, onError) { if (files.length === 0) { return } var names = [] for (var i = 0; i < files.length; i++) { names.push(files[i].name) this.uploadImage(files[i], onSuccess, onError) } this.updateStatusBar( 'upload-image', this.options.imageTexts.sbOnDrop.replace( '#images_names#', names.join(', '), ), ) } /** * Upload asynchronously a list of images to a server. * * Can be triggered by: * - drag&drop; * - copy-paste; * - the browse-file window (opened when the user clicks on the *upload-image* icon). * @param imageUploadFunction {Function} The custom function to upload the image passed in options. * @param {FileList} files The files to upload the the server. */ EasyMDE.prototype.uploadImagesUsingCustomFunction = function ( imageUploadFunction, files, ) { if (files.length === 0) { return } var names = [] for (var i = 0; i < files.length; i++) { names.push(files[i].name) this.uploadImageUsingCustomFunction(imageUploadFunction, files[i]) } this.updateStatusBar( 'upload-image', this.options.imageTexts.sbOnDrop.replace( '#images_names#', names.join(', '), ), ) } /** * Update an item in the status bar. * @param itemName {string} The name of the item to update (ie. 'upload-image', 'autosave', etc.). * @param content {string} the new content of the item to write in the status bar. */ EasyMDE.prototype.updateStatusBar = function (itemName, content) { if (!this.gui.statusbar) { return } var matchingClasses = this.gui.statusbar.getElementsByClassName(itemName) if (matchingClasses.length === 1) { this.gui.statusbar.getElementsByClassName(itemName)[0].textContent = content } else if (matchingClasses.length === 0) { console.log('EasyMDE: status bar item ' + itemName + ' was not found.') } else { console.log( 'EasyMDE: Several status bar items named ' + itemName + ' was found.', ) } } /** * 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, language) { if (language && hljs.getLanguage(language)) { return hljs.highlight(language, code).value } else { return hljs.highlightAuto(code).value } } } } // Set options marked.setOptions(markedOptions) // Convert the markdown to HTML var htmlText = marked.parse(text) // Sanitize HTML if ( this.options.renderingConfig && typeof this.options.renderingConfig.sanitizerFunction === 'function' ) { htmlText = this.options.renderingConfig.sanitizerFunction.call( this, htmlText, ) } // Edit the HTML anchors to add 'target="_blank"' by default. htmlText = addAnchorTargetBlank(htmlText) // Remove list-style when rendering checkboxes htmlText = removeListStyleWhenCheckbox(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 () { var action = bindings[key] if (typeof action === 'function') { action(self) } else if (typeof action === 'string') { window.open(action, '_blank') } } })(key) } } keyMaps['Enter'] = 'newlineAndIndentContinueMarkdownList' keyMaps['Tab'] = 'tabAndIndentMarkdownList' keyMaps['Shift-Tab'] = 'shiftTabAndUnindentMarkdownList' keyMaps['Esc'] = function (cm) { if (cm.getOption('fullScreen')) toggleFullScreen(self) } this.documentOnKeyDown = function (e) { e = e || window.event if (e.keyCode == 27) { if (self.codemirror.getOption('fullScreen')) toggleFullScreen(self) } } document.addEventListener('keydown', this.documentOnKeyDown, false) var mode, backdrop // CodeMirror overlay mode if (options.overlayMode) { CodeMirror.defineMode('overlay-mode', function (config) { return CodeMirror.overlayMode( CodeMirror.getMode( config, options.spellChecker !== false ? 'spell-checker' : 'gfm', ), options.overlayMode.mode, options.overlayMode.combine, ) }) mode = 'overlay-mode' backdrop = options.parsingConfig backdrop.gitHubSpice = false } else { mode = options.parsingConfig mode.name = 'gfm' mode.gitHubSpice = false } if (options.spellChecker !== false) { mode = 'spell-checker' backdrop = options.parsingConfig backdrop.name = 'gfm' backdrop.gitHubSpice = false if (typeof options.spellChecker === 'function') { options.spellChecker({ codeMirrorInstance: CodeMirror, }) } else { CodeMirrorSpellChecker({ codeMirrorInstance: CodeMirror, }) } } // eslint-disable-next-line no-unused-vars function configureMouse(cm, repeat, event) { return { addNew: false, } } CodeMirror.getMode('php').mime = 'text/x-php' 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: options.lineNumbers === true ? true : false, autofocus: options.autofocus === true ? true : false, extraKeys: keyMaps, direction: options.direction, lineWrapping: options.lineWrapping === false ? false : true, allowDropFileTypes: ['text/plain'], placeholder: options.placeholder || el.getAttribute('placeholder') || '', styleSelectedText: options.styleSelectedText != undefined ? options.styleSelectedText : !isMobile(), scrollbarStyle: options.scrollbarStyle != undefined ? options.scrollbarStyle : 'native', configureMouse: configureMouse, inputStyle: options.inputStyle != undefined ? options.inputStyle : isMobile() ? 'contenteditable' : 'textarea', spellcheck: options.nativeSpellcheck != undefined ? options.nativeSpellcheck : true, autoRefresh: options.autoRefresh != undefined ? options.autoRefresh : false, }) this.codemirror.getScrollerElement().style.minHeight = options.minHeight if (typeof options.maxHeight !== 'undefined') { this.codemirror.getScrollerElement().style.height = options.maxHeight } if (options.forceSync === true) { var cm = this.codemirror cm.on('change', function () { cm.save() }) } this.gui = {} // Wrap Codemirror with container before create toolbar, etc, // to use with sideBySideFullscreen option. var easyMDEContainer = document.createElement('div') easyMDEContainer.classList.add('EasyMDEContainer') easyMDEContainer.setAttribute('role', 'application') var cmWrapper = this.codemirror.getWrapperElement() cmWrapper.parentNode.insertBefore(easyMDEContainer, cmWrapper) easyMDEContainer.appendChild(cmWrapper) 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() // use to load localstorage content this.codemirror.on('change', function () { clearTimeout(self._autosave_timeout) self._autosave_timeout = setTimeout( function () { self.autosave() }, self.options.autosave.submit_delay || self.options.autosave.delay || 1000, ) }) } function calcHeight(naturalWidth, naturalHeight) { var height var viewportWidth = window .getComputedStyle(document.querySelector('.CodeMirror-sizer')) .width.replace('px', '') if (naturalWidth < viewportWidth) { height = naturalHeight + 'px' } else { height = (naturalHeight / naturalWidth) * 100 + '%' } return height } var _vm = this function assignImageBlockAttributes(parentEl, img) { parentEl.setAttribute('data-img-src', img.url) parentEl.setAttribute( 'style', '--bg-image:url(' + img.url + ');--width:' + img.naturalWidth + 'px;--height:' + calcHeight(img.naturalWidth, img.naturalHeight), ) _vm.codemirror.setSize() } function handleImages() { if (!options.previewImagesInEditor) { return } easyMDEContainer .querySelectorAll('.cm-image-marker') .forEach(function (e) { var parentEl = e.parentElement if (!parentEl.innerText.match(/^!\[.*?\]\(.*\)/g)) { // if img pasted on the same line with other text, don't preview, preview only images on separate line return } if (!parentEl.hasAttribute('data-img-src')) { var srcAttr = parentEl.innerText.match('\\((.*)\\)') // might require better parsing according to markdown spec if (!window.EMDEimagesCache) { window.EMDEimagesCache = {} } if (srcAttr && srcAttr.length >= 2) { var keySrc = srcAttr[1] if (options.imagesPreviewHandler) { var newSrc = options.imagesPreviewHandler( srcAttr[1], ) // defensive check making sure the handler provided by the user returns a string if (typeof newSrc === 'string') { keySrc = newSrc } } if (!window.EMDEimagesCache[keySrc]) { var img = document.createElement('img') img.onload = function () { window.EMDEimagesCache[keySrc] = { naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight, url: keySrc, } assignImageBlockAttributes( parentEl, window.EMDEimagesCache[keySrc], ) } img.src = keySrc } else { assignImageBlockAttributes( parentEl, window.EMDEimagesCache[keySrc], ) } } } }) } this.codemirror.on('update', function () { handleImages() }) this.gui.sideBySide = this.createSideBySide() this._rendered = this.element if (options.autofocus === true || el.autofocus) { this.codemirror.focus() } // Fixes CodeMirror bug (#344) var temp_cm = this.codemirror setTimeout( function () { temp_cm.refresh() }.bind(temp_cm), 0, ) } EasyMDE.prototype.cleanup = function () { document.removeEventListener('keydown', this.documentOnKeyDown) } // 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, ) }) } 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 } var value = easyMDE.value() if (value !== '') { localStorage.setItem( 'smde_' + this.options.autosave.uniqueId, value, ) } else { localStorage.removeItem('smde_' + this.options.autosave.uniqueId) } var el = document.getElementById('autosaved') if (el != null && el != undefined && el != '') { var d = new Date() var dd = new Intl.DateTimeFormat( [this.options.autosave.timeFormat.locale, 'en-US'], this.options.autosave.timeFormat.format, ).format(d) var save = this.options.autosave.text == undefined ? 'Autosaved: ' : this.options.autosave.text el.innerHTML = save + dd } } 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') } } /** * Open the browse-file window to upload an image to a server. * @param [onSuccess] {function} see EasyMDE.prototype.uploadImage * @param [onError] {function} see EasyMDE.prototype.uploadImage */ EasyMDE.prototype.openBrowseFileWindow = function (onSuccess, onError) { var self = this var imageInput = this.gui.toolbar.getElementsByClassName('imageInput')[0] imageInput.click() //dispatchEvent(new MouseEvent('click')); // replaced with click() for IE11 compatibility. function onChange(event) { if (self.options.imageUploadFunction) { self.uploadImagesUsingCustomFunction( self.options.imageUploadFunction, event.target.files, ) } else { self.uploadImages(event.target.files, onSuccess, onError) } imageInput.removeEventListener('change', onChange) } imageInput.addEventListener('change', onChange) } /** * Upload an image to the server. * * @param file {File} The image to upload, as a HTML5 File object (https://developer.mozilla.org/en-US/docs/Web/API/File) * @param [onSuccess] {function} A callback function to execute after the image has been successfully uploaded, with one parameter: * - url (string): The URL of the uploaded image. * @param [onError] {function} A callback function to execute when the image upload fails, with one parameter: * - error (string): the detailed error to display to the user (based on messages from options.errorMessages). */ EasyMDE.prototype.uploadImage = function (file, onSuccess, onError) { var self = this onSuccess = onSuccess || function onSuccess(imageUrl) { afterImageUploaded(self, imageUrl) } function onErrorSup(errorMessage) { // show error on status bar and reset after 10000ms self.updateStatusBar('upload-image', errorMessage) setTimeout(function () { self.updateStatusBar('upload-image', self.options.imageTexts.sbInit) }, 10000) // run custom error handler if (onError && typeof onError === 'function') { onError(errorMessage) } // run error handler from options, this alerts the message. self.options.errorCallback(errorMessage) } function fillErrorMessage(errorMessage) { var units = self.options.imageTexts.sizeUnits.split(',') return errorMessage .replace('#image_name#', file.name) .replace('#image_size#', humanFileSize(file.size, units)) .replace( '#image_max_size#', humanFileSize(self.options.imageMaxSize, units), ) } if (file.size > this.options.imageMaxSize) { onErrorSup(fillErrorMessage(this.options.errorMessages.fileTooLarge)) return } var formData = new FormData() formData.append('image', file) // insert CSRF body token if provided in config. if (self.options.imageCSRFToken && !self.options.imageCSRFHeader) { formData.append(self.options.imageCSRFName, self.options.imageCSRFToken) } var request = new XMLHttpRequest() request.upload.onprogress = function (event) { if (event.lengthComputable) { var progress = '' + Math.round((event.loaded * 100) / event.total) self.updateStatusBar( 'upload-image', self.options.imageTexts.sbProgress .replace('#file_name#', file.name) .replace('#progress#', progress), ) } } request.open('POST', this.options.imageUploadEndpoint) // insert CSRF header token if provided in config. if (self.options.imageCSRFToken && self.options.imageCSRFHeader) { request.setRequestHeader( self.options.imageCSRFName, self.options.imageCSRFToken, ) } request.onload = function () { try { var response = JSON.parse(this.responseText) } catch (error) { console.error('EasyMDE: The server did not return a valid json.') onErrorSup(fillErrorMessage(self.options.errorMessages.importError)) return } if ( this.status === 200 && response && !response.error && response.data && response.data.filePath ) { onSuccess( (self.options.imagePathAbsolute ? '' : window.location.origin + '/') + response.data.filePath, ) } else { if ( response.error && response.error in self.options.errorMessages ) { // preformatted error message onErrorSup( fillErrorMessage( self.options.errorMessages[response.error], ), ) } else if (response.error) { // server side generated error message onErrorSup(fillErrorMessage(response.error)) } else { //unknown error console.error( 'EasyMDE: Received an unexpected response after uploading the image.' + this.status + ' (' + this.statusText + ')', ) onErrorSup( fillErrorMessage(self.options.errorMessages.importError), ) } } } request.onerror = function (event) { console.error( 'EasyMDE: An unexpected error occurred when trying to upload the image.' + event.target.status + ' (' + event.target.statusText + ')', ) onErrorSup(self.options.errorMessages.importError) } request.send(formData) } /** * Upload an image to the server using a custom upload function. * * @param imageUploadFunction {Function} The custom function to upload the image passed in options * @param file {File} The image to upload, as a HTML5 File object (https://developer.mozilla.org/en-US/docs/Web/API/File). */ EasyMDE.prototype.uploadImageUsingCustomFunction = function ( imageUploadFunction, file, ) { var self = this function onSuccess(imageUrl) { afterImageUploaded(self, imageUrl) } function onError(errorMessage) { var filledErrorMessage = fillErrorMessage(errorMessage) // show error on status bar and reset after 10000ms self.updateStatusBar('upload-image', filledErrorMessage) setTimeout(function () { self.updateStatusBar('upload-image', self.options.imageTexts.sbInit) }, 10000) // run error handler from options, this alerts the message. self.options.errorCallback(filledErrorMessage) } function fillErrorMessage(errorMessage) { var units = self.options.imageTexts.sizeUnits.split(',') return errorMessage .replace('#image_name#', file.name) .replace('#image_size#', humanFileSize(file.size, units)) .replace( '#image_max_size#', humanFileSize(self.options.imageMaxSize, units), ) } imageUploadFunction.apply(this, [file, onSuccess, onError]) } EasyMDE.prototype.setPreviewMaxHeight = function () { var cm = this.codemirror var wrapper = cm.getWrapperElement() var preview = wrapper.nextSibling // Calc preview max height var paddingTop = parseInt(window.getComputedStyle(wrapper).paddingTop) var borderTopWidth = parseInt( window.getComputedStyle(wrapper).borderTopWidth, ) var optionsMaxHeight = parseInt(this.options.maxHeight) var wrapperMaxHeight = optionsMaxHeight + paddingTop * 2 + borderTopWidth * 2 var previewMaxHeight = wrapperMaxHeight.toString() + 'px' preview.style.height = previewMaxHeight } EasyMDE.prototype.createSideBySide = function () { var cm = this.codemirror var wrapper = cm.getWrapperElement() var preview = wrapper.nextSibling if (!preview || !preview.classList.contains('editor-preview-side')) { preview = document.createElement('div') preview.className = 'editor-preview-side' if (this.options.previewClass) { if (Array.isArray(this.options.previewClass)) { for (var i = 0; i < this.options.previewClass.length; i++) { preview.classList.add(this.options.previewClass[i]) } } else if (typeof this.options.previewClass === 'string') { preview.classList.add(this.options.previewClass) } } wrapper.parentNode.insertBefore(preview, wrapper.nextSibling) } if (typeof this.options.maxHeight !== 'undefined') { this.setPreviewMaxHeight() } 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' bar.setAttribute('role', '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 if (item.children) { el = createToolbarDropdown( item, self.options.toolbarTips, self.options.shortcuts, self, ) } else { el = createToolbarButton( item, true, self.options.toolbarTips, self.options.shortcuts, 'button', self, ) } toolbarData[item.name || item] = el bar.appendChild(el) // Create the input element (ie. ), used among // with the 'import-image' icon to open the browse-file window. if (item.name === 'upload-image') { var imageInput = document.createElement('input') imageInput.className = 'imageInput' imageInput.type = 'file' imageInput.multiple = true imageInput.name = 'image' imageInput.accept = self.options.imageAccept imageInput.style.display = 'none' imageInput.style.opacity = 0 bar.appendChild(imageInput) } })(items[i]) } self.toolbar_div = bar 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.classList.add('active') } else if (key != 'fullscreen' && key != 'side-by-side') { el.classList.remove('active') } })(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, onActivity, defaultValue for (i = 0; i < status.length; i++) { // Reset some values onUpdate = undefined onActivity = 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, onActivity: status[i].onActivity, }) } 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 = '1:1' } onActivity = function (el) { var pos = cm.getCursor() var posLine = pos.line + 1 var posColumn = pos.ch + 1 el.innerHTML = posLine + ':' + posColumn } } else if (name === 'autosave') { defaultValue = function (el) { if ( options.autosave != undefined && options.autosave.enabled === true ) { el.setAttribute('id', 'autosaved') } } } else if (name === 'upload-image') { defaultValue = function (el) { el.innerHTML = options.imageTexts.sbInit } } items.push({ className: name, defaultValue: defaultValue, onUpdate: onUpdate, onActivity: onActivity, }) } } // 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), ) } if (typeof item.onActivity === 'function') { // Create a closure around the span of the current action, then execute the onActivity handler this.codemirror.on( 'cursorActivity', (function (el, item) { return function () { item.onActivity(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 var preview_result = this.options.previewRender(val, preview) if (preview_result !== null) { preview.innerHTML = preview_result } } 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.toggleHeading4 = toggleHeading4 EasyMDE.toggleHeading5 = toggleHeading5 EasyMDE.toggleHeading6 = toggleHeading6 EasyMDE.toggleCodeBlock = toggleCodeBlock EasyMDE.toggleUnorderedList = toggleUnorderedList EasyMDE.toggleOrderedList = toggleOrderedList EasyMDE.cleanBlock = cleanBlock EasyMDE.drawLink = drawLink EasyMDE.drawImage = drawImage EasyMDE.drawUploadedImage = drawUploadedImage 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.toggleHeading4 = function () { toggleHeading4(this) } EasyMDE.prototype.toggleHeading5 = function () { toggleHeading5(this) } EasyMDE.prototype.toggleHeading6 = function () { toggleHeading6(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.drawUploadedImage = function () { drawUploadedImage(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 preview.classList.contains('editor-preview-active') } EasyMDE.prototype.isSideBySideActive = function () { var cm = this.codemirror var wrapper = cm.getWrapperElement() var preview = wrapper.nextSibling return preview.classList.contains('editor-preview-active-side') } 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() var easyMDEContainer = wrapper.parentNode if (easyMDEContainer) { if (this.gui.toolbar) { easyMDEContainer.removeChild(this.gui.toolbar) } if (this.gui.statusbar) { easyMDEContainer.removeChild(this.gui.statusbar) } if (this.gui.sideBySide) { easyMDEContainer.removeChild(this.gui.sideBySide) } } // Unwrap easyMDEcontainer before codemirror toTextArea() call easyMDEContainer.parentNode.insertBefore(wrapper, easyMDEContainer) easyMDEContainer.remove() cm.toTextArea() if (this.autosaveTimeoutId) { clearTimeout(this.autosaveTimeoutId) this.autosaveTimeoutId = undefined this.clearAutosavedValue() } } window.EasyMDE = EasyMDE