mirror of
https://github.com/Hopiu/django-markdownx.git
synced 2026-03-17 05:50:23 +00:00
853 lines
28 KiB
TypeScript
853 lines
28 KiB
TypeScript
/**
|
|
* **Markdownx**
|
|
*
|
|
* Frontend (JavaScript) management of Django-MarkdownX package.
|
|
*
|
|
* Written in JavaScript ECMA 2016, trans-compiled to ECMA 5 (2011).
|
|
*
|
|
* Requirements:
|
|
* - Modern browser with support for HTML5 and ECMA 2011+ (IE 10+). Older browsers would work but some
|
|
* features may be missing
|
|
* - TypeScript 2 +
|
|
*
|
|
* JavaScript ECMA 5 files formatted as `.js` are trans-compiled files. Please do not edit such files as all
|
|
* changes will be lost. Please modify `.ts` stored in `django-markdownx/markdownx/.static/markdownx/js` directory.
|
|
* See **Contributions** in the documentations for additional instructions.
|
|
*
|
|
* @Copyright 2017 - Adi, Pouria Hadjibagheri.
|
|
*/
|
|
|
|
// Import, definitions and constant ------------------------------------------------------------------------------------
|
|
|
|
"use strict";
|
|
|
|
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,
|
|
triggerEvent,
|
|
preparePostData,
|
|
triggerCustomEvent
|
|
} from "./utils";
|
|
|
|
const UPLOAD_URL_ATTRIBUTE: string = "data-markdownx-upload-urls-path",
|
|
PROCESSING_URL_ATTRIBUTE: string = "data-markdownx-urls-path",
|
|
RESIZABILITY_ATTRIBUTE: string = "data-markdownx-editor-resizable",
|
|
LATENCY_ATTRIBUTE: string = "data-markdownx-latency",
|
|
LATENCY_MINIMUM: number = 500, // microseconds.
|
|
XHR_RESPONSE_ERROR: string = "Invalid response",
|
|
UPLOAD_START_OPACITY: string = "0.3",
|
|
NORMAL_OPACITY: string = "1";
|
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
const EventHandlers: EventHandlers = {
|
|
|
|
/**
|
|
* Routine tasks for event handlers (e.g. default preventions).
|
|
*
|
|
* @param {Event} event
|
|
* @returns {Event}
|
|
*/
|
|
inhibitDefault: function (event: Event | KeyboardEvent): any {
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
return event
|
|
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {DragEvent} event
|
|
* returns {Event}
|
|
*/
|
|
onDragEnter: function (event: DragEvent): Event {
|
|
|
|
event.dataTransfer.dropEffect = 'copy';
|
|
|
|
return EventHandlers.inhibitDefault(event)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
const keyboardEvents: KeyboardEvents = {
|
|
|
|
/**
|
|
* Custom hotkeys.
|
|
*/
|
|
keys: {
|
|
TAB: "Tab",
|
|
DUPLICATE: "d",
|
|
UNINDENT: "[",
|
|
INDENT: "]"
|
|
},
|
|
|
|
/**
|
|
* Hotkey response functions.
|
|
*/
|
|
handlers: {
|
|
|
|
/**
|
|
* Smart application of tab indentations under various conditions.
|
|
*
|
|
* @param {JSON} properties
|
|
* @returns {string}
|
|
*/
|
|
applyTab: function (properties) {
|
|
|
|
// 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 {JSON} properties
|
|
* @returns {string}
|
|
*/
|
|
removeTab: function (properties) {
|
|
|
|
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.
|
|
|
|
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 ? // 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`.
|
|
|
|
} else if (!lineTotal) {
|
|
|
|
// Replacing `\t` within a single line selection.
|
|
substitution =
|
|
properties.value
|
|
.substring(properties.start)
|
|
.replace("\t", '')
|
|
|
|
} else {
|
|
|
|
// 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 properties.value
|
|
.substring(0, properties.start) + // Text preceding to selection / cursor.
|
|
substitution
|
|
|
|
},
|
|
|
|
/**
|
|
* Handles multi line indentations.
|
|
*
|
|
* @param {JSON} properties
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
_multiLineIndentation: function (properties) {
|
|
|
|
// 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
|
|
// whether 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
|
|
)
|
|
);
|
|
|
|
},
|
|
|
|
/**
|
|
* Smart application of indentation at the beginning of the line.
|
|
*
|
|
* @param {JSON} properties
|
|
* @returns {string}
|
|
*/
|
|
applyIndentation: function (properties) {
|
|
|
|
// 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.
|
|
*
|
|
* @param {JSON} properties
|
|
* @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.
|
|
*
|
|
* @param {JSON} properties
|
|
* @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}
|
|
*/
|
|
hub: function (event: KeyboardEvent): Function | false {
|
|
|
|
switch (event.key) {
|
|
case this.keys.TAB: // Tab.
|
|
// Shift pressed: un-indent, otherwise indent.
|
|
return event.shiftKey ? this.handlers.removeTab : this.handlers.applyTab;
|
|
|
|
case this.keys.DUPLICATE: // Line duplication.
|
|
// Is CTRL or CMD (on Mac) pressed?
|
|
return (event.ctrlKey || event.metaKey) ? this.handlers.applyDuplication : false;
|
|
|
|
case this.keys.INDENT: // Indentation.
|
|
// Is CTRL or CMD (on Mac) pressed?
|
|
return (event.ctrlKey || event.metaKey) ? this.handlers.applyIndentation : false;
|
|
|
|
case this.keys.UNINDENT: // Unindentation.
|
|
// Is CTRL or CMD (on Mac) pressed?
|
|
return (event.ctrlKey || event.metaKey) ? this.handlers.removeIndentation : false;
|
|
|
|
default:
|
|
// default would prevent the
|
|
// inhibition of default settings.
|
|
return false
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
/**
|
|
* Get either the height of an element as defined in style/CSS or its browser-computed height.
|
|
*
|
|
* @param {HTMLElement} element
|
|
* @returns {number}
|
|
*/
|
|
function getHeight (element: HTMLElement): number {
|
|
|
|
return Math.max( // Maximum of computed or set heights.
|
|
parseInt(window.getComputedStyle(element).height), // Height is not set in styles.
|
|
(parseInt(element.style.height) || 0) // Property's own height if set, otherwise 0.
|
|
)
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Update the height of an element based on its scroll height.
|
|
*
|
|
* @param {HTMLTextAreaElement} editor
|
|
* @returns {HTMLTextAreaElement}
|
|
*/
|
|
function updateHeight(editor: HTMLTextAreaElement): HTMLTextAreaElement {
|
|
|
|
// Ensure that the editor is resizable before anything else.
|
|
// Change size if scroll is larger that height, otherwise do nothing.
|
|
|
|
if (editor.scrollTop)
|
|
editor.style.height = `${editor.scrollTop + getHeight(editor)}px`;
|
|
|
|
return editor
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* @example
|
|
*
|
|
* let element = document.getElementsByClassName('markdownx');
|
|
*
|
|
* new MarkdownX(
|
|
* element,
|
|
* element.querySelector('.markdownx-editor'),
|
|
* element.querySelector('.markdownx-preview')
|
|
* )
|
|
*
|
|
* @param {HTMLElement} parent - Markdown editor element.
|
|
* @param {HTMLTextAreaElement} editor - Markdown editor element.
|
|
* @param {HTMLElement} preview - Markdown preview element.
|
|
*/
|
|
const MarkdownX = function (parent: HTMLElement, editor: HTMLTextAreaElement, preview: HTMLElement): void {
|
|
|
|
/**
|
|
* MarkdownX properties.
|
|
*/
|
|
const properties: MarkdownxProperties = {
|
|
|
|
editor: editor,
|
|
preview: preview,
|
|
parent: parent,
|
|
_latency: null,
|
|
_editorIsResizable: null
|
|
|
|
};
|
|
|
|
/**
|
|
* Initialisation settings (mounting events, retrieval of initial data,
|
|
* setting animation properties, latency, timeout, and resizability).
|
|
*
|
|
* @private
|
|
*/
|
|
const _initialize = () => {
|
|
|
|
this.timeout = null;
|
|
|
|
// Events
|
|
// ----------------------------------------------------------------------------------------------
|
|
let documentListeners = {
|
|
object: document,
|
|
listeners: [
|
|
{ type: "drop" , capture: false, listener: EventHandlers.inhibitDefault },
|
|
{ type: "dragover" , capture: false, listener: EventHandlers.inhibitDefault },
|
|
{ type: "dragenter", capture: false, listener: EventHandlers.inhibitDefault },
|
|
{ type: "dragleave", capture: false, listener: EventHandlers.inhibitDefault }
|
|
]
|
|
},
|
|
editorListeners = {
|
|
object: properties.editor,
|
|
listeners: [
|
|
{ type: "drop", capture: false, listener: onDrop },
|
|
{ type: "input", capture: true , listener: inputChanged },
|
|
{ type: "keydown", capture: true , listener: onKeyDown },
|
|
{ type: "dragover", capture: false, listener: EventHandlers.onDragEnter },
|
|
{ type: "dragenter", capture: false, listener: EventHandlers.onDragEnter },
|
|
{ type: "dragleave", capture: false, listener: EventHandlers.inhibitDefault },
|
|
{ type: "compositionstart", capture: true , listener: onKeyDown }
|
|
]
|
|
};
|
|
|
|
// Initialise
|
|
// --------------------------------------------------------
|
|
|
|
// Mounting the defined events.
|
|
mountEvents(editorListeners, documentListeners);
|
|
|
|
properties.editor.setAttribute('data-markdownx-init', '');
|
|
|
|
// Set animation for image uploads lock down.
|
|
properties.editor.style.transition = "opacity 1s ease";
|
|
properties.editor.style.webkitTransition = "opacity 1s ease";
|
|
|
|
// Upload latency - must be a value >= 500 microseconds.
|
|
properties._latency =
|
|
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.offsetHeight > 0 &&
|
|
properties.editor.offsetWidth > 0
|
|
);
|
|
|
|
getMarkdown();
|
|
|
|
triggerCustomEvent("markdownx.init");
|
|
|
|
};
|
|
|
|
/**
|
|
* settings for `timeout`.
|
|
*
|
|
* @private
|
|
*/
|
|
const _markdownify = (): void => {
|
|
|
|
clearTimeout(this.timeout);
|
|
this.timeout = setTimeout(getMarkdown, properties._latency)
|
|
|
|
};
|
|
|
|
/**
|
|
* Handling changes in the editor.
|
|
*/
|
|
const inputChanged = (): void => {
|
|
|
|
properties.editor = properties._editorIsResizable ?
|
|
updateHeight(properties.editor) : properties.editor;
|
|
|
|
return _markdownify()
|
|
|
|
};
|
|
|
|
/**
|
|
* Handling of drop events (when a file is dropped into `properties.editor`).
|
|
*
|
|
* @param {DragEvent} event
|
|
*/
|
|
const onDrop = (event: DragEvent): void => {
|
|
|
|
if (event.dataTransfer && event.dataTransfer.files.length)
|
|
Object.keys(event.dataTransfer.files).map(fileKey =>
|
|
|
|
sendFile(event.dataTransfer.files[fileKey])
|
|
|
|
);
|
|
|
|
EventHandlers.inhibitDefault(event);
|
|
|
|
};
|
|
|
|
/**
|
|
* Handling of keyboard events (i.e. primarily hotkeys).
|
|
*
|
|
* @param {KeyboardEvent} event
|
|
* @returns {Boolean | null}
|
|
*/
|
|
const onKeyDown = (event: KeyboardEvent): Boolean | null => {
|
|
|
|
const handlerFunc: Function | Boolean = keyboardEvents.hub(event);
|
|
|
|
if (typeof handlerFunc != 'function') return false;
|
|
|
|
EventHandlers.inhibitDefault(event);
|
|
|
|
// Holding the start location before anything changes.
|
|
const SELECTION_START: number = properties.editor.selectionStart;
|
|
|
|
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.
|
|
properties.editor.selectionEnd = properties.editor.selectionStart = SELECTION_START;
|
|
|
|
return false
|
|
|
|
};
|
|
|
|
/**
|
|
* Uploading the `file` onto the server through an AJAX request.
|
|
*
|
|
* @param {File} file
|
|
*/
|
|
const sendFile = (file: File) => {
|
|
|
|
properties.editor.style.opacity = UPLOAD_START_OPACITY;
|
|
|
|
const xhr = new Request(
|
|
properties.editor.getAttribute(UPLOAD_URL_ATTRIBUTE), // URL
|
|
preparePostData({image: file}) // Data
|
|
);
|
|
|
|
xhr.success = (resp: string): void | null => {
|
|
|
|
const response: ImageUploadResponse = JSON.parse(resp);
|
|
|
|
if (response.image_code) {
|
|
|
|
insertImage(response.image_code);
|
|
triggerCustomEvent('markdownx.fileUploadEnd', properties.parent, [response])
|
|
|
|
} else if (response.image_path) {
|
|
|
|
// ToDo: Deprecate.
|
|
insertImage(``);
|
|
triggerCustomEvent('markdownx.fileUploadEnd', properties.parent, [response])
|
|
|
|
} else {
|
|
|
|
console.error(XHR_RESPONSE_ERROR, response);
|
|
triggerCustomEvent('markdownx.fileUploadError', properties.parent, [response]);
|
|
insertImage(XHR_RESPONSE_ERROR);
|
|
|
|
}
|
|
|
|
properties.editor.style.opacity = NORMAL_OPACITY;
|
|
|
|
};
|
|
|
|
xhr.error = (response: any): void => {
|
|
|
|
console.error(response);
|
|
triggerCustomEvent('fileUploadError', properties.parent, [response]);
|
|
insertImage(XHR_RESPONSE_ERROR);
|
|
properties.editor.style.opacity = NORMAL_OPACITY;
|
|
|
|
};
|
|
|
|
return xhr.send()
|
|
|
|
};
|
|
|
|
/**
|
|
* Uploading the markdown text from `properties.editor` onto the server
|
|
* through an AJAX request, and upon receiving the HTML encoded text
|
|
* in response, the response will be display in `properties.preview`.
|
|
*/
|
|
const getMarkdown = () => {
|
|
|
|
const xhr = new Request(
|
|
properties.editor.getAttribute(PROCESSING_URL_ATTRIBUTE), // URL
|
|
preparePostData({content: properties.editor.value}) // Data
|
|
);
|
|
|
|
xhr.success = (response: string): void => {
|
|
|
|
properties.preview.innerHTML = response;
|
|
properties.editor = updateHeight(properties.editor);
|
|
|
|
triggerCustomEvent('markdownx.update', properties.parent, [response])
|
|
|
|
};
|
|
|
|
xhr.error = (response: any): void => {
|
|
|
|
console.error(response);
|
|
|
|
triggerCustomEvent('markdownx.updateError', properties.parent, [response])
|
|
|
|
};
|
|
|
|
return xhr.send()
|
|
|
|
};
|
|
|
|
/**
|
|
* Inserts markdown encoded image URL into `properties.editor` where
|
|
* the cursor is located.
|
|
*
|
|
* @param textToInsert
|
|
*/
|
|
const insertImage = (textToInsert: string): void => {
|
|
|
|
properties.editor.value =
|
|
`${properties.editor.value.substring(0, properties.editor.selectionStart)}` + // Preceding text.
|
|
textToInsert +
|
|
`${properties.editor.value.substring(properties.editor.selectionEnd)}`; // Succeeding text.
|
|
|
|
properties.editor.selectionStart =
|
|
properties.editor.selectionEnd =
|
|
properties.editor.selectionStart + textToInsert.length;
|
|
|
|
triggerEvent(properties.editor, 'keyup');
|
|
inputChanged();
|
|
|
|
};
|
|
|
|
|
|
_initialize();
|
|
|
|
};
|
|
|
|
|
|
(function(funcName: any, baseObj: any) {
|
|
// The public function name defaults to window.docReady
|
|
// but you can pass in your own object and own function
|
|
// name and those will be used.
|
|
// if you want to put them in a different namespace
|
|
funcName = funcName || "docReady";
|
|
baseObj = baseObj || window;
|
|
|
|
let readyList = [],
|
|
readyFired = false,
|
|
readyEventHandlersInstalled = false;
|
|
|
|
/**
|
|
* Called when the document is ready. This function protects itself
|
|
* against being called more than once.
|
|
*/
|
|
const ready = () => {
|
|
if (!readyFired) {
|
|
// Must be `true` before the callbacks are called.
|
|
readyFired = true;
|
|
|
|
// if a callback here happens to add new ready handlers,
|
|
// the docReady() function will see that it already fired
|
|
// and will schedule the callback to run right after
|
|
// this event loop finishes so all handlers will still execute
|
|
// in order and no new ones will be added to the readyList
|
|
// while we are processing the list
|
|
readyList.map(ready => ready.fn.call(window, ready.ctx));
|
|
|
|
// allow any closures held by these functions to free
|
|
readyList = [];
|
|
}
|
|
};
|
|
|
|
const readyStateChange = () => document.readyState === "complete" ? ready() : null;
|
|
|
|
// This is the one public interface
|
|
// docReady(fn, context);
|
|
// the context argument is optional - if present, it will be passed
|
|
// as an argument to the callback
|
|
baseObj[funcName] = (callback, context) => {
|
|
|
|
// if ready has already fired, then just schedule the callback
|
|
// to fire asynchronously, but right away
|
|
if (readyFired) {
|
|
|
|
setTimeout(() => callback(context), 1);
|
|
return;
|
|
|
|
} else {
|
|
|
|
// add the function and context to the list
|
|
readyList.push({fn: callback, ctx: context});
|
|
|
|
}
|
|
|
|
// If the document is already ready, schedule the ready
|
|
// function to run.
|
|
if (document.readyState === "complete") {
|
|
|
|
setTimeout(ready, 1);
|
|
|
|
} else if (!readyEventHandlersInstalled) {
|
|
|
|
// otherwise if we don't have event handlers installed,
|
|
// install them first choice is DOMContentLoaded event.
|
|
document.addEventListener("DOMContentLoaded", ready, false);
|
|
|
|
// backup is window load event
|
|
window.addEventListener("load", ready, false);
|
|
|
|
readyEventHandlersInstalled = true;
|
|
|
|
}
|
|
}
|
|
|
|
})("docReady", window);
|
|
|
|
|
|
docReady(() => {
|
|
|
|
const ELEMENTS = document.getElementsByClassName('markdownx');
|
|
|
|
return Object.keys(ELEMENTS).map(key => {
|
|
|
|
let element = ELEMENTS[key],
|
|
editor = element.querySelector('.markdownx-editor'),
|
|
preview = element.querySelector('.markdownx-preview');
|
|
|
|
// Only add the new MarkdownX instance to fields that have no MarkdownX instance yet.
|
|
if (!editor.hasAttribute('data-markdownx-init'))
|
|
return new MarkdownX(element, editor, preview)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
export {
|
|
|
|
MarkdownX
|
|
|
|
};
|