diff --git a/.eslintrc b/.eslintrc index af736ed..cfca6fd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,5 +19,9 @@ "browser": true, "node":true }, - "extends": "eslint:recommended" + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + } } \ No newline at end of file diff --git a/src/js/action.js b/src/js/action.js new file mode 100644 index 0000000..e18f5f4 --- /dev/null +++ b/src/js/action.js @@ -0,0 +1,607 @@ +/** + * Created by WittBulter on 2017/1/17. + */ +import Base from './base' + +export default class Action { + constructor (){ + this.saved_overflow = '' + } + + /** + * Action for toggling bold. + */ + static toggleBold (editor){ + Base.toggleBlock(editor, "bold", editor.options.blockStyles.bold); + } + + /** + * Action for toggling italic. + */ + static toggleItalic (editor){ + Base.toggleBlock(editor, "italic", editor.options.blockStyles.italic); + } + + /** + * Action for drawing a link. + */ + static drawLink (editor){ + const cm = editor.codemirror; + const stat = getState(cm); + const options = editor.options; + let url = "http://"; + if(options.promptURLs) { + url = prompt(options.promptTexts.link); + if(!url) { + return false; + } + } + Base.replaceSelection(cm, stat.link, options.insertTexts.link, url); + } + + /** + * Action for toggling heading size: normal -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> normal + */ + static toggleHeadingSmaller (editor){ + const cm = editor.codemirror; + Base.toggleHeading(cm, "smaller"); + } + + /** + * Action for toggling heading size: normal -> h6 -> h5 -> h4 -> h3 -> h2 -> h1 -> normal + */ + static toggleHeadingBigger (editor){ + const cm = editor.codemirror; + Base.toggleHeading(cm, "bigger"); + } + + /** + * Action for drawing an img. + */ + static drawImage (editor){ + const cm = editor.codemirror; + const stat = Base.getState(cm); + const options = editor.options; + let url = "http://"; + if(options.promptURLs) { + url = prompt(options.promptTexts.image); + if(!url) { + return false; + } + } + Base.replaceSelection(cm, stat.image, options.insertTexts.image, url); + } + + /** + * Action for toggling blockquote. + */ + static toggleBlockquote (editor){ + const cm = editor.codemirror; + Base.toggleLine(cm, "quote"); + } + + /** + * Action for toggling ol. + */ + static toggleOrderedList (editor){ + const cm = editor.codemirror; + Base.toggleLine(cm, "ordered-list"); + } + + /** + * Action for toggling ul. + */ + static toggleUnorderedList (editor){ + const cm = editor.codemirror; + Base.toggleLine(cm, "unordered-list"); + } + + /** + * Action for toggling code block. + */ + static toggleCodeBlock (editor){ + let fenceCharsToInsert = editor.options.blockStyles.code; + + const 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; + } + + const 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; + } + + const code_type = (cm, line_num, line = cm.getLineHandle(line_num), 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. + */ + firstTok = firstTok || cm.getTokenAt({ + line: line_num, + ch: 1 + }); + lastTok = lastTok || (!!line.text && cm.getTokenAt({ + line: line_num, + ch: line.text.length - 1 + })); + let 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"; + } + } + + const insertFencingAtSelection = (cm, cur_start, cur_end, fenceCharsToInsert) => { + let 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--; + } + Base.replaceSelection(cm, false, [repl_start, repl_end]); + cm.setSelection({ + line: start_line_sel, + ch: 0 + }, { + line: end_line_sel, + ch: 0 + }); + } + + let 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); + let block_start, block_end, lineCount; + + if(is_code === "single") { + // similar to some SimpleMDE _toggleBlock logic + let 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; + } + } + let fencedTok = cm.getTokenAt({ + line: block_start, + ch: 1 + }); + let fence_chars = token_state(fencedTok).fencedChars; + let start_text, start_line; + let 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 + let 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 + let 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 + let no_sel_and_starting_of_line = (cur_start.line === cur_end.line && cur_start.ch === cur_end.ch && cur_start.ch === 0); + let 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 { + Base.replaceSelection(cm, false, ["`", "`"]); + } + } + } + + /** + * Toggle full screen of the editor. + */ + static toggleFullScreen (editor){ + // Set fullscreen + let cm = editor.codemirror; + cm.setOption("fullScreen", !cm.getOption("fullScreen")); + + + // Prevent scrolling on body during fullscreen active + if(cm.getOption("fullScreen")) { + this.saved_overflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = this.saved_overflow; + } + + + // Update toolbar class + let 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 + let 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 + const sidebyside = cm.getWrapperElement().nextSibling; + if(/editor-preview-active-side/.test(sidebyside.className)) + this.toggleSideBySide(editor); + } + + + /** + * Toggle side by side preview + */ + static toggleSideBySide (editor){ + let cm = editor.codemirror; + let wrapper = cm.getWrapperElement(); + let preview = wrapper.nextSibling; + let toolbarButton = editor.toolbarElements["side-by-side"]; + let useSideBySideListener = false; + if(/editor-preview-active-side/.test(preview.className)) { + preview.className = preview.className.replace( + /\s*editor-preview-active-side\s*/g, "" + ); + 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")) + this.toggleFullScreen(editor); + preview.className += " editor-preview-active-side"; + }, 1); + toolbarButton.className += " active"; + wrapper.className += " CodeMirror-sided"; + useSideBySideListener = true; + } + + // Hide normal preview if active + let previewNormal = wrapper.lastChild; + if(/editor-preview-active/.test(previewNormal.className)) { + previewNormal.className = previewNormal.className.replace( + /\s*editor-preview-active\s*/g, "" + ); + let toolbar = editor.toolbarElements.preview; + let 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, ""); + } + + const 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. + */ + static togglePreview (editor){ + let cm = editor.codemirror; + let wrapper = cm.getWrapperElement(); + let toolbar_div = wrapper.previousSibling; + let toolbar = editor.options.toolbar ? editor.toolbarElements.preview : false; + let 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 + const sidebyside = cm.getWrapperElement().nextSibling; + if(/editor-preview-active-side/.test(sidebyside.className)) + this.toggleSideBySide(editor); + } + + /** + * Action for toggling strikethrough. + */ + static toggleStrikethrough (editor){ + Base.toggleBlock(editor, "strikethrough", "~~"); + } + + /** + * Action for toggling heading size 1, 2, 3 + */ + static toggleHeading1 (editor){ + const cm = editor.codemirror; + Base.toggleHeading(cm, undefined, 1); + } + static toggleHeading2 (editor){ + const cm = editor.codemirror; + Base.toggleHeading(cm, undefined, 2); + } + static toggleHeading3 (editor){ + const cm = editor.codemirror; + Base.toggleHeading(cm, undefined, 3); + } + + /** + * Action for clean block (remove headline, list, blockquote code, markers) + */ + static cleanBlock (editor){ + const cm = editor.codemirror; + Base.cleanBlock(cm); + } + + /** + * Action for drawing a table. + */ + static drawTable (editor){ + const cm = editor.codemirror; + const stat = Base.getState(cm); + const options = editor.options; + Base.replaceSelection(cm, stat.table, options.insertTexts.table); + } + + /** + * Action for drawing a horizontal rule. + */ + static drawHorizontalRule (editor){ + const cm = editor.codemirror; + const stat = Base.getState(cm); + const options = editor.options; + Base.replaceSelection(cm, stat.image, options.insertTexts.horizontalRule); + } + + /** + * Undo action. + */ + static undo (editor){ + const cm = editor.codemirror; + cm.undo(); + cm.focus(); + } + + /** + * Redo action. + */ + static redo (editor){ + const cm = editor.codemirror; + cm.redo(); + cm.focus(); + } + + + + + + + + +} \ No newline at end of file diff --git a/src/js/base.js b/src/js/base.js new file mode 100644 index 0000000..324ae53 --- /dev/null +++ b/src/js/base.js @@ -0,0 +1,276 @@ +/** + * Created by WittBulter on 2017/1/17. + */ + +export default new class Base { + constructor (){} + + getState (cm, pos = cm.getCursor("start")){ + let stat = cm.getTokenAt(pos); + if(!stat.type) return {}; + const types = stat.type.split(" "); + + let ret = {}, + data, text; + for(let 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; + } + + 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; + let cm = editor.codemirror; + let stat = this.getState(cm); + + let text; + let start = start_chars; + let end = end_chars; + + let startPoint = cm.getCursor("start"); + let 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(); + } + + replaceSelection (cm, active, startEnd, url){ + if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) + return; + + let text; + let start = startEnd[0]; + let end = startEnd[1]; + let startPoint = cm.getCursor("start"); + let 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(); + } + + toggleHeading (cm, direction, size){ + if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) + return; + + const startPoint = cm.getCursor("start"); + const endPoint = cm.getCursor("end"); + for(let i = startPoint.line; i <= endPoint.line; i++) { + (function(i) { + let text = cm.getLine(i); + let 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(); + } + + toggleLine (cm, name){ + if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) return; + + let stat = this.getState(cm); + const startPoint = cm.getCursor("start"); + const endPoint = cm.getCursor("end"); + const repl = { + "quote": /^(\s*)\>\s+/, + "unordered-list": /^(\s*)(\*|\-|\+)\s+/, + "ordered-list": /^(\s*)\d+\.\s+/ + }; + const map = { + "quote": "> ", + "unordered-list": "* ", + "ordered-list": "1. " + }; + for(let i = startPoint.line; i <= endPoint.line; i++) { + (function(i) { + let text = cm.getLine(i); + if(stat[name]) { + text = text.replace(repl[name], "$1"); + } else { + text = map[name] + text; + } + cm.replaceRange(text, { + line: i, + ch: 0 + }, { + line: i, + ch: 99999999999999 + }); + })(i); + } + cm.focus(); + } + + + cleanBlock (cm){ + if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) + return; + + const startPoint = cm.getCursor("start"); + const endPoint = cm.getCursor("end"); + let text; + + for(let 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 + }); + } + } + +} \ No newline at end of file diff --git a/src/js/metadata.js b/src/js/metadata.js new file mode 100644 index 0000000..3b306e6 --- /dev/null +++ b/src/js/metadata.js @@ -0,0 +1,238 @@ +/** + * Created by WittBulter on 2017/1/17. + */ +import Action from './action' + +export const bindings = { + "toggleBold": Action.toggleBold, + "toggleItalic": Action.toggleItalic, + "drawLink": Action.drawLink, + "toggleHeadingSmaller": Action.toggleHeadingSmaller, + "toggleHeadingBigger": Action.toggleHeadingBigger, + "drawImage": Action.drawImage, + "toggleBlockquote": Action.toggleBlockquote, + "toggleOrderedList": Action.toggleOrderedList, + "toggleUnorderedList": Action.toggleUnorderedList, + "toggleCodeBlock": Action.toggleCodeBlock, + "togglePreview": Action.togglePreview, + "toggleStrikethrough": Action.toggleStrikethrough, + "toggleHeading1": Action.toggleHeading1, + "toggleHeading2": Action.toggleHeading2, + "toggleHeading3": Action.toggleHeading3, + "cleanBlock": Action.cleanBlock, + "drawTable": Action.drawTable, + "drawHorizontalRule": Action.drawHorizontalRule, + "undo": Action.undo, + "redo": Action.redo, + "toggleSideBySide": Action.toggleSideBySide, + "toggleFullScreen": Action.toggleFullScreen +}; + +export const 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" +}; + +export const toolbarBuiltInButtons = { + "bold": { + name: "bold", + action: Action.toggleBold, + className: "fa fa-bold", + title: "Bold", + default: true + }, + "italic": { + name: "italic", + action: Action.toggleItalic, + className: "fa fa-italic", + title: "Italic", + default: true + }, + "strikethrough": { + name: "strikethrough", + action: Action.toggleStrikethrough, + className: "fa fa-strikethrough", + title: "Strikethrough" + }, + "heading": { + name: "heading", + action: Action.toggleHeadingSmaller, + className: "fa fa-header", + title: "Heading", + default: true + }, + "heading-smaller": { + name: "heading-smaller", + action: Action.toggleHeadingSmaller, + className: "fa fa-header fa-header-x fa-header-smaller", + title: "Smaller Heading" + }, + "heading-bigger": { + name: "heading-bigger", + action: Action.toggleHeadingBigger, + className: "fa fa-header fa-header-x fa-header-bigger", + title: "Bigger Heading" + }, + "heading-1": { + name: "heading-1", + action: Action.toggleHeading1, + className: "fa fa-header fa-header-x fa-header-1", + title: "Big Heading" + }, + "heading-2": { + name: "heading-2", + action: Action.toggleHeading2, + className: "fa fa-header fa-header-x fa-header-2", + title: "Medium Heading" + }, + "heading-3": { + name: "heading-3", + action: Action.toggleHeading3, + className: "fa fa-header fa-header-x fa-header-3", + title: "Small Heading" + }, + "separator-1": { + name: "separator-1" + }, + "code": { + name: "code", + action: Action.toggleCodeBlock, + className: "fa fa-code", + title: "Code" + }, + "quote": { + name: "quote", + action: Action.toggleBlockquote, + className: "fa fa-quote-left", + title: "Quote", + default: true + }, + "unordered-list": { + name: "unordered-list", + action: Action.toggleUnorderedList, + className: "fa fa-list-ul", + title: "Generic List", + default: true + }, + "ordered-list": { + name: "ordered-list", + action: Action.toggleOrderedList, + className: "fa fa-list-ol", + title: "Numbered List", + default: true + }, + "clean-block": { + name: "clean-block", + action: Action.cleanBlock, + className: "fa fa-eraser fa-clean-block", + title: "Clean block" + }, + "separator-2": { + name: "separator-2" + }, + "link": { + name: "link", + action: Action.drawLink, + className: "fa fa-link", + title: "Create Link", + default: true + }, + "image": { + name: "image", + action: Action.drawImage, + className: "fa fa-picture-o", + title: "Insert Image", + default: true + }, + "table": { + name: "table", + action: Action.drawTable, + className: "fa fa-table", + title: "Insert Table" + }, + "horizontal-rule": { + name: "horizontal-rule", + action: Action.drawHorizontalRule, + className: "fa fa-minus", + title: "Insert Horizontal Line" + }, + "separator-3": { + name: "separator-3" + }, + "preview": { + name: "preview", + action: Action.togglePreview, + className: "fa fa-eye no-disable", + title: "Toggle Preview", + default: true + }, + "side-by-side": { + name: "side-by-side", + action: Action.toggleSideBySide, + className: "fa fa-columns no-disable no-mobile", + title: "Toggle Side by Side", + default: true + }, + "fullscreen": { + name: "fullscreen", + action: Action.toggleFullScreen, + className: "fa fa-arrows-alt no-disable no-mobile", + title: "Toggle Fullscreen", + default: true + }, + "separator-4": { + name: "separator-4" + }, + "guide": { + name: "guide", + action: "https://simplemde.com/markdown-guide", + className: "fa fa-question-circle", + title: "Markdown Guide", + default: true + }, + "separator-5": { + name: "separator-5" + }, + "undo": { + name: "undo", + action: Action.undo, + className: "fa fa-undo no-disable", + title: "Undo" + }, + "redo": { + name: "redo", + action: Action.redo, + className: "fa fa-repeat no-disable", + title: "Redo" + } +}; + +export const blockStyles = { + "bold": "**", + "code": "```", + "italic": "*" +}; + +export const 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"] +}; + +export const promptTexts = { + link: "URL for the link:", + image: "URL of the image:" +}; \ No newline at end of file diff --git a/src/js/simplemde.js b/src/js/simplemde.js index ded2559..1034736 100644 --- a/src/js/simplemde.js +++ b/src/js/simplemde.js @@ -12,96 +12,29 @@ import 'codemirror/mode/markdown/markdown' import 'codemirror/mode/gfm/gfm' import 'codemirror/mode/xml/xml' - -// Some variables -const isMac = navigator.platform.includes('Mac'); - -// 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; -}; - - -/** - * 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; -} - +import Action from './action' +import utils from './utils' +import base from './base' +import { + bindings, + shortcuts, + toolbarBuiltInButtons, + blockStyles, + insertTexts, + promptTexts +} from './metadata' /** * Create icon element for toolbar. */ -function createIcon(options, enableTooltips, shortcuts) { - options = options || {}; - var el = document.createElement("a"); +const createIcon = (options = {}, enableTooltips, shortcuts) => { + let el = document.createElement("a"); enableTooltips = (enableTooltips == undefined) ? true : enableTooltips; if(options.title && enableTooltips) { el.title = createTootlip(options.title, options.action, shortcuts); - if(isMac) { + if(utils.isMac()) { el.title = el.title.replace("Ctrl", "⌘"); el.title = el.title.replace("Alt", "⌥"); } @@ -112,1916 +45,738 @@ function createIcon(options, enableTooltips, shortcuts) { return el; } -function createSep() { - var el = document.createElement("i"); +const createSep = () => { + let el = document.createElement("i"); el.className = "separator"; el.innerHTML = "|"; return el; } -function createTootlip(title, action, shortcuts) { - var actionName; - var tooltip = title; +const createTootlip = (title, action, shortcuts) => { + let tooltip = title; - if(action) { - actionName = getBindingName(action); - if(shortcuts[actionName]) { - tooltip += " (" + fixShortcut(shortcuts[actionName]) + ")"; - } + if(action && shortcuts[utils.getBindingName(action)]) { + tooltip += " (" + utils.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 - 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); -} - - -/** - * Action for toggling bold. + * Interface of SimpleMDE. */ -function toggleBold(editor) { - _toggleBlock(editor, "bold", editor.options.blockStyles.bold); -} -/** - * Action for toggling italic. - */ -function toggleItalic(editor) { - _toggleBlock(editor, "italic", editor.options.blockStyles.italic); -} +export class SimpleMDE extends Action { + constructor(options = {}) { + // Used later to refer to it"s parent + options.parent = this; -/** - * 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; - } + // Check if Font Awesome needs to be auto downloaded + let autoDownloadFA = true; - 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"; + if(options.autoDownloadFontAwesome === false) { + autoDownloadFA = false; } - } - 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 SimpleMDE _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 + if(options.autoDownloadFontAwesome !== true) { + const styleSheets = document.styleSheets; + for(let i = 0; i < styleSheets.length; i++) { + if(!styleSheets[i].href) 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(styleSheets[i].href.indexOf("//maxcdn.bootstrapcdn.com/font-awesome/") > -1) { + autoDownloadFA = false; } } } - // 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 :( + if(autoDownloadFA) { + let 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); } - 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"); -} + // 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("SimpleMDE: Error. No element was found."); + return; + } -/** - * 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); -} + // Handle toolbar + if(options.toolbar === undefined) { + // Initialize + options.toolbar = []; -/** - * Action for toggling ul. - */ -function toggleUnorderedList(editor) { - var cm = editor.codemirror; - _toggleLine(cm, "unordered-list"); -} - + // Loop over the built in buttons, to get the preferred order + for(let key of toolbarBuiltInButtons) { + if(toolbarBuiltInButtons.hasOwnProperty(key)) { + if(key.indexOf("separator-") != -1) { + options.toolbar.push("|"); + } -/** - * Action for toggling ol. - */ -function toggleOrderedList(editor) { - var cm = editor.codemirror; - _toggleLine(cm, "ordered-list"); -} + if(toolbarBuiltInButtons[key].default === true || (options.showIcons && options.showIcons.constructor === Array && options.showIcons.indexOf(key) != -1)) { + options.toolbar.push(key); + } + } + } + } -/** - * 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 = "http://"; - if(options.promptURLs) { - url = prompt(options.promptTexts.link); - if(!url) { - return false; + // Handle status bar + if(!options.hasOwnProperty("status")) { + options.status = ["autosave", "lines", "words", "cursor"]; } - } - _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 = "http://"; - if(options.promptURLs) { - url = prompt(options.promptTexts.image); - if(!url) { - return false; + + // Add default preview rendering function + if(!options.previewRender) { + options.previewRender = function(plainText) { + // Note: "this" refers to the options object + return this.parent.markdown(plainText); + }; } - } - _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); -} + // Set default options for parsing config + options.parsingConfig = Object.assign({ + highlightFormatting: true // needed for toggleCodeBlock to detect types of code + }, options.parsingConfig || {}); -/** - * Undo action. - */ -function undo(editor) { - var cm = editor.codemirror; - cm.undo(); - cm.focus(); -} + // Merging the insertTexts, with the given options + options.insertTexts = Object.assign({}, insertTexts, options.insertTexts || {}); -/** - * Redo action. - */ -function redo(editor) { - var cm = editor.codemirror; - cm.redo(); - cm.focus(); -} + // Merging the promptTexts, with the given options + options.promptTexts = promptTexts; -/** - * Toggle side by side preview - */ -function toggleSideBySide(editor) { - var cm = editor.codemirror; - var wrapper = cm.getWrapperElement(); - var preview = wrapper.nextSibling; - var toolbarButton = 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, "" - ); - 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); - toolbarButton.className += " active"; - wrapper.className += " CodeMirror-sided"; - useSideBySideListener = true; - } + // Merging the blockStyles, with the given options + options.blockStyles = Object.assign({}, blockStyles, options.blockStyles || {}); - // 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); - }; + // Merging the shortcuts, with the given options + options.shortcuts = Object.assign({}, shortcuts, options.shortcuts || {}); - 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); - } + // 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; - // Refresh to fix selection being off (#309) - cm.refresh(); -} + // Update this options + this.options = options; -/** - * 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); -} + // Auto render + this.render(); -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 = cm.getCursor("start"); - var 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; + // 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); } } - cm.setSelection(startPoint, endPoint); - cm.focus(); -} -function _toggleHeading(cm, direction, size) { - if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) - return; + /** + * Default markdown render. + */ + markdown(text) { + if(marked) { + // Initialize + let markedOptions = {}; - 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(/[^#]/); + // Update options + const update = this.options && this.options.renderingConfig && this.options.renderingConfig.singleLineBreaks === false + markedOptions.breaks = !update - 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); - } - } + if(this.options && this.options.renderingConfig && this.options.renderingConfig.codeSyntaxHighlighting === true && window.hljs) { + markedOptions.highlight = function(code) { + return window.hljs.highlightAuto(code).value; + }; } - cm.replaceRange(text, { - line: i, - ch: 0 - }, { - line: i, - ch: 99999999999999 - }); - })(i); - } - cm.focus(); -} - + // Set options + marked.setOptions(markedOptions); -function _toggleLine(cm, name) { - if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) - return; - - var stat = getState(cm); - var startPoint = cm.getCursor("start"); - var endPoint = cm.getCursor("end"); - var repl = { - "quote": /^(\s*)\>\s+/, - "unordered-list": /^(\s*)(\*|\-|\+)\s+/, - "ordered-list": /^(\s*)\d+\.\s+/ - }; - var map = { - "quote": "> ", - "unordered-list": "* ", - "ordered-list": "1. " + // Return + return marked(text); + } }; - 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 { - text = map[name] + text; - } - 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(/(\*\*|~~)/, ""); + /** + * Render editor to the given element. + */ + render(el = this.element || document.getElementsByTagName("textarea")[0]) { + if(this._rendered && this._rendered === el) { + // Already rendered. + return; } - 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; + this.element = el; + const options = this.options; + + const self = this; + let keyMaps = {}; + + for(let key of options.shortcuts) { + // null stands for "do not bind this command" + if(options.shortcuts[key] !== null && bindings[key] !== null) { + (function(key) { + keyMaps[utils.fixShortcut(options.shortcuts[key])] = function() { + bindings[key](self); + }; + })(key); } } - } 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(); -} + keyMaps["Enter"] = "newlineAndIndentContinueMarkdownList"; + keyMaps["Tab"] = "tabAndIndentMarkdownList"; + keyMaps["Shift-Tab"] = "shiftTabAndUnindentMarkdownList"; + keyMaps["Esc"] = function(cm) { + if(cm.getOption("fullScreen")) Action.toggleFullScreen(self); + }; -function _cleanBlock(cm) { - if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) - return; + document.addEventListener("keydown", function(e) { + e = e || window.event; - var startPoint = cm.getCursor("start"); - var endPoint = cm.getCursor("end"); - var text; + if(e.keyCode == 27) { + if(self.codemirror.getOption("fullScreen")) Action.toggleFullScreen(self); + } + }, false); - for(var line = startPoint.line; line <= endPoint.line; line++) { - text = cm.getLine(line); - text = text.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/, ""); + let mode, backdrop; + if(options.spellChecker !== false) { + mode = "spell-checker"; + backdrop = options.parsingConfig; + backdrop.name = "gfm"; + backdrop.gitHubSpice = false; - cm.replaceRange(text, { - line: line, - ch: 0 - }, { - line: line, - ch: 99999999999999 + CodeMirrorSpellChecker({ + codeMirrorInstance: CodeMirror + }); + } else { + mode = options.parsingConfig; + mode.name = "gfm"; + mode.gitHubSpice = false; + } + + this.codemirror = CodeMirror.fromTextArea(el, { + mode: mode, + backdrop: backdrop, + theme: "paper", + tabSize: (options.tabSize != undefined) ? options.tabSize : 2, + indentUnit: (options.tabSize != undefined) ? options.tabSize : 2, + indentWithTabs: !(options.indentWithTabs === false), + lineNumbers: false, + autofocus: options.autofocus === true, + extraKeys: keyMaps, + lineWrapping: !(options.lineWrapping === false), + allowDropFileTypes: ["text/plain"], + placeholder: options.placeholder || el.getAttribute("placeholder") || "", + styleSelectedText: (options.styleSelectedText != undefined) ? options.styleSelectedText : true }); - } -} -// 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]; - } + if(options.forceSync === true) { + const cm = this.codemirror; + cm.on("change", function() { + cm.save(); + }); } - } - return target; -} + this.gui = {}; -// 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_\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; + 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(); } - } - 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", - title: "Heading", - default: true - }, - "heading-smaller": { - name: "heading-smaller", - action: toggleHeadingSmaller, - className: "fa fa-header fa-header-x fa-header-smaller", - title: "Smaller Heading" - }, - "heading-bigger": { - name: "heading-bigger", - action: toggleHeadingBigger, - className: "fa fa-header fa-header-x fa-header-bigger", - title: "Bigger Heading" - }, - "heading-1": { - name: "heading-1", - action: toggleHeading1, - className: "fa fa-header fa-header-x fa-header-1", - title: "Big Heading" - }, - "heading-2": { - name: "heading-2", - action: toggleHeading2, - className: "fa fa-header fa-header-x fa-header-2", - title: "Medium Heading" - }, - "heading-3": { - name: "heading-3", - action: toggleHeading3, - className: "fa fa-header fa-header-x fa-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 fa-clean-block", - 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-picture-o", - 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 no-disable", - title: "Toggle Preview", - default: true - }, - "side-by-side": { - name: "side-by-side", - action: toggleSideBySide, - className: "fa fa-columns no-disable no-mobile", - title: "Toggle Side by Side", - default: true - }, - "fullscreen": { - name: "fullscreen", - action: toggleFullScreen, - className: "fa fa-arrows-alt no-disable no-mobile", - title: "Toggle Fullscreen", - default: true - }, - "separator-4": { - name: "separator-4" - }, - "guide": { - name: "guide", - action: "https://simplemde.com/markdown-guide", - className: "fa fa-question-circle", - title: "Markdown Guide", - default: true - }, - "separator-5": { - name: "separator-5" - }, - "undo": { - name: "undo", - action: undo, - className: "fa fa-undo no-disable", - title: "Undo" - }, - "redo": { - name: "redo", - action: redo, - className: "fa fa-repeat no-disable", - 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": "*" -}; + this.gui.sideBySide = this.createSideBySide(); -/** - * Interface of SimpleMDE. - */ -function SimpleMDE(options) { - // Handle options parameter - options = options || {}; + this._rendered = this.element; - // Used later to refer to it"s parent - options.parent = this; - + // Fixes CodeMirror bug (#344) + const temp_cm = this.codemirror; + setTimeout(function() { + temp_cm.refresh(); + }.bind(temp_cm), 0); + }; - // Check if Font Awesome needs to be auto downloaded - var autoDownloadFA = true; - if(options.autoDownloadFontAwesome === false) { - autoDownloadFA = false; - } + autosave() { + if(utils.isLocalStorageAvailable()) { + const simplemde = this; - 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(this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == "") { + console.log("SimpleMDE: You must set a uniqueId to use the autosave feature"); + return; } - } - } - - 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("SimpleMDE: Error. No element was found."); - return; - } - - - // Handle toolbar - if(options.toolbar === undefined) { - // Initialize - options.toolbar = []; + if(simplemde.element.form != null && simplemde.element.form != undefined) { + simplemde.element.form.addEventListener("submit", function() { + localStorage.removeItem("smde_" + simplemde.options.autosave.uniqueId); + }); + } - // 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(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; } - if(toolbarBuiltInButtons[key].default === true || (options.showIcons && options.showIcons.constructor === Array && options.showIcons.indexOf(key) != -1)) { - options.toolbar.push(key); - } + this.options.autosave.loaded = true; } - } - } - - - // 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 = 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 || {}); - - - // 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. - */ -SimpleMDE.prototype.markdown = function(text) { - if(marked) { - // Initialize - var markedOptions = {}; + localStorage.setItem("smde_" + this.options.autosave.uniqueId, simplemde.value()); + + let el = document.getElementById("autosaved"); + if(el != null && el != undefined && el != "") { + let d = new Date(); + let hh = d.getHours(); + let m = d.getMinutes(); + let dd = "am"; + let 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; + } - // Update options - if(this.options && this.options.renderingConfig && this.options.renderingConfig.singleLineBreaks === false) { - markedOptions.breaks = false; + this.autosaveTimeoutId = setTimeout(function() { + simplemde.autosave(); + }, this.options.autosave.delay || 10000); } else { - markedOptions.breaks = true; + console.log("SimpleMDE: localStorage not available, cannot autosave"); } + }; - if(this.options && this.options.renderingConfig && this.options.renderingConfig.codeSyntaxHighlighting === true && window.hljs) { - markedOptions.highlight = function(code) { - return window.hljs.highlightAuto(code).value; - }; - } - - - // Set options - marked.setOptions(markedOptions); - - - // Return - return marked(text); - } -}; - -/** - * Render editor to the given element. - */ -SimpleMDE.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 = {}; + clearAutosavedValue() { + if(utils.isLocalStorageAvailable()) { + if(this.options.autosave == undefined || this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == "") { + console.log("SimpleMDE: You must set a uniqueId to clear the autosave value"); + return; + } - 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); + localStorage.removeItem("smde_" + this.options.autosave.uniqueId); + } else { + console.log("SimpleMDE: localStorage not available, cannot autosave"); } - } - - 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; + createSideBySide() { + let cm = this.codemirror; + let wrapper = cm.getWrapperElement(); + let preview = wrapper.nextSibling; - if(e.keyCode == 27) { - if(self.codemirror.getOption("fullScreen")) toggleFullScreen(self); + if(!preview || !/editor-preview-side/.test(preview.className)) { + preview = document.createElement("div"); + preview.className = "editor-preview-side"; + wrapper.parentNode.insertBefore(preview, wrapper.nextSibling); } - }, false); - - var mode, backdrop; - if(options.spellChecker !== false) { - mode = "spell-checker"; - backdrop = options.parsingConfig; - backdrop.name = "gfm"; - backdrop.gitHubSpice = false; - CodeMirrorSpellChecker({ - codeMirrorInstance: CodeMirror + // Syncs scroll editor -> preview + let cScroll = false; + let pScroll = false; + cm.on("scroll", function(v) { + if(cScroll) { + cScroll = false; + return; + } + pScroll = true; + let height = v.getScrollInfo().height - v.getScrollInfo().clientHeight; + let ratio = parseFloat(v.getScrollInfo().top) / height; + preview.scrollTop = (preview.scrollHeight - preview.clientHeight) * ratio; }); - } else { - mode = options.parsingConfig; - mode.name = "gfm"; - mode.gitHubSpice = false; - } - this.codemirror = CodeMirror.fromTextArea(el, { - mode: mode, - backdrop: backdrop, - theme: "paper", - 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 : true - }); - - if(options.forceSync === true) { - var cm = this.codemirror; - cm.on("change", function() { - cm.save(); - }); - } + // Syncs scroll preview -> editor + preview.onscroll = function() { + if(pScroll) { + pScroll = false; + return; + } + cScroll = true; + let height = preview.scrollHeight - preview.clientHeight; + let ratio = parseFloat(preview.scrollTop) / height; + let move = (cm.getScrollInfo().height - cm.getScrollInfo().clientHeight) * ratio; + cm.scrollTo(0, move); + }; + return preview; + }; - this.gui = {}; + createToolbar(items) { + items = items || this.options.toolbar; - 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(); - } + if(!items || items.length === 0) { + return; + } + let i; + for(i = 0; i < items.length; i++) { + if(toolbarBuiltInButtons[items[i]] != undefined) { + items[i] = toolbarBuiltInButtons[items[i]]; + } + } - this.gui.sideBySide = this.createSideBySide(); + let bar = document.createElement("div"); + bar.className = "editor-toolbar"; - this._rendered = this.element; + let self = this; + let toolbarData = {}; + self.toolbar = items; - // Fixes CodeMirror bug (#344) - var temp_cm = this.codemirror; - setTimeout(function() { - temp_cm.refresh(); - }.bind(temp_cm), 0); -}; + for(i = 0; i < items.length; i++) { + if(items[i].name == "guide" && self.options.toolbarGuideIcon === false) + continue; -// 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; - } + if(self.options.hideIcons && self.options.hideIcons.indexOf(items[i].name) != -1) + continue; - return true; -} + // 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") && utils.isMobile()) + continue; -SimpleMDE.prototype.autosave = function() { - if(isLocalStorageAvailable()) { - var simplemde = this; - if(this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == "") { - console.log("SimpleMDE: You must set a uniqueId to use the autosave feature"); - return; - } + // Don't include trailing separators + if(items[i] === "|") { + let nonSeparatorIconsFollow = false; - if(simplemde.element.form != null && simplemde.element.form != undefined) { - simplemde.element.form.addEventListener("submit", function() { - localStorage.removeItem("smde_" + simplemde.options.autosave.uniqueId); - }); - } + for(let x = (i + 1); x < items.length; x++) { + if(items[x] !== "|" && (!self.options.hideIcons || self.options.hideIcons.indexOf(items[x].name) == -1)) { + nonSeparatorIconsFollow = 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; + if(!nonSeparatorIconsFollow) + continue; } - this.options.autosave.loaded = true; - } - - localStorage.setItem("smde_" + this.options.autosave.uniqueId, simplemde.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; - } + // Create the icon and append to the toolbar + (function(item) { + let el; + if(item === "|") { + el = createSep(); + } else { + el = createIcon(item, self.options.toolbarTips, self.options.shortcuts); + } - this.autosaveTimeoutId = setTimeout(function() { - simplemde.autosave(); - }, this.options.autosave.delay || 10000); - } else { - console.log("SimpleMDE: localStorage not available, cannot autosave"); - } -}; + // 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.href = item.action; + el.target = "_blank"; + } + } -SimpleMDE.prototype.clearAutosavedValue = function() { - if(isLocalStorageAvailable()) { - if(this.options.autosave == undefined || this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == "") { - console.log("SimpleMDE: You must set a uniqueId to clear the autosave value"); - return; + toolbarData[item.name || item] = el; + bar.appendChild(el); + })(items[i]); } - localStorage.removeItem("smde_" + this.options.autosave.uniqueId); - } else { - console.log("SimpleMDE: localStorage not available, cannot autosave"); - } -}; + self.toolbarElements = toolbarData; -SimpleMDE.prototype.createSideBySide = function() { - var cm = this.codemirror; - var wrapper = cm.getWrapperElement(); - var preview = wrapper.nextSibling; + let cm = this.codemirror; + cm.on("cursorActivity", function() { + let stat = base.getState(cm); - if(!preview || !/editor-preview-side/.test(preview.className)) { - preview = document.createElement("div"); - preview.className = "editor-preview-side"; - wrapper.parentNode.insertBefore(preview, wrapper.nextSibling); - } + for(let key of toolbarData) { + (function(key) { + let 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); + } + }); - // 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); + const cmWrapper = cm.getWrapperElement(); + cmWrapper.parentNode.insertBefore(bar, cmWrapper); + return bar; }; - return preview; -}; - -SimpleMDE.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; + createStatusbar(status) { + // Initialize + status = status || this.options.status; + let options = this.options; + let cm = this.codemirror; - // 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; + // Make sure the status variable is valid + if(!status || status.length === 0) + return; - // 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; - } - } + // Set up the built-in items + let items = []; + let i, onUpdate, defaultValue; - if(!nonSeparatorIconsFollow) - continue; - } + for(i = 0; i < status.length; i++) { + // Reset some values + onUpdate = undefined; + defaultValue = undefined; - // Create the icon and append to the toolbar - (function(item) { - var el; - if(item === "|") { - el = createSep(); + // 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 { - el = createIcon(item, self.options.toolbarTips, self.options.shortcuts); - } + let name = status[i]; - // bind events, special for info - if(item.action) { - if(typeof item.action === "function") { - el.onclick = function(e) { - e.preventDefault(); - item.action(self); + if(name === "words") { + defaultValue = function(el) { + el.innerHTML = utils.wordCount(cm.getValue()); + }; + onUpdate = function(el) { + el.innerHTML = utils.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) { + const 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"); + } }; - } else if(typeof item.action === "string") { - el.href = item.action; - el.target = "_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); + items.push({ + className: name, + defaultValue: defaultValue, + onUpdate: onUpdate + }); + } } - }); - var cmWrapper = cm.getWrapperElement(); - cmWrapper.parentNode.insertBefore(bar, cmWrapper); - return bar; -}; -SimpleMDE.prototype.createStatusbar = function(status) { - // Initialize - status = status || this.options.status; - var options = this.options; - var cm = this.codemirror; + // Create element for the status bar + let bar = document.createElement("div"); + bar.className = "editor-statusbar"; - // Make sure the status variable is valid - if(!status || status.length === 0) - return; + // Create a new span for each item + for(i = 0; i < items.length; i++) { + // Store in temporary variable + let item = items[i]; - // Set up the built-in items - var items = []; - var i, onUpdate, defaultValue; + // Create span element + let el = document.createElement("span"); + el.className = item.className; - for(i = 0; i < status.length; i++) { - // Reset some values - onUpdate = undefined; - defaultValue = undefined; + // Ensure the defaultValue is a function + if(typeof item.defaultValue === "function") { + item.defaultValue(el); + } - // 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"); - } - }; + // 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))); } - items.push({ - className: name, - defaultValue: defaultValue, - onUpdate: onUpdate - }); + + // Append the item to the status bar + bar.appendChild(el); } - } - // Create element for the status bar - var bar = document.createElement("div"); - bar.className = "editor-statusbar"; + // Insert the status bar into the DOM + let cmWrapper = this.codemirror.getWrapperElement(); + cmWrapper.parentNode.insertBefore(bar, cmWrapper.nextSibling); + return bar; + }; + /** + * Get or set the text content. + */ + value(val) { + if(val === undefined) { + return this.codemirror.getValue(); + } else { + this.codemirror.getDoc().setValue(val); + return this; + } + }; - // Create a new span for each item - for(i = 0; i < items.length; i++) { - // Store in temporary variable - var item = items[i]; + /** + * Bind instance methods for exports. + */ + toggleBold() { + Action.toggleBold(this); + }; + toggleItalic() { + Action.toggleItalic(this); + }; + toggleStrikethrough() { + Action.toggleStrikethrough(this); + }; + toggleBlockquote() { + Action.toggleBlockquote(this); + }; + toggleHeadingSmaller() { + Action.toggleHeadingSmaller(this); + }; + toggleHeadingBigger() { + Action.toggleHeadingBigger(this); + }; + toggleHeading1() { + Action.toggleHeading1(this); + }; + toggleHeading2() { + Action.toggleHeading2(this); + }; + toggleHeading3() { + Action.toggleHeading3(this); + }; + toggleCodeBlock() { + Action.toggleCodeBlock(this); + }; + toggleUnorderedList() { + Action.toggleUnorderedList(this); + }; + toggleOrderedList() { + Action.toggleOrderedList(this); + }; + cleanBlock() { + Action.cleanBlock(this); + }; + drawLink() { + Action.drawLink(this); + }; + drawImage() { + Action.drawImage(this); + }; + drawTable() { + Action.drawTable(this); + }; + drawHorizontalRule() { + Action.drawHorizontalRule(this); + }; + undo() { + Action.undo(this); + }; + redo() { + Action.redo(this); + }; + togglePreview() { + Action.togglePreview(this); + }; - // Create span element - var el = document.createElement("span"); - el.className = item.className; + toggleSideBySide() { + Action.toggleSideBySide(this); + }; + toggleFullScreen() { + Action.toggleFullScreen(this); + }; + isPreviewActive() { + const cm = this.codemirror; + const wrapper = cm.getWrapperElement(); + const preview = wrapper.lastChild; - // Ensure the defaultValue is a function - if(typeof item.defaultValue === "function") { - item.defaultValue(el); - } + return /editor-preview-active/.test(preview.className); + }; + isSideBySideActive() { + const cm = this.codemirror; + const wrapper = cm.getWrapperElement(); + const preview = wrapper.nextSibling; - // 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))); - } + return /editor-preview-active-side/.test(preview.className); + }; + isFullscreenActive() { + const cm = this.codemirror; - // Append the item to the status bar - bar.appendChild(el); - } + return cm.getOption("fullScreen"); + }; + getState() { + const cm = this.codemirror; - // Insert the status bar into the DOM - var cmWrapper = this.codemirror.getWrapperElement(); - cmWrapper.parentNode.insertBefore(bar, cmWrapper.nextSibling); - return bar; -}; + return base.getState(cm); + }; -/** - * Get or set the text content. - */ -SimpleMDE.prototype.value = function(val) { - if(val === undefined) { - return this.codemirror.getValue(); - } else { - this.codemirror.getDoc().setValue(val); - return this; - } -}; + toTextArea() { + const cm = this.codemirror; + const 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); + } + } -/** - * Bind static methods for exports. - */ -SimpleMDE.toggleBold = toggleBold; -SimpleMDE.toggleItalic = toggleItalic; -SimpleMDE.toggleStrikethrough = toggleStrikethrough; -SimpleMDE.toggleBlockquote = toggleBlockquote; -SimpleMDE.toggleHeadingSmaller = toggleHeadingSmaller; -SimpleMDE.toggleHeadingBigger = toggleHeadingBigger; -SimpleMDE.toggleHeading1 = toggleHeading1; -SimpleMDE.toggleHeading2 = toggleHeading2; -SimpleMDE.toggleHeading3 = toggleHeading3; -SimpleMDE.toggleCodeBlock = toggleCodeBlock; -SimpleMDE.toggleUnorderedList = toggleUnorderedList; -SimpleMDE.toggleOrderedList = toggleOrderedList; -SimpleMDE.cleanBlock = cleanBlock; -SimpleMDE.drawLink = drawLink; -SimpleMDE.drawImage = drawImage; -SimpleMDE.drawTable = drawTable; -SimpleMDE.drawHorizontalRule = drawHorizontalRule; -SimpleMDE.undo = undo; -SimpleMDE.redo = redo; -SimpleMDE.togglePreview = togglePreview; -SimpleMDE.toggleSideBySide = toggleSideBySide; -SimpleMDE.toggleFullScreen = toggleFullScreen; + cm.toTextArea(); -/** - * Bind instance methods for exports. - */ -SimpleMDE.prototype.toggleBold = function() { - toggleBold(this); -}; -SimpleMDE.prototype.toggleItalic = function() { - toggleItalic(this); -}; -SimpleMDE.prototype.toggleStrikethrough = function() { - toggleStrikethrough(this); -}; -SimpleMDE.prototype.toggleBlockquote = function() { - toggleBlockquote(this); -}; -SimpleMDE.prototype.toggleHeadingSmaller = function() { - toggleHeadingSmaller(this); -}; -SimpleMDE.prototype.toggleHeadingBigger = function() { - toggleHeadingBigger(this); -}; -SimpleMDE.prototype.toggleHeading1 = function() { - toggleHeading1(this); -}; -SimpleMDE.prototype.toggleHeading2 = function() { - toggleHeading2(this); -}; -SimpleMDE.prototype.toggleHeading3 = function() { - toggleHeading3(this); -}; -SimpleMDE.prototype.toggleCodeBlock = function() { - toggleCodeBlock(this); -}; -SimpleMDE.prototype.toggleUnorderedList = function() { - toggleUnorderedList(this); -}; -SimpleMDE.prototype.toggleOrderedList = function() { - toggleOrderedList(this); -}; -SimpleMDE.prototype.cleanBlock = function() { - cleanBlock(this); -}; -SimpleMDE.prototype.drawLink = function() { - drawLink(this); -}; -SimpleMDE.prototype.drawImage = function() { - drawImage(this); -}; -SimpleMDE.prototype.drawTable = function() { - drawTable(this); -}; -SimpleMDE.prototype.drawHorizontalRule = function() { - drawHorizontalRule(this); -}; -SimpleMDE.prototype.undo = function() { - undo(this); -}; -SimpleMDE.prototype.redo = function() { - redo(this); -}; -SimpleMDE.prototype.togglePreview = function() { - togglePreview(this); -}; -SimpleMDE.prototype.toggleSideBySide = function() { - toggleSideBySide(this); -}; -SimpleMDE.prototype.toggleFullScreen = function() { - toggleFullScreen(this); -}; - -SimpleMDE.prototype.isPreviewActive = function() { - var cm = this.codemirror; - var wrapper = cm.getWrapperElement(); - var preview = wrapper.lastChild; - - return /editor-preview-active/.test(preview.className); -}; - -SimpleMDE.prototype.isSideBySideActive = function() { - var cm = this.codemirror; - var wrapper = cm.getWrapperElement(); - var preview = wrapper.nextSibling; - - return /editor-preview-active-side/.test(preview.className); -}; - -SimpleMDE.prototype.isFullscreenActive = function() { - var cm = this.codemirror; - - return cm.getOption("fullScreen"); -}; - -SimpleMDE.prototype.getState = function() { - var cm = this.codemirror; - - return getState(cm); -}; - -SimpleMDE.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.autosaveTimeoutId) { + clearTimeout(this.autosaveTimeoutId); + this.autosaveTimeoutId = undefined; + this.clearAutosavedValue(); } - 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 = SimpleMDE; \ No newline at end of file +} \ No newline at end of file diff --git a/src/js/utils.js b/src/js/utils.js new file mode 100644 index 0000000..a627ed0 --- /dev/null +++ b/src/js/utils.js @@ -0,0 +1,69 @@ +/** + * Created by WittBulter on 2017/1/17. + */ +import {bindings} from './metadata' + +export default new class Utils { + constructor (){} + + isMac (){ + return navigator.platform.includes('Mac') + } + + isMobile (){ + let 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; + } + + /* The right word count in respect for CJK. */ + wordCount (data){ + const pattern = /[a-zA-Z0-9_\u0392-\u03c9\u0410-\u04F9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g; + const m = data.match(pattern); + let count = 0; + if(m === null) return count; + for(let i = 0; i < m.length; i++) { + if(m[i].charCodeAt(0) >= 0x4E00) { + count += m[i].length; + } else { + count += 1; + } + } + return count; + } + + getBindingName (f){ + for(let key of bindings) { + if(bindings[key] === f) { + return key; + } + } + return null; + }; + + /** + * Fix shortcut. Mac use Command, others use Ctrl. + */ + fixShortcut (name){ + name = this.isMac() ? name.replace("Ctrl", "Cmd") : name.replace("Cmd", "Ctrl") + return name; + } + + // 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. + isLocalStorageAvailable (){ + if(typeof localStorage === "object") { + try { + localStorage.setItem("smde_localStorage", 1); + localStorage.removeItem("smde_localStorage"); + } catch(e) { + return false; + } + } else { + return false; + } + + return true; + } +} \ No newline at end of file