UNPKG

@editorjs/editorjs

Version:

Editor.js — open source block-style WYSIWYG editor with JSON output

1,720 lines 394 kB
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-hint--align-start{text-align:left}.ce-hint--align-center{text-align:center}.ce-hint__description{opacity:.6;margin-top:3px}")),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})(); var Ce = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {}; function Ke(n) { return n && n.__esModule && Object.prototype.hasOwnProperty.call(n, "default") ? n.default : n; } function Xn(n) { if (n.__esModule) return n; var e = n.default; if (typeof e == "function") { var t = function o() { return this instanceof o ? Reflect.construct(e, arguments, this.constructor) : e.apply(this, arguments); }; t.prototype = e.prototype; } else t = {}; return Object.defineProperty(t, "__esModule", { value: !0 }), Object.keys(n).forEach(function(o) { var i = Object.getOwnPropertyDescriptor(n, o); Object.defineProperty(t, o, i.get ? i : { enumerable: !0, get: function() { return n[o]; } }); }), t; } function ot() { } Object.assign(ot, { default: ot, register: ot, revert: function() { }, __esModule: !0 }); Element.prototype.matches || (Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function(n) { const e = (this.document || this.ownerDocument).querySelectorAll(n); let t = e.length; for (; --t >= 0 && e.item(t) !== this; ) ; return t > -1; }); Element.prototype.closest || (Element.prototype.closest = function(n) { let e = this; if (!document.documentElement.contains(e)) return null; do { if (e.matches(n)) return e; e = e.parentElement || e.parentNode; } while (e !== null); return null; }); Element.prototype.prepend || (Element.prototype.prepend = function(e) { const t = document.createDocumentFragment(); Array.isArray(e) || (e = [e]), e.forEach((o) => { const i = o instanceof Node; t.appendChild(i ? o : document.createTextNode(o)); }), this.insertBefore(t, this.firstChild); }); Element.prototype.scrollIntoViewIfNeeded || (Element.prototype.scrollIntoViewIfNeeded = function(n) { n = arguments.length === 0 ? !0 : !!n; const e = this.parentNode, t = window.getComputedStyle(e, null), o = parseInt(t.getPropertyValue("border-top-width")), i = parseInt(t.getPropertyValue("border-left-width")), s = this.offsetTop - e.offsetTop < e.scrollTop, r = this.offsetTop - e.offsetTop + this.clientHeight - o > e.scrollTop + e.clientHeight, a = this.offsetLeft - e.offsetLeft < e.scrollLeft, l = this.offsetLeft - e.offsetLeft + this.clientWidth - i > e.scrollLeft + e.clientWidth, c = s && !r; (s || r) && n && (e.scrollTop = this.offsetTop - e.offsetTop - e.clientHeight / 2 - o + this.clientHeight / 2), (a || l) && n && (e.scrollLeft = this.offsetLeft - e.offsetLeft - e.clientWidth / 2 - i + this.clientWidth / 2), (s || r || a || l) && !n && this.scrollIntoView(c); }); window.requestIdleCallback = window.requestIdleCallback || function(n) { const e = Date.now(); return setTimeout(function() { n({ didTimeout: !1, timeRemaining: function() { return Math.max(0, 50 - (Date.now() - e)); } }); }, 1); }; window.cancelIdleCallback = window.cancelIdleCallback || function(n) { clearTimeout(n); }; let Vn = (n = 21) => crypto.getRandomValues(new Uint8Array(n)).reduce((e, t) => (t &= 63, t < 36 ? e += t.toString(36) : t < 62 ? e += (t - 26).toString(36).toUpperCase() : t > 62 ? e += "-" : e += "_", e), ""); var Lo = /* @__PURE__ */ ((n) => (n.VERBOSE = "VERBOSE", n.INFO = "INFO", n.WARN = "WARN", n.ERROR = "ERROR", n))(Lo || {}); const y = { BACKSPACE: 8, TAB: 9, ENTER: 13, SHIFT: 16, CTRL: 17, ALT: 18, ESC: 27, SPACE: 32, LEFT: 37, UP: 38, DOWN: 40, RIGHT: 39, DELETE: 46, META: 91, SLASH: 191 }, qn = { LEFT: 0, WHEEL: 1, RIGHT: 2, BACKWARD: 3, FORWARD: 4 }; function Ie(n, e, t = "log", o, i = "color: inherit") { if (!("console" in window) || !window.console[t]) return; const s = ["info", "log", "warn", "error"].includes(t), r = []; switch (Ie.logLevel) { case "ERROR": if (t !== "error") return; break; case "WARN": if (!["error", "warn"].includes(t)) return; break; case "INFO": if (!s || n) return; break; } o && r.push(o); const a = "Editor.js 2.31.5", l = `line-height: 1em; color: #006FEA; display: inline-block; font-size: 11px; line-height: 1em; background-color: #fff; padding: 4px 9px; border-radius: 30px; border: 1px solid rgba(56, 138, 229, 0.16); margin: 4px 5px 4px 0;`; n && (s ? (r.unshift(l, i), e = `%c${a}%c ${e}`) : e = `( ${a} )${e}`); try { s ? o ? console[t](`${e} %o`, ...r) : console[t](e, ...r) : console[t](e); } catch { } } Ie.logLevel = "VERBOSE"; function Zn(n) { Ie.logLevel = n; } const S = Ie.bind(window, !1), X = Ie.bind(window, !0); function le(n) { return Object.prototype.toString.call(n).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); } function A(n) { return le(n) === "function" || le(n) === "asyncfunction"; } function D(n) { return le(n) === "object"; } function te(n) { return le(n) === "string"; } function Gn(n) { return le(n) === "boolean"; } function yo(n) { return le(n) === "number"; } function wo(n) { return le(n) === "undefined"; } function V(n) { return n ? Object.keys(n).length === 0 && n.constructor === Object : !0; } function Po(n) { return n > 47 && n < 58 || // number keys n === 32 || n === 13 || // Space bar & return key(s) n === 229 || // processing key input for certain languages — Chinese, Japanese, etc. n > 64 && n < 91 || // letter keys n > 95 && n < 112 || // Numpad keys n > 185 && n < 193 || // ;=,-./` (in order) n > 218 && n < 223; } async function Qn(n, e = () => { }, t = () => { }) { async function o(i, s, r) { try { await i.function(i.data), await s(wo(i.data) ? {} : i.data); } catch { r(wo(i.data) ? {} : i.data); } } return n.reduce(async (i, s) => (await i, o(s, e, t)), Promise.resolve()); } function No(n) { return Array.prototype.slice.call(n); } function Fe(n, e) { return function() { const t = this, o = arguments; window.setTimeout(() => n.apply(t, o), e); }; } function Jn(n) { return n.name.split(".").pop(); } function ei(n) { return /^[-\w]+\/([-+\w]+|\*)$/.test(n); } function Eo(n, e, t) { let o; return (...i) => { const s = this, r = () => { o = null, t || n.apply(s, i); }, a = t && !o; window.clearTimeout(o), o = window.setTimeout(r, e), a && n.apply(s, i); }; } function dt(n, e, t = void 0) { let o, i, s, r = null, a = 0; t || (t = {}); const l = function() { a = t.leading === !1 ? 0 : Date.now(), r = null, s = n.apply(o, i), r || (o = i = null); }; return function() { const c = Date.now(); !a && t.leading === !1 && (a = c); const d = e - (c - a); return o = this, i = arguments, d <= 0 || d > e ? (r && (clearTimeout(r), r = null), a = c, s = n.apply(o, i), r || (o = i = null)) : !r && t.trailing !== !1 && (r = setTimeout(l, d)), s; }; } function ti() { const n = { win: !1, mac: !1, x11: !1, linux: !1 }, e = Object.keys(n).find((t) => window.navigator.appVersion.toLowerCase().indexOf(t) !== -1); return e && (n[e] = !0), n; } function je(n) { return n[0].toUpperCase() + n.slice(1); } function ut(n, ...e) { if (!e.length) return n; const t = e.shift(); if (D(n) && D(t)) for (const o in t) D(t[o]) ? (n[o] || Object.assign(n, { [o]: {} }), ut(n[o], t[o])) : Object.assign(n, { [o]: t[o] }); return ut(n, ...e); } function vt(n) { const e = ti(); return n = n.replace(/shift/gi, "⇧").replace(/backspace/gi, "⌫").replace(/enter/gi, "⏎").replace(/up/gi, "↑").replace(/left/gi, "→").replace(/down/gi, "↓").replace(/right/gi, "←").replace(/escape/gi, "⎋").replace(/insert/gi, "Ins").replace(/delete/gi, "␡").replace(/\+/gi, " + "), e.mac ? n = n.replace(/ctrl|cmd/gi, "⌘").replace(/alt/gi, "⌥") : n = n.replace(/cmd/gi, "Ctrl").replace(/windows/gi, "WIN"), n; } function oi(n) { try { return new URL(n).href; } catch { } return n.substring(0, 2) === "//" ? window.location.protocol + n : window.location.origin + n; } function ni() { return Vn(10); } function ii(n) { window.open(n, "_blank"); } function si(n = "") { return `${n}${Math.floor(Math.random() * 1e8).toString(16)}`; } function ht(n, e, t) { const o = `«${e}» is deprecated and will be removed in the next major release. Please use the «${t}» instead.`; n && X(o, "warn"); } function me(n, e, t) { const o = t.value ? "value" : "get", i = t[o], s = `#${e}Cache`; if (t[o] = function(...r) { return this[s] === void 0 && (this[s] = i.apply(this, ...r)), this[s]; }, o === "get" && t.set) { const r = t.set; t.set = function(a) { delete n[s], r.apply(this, a); }; } return t; } const Ro = 650; function be() { return window.matchMedia(`(max-width: ${Ro}px)`).matches; } const pt = typeof window < "u" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1); function ri(n, e) { const t = Array.isArray(n) || D(n), o = Array.isArray(e) || D(e); return t || o ? JSON.stringify(n) === JSON.stringify(e) : n === e; } class u { /** * Check if passed tag has no closed tag * * @param {HTMLElement} tag - element to check * @returns {boolean} */ static isSingleTag(e) { return e.tagName && [ "AREA", "BASE", "BR", "COL", "COMMAND", "EMBED", "HR", "IMG", "INPUT", "KEYGEN", "LINK", "META", "PARAM", "SOURCE", "TRACK", "WBR" ].includes(e.tagName); } /** * Check if element is BR or WBR * * @param {HTMLElement} element - element to check * @returns {boolean} */ static isLineBreakTag(e) { return e && e.tagName && [ "BR", "WBR" ].includes(e.tagName); } /** * Helper for making Elements with class name and attributes * * @param {string} tagName - new Element tag name * @param {string[]|string} [classNames] - list or name of CSS class name(s) * @param {object} [attributes] - any attributes * @returns {HTMLElement} */ static make(e, t = null, o = {}) { const i = document.createElement(e); if (Array.isArray(t)) { const s = t.filter((r) => r !== void 0); i.classList.add(...s); } else t && i.classList.add(t); for (const s in o) Object.prototype.hasOwnProperty.call(o, s) && (i[s] = o[s]); return i; } /** * Creates Text Node with the passed content * * @param {string} content - text content * @returns {Text} */ static text(e) { return document.createTextNode(e); } /** * Append one or several elements to the parent * * @param {Element|DocumentFragment} parent - where to append * @param {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list */ static append(e, t) { Array.isArray(t) ? t.forEach((o) => e.appendChild(o)) : e.appendChild(t); } /** * Append element or a couple to the beginning of the parent elements * * @param {Element} parent - where to append * @param {Element|Element[]} elements - element or elements list */ static prepend(e, t) { Array.isArray(t) ? (t = t.reverse(), t.forEach((o) => e.prepend(o))) : e.prepend(t); } /** * Swap two elements in parent * * @param {HTMLElement} el1 - from * @param {HTMLElement} el2 - to * @deprecated */ static swap(e, t) { const o = document.createElement("div"), i = e.parentNode; i.insertBefore(o, e), i.insertBefore(e, t), i.insertBefore(t, o), i.removeChild(o); } /** * Selector Decorator * * Returns first match * * @param {Element} el - element we searching inside. Default - DOM Document * @param {string} selector - searching string * @returns {Element} */ static find(e = document, t) { return e.querySelector(t); } /** * Get Element by Id * * @param {string} id - id to find * @returns {HTMLElement | null} */ static get(e) { return document.getElementById(e); } /** * Selector Decorator. * * Returns all matches * * @param {Element|Document} el - element we searching inside. Default - DOM Document * @param {string} selector - searching string * @returns {NodeList} */ static findAll(e = document, t) { return e.querySelectorAll(t); } /** * Returns CSS selector for all text inputs */ static get allInputsSelector() { return "[contenteditable=true], textarea, input:not([type]), " + ["text", "password", "email", "number", "search", "tel", "url"].map((t) => `input[type="${t}"]`).join(", "); } /** * Find all contenteditable, textarea and editable input elements passed holder contains * * @param holder - element where to find inputs */ static findAllInputs(e) { return No(e.querySelectorAll(u.allInputsSelector)).reduce((t, o) => u.isNativeInput(o) || u.containsOnlyInlineElements(o) ? [...t, o] : [...t, ...u.getDeepestBlockElements(o)], []); } /** * Search for deepest node which is Leaf. * Leaf is the vertex that doesn't have any child nodes * * @description Method recursively goes throw the all Node until it finds the Leaf * @param {Node} node - root Node. From this vertex we start Deep-first search * {@link https://en.wikipedia.org/wiki/Depth-first_search} * @param {boolean} [atLast] - find last text node * @returns - it can be text Node or Element Node, so that caret will able to work with it * Can return null if node is Document or DocumentFragment, or node is not attached to the DOM */ static getDeepestNode(e, t = !1) { const o = t ? "lastChild" : "firstChild", i = t ? "previousSibling" : "nextSibling"; if (e && e.nodeType === Node.ELEMENT_NODE && e[o]) { let s = e[o]; if (u.isSingleTag(s) && !u.isNativeInput(s) && !u.isLineBreakTag(s)) if (s[i]) s = s[i]; else if (s.parentNode[i]) s = s.parentNode[i]; else return s.parentNode; return this.getDeepestNode(s, t); } return e; } /** * Check if object is DOM node * * @param {*} node - object to check * @returns {boolean} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any static isElement(e) { return yo(e) ? !1 : e && e.nodeType && e.nodeType === Node.ELEMENT_NODE; } /** * Check if object is DocumentFragment node * * @param {object} node - object to check * @returns {boolean} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any static isFragment(e) { return yo(e) ? !1 : e && e.nodeType && e.nodeType === Node.DOCUMENT_FRAGMENT_NODE; } /** * Check if passed element is contenteditable * * @param {HTMLElement} element - html element to check * @returns {boolean} */ static isContentEditable(e) { return e.contentEditable === "true"; } /** * Checks target if it is native input * * @param {*} target - HTML element or string * @returns {boolean} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any static isNativeInput(e) { const t = [ "INPUT", "TEXTAREA" ]; return e && e.tagName ? t.includes(e.tagName) : !1; } /** * Checks if we can set caret * * @param {HTMLElement} target - target to check * @returns {boolean} */ static canSetCaret(e) { let t = !0; if (u.isNativeInput(e)) switch (e.type) { case "file": case "checkbox": case "radio": case "hidden": case "submit": case "button": case "image": case "reset": t = !1; break; } else t = u.isContentEditable(e); return t; } /** * Checks node if it is empty * * @description Method checks simple Node without any childs for emptiness * If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method * @param {Node} node - node to check * @param {string} [ignoreChars] - char or substring to treat as empty * @returns {boolean} true if it is empty */ static isNodeEmpty(e, t) { let o; return this.isSingleTag(e) && !this.isLineBreakTag(e) ? !1 : (this.isElement(e) && this.isNativeInput(e) ? o = e.value : o = e.textContent.replace("​", ""), t && (o = o.replace(new RegExp(t, "g"), "")), o.length === 0); } /** * checks node if it is doesn't have any child nodes * * @param {Node} node - node to check * @returns {boolean} */ static isLeaf(e) { return e ? e.childNodes.length === 0 : !1; } /** * breadth-first search (BFS) * {@link https://en.wikipedia.org/wiki/Breadth-first_search} * * @description Pushes to stack all DOM leafs and checks for emptiness * @param {Node} node - node to check * @param {string} [ignoreChars] - char or substring to treat as empty * @returns {boolean} */ static isEmpty(e, t) { const o = [e]; for (; o.length > 0; ) if (e = o.shift(), !!e) { if (this.isLeaf(e) && !this.isNodeEmpty(e, t)) return !1; e.childNodes && o.push(...Array.from(e.childNodes)); } return !0; } /** * Check if string contains html elements * * @param {string} str - string to check * @returns {boolean} */ static isHTMLString(e) { const t = u.make("div"); return t.innerHTML = e, t.childElementCount > 0; } /** * Return length of node`s text content * * @param {Node} node - node with content * @returns {number} */ static getContentLength(e) { return u.isNativeInput(e) ? e.value.length : e.nodeType === Node.TEXT_NODE ? e.length : e.textContent.length; } /** * Return array of names of block html elements * * @returns {string[]} */ static get blockElements() { return [ "address", "article", "aside", "blockquote", "canvas", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", "main", "nav", "noscript", "ol", "output", "p", "pre", "ruby", "section", "table", "tbody", "thead", "tr", "tfoot", "ul", "video" ]; } /** * Check if passed content includes only inline elements * * @param {string|HTMLElement} data - element or html string * @returns {boolean} */ static containsOnlyInlineElements(e) { let t; te(e) ? (t = document.createElement("div"), t.innerHTML = e) : t = e; const o = (i) => !u.blockElements.includes(i.tagName.toLowerCase()) && Array.from(i.children).every(o); return Array.from(t.children).every(o); } /** * Find and return all block elements in the passed parent (including subtree) * * @param {HTMLElement} parent - root element * @returns {HTMLElement[]} */ static getDeepestBlockElements(e) { return u.containsOnlyInlineElements(e) ? [e] : Array.from(e.children).reduce((t, o) => [...t, ...u.getDeepestBlockElements(o)], []); } /** * Helper for get holder from {string} or return HTMLElement * * @param {string | HTMLElement} element - holder's id or holder's HTML Element * @returns {HTMLElement} */ static getHolder(e) { return te(e) ? document.getElementById(e) : e; } /** * Returns true if element is anchor (is A tag) * * @param {Element} element - element to check * @returns {boolean} */ static isAnchor(e) { return e.tagName.toLowerCase() === "a"; } /** * Returns the closest ancestor anchor (A tag) of the given element (including itself) * * @param element - element to check * @returns {HTMLAnchorElement | null} */ static getClosestAnchor(e) { return e.closest("a"); } /** * Return element's offset related to the document * * @todo handle case when editor initialized in scrollable popup * @param el - element to compute offset */ static offset(e) { const t = e.getBoundingClientRect(), o = window.pageXOffset || document.documentElement.scrollLeft, i = window.pageYOffset || document.documentElement.scrollTop, s = t.top + i, r = t.left + o; return { top: s, left: r, bottom: s + t.height, right: r + t.width }; } /** * Find text node and offset by total content offset * * @param {Node} root - root node to start search from * @param {number} totalOffset - offset relative to the root node content * @returns {{node: Node | null, offset: number}} - node and offset inside node */ static getNodeByOffset(e, t) { let o = 0, i = null; const s = document.createTreeWalker( e, NodeFilter.SHOW_TEXT, null ); let r = s.nextNode(); for (; r; ) { const c = r.textContent, d = c === null ? 0 : c.length; if (i = r, o + d >= t) break; o += d, r = s.nextNode(); } if (!i) return { node: null, offset: 0 }; const a = i.textContent; if (a === null || a.length === 0) return { node: null, offset: 0 }; const l = Math.min(t - o, a.length); return { node: i, offset: l }; } } function ai(n) { return !/[^\t\n\r ]/.test(n); } function li(n) { const e = window.getComputedStyle(n), t = parseFloat(e.fontSize), o = parseFloat(e.lineHeight) || t * 1.2, i = parseFloat(e.paddingTop), s = parseFloat(e.borderTopWidth), r = parseFloat(e.marginTop), a = t * 0.8, l = (o - t) / 2; return r + s + i + l + a; } function Do(n) { n.dataset.empty = u.isEmpty(n) ? "true" : "false"; } const ci = { blockTunes: { toggler: { "Click to tune": "", "or drag to move": "" } }, inlineToolbar: { converter: { "Convert to": "" } }, toolbar: { toolbox: { Add: "" } }, popover: { Filter: "", "Nothing found": "", "Convert to": "" } }, di = { Text: "", Link: "", Bold: "", Italic: "" }, ui = { link: { "Add a link": "" }, stub: { "The block can not be displayed correctly.": "" } }, hi = { delete: { Delete: "", "Click to delete": "" }, moveUp: { "Move up": "" }, moveDown: { "Move down": "" } }, Fo = { ui: ci, toolNames: di, tools: ui, blockTunes: hi }, jo = class he { /** * Type-safe translation for internal UI texts: * Perform translation of the string by namespace and a key * * @example I18n.ui(I18nInternalNS.ui.blockTunes.toggler, 'Click to tune') * @param internalNamespace - path to translated string in dictionary * @param dictKey - dictionary key. Better to use default locale original text */ static ui(e, t) { return he._t(e, t); } /** * Translate for external strings that is not presented in default dictionary. * For example, for user-specified tool names * * @param namespace - path to translated string in dictionary * @param dictKey - dictionary key. Better to use default locale original text */ static t(e, t) { return he._t(e, t); } /** * Adjust module for using external dictionary * * @param dictionary - new messages list to override default */ static setDictionary(e) { he.currentDictionary = e; } /** * Perform translation both for internal and external namespaces * If there is no translation found, returns passed key as a translated message * * @param namespace - path to translated string in dictionary * @param dictKey - dictionary key. Better to use default locale original text */ static _t(e, t) { const o = he.getNamespace(e); return !o || !o[t] ? t : o[t]; } /** * Find messages section by namespace path * * @param namespace - path to section */ static getNamespace(e) { return e.split(".").reduce((o, i) => !o || !Object.keys(o).length ? {} : o[i], he.currentDictionary); } }; jo.currentDictionary = Fo; let z = jo; class Ho extends Error { } class Oe { constructor() { this.subscribers = {}; } /** * Subscribe any event on callback * * @param eventName - event name * @param callback - subscriber */ on(e, t) { e in this.subscribers || (this.subscribers[e] = []), this.subscribers[e].push(t); } /** * Subscribe any event on callback. Callback will be called once and be removed from subscribers array after call. * * @param eventName - event name * @param callback - subscriber */ once(e, t) { e in this.subscribers || (this.subscribers[e] = []); const o = (i) => { const s = t(i), r = this.subscribers[e].indexOf(o); return r !== -1 && this.subscribers[e].splice(r, 1), s; }; this.subscribers[e].push(o); } /** * Emit callbacks with passed data * * @param eventName - event name * @param data - subscribers get this data when they were fired */ emit(e, t) { V(this.subscribers) || !this.subscribers[e] || this.subscribers[e].reduce((o, i) => { const s = i(o); return s !== void 0 ? s : o; }, t); } /** * Unsubscribe callback from event * * @param eventName - event name * @param callback - event handler */ off(e, t) { if (this.subscribers[e] === void 0) { console.warn(`EventDispatcher .off(): there is no subscribers for event "${e.toString()}". Probably, .off() called before .on()`); return; } for (let o = 0; o < this.subscribers[e].length; o++) if (this.subscribers[e][o] === t) { delete this.subscribers[e][o]; break; } } /** * Destroyer * clears subscribers list */ destroy() { this.subscribers = {}; } } function J(n) { Object.setPrototypeOf(this, { /** * Block id * * @returns {string} */ get id() { return n.id; }, /** * Tool name * * @returns {string} */ get name() { return n.name; }, /** * Tool config passed on Editor's initialization * * @returns {ToolConfig} */ get config() { return n.config; }, /** * .ce-block element, that wraps plugin contents * * @returns {HTMLElement} */ get holder() { return n.holder; }, /** * True if Block content is empty * * @returns {boolean} */ get isEmpty() { return n.isEmpty; }, /** * True if Block is selected with Cross-Block selection * * @returns {boolean} */ get selected() { return n.selected; }, /** * Set Block's stretch state * * @param {boolean} state — state to set */ set stretched(t) { n.stretched = t; }, /** * True if Block is stretched * * @returns {boolean} */ get stretched() { return n.stretched; }, /** * True if Block has inputs to be focused */ get focusable() { return n.focusable; }, /** * Call Tool method with errors handler under-the-hood * * @param {string} methodName - method to call * @param {object} param - object with parameters * @returns {unknown} */ call(t, o) { return n.call(t, o); }, /** * Save Block content * * @returns {Promise<void|SavedData>} */ save() { return n.save(); }, /** * Validate Block data * * @param {BlockToolData} data - data to validate * @returns {Promise<boolean>} */ validate(t) { return n.validate(t); }, /** * Allows to say Editor that Block was changed. Used to manually trigger Editor's 'onChange' callback * Can be useful for block changes invisible for editor core. */ dispatchChange() { n.dispatchChange(); }, /** * Tool could specify several entries to be displayed at the Toolbox (for example, "Heading 1", "Heading 2", "Heading 3") * This method returns the entry that is related to the Block (depended on the Block data) */ getActiveToolboxEntry() { return n.getActiveToolboxEntry(); } }); } class _e { constructor() { this.allListeners = []; } /** * Assigns event listener on element and returns unique identifier * * @param {EventTarget} element - DOM element that needs to be listened * @param {string} eventType - event type * @param {Function} handler - method that will be fired on event * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once} */ on(e, t, o, i = !1) { const s = si("l"), r = { id: s, element: e, eventType: t, handler: o, options: i }; if (!this.findOne(e, t, o)) return this.allListeners.push(r), e.addEventListener(t, o, i), s; } /** * Removes event listener from element * * @param {EventTarget} element - DOM element that we removing listener * @param {string} eventType - event type * @param {Function} handler - remove handler, if element listens several handlers on the same event type * @param {boolean|AddEventListenerOptions} options - useCapture or {capture, passive, once} */ off(e, t, o, i) { const s = this.findAll(e, t, o); s.forEach((r, a) => { const l = this.allListeners.indexOf(s[a]); l > -1 && (this.allListeners.splice(l, 1), r.element.removeEventListener(r.eventType, r.handler, r.options)); }); } /** * Removes listener by id * * @param {string} id - listener identifier */ offById(e) { const t = this.findById(e); t && t.element.removeEventListener(t.eventType, t.handler, t.options); } /** * Finds and returns first listener by passed params * * @param {EventTarget} element - event target * @param {string} [eventType] - event type * @param {Function} [handler] - event handler * @returns {ListenerData|null} */ findOne(e, t, o) { const i = this.findAll(e, t, o); return i.length > 0 ? i[0] : null; } /** * Return all stored listeners by passed params * * @param {EventTarget} element - event target * @param {string} eventType - event type * @param {Function} handler - event handler * @returns {ListenerData[]} */ findAll(e, t, o) { let i; const s = e ? this.findByEventTarget(e) : []; return e && t && o ? i = s.filter((r) => r.eventType === t && r.handler === o) : e && t ? i = s.filter((r) => r.eventType === t) : i = s, i; } /** * Removes all listeners */ removeAll() { this.allListeners.map((e) => { e.element.removeEventListener(e.eventType, e.handler, e.options); }), this.allListeners = []; } /** * Module cleanup on destruction */ destroy() { this.removeAll(); } /** * Search method: looks for listener by passed element * * @param {EventTarget} element - searching element * @returns {Array} listeners that found on element */ findByEventTarget(e) { return this.allListeners.filter((t) => { if (t.element === e) return t; }); } /** * Search method: looks for listener by passed event type * * @param {string} eventType - event type * @returns {ListenerData[]} listeners that found on element */ findByType(e) { return this.allListeners.filter((t) => { if (t.eventType === e) return t; }); } /** * Search method: looks for listener by passed handler * * @param {Function} handler - event handler * @returns {ListenerData[]} listeners that found on element */ findByHandler(e) { return this.allListeners.filter((t) => { if (t.handler === e) return t; }); } /** * Returns listener data found by id * * @param {string} id - listener identifier * @returns {ListenerData} */ findById(e) { return this.allListeners.find((t) => t.id === e); } } class E { /** * @class * @param options - Module options * @param options.config - Module config * @param options.eventsDispatcher - Common event bus */ constructor({ config: e, eventsDispatcher: t }) { if (this.nodes = {}, this.listeners = new _e(), this.readOnlyMutableListeners = { /** * Assigns event listener on DOM element and pushes into special array that might be removed * * @param {EventTarget} element - DOM Element * @param {string} eventType - Event name * @param {Function} handler - Event handler * @param {boolean|AddEventListenerOptions} options - Listening options */ on: (o, i, s, r = !1) => { this.mutableListenerIds.push( this.listeners.on(o, i, s, r) ); }, /** * Clears all mutable listeners */ clearAll: () => { for (const o of this.mutableListenerIds) this.listeners.offById(o); this.mutableListenerIds = []; } }, this.mutableListenerIds = [], new.target === E) throw new TypeError("Constructors for abstract class Module are not allowed."); this.config = e, this.eventsDispatcher = t; } /** * Editor modules setter * * @param {EditorModules} Editor - Editor's Modules */ set state(e) { this.Editor = e; } /** * Remove memorized nodes */ removeAllNodes() { for (const e in this.nodes) { const t = this.nodes[e]; t instanceof HTMLElement && t.remove(); } } /** * Returns true if current direction is RTL (Right-To-Left) */ get isRtl() { return this.config.i18n.direction === "rtl"; } } class b { constructor() { this.instance = null, this.selection = null, this.savedSelectionRange = null, this.isFakeBackgroundEnabled = !1, this.commandBackground = "backColor"; } /** * Editor styles * * @returns {{editorWrapper: string, editorZone: string}} */ static get CSS() { return { editorWrapper: "codex-editor", editorZone: "codex-editor__redactor" }; } /** * Returns selected anchor * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode} * * @returns {Node|null} */ static get anchorNode() { const e = window.getSelection(); return e ? e.anchorNode : null; } /** * Returns selected anchor element * * @returns {Element|null} */ static get anchorElement() { const e = window.getSelection(); if (!e) return null; const t = e.anchorNode; return t ? u.isElement(t) ? t : t.parentElement : null; } /** * Returns selection offset according to the anchor node * {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset} * * @returns {number|null} */ static get anchorOffset() { const e = window.getSelection(); return e ? e.anchorOffset : null; } /** * Is current selection range collapsed * * @returns {boolean|null} */ static get isCollapsed() { const e = window.getSelection(); return e ? e.isCollapsed : null; } /** * Check current selection if it is at Editor's zone * * @returns {boolean} */ static get isAtEditor() { return this.isSelectionAtEditor(b.get()); } /** * Check if passed selection is at Editor's zone * * @param selection - Selection object to check */ static isSelectionAtEditor(e) { if (!e) return !1; let t = e.anchorNode || e.focusNode; t && t.nodeType === Node.TEXT_NODE && (t = t.parentNode); let o = null; return t && t instanceof Element && (o = t.closest(`.${b.CSS.editorZone}`)), o ? o.nodeType === Node.ELEMENT_NODE : !1; } /** * Check if passed range at Editor zone * * @param range - range to check */ static isRangeAtEditor(e) { if (!e) return; let t = e.startContainer; t && t.nodeType === Node.TEXT_NODE && (t = t.parentNode); let o = null; return t && t instanceof Element && (o = t.closest(`.${b.CSS.editorZone}`)), o ? o.nodeType === Node.ELEMENT_NODE : !1; } /** * Methods return boolean that true if selection exists on the page */ static get isSelectionExists() { return !!b.get().anchorNode; } /** * Return first range * * @returns {Range|null} */ static get range() { return this.getRangeFromSelection(this.get()); } /** * Returns range from passed Selection object * * @param selection - Selection object to get Range from */ static getRangeFromSelection(e) { return e && e.rangeCount ? e.getRangeAt(0) : null; } /** * Calculates position and size of selected text * * @returns {DOMRect | ClientRect} */ static get rect() { let e = document.selection, t, o = { x: 0, y: 0, width: 0, height: 0 }; if (e && e.type !== "Control") return e = e, t = e.createRange(), o.x = t.boundingLeft, o.y = t.boundingTop, o.width = t.boundingWidth, o.height = t.boundingHeight, o; if (!window.getSelection) return S("Method window.getSelection is not supported", "warn"), o; if (e = window.getSelection(), e.rangeCount === null || isNaN(e.rangeCount)) return S("Method SelectionUtils.rangeCount is not supported", "warn"), o; if (e.rangeCount === 0) return o; if (t = e.getRangeAt(0).cloneRange(), t.getBoundingClientRect && (o = t.getBoundingClientRect()), o.x === 0 && o.y === 0) { const i = document.createElement("span"); if (i.getBoundingClientRect) { i.appendChild(document.createTextNode("​")), t.insertNode(i), o = i.getBoundingClientRect(); const s = i.parentNode; s.removeChild(i), s.normalize(); } } return o; } /** * Returns selected text as String * * @returns {string} */ static get text() { return window.getSelection ? window.getSelection().toString() : ""; } /** * Returns window SelectionUtils * {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection} * * @returns {Selection} */ static get() { return window.getSelection(); } /** * Set focus to contenteditable or native input element * * @param element - element where to set focus * @param offset - offset of cursor */ static setCursor(e, t = 0) { const o = document.createRange(), i = window.getSelection(); return u.isNativeInput(e) ? u.canSetCaret(e) ? (e.focus(), e.selectionStart = e.selectionEnd = t, e.getBoundingClientRect()) : void 0 : (o.setStart(e, t), o.setEnd(e, t), i.removeAllRanges(), i.addRange(o), o.getBoundingClientRect()); } /** * Check if current range exists and belongs to container * * @param container - where range should be */ static isRangeInsideContainer(e) { const t = b.range; return t === null ? !1 : e.contains(t.startContainer); } /** * Adds fake cursor to the current range */ static addFakeCursor() { const e = b.range; if (e === null) return; const t = u.make("span", "codex-editor__fake-cursor"); t.dataset.mutationFree = "true", e.collapse(), e.insertNode(t); } /** * Check if passed element contains a fake cursor * * @param el - where to check */ static isFakeCursorInsideContainer(e) { return u.find(e, ".codex-editor__fake-cursor") !== null; } /** * Removes fake cursor from a container * * @param container - container to look for */ static removeFakeCursor(e = document.body) { const t = u.find(e, ".codex-editor__fake-cursor"); t && t.remove(); } /** * Removes fake background */ removeFakeBackground() { this.isFakeBackgroundEnabled && (document.execCommand(this.commandBackground, !1, "transparent"), this.isFakeBackgroundEnabled = !1); } /** * Sets fake background */ setFakeBackground() { document.execCommand(this.commandBackground, !1, "#a8d6ff"), this.isFakeBackgroundEnabled = !0; } /** * Save SelectionUtils's range */ save() { this.savedSelectionRange = b.range; } /** * Restore saved SelectionUtils's range */ restore() { if (!this.savedSelectionRange) return; const e = window.getSelection(); e.removeAllRanges(), e.addRange(this.savedSelectionRange); } /** * Clears saved selection */ clearSaved() { this.savedSelectionRange = null; } /** * Collapse current selection */ collapseToEnd() { const e = window.getSelection(), t = document.createRange(); t.selectNodeContents(e.focusNode), t.collapse(!1), e.removeAllRanges(), e.addRange(t); } /** * Looks ahead to find passed tag from current selection * * @param {string} tagName - tag to found * @param {string} [className] - tag's class name * @param {number} [searchDepth] - count of tags that can be included. For better performance. * @returns {HTMLElement|null} */ findParentTag(e, t, o = 10) { const i = window.getSelection(); let s = null; return !i || !i.anchorNode || !i.focusNode ? null : ([ /** the Node in which the selection begins */ i.anchorNode, /** the Node in which the selection ends */ i.focusNode ].forEach((a) => { let l = o; for (; l > 0 && a.parentNode && !(a.tagName === e && (s = a, t && a.classList && !a.classList.contains(t) && (s = null), s)); ) a = a.parentNode, l--; }), s); } /** * Expands selection range to the passed parent node * * @param {HTMLElement} element - element which contents should be selected */ expandToTag(e) { const t = window.getSelection(); t.removeAllRanges(); const o = document.createRange(); o.selectNodeContents(e), t.addRange(o); } } function pi(n, e) { const { type: t, target: o, addedNodes: i, removedNodes: s } = n; return n.type === "attributes" && n.attributeName === "data-empty" ? !1 : !!(e.contains(o) || t === "childList" && (Array.from(i).some((l) => l === e) || Array.from(s).some((l) => l === e))); } const ft = "redactor dom changed", $o = "block changed", zo = "fake cursor is about to be toggled", Uo = "fake cursor have been set", Te = "editor mobile layout toggled"; function gt(n, e) { if (!n.conversionConfig) return !1; const t = n.conversionConfig[e]; return A(t) || te(t); } function He(n, e) { return gt(n.tool, e); } function Wo(n, e) { return Object.entries(n).some(([t, o]) => e[t] && ri(e[t], o)); } async function Yo(n, e) { const o = (await n.save()).data, i = e.find((s) => s.name === n.name); return i !== void 0 && !gt(i, "export") ? [] : e.reduce((s, r) => { if (!gt(r, "import") || r.toolbox === void 0) return s; const a = r.toolbox.filter((l) => { if (V(l) || l.icon === void 0) return !1; if (l.data !== void 0) { if (Wo(l.data, o)) return !1; } else if (r.name === n.name) return !1; return !0; }); return s.push({ ...r, toolbox: a }), s; }, []); } function xo(n, e) { return n.mergeable ? n.name === e.name ? !0 : He(e, "export") && He(n, "import") : !1; } function fi(n, e) { const t = e == null ? void 0 : e.export; return A(t) ? t(n) : te(t) ? n[t] : (t !== void 0 && S("Conversion «export» property must be a string or function. String means key of saved data object to export. Function should export processed string to export."), ""); } function Bo(n, e, t) { const o = e == null ? void 0 : e.import; return A(o) ? o(n, t) : te(o) ? { [o]: n } : (o !== void 0 && S("Conversion «import» property must be a string or function. String means key of tool data to import. Function accepts a imported string and return composed tool data."), {}); } var _ = /* @__PURE__ */ ((n) => (n.Default = "default", n.Separator = "separator", n.Html = "html", n))(_ || {}), ee = /* @__PURE__ */ ((n) => (n.APPEND_CALLBACK = "appendCallback", n.RENDERED = "rendered", n.MOVED = "moved", n.UPDATED = "updated", n.REMOVED = "removed", n.ON_PASTE = "onPaste", n))(ee || {}); class R extends Oe { /** * @param options - block constructor options * @param [options.id] - block's id. Will be generated if omitted. * @param options.data - Tool's initial data * @param options.tool — block's tool * @param options.api - Editor API module for pass it to the Block Tunes * @param options.readOnly - Read-Only flag * @param [eventBus] - Editor common event bus. Allows to subscribe on some Editor events. Could be omitted when "virtual" Block is created. See BlocksAPI@composeBlockData. */ constructor({ id: e = ni(), data: t, tool: o, readOnly: i, tunesData: s }, r) { super(), this.cachedInputs = [], this.toolRenderedElement = null, this.tunesInstances = /* @__PURE__ */ new Map(), this.defaultTunesInstances = /* @__PURE__ */ new Map(), this.unavailableTunesData = {}, this.inputIndex = 0, this.editorEventBus = null, this.handleFocus = () => { this.dropInputsCache(), this.updateCurrentInput(); }, this.didMutated = (a = void 0) => { const l = a === void 0, c = a instanceof InputEvent; !l && !c && this.detectToolRootChange(a); let d; l || c ? d = !0 : d = !(a.length > 0 && a.every((p) => { const { addedNodes: g, removedNodes: f, target: v } = p; return [ ...Array.from(g), ...Array.from(f), v ].some((T) => (u.isElement(T) || (T = T.parentElement), T && T.closest('[data-mutation-free="true"]') !== null)); })), d && (this.dropInputsCache(), this.updateCurrentInput(), this.toggleInputsEmptyMark(), this.call( "updated" /* UPDATED */ ), this.emit("didMutated", this)); }, this.name = o.name, this.id = e, this.settings = o.settings, this.config = o.settings.config || {}, this.editorEventBus = r || null, this.blockAPI = new J(this), this.tool = o, this.toolInstance = o.create(t, this.blockAPI, i), this.tunes = o.tunes, this.composeTunes(s), this.holder = this.compose(), window.requestIdleCallback(() => { this.watchBlockMutations(), this.addInputEvents(), this.toggleInputsEmptyMark(); }); } /** * CSS classes for the Block * * @returns {{wrapper: string, content: string}} */ static get CSS() { return { wrapper: "ce-block", wrapperStretched: "ce-block--stretched", content: "ce-block__content", selected: "ce-block--selected", dropTarget: "ce-block--drop-target" }; } /** * Find and return all editable elements (contenteditable and native inputs) in the Tool HTML */ get inputs() { if (this.cachedInputs.length !== 0) return this.cachedInputs; const e = u.findAllInputs(this.holder); return this.inputIndex > e.length - 1 && (this.inputIndex = e.length - 1), this.cachedInputs = e, e; } /** * Return current Tool`s input * If Block doesn't contain inputs, return undefined */ get currentInput() { return this.inputs[this.inputIndex]; } /** * Set input index to the passed element * * @param element - HTML Element to set as current input */ set currentInput(e) { const t = this.inputs.findIndex((o) => o === e || o.contains(e)); t !== -1 && (this.inputIndex = t); } /** * Return first Tool`s input * If Block doesn't contain inputs, return undefined */ get firstInput() { return this.inputs[0]; } /** * Return first Tool`s input * If Block doesn't contain inputs, return undefined */ get lastInput() { const e = this.inputs; return e[e.length - 1]; } /** * Return next Tool`s input or undefined if it doesn't exist * If Block doesn't contain inputs, return undefined */ get nextInput() { return this.inputs[this.inputIndex + 1]; } /** * Return previous Tool`s input or undefined if it doesn't exist * If Block doesn't contain inputs, return undefined */ get previousInput() { return this.inputs[this.inputIndex - 1]; } /** * Get Block's JSON data * * @returns {object} */ get data() { return this.save().then((e) => e && !V(e.data) ? e.data : {}); } /** * Returns tool's sanitizer config * * @returns {object} */ get sanitize() { return this.tool.sanitizeConfig; } /** * is block mergeable * We plugin have merge function then we call it mergeable * * @returns {boolean} */ get mergeable() { return A(this.toolInstance.merge); } /** * If Block contains inputs, it is focusable */ get focusable() { return this.inputs.length !== 0; } /** * Check block for emptiness * * @returns {boolean} */ get isEmpty() { const e = u.isEmpty(this.pluginsContent, "/"), t = !this.hasMedia; return e && t; } /** * Check if block has a media content such as images, iframe and other * * @returns {boolean} */ get hasMedia() { const e = [ "img", "iframe", "video", "audio", "source", "input", "textarea", "twitterwidget" ]; return !!this.holder.querySelector(e.join(",")); } /** * Set selected state * We don't need to mark Block as Selected when it i