frostui-editor
Version:
FrostUI-Editor is a free, open-source WYSIWYG editor for Javascript.
1,196 lines (1,027 loc) • 122 kB
JavaScript
/**
* FrostUI-Editor v1.1.5
* https://github.com/elusivecodes/FrostUI-Editor
*/
(function(global, factory) {
'use strict';
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory;
} else {
factory(global);
}
})(window, function(window) {
'use strict';
if (!window) {
throw new Error('FrostUI-Editor requires a Window.');
}
if (!('UI' in window)) {
throw new Error('FrostUI-Editor requires FrostUI.');
}
const Core = window.Core;
const DOM = window.DOM;
const dom = window.dom;
const UI = window.UI;
const document = window.document;
/**
* Editor
* @class
*/
class Editor extends UI.BaseComponent {
/**
* New Editor constructor.
* @param {HTMLElement} node The input node.
* @param {object} [settings] The options to create the Editor with.
* @returns {Editor} A new Editor object.
*/
constructor(node, settings) {
super(node, settings);
if (!this._settings.buttons) {
this._settings.buttons = this.constructor.buttons;
}
if (!this._settings.fonts) {
this._settings.fonts = this.constructor.fonts;
}
this._settings.fonts = this._settings.fonts.filter(font => {
return document.fonts.check(`12px ${font}`);
});
if (!this._settings.fonts.includes(this._settings.defaultFont)) {
this._settings.defaultFont = this._settings.fonts.slice().shift();
}
this._buttons = [];
this._id = 'editor' + this.constructor._generateId();
this._render();
this._events();
const html = dom.getValue(this._node);
dom.setHTML(this._editor, html);
dom.setValue(this._source, html);
this._focusEditor();
this._execCommand('defaultParagraphSeparator', 'p');
this._checkEmpty();
this._refreshToolbar();
this._refreshLineNumbers();
dom.blur(this._editor);
EditorSet.add(this);
dom.triggerEvent(this._node, 'init.ui.editor');
this._refreshDisabled();
}
/**
* Disable the Editor.
* @returns {Editor} The Editor.
*/
disable() {
dom.setAttribute(this._node, 'disabled', true);
this._refreshDisabled();
this._refreshToolbar();
return this;
}
/**
* Dispose the Editor.
*/
dispose() {
EditorSet.remove(this);
if (this._popper) {
this._popper.dispose();
this._popper = null;
}
if (this._modal) {
UI.Modal.init(this._modal).dispose();
dom.remove(this._modal);
this._modal = null;
}
if (this._fullScreen) {
UI.Modal.init(this._fullScreen).dispose();
dom.remove(this._fullScreen);
this._fullScreen = null;
}
this._observer.disconnect();
this._observer = null;
dom.remove(this._container);
dom.show(this._node);
dom.removeAttribute(this._node, 'tabindex');
this._buttons = null;
this._container = null;
this._toolbar = null;
this._editorBody = null;
this._editorContainer = null;
this._editorScroll = null;
this._editor = null;
this._imgHighlight = null;
this._imgCursor = null;
this._imgResize = null;
this._imgSizeInfo = null;
this._sourceOuter = null;
this._sourceContainer = null;
this._sourceScroll = null;
this._lineNumbers = null;
this._source = null;
this._popover = null;
this._popoverArrow = null;
this._popoverBody = null;
this._dropTarget = null;
this._dropText = null;
this._resizeBar = null;
this._currentNode = null;
super.dispose();
}
/**
* Enable the Editor.
* @returns {Editor} The Editor.
*/
enable() {
dom.removeAttribute(this._node, 'disabled');
this._refreshDisabled();
this._refreshToolbar();
return this;
}
}
/**
* EditorSet Class
* @class
*/
class EditorSet {
/**
* Add an Editor to the set.
* @param {Editor} editor The editor to add.
*/
static add(editor) {
this._editors.push(editor);
if (this._running) {
return;
}
this._dragCount = 0;
dom.addEvent(document.body, 'dragenter.ui.editor', _ => {
if (this._dragCount === 0) {
for (const editor of this._editors) {
editor._showDropTarget();
}
}
this._dragCount++;
});
dom.addEvent(document.body, 'dragleave.ui.editor', _ => {
this._dragCount--;
if (this._dragCount === 0) {
for (const editor of this._editors) {
editor._resetDropText();
dom.hide(editor._dropTarget);
}
}
});
dom.addEvent(document.body, 'dragend.ui.editor drop.ui.editor', _ => {
this._dragCount = 0;
});
dom.addEvent(window, 'click.ui.editor', _ => {
for (const editor of this._editors) {
editor._removePopover();
}
});
dom.addEvent(window, 'resize.ui.editor', DOM.debounce(_ => {
for (const editor of this._editors) {
if (editor._currentNode && dom.is(editor._currentNode, 'img')) {
editor._highlightImage(editor._currentNode);
}
}
}));
this._running = true;
}
/**
* Remove a Editor from the set.
* @param {Editor} editor The editor to remove.
*/
static remove(editor) {
this._editors = this._editors.filter(oldEditor => oldEditor !== editor);
if (this._editors.length) {
return;
}
dom.removeEvent(document.body, 'dragenter.ui.editor');
dom.removeEvent(document.body, 'dragleave.ui.editor');
dom.removeEvent(document.body, 'dragend.ui.editor');
dom.removeEvent(window, 'click.ui.editor');
dom.removeEvent(window, 'resize.ui.editor');
this._running = false;
}
}
/**
* Editor Icons
*/
Editor.icons = {
alignCenter: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="currentColor"/></svg>',
alignJustify: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 3h18v2H3V3m0 4h18v2H3V7m0 4h18v2H3v-2m0 4h18v2H3v-2m0 4h18v2H3v-2z" fill="currentColor"/></svg>',
alignLeft: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 3h18v2H3V3m0 4h12v2H3V7m0 4h18v2H3v-2m0 4h12v2H3v-2m0 4h18v2H3v-2z" fill="currentColor"/></svg>',
alignRight: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 3h18v2H3V3m6 4h12v2H9V7m-6 4h18v2H3v-2m6 4h12v2H9v-2m-6 4h18v2H3v-2z" fill="currentColor"/></svg>',
bold: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M13.5 15.5H10v-3h3.5A1.5 1.5 0 0 1 15 14a1.5 1.5 0 0 1-1.5 1.5m-3.5-9h3A1.5 1.5 0 0 1 14.5 8A1.5 1.5 0 0 1 13 9.5h-3m5.6 1.29c.97-.68 1.65-1.79 1.65-2.79c0-2.26-1.75-4-4-4H7v14h7.04c2.1 0 3.71-1.7 3.71-3.79c0-1.52-.86-2.82-2.15-3.42z" fill="currentColor"/></svg>',
floatLeft: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 7h6v6H3V7m0-4h18v2H3V3m18 4v2H11V7h10m0 4v2H11v-2h10M3 15h14v2H3v-2m0 4h18v2H3v-2z" fill="currentColor"/></svg>',
floatNone: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 7h6v6H3V7m0-4h18v2H3V3m18 8v2H11v-2h10M3 15h14v2H3v-2m0 4h18v2H3v-2z" fill="currentColor"/></svg>',
floatRight: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M15 7h6v6h-6V7M3 3h18v2H3V3m10 4v2H3V7h10m-4 4v2H3v-2h6m-6 4h14v2H3v-2m0 4h18v2H3v-2z" fill="currentColor"/></svg>',
fullScreen: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M9.5 13.09l1.41 1.41l-4.5 4.5H10v2H3v-7h2v3.59l4.5-4.5m1.41-3.59L9.5 10.91L5 6.41V10H3V3h7v2H6.41l4.5 4.5m3.59 3.59l4.5 4.5V14h2v7h-7v-2h3.59l-4.5-4.5l1.41-1.41M13.09 9.5l4.5-4.5H14V3h7v7h-2V6.41l-4.5 4.5l-1.41-1.41z" fill="currentColor"/></svg>',
hr: '<span class="d-block" style="width: 12px; border-bottom: 2px solid currentColor;"></span>',
image: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M8.5 13.5l2.5 3l3.5-4.5l4.5 6H5m16 1V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z" fill="currentColor"/></svg>',
imageOriginal: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M12 8c-3.56 0-5.35 4.31-2.83 6.83C11.69 17.35 16 15.56 16 12c0-2.21-1.79-4-4-4m-7 7H3v4c0 1.1.9 2 2 2h4v-2H5M5 5h4V3H5c-1.1 0-2 .9-2 2v4h2m14-6h-4v2h4v4h2V5c0-1.1-.9-2-2-2m0 16h-4v2h4c1.1 0 2-.9 2-2v-4h-2" fill="currentColor"/></svg>',
imageRemove: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M5 3c-1.1 0-2 .9-2 2v14a2 2 0 0 0 2 2h9.09c-.06-.33-.09-.66-.09-1c0-.68.12-1.36.35-2H5l3.5-4.5l2.5 3l3.5-4.5l2.23 2.97c.97-.63 2.11-.97 3.27-.97c.34 0 .67.03 1 .09V5a2 2 0 0 0-2-2H5m11.47 14.88L18.59 20l-2.12 2.12l1.41 1.42L20 21.41l2.12 2.13l1.42-1.42L21.41 20l2.13-2.12l-1.42-1.42L20 18.59l-2.12-2.12l-1.42 1.41z" fill="currentColor"/></svg>',
indent: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M11 13h10v-2H11m0-2h10V7H11M3 3v2h18V3M11 17h10v-2H11M3 8v8l4-4m-4 9h18v-2H3v2z" fill="currentColor"/></svg>',
italic: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4h-8z" fill="currentColor"/></svg>',
link: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7a5 5 0 0 0-5 5a5 5 0 0 0 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1M8 13h8v-2H8v2m9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1c0 1.71-1.39 3.1-3.1 3.1h-4V17h4a5 5 0 0 0 5-5a5 5 0 0 0-5-5z" fill="currentColor"/></svg>',
linkEdit: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7h-2v7H5V5h7V3H5m12.78 1a.69.69 0 0 0-.48.2l-1.22 1.21l2.5 2.5L19.8 6.7c.26-.26.26-.7 0-.95L18.25 4.2c-.13-.13-.3-.2-.47-.2m-2.41 2.12L8 13.5V16h2.5l7.37-7.38l-2.5-2.5z" fill="currentColor"/></svg>',
orderedList: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z" fill="currentColor"/></svg>',
outdent: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M11 13h10v-2H11m0-2h10V7H11M3 3v2h18V3M3 21h18v-2H3m0-7l4 4V8m4 9h10v-2H11v2z" fill="currentColor"/></svg>',
paragraph: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M10 11a4 4 0 0 1-4-4a4 4 0 0 1 4-4h8v2h-2v16h-2V5h-2v16h-2V11z" fill="currentColor"/></svg>',
redo: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16a8.002 8.002 0 0 1 7.6-5.5c1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z" fill="currentColor"/></svg>',
removeFormat: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M6 5v.18L8.82 8h2.4l-.72 1.68l2.1 2.1L14.21 8H20V5H6M3.27 5L2 6.27l6.97 6.97L6.5 19h3l1.57-3.66L16.73 21L18 19.73L3.55 5.27L3.27 5z" fill="currentColor"/></svg>',
source: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M14.6 16.6l4.6-4.6l-4.6-4.6L16 6l6 6l-6 6l-1.4-1.4m-5.2 0L4.8 12l4.6-4.6L8 6l-6 6l6 6l1.4-1.4z" fill="currentColor"/></svg>',
strikethrough: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M3 14h18v-2H3m2-8v3h5v3h4V7h5V4m-9 15h4v-3h-4v3z" fill="currentColor"/></svg>',
style: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M18.5 4l1.16 4.35l-.96.26c-.45-.87-.91-1.74-1.44-2.18C16.73 6 16.11 6 15.5 6H13v10.5c0 .5 0 1 .33 1.25c.34.25 1 .25 1.67.25v1H9v-1c.67 0 1.33 0 1.67-.25c.33-.25.33-.75.33-1.25V6H8.5c-.61 0-1.23 0-1.76.43c-.53.44-.99 1.31-1.44 2.18l-.96-.26L5.5 4h13z" fill="currentColor"/></svg>',
superscript: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M16 7.41L11.41 12L16 16.59L14.59 18L10 13.41L5.41 18L4 16.59L8.59 12L4 7.41L5.41 6L10 10.59L14.59 6L16 7.41M21.85 9h-4.88V8l.89-.82c.76-.64 1.32-1.18 1.7-1.63c.37-.44.56-.85.57-1.23a.884.884 0 0 0-.27-.7c-.18-.19-.47-.28-.86-.29c-.31.01-.58.07-.84.17l-.66.39l-.45-1.17c.27-.22.59-.39.98-.53S18.85 2 19.32 2c.78 0 1.38.2 1.78.61c.4.39.62.93.62 1.57c-.01.56-.19 1.08-.54 1.55c-.34.48-.76.93-1.27 1.36l-.64.52v.02h2.58V9z" fill="currentColor"/></svg>',
table: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2m0 4v4h6V8H5m8 0v4h6V8h-6m-8 6v4h6v-4H5m8 0v4h6v-4h-6z" fill="currentColor"/></svg>',
tableColumnAfter: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M11 2a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H2V2h9m-7 8v4h7v-4H4m0 6v4h7v-4H4M4 4v4h7V4H4m11 7h3V8h2v3h3v2h-3v3h-2v-3h-3v-2z" fill="currentColor"/></svg>',
tableColumnBefore: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M13 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h9V2h-9m7 8v4h-7v-4h7m0 6v4h-7v-4h7m0-12v4h-7V4h7M9 11H6V8H4v3H1v2h3v3h2v-3h3v-2z" fill="currentColor"/></svg>',
tableColumnRemove: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M4 2h7a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m0 8v4h7v-4H4m0 6v4h7v-4H4M4 4v4h7V4H4m13.59 8L15 9.41L16.41 8L19 10.59L21.59 8L23 9.41L20.41 12L23 14.59L21.59 16L19 13.41L16.41 16L15 14.59L17.59 12z" fill="currentColor"/></svg>',
tableRemove: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M15.46 15.88l1.42-1.42L19 16.59l2.12-2.13l1.42 1.42L20.41 18l2.13 2.12l-1.42 1.42L19 19.41l-2.12 2.13l-1.42-1.42L17.59 18l-2.13-2.12M4 3h14a2 2 0 0 1 2 2v7.08a6.01 6.01 0 0 0-4.32.92H12v4h1.08c-.11.68-.11 1.35 0 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m0 4v4h6V7H4m8 0v4h6V7h-6m-8 6v4h6v-4H4z" fill="currentColor"/></svg>',
tableRowAfter: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M22 10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V3h2v2h4V3h2v2h4V3h2v2h4V3h2v7M4 10h4V7H4v3m6 0h4V7h-4v3m10 0V7h-4v3h4m-9 4h2v3h3v2h-3v3h-2v-3H8v-2h3v-3z" fill="currentColor"/></svg>',
tableRowBefore: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M22 14a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7h2v-2h4v2h2v-2h4v2h2v-2h4v2h2v-7M4 14h4v3H4v-3m6 0h4v3h-4v-3m10 0v3h-4v-3h4m-9-4h2V7h3V5h-3V2h-2v3H8v2h3v3z" fill="currentColor"/></svg>',
tableRowRemove: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M9.41 13L12 15.59L14.59 13L16 14.41L13.41 17L16 19.59L14.59 21L12 18.41L9.41 21L8 19.59L10.59 17L8 14.41L9.41 13M22 9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v3M4 9h4V6H4v3m6 0h4V6h-4v3m6 0h4V6h-4v3z" fill="currentColor"/></svg>',
underline: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M5 21h14v-2H5v2m7-4a6 6 0 0 0 6-6V3h-2.5v8a3.5 3.5 0 0 1-3.5 3.5A3.5 3.5 0 0 1 8.5 11V3H6v8a6 6 0 0 0 6 6z" fill="currentColor"/></svg>',
undo: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05 1-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88c3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z" fill="currentColor"/></svg>',
unlink: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M17 7h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1c0 1.43-.98 2.63-2.31 3l1.46 1.44C20.88 15.61 22 13.95 22 12a5 5 0 0 0-5-5m-1 4h-2.19l2 2H16v-2M2 4.27l3.11 3.11A4.991 4.991 0 0 0 2 12a5 5 0 0 0 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1c0-1.59 1.21-2.9 2.76-3.07L8.73 11H8v2h2.73L13 15.27V17h1.73l4.01 4L20 19.74L3.27 3L2 4.27z" fill="currentColor"/></svg>',
unorderedList: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M7 5h14v2H7V5m0 8v-2h14v2H7M4 4.5A1.5 1.5 0 0 1 5.5 6A1.5 1.5 0 0 1 4 7.5A1.5 1.5 0 0 1 2.5 6A1.5 1.5 0 0 1 4 4.5m0 6A1.5 1.5 0 0 1 5.5 12A1.5 1.5 0 0 1 4 13.5A1.5 1.5 0 0 1 2.5 12A1.5 1.5 0 0 1 4 10.5M7 19v-2h14v2H7m-3-2.5A1.5 1.5 0 0 1 5.5 18A1.5 1.5 0 0 1 4 19.5A1.5 1.5 0 0 1 2.5 18A1.5 1.5 0 0 1 4 16.5z" fill="currentColor"/></svg>',
video: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M17 10.5V7a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11l-4 4z" fill="currentColor"/></svg>'
};
/**
* Editor Plugins
*/
Editor.plugins = {
alignCenter: {
command: 'justifyCenter'
},
alignJustify: {
command: 'justifyFull'
},
alignLeft: {
command: 'justifyLeft'
},
alignRight: {
command: 'justifyRight'
},
bold: {
command: 'bold'
},
color: {
setContent() {
const backColor = document.queryCommandValue('backColor');
const foreColor = document.queryCommandValue('foreColor');
const span = dom.create('strong', {
text: 'A',
class: 'd-inline-block px-1 pe-none',
style: {
color: foreColor,
backgroundColor: backColor
}
});
return dom.getProperty(span, 'outerHTML');
},
dropdown(dropdown) {
this._colorDropdown(dropdown);
}
},
font: {
setContent() {
const fontName = document.queryCommandValue('fontName').replace(/"/g, '');
return this._settings.fonts.includes(fontName) ? fontName : this._settings.defaultFont;
},
dropdown(dropdown) {
this._fontDropdown(dropdown);
}
},
fontSize: {
setContent() {
const size = document.queryCommandValue('fontSize');
if (size) {
return this.constructor.fontSizes[size];
}
const fontSizePx = dom.css(this._editor, 'fontSize');
const fontSize = parseFloat(fontSizePx);
return Math.round(fontSize);
},
dropdown(dropdown) {
this._fontSizeDropdown(dropdown);
}
},
fullScreen: {
action() {
if (this._fullScreen) {
UI.Modal.init(this._fullScreen).hide();
return;
}
this._fullScreen = this.constructor._createModal({
content: this._container,
fullScreen: true,
onShown: _ => {
if (dom.isVisible(this._sourceContainer)) {
dom.focus(this._source);
} else {
dom.focus(this._editor);
}
},
onHide: _ => {
dom.insertBefore(this._container, this._node);
this._fullScreen = null;
}
});
}
},
hr: {
command: 'insertHorizontalRule'
},
image: {
action() {
this._showImageModal();
}
},
indent: {
command: 'indent'
},
italic: {
command: 'italic'
},
link: {
action() {
this._showLinkModal();
}
},
orderedList: {
command: 'insertOrderedList'
},
outdent: {
command: 'outdent'
},
paragraph: {
dropdown: [
['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
['indent', 'outdent']
]
},
redo: {
command: 'redo',
disableCheck: _ => !document.queryCommandEnabled('redo')
},
removeFormat: {
command: 'removeFormat'
},
source: {
action() {
if (dom.isVisible(this._sourceContainer)) {
this._showEditor();
dom.focus(this._editor);
} else {
this._showSource();
dom.focus(this._source);
}
this._refreshToolbar();
}
},
strikethrough: {
command: 'strikeThrough'
},
style: {
dropdown(dropdown) {
this._styleDropdown(dropdown);
}
},
subscript: {
command: 'subscript'
},
superscript: {
command: 'superscript'
},
table: {
dropdown(dropdown) {
this._tableDropdown(dropdown);
}
},
underline: {
command: 'underline'
},
undo: {
command: 'undo',
disableCheck: _ => !document.queryCommandEnabled('undo')
},
unlink: {
command: 'unlink'
},
unorderedList: {
command: 'insertUnorderedList'
},
video: {
action() {
this._showVideoModal();
}
}
};
/**
* Editor Popovers
*/
Editor.popovers = {
floatLeft: {
action(node) {
this._setStyle(node, 'float', 'left');
}
},
floatRight: {
action(node) {
this._setStyle(node, 'float', 'right');
}
},
floatNone: {
action(node) {
this._setStyle(node, 'float', '');
}
},
imageFull: {
content: '100%',
action(node) {
this._setStyle(node, 'width', '100%');
}
},
imageFull: {
content: '100%',
action(node) {
this._setStyle(node, 'width', '100%');
}
},
imageHalf: {
content: '50%',
action(node) {
this._setStyle(node, 'width', '50%');
}
},
imageQuarter: {
content: '25%',
action(node) {
this._setStyle(node, 'width', '25%');
}
},
imageOriginal: {
action(node) {
this._setStyle(node, 'width', '');
}
},
imageRemove: {
action(node) {
this._removeNode(node);
this._removePopover();
}
},
link: {
render(node) {
const href = dom.getAttribute(node, 'href');
return dom.create('a', {
text: href,
class: 'me-1',
attributes: {
href,
target: '_blank'
}
});
}
},
linkEdit: {
action(node) {
this._showLinkModal(node);
}
},
tableColumnAfter: {
action(node) {
this._updateTable(node, (td, _, table) => {
const index = dom.index(td);
const rows = dom.find(':scope > thead > tr, :scope > tbody > tr', table);
for (const row of rows) {
const newTd = dom.create('td');
const cells = dom.children(row, 'th, td');
dom.after(cells[index], newTd);
}
});
}
},
tableColumnBefore: {
action(node) {
this._updateTable(node, (td, _, table) => {
const index = dom.index(td);
const rows = dom.find(':scope > thead > tr, :scope > tbody > tr', table);
for (const row of rows) {
const newTd = dom.create('td');
const cells = dom.children(row, 'th, td');
dom.before(cells[index], newTd);
}
});
}
},
tableColumnRemove: {
action(node) {
this._updateTable(node, (td, _, table) => {
const index = dom.index(td);
const rows = dom.find(':scope > thead > tr, :scope > tbody > tr', table);
for (const row of rows) {
const cells = dom.children(row, 'th, td');
dom.remove(cells[index]);
}
});
}
},
tableRemove: {
action(node) {
const table = dom.closest(node, 'table', this._editor).shift();
this._removeNode(table);
this._removePopover();
}
},
tableRowAfter: {
action(node) {
this._updateTable(node, (_, tr) => {
const columns = dom.children(tr).length;
const newTr = dom.create('tr');
for (let i = 0; i < columns; i++) {
const newTd = dom.create('td');
dom.append(newTr, newTd);
}
dom.after(tr, newTr);
});
}
},
tableRowBefore: {
action(node) {
this._updateTable(node, (_, tr) => {
const columns = dom.children(tr).length;
const newTr = dom.create('tr');
for (let i = 0; i < columns; i++) {
const newTd = dom.create('td');
dom.append(newTr, newTd);
}
dom.before(tr, newTr);
});
}
},
tableRowRemove: {
action(node) {
this._updateTable(node, (_, tr) => {
dom.remove(tr);
});
}
},
unlink: {
action(node) {
dom.select(node);
this.unlink();
const range = this.constructor._getRange();
range.collapse();
}
}
};
/**
* Editor API
*/
Object.assign(Editor.prototype, {
/**
* Set the background color.
* @param {string} value The background color.
* @returns {Editor} The Editor.
*/
backColor(value) {
return this._execCommand('backColor', value);
},
/**
* Toggle bold state.
* @returns {Editor} The Editor.
*/
bold() {
return this._execCommand('bold');
},
/**
* Set the font family.
* @param {string} value The font family.
* @returns {Editor} The Editor.
*/
fontName(value) {
return this._execCommand('fontName', value);
},
/**
* Set the font size.
* @param {string} value The font size.
* @returns {Editor} The Editor.
*/
fontSize(value) {
value = Object.keys(this.constructor.fontSizes)
.find(key => this.constructor.fontSizes[key] === value);
return this._execCommand('fontSize', value);
},
/**
* Format the selected block level element.
* @param {string} value The tag name.
* @returns {Editor} The Editor.
*/
formatBlock(value) {
return this._execCommand('formatBlock', value);
},
/**
* Set the foreground color.
* @param {string} value The foreground color.
* @returns {Editor} The Editor.
*/
foreColor(value) {
return this._execCommand('foreColor', value);
},
/**
* Indent the selection.
* @returns {Editor} The Editor.
*/
indent() {
return this._execCommand('indent');
},
/**
* Insert a horizontal rule.
* @returns {Editor} The Editor.
*/
insertHorizontalRule() {
return this._execCommand('insertHorizontalRule');
},
/**
* Insert a HTML string.
* @param {string} value The HTML string.
* @returns {Editor} The Editor.
*/
insertHTML(value) {
return this._execCommand('insertHTML', value);
},
/**
* Insert an image.
* @param {string} src The image src.
* @returns {Editor} The Editor.
*/
insertImage(src) {
const img = dom.create('img', {
attributes: { src }
});
const newImg = this._insertNode(img);
const image = new Image;
image.onload = _ => {
const maxWidth = dom.width(this._editor, DOM.CONTENT_BOX);
const width = Math.min(image.width, maxWidth);
dom.setStyle(newImg, 'width', `${width}px`);
};
image.src = src;
return this;
},
/**
* Insert a link.
* @param {string} href The link href.
* @param {string} text The link text.
* @param {Boolean} [newWindow] Whether to open the link in a new window.
* @returns {Editor} The Editor.
*/
insertLink(href, text, newWindow) {
const link = dom.create('a', {
text,
attributes: {
href
}
});
if (newWindow) {
dom.setAttribute(link, 'target', '_blank');
}
this._insertNode(link);
return this;
},
/**
* Insert a text string.
* @param {string} value The text.
* @returns {Editor} The Editor.
*/
insertText(value) {
return this._execCommand('insertText', value);
},
/**
* Create an ordered list for the selection.
* @returns {Editor} The Editor.
*/
insertOrderedList() {
return this._execCommand('insertOrderedList');
},
/**
* Create an unordered list for the selection.
* @returns {Editor} The Editor.
*/
insertUnorderedList() {
return this._execCommand('insertUnorderedList');
},
/**
* Toggle italic state.
* @returns {Editor} The Editor.
*/
italic() {
return this._execCommand('italic');
},
/**
* Center the selection.
* @returns {Editor} The Editor.
*/
justifyCenter() {
return this._execCommand('justifyCenter');
},
/**
* Justify the selection.
* @returns {Editor} The Editor.
*/
justifyFull() {
return this._execCommand('justifyFull');
},
/**
* Align the selection to the left.
* @returns {Editor} The Editor.
*/
justifyLeft() {
return this._execCommand('justifyLeft');
},
/**
* Align the selection to the right.
* @returns {Editor} The Editor.
*/
justifyRight() {
return this._execCommand('justifyRight');
},
/**
* Outdent the selection.
* @returns {Editor} The Editor.
*/
outdent() {
return this._execCommand('outdent');
},
/**
* Perform a redo.
* @returns {Editor} The Editor.
*/
redo() {
return this._execCommand('redo');
},
/**
* Remove all formatting from the selection.
* @returns {Editor} The Editor.
*/
removeFormat() {
return this._execCommand('removeFormat');
},
/**
* Toggle strikethrough state.
* @returns {Editor} The Editor.
*/
strikethrough() {
return this._execCommand('strikethrough');
},
/**
* Toggle subscript state.
* @returns {Editor} The Editor.
*/
subscript() {
return this._execCommand('subscript');
},
/**
* Toggle superscript state.
* @returns {Editor} The Editor.
*/
superscript() {
return this._execCommand('superscript');
},
/**
* Toggle underline state.
* @returns {Editor} The Editor.
*/
underline() {
return this._execCommand('underline');
},
/**
* Perform an undo.
* @returns {Editor} The Editor.
*/
undo() {
this._execCommand('undo');
// fix for preserve previous range
if (document.queryCommandEnabled('undo')) {
this._execCommand('undo');
this._execCommand('redo');
} else {
const range = this.constructor._getRange();
if (range && !range.collapsed) {
range.collapse();
}
}
return this;
},
/**
* Remove the selected anchor element.
* @returns {Editor} The Editor.
*/
unlink() {
return this._execCommand('unlink');
}
});
/**
* Editor Events
*/
Object.assign(Editor.prototype, {
/**
* Attach events for the Editor.
*/
_events() {
this._eventsToolbar();
this._eventsEditor();
this._eventsPopover();
this._eventsSource();
this._eventsDrop();
if (this._settings.resizable) {
this._eventsResize();
}
},
/**
* Attach drop events.
*/
_eventsDrop() {
dom.addEventDelegate(this._editor, 'dragstart.ui.editor', 'img', e => {
e.preventDefault();
});
dom.addEvent(this._dropTarget, 'dragenter.ui.editor', _ => {
dom.addClass(this._dropText, this.constructor.classes.dropHover);
dom.setText(this._dropText, this.constructor.lang.drop.drop);
dom.show(this._dropTarget);
});
dom.addEvent(this._dropTarget, 'dragleave.ui.editor', _ => {
this._resetDropText();
});
dom.addEvent(this._dropTarget, 'dragover.ui.editor', e => {
e.preventDefault();
});
dom.addEvent(this._dropTarget, 'drop.ui.editor', e => {
e.preventDefault();
// reset drag count
EditorSet._dragCount = 0;
this._resetDropText();
dom.hide(this._dropTarget);
if (!e.dataTransfer.files.length) {
const text = e.dataTransfer.getData('text');
this.insertText(text);
return;
}
const file = e.dataTransfer.files[0];
if (file.type.substring(0, 5) !== 'image') {
return;
}
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = _ => {
this.insertImage(reader.result);
};
});
},
/**
* Attach editor events.
*/
_eventsEditor() {
dom.addEvent(this._editor, 'focus.ui.editor', e => {
if (this._noFocus) {
this._noFocus = false;
return;
}
const range = this.constructor._getRange();
if (range && !range.collapsed && !e.relatedTarget) {
range.collapse();
}
setTimeout(_ => {
const selection = window.getSelection();
if (!dom.hasDescendent(this._editor, selection.anchorNode)) {
return;
}
this._refreshCursor();
this._refreshToolbar();
}, 0);
dom.triggerEvent(this._node, 'focus.ui.editor');
});
dom.addEvent(this._editor, 'blur.ui.editor', _ => {
if (this._noBlur) {
this._noBlur = false;
return;
}
dom.triggerEvent(this._node, 'blur.ui.editor');
});
dom.addEvent(this._editor, 'input.ui.editor change.ui.editor', _ => {
const html = dom.getHTML(this._editor);
if (html === dom.getValue(this._node)) {
return;
}
dom.setValue(this._node, html);
dom.setValue(this._source, html);
this._checkEmpty();
dom.triggerEvent(this._node, 'change.ui.editor');
});
if (this._settings.keyDown) {
dom.addEvent(this._editor, 'keydown.ui.editor', e => {
this._settings.keyDown.bind(this)(e);
const event = new KeyboardEvent('', e);
this._node.dispatchEvent(event);
});
}
dom.addEvent(this._editor, 'keyup.ui.editor', e => {
this._refreshCursor();
this._refreshToolbar();
const event = new KeyboardEvent('', e);
this._node.dispatchEvent(event);
});
dom.addEventDelegate(this._editor, 'click.ui.editor', 'a, td, img', e => {
e.preventDefault();
e.stopPropagation();
setTimeout(_ => {
this._refreshToolbar();
}, 0);
if (dom.is(e.currentTarget, 'img')) {
window.getSelection().collapseToEnd();
}
this._refreshPopover(e.currentTarget, e);
}, true);
this._observer = new MutationObserver(_ => {
if (this._noMutate) {
this._noMutate = false;
return;
}
dom.triggerEvent(this._editor, 'change.ui.editor');
this._cleanupStyles();
});
this._observer.observe(this._editor, { attributes: true, childList: true, subtree: true });
},
/**
* Attach popover events.
*/
_eventsPopover() {
dom.addEventDelegate(this._popover, 'click.ui.editor', '[data-ui-action]', e => {
const action = dom.getDataset(e.currentTarget, 'uiAction');
if (!(action in this.constructor.popovers)) {
throw new Error(`Unknown action ${action}`);
}
e.preventDefault();
this.constructor.popovers[action].action.bind(this)(this._currentNode);
this._refreshPopover(this._currentNode);
});
let originalWidth;
dom.addEvent(this._imgResize, 'mousedown.ui.editor', dom.mouseDragFactory(
e => {
if (!this._currentNode || !dom.is(this._currentNode, 'img')) {
return false;
}
e.preventDefault();
e.stopPropagation();
originalWidth = dom.getStyle(this._currentNode, 'width');
dom.hide(this._imgCursor);
dom.hide(this._popover);
},
e => {
const imgRect = dom.rect(this._currentNode, true);
const ratio = imgRect.width / imgRect.height;
const width = Math.max(
e.pageX - imgRect.x,
(e.pageY - imgRect.y) * ratio,
1
);
dom.setStyle(this._currentNode, 'width', `${Math.round(width)}px`);
this._highlightImage(this._currentNode);
},
_ => {
const width = dom.getStyle(this._currentNode, 'width');
if (width !== originalWidth) {
dom.setStyle(this._currentNode, 'width', originalWidth);
this._setStyle(this._currentNode, 'width', width);
}
originalWidth = null;
}
));
dom.addEvent(this._editorBody, 'scroll.ui.editor', _ => {
this._removePopover();
});
},
/**
* Attach resize events.
*/
_eventsResize() {
let resizeOffset = 0;
dom.addEvent(this._resizeBar, 'mousedown.ui.editor', dom.mouseDragFactory(
e => {
e.preventDefault();
const barPosition = dom.position(this._resizeBar, true);
resizeOffset = e.pageY - barPosition.y;
this._removePopover();
},
e => {
const editorPosition = dom.position(this._editorBody, true);
const height = Math.max(0, e.pageY - editorPosition.y - resizeOffset);
dom.setStyle(this._editorBody, 'height', `${height}px`);
},
_ => {
resizeOffset = 0;
}
));
},
/**
* Attach source events.
*/