@editorjs/editorjs
Version:
Editor.js — open source block-style WYSIWYG editor with JSON output
1,720 lines • 394 kB
JavaScript
(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