UNPKG

frostui-editor

Version:

FrostUI-Editor is a free, open-source WYSIWYG editor for Javascript.

1,196 lines (1,027 loc) 122 kB
/** * 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. */