diff --git a/markdownx/.static/markdownx/js/markdownx.ts b/markdownx/.static/markdownx/js/markdownx.ts index 873295f..3ac8c76 100644 --- a/markdownx/.static/markdownx/js/markdownx.ts +++ b/markdownx/.static/markdownx/js/markdownx.ts @@ -21,6 +21,51 @@ declare function docReady(args: any): any; +interface ImageUploadResponse { + image_code?: string, + image_path?: string, + [propName: string]: any; +} + +interface HandlerFunction { + (properties: { + start: number, + end: number, + value: string + }): string +} + +interface KeyboardEvents { + keys: { + TAB: string, + DUPLICATE: string, + UNINDENT: string, + INDENT: string + }, + handlers: { + _multiLineIndentation: HandlerFunction, + applyTab: HandlerFunction, + applyIndentation: HandlerFunction, + removeIndentation: HandlerFunction, + removeTab: HandlerFunction, + applyDuplication: HandlerFunction + }, + hub: Function +} + +interface EventHandlers { + inhibitDefault: Function, + onDragEnter: Function +} + +interface MarkdownxProperties { + parent: HTMLElement, + editor: HTMLTextAreaElement, + preview: HTMLElement, + _latency: number | null, + _editorIsResizable: Boolean | null +} + import { Request, mountEvents, @@ -41,7 +86,10 @@ const UPLOAD_URL_ATTRIBUTE: string = "data-markdownx-upload-urls-path", // --------------------------------------------------------------------------------------------------------------------- -const EventHandlers = { +/** + * + */ +const EventHandlers: EventHandlers = { /** * Routine tasks for event handlers (e.g. default preventions). @@ -74,175 +122,273 @@ const EventHandlers = { }; -const keyboardEvents = { +/** + * + */ +const keyboardEvents: KeyboardEvents = { /** - * + * Custom hotkeys. */ keys: { - - TAB: "Tab", + TAB: "Tab", DUPLICATE: "d", - UNINDENT: "[", - INDENT: "]" - + UNINDENT: "[", + INDENT: "]" }, /** - * + * Hotkey response functions. */ handlers: { /** + * Smart application of tab indentations under various conditions. * - * @param {number} start - * @param {number} end - * @param {string} value * @returns {string} - * @private */ - applyTab: function (start: number, end: number, value: string): string { + applyTab: function (properties) { - return value.substring(0, start) + ( - value.substring(start, end).match(/\n/g) === null ? - `\t${value.substring(start)}` : - value.substring(start, end).replace(/^/gm, '\t') + value.substring(end) - ) - - }, - - - /** - * - * @param start - * @param end - * @param value - * @returns {string} - * @private - */ - _multiLineIndentation: function (start: number, end: number, value: string): string { - - const endLine: string = new RegExp(`(?:\n|.){0,${end}}(^.*$)`, "m").exec(value)[1]; - - return value.substring( - value.indexOf( - new RegExp(`(?:\n|.){0,${start}}(^.*$)`, "m").exec(value)[1] // Start line. - ), - (value.indexOf(endLine) ? value.indexOf(endLine) + endLine.length : end) - ); + // Do not replace with variables; this + // feature is optimised for swift response. + return properties.value + .substring(0, properties.start) + // Preceding text. + ( + properties.value + .substring(properties.start, properties.end) // Selected text + .match(/\n/gm) === null ? // Not multi line? + `\t${properties.value.substring(properties.start)}` : // Add `\t`. + properties.value // Otherwise: + .substring(properties.start, properties.end) + .replace(/^/gm, '\t') + // Add `\t` to be beginning of each line. + properties.value.substring(properties.end) // Succeeding text. + ) }, /** + * Smart removal of tab indentations. * - * @param start - * @param end - * @param value + * @param {JSON} properties * @returns {string} - * @private */ - applyIndentation: function (start: number, end: number, value: string): string { + removeTab: function (properties) { - if (start === end) { - const line: string = new RegExp(`(?:\n|.){0,${start}}(^.+$)`, "m").exec(value)[1]; - return value.replace(line, `\t${line}`) - } + let substitution: string = null, + lineTotal: number = ( + properties.value + .substring( + properties.start, + properties.end + ).match(/\n/g) || [] // Number of lines (\n) or empty array (zero). + ).length; // Length of the array is equal to the number of lines. - const content: string = this._multiLineIndentation(start, end, value); + if (properties.start === properties.end) { - return value.replace(content, content.replace(/(^.+$)\n*/gmi, "\t$&")) + // Replacing `\t` at a specific location + // (+/- 1 chars) where there is no selection. + properties.start = + properties.start > 0 && + properties.value[properties.start - 1] // -1 is to account any tabs just before the cursor. + .match(/\t/) !== null ? // if there's no `\t`, check the preceding character. + properties.start - 1 : properties.start; - }, + substitution = properties.value + .substring(properties.start) + .replace("\t", ''); // Remove only a single `\t`. - /** - * - * @param start - * @param end - * @param value - * @returns {string} - * @private - */ - removeIndentation: function (start: number, end: number, value: string): string { - - if (start === end) { - const line: string = new RegExp(`(?:\n|.){0,${start}}(^\t.+$)`, "m").exec(value)[1]; - return value.replace(line, line.substring(1)) - } - - const content: string = this._multiLineIndentation(start, end, value); - - return value.replace(content, content.replace(/^\t(.+)\n*$/gmi, "$1")) - - }, - - /** - * - * @param {number} start - * @param {number} end - * @param {string} value - * @returns {string} - * @private - */ - removeTab: function (start: number, end: number, value: string): string { - - let endString: string = null, - lineNumbers: number = (value.substring(start, end).match(/\n/g) || []).length; - - if (start === end) { - - // Replacing `\t` at a specific location (+/- 1 chars) where there is no selection. - start = start > 0 && value[start - 1].match(/\t/) !== null ? start - 1 : start; - endString = value.substring(start).replace(/\t/, ''); - - } else if (!lineNumbers) { + } else if (!lineTotal) { // Replacing `\t` within a single line selection. - endString = value.substring(start).replace(/\t/, '') - + substitution = + properties.value + .substring(properties.start) + .replace("\t", '') } else { - // Replacing `\t` in the beginning of each line in a multi-line selection. - endString = value.substring(start, end).replace(/^\t/gm, '') + value.substring(end, value.length); + // Replacing `\t` in the beginning of each line + // in a multi-line selection. + substitution = + properties.value.substring( + properties.start, + properties.end + ).replace(/^\t/gm, '') + // Selection. + properties.value.substring(properties.end); // After the selection } - return value.substring(0, start) + endString; + return properties.value + .substring(0, properties.start) + // Text preceding to selection / cursor. + substitution }, /** + * Handles multi line indentations. * - * @param {number} start - * @param {number} end - * @param {string} value + * @param properties * @returns {string} * @private */ - applyDuplication: function (start: number, end: number, value: string): string { + _multiLineIndentation: function (properties) { - // Selected. - if (start !== end) return ( - value.substring(0, start) + - value.substring(start, end) + - (~value.charAt(start - 1).indexOf('\n') || ~value.charAt(start).indexOf('\n') ? '\n' : '') + - value.substring(start, end) + - value.substring(end) + // Last line in the selection; regardless of + // where of not the entire line is selected. + const endLine: string = + new RegExp(`(?:\n|.){0,${properties.end}}(^.*$)`, "m") + .exec(properties.value)[1]; + + // Do not replace with variables; this + // feature is optimised for swift response. + return properties.value.substring( + // First line of the selection, regardless of + // where or not the entire line is selected. + properties.value.indexOf( + new RegExp(`(?:\n|.){0,${properties.start}}(^.*$)`, "m") + .exec(properties.value)[1] // Start line. + ), ( + // If there is a last line in a multi line selected + // value where the last line is not empty or `\n`: + properties.value.indexOf(endLine) ? + // Location where the last line finishes with + // respect to the entire value. + properties.value.indexOf(endLine) + endLine.length : + // Otherwise, where the selection ends. + properties.end + ) ); - // Not selected. - let pattern: RegExp = new RegExp(`(?:.|\n){0,${end}}\n([^].+)(?:.|\n)*`, 'm'), - line: string = ''; + }, - value.replace(pattern, (match, p1) => line += p1); + /** + * Smart application of indentation at the beginning of the line. + * + * @returns {string} + */ + applyIndentation: function (properties) { - return value.replace(line, `${line}\n${line}`) + // Single line? + if (properties.start === properties.end) { + // Current line, from the beginning to the end, regardless of any selections. + const line: string = + new RegExp(`(?:\n|.){0,${properties.start}}(^.+$)`, "m") + .exec(properties.value)[1]; + + return properties.value.replace(line, `\t${line}`) + } + + // Multi line + const content: string = this._multiLineIndentation({ + start: properties.start, + end: properties.end, + value: properties.value + }); + + return properties.value + .replace( + content, // Existing contents. + content.replace(/(^.+$)\n*/gmi, "\t$&") // Indented contents. + ) + + }, + + /** + * Smart removal of indentation from the beginning of the line. + * + * @returns {string} + */ + removeIndentation: function (properties) { + + // Single Line + if (properties.start === properties.end) { + // Entire line where the line immediately begins + // with a one or more `\t`, regardless of any + // selections. + const line: string = + new RegExp(`(?:\n|.){0,${properties.start}}(^\t.+$)`, "m") + .exec(properties.value)[1]; + + return properties.value + .replace( + line, // Existing content. + line.substring(1) // First character (necessarily a `\t`) removed. + ) + } + + // Multi line + const content: string = this._multiLineIndentation({ + start: properties.start, + end: properties.end, + value: properties.value + }); + + return properties.value + .replace( + content, // Existing content. + content.replace(/^\t(.+)\n*$/gmi, "$1") // A single `\t` removed from the beginning. + ) + + }, + + /** + * Duplication of the current or selected lines. + * + * @returns {string} + */ + applyDuplication: function (properties) { + + // With selection. + // Do not replace with variables. This + // feature is optimised for swift response. + if (properties.start !== properties.end) + return ( + properties.value.substring( // Text preceding the selected area. + 0, + properties.start + ) + + properties.value.substring( // Selected area + properties.start, + properties.end + ) + + ( + ~properties.value // First character before the cursor is linebreak? + .charAt(properties.start - 1) + .indexOf('\n') || // --> or + ~properties.value // Character on the cursor is linebreak? + .charAt(properties.start) + .indexOf('\n') ? '\n' : '' // If either, add linebreak, otherwise add nothing. + ) + + properties.value.substring( // Selected area (again for duplication). + properties.start, + properties.end + ) + + properties.value.substring(properties.end) // Text succeeding the selected area. + ); + + // Without selection. + let pattern: RegExp = // Separate lines up to the end of the current line. + new RegExp(`(?:.|\n){0,160}(^.*$)`, 'm'), + line: string = ''; + + // Add anything found to the `line`. Note that + // `replace` is used a simple hack; it functions + // in a similar way to `regex.search` in Python. + properties.value + .replace(pattern, (match, p1) => line += p1); + + return properties.value + .replace( + line, // Existing line. + `${line}\n${line}` // Doubled ... magic! + ) }, }, /** + * Mapping of hotkeys from keyboard events to their corresponding functions. * * @param {KeyboardEvent} event * @returns {Function | Boolean} @@ -267,6 +413,8 @@ const keyboardEvents = { return (event.ctrlKey || event.metaKey) ? this.handlers.removeIndentation : false; default: + // default would prevent the + // inhibition of default settings. return false } @@ -311,10 +459,13 @@ function updateHeight(editor: HTMLTextAreaElement): HTMLTextAreaElement { /** * @example * - * let editor = document.getElementById('MyMarkdownEditor'), - * preview = document.getElementById('MyMarkdownPreview'); + * let element = document.getElementsByClassName('markdownx'); * - * let mdx = new MarkdownX(editor, preview) + * new MarkdownX( + * element, + * element.querySelector('.markdownx-editor'), + * element.querySelector('.markdownx-preview') + * ) * * @param {HTMLElement} parent - Markdown editor element. * @param {HTMLTextAreaElement} editor - Markdown editor element. @@ -322,7 +473,7 @@ function updateHeight(editor: HTMLTextAreaElement): HTMLTextAreaElement { */ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, preview: HTMLElement): void { - const properties = { + const properties: MarkdownxProperties = { editor: editor, preview: preview, @@ -380,7 +531,7 @@ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, pr // If `true`, the editor will expand to scrollHeight when needed. properties._editorIsResizable = - (properties.editor.getAttribute(RESIZABILITY_ATTRIBUTE).match(/True/i) || []).length > 0; + (properties.editor.getAttribute(RESIZABILITY_ATTRIBUTE).match(/true/i) || []).length > 0; getMarkdown(); @@ -445,11 +596,11 @@ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, pr // Holding the start location before anything changes. const SELECTION_START: number = properties.editor.selectionStart; - properties.editor.value = handlerFunc( - properties.editor.selectionStart, - properties.editor.selectionEnd, - properties.editor.value - ); + properties.editor.value = handlerFunc({ + start: properties.editor.selectionStart, + end: properties.editor.selectionEnd, + value: properties.editor.value + }); _markdownify(); @@ -475,9 +626,9 @@ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, pr preparePostData({image: file}) // Data ); - xhr.success = (resp: string): void => { + xhr.success = (resp: string): void | null => { - const response = JSON.parse(resp); + const response: ImageUploadResponse = JSON.parse(resp); if (response.image_code) { @@ -493,16 +644,16 @@ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, pr } else { console.error(XHR_RESPONSE_ERROR, response); - triggerCustomEvent('markdownx.fileUploadError', properties.parent, [response]) + triggerCustomEvent('markdownx.fileUploadError', properties.parent, [response]); + return null; } - properties.preview.innerHTML = this.response; properties.editor.style.opacity = NORMAL_OPACITY; }; - xhr.error = (response: string): void => { + xhr.error = (response: any): void => { properties.editor.style.opacity = NORMAL_OPACITY; console.error(response); @@ -533,7 +684,7 @@ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, pr }; - xhr.error = (response: string): void => { + xhr.error = (response: any): void => { console.error(response); @@ -549,16 +700,16 @@ const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, pr * * @param textToInsert */ - const insertImage = (textToInsert): void => { + const insertImage = (textToInsert: string): void => { - let cursorPosition = properties.editor.selectionStart, - text = properties.editor.value, - textBeforeCursor = text.substring(0, cursorPosition), - textAfterCursor = text.substring(cursorPosition, text.length); + properties.editor.value = + `${properties.editor.value.substring(0, properties.editor.selectionStart)}\n\n` + // Preceding text. + textToInsert + + `\n\n${properties.editor.value.substring(properties.editor.selectionEnd)}`; // Succeeding text. - properties.editor.value = `${textBeforeCursor}${textToInsert}${textAfterCursor}`; - properties.editor.selectionStart = cursorPosition + textToInsert.length; - properties.editor.selectionEnd = cursorPosition + textToInsert.length; + properties.editor.selectionStart = + properties.editor.selectionEnd = + properties.editor.selectionStart + textToInsert.length; triggerEvent(properties.editor, 'keyup'); inputChanged(); diff --git a/markdownx/.static/markdownx/js/utils.ts b/markdownx/.static/markdownx/js/utils.ts index bb4d0f0..9c705b8 100644 --- a/markdownx/.static/markdownx/js/utils.ts +++ b/markdownx/.static/markdownx/js/utils.ts @@ -58,12 +58,21 @@ export function zip (...rows) { } +interface EventListener { + object: Element | Document, + listeners: { + type: string, + listener: any, + capture: boolean, + } [] +} + /** * * @param collections * @returns */ -export function mountEvents (...collections): any { +export function mountEvents (...collections: EventListener[]): any[] { return collections.map(events => events.listeners.map(series => diff --git a/markdownx/static/markdownx/js/markdownx.js b/markdownx/static/markdownx/js/markdownx.js index 60aa0eb..411b6ab 100644 --- a/markdownx/static/markdownx/js/markdownx.js +++ b/markdownx/static/markdownx/js/markdownx.js @@ -22,6 +22,9 @@ var utils_1 = require("./utils"); var UPLOAD_URL_ATTRIBUTE = "data-markdownx-upload-urls-path", PROCESSING_URL_ATTRIBUTE = "data-markdownx-urls-path", RESIZABILITY_ATTRIBUTE = "data-markdownx-editor-resizable", LATENCY_ATTRIBUTE = "data-markdownx-latency", LATENCY_MINIMUM = 500, // microseconds. XHR_RESPONSE_ERROR = "Invalid response", UPLOAD_START_OPACITY = "0.3", NORMAL_OPACITY = "1"; // --------------------------------------------------------------------------------------------------------------------- +/** + * + */ var EventHandlers = { /** * Routine tasks for event handlers (e.g. default preventions). @@ -44,6 +47,9 @@ var EventHandlers = { return this.inhibitDefault(event); } }; +/** + * + */ var keyboardEvents = { /** * @@ -60,107 +66,173 @@ var keyboardEvents = { handlers: { /** * - * @param {number} start - * @param {number} end - * @param {string} value * @returns {string} - * @private */ - applyTab: function (start, end, value) { - return value.substring(0, start) + (value.substring(start, end).match(/\n/g) === null ? - "\t" + value.substring(start) : - value.substring(start, end).replace(/^/gm, '\t') + value.substring(end)); + applyTab: function (properties) { + // Do not replace with variables; this + // feature is optimised for swift response. + return properties.value + .substring(0, properties.start) + + (properties.value + .substring(properties.start, properties.end) // Selected text + .match(/\n/gm) === null ? + "\t" + properties.value.substring(properties.start) : + properties.value // Otherwise: + .substring(properties.start, properties.end) + .replace(/^/gm, '\t') + + properties.value.substring(properties.end) // Succeeding text. + ); }, /** * - * @param start - * @param end - * @param value * @returns {string} - * @private */ - _multiLineIndentation: function (start, end, value) { - var endLine = new RegExp("(?:\n|.){0," + end + "}(^.*$)", "m").exec(value)[1]; - return value.substring(value.indexOf(new RegExp("(?:\n|.){0," + start + "}(^.*$)", "m").exec(value)[1] // Start line. - ), (value.indexOf(endLine) ? value.indexOf(endLine) + endLine.length : end)); + _multiLineIndentation: function (properties) { + // Last line in the selection; regardless of + // where of not the entire line is selected. + var endLine = new RegExp("(?:\n|.){0," + properties.end + "}(^.*$)", "m") + .exec(properties.value)[1]; + // Do not replace with variables; this + // feature is optimised for swift response. + return properties.value.substring( + // First line of the selection, regardless of + // where or not the entire line is selected. + properties.value.indexOf(new RegExp("(?:\n|.){0," + properties.start + "}(^.*$)", "m") + .exec(properties.value)[1] // Start line. + ), ( + // If there is a last line in a multi line selected + // value where the last line is not empty or `\n`: + properties.value.indexOf(endLine) ? + // Location where the last line finishes with + // respect to the entire value. + properties.value.indexOf(endLine) + endLine.length : + // Otherwise, where the selection ends. + properties.end)); }, /** * - * @param start - * @param end - * @param value * @returns {string} - * @private */ - applyIndentation: function (start, end, value) { - if (start === end) { - var line = new RegExp("(?:\n|.){0," + start + "}(^.+$)", "m").exec(value)[1]; - return value.replace(line, "\t" + line); + applyIndentation: function (properties) { + // Single line? + if (properties.start === properties.end) { + // Current line, from the beginning to the end, regardless of any selections. + var line = new RegExp("(?:\n|.){0," + properties.start + "}(^.+$)", "m") + .exec(properties.value)[1]; + return properties.value.replace(line, "\t" + line); } - var content = this._multiLineIndentation(start, end, value); - return value.replace(content, content.replace(/(^.+$)\n*/gmi, "\t$&")); + // Multi line + var content = this._multiLineIndentation({ + start: properties.start, + end: properties.end, + value: properties.value + }); + return properties.value + .replace(content, // Existing contents. + content.replace(/(^.+$)\n*/gmi, "\t$&") // Indented contents. + ); }, /** * - * @param start - * @param end - * @param value * @returns {string} - * @private */ - removeIndentation: function (start, end, value) { - if (start === end) { - var line = new RegExp("(?:\n|.){0," + start + "}(^\t.+$)", "m").exec(value)[1]; - return value.replace(line, line.substring(1)); + removeIndentation: function (properties) { + // Single Line + if (properties.start === properties.end) { + // Entire line where the line immediately begins + // with a one or more `\t`, regardless of any + // selections. + var line = new RegExp("(?:\n|.){0," + properties.start + "}(^\t.+$)", "m") + .exec(properties.value)[1]; + return properties.value + .replace(line, // Existing content. + line.substring(1) // First character (necessarily a `\t`) removed. + ); } - var content = this._multiLineIndentation(start, end, value); - return value.replace(content, content.replace(/^\t(.+)\n*$/gmi, "$1")); + // Multi line + var content = this._multiLineIndentation({ + start: properties.start, + end: properties.end, + value: properties.value + }); + return properties.value + .replace(content, // Existing content. + content.replace(/^\t(.+)\n*$/gmi, "$1") // A single `\t` removed from the beginning. + ); }, /** * - * @param {number} start - * @param {number} end - * @param {string} value * @returns {string} - * @private */ - removeTab: function (start, end, value) { - var endString = null, lineNumbers = (value.substring(start, end).match(/\n/g) || []).length; - if (start === end) { - // Replacing `\t` at a specific location (+/- 1 chars) where there is no selection. - start = start > 0 && value[start - 1].match(/\t/) !== null ? start - 1 : start; - endString = value.substring(start).replace(/\t/, ''); + removeTab: function (properties) { + var substitution = null, lineTotal = (properties.value + .substring(properties.start, properties.end).match(/\n/g) || [] // Number of lines (\n) or empty array (zero). + ).length; // Length of the array is equal to the number of lines. + if (properties.start === properties.end) { + // Replacing `\t` at a specific location + // (+/- 1 chars) where there is no selection. + properties.start = + properties.start > 0 && + properties.value[properties.start - 1] // -1 is to account any tabs just before the cursor. + .match(/\t/) !== null ? + properties.start - 1 : properties.start; + substitution = properties.value + .substring(properties.start) + .replace("\t", ''); // Remove only a single `\t`. } - else if (!lineNumbers) { + else if (!lineTotal) { // Replacing `\t` within a single line selection. - endString = value.substring(start).replace(/\t/, ''); + substitution = + properties.value + .substring(properties.start) + .replace("\t", ''); } else { - // Replacing `\t` in the beginning of each line in a multi-line selection. - endString = value.substring(start, end).replace(/^\t/gm, '') + value.substring(end, value.length); + // Replacing `\t` in the beginning of each line + // in a multi-line selection. + substitution = + properties.value.substring(properties.start, properties.end).replace(/^\t/gm, '') + + properties.value.substring(properties.end); // After the selection } - return value.substring(0, start) + endString; + return properties.value + .substring(0, properties.start) + + substitution; }, /** * - * @param {number} start - * @param {number} end - * @param {string} value * @returns {string} - * @private */ - applyDuplication: function (start, end, value) { - // Selected. - if (start !== end) - return (value.substring(0, start) + - value.substring(start, end) + - (~value.charAt(start - 1).indexOf('\n') || ~value.charAt(start).indexOf('\n') ? '\n' : '') + - value.substring(start, end) + - value.substring(end)); - // Not selected. - var pattern = new RegExp("(?:.|\n){0," + end + "}\n([^].+)(?:.|\n)*", 'm'), line = ''; - value.replace(pattern, function (match, p1) { return line += p1; }); - return value.replace(line, line + "\n" + line); + applyDuplication: function (properties) { + // With selection. + // Do not replace with variables. This + // feature is optimised for swift response. + if (properties.start !== properties.end) + return (properties.value.substring(// Text preceding the selected area. + 0, properties.start) + + properties.value.substring(// Selected area + properties.start, properties.end) + + (~properties.value // First character before the cursor is linebreak? + .charAt(properties.start - 1) + .indexOf('\n') || + ~properties.value // Character on the cursor is linebreak? + .charAt(properties.start) + .indexOf('\n') ? '\n' : '' // If either, add linebreak, otherwise add nothing. + ) + + properties.value.substring(// Selected area (again for duplication). + properties.start, properties.end) + + properties.value.substring(properties.end) // Text succeeding the selected area. + ); + // Without selection. + var pattern = new RegExp("(?:.|\n){0,160}(^.*$)", 'm'), line = ''; + // Add anything found to the `line`. Note that + // `replace` is used a simple hack; it functions + // in a similar way to `regex.search` in Python. + properties.value + .replace(pattern, function (match, p1) { return line += p1; }); + return properties.value + .replace(line, // Existing line. + line + "\n" + line // Doubled ... magic! + ); }, }, /** @@ -270,7 +342,7 @@ var MarkdownX = function (parent, editor, preview) { Math.max(parseInt(properties.editor.getAttribute(LATENCY_ATTRIBUTE)) || 0, LATENCY_MINIMUM); // If `true`, the editor will expand to scrollHeight when needed. properties._editorIsResizable = - (properties.editor.getAttribute(RESIZABILITY_ATTRIBUTE).match(/True/i) || []).length > 0; + (properties.editor.getAttribute(RESIZABILITY_ATTRIBUTE).match(/true/i) || []).length > 0; getMarkdown(); utils_1.triggerCustomEvent("markdownx.init"); }; @@ -314,7 +386,11 @@ var MarkdownX = function (parent, editor, preview) { EventHandlers.inhibitDefault(event); // Holding the start location before anything changes. var SELECTION_START = properties.editor.selectionStart; - properties.editor.value = handlerFunc(properties.editor.selectionStart, properties.editor.selectionEnd, properties.editor.value); + properties.editor.value = handlerFunc({ + start: properties.editor.selectionStart, + end: properties.editor.selectionEnd, + value: properties.editor.value + }); _markdownify(); properties.editor.focus(); // Set the cursor location to the start location of the selection. @@ -344,8 +420,8 @@ var MarkdownX = function (parent, editor, preview) { else { console.error(XHR_RESPONSE_ERROR, response); utils_1.triggerCustomEvent('markdownx.fileUploadError', properties.parent, [response]); + return null; } - properties.preview.innerHTML = _this.response; properties.editor.style.opacity = NORMAL_OPACITY; }; xhr.error = function (response) { @@ -378,10 +454,13 @@ var MarkdownX = function (parent, editor, preview) { * @param textToInsert */ var insertImage = function (textToInsert) { - var cursorPosition = properties.editor.selectionStart, text = properties.editor.value, textBeforeCursor = text.substring(0, cursorPosition), textAfterCursor = text.substring(cursorPosition, text.length); - properties.editor.value = "" + textBeforeCursor + textToInsert + textAfterCursor; - properties.editor.selectionStart = cursorPosition + textToInsert.length; - properties.editor.selectionEnd = cursorPosition + textToInsert.length; + properties.editor.value = + properties.editor.value.substring(0, properties.editor.selectionStart) + "\n\n" + + textToInsert + + ("\n\n" + properties.editor.value.substring(properties.editor.selectionEnd)); // Succeeding text. + properties.editor.selectionStart = + properties.editor.selectionEnd = + properties.editor.selectionStart + textToInsert.length; utils_1.triggerEvent(properties.editor, 'keyup'); inputChanged(); }; diff --git a/markdownx/static/markdownx/js/markdownx.min.js b/markdownx/static/markdownx/js/markdownx.min.js index 5c68988..74e8921 100644 --- a/markdownx/static/markdownx/js/markdownx.min.js +++ b/markdownx/static/markdownx/js/markdownx.min.js @@ -1 +1 @@ -(function e(t,r,n){function i(a,s){if(!r[a]){if(!t[a]){var u=typeof require=="function"&&require;if(!s&&u)return u(a,!0);if(o)return o(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var c=r[a]={exports:{}};t[a][0].call(c.exports,function(e){var r=t[a][1][e];return i(r?r:e)},c,c.exports,e,t,r,n)}return r[a].exports}var o=typeof require=="function"&&require;for(var a=0;a0&&r[e-1].match(/\t/)!==null?e-1:e;n=r.substring(e).replace(/\t/,"")}else if(!i){n=r.substring(e).replace(/\t/,"")}else{n=r.substring(e,t).replace(/^\t/gm,"")+r.substring(t,r.length)}return r.substring(0,e)+n},applyDuplication:function(e,t,r){if(e!==t)return r.substring(0,e)+r.substring(e,t)+(~r.charAt(e-1).indexOf("\n")||~r.charAt(e).indexOf("\n")?"\n":"")+r.substring(e,t)+r.substring(t);var n=new RegExp("(?:.|\n){0,"+t+"}\n([^].+)(?:.|\n)*","m"),i="";r.replace(n,function(e,t){return i+=t});return r.replace(i,i+"\n"+i)}},hub:function(e){switch(e.key){case this.keys.TAB:return e.shiftKey?this.handlers.removeTab:this.handlers.applyTab;case this.keys.DUPLICATE:return e.ctrlKey||e.metaKey?this.handlers.applyDuplication:false;case this.keys.INDENT:return e.ctrlKey||e.metaKey?this.handlers.applyIndentation:false;case this.keys.UNINDENT:return e.ctrlKey||e.metaKey?this.handlers.removeIndentation:false;default:return false}}};function h(e){return Math.max(parseInt(window.getComputedStyle(e).height),parseInt(e.style.height)||0)}function v(e){if(e.scrollTop)e.style.height=e.scrollTop+h(e)+"px";return e}var m=function(e,t,r){var h=this;var m={editor:t,preview:r,parent:e,_latency:null,_editorIsResizable:null};var g=function(){h.timeout=null;var e={object:document,listeners:[{type:"drop",capture:false,listener:f.inhibitDefault},{type:"dragover",capture:false,listener:f.inhibitDefault},{type:"dragenter",capture:false,listener:f.inhibitDefault},{type:"dragleave",capture:false,listener:f.inhibitDefault}]},r={object:m.editor,listeners:[{type:"drop",capture:false,listener:b},{type:"input",capture:true,listener:x},{type:"keydown",capture:true,listener:w},{type:"dragover",capture:false,listener:f.onDragEnter},{type:"dragenter",capture:false,listener:f.onDragEnter},{type:"dragleave",capture:false,listener:f.inhibitDefault},{type:"compositionstart",capture:true,listener:w}]};if(!t.style.maxHeight)t.style.maxHeight=window.innerHeight*75/100+"px";n.mountEvents(r,e);m.editor.style.transition="opacity 1s ease";m.editor.style.webkitTransition="opacity 1s ease";m._latency=Math.max(parseInt(m.editor.getAttribute(s))||0,u);m._editorIsResizable=(m.editor.getAttribute(a).match(/True/i)||[]).length>0;T();n.triggerCustomEvent("markdownx.init")};var y=function(){clearTimeout(h.timeout);h.timeout=setTimeout(T,m._latency)};var x=function(){m.editor=m._editorIsResizable?v(m.editor):m.editor;return y()};var b=function(e){if(e.dataTransfer&&e.dataTransfer.files.length)Object.keys(e.dataTransfer.files).map(function(t){return E(e.dataTransfer.files[t])});f.inhibitDefault(e)};var w=function(e){var t=p.hub(e);if(typeof t!="function")return false;f.inhibitDefault(e);var r=m.editor.selectionStart;m.editor.value=t(m.editor.selectionStart,m.editor.selectionEnd,m.editor.value);y();m.editor.focus();m.editor.selectionEnd=m.editor.selectionStart=r;return false};var E=function(e){m.editor.style.opacity=c;var t=new n.Request(m.editor.getAttribute(i),n.preparePostData({image:e}));t.success=function(e){var t=JSON.parse(e);if(t.image_code){k(t.image_code);n.triggerCustomEvent("markdownx.fileUploadEnd",m.parent,[t])}else if(t.image_path){k('![]("'+t.image_path+'")');n.triggerCustomEvent("markdownx.fileUploadEnd",m.parent,[t])}else{console.error(l,t);n.triggerCustomEvent("markdownx.fileUploadError",m.parent,[t])}m.preview.innerHTML=h.response;m.editor.style.opacity=d};t.error=function(e){m.editor.style.opacity=d;console.error(e);n.triggerCustomEvent("fileUploadError",m.parent,[e])};return t.send()};var T=function(){var e=new n.Request(m.editor.getAttribute(o),n.preparePostData({content:m.editor.value}));e.success=function(e){m.preview.innerHTML=e;m.editor=v(m.editor);n.triggerCustomEvent("markdownx.update",m.parent,[e])};e.error=function(e){console.error(e);n.triggerCustomEvent("markdownx.updateError",m.parent,[e])};return e.send()};var k=function(e){var t=m.editor.selectionStart,r=m.editor.value,i=r.substring(0,t),o=r.substring(t,r.length);m.editor.value=""+i+e+o;m.editor.selectionStart=t+e.length;m.editor.selectionEnd=t+e.length;n.triggerEvent(m.editor,"keyup");x()};g()};r.MarkdownX=m;(function(e,t){e=e||"docReady";t=t||window;var r=[],n=false,i=false;var o=function(){if(!n){n=true;r.map(function(e){return e.fn.call(window,e.ctx)});r=[]}};var a=function(){return document.readyState==="complete"?o():null};t[e]=function(e,t){if(n){setTimeout(function(){return e(t)},1);return}else{r.push({fn:e,ctx:t})}if(document.readyState==="complete"){setTimeout(o,1)}else if(!i){document.addEventListener("DOMContentLoaded",o,false);window.addEventListener("load",o,false);i=true}}})("docReady",window);docReady(function(){var e=document.getElementsByClassName("markdownx");return Object.keys(e).map(function(t){return new m(e[t],e[t].querySelector(".markdownx-editor"),e[t].querySelector(".markdownx-preview"))})})},{"./utils":2}],2:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:true});function n(e){if(document.cookie&&document.cookie.length){var t=document.cookie.split(";").filter(function(t){return t.indexOf(e+"=")!==-1})[0];try{return decodeURIComponent(t.trim().substring(e.length+1))}catch(t){if(t instanceof TypeError){console.info('No cookie with key "'+e+'". Wrong name?');return null}throw t}}return null}r.getCookie=n;function i(){var e=[];for(var t=0;t-1)r.splice(n,1);e.className=r.join(" ")}})}r.removeClass=f},{}]},{},[1]); \ No newline at end of file +(function e(t,r,n){function a(s,o){if(!r[s]){if(!t[s]){var u=typeof require=="function"&&require;if(!o&&u)return u(s,!0);if(i)return i(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var c=r[s]={exports:{}};t[s][0].call(c.exports,function(e){var r=t[s][1][e];return a(r?r:e)},c,c.exports,e,t,r,n)}return r[s].exports}var i=typeof require=="function"&&require;for(var s=0;s0&&e.value[e.start-1].match(/\t/)!==null?e.start-1:e.start;t=e.value.substring(e.start).replace("\t","")}else if(!r){t=e.value.substring(e.start).replace("\t","")}else{t=e.value.substring(e.start,e.end).replace(/^\t/gm,"")+e.value.substring(e.end)}return e.value.substring(0,e.start)+t},applyDuplication:function(e){if(e.start!==e.end)return e.value.substring(0,e.start)+e.value.substring(e.start,e.end)+(~e.value.charAt(e.start-1).indexOf("\n")||~e.value.charAt(e.start).indexOf("\n")?"\n":"")+e.value.substring(e.start,e.end)+e.value.substring(e.end);var t=new RegExp("(?:.|\n){0,160}(^.*$)","m"),r="";e.value.replace(t,function(e,t){return r+=t});return e.value.replace(r,r+"\n"+r)}},hub:function(e){switch(e.key){case this.keys.TAB:return e.shiftKey?this.handlers.removeTab:this.handlers.applyTab;case this.keys.DUPLICATE:return e.ctrlKey||e.metaKey?this.handlers.applyDuplication:false;case this.keys.INDENT:return e.ctrlKey||e.metaKey?this.handlers.applyIndentation:false;case this.keys.UNINDENT:return e.ctrlKey||e.metaKey?this.handlers.removeIndentation:false;default:return false}}};function v(e){return Math.max(parseInt(window.getComputedStyle(e).height),parseInt(e.style.height)||0)}function h(e){if(e.scrollTop)e.style.height=e.scrollTop+v(e)+"px";return e}var m=function(e,t,r){var v=this;var m={editor:t,preview:r,parent:e,_latency:null,_editorIsResizable:null};var g=function(){v.timeout=null;var e={object:document,listeners:[{type:"drop",capture:false,listener:f.inhibitDefault},{type:"dragover",capture:false,listener:f.inhibitDefault},{type:"dragenter",capture:false,listener:f.inhibitDefault},{type:"dragleave",capture:false,listener:f.inhibitDefault}]},r={object:m.editor,listeners:[{type:"drop",capture:false,listener:b},{type:"input",capture:true,listener:x},{type:"keydown",capture:true,listener:w},{type:"dragover",capture:false,listener:f.onDragEnter},{type:"dragenter",capture:false,listener:f.onDragEnter},{type:"dragleave",capture:false,listener:f.inhibitDefault},{type:"compositionstart",capture:true,listener:w}]};if(!t.style.maxHeight)t.style.maxHeight=window.innerHeight*75/100+"px";n.mountEvents(r,e);m.editor.style.transition="opacity 1s ease";m.editor.style.webkitTransition="opacity 1s ease";m._latency=Math.max(parseInt(m.editor.getAttribute(o))||0,u);m._editorIsResizable=(m.editor.getAttribute(s).match(/true/i)||[]).length>0;T();n.triggerCustomEvent("markdownx.init")};var y=function(){clearTimeout(v.timeout);v.timeout=setTimeout(T,m._latency)};var x=function(){m.editor=m._editorIsResizable?h(m.editor):m.editor;return y()};var b=function(e){if(e.dataTransfer&&e.dataTransfer.files.length)Object.keys(e.dataTransfer.files).map(function(t){return E(e.dataTransfer.files[t])});f.inhibitDefault(e)};var w=function(e){var t=p.hub(e);if(typeof t!="function")return false;f.inhibitDefault(e);var r=m.editor.selectionStart;m.editor.value=t({start:m.editor.selectionStart,end:m.editor.selectionEnd,value:m.editor.value});y();m.editor.focus();m.editor.selectionEnd=m.editor.selectionStart=r;return false};var E=function(e){m.editor.style.opacity=c;var t=new n.Request(m.editor.getAttribute(a),n.preparePostData({image:e}));t.success=function(e){var t=JSON.parse(e);if(t.image_code){k(t.image_code);n.triggerCustomEvent("markdownx.fileUploadEnd",m.parent,[t])}else if(t.image_path){k('![]("'+t.image_path+'")');n.triggerCustomEvent("markdownx.fileUploadEnd",m.parent,[t])}else{console.error(l,t);n.triggerCustomEvent("markdownx.fileUploadError",m.parent,[t]);return null}m.editor.style.opacity=d};t.error=function(e){m.editor.style.opacity=d;console.error(e);n.triggerCustomEvent("fileUploadError",m.parent,[e])};return t.send()};var T=function(){var e=new n.Request(m.editor.getAttribute(i),n.preparePostData({content:m.editor.value}));e.success=function(e){m.preview.innerHTML=e;m.editor=h(m.editor);n.triggerCustomEvent("markdownx.update",m.parent,[e])};e.error=function(e){console.error(e);n.triggerCustomEvent("markdownx.updateError",m.parent,[e])};return e.send()};var k=function(e){m.editor.value=m.editor.value.substring(0,m.editor.selectionStart)+"\n\n"+e+("\n\n"+m.editor.value.substring(m.editor.selectionEnd));m.editor.selectionStart=m.editor.selectionEnd=m.editor.selectionStart+e.length;n.triggerEvent(m.editor,"keyup");x()};g()};r.MarkdownX=m;(function(e,t){e=e||"docReady";t=t||window;var r=[],n=false,a=false;var i=function(){if(!n){n=true;r.map(function(e){return e.fn.call(window,e.ctx)});r=[]}};var s=function(){return document.readyState==="complete"?i():null};t[e]=function(e,t){if(n){setTimeout(function(){return e(t)},1);return}else{r.push({fn:e,ctx:t})}if(document.readyState==="complete"){setTimeout(i,1)}else if(!a){document.addEventListener("DOMContentLoaded",i,false);window.addEventListener("load",i,false);a=true}}})("docReady",window);docReady(function(){var e=document.getElementsByClassName("markdownx");return Object.keys(e).map(function(t){return new m(e[t],e[t].querySelector(".markdownx-editor"),e[t].querySelector(".markdownx-preview"))})})},{"./utils":2}],2:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:true});function n(e){if(document.cookie&&document.cookie.length){var t=document.cookie.split(";").filter(function(t){return t.indexOf(e+"=")!==-1})[0];try{return decodeURIComponent(t.trim().substring(e.length+1))}catch(t){if(t instanceof TypeError){console.info('No cookie with key "'+e+'". Wrong name?');return null}throw t}}return null}r.getCookie=n;function a(){var e=[];for(var t=0;t-1)r.splice(n,1);e.className=r.join(" ")}})}r.removeClass=f},{}]},{},[1]); \ No newline at end of file