You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3041 lines
100 KiB
JavaScript
3041 lines
100 KiB
JavaScript
'use strict';
|
|
var CodeMirror = require('codemirror');
|
|
require('codemirror/addon/edit/continuelist.js');
|
|
require('./codemirror/tablist');
|
|
require('codemirror/addon/display/fullscreen.js');
|
|
require('codemirror/mode/markdown/markdown.js');
|
|
require('codemirror/addon/mode/overlay.js');
|
|
require('codemirror/addon/display/placeholder.js');
|
|
require('codemirror/addon/display/autorefresh.js');
|
|
require('codemirror/addon/selection/mark-selection.js');
|
|
require('codemirror/addon/search/searchcursor.js');
|
|
require('codemirror/mode/gfm/gfm.js');
|
|
require('codemirror/mode/xml/xml.js');
|
|
var CodeMirrorSpellChecker = require('codemirror-spell-checker');
|
|
var marked = require('marked').marked;
|
|
|
|
|
|
// Some variables
|
|
var isMac = /Mac/.test(navigator.platform);
|
|
var anchorToExternalRegex = new RegExp(/(<a.*?https?:\/\/.*?[^a]>)+?/g);
|
|
|
|
// Mapping of actions that can be bound to keyboard shortcuts or toolbar buttons
|
|
var bindings = {
|
|
'toggleBold': toggleBold,
|
|
'toggleItalic': toggleItalic,
|
|
'drawLink': drawLink,
|
|
'toggleHeadingSmaller': toggleHeadingSmaller,
|
|
'toggleHeadingBigger': toggleHeadingBigger,
|
|
'drawImage': drawImage,
|
|
'toggleBlockquote': toggleBlockquote,
|
|
'toggleOrderedList': toggleOrderedList,
|
|
'toggleUnorderedList': toggleUnorderedList,
|
|
'toggleCodeBlock': toggleCodeBlock,
|
|
'togglePreview': togglePreview,
|
|
'toggleStrikethrough': toggleStrikethrough,
|
|
'toggleHeading1': toggleHeading1,
|
|
'toggleHeading2': toggleHeading2,
|
|
'toggleHeading3': toggleHeading3,
|
|
'toggleHeading4': toggleHeading4,
|
|
'toggleHeading5': toggleHeading5,
|
|
'toggleHeading6': toggleHeading6,
|
|
'cleanBlock': cleanBlock,
|
|
'drawTable': drawTable,
|
|
'drawHorizontalRule': drawHorizontalRule,
|
|
'undo': undo,
|
|
'redo': redo,
|
|
'toggleSideBySide': toggleSideBySide,
|
|
'toggleFullScreen': toggleFullScreen,
|
|
};
|
|
|
|
var shortcuts = {
|
|
'toggleBold': 'Cmd-B',
|
|
'toggleItalic': 'Cmd-I',
|
|
'drawLink': 'Cmd-K',
|
|
'toggleHeadingSmaller': 'Cmd-H',
|
|
'toggleHeadingBigger': 'Shift-Cmd-H',
|
|
'toggleHeading1': 'Ctrl+Alt+1',
|
|
'toggleHeading2': 'Ctrl+Alt+2',
|
|
'toggleHeading3': 'Ctrl+Alt+3',
|
|
'toggleHeading4': 'Ctrl+Alt+4',
|
|
'toggleHeading5': 'Ctrl+Alt+5',
|
|
'toggleHeading6': 'Ctrl+Alt+6',
|
|
'cleanBlock': 'Cmd-E',
|
|
'drawImage': 'Cmd-Alt-I',
|
|
'toggleBlockquote': 'Cmd-\'',
|
|
'toggleOrderedList': 'Cmd-Alt-L',
|
|
'toggleUnorderedList': 'Cmd-L',
|
|
'toggleCodeBlock': 'Cmd-Alt-C',
|
|
'togglePreview': 'Cmd-P',
|
|
'toggleSideBySide': 'F9',
|
|
'toggleFullScreen': 'F11',
|
|
};
|
|
|
|
var getBindingName = function (f) {
|
|
for (var key in bindings) {
|
|
if (bindings[key] === f) {
|
|
return key;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
var isMobile = function () {
|
|
var check = false;
|
|
(function (a) {
|
|
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(a.substr(0, 4))) check = true;
|
|
})(navigator.userAgent || navigator.vendor || window.opera);
|
|
return check;
|
|
};
|
|
|
|
/**
|
|
* Modify HTML to add 'target="_blank"' to links so they open in new tabs by default.
|
|
* @param {string} htmlText - HTML to be modified.
|
|
* @return {string} The modified HTML text.
|
|
*/
|
|
function addAnchorTargetBlank(htmlText) {
|
|
var match;
|
|
while ((match = anchorToExternalRegex.exec(htmlText)) !== null) {
|
|
// With only one capture group in the RegExp, we can safely take the first index from the match.
|
|
var linkString = match[0];
|
|
|
|
if (linkString.indexOf('target=') === -1) {
|
|
var fixedLinkString = linkString.replace(/>$/, ' target="_blank">');
|
|
htmlText = htmlText.replace(linkString, fixedLinkString);
|
|
}
|
|
}
|
|
return htmlText;
|
|
}
|
|
|
|
/**
|
|
* Modify HTML to remove the list-style when rendering checkboxes.
|
|
* @param {string} htmlText - HTML to be modified.
|
|
* @return {string} The modified HTML text.
|
|
*/
|
|
function removeListStyleWhenCheckbox(htmlText) {
|
|
|
|
var parser = new DOMParser();
|
|
var htmlDoc = parser.parseFromString(htmlText, 'text/html');
|
|
var listItems = htmlDoc.getElementsByTagName('li');
|
|
|
|
for (var i = 0; i < listItems.length; i++) {
|
|
var listItem = listItems[i];
|
|
|
|
for (var j = 0; j < listItem.children.length; j++) {
|
|
var listItemChild = listItem.children[j];
|
|
|
|
if (listItemChild instanceof HTMLInputElement && listItemChild.type === 'checkbox') {
|
|
// From Github: margin: 0 .2em .25em -1.6em;
|
|
listItem.style.marginLeft = '-1.5em';
|
|
listItem.style.listStyleType = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
return htmlDoc.documentElement.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Fix shortcut. Mac use Command, others use Ctrl.
|
|
*/
|
|
function fixShortcut(name) {
|
|
if (isMac) {
|
|
name = name.replace('Ctrl', 'Cmd');
|
|
} else {
|
|
name = name.replace('Cmd', 'Ctrl');
|
|
}
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Create dropdown block
|
|
*/
|
|
function createToolbarDropdown(options, enableTooltips, shortcuts, parent) {
|
|
var el = createToolbarButton(options, false, enableTooltips, shortcuts, 'button', parent);
|
|
el.classList.add('easymde-dropdown');
|
|
|
|
el.onclick = function () {
|
|
el.focus();
|
|
};
|
|
|
|
var content = document.createElement('div');
|
|
content.className = 'easymde-dropdown-content';
|
|
for (var childrenIndex = 0; childrenIndex < options.children.length; childrenIndex++) {
|
|
|
|
var child = options.children[childrenIndex];
|
|
var childElement;
|
|
|
|
if (typeof child === 'string' && child in toolbarBuiltInButtons) {
|
|
childElement = createToolbarButton(toolbarBuiltInButtons[child], true, enableTooltips, shortcuts, 'button', parent);
|
|
} else {
|
|
childElement = createToolbarButton(child, true, enableTooltips, shortcuts, 'button', parent);
|
|
}
|
|
|
|
childElement.addEventListener('click', function (e) { e.stopPropagation(); }, false);
|
|
content.appendChild(childElement);
|
|
}
|
|
el.appendChild(content);
|
|
return el;
|
|
}
|
|
|
|
/**
|
|
* Create button element for toolbar.
|
|
*/
|
|
function createToolbarButton(options, enableActions, enableTooltips, shortcuts, markup, parent) {
|
|
options = options || {};
|
|
var el = document.createElement(markup);
|
|
|
|
// Add 'custom' attributes as early as possible, so that 'official' attributes will never be overwritten.
|
|
if (options.attributes) {
|
|
for (var attribute in options.attributes) {
|
|
if (Object.prototype.hasOwnProperty.call(options.attributes, attribute)) {
|
|
el.setAttribute(attribute, options.attributes[attribute]);
|
|
}
|
|
}
|
|
}
|
|
|
|
el.className = options.name;
|
|
el.setAttribute('type', markup);
|
|
enableTooltips = (enableTooltips == undefined) ? true : enableTooltips;
|
|
|
|
if (options.text) {
|
|
el.innerText = options.text;
|
|
}
|
|
|
|
// Properly handle custom shortcuts
|
|
if (options.name && options.name in shortcuts) {
|
|
bindings[options.name] = options.action;
|
|
}
|
|
|
|
if (options.title && enableTooltips) {
|
|
el.title = createTooltip(options.title, options.action, shortcuts);
|
|
|
|
if (isMac) {
|
|
el.title = el.title.replace('Ctrl', '⌘');
|
|
el.title = el.title.replace('Alt', '⌥');
|
|
}
|
|
}
|
|
|
|
if (options.title) {
|
|
el.setAttribute('aria-label', options.title);
|
|
}
|
|
|
|
if (options.noDisable) {
|
|
el.classList.add('no-disable');
|
|
}
|
|
|
|
if (options.noMobile) {
|
|
el.classList.add('no-mobile');
|
|
}
|
|
|
|
// Prevent errors if there is no class name in custom options
|
|
var classNameParts = [];
|
|
if (typeof options.className !== 'undefined') {
|
|
classNameParts = options.className.split(' ');
|
|
}
|
|
|
|
// Provide backwards compatibility with simple-markdown-editor by adding custom classes to the button.
|
|
var iconClasses = [];
|
|
for (var classNameIndex = 0; classNameIndex < classNameParts.length; classNameIndex++) {
|
|
var classNamePart = classNameParts[classNameIndex];
|
|
// Split icon classes from the button.
|
|
// Regex will detect "fa", "fas", "fa-something" and "fa-some-icon-1", but not "fanfare".
|
|
if (classNamePart.match(/^fa([srlb]|(-[\w-]*)|$)/)) {
|
|
iconClasses.push(classNamePart);
|
|
} else {
|
|
el.classList.add(classNamePart);
|
|
}
|
|
}
|
|
|
|
el.tabIndex = -1;
|
|
|
|
if (iconClasses.length > 0) {
|
|
// Create icon element and append as a child to the button
|
|
var icon = document.createElement('i');
|
|
for (var iconClassIndex = 0; iconClassIndex < iconClasses.length; iconClassIndex++) {
|
|
var iconClass = iconClasses[iconClassIndex];
|
|
icon.classList.add(iconClass);
|
|
}
|
|
el.appendChild(icon);
|
|
}
|
|
|
|
// If there is a custom icon markup set, use that
|
|
if (typeof options.icon !== 'undefined') {
|
|
el.innerHTML = options.icon;
|
|
}
|
|
|
|
if (options.action && enableActions) {
|
|
if (typeof options.action === 'function') {
|
|
el.onclick = function (e) {
|
|
e.preventDefault();
|
|
options.action(parent);
|
|
};
|
|
} else if (typeof options.action === 'string') {
|
|
el.onclick = function (e) {
|
|
e.preventDefault();
|
|
window.open(options.action, '_blank');
|
|
};
|
|
}
|
|
}
|
|
|
|
return el;
|
|
}
|
|
|
|
function createSep() {
|
|
var el = document.createElement('i');
|
|
el.className = 'separator';
|
|
el.innerHTML = '|';
|
|
return el;
|
|
}
|
|
|
|
function createTooltip(title, action, shortcuts) {
|
|
var actionName;
|
|
var tooltip = title;
|
|
|
|
if (action) {
|
|
actionName = getBindingName(action);
|
|
if (shortcuts[actionName]) {
|
|
tooltip += ' (' + fixShortcut(shortcuts[actionName]) + ')';
|
|
}
|
|
}
|
|
|
|
return tooltip;
|
|
}
|
|
|
|
/**
|
|
* The state of CodeMirror at the given position.
|
|
*/
|
|
function getState(cm, pos) {
|
|
pos = pos || cm.getCursor('start');
|
|
var stat = cm.getTokenAt(pos);
|
|
if (!stat.type) return {};
|
|
|
|
var types = stat.type.split(' ');
|
|
|
|
var ret = {},
|
|
data, text;
|
|
for (var i = 0; i < types.length; i++) {
|
|
data = types[i];
|
|
if (data === 'strong') {
|
|
ret.bold = true;
|
|
} else if (data === 'variable-2') {
|
|
text = cm.getLine(pos.line);
|
|
if (/^\s*\d+\.\s/.test(text)) {
|
|
ret['ordered-list'] = true;
|
|
} else {
|
|
ret['unordered-list'] = true;
|
|
}
|
|
} else if (data === 'atom') {
|
|
ret.quote = true;
|
|
} else if (data === 'em') {
|
|
ret.italic = true;
|
|
} else if (data === 'quote') {
|
|
ret.quote = true;
|
|
} else if (data === 'strikethrough') {
|
|
ret.strikethrough = true;
|
|
} else if (data === 'comment') {
|
|
ret.code = true;
|
|
} else if (data === 'link' && !ret.image) {
|
|
ret.link = true;
|
|
} else if (data === 'image') {
|
|
ret.image = true;
|
|
} else if (data.match(/^header(-[1-6])?$/)) {
|
|
ret[data.replace('header', 'heading')] = true;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
// Saved overflow setting
|
|
var saved_overflow = '';
|
|
|
|
/**
|
|
* Toggle full screen of the editor.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function toggleFullScreen(editor) {
|
|
// Set fullscreen
|
|
var cm = editor.codemirror;
|
|
cm.setOption('fullScreen', !cm.getOption('fullScreen'));
|
|
|
|
|
|
// Prevent scrolling on body during fullscreen active
|
|
if (cm.getOption('fullScreen')) {
|
|
saved_overflow = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = saved_overflow;
|
|
}
|
|
|
|
var wrapper = cm.getWrapperElement();
|
|
var sidebyside = wrapper.nextSibling;
|
|
|
|
if (sidebyside.classList.contains('editor-preview-active-side')) {
|
|
if (editor.options.sideBySideFullscreen === false) {
|
|
// if side-by-side not-fullscreen ok, apply classes as needed
|
|
var easyMDEContainer = wrapper.parentNode;
|
|
if (cm.getOption('fullScreen')) {
|
|
easyMDEContainer.classList.remove('sided--no-fullscreen');
|
|
} else {
|
|
easyMDEContainer.classList.add('sided--no-fullscreen');
|
|
}
|
|
} else {
|
|
toggleSideBySide(editor);
|
|
}
|
|
}
|
|
|
|
if (editor.options.onToggleFullScreen) {
|
|
editor.options.onToggleFullScreen(cm.getOption('fullScreen') || false);
|
|
}
|
|
|
|
// Remove or set maxHeight
|
|
if (typeof editor.options.maxHeight !== 'undefined') {
|
|
if (cm.getOption('fullScreen')) {
|
|
cm.getScrollerElement().style.removeProperty('height');
|
|
sidebyside.style.removeProperty('height');
|
|
} else {
|
|
cm.getScrollerElement().style.height = editor.options.maxHeight;
|
|
editor.setPreviewMaxHeight();
|
|
}
|
|
}
|
|
|
|
// Update toolbar class
|
|
editor.toolbar_div.classList.toggle('fullscreen');
|
|
|
|
// Update toolbar button
|
|
if (editor.toolbarElements && editor.toolbarElements.fullscreen) {
|
|
var toolbarButton = editor.toolbarElements.fullscreen;
|
|
toolbarButton.classList.toggle('active');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Action for toggling bold.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function toggleBold(editor) {
|
|
_toggleBlock(editor, 'bold', editor.options.blockStyles.bold);
|
|
}
|
|
|
|
|
|
/**
|
|
* Action for toggling italic.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function toggleItalic(editor) {
|
|
_toggleBlock(editor, 'italic', editor.options.blockStyles.italic);
|
|
}
|
|
|
|
|
|
/**
|
|
* Action for toggling strikethrough.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function toggleStrikethrough(editor) {
|
|
_toggleBlock(editor, 'strikethrough', '~~');
|
|
}
|
|
|
|
/**
|
|
* Action for toggling code block.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function toggleCodeBlock(editor) {
|
|
var fenceCharsToInsert = editor.options.blockStyles.code;
|
|
|
|
function fencing_line(line) {
|
|
/* return true, if this is a ``` or ~~~ line */
|
|
if (typeof line !== 'object') {
|
|
throw 'fencing_line() takes a \'line\' object (not a line number, or line text). Got: ' + typeof line + ': ' + line;
|
|
}
|
|
return line.styles && line.styles[2] && line.styles[2].indexOf('formatting-code-block') !== -1;
|
|
}
|
|
|
|
function token_state(token) {
|
|
// base goes an extra level deep when mode backdrops are used, e.g. spellchecker on
|
|
return token.state.base.base || token.state.base;
|
|
}
|
|
|
|
function code_type(cm, line_num, line, firstTok, lastTok) {
|
|
/*
|
|
* Return "single", "indented", "fenced" or false
|
|
*
|
|
* cm and line_num are required. Others are optional for efficiency
|
|
* To check in the middle of a line, pass in firstTok yourself.
|
|
*/
|
|
line = line || cm.getLineHandle(line_num);
|
|
firstTok = firstTok || cm.getTokenAt({
|
|
line: line_num,
|
|
ch: 1,
|
|
});
|
|
lastTok = lastTok || (!!line.text && cm.getTokenAt({
|
|
line: line_num,
|
|
ch: line.text.length - 1,
|
|
}));
|
|
var types = firstTok.type ? firstTok.type.split(' ') : [];
|
|
if (lastTok && token_state(lastTok).indentedCode) {
|
|
// have to check last char, since first chars of first line aren"t marked as indented
|
|
return 'indented';
|
|
} else if (types.indexOf('comment') === -1) {
|
|
// has to be after "indented" check, since first chars of first indented line aren"t marked as such
|
|
return false;
|
|
} else if (token_state(firstTok).fencedChars || token_state(lastTok).fencedChars || fencing_line(line)) {
|
|
return 'fenced';
|
|
} else {
|
|
return 'single';
|
|
}
|
|
}
|
|
|
|
function insertFencingAtSelection(cm, cur_start, cur_end, fenceCharsToInsert) {
|
|
var start_line_sel = cur_start.line + 1,
|
|
end_line_sel = cur_end.line + 1,
|
|
sel_multi = cur_start.line !== cur_end.line,
|
|
repl_start = fenceCharsToInsert + '\n',
|
|
repl_end = '\n' + fenceCharsToInsert;
|
|
if (sel_multi) {
|
|
end_line_sel++;
|
|
}
|
|
// handle last char including \n or not
|
|
if (sel_multi && cur_end.ch === 0) {
|
|
repl_end = fenceCharsToInsert + '\n';
|
|
end_line_sel--;
|
|
}
|
|
_replaceSelection(cm, false, [repl_start, repl_end]);
|
|
cm.setSelection({
|
|
line: start_line_sel,
|
|
ch: 0,
|
|
}, {
|
|
line: end_line_sel,
|
|
ch: 0,
|
|
});
|
|
}
|
|
|
|
var cm = editor.codemirror,
|
|
cur_start = cm.getCursor('start'),
|
|
cur_end = cm.getCursor('end'),
|
|
tok = cm.getTokenAt({
|
|
line: cur_start.line,
|
|
ch: cur_start.ch || 1,
|
|
}), // avoid ch 0 which is a cursor pos but not token
|
|
line = cm.getLineHandle(cur_start.line),
|
|
is_code = code_type(cm, cur_start.line, line, tok);
|
|
var block_start, block_end, lineCount;
|
|
|
|
if (is_code === 'single') {
|
|
// similar to some EasyMDE _toggleBlock logic
|
|
var start = line.text.slice(0, cur_start.ch).replace('`', ''),
|
|
end = line.text.slice(cur_start.ch).replace('`', '');
|
|
cm.replaceRange(start + end, {
|
|
line: cur_start.line,
|
|
ch: 0,
|
|
}, {
|
|
line: cur_start.line,
|
|
ch: 99999999999999,
|
|
});
|
|
cur_start.ch--;
|
|
if (cur_start !== cur_end) {
|
|
cur_end.ch--;
|
|
}
|
|
cm.setSelection(cur_start, cur_end);
|
|
cm.focus();
|
|
} else if (is_code === 'fenced') {
|
|
if (cur_start.line !== cur_end.line || cur_start.ch !== cur_end.ch) {
|
|
// use selection
|
|
|
|
// find the fenced line so we know what type it is (tilde, backticks, number of them)
|
|
for (block_start = cur_start.line; block_start >= 0; block_start--) {
|
|
line = cm.getLineHandle(block_start);
|
|
if (fencing_line(line)) {
|
|
break;
|
|
}
|
|
}
|
|
var fencedTok = cm.getTokenAt({
|
|
line: block_start,
|
|
ch: 1,
|
|
});
|
|
var fence_chars = token_state(fencedTok).fencedChars;
|
|
var start_text, start_line;
|
|
var end_text, end_line;
|
|
// check for selection going up against fenced lines, in which case we don't want to add more fencing
|
|
if (fencing_line(cm.getLineHandle(cur_start.line))) {
|
|
start_text = '';
|
|
start_line = cur_start.line;
|
|
} else if (fencing_line(cm.getLineHandle(cur_start.line - 1))) {
|
|
start_text = '';
|
|
start_line = cur_start.line - 1;
|
|
} else {
|
|
start_text = fence_chars + '\n';
|
|
start_line = cur_start.line;
|
|
}
|
|
if (fencing_line(cm.getLineHandle(cur_end.line))) {
|
|
end_text = '';
|
|
end_line = cur_end.line;
|
|
if (cur_end.ch === 0) {
|
|
end_line += 1;
|
|
}
|
|
} else if (cur_end.ch !== 0 && fencing_line(cm.getLineHandle(cur_end.line + 1))) {
|
|
end_text = '';
|
|
end_line = cur_end.line + 1;
|
|
} else {
|
|
end_text = fence_chars + '\n';
|
|
end_line = cur_end.line + 1;
|
|
}
|
|
if (cur_end.ch === 0) {
|
|
// full last line selected, putting cursor at beginning of next
|
|
end_line -= 1;
|
|
}
|
|
cm.operation(function () {
|
|
// end line first, so that line numbers don't change
|
|
cm.replaceRange(end_text, {
|
|
line: end_line,
|
|
ch: 0,
|
|
}, {
|
|
line: end_line + (end_text ? 0 : 1),
|
|
ch: 0,
|
|
});
|
|
cm.replaceRange(start_text, {
|
|
line: start_line,
|
|
ch: 0,
|
|
}, {
|
|
line: start_line + (start_text ? 0 : 1),
|
|
ch: 0,
|
|
});
|
|
});
|
|
cm.setSelection({
|
|
line: start_line + (start_text ? 1 : 0),
|
|
ch: 0,
|
|
}, {
|
|
line: end_line + (start_text ? 1 : -1),
|
|
ch: 0,
|
|
});
|
|
cm.focus();
|
|
} else {
|
|
// no selection, search for ends of this fenced block
|
|
var search_from = cur_start.line;
|
|
if (fencing_line(cm.getLineHandle(cur_start.line))) { // gets a little tricky if cursor is right on a fenced line
|
|
if (code_type(cm, cur_start.line + 1) === 'fenced') {
|
|
block_start = cur_start.line;
|
|
search_from = cur_start.line + 1; // for searching for "end"
|
|
} else {
|
|
block_end = cur_start.line;
|
|
search_from = cur_start.line - 1; // for searching for "start"
|
|
}
|
|
}
|
|
if (block_start === undefined) {
|
|
for (block_start = search_from; block_start >= 0; block_start--) {
|
|
line = cm.getLineHandle(block_start);
|
|
if (fencing_line(line)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (block_end === undefined) {
|
|
lineCount = cm.lineCount();
|
|
for (block_end = search_from; block_end < lineCount; block_end++) {
|
|
line = cm.getLineHandle(block_end);
|
|
if (fencing_line(line)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
cm.operation(function () {
|
|
cm.replaceRange('', {
|
|
line: block_start,
|
|
ch: 0,
|
|
}, {
|
|
line: block_start + 1,
|
|
ch: 0,
|
|
});
|
|
cm.replaceRange('', {
|
|
line: block_end - 1,
|
|
ch: 0,
|
|
}, {
|
|
line: block_end,
|
|
ch: 0,
|
|
});
|
|
});
|
|
cm.focus();
|
|
}
|
|
} else if (is_code === 'indented') {
|
|
if (cur_start.line !== cur_end.line || cur_start.ch !== cur_end.ch) {
|
|
// use selection
|
|
block_start = cur_start.line;
|
|
block_end = cur_end.line;
|
|
if (cur_end.ch === 0) {
|
|
block_end--;
|
|
}
|
|
} else {
|
|
// no selection, search for ends of this indented block
|
|
for (block_start = cur_start.line; block_start >= 0; block_start--) {
|
|
line = cm.getLineHandle(block_start);
|
|
if (line.text.match(/^\s*$/)) {
|
|
// empty or all whitespace - keep going
|
|
continue;
|
|
} else {
|
|
if (code_type(cm, block_start, line) !== 'indented') {
|
|
block_start += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
lineCount = cm.lineCount();
|
|
for (block_end = cur_start.line; block_end < lineCount; block_end++) {
|
|
line = cm.getLineHandle(block_end);
|
|
if (line.text.match(/^\s*$/)) {
|
|
// empty or all whitespace - keep going
|
|
continue;
|
|
} else {
|
|
if (code_type(cm, block_end, line) !== 'indented') {
|
|
block_end -= 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if we are going to un-indent based on a selected set of lines, and the next line is indented too, we need to
|
|
// insert a blank line so that the next line(s) continue to be indented code
|
|
var next_line = cm.getLineHandle(block_end + 1),
|
|
next_line_last_tok = next_line && cm.getTokenAt({
|
|
line: block_end + 1,
|
|
ch: next_line.text.length - 1,
|
|
}),
|
|
next_line_indented = next_line_last_tok && token_state(next_line_last_tok).indentedCode;
|
|
if (next_line_indented) {
|
|
cm.replaceRange('\n', {
|
|
line: block_end + 1,
|
|
ch: 0,
|
|
});
|
|
}
|
|
|
|
for (var i = block_start; i <= block_end; i++) {
|
|
cm.indentLine(i, 'subtract'); // TODO: this doesn't get tracked in the history, so can't be undone :(
|
|
}
|
|
cm.focus();
|
|
} else {
|
|
// insert code formatting
|
|
var no_sel_and_starting_of_line = (cur_start.line === cur_end.line && cur_start.ch === cur_end.ch && cur_start.ch === 0);
|
|
var sel_multi = cur_start.line !== cur_end.line;
|
|
if (no_sel_and_starting_of_line || sel_multi) {
|
|
insertFencingAtSelection(cm, cur_start, cur_end, fenceCharsToInsert);
|
|
} else {
|
|
_replaceSelection(cm, false, ['`', '`']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Action for toggling blockquote.
|
|
*/
|
|
function toggleBlockquote(editor) {
|
|
_toggleLine(editor.codemirror, 'quote');
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size: normal -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> normal
|
|
*/
|
|
function toggleHeadingSmaller(editor) {
|
|
_toggleHeading(editor.codemirror, 'smaller');
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size: normal -> h6 -> h5 -> h4 -> h3 -> h2 -> h1 -> normal
|
|
*/
|
|
function toggleHeadingBigger(editor) {
|
|
_toggleHeading(editor.codemirror, 'bigger');
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size 1
|
|
*/
|
|
function toggleHeading1(editor) {
|
|
_toggleHeading(editor.codemirror, undefined, 1);
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size 2
|
|
*/
|
|
function toggleHeading2(editor) {
|
|
_toggleHeading(editor.codemirror, undefined, 2);
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size 3
|
|
*/
|
|
function toggleHeading3(editor) {
|
|
_toggleHeading(editor.codemirror, undefined, 3);
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size 4
|
|
*/
|
|
function toggleHeading4(editor) {
|
|
_toggleHeading(editor.codemirror, undefined, 4);
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size 5
|
|
*/
|
|
function toggleHeading5(editor) {
|
|
_toggleHeading(editor.codemirror, undefined, 5);
|
|
}
|
|
|
|
/**
|
|
* Action for toggling heading size 6
|
|
*/
|
|
function toggleHeading6(editor) {
|
|
_toggleHeading(editor.codemirror, undefined, 6);
|
|
}
|
|
|
|
|
|
/**
|
|
* Action for toggling ul.
|
|
*/
|
|
function toggleUnorderedList(editor) {
|
|
var cm = editor.codemirror;
|
|
|
|
var listStyle = '*'; // Default
|
|
if (['-', '+', '*'].includes(editor.options.unorderedListStyle)) {
|
|
listStyle = editor.options.unorderedListStyle;
|
|
}
|
|
|
|
_toggleLine(cm, 'unordered-list', listStyle);
|
|
}
|
|
|
|
|
|
/**
|
|
* Action for toggling ol.
|
|
*/
|
|
function toggleOrderedList(editor) {
|
|
_toggleLine(editor.codemirror, 'ordered-list');
|
|
}
|
|
|
|
/**
|
|
* Action for clean block (remove headline, list, blockquote code, markers)
|
|
*/
|
|
function cleanBlock(editor) {
|
|
_cleanBlock(editor.codemirror);
|
|
}
|
|
|
|
/**
|
|
* Action for drawing a link.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function drawLink(editor) {
|
|
var options = editor.options;
|
|
var url = 'https://';
|
|
if (options.promptURLs) {
|
|
var result = prompt(options.promptTexts.link, url);
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
url = escapePromptURL(result);
|
|
}
|
|
_toggleLink(editor, 'link', options.insertTexts.link, url);
|
|
}
|
|
|
|
/**
|
|
* Action for drawing an img.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function drawImage(editor) {
|
|
var options = editor.options;
|
|
var url = 'https://';
|
|
if (options.promptURLs) {
|
|
var result = prompt(options.promptTexts.image, url);
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
url = escapePromptURL(result);
|
|
}
|
|
_toggleLink(editor, 'image', options.insertTexts.image, url);
|
|
}
|
|
|
|
/**
|
|
* Encode and escape URLs to prevent breaking up rendered Markdown links.
|
|
* @param {string} url The url of the link or image
|
|
*/
|
|
function escapePromptURL(url) {
|
|
return encodeURI(url).replace(/([\\()])/g, '\\$1');
|
|
}
|
|
|
|
/**
|
|
* Action for opening the browse-file window to upload an image to a server.
|
|
* @param {EasyMDE} editor The EasyMDE object
|
|
*/
|
|
function drawUploadedImage(editor) {
|
|
// TODO: Draw the image template with a fake url? ie: '![](importing foo.png...)'
|
|
editor.openBrowseFileWindow();
|
|
}
|
|
|
|
/**
|
|
* Action executed after an image have been successfully imported on the server.
|
|
* @param {EasyMDE} editor The EasyMDE object
|
|
* @param {string} url The url of the uploaded image
|
|
*/
|
|
function afterImageUploaded(editor, url) {
|
|
var cm = editor.codemirror;
|
|
var stat = getState(cm);
|
|
var options = editor.options;
|
|
var imageName = url.substr(url.lastIndexOf('/') + 1);
|
|
var ext = imageName.substring(imageName.lastIndexOf('.') + 1).replace(/\?.*$/, '').toLowerCase();
|
|
|
|
// Check if media is an image
|
|
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'apng', 'avif', 'webp'].includes(ext)) {
|
|
_replaceSelection(cm, stat.image, options.insertTexts.uploadedImage, url);
|
|
} else {
|
|
var text_link = options.insertTexts.link;
|
|
text_link[0] = '[' + imageName;
|
|
_replaceSelection(cm, stat.link, text_link, url);
|
|
}
|
|
|
|
// show uploaded image filename for 1000ms
|
|
editor.updateStatusBar('upload-image', editor.options.imageTexts.sbOnUploaded.replace('#image_name#', imageName));
|
|
setTimeout(function () {
|
|
editor.updateStatusBar('upload-image', editor.options.imageTexts.sbInit);
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Action for drawing a table.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function drawTable(editor) {
|
|
var cm = editor.codemirror;
|
|
var stat = getState(cm);
|
|
var options = editor.options;
|
|
_replaceSelection(cm, stat.table, options.insertTexts.table);
|
|
}
|
|
|
|
/**
|
|
* Action for drawing a horizontal rule.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function drawHorizontalRule(editor) {
|
|
var cm = editor.codemirror;
|
|
var stat = getState(cm);
|
|
var options = editor.options;
|
|
_replaceSelection(cm, stat.image, options.insertTexts.horizontalRule);
|
|
}
|
|
|
|
|
|
/**
|
|
* Undo action.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function undo(editor) {
|
|
var cm = editor.codemirror;
|
|
cm.undo();
|
|
cm.focus();
|
|
}
|
|
|
|
|
|
/**
|
|
* Redo action.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function redo(editor) {
|
|
var cm = editor.codemirror;
|
|
cm.redo();
|
|
cm.focus();
|
|
}
|
|
|
|
|
|
/**
|
|
* Toggle side by side preview
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function toggleSideBySide(editor) {
|
|
var cm = editor.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var preview = wrapper.nextSibling;
|
|
var toolbarButton = editor.toolbarElements && editor.toolbarElements['side-by-side'];
|
|
var useSideBySideListener = false;
|
|
|
|
var easyMDEContainer = wrapper.parentNode;
|
|
|
|
if (preview.classList.contains('editor-preview-active-side')) {
|
|
if (editor.options.sideBySideFullscreen === false) {
|
|
// if side-by-side not-fullscreen ok, remove classes when hiding side
|
|
easyMDEContainer.classList.remove('sided--no-fullscreen');
|
|
}
|
|
preview.classList.remove('editor-preview-active-side');
|
|
if (toolbarButton) toolbarButton.classList.remove('active');
|
|
wrapper.classList.remove('CodeMirror-sided');
|
|
} else {
|
|
// When the preview button is clicked for the first time,
|
|
// give some time for the transition from editor.css to fire and the view to slide from right to left,
|
|
// instead of just appearing.
|
|
setTimeout(function () {
|
|
if (!cm.getOption('fullScreen')) {
|
|
if (editor.options.sideBySideFullscreen === false) {
|
|
// if side-by-side not-fullscreen ok, add classes when not fullscreen and showing side
|
|
easyMDEContainer.classList.add('sided--no-fullscreen');
|
|
} else {
|
|
toggleFullScreen(editor);
|
|
}
|
|
}
|
|
preview.classList.add('editor-preview-active-side');
|
|
}, 1);
|
|
if (toolbarButton) toolbarButton.classList.add('active');
|
|
wrapper.classList.add('CodeMirror-sided');
|
|
useSideBySideListener = true;
|
|
}
|
|
|
|
// Hide normal preview if active
|
|
var previewNormal = wrapper.lastChild;
|
|
if (previewNormal.classList.contains('editor-preview-active')) {
|
|
previewNormal.classList.remove('editor-preview-active');
|
|
var toolbar = editor.toolbarElements.preview;
|
|
var toolbar_div = editor.toolbar_div;
|
|
toolbar.classList.remove('active');
|
|
toolbar_div.classList.remove('disabled-for-preview');
|
|
}
|
|
|
|
var sideBySideRenderingFunction = function () {
|
|
var newValue = editor.options.previewRender(editor.value(), preview);
|
|
if (newValue != null) {
|
|
preview.innerHTML = newValue;
|
|
}
|
|
};
|
|
|
|
if (!cm.sideBySideRenderingFunction) {
|
|
cm.sideBySideRenderingFunction = sideBySideRenderingFunction;
|
|
}
|
|
|
|
if (useSideBySideListener) {
|
|
var newValue = editor.options.previewRender(editor.value(), preview);
|
|
if (newValue != null) {
|
|
preview.innerHTML = newValue;
|
|
}
|
|
cm.on('update', cm.sideBySideRenderingFunction);
|
|
} else {
|
|
cm.off('update', cm.sideBySideRenderingFunction);
|
|
}
|
|
|
|
// Refresh to fix selection being off (#309)
|
|
cm.refresh();
|
|
}
|
|
|
|
|
|
/**
|
|
* Preview action.
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function togglePreview(editor) {
|
|
var cm = editor.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var toolbar_div = editor.toolbar_div;
|
|
var toolbar = editor.options.toolbar ? editor.toolbarElements.preview : false;
|
|
var preview = wrapper.lastChild;
|
|
|
|
// Turn off side by side if needed
|
|
var sidebyside = cm.getWrapperElement().nextSibling;
|
|
if (sidebyside.classList.contains('editor-preview-active-side'))
|
|
toggleSideBySide(editor);
|
|
|
|
if (!preview || !preview.classList.contains('editor-preview-full')) {
|
|
|
|
preview = document.createElement('div');
|
|
preview.className = 'editor-preview-full';
|
|
|
|
if (editor.options.previewClass) {
|
|
|
|
if (Array.isArray(editor.options.previewClass)) {
|
|
for (var i = 0; i < editor.options.previewClass.length; i++) {
|
|
preview.classList.add(editor.options.previewClass[i]);
|
|
}
|
|
|
|
} else if (typeof editor.options.previewClass === 'string') {
|
|
preview.classList.add(editor.options.previewClass);
|
|
}
|
|
}
|
|
|
|
wrapper.appendChild(preview);
|
|
}
|
|
|
|
if (preview.classList.contains('editor-preview-active')) {
|
|
preview.classList.remove('editor-preview-active');
|
|
if (toolbar) {
|
|
toolbar.classList.remove('active');
|
|
toolbar_div.classList.remove('disabled-for-preview');
|
|
}
|
|
} else {
|
|
// When the preview button is clicked for the first time,
|
|
// give some time for the transition from editor.css to fire and the view to slide from right to left,
|
|
// instead of just appearing.
|
|
setTimeout(function () {
|
|
preview.classList.add('editor-preview-active');
|
|
}, 1);
|
|
if (toolbar) {
|
|
toolbar.classList.add('active');
|
|
toolbar_div.classList.add('disabled-for-preview');
|
|
}
|
|
}
|
|
preview.innerHTML = editor.options.previewRender(editor.value(), preview);
|
|
|
|
}
|
|
|
|
function _replaceSelection(cm, active, startEnd, url) {
|
|
if (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
|
|
return;
|
|
|
|
var text;
|
|
var start = startEnd[0];
|
|
var end = startEnd[1];
|
|
var startPoint = {},
|
|
endPoint = {};
|
|
Object.assign(startPoint, cm.getCursor('start'));
|
|
Object.assign(endPoint, cm.getCursor('end'));
|
|
if (url) {
|
|
start = start.replace('#url#', url); // url is in start for upload-image
|
|
end = end.replace('#url#', url);
|
|
}
|
|
if (active) {
|
|
text = cm.getLine(startPoint.line);
|
|
start = text.slice(0, startPoint.ch);
|
|
end = text.slice(startPoint.ch);
|
|
cm.replaceRange(start + end, {
|
|
line: startPoint.line,
|
|
ch: 0,
|
|
});
|
|
} else {
|
|
text = cm.getSelection();
|
|
cm.replaceSelection(start + text + end);
|
|
|
|
startPoint.ch += start.length;
|
|
if (startPoint !== endPoint) {
|
|
endPoint.ch += start.length;
|
|
}
|
|
}
|
|
cm.setSelection(startPoint, endPoint);
|
|
cm.focus();
|
|
}
|
|
|
|
|
|
function _toggleHeading(cm, direction, size) {
|
|
if (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
|
|
return;
|
|
|
|
var startPoint = cm.getCursor('start');
|
|
var endPoint = cm.getCursor('end');
|
|
for (var i = startPoint.line; i <= endPoint.line; i++) {
|
|
(function (i) {
|
|
var text = cm.getLine(i);
|
|
var currHeadingLevel = text.search(/[^#]/);
|
|
|
|
if (direction !== undefined) {
|
|
if (currHeadingLevel <= 0) {
|
|
if (direction == 'bigger') {
|
|
text = '###### ' + text;
|
|
} else {
|
|
text = '# ' + text;
|
|
}
|
|
} else if (currHeadingLevel == 6 && direction == 'smaller') {
|
|
text = text.substr(7);
|
|
} else if (currHeadingLevel == 1 && direction == 'bigger') {
|
|
text = text.substr(2);
|
|
} else {
|
|
if (direction == 'bigger') {
|
|
text = text.substr(1);
|
|
} else {
|
|
text = '#' + text;
|
|
}
|
|
}
|
|
} else {
|
|
if (currHeadingLevel <= 0) {
|
|
text = '#'.repeat(size) + ' ' + text;
|
|
} else if (currHeadingLevel == size) {
|
|
text = text.substr(currHeadingLevel + 1);
|
|
} else {
|
|
text = '#'.repeat(size) + ' ' + text.substr(currHeadingLevel + 1);
|
|
}
|
|
}
|
|
|
|
cm.replaceRange(text, {
|
|
line: i,
|
|
ch: 0,
|
|
}, {
|
|
line: i,
|
|
ch: 99999999999999,
|
|
});
|
|
})(i);
|
|
}
|
|
cm.focus();
|
|
}
|
|
|
|
|
|
function _toggleLine(cm, name, liststyle) {
|
|
if (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
|
|
return;
|
|
|
|
var listRegexp = /^(\s*)(\*|-|\+|\d*\.)(\s+)/;
|
|
var whitespacesRegexp = /^\s*/;
|
|
|
|
var stat = getState(cm);
|
|
var startPoint = cm.getCursor('start');
|
|
var endPoint = cm.getCursor('end');
|
|
var repl = {
|
|
'quote': /^(\s*)>\s+/,
|
|
'unordered-list': listRegexp,
|
|
'ordered-list': listRegexp,
|
|
};
|
|
|
|
var _getChar = function (name, i) {
|
|
var map = {
|
|
'quote': '>',
|
|
'unordered-list': liststyle,
|
|
'ordered-list': '%%i.',
|
|
};
|
|
|
|
return map[name].replace('%%i', i);
|
|
};
|
|
|
|
var _checkChar = function (name, char) {
|
|
var map = {
|
|
'quote': '>',
|
|
'unordered-list': '\\' + liststyle,
|
|
'ordered-list': '\\d+.',
|
|
};
|
|
var rt = new RegExp(map[name]);
|
|
|
|
return char && rt.test(char);
|
|
};
|
|
|
|
var _toggle = function (name, text, untoggleOnly) {
|
|
var arr = listRegexp.exec(text);
|
|
var char = _getChar(name, line);
|
|
if (arr !== null) {
|
|
if (_checkChar(name, arr[2])) {
|
|
char = '';
|
|
}
|
|
text = arr[1] + char + arr[3] + text.replace(whitespacesRegexp, '').replace(repl[name], '$1');
|
|
} else if (untoggleOnly == false) {
|
|
text = char + ' ' + text;
|
|
}
|
|
return text;
|
|
};
|
|
|
|
var line = 1;
|
|
for (var i = startPoint.line; i <= endPoint.line; i++) {
|
|
(function (i) {
|
|
var text = cm.getLine(i);
|
|
if (stat[name]) {
|
|
text = text.replace(repl[name], '$1');
|
|
} else {
|
|
// If we're toggling unordered-list formatting, check if the current line
|
|
// is part of an ordered-list, and if so, untoggle that first.
|
|
// Workaround for https://github.com/Ionaru/easy-markdown-editor/issues/92
|
|
if (name == 'unordered-list') {
|
|
text = _toggle('ordered-list', text, true);
|
|
}
|
|
text = _toggle(name, text, false);
|
|
line += 1;
|
|
}
|
|
cm.replaceRange(text, {
|
|
line: i,
|
|
ch: 0,
|
|
}, {
|
|
line: i,
|
|
ch: 99999999999999,
|
|
});
|
|
})(i);
|
|
}
|
|
cm.focus();
|
|
}
|
|
|
|
/**
|
|
* @param {EasyMDE} editor
|
|
* @param {'link' | 'image'} type
|
|
* @param {string} startEnd
|
|
* @param {string} url
|
|
*/
|
|
function _toggleLink(editor, type, startEnd, url) {
|
|
if (!editor.codemirror || editor.isPreviewActive()) {
|
|
return;
|
|
}
|
|
|
|
var cm = editor.codemirror;
|
|
var stat = getState(cm);
|
|
var active = stat[type];
|
|
if (!active) {
|
|
_replaceSelection(cm, active, startEnd, url);
|
|
return;
|
|
}
|
|
|
|
var startPoint = cm.getCursor('start');
|
|
var endPoint = cm.getCursor('end');
|
|
var text = cm.getLine(startPoint.line);
|
|
var start = text.slice(0, startPoint.ch);
|
|
var end = text.slice(startPoint.ch);
|
|
|
|
if (type == 'link') {
|
|
start = start.replace(/(.*(?<!!))\[/, '$1');
|
|
} else if (type == 'image') {
|
|
start = start.replace(/(.*)!\[$/, '$1');
|
|
}
|
|
end = end.replace(/]\(.*?\)/, '');
|
|
|
|
cm.replaceRange(start + end, {
|
|
line: startPoint.line,
|
|
ch: 0,
|
|
}, {
|
|
line: startPoint.line,
|
|
ch: 99999999999999,
|
|
});
|
|
|
|
startPoint.ch -= startEnd[0].length;
|
|
if (startPoint !== endPoint) {
|
|
endPoint.ch -= startEnd[0].length;
|
|
}
|
|
cm.setSelection(startPoint, endPoint);
|
|
cm.focus();
|
|
}
|
|
|
|
/**
|
|
* @param {EasyMDE} editor
|
|
*/
|
|
function _toggleBlock(editor, type, start_chars, end_chars) {
|
|
if (!editor.codemirror || editor.isPreviewActive()) {
|
|
return;
|
|
}
|
|
|
|
end_chars = (typeof end_chars === 'undefined') ? start_chars : end_chars;
|
|
var cm = editor.codemirror;
|
|
var stat = getState(cm);
|
|
|
|
var text;
|
|
var start = start_chars;
|
|
var end = end_chars;
|
|
|
|
var startPoint = cm.getCursor('start');
|
|
var endPoint = cm.getCursor('end');
|
|
|
|
if (stat[type]) {
|
|
text = cm.getLine(startPoint.line);
|
|
start = text.slice(0, startPoint.ch);
|
|
end = text.slice(startPoint.ch);
|
|
if (type == 'bold') {
|
|
start = start.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/, '');
|
|
end = end.replace(/(\*\*|__)/, '');
|
|
} else if (type == 'italic') {
|
|
start = start.replace(/(\*|_)(?![\s\S]*(\*|_))/, '');
|
|
end = end.replace(/(\*|_)/, '');
|
|
} else if (type == 'strikethrough') {
|
|
start = start.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/, '');
|
|
end = end.replace(/(\*\*|~~)/, '');
|
|
}
|
|
cm.replaceRange(start + end, {
|
|
line: startPoint.line,
|
|
ch: 0,
|
|
}, {
|
|
line: startPoint.line,
|
|
ch: 99999999999999,
|
|
});
|
|
|
|
if (type == 'bold' || type == 'strikethrough') {
|
|
startPoint.ch -= 2;
|
|
if (startPoint !== endPoint) {
|
|
endPoint.ch -= 2;
|
|
}
|
|
} else if (type == 'italic') {
|
|
startPoint.ch -= 1;
|
|
if (startPoint !== endPoint) {
|
|
endPoint.ch -= 1;
|
|
}
|
|
}
|
|
} else {
|
|
text = cm.getSelection();
|
|
if (type == 'bold') {
|
|
text = text.split('**').join('');
|
|
text = text.split('__').join('');
|
|
} else if (type == 'italic') {
|
|
text = text.split('*').join('');
|
|
text = text.split('_').join('');
|
|
} else if (type == 'strikethrough') {
|
|
text = text.split('~~').join('');
|
|
}
|
|
cm.replaceSelection(start + text + end);
|
|
|
|
startPoint.ch += start_chars.length;
|
|
endPoint.ch = startPoint.ch + text.length;
|
|
}
|
|
|
|
cm.setSelection(startPoint, endPoint);
|
|
cm.focus();
|
|
}
|
|
|
|
function _cleanBlock(cm) {
|
|
if (cm.getWrapperElement().lastChild.classList.contains('editor-preview-active'))
|
|
return;
|
|
|
|
var startPoint = cm.getCursor('start');
|
|
var endPoint = cm.getCursor('end');
|
|
var text;
|
|
|
|
for (var line = startPoint.line; line <= endPoint.line; line++) {
|
|
text = cm.getLine(line);
|
|
text = text.replace(/^[ ]*([# ]+|\*|-|[> ]+|[0-9]+(.|\)))[ ]*/, '');
|
|
|
|
cm.replaceRange(text, {
|
|
line: line,
|
|
ch: 0,
|
|
}, {
|
|
line: line,
|
|
ch: 99999999999999,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a number of bytes to a human-readable file size. If you desire
|
|
* to add a space between the value and the unit, you need to add this space
|
|
* to the given units.
|
|
* @param bytes {number} A number of bytes, as integer. Ex: 421137
|
|
* @param units {number[]} An array of human-readable units, ie. [' B', ' K', ' MB']
|
|
* @returns string A human-readable file size. Ex: '412 KB'
|
|
*/
|
|
function humanFileSize(bytes, units) {
|
|
if (Math.abs(bytes) < 1024) {
|
|
return '' + bytes + units[0];
|
|
}
|
|
var u = 0;
|
|
do {
|
|
bytes /= 1024;
|
|
++u;
|
|
} while (Math.abs(bytes) >= 1024 && u < units.length);
|
|
return '' + bytes.toFixed(1) + units[u];
|
|
}
|
|
|
|
// Merge the properties of one object into another.
|
|
function _mergeProperties(target, source) {
|
|
for (var property in source) {
|
|
if (Object.prototype.hasOwnProperty.call(source, property)) {
|
|
if (source[property] instanceof Array) {
|
|
target[property] = source[property].concat(target[property] instanceof Array ? target[property] : []);
|
|
} else if (
|
|
source[property] !== null &&
|
|
typeof source[property] === 'object' &&
|
|
source[property].constructor === Object
|
|
) {
|
|
target[property] = _mergeProperties(target[property] || {}, source[property]);
|
|
} else {
|
|
target[property] = source[property];
|
|
}
|
|
}
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
// Merge an arbitrary number of objects into one.
|
|
function extend(target) {
|
|
for (var i = 1; i < arguments.length; i++) {
|
|
target = _mergeProperties(target, arguments[i]);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
/* The right word count in respect for CJK. */
|
|
function wordCount(data) {
|
|
var pattern = /[a-zA-Z0-9_\u00A0-\u02AF\u0392-\u03c9\u0410-\u04F9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g;
|
|
var m = data.match(pattern);
|
|
var count = 0;
|
|
if (m === null) return count;
|
|
for (var i = 0; i < m.length; i++) {
|
|
if (m[i].charCodeAt(0) >= 0x4E00) {
|
|
count += m[i].length;
|
|
} else {
|
|
count += 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
var iconClassMap = {
|
|
'bold': 'fa fa-bold',
|
|
'italic': 'fa fa-italic',
|
|
'strikethrough': 'fa fa-strikethrough',
|
|
'heading': 'fa fa-header fa-heading',
|
|
'heading-smaller': 'fa fa-header fa-heading header-smaller',
|
|
'heading-bigger': 'fa fa-header fa-heading header-bigger',
|
|
'heading-1': 'fa fa-header fa-heading header-1',
|
|
'heading-2': 'fa fa-header fa-heading header-2',
|
|
'heading-3': 'fa fa-header fa-heading header-3',
|
|
'code': 'fa fa-code',
|
|
'quote': 'fa fa-quote-left',
|
|
'ordered-list': 'fa fa-list-ol',
|
|
'unordered-list': 'fa fa-list-ul',
|
|
'clean-block': 'fa fa-eraser',
|
|
'link': 'fa fa-link',
|
|
'image': 'fa fa-image',
|
|
'upload-image': 'fa fa-image',
|
|
'table': 'fa fa-table',
|
|
'horizontal-rule': 'fa fa-minus',
|
|
'preview': 'fa fa-eye',
|
|
'side-by-side': 'fa fa-columns',
|
|
'fullscreen': 'fa fa-arrows-alt',
|
|
'guide': 'fa fa-question-circle',
|
|
'undo': 'fa fa-undo',
|
|
'redo': 'fa fa-repeat fa-redo',
|
|
};
|
|
|
|
var toolbarBuiltInButtons = {
|
|
'bold': {
|
|
name: 'bold',
|
|
action: toggleBold,
|
|
className: iconClassMap['bold'],
|
|
title: 'Bold',
|
|
default: true,
|
|
},
|
|
'italic': {
|
|
name: 'italic',
|
|
action: toggleItalic,
|
|
className: iconClassMap['italic'],
|
|
title: 'Italic',
|
|
default: true,
|
|
},
|
|
'strikethrough': {
|
|
name: 'strikethrough',
|
|
action: toggleStrikethrough,
|
|
className: iconClassMap['strikethrough'],
|
|
title: 'Strikethrough',
|
|
},
|
|
'heading': {
|
|
name: 'heading',
|
|
action: toggleHeadingSmaller,
|
|
className: iconClassMap['heading'],
|
|
title: 'Heading',
|
|
default: true,
|
|
},
|
|
'heading-smaller': {
|
|
name: 'heading-smaller',
|
|
action: toggleHeadingSmaller,
|
|
className: iconClassMap['heading-smaller'],
|
|
title: 'Smaller Heading',
|
|
},
|
|
'heading-bigger': {
|
|
name: 'heading-bigger',
|
|
action: toggleHeadingBigger,
|
|
className: iconClassMap['heading-bigger'],
|
|
title: 'Bigger Heading',
|
|
},
|
|
'heading-1': {
|
|
name: 'heading-1',
|
|
action: toggleHeading1,
|
|
className: iconClassMap['heading-1'],
|
|
title: 'Big Heading',
|
|
},
|
|
'heading-2': {
|
|
name: 'heading-2',
|
|
action: toggleHeading2,
|
|
className: iconClassMap['heading-2'],
|
|
title: 'Medium Heading',
|
|
},
|
|
'heading-3': {
|
|
name: 'heading-3',
|
|
action: toggleHeading3,
|
|
className: iconClassMap['heading-3'],
|
|
title: 'Small Heading',
|
|
},
|
|
'separator-1': {
|
|
name: 'separator-1',
|
|
},
|
|
'code': {
|
|
name: 'code',
|
|
action: toggleCodeBlock,
|
|
className: iconClassMap['code'],
|
|
title: 'Code',
|
|
},
|
|
'quote': {
|
|
name: 'quote',
|
|
action: toggleBlockquote,
|
|
className: iconClassMap['quote'],
|
|
title: 'Quote',
|
|
default: true,
|
|
},
|
|
'unordered-list': {
|
|
name: 'unordered-list',
|
|
action: toggleUnorderedList,
|
|
className: iconClassMap['unordered-list'],
|
|
title: 'Generic List',
|
|
default: true,
|
|
},
|
|
'ordered-list': {
|
|
name: 'ordered-list',
|
|
action: toggleOrderedList,
|
|
className: iconClassMap['ordered-list'],
|
|
title: 'Numbered List',
|
|
default: true,
|
|
},
|
|
'clean-block': {
|
|
name: 'clean-block',
|
|
action: cleanBlock,
|
|
className: iconClassMap['clean-block'],
|
|
title: 'Clean block',
|
|
},
|
|
'separator-2': {
|
|
name: 'separator-2',
|
|
},
|
|
'link': {
|
|
name: 'link',
|
|
action: drawLink,
|
|
className: iconClassMap['link'],
|
|
title: 'Create Link',
|
|
default: true,
|
|
},
|
|
'image': {
|
|
name: 'image',
|
|
action: drawImage,
|
|
className: iconClassMap['image'],
|
|
title: 'Insert Image',
|
|
default: true,
|
|
},
|
|
'upload-image': {
|
|
name: 'upload-image',
|
|
action: drawUploadedImage,
|
|
className: iconClassMap['upload-image'],
|
|
title: 'Import an image',
|
|
},
|
|
'table': {
|
|
name: 'table',
|
|
action: drawTable,
|
|
className: iconClassMap['table'],
|
|
title: 'Insert Table',
|
|
},
|
|
'horizontal-rule': {
|
|
name: 'horizontal-rule',
|
|
action: drawHorizontalRule,
|
|
className: iconClassMap['horizontal-rule'],
|
|
title: 'Insert Horizontal Line',
|
|
},
|
|
'separator-3': {
|
|
name: 'separator-3',
|
|
},
|
|
'preview': {
|
|
name: 'preview',
|
|
action: togglePreview,
|
|
className: iconClassMap['preview'],
|
|
noDisable: true,
|
|
title: 'Toggle Preview',
|
|
default: true,
|
|
},
|
|
'side-by-side': {
|
|
name: 'side-by-side',
|
|
action: toggleSideBySide,
|
|
className: iconClassMap['side-by-side'],
|
|
noDisable: true,
|
|
noMobile: true,
|
|
title: 'Toggle Side by Side',
|
|
default: true,
|
|
},
|
|
'fullscreen': {
|
|
name: 'fullscreen',
|
|
action: toggleFullScreen,
|
|
className: iconClassMap['fullscreen'],
|
|
noDisable: true,
|
|
noMobile: true,
|
|
title: 'Toggle Fullscreen',
|
|
default: true,
|
|
},
|
|
'separator-4': {
|
|
name: 'separator-4',
|
|
},
|
|
'guide': {
|
|
name: 'guide',
|
|
action: 'https://www.markdownguide.org/basic-syntax/',
|
|
className: iconClassMap['guide'],
|
|
noDisable: true,
|
|
title: 'Markdown Guide',
|
|
default: true,
|
|
},
|
|
'separator-5': {
|
|
name: 'separator-5',
|
|
},
|
|
'undo': {
|
|
name: 'undo',
|
|
action: undo,
|
|
className: iconClassMap['undo'],
|
|
noDisable: true,
|
|
title: 'Undo',
|
|
},
|
|
'redo': {
|
|
name: 'redo',
|
|
action: redo,
|
|
className: iconClassMap['redo'],
|
|
noDisable: true,
|
|
title: 'Redo',
|
|
},
|
|
};
|
|
|
|
var insertTexts = {
|
|
link: ['[', '](#url#)'],
|
|
image: ['![', '](#url#)'],
|
|
uploadedImage: ['![](#url#)', ''],
|
|
// uploadedImage: ['![](#url#)\n', ''], // TODO: New line insertion doesn't work here.
|
|
table: ['', '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n'],
|
|
horizontalRule: ['', '\n\n-----\n\n'],
|
|
};
|
|
|
|
var promptTexts = {
|
|
link: 'URL for the link:',
|
|
image: 'URL of the image:',
|
|
};
|
|
|
|
var timeFormat = {
|
|
locale: 'en-US',
|
|
format: {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
},
|
|
};
|
|
|
|
var blockStyles = {
|
|
'bold': '**',
|
|
'code': '```',
|
|
'italic': '*',
|
|
};
|
|
|
|
/**
|
|
* Texts displayed to the user (mainly on the status bar) for the import image
|
|
* feature. Can be used for customization or internationalization.
|
|
*/
|
|
var imageTexts = {
|
|
sbInit: 'Attach files by drag and dropping or pasting from clipboard.',
|
|
sbOnDragEnter: 'Drop image to upload it.',
|
|
sbOnDrop: 'Uploading image #images_names#...',
|
|
sbProgress: 'Uploading #file_name#: #progress#%',
|
|
sbOnUploaded: 'Uploaded #image_name#',
|
|
sizeUnits: ' B, KB, MB',
|
|
};
|
|
|
|
/**
|
|
* Errors displayed to the user, using the `errorCallback` option. Can be used for
|
|
* customization or internationalization.
|
|
*/
|
|
var errorMessages = {
|
|
noFileGiven: 'You must select a file.',
|
|
typeNotAllowed: 'This image type is not allowed.',
|
|
fileTooLarge: 'Image #image_name# is too big (#image_size#).\n' +
|
|
'Maximum file size is #image_max_size#.',
|
|
importError: 'Something went wrong when uploading the image #image_name#.',
|
|
};
|
|
|
|
/**
|
|
* Interface of EasyMDE.
|
|
*/
|
|
function EasyMDE(options) {
|
|
// Handle options parameter
|
|
options = options || {};
|
|
|
|
// Used later to refer to it"s parent
|
|
options.parent = this;
|
|
|
|
// Check if Font Awesome needs to be auto downloaded
|
|
var autoDownloadFA = true;
|
|
|
|
if (options.autoDownloadFontAwesome === false) {
|
|
autoDownloadFA = false;
|
|
}
|
|
|
|
if (options.autoDownloadFontAwesome !== true) {
|
|
var styleSheets = document.styleSheets;
|
|
for (var i = 0; i < styleSheets.length; i++) {
|
|
if (!styleSheets[i].href)
|
|
continue;
|
|
|
|
if (styleSheets[i].href.indexOf('//maxcdn.bootstrapcdn.com/font-awesome/') > -1) {
|
|
autoDownloadFA = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (autoDownloadFA) {
|
|
var link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.href = 'https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css';
|
|
document.getElementsByTagName('head')[0].appendChild(link);
|
|
}
|
|
|
|
|
|
// Find the textarea to use
|
|
if (options.element) {
|
|
this.element = options.element;
|
|
} else if (options.element === null) {
|
|
// This means that the element option was specified, but no element was found
|
|
console.log('EasyMDE: Error. No element was found.');
|
|
return;
|
|
}
|
|
|
|
|
|
// Handle toolbar
|
|
if (options.toolbar === undefined) {
|
|
// Initialize
|
|
options.toolbar = [];
|
|
|
|
|
|
// Loop over the built in buttons, to get the preferred order
|
|
for (var key in toolbarBuiltInButtons) {
|
|
if (Object.prototype.hasOwnProperty.call(toolbarBuiltInButtons, key)) {
|
|
if (key.indexOf('separator-') != -1) {
|
|
options.toolbar.push('|');
|
|
}
|
|
|
|
if (toolbarBuiltInButtons[key].default === true || (options.showIcons && options.showIcons.constructor === Array && options.showIcons.indexOf(key) != -1)) {
|
|
options.toolbar.push(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Editor preview styling class.
|
|
if (!Object.prototype.hasOwnProperty.call(options, 'previewClass')) {
|
|
options.previewClass = 'editor-preview';
|
|
}
|
|
|
|
// Handle status bar
|
|
if (!Object.prototype.hasOwnProperty.call(options, 'status')) {
|
|
options.status = ['autosave', 'lines', 'words', 'cursor'];
|
|
|
|
if (options.uploadImage) {
|
|
options.status.unshift('upload-image');
|
|
}
|
|
}
|
|
|
|
|
|
// Add default preview rendering function
|
|
if (!options.previewRender) {
|
|
options.previewRender = function (plainText) {
|
|
// Note: "this" refers to the options object
|
|
return this.parent.markdown(plainText);
|
|
};
|
|
}
|
|
|
|
|
|
// Set default options for parsing config
|
|
options.parsingConfig = extend({
|
|
highlightFormatting: true, // needed for toggleCodeBlock to detect types of code
|
|
}, options.parsingConfig || {});
|
|
|
|
|
|
// Merging the insertTexts, with the given options
|
|
options.insertTexts = extend({}, insertTexts, options.insertTexts || {});
|
|
|
|
|
|
// Merging the promptTexts, with the given options
|
|
options.promptTexts = extend({}, promptTexts, options.promptTexts || {});
|
|
|
|
|
|
// Merging the blockStyles, with the given options
|
|
options.blockStyles = extend({}, blockStyles, options.blockStyles || {});
|
|
|
|
|
|
if (options.autosave != undefined) {
|
|
// Merging the Autosave timeFormat, with the given options
|
|
options.autosave.timeFormat = extend({}, timeFormat, options.autosave.timeFormat || {});
|
|
}
|
|
|
|
options.iconClassMap = extend({}, iconClassMap, options.iconClassMap || {});
|
|
|
|
// Merging the shortcuts, with the given options
|
|
options.shortcuts = extend({}, shortcuts, options.shortcuts || {});
|
|
|
|
options.maxHeight = options.maxHeight || undefined;
|
|
|
|
options.direction = options.direction || 'ltr';
|
|
|
|
if (typeof options.maxHeight !== 'undefined') {
|
|
// Min and max height are equal if maxHeight is set
|
|
options.minHeight = options.maxHeight;
|
|
} else {
|
|
options.minHeight = options.minHeight || '300px';
|
|
}
|
|
|
|
options.errorCallback = options.errorCallback || function (errorMessage) {
|
|
alert(errorMessage);
|
|
};
|
|
|
|
// Import-image default configuration
|
|
options.uploadImage = options.uploadImage || false;
|
|
options.imageMaxSize = options.imageMaxSize || 2097152; // 1024 * 1024 * 2
|
|
options.imageAccept = options.imageAccept || 'image/png, image/jpeg, image/gif, image/avif';
|
|
options.imageTexts = extend({}, imageTexts, options.imageTexts || {});
|
|
options.errorMessages = extend({}, errorMessages, options.errorMessages || {});
|
|
options.imagePathAbsolute = options.imagePathAbsolute || false;
|
|
options.imageCSRFName = options.imageCSRFName || 'csrfmiddlewaretoken';
|
|
options.imageCSRFHeader = options.imageCSRFHeader || false;
|
|
|
|
|
|
// Change unique_id to uniqueId for backwards compatibility
|
|
if (options.autosave != undefined && options.autosave.unique_id != undefined && options.autosave.unique_id != '')
|
|
options.autosave.uniqueId = options.autosave.unique_id;
|
|
|
|
// If overlay mode is specified and combine is not provided, default it to true
|
|
if (options.overlayMode && options.overlayMode.combine === undefined) {
|
|
options.overlayMode.combine = true;
|
|
}
|
|
|
|
// Update this options
|
|
this.options = options;
|
|
|
|
|
|
// Auto render
|
|
this.render();
|
|
|
|
|
|
// The codemirror component is only available after rendering
|
|
// so, the setter for the initialValue can only run after
|
|
// the element has been rendered
|
|
if (options.initialValue && (!this.options.autosave || this.options.autosave.foundSavedValue !== true)) {
|
|
this.value(options.initialValue);
|
|
}
|
|
|
|
if (options.uploadImage) {
|
|
var self = this;
|
|
|
|
this.codemirror.on('dragenter', function (cm, event) {
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbOnDragEnter);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
this.codemirror.on('dragend', function (cm, event) {
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbInit);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
this.codemirror.on('dragleave', function (cm, event) {
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbInit);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.codemirror.on('dragover', function (cm, event) {
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbOnDragEnter);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.codemirror.on('drop', function (cm, event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
if (options.imageUploadFunction) {
|
|
self.uploadImagesUsingCustomFunction(options.imageUploadFunction, event.dataTransfer.files);
|
|
} else {
|
|
self.uploadImages(event.dataTransfer.files);
|
|
}
|
|
});
|
|
|
|
this.codemirror.on('paste', function (cm, event) {
|
|
if (options.imageUploadFunction) {
|
|
self.uploadImagesUsingCustomFunction(options.imageUploadFunction, event.clipboardData.files);
|
|
} else {
|
|
self.uploadImages(event.clipboardData.files);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload asynchronously a list of images to a server.
|
|
*
|
|
* Can be triggered by:
|
|
* - drag&drop;
|
|
* - copy-paste;
|
|
* - the browse-file window (opened when the user clicks on the *upload-image* icon).
|
|
* @param {FileList} files The files to upload the the server.
|
|
* @param [onSuccess] {function} see EasyMDE.prototype.uploadImage
|
|
* @param [onError] {function} see EasyMDE.prototype.uploadImage
|
|
*/
|
|
EasyMDE.prototype.uploadImages = function (files, onSuccess, onError) {
|
|
if (files.length === 0) {
|
|
return;
|
|
}
|
|
var names = [];
|
|
for (var i = 0; i < files.length; i++) {
|
|
names.push(files[i].name);
|
|
this.uploadImage(files[i], onSuccess, onError);
|
|
}
|
|
this.updateStatusBar('upload-image', this.options.imageTexts.sbOnDrop.replace('#images_names#', names.join(', ')));
|
|
};
|
|
|
|
/**
|
|
* Upload asynchronously a list of images to a server.
|
|
*
|
|
* Can be triggered by:
|
|
* - drag&drop;
|
|
* - copy-paste;
|
|
* - the browse-file window (opened when the user clicks on the *upload-image* icon).
|
|
* @param imageUploadFunction {Function} The custom function to upload the image passed in options.
|
|
* @param {FileList} files The files to upload the the server.
|
|
*/
|
|
EasyMDE.prototype.uploadImagesUsingCustomFunction = function (imageUploadFunction, files) {
|
|
if (files.length === 0) {
|
|
return;
|
|
}
|
|
var names = [];
|
|
for (var i = 0; i < files.length; i++) {
|
|
names.push(files[i].name);
|
|
this.uploadImageUsingCustomFunction(imageUploadFunction, files[i]);
|
|
}
|
|
this.updateStatusBar('upload-image', this.options.imageTexts.sbOnDrop.replace('#images_names#', names.join(', ')));
|
|
};
|
|
|
|
/**
|
|
* Update an item in the status bar.
|
|
* @param itemName {string} The name of the item to update (ie. 'upload-image', 'autosave', etc.).
|
|
* @param content {string} the new content of the item to write in the status bar.
|
|
*/
|
|
EasyMDE.prototype.updateStatusBar = function (itemName, content) {
|
|
if (!this.gui.statusbar) {
|
|
return;
|
|
}
|
|
|
|
var matchingClasses = this.gui.statusbar.getElementsByClassName(itemName);
|
|
if (matchingClasses.length === 1) {
|
|
this.gui.statusbar.getElementsByClassName(itemName)[0].textContent = content;
|
|
} else if (matchingClasses.length === 0) {
|
|
console.log('EasyMDE: status bar item ' + itemName + ' was not found.');
|
|
} else {
|
|
console.log('EasyMDE: Several status bar items named ' + itemName + ' was found.');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Default markdown render.
|
|
*/
|
|
EasyMDE.prototype.markdown = function (text) {
|
|
if (marked) {
|
|
// Initialize
|
|
var markedOptions;
|
|
if (this.options && this.options.renderingConfig && this.options.renderingConfig.markedOptions) {
|
|
markedOptions = this.options.renderingConfig.markedOptions;
|
|
} else {
|
|
markedOptions = {};
|
|
}
|
|
|
|
// Update options
|
|
if (this.options && this.options.renderingConfig && this.options.renderingConfig.singleLineBreaks === false) {
|
|
markedOptions.breaks = false;
|
|
} else {
|
|
markedOptions.breaks = true;
|
|
}
|
|
|
|
if (this.options && this.options.renderingConfig && this.options.renderingConfig.codeSyntaxHighlighting === true) {
|
|
|
|
/* Get HLJS from config or window */
|
|
var hljs = this.options.renderingConfig.hljs || window.hljs;
|
|
|
|
/* Check if HLJS loaded */
|
|
if (hljs) {
|
|
markedOptions.highlight = function (code, language) {
|
|
if (language && hljs.getLanguage(language)) {
|
|
return hljs.highlight(language, code).value;
|
|
} else {
|
|
return hljs.highlightAuto(code).value;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// Set options
|
|
marked.setOptions(markedOptions);
|
|
|
|
// Convert the markdown to HTML
|
|
var htmlText = marked.parse(text);
|
|
|
|
// Sanitize HTML
|
|
if (this.options.renderingConfig && typeof this.options.renderingConfig.sanitizerFunction === 'function') {
|
|
htmlText = this.options.renderingConfig.sanitizerFunction.call(this, htmlText);
|
|
}
|
|
|
|
// Edit the HTML anchors to add 'target="_blank"' by default.
|
|
htmlText = addAnchorTargetBlank(htmlText);
|
|
|
|
// Remove list-style when rendering checkboxes
|
|
htmlText = removeListStyleWhenCheckbox(htmlText);
|
|
|
|
return htmlText;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Render editor to the given element.
|
|
*/
|
|
EasyMDE.prototype.render = function (el) {
|
|
if (!el) {
|
|
el = this.element || document.getElementsByTagName('textarea')[0];
|
|
}
|
|
|
|
if (this._rendered && this._rendered === el) {
|
|
// Already rendered.
|
|
return;
|
|
}
|
|
|
|
this.element = el;
|
|
var options = this.options;
|
|
|
|
var self = this;
|
|
var keyMaps = {};
|
|
|
|
for (var key in options.shortcuts) {
|
|
// null stands for "do not bind this command"
|
|
if (options.shortcuts[key] !== null && bindings[key] !== null) {
|
|
(function (key) {
|
|
keyMaps[fixShortcut(options.shortcuts[key])] = function () {
|
|
var action = bindings[key];
|
|
if (typeof action === 'function') {
|
|
action(self);
|
|
} else if (typeof action === 'string') {
|
|
window.open(action, '_blank');
|
|
}
|
|
};
|
|
})(key);
|
|
}
|
|
}
|
|
|
|
keyMaps['Enter'] = 'newlineAndIndentContinueMarkdownList';
|
|
keyMaps['Tab'] = 'tabAndIndentMarkdownList';
|
|
keyMaps['Shift-Tab'] = 'shiftTabAndUnindentMarkdownList';
|
|
keyMaps['Esc'] = function (cm) {
|
|
if (cm.getOption('fullScreen')) toggleFullScreen(self);
|
|
};
|
|
|
|
this.documentOnKeyDown = function (e) {
|
|
e = e || window.event;
|
|
|
|
if (e.keyCode == 27) {
|
|
if (self.codemirror.getOption('fullScreen')) toggleFullScreen(self);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', this.documentOnKeyDown, false);
|
|
|
|
var mode, backdrop;
|
|
|
|
// CodeMirror overlay mode
|
|
if (options.overlayMode) {
|
|
CodeMirror.defineMode('overlay-mode', function (config) {
|
|
return CodeMirror.overlayMode(CodeMirror.getMode(config, options.spellChecker !== false ? 'spell-checker' : 'gfm'), options.overlayMode.mode, options.overlayMode.combine);
|
|
});
|
|
|
|
mode = 'overlay-mode';
|
|
backdrop = options.parsingConfig;
|
|
backdrop.gitHubSpice = false;
|
|
} else {
|
|
mode = options.parsingConfig;
|
|
mode.name = 'gfm';
|
|
mode.gitHubSpice = false;
|
|
}
|
|
if (options.spellChecker !== false) {
|
|
mode = 'spell-checker';
|
|
backdrop = options.parsingConfig;
|
|
backdrop.name = 'gfm';
|
|
backdrop.gitHubSpice = false;
|
|
|
|
if (typeof options.spellChecker === 'function') {
|
|
options.spellChecker({
|
|
codeMirrorInstance: CodeMirror,
|
|
});
|
|
} else {
|
|
CodeMirrorSpellChecker({
|
|
codeMirrorInstance: CodeMirror,
|
|
});
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function configureMouse(cm, repeat, event) {
|
|
return {
|
|
addNew: false,
|
|
};
|
|
}
|
|
|
|
this.codemirror = CodeMirror.fromTextArea(el, {
|
|
mode: mode,
|
|
backdrop: backdrop,
|
|
theme: (options.theme != undefined) ? options.theme : 'easymde',
|
|
tabSize: (options.tabSize != undefined) ? options.tabSize : 2,
|
|
indentUnit: (options.tabSize != undefined) ? options.tabSize : 2,
|
|
indentWithTabs: (options.indentWithTabs === false) ? false : true,
|
|
lineNumbers: (options.lineNumbers === true) ? true : false,
|
|
autofocus: (options.autofocus === true) ? true : false,
|
|
extraKeys: keyMaps,
|
|
direction: options.direction,
|
|
lineWrapping: (options.lineWrapping === false) ? false : true,
|
|
allowDropFileTypes: ['text/plain'],
|
|
placeholder: options.placeholder || el.getAttribute('placeholder') || '',
|
|
styleSelectedText: (options.styleSelectedText != undefined) ? options.styleSelectedText : !isMobile(),
|
|
scrollbarStyle: (options.scrollbarStyle != undefined) ? options.scrollbarStyle : 'native',
|
|
configureMouse: configureMouse,
|
|
inputStyle: (options.inputStyle != undefined) ? options.inputStyle : isMobile() ? 'contenteditable' : 'textarea',
|
|
spellcheck: (options.nativeSpellcheck != undefined) ? options.nativeSpellcheck : true,
|
|
autoRefresh: (options.autoRefresh != undefined) ? options.autoRefresh : false,
|
|
});
|
|
|
|
this.codemirror.getScrollerElement().style.minHeight = options.minHeight;
|
|
|
|
if (typeof options.maxHeight !== 'undefined') {
|
|
this.codemirror.getScrollerElement().style.height = options.maxHeight;
|
|
}
|
|
|
|
if (options.forceSync === true) {
|
|
var cm = this.codemirror;
|
|
cm.on('change', function () {
|
|
cm.save();
|
|
});
|
|
}
|
|
|
|
this.gui = {};
|
|
|
|
// Wrap Codemirror with container before create toolbar, etc,
|
|
// to use with sideBySideFullscreen option.
|
|
var easyMDEContainer = document.createElement('div');
|
|
easyMDEContainer.classList.add('EasyMDEContainer');
|
|
easyMDEContainer.setAttribute('role', 'application');
|
|
var cmWrapper = this.codemirror.getWrapperElement();
|
|
cmWrapper.parentNode.insertBefore(easyMDEContainer, cmWrapper);
|
|
easyMDEContainer.appendChild(cmWrapper);
|
|
|
|
if (options.toolbar !== false) {
|
|
this.gui.toolbar = this.createToolbar();
|
|
}
|
|
if (options.status !== false) {
|
|
this.gui.statusbar = this.createStatusbar();
|
|
}
|
|
if (options.autosave != undefined && options.autosave.enabled === true) {
|
|
this.autosave(); // use to load localstorage content
|
|
this.codemirror.on('change', function () {
|
|
clearTimeout(self._autosave_timeout);
|
|
self._autosave_timeout = setTimeout(function () {
|
|
self.autosave();
|
|
}, self.options.autosave.submit_delay || self.options.autosave.delay || 1000);
|
|
});
|
|
}
|
|
|
|
function calcHeight(naturalWidth, naturalHeight) {
|
|
var height;
|
|
var viewportWidth = window.getComputedStyle(document.querySelector('.CodeMirror-sizer')).width.replace('px', '');
|
|
if (naturalWidth < viewportWidth) {
|
|
height = naturalHeight + 'px';
|
|
} else {
|
|
height = (naturalHeight / naturalWidth * 100) + '%';
|
|
}
|
|
return height;
|
|
}
|
|
|
|
var _vm = this;
|
|
|
|
|
|
function assignImageBlockAttributes(parentEl, img) {
|
|
parentEl.setAttribute('data-img-src', img.url);
|
|
parentEl.setAttribute('style', '--bg-image:url(' + img.url + ');--width:' + img.naturalWidth + 'px;--height:' + calcHeight(img.naturalWidth, img.naturalHeight));
|
|
_vm.codemirror.setSize();
|
|
}
|
|
|
|
function handleImages() {
|
|
if (!options.previewImagesInEditor) {
|
|
return;
|
|
}
|
|
|
|
easyMDEContainer.querySelectorAll('.cm-image-marker').forEach(function (e) {
|
|
var parentEl = e.parentElement;
|
|
if (!parentEl.innerText.match(/^!\[.*?\]\(.*\)/g)) {
|
|
// if img pasted on the same line with other text, don't preview, preview only images on separate line
|
|
return;
|
|
}
|
|
if (!parentEl.hasAttribute('data-img-src')) {
|
|
var srcAttr = parentEl.innerText.match('\\((.*)\\)'); // might require better parsing according to markdown spec
|
|
if (!window.EMDEimagesCache) {
|
|
window.EMDEimagesCache = {};
|
|
}
|
|
|
|
if (srcAttr && srcAttr.length >= 2) {
|
|
var keySrc = srcAttr[1];
|
|
|
|
if (options.imagesPreviewHandler) {
|
|
var newSrc = options.imagesPreviewHandler(srcAttr[1]);
|
|
// defensive check making sure the handler provided by the user returns a string
|
|
if (typeof newSrc === 'string') {
|
|
keySrc = newSrc;
|
|
}
|
|
}
|
|
|
|
if (!window.EMDEimagesCache[keySrc]) {
|
|
var img = document.createElement('img');
|
|
img.onload = function () {
|
|
window.EMDEimagesCache[keySrc] = {
|
|
naturalWidth: img.naturalWidth,
|
|
naturalHeight: img.naturalHeight,
|
|
url: keySrc,
|
|
};
|
|
assignImageBlockAttributes(parentEl, window.EMDEimagesCache[keySrc]);
|
|
};
|
|
img.src = keySrc;
|
|
} else {
|
|
assignImageBlockAttributes(parentEl, window.EMDEimagesCache[keySrc]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
this.codemirror.on('update', function () {
|
|
handleImages();
|
|
});
|
|
|
|
this.gui.sideBySide = this.createSideBySide();
|
|
this._rendered = this.element;
|
|
|
|
if (options.autofocus === true || el.autofocus) {
|
|
this.codemirror.focus();
|
|
}
|
|
|
|
// Fixes CodeMirror bug (#344)
|
|
var temp_cm = this.codemirror;
|
|
setTimeout(function () {
|
|
temp_cm.refresh();
|
|
}.bind(temp_cm), 0);
|
|
};
|
|
|
|
EasyMDE.prototype.cleanup = function () {
|
|
document.removeEventListener('keydown', this.documentOnKeyDown);
|
|
};
|
|
|
|
// Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem throw QuotaExceededError. We're going to detect this and set a variable accordingly.
|
|
function isLocalStorageAvailable() {
|
|
if (typeof localStorage === 'object') {
|
|
try {
|
|
localStorage.setItem('smde_localStorage', 1);
|
|
localStorage.removeItem('smde_localStorage');
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
EasyMDE.prototype.autosave = function () {
|
|
if (isLocalStorageAvailable()) {
|
|
var easyMDE = this;
|
|
|
|
if (this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == '') {
|
|
console.log('EasyMDE: You must set a uniqueId to use the autosave feature');
|
|
return;
|
|
}
|
|
|
|
if (this.options.autosave.binded !== true) {
|
|
if (easyMDE.element.form != null && easyMDE.element.form != undefined) {
|
|
easyMDE.element.form.addEventListener('submit', function () {
|
|
clearTimeout(easyMDE.autosaveTimeoutId);
|
|
easyMDE.autosaveTimeoutId = undefined;
|
|
|
|
localStorage.removeItem('smde_' + easyMDE.options.autosave.uniqueId);
|
|
});
|
|
}
|
|
|
|
this.options.autosave.binded = true;
|
|
}
|
|
|
|
if (this.options.autosave.loaded !== true) {
|
|
if (typeof localStorage.getItem('smde_' + this.options.autosave.uniqueId) == 'string' && localStorage.getItem('smde_' + this.options.autosave.uniqueId) != '') {
|
|
this.codemirror.setValue(localStorage.getItem('smde_' + this.options.autosave.uniqueId));
|
|
this.options.autosave.foundSavedValue = true;
|
|
}
|
|
|
|
this.options.autosave.loaded = true;
|
|
}
|
|
|
|
var value = easyMDE.value();
|
|
if (value !== '') {
|
|
localStorage.setItem('smde_' + this.options.autosave.uniqueId, value);
|
|
} else {
|
|
localStorage.removeItem('smde_' + this.options.autosave.uniqueId);
|
|
}
|
|
|
|
var el = document.getElementById('autosaved');
|
|
if (el != null && el != undefined && el != '') {
|
|
var d = new Date();
|
|
var dd = new Intl.DateTimeFormat([this.options.autosave.timeFormat.locale, 'en-US'], this.options.autosave.timeFormat.format).format(d);
|
|
var save = this.options.autosave.text == undefined ? 'Autosaved: ' : this.options.autosave.text;
|
|
|
|
el.innerHTML = save + dd;
|
|
}
|
|
} else {
|
|
console.log('EasyMDE: localStorage not available, cannot autosave');
|
|
}
|
|
};
|
|
|
|
EasyMDE.prototype.clearAutosavedValue = function () {
|
|
if (isLocalStorageAvailable()) {
|
|
if (this.options.autosave == undefined || this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == '') {
|
|
console.log('EasyMDE: You must set a uniqueId to clear the autosave value');
|
|
return;
|
|
}
|
|
|
|
localStorage.removeItem('smde_' + this.options.autosave.uniqueId);
|
|
} else {
|
|
console.log('EasyMDE: localStorage not available, cannot autosave');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Open the browse-file window to upload an image to a server.
|
|
* @param [onSuccess] {function} see EasyMDE.prototype.uploadImage
|
|
* @param [onError] {function} see EasyMDE.prototype.uploadImage
|
|
*/
|
|
EasyMDE.prototype.openBrowseFileWindow = function (onSuccess, onError) {
|
|
var self = this;
|
|
var imageInput = this.gui.toolbar.getElementsByClassName('imageInput')[0];
|
|
imageInput.click(); //dispatchEvent(new MouseEvent('click')); // replaced with click() for IE11 compatibility.
|
|
function onChange(event) {
|
|
if (self.options.imageUploadFunction) {
|
|
self.uploadImagesUsingCustomFunction(self.options.imageUploadFunction, event.target.files);
|
|
} else {
|
|
self.uploadImages(event.target.files, onSuccess, onError);
|
|
}
|
|
imageInput.removeEventListener('change', onChange);
|
|
}
|
|
|
|
imageInput.addEventListener('change', onChange);
|
|
};
|
|
|
|
/**
|
|
* Upload an image to the server.
|
|
*
|
|
* @param file {File} The image to upload, as a HTML5 File object (https://developer.mozilla.org/en-US/docs/Web/API/File)
|
|
* @param [onSuccess] {function} A callback function to execute after the image has been successfully uploaded, with one parameter:
|
|
* - url (string): The URL of the uploaded image.
|
|
* @param [onError] {function} A callback function to execute when the image upload fails, with one parameter:
|
|
* - error (string): the detailed error to display to the user (based on messages from options.errorMessages).
|
|
*/
|
|
EasyMDE.prototype.uploadImage = function (file, onSuccess, onError) {
|
|
var self = this;
|
|
onSuccess = onSuccess || function onSuccess(imageUrl) {
|
|
afterImageUploaded(self, imageUrl);
|
|
};
|
|
|
|
function onErrorSup(errorMessage) {
|
|
// show error on status bar and reset after 10000ms
|
|
self.updateStatusBar('upload-image', errorMessage);
|
|
|
|
setTimeout(function () {
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbInit);
|
|
}, 10000);
|
|
|
|
// run custom error handler
|
|
if (onError && typeof onError === 'function') {
|
|
onError(errorMessage);
|
|
}
|
|
// run error handler from options, this alerts the message.
|
|
self.options.errorCallback(errorMessage);
|
|
}
|
|
|
|
function fillErrorMessage(errorMessage) {
|
|
var units = self.options.imageTexts.sizeUnits.split(',');
|
|
return errorMessage
|
|
.replace('#image_name#', file.name)
|
|
.replace('#image_size#', humanFileSize(file.size, units))
|
|
.replace('#image_max_size#', humanFileSize(self.options.imageMaxSize, units));
|
|
}
|
|
|
|
if (file.size > this.options.imageMaxSize) {
|
|
onErrorSup(fillErrorMessage(this.options.errorMessages.fileTooLarge));
|
|
return;
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
// insert CSRF body token if provided in config.
|
|
if (self.options.imageCSRFToken && !self.options.imageCSRFHeader) {
|
|
formData.append(self.options.imageCSRFName, self.options.imageCSRFToken);
|
|
}
|
|
|
|
var request = new XMLHttpRequest();
|
|
request.upload.onprogress = function (event) {
|
|
if (event.lengthComputable) {
|
|
var progress = '' + Math.round((event.loaded * 100) / event.total);
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbProgress.replace('#file_name#', file.name).replace('#progress#', progress));
|
|
}
|
|
};
|
|
request.open('POST', this.options.imageUploadEndpoint);
|
|
|
|
// insert CSRF header token if provided in config.
|
|
if (self.options.imageCSRFToken && self.options.imageCSRFHeader) {
|
|
request.setRequestHeader(self.options.imageCSRFName, self.options.imageCSRFToken);
|
|
}
|
|
|
|
request.onload = function () {
|
|
try {
|
|
var response = JSON.parse(this.responseText);
|
|
} catch (error) {
|
|
console.error('EasyMDE: The server did not return a valid json.');
|
|
onErrorSup(fillErrorMessage(self.options.errorMessages.importError));
|
|
return;
|
|
}
|
|
if (this.status === 200 && response && !response.error && response.data && response.data.filePath) {
|
|
onSuccess((self.options.imagePathAbsolute ? '' : (window.location.origin + '/')) + response.data.filePath);
|
|
} else {
|
|
if (response.error && response.error in self.options.errorMessages) { // preformatted error message
|
|
onErrorSup(fillErrorMessage(self.options.errorMessages[response.error]));
|
|
} else if (response.error) { // server side generated error message
|
|
onErrorSup(fillErrorMessage(response.error));
|
|
} else { //unknown error
|
|
console.error('EasyMDE: Received an unexpected response after uploading the image.'
|
|
+ this.status + ' (' + this.statusText + ')');
|
|
onErrorSup(fillErrorMessage(self.options.errorMessages.importError));
|
|
}
|
|
}
|
|
};
|
|
|
|
request.onerror = function (event) {
|
|
console.error('EasyMDE: An unexpected error occurred when trying to upload the image.'
|
|
+ event.target.status + ' (' + event.target.statusText + ')');
|
|
onErrorSup(self.options.errorMessages.importError);
|
|
};
|
|
|
|
request.send(formData);
|
|
|
|
};
|
|
|
|
/**
|
|
* Upload an image to the server using a custom upload function.
|
|
*
|
|
* @param imageUploadFunction {Function} The custom function to upload the image passed in options
|
|
* @param file {File} The image to upload, as a HTML5 File object (https://developer.mozilla.org/en-US/docs/Web/API/File).
|
|
*/
|
|
EasyMDE.prototype.uploadImageUsingCustomFunction = function (imageUploadFunction, file) {
|
|
var self = this;
|
|
|
|
function onSuccess(imageUrl) {
|
|
afterImageUploaded(self, imageUrl);
|
|
}
|
|
|
|
function onError(errorMessage) {
|
|
var filledErrorMessage = fillErrorMessage(errorMessage);
|
|
// show error on status bar and reset after 10000ms
|
|
self.updateStatusBar('upload-image', filledErrorMessage);
|
|
|
|
setTimeout(function () {
|
|
self.updateStatusBar('upload-image', self.options.imageTexts.sbInit);
|
|
}, 10000);
|
|
|
|
// run error handler from options, this alerts the message.
|
|
self.options.errorCallback(filledErrorMessage);
|
|
}
|
|
|
|
function fillErrorMessage(errorMessage) {
|
|
var units = self.options.imageTexts.sizeUnits.split(',');
|
|
return errorMessage
|
|
.replace('#image_name#', file.name)
|
|
.replace('#image_size#', humanFileSize(file.size, units))
|
|
.replace('#image_max_size#', humanFileSize(self.options.imageMaxSize, units));
|
|
}
|
|
|
|
imageUploadFunction.apply(this, [file, onSuccess, onError]);
|
|
};
|
|
|
|
EasyMDE.prototype.setPreviewMaxHeight = function () {
|
|
var cm = this.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var preview = wrapper.nextSibling;
|
|
|
|
// Calc preview max height
|
|
var paddingTop = parseInt(window.getComputedStyle(wrapper).paddingTop);
|
|
var borderTopWidth = parseInt(window.getComputedStyle(wrapper).borderTopWidth);
|
|
var optionsMaxHeight = parseInt(this.options.maxHeight);
|
|
var wrapperMaxHeight = optionsMaxHeight + paddingTop * 2 + borderTopWidth * 2;
|
|
var previewMaxHeight = wrapperMaxHeight.toString() + 'px';
|
|
|
|
preview.style.height = previewMaxHeight;
|
|
};
|
|
|
|
EasyMDE.prototype.createSideBySide = function () {
|
|
var cm = this.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var preview = wrapper.nextSibling;
|
|
|
|
if (!preview || !preview.classList.contains('editor-preview-side')) {
|
|
preview = document.createElement('div');
|
|
preview.className = 'editor-preview-side';
|
|
|
|
if (this.options.previewClass) {
|
|
|
|
if (Array.isArray(this.options.previewClass)) {
|
|
for (var i = 0; i < this.options.previewClass.length; i++) {
|
|
preview.classList.add(this.options.previewClass[i]);
|
|
}
|
|
|
|
} else if (typeof this.options.previewClass === 'string') {
|
|
preview.classList.add(this.options.previewClass);
|
|
}
|
|
}
|
|
|
|
wrapper.parentNode.insertBefore(preview, wrapper.nextSibling);
|
|
}
|
|
|
|
if (typeof this.options.maxHeight !== 'undefined') {
|
|
this.setPreviewMaxHeight();
|
|
}
|
|
|
|
if (this.options.syncSideBySidePreviewScroll === false) return preview;
|
|
// Syncs scroll editor -> preview
|
|
var cScroll = false;
|
|
var pScroll = false;
|
|
cm.on('scroll', function (v) {
|
|
if (cScroll) {
|
|
cScroll = false;
|
|
return;
|
|
}
|
|
pScroll = true;
|
|
var height = v.getScrollInfo().height - v.getScrollInfo().clientHeight;
|
|
var ratio = parseFloat(v.getScrollInfo().top) / height;
|
|
var move = (preview.scrollHeight - preview.clientHeight) * ratio;
|
|
preview.scrollTop = move;
|
|
});
|
|
|
|
// Syncs scroll preview -> editor
|
|
preview.onscroll = function () {
|
|
if (pScroll) {
|
|
pScroll = false;
|
|
return;
|
|
}
|
|
cScroll = true;
|
|
var height = preview.scrollHeight - preview.clientHeight;
|
|
var ratio = parseFloat(preview.scrollTop) / height;
|
|
var move = (cm.getScrollInfo().height - cm.getScrollInfo().clientHeight) * ratio;
|
|
cm.scrollTo(0, move);
|
|
};
|
|
return preview;
|
|
};
|
|
|
|
EasyMDE.prototype.createToolbar = function (items) {
|
|
items = items || this.options.toolbar;
|
|
|
|
if (!items || items.length === 0) {
|
|
return;
|
|
}
|
|
var i;
|
|
for (i = 0; i < items.length; i++) {
|
|
if (toolbarBuiltInButtons[items[i]] != undefined) {
|
|
items[i] = toolbarBuiltInButtons[items[i]];
|
|
}
|
|
}
|
|
|
|
var bar = document.createElement('div');
|
|
bar.className = 'editor-toolbar';
|
|
bar.setAttribute('role', 'toolbar');
|
|
|
|
var self = this;
|
|
|
|
var toolbarData = {};
|
|
self.toolbar = items;
|
|
|
|
for (i = 0; i < items.length; i++) {
|
|
if (items[i].name == 'guide' && self.options.toolbarGuideIcon === false)
|
|
continue;
|
|
|
|
if (self.options.hideIcons && self.options.hideIcons.indexOf(items[i].name) != -1)
|
|
continue;
|
|
|
|
// Fullscreen does not work well on mobile devices (even tablets)
|
|
// In the future, hopefully this can be resolved
|
|
if ((items[i].name == 'fullscreen' || items[i].name == 'side-by-side') && isMobile())
|
|
continue;
|
|
|
|
|
|
// Don't include trailing separators
|
|
if (items[i] === '|') {
|
|
var nonSeparatorIconsFollow = false;
|
|
|
|
for (var x = (i + 1); x < items.length; x++) {
|
|
if (items[x] !== '|' && (!self.options.hideIcons || self.options.hideIcons.indexOf(items[x].name) == -1)) {
|
|
nonSeparatorIconsFollow = true;
|
|
}
|
|
}
|
|
|
|
if (!nonSeparatorIconsFollow)
|
|
continue;
|
|
}
|
|
|
|
|
|
// Create the icon and append to the toolbar
|
|
(function (item) {
|
|
var el;
|
|
if (item === '|') {
|
|
el = createSep();
|
|
} else if (item.children) {
|
|
el = createToolbarDropdown(item, self.options.toolbarTips, self.options.shortcuts, self);
|
|
} else {
|
|
el = createToolbarButton(item, true, self.options.toolbarTips, self.options.shortcuts, 'button', self);
|
|
}
|
|
|
|
|
|
toolbarData[item.name || item] = el;
|
|
bar.appendChild(el);
|
|
|
|
// Create the input element (ie. <input type='file'>), used among
|
|
// with the 'import-image' icon to open the browse-file window.
|
|
if (item.name === 'upload-image') {
|
|
var imageInput = document.createElement('input');
|
|
imageInput.className = 'imageInput';
|
|
imageInput.type = 'file';
|
|
imageInput.multiple = true;
|
|
imageInput.name = 'image';
|
|
imageInput.accept = self.options.imageAccept;
|
|
imageInput.style.display = 'none';
|
|
imageInput.style.opacity = 0;
|
|
bar.appendChild(imageInput);
|
|
}
|
|
})(items[i]);
|
|
}
|
|
|
|
self.toolbar_div = bar;
|
|
self.toolbarElements = toolbarData;
|
|
|
|
var cm = this.codemirror;
|
|
cm.on('cursorActivity', function () {
|
|
var stat = getState(cm);
|
|
|
|
for (var key in toolbarData) {
|
|
(function (key) {
|
|
var el = toolbarData[key];
|
|
if (stat[key]) {
|
|
el.classList.add('active');
|
|
} else if (key != 'fullscreen' && key != 'side-by-side') {
|
|
el.classList.remove('active');
|
|
}
|
|
})(key);
|
|
}
|
|
});
|
|
|
|
var cmWrapper = cm.getWrapperElement();
|
|
cmWrapper.parentNode.insertBefore(bar, cmWrapper);
|
|
return bar;
|
|
};
|
|
|
|
EasyMDE.prototype.createStatusbar = function (status) {
|
|
// Initialize
|
|
status = status || this.options.status;
|
|
var options = this.options;
|
|
var cm = this.codemirror;
|
|
|
|
// Make sure the status variable is valid
|
|
if (!status || status.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Set up the built-in items
|
|
var items = [];
|
|
var i, onUpdate, onActivity, defaultValue;
|
|
|
|
for (i = 0; i < status.length; i++) {
|
|
// Reset some values
|
|
onUpdate = undefined;
|
|
onActivity = undefined;
|
|
defaultValue = undefined;
|
|
|
|
|
|
// Handle if custom or not
|
|
if (typeof status[i] === 'object') {
|
|
items.push({
|
|
className: status[i].className,
|
|
defaultValue: status[i].defaultValue,
|
|
onUpdate: status[i].onUpdate,
|
|
onActivity: status[i].onActivity,
|
|
});
|
|
} else {
|
|
var name = status[i];
|
|
|
|
if (name === 'words') {
|
|
defaultValue = function (el) {
|
|
el.innerHTML = wordCount(cm.getValue());
|
|
};
|
|
onUpdate = function (el) {
|
|
el.innerHTML = wordCount(cm.getValue());
|
|
};
|
|
} else if (name === 'lines') {
|
|
defaultValue = function (el) {
|
|
el.innerHTML = cm.lineCount();
|
|
};
|
|
onUpdate = function (el) {
|
|
el.innerHTML = cm.lineCount();
|
|
};
|
|
} else if (name === 'cursor') {
|
|
defaultValue = function (el) {
|
|
el.innerHTML = '1:1';
|
|
};
|
|
onActivity = function (el) {
|
|
var pos = cm.getCursor();
|
|
var posLine = pos.line + 1;
|
|
var posColumn = pos.ch + 1;
|
|
el.innerHTML = posLine + ':' + posColumn;
|
|
};
|
|
} else if (name === 'autosave') {
|
|
defaultValue = function (el) {
|
|
if (options.autosave != undefined && options.autosave.enabled === true) {
|
|
el.setAttribute('id', 'autosaved');
|
|
}
|
|
};
|
|
} else if (name === 'upload-image') {
|
|
defaultValue = function (el) {
|
|
el.innerHTML = options.imageTexts.sbInit;
|
|
};
|
|
}
|
|
|
|
items.push({
|
|
className: name,
|
|
defaultValue: defaultValue,
|
|
onUpdate: onUpdate,
|
|
onActivity: onActivity,
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// Create element for the status bar
|
|
var bar = document.createElement('div');
|
|
bar.className = 'editor-statusbar';
|
|
|
|
|
|
// Create a new span for each item
|
|
for (i = 0; i < items.length; i++) {
|
|
// Store in temporary variable
|
|
var item = items[i];
|
|
|
|
|
|
// Create span element
|
|
var el = document.createElement('span');
|
|
el.className = item.className;
|
|
|
|
|
|
// Ensure the defaultValue is a function
|
|
if (typeof item.defaultValue === 'function') {
|
|
item.defaultValue(el);
|
|
}
|
|
|
|
|
|
// Ensure the onUpdate is a function
|
|
if (typeof item.onUpdate === 'function') {
|
|
// Create a closure around the span of the current action, then execute the onUpdate handler
|
|
this.codemirror.on('update', (function (el, item) {
|
|
return function () {
|
|
item.onUpdate(el);
|
|
};
|
|
}(el, item)));
|
|
}
|
|
if (typeof item.onActivity === 'function') {
|
|
// Create a closure around the span of the current action, then execute the onActivity handler
|
|
this.codemirror.on('cursorActivity', (function (el, item) {
|
|
return function () {
|
|
item.onActivity(el);
|
|
};
|
|
}(el, item)));
|
|
}
|
|
|
|
|
|
// Append the item to the status bar
|
|
bar.appendChild(el);
|
|
}
|
|
|
|
|
|
// Insert the status bar into the DOM
|
|
var cmWrapper = this.codemirror.getWrapperElement();
|
|
cmWrapper.parentNode.insertBefore(bar, cmWrapper.nextSibling);
|
|
return bar;
|
|
};
|
|
|
|
/**
|
|
* Get or set the text content.
|
|
*/
|
|
EasyMDE.prototype.value = function (val) {
|
|
var cm = this.codemirror;
|
|
if (val === undefined) {
|
|
return cm.getValue();
|
|
} else {
|
|
cm.getDoc().setValue(val);
|
|
if (this.isPreviewActive()) {
|
|
var wrapper = cm.getWrapperElement();
|
|
var preview = wrapper.lastChild;
|
|
preview.innerHTML = this.options.previewRender(val, preview);
|
|
}
|
|
return this;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Bind static methods for exports.
|
|
*/
|
|
EasyMDE.toggleBold = toggleBold;
|
|
EasyMDE.toggleItalic = toggleItalic;
|
|
EasyMDE.toggleStrikethrough = toggleStrikethrough;
|
|
EasyMDE.toggleBlockquote = toggleBlockquote;
|
|
EasyMDE.toggleHeadingSmaller = toggleHeadingSmaller;
|
|
EasyMDE.toggleHeadingBigger = toggleHeadingBigger;
|
|
EasyMDE.toggleHeading1 = toggleHeading1;
|
|
EasyMDE.toggleHeading2 = toggleHeading2;
|
|
EasyMDE.toggleHeading3 = toggleHeading3;
|
|
EasyMDE.toggleHeading4 = toggleHeading4;
|
|
EasyMDE.toggleHeading5 = toggleHeading5;
|
|
EasyMDE.toggleHeading6 = toggleHeading6;
|
|
EasyMDE.toggleCodeBlock = toggleCodeBlock;
|
|
EasyMDE.toggleUnorderedList = toggleUnorderedList;
|
|
EasyMDE.toggleOrderedList = toggleOrderedList;
|
|
EasyMDE.cleanBlock = cleanBlock;
|
|
EasyMDE.drawLink = drawLink;
|
|
EasyMDE.drawImage = drawImage;
|
|
EasyMDE.drawUploadedImage = drawUploadedImage;
|
|
EasyMDE.drawTable = drawTable;
|
|
EasyMDE.drawHorizontalRule = drawHorizontalRule;
|
|
EasyMDE.undo = undo;
|
|
EasyMDE.redo = redo;
|
|
EasyMDE.togglePreview = togglePreview;
|
|
EasyMDE.toggleSideBySide = toggleSideBySide;
|
|
EasyMDE.toggleFullScreen = toggleFullScreen;
|
|
|
|
/**
|
|
* Bind instance methods for exports.
|
|
*/
|
|
EasyMDE.prototype.toggleBold = function () {
|
|
toggleBold(this);
|
|
};
|
|
EasyMDE.prototype.toggleItalic = function () {
|
|
toggleItalic(this);
|
|
};
|
|
EasyMDE.prototype.toggleStrikethrough = function () {
|
|
toggleStrikethrough(this);
|
|
};
|
|
EasyMDE.prototype.toggleBlockquote = function () {
|
|
toggleBlockquote(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeadingSmaller = function () {
|
|
toggleHeadingSmaller(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeadingBigger = function () {
|
|
toggleHeadingBigger(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeading1 = function () {
|
|
toggleHeading1(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeading2 = function () {
|
|
toggleHeading2(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeading3 = function () {
|
|
toggleHeading3(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeading4 = function () {
|
|
toggleHeading4(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeading5 = function () {
|
|
toggleHeading5(this);
|
|
};
|
|
EasyMDE.prototype.toggleHeading6 = function () {
|
|
toggleHeading6(this);
|
|
};
|
|
EasyMDE.prototype.toggleCodeBlock = function () {
|
|
toggleCodeBlock(this);
|
|
};
|
|
EasyMDE.prototype.toggleUnorderedList = function () {
|
|
toggleUnorderedList(this);
|
|
};
|
|
EasyMDE.prototype.toggleOrderedList = function () {
|
|
toggleOrderedList(this);
|
|
};
|
|
EasyMDE.prototype.cleanBlock = function () {
|
|
cleanBlock(this);
|
|
};
|
|
EasyMDE.prototype.drawLink = function () {
|
|
drawLink(this);
|
|
};
|
|
EasyMDE.prototype.drawImage = function () {
|
|
drawImage(this);
|
|
};
|
|
EasyMDE.prototype.drawUploadedImage = function () {
|
|
drawUploadedImage(this);
|
|
};
|
|
EasyMDE.prototype.drawTable = function () {
|
|
drawTable(this);
|
|
};
|
|
EasyMDE.prototype.drawHorizontalRule = function () {
|
|
drawHorizontalRule(this);
|
|
};
|
|
EasyMDE.prototype.undo = function () {
|
|
undo(this);
|
|
};
|
|
EasyMDE.prototype.redo = function () {
|
|
redo(this);
|
|
};
|
|
EasyMDE.prototype.togglePreview = function () {
|
|
togglePreview(this);
|
|
};
|
|
EasyMDE.prototype.toggleSideBySide = function () {
|
|
toggleSideBySide(this);
|
|
};
|
|
EasyMDE.prototype.toggleFullScreen = function () {
|
|
toggleFullScreen(this);
|
|
};
|
|
|
|
EasyMDE.prototype.isPreviewActive = function () {
|
|
var cm = this.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var preview = wrapper.lastChild;
|
|
|
|
return preview.classList.contains('editor-preview-active');
|
|
};
|
|
|
|
EasyMDE.prototype.isSideBySideActive = function () {
|
|
var cm = this.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var preview = wrapper.nextSibling;
|
|
|
|
return preview.classList.contains('editor-preview-active-side');
|
|
};
|
|
|
|
EasyMDE.prototype.isFullscreenActive = function () {
|
|
var cm = this.codemirror;
|
|
|
|
return cm.getOption('fullScreen');
|
|
};
|
|
|
|
EasyMDE.prototype.getState = function () {
|
|
var cm = this.codemirror;
|
|
|
|
return getState(cm);
|
|
};
|
|
|
|
EasyMDE.prototype.toTextArea = function () {
|
|
var cm = this.codemirror;
|
|
var wrapper = cm.getWrapperElement();
|
|
var easyMDEContainer = wrapper.parentNode;
|
|
|
|
if (easyMDEContainer) {
|
|
if (this.gui.toolbar) {
|
|
easyMDEContainer.removeChild(this.gui.toolbar);
|
|
}
|
|
if (this.gui.statusbar) {
|
|
easyMDEContainer.removeChild(this.gui.statusbar);
|
|
}
|
|
if (this.gui.sideBySide) {
|
|
easyMDEContainer.removeChild(this.gui.sideBySide);
|
|
}
|
|
}
|
|
|
|
// Unwrap easyMDEcontainer before codemirror toTextArea() call
|
|
easyMDEContainer.parentNode.insertBefore(wrapper, easyMDEContainer);
|
|
easyMDEContainer.remove();
|
|
|
|
cm.toTextArea();
|
|
|
|
if (this.autosaveTimeoutId) {
|
|
clearTimeout(this.autosaveTimeoutId);
|
|
this.autosaveTimeoutId = undefined;
|
|
this.clearAutosavedValue();
|
|
}
|
|
};
|
|
|
|
module.exports = EasyMDE;
|