UNPKG

@enzedonline/quill-blot-formatter2

Version:

An update for quill-blot-formatter to make quilljs v2 compatible.

1,041 lines 160 kB
class x { formatter; toolbarButtons = []; debug; constructor(t) { this.formatter = t, this.debug = this.formatter.options.debug || !1, this.debug && console.debug("Action created:", this.constructor.name); } /** * Called when the action is created. * Override this method to implement custom initialization logic. */ onCreate = () => { }; /** * Called when the action is being destroyed. * Override this method to implement custom cleanup logic. */ onDestroy = () => { }; /** * Called when the action should be updated. * Override this method to implement custom update logic. */ onUpdate = () => { }; } class v extends x { /** * Moves the caret (text cursor) backward by a specified number of characters within the current selection. * * If the caret is at the beginning of a text node, it attempts to move to the end of the previous sibling text node. * If there is no previous sibling or the selection is not valid, the caret position remains unchanged. * * @param n - The number of characters to move the caret back. Defaults to 1. */ static sendCaretBack = (t = 1, e = !1) => { const i = window.getSelection(); if (i && i.rangeCount > 0) { const o = i.getRangeAt(0), s = o.startContainer, n = o.startOffset; if (n > 0) o.setStart(s, n - t); else if (s.previousSibling) { const r = s.previousSibling; r.nodeType === Node.TEXT_NODE && o.setStart(r, r.textContent?.length || 0); } o.collapse(!0), i.removeAllRanges(), i.addRange(o), e && console.debug("Caret moved back by", t, "characters"); } }; /** * Places the caret (text cursor) immediately before the specified blot in the Quill editor. * * @param quill - The Quill editor instance. * @param targetBlot - The blot before which the caret should be placed. */ static placeCaretBeforeBlot = (t, e, i = !1) => { const o = t.getIndex(e); t.setSelection(o, 0, "user"), i && console.debug("Caret placed before blot at index:", o, e); }; /** * Places the caret (text cursor) immediately after the specified blot in the Quill editor. * * This method first clears any existing selection and ensures the editor is focused. * It then calculates the index of the target blot and determines whether it is the last blot in the document. * - If the target blot is the last one, the caret is placed at the very end of the document. * - Otherwise, the caret is positioned just after the target blot, using a combination of Quill's selection API * and a native browser adjustment to avoid placing the caret inside a formatting span wrapper. * * @param quill - The Quill editor instance. * @param targetBlot - The blot after which the caret should be placed. */ static placeCaretAfterBlot = (t, e, i = !1) => { t.setSelection(null), t.root.focus(); const o = t.getIndex(e), s = t.getLength(); o + 1 >= s - 1 ? (t.setSelection(s - 1, 0, "user"), i && console.debug("Caret placed at the end of the document after blot:", e)) : (i && console.debug("Caret placed after character following blot at index:", o, e), t.setSelection(o + 2, 0, "user"), this.sendCaretBack(1, i)); }; /** * Initializes event listeners for the CaretAction. * * Adds a 'keyup' event listener to the document and an 'input' event listener * to the Quill editor's root element. Both listeners trigger the `onKeyUp` handler. * * @remarks * This method should be called when the action is created to ensure proper * caret handling and formatting updates in response to user input. */ onCreate = () => { document.addEventListener("keyup", this.onKeyUp), this.formatter.quill.root.addEventListener("input", this.onKeyUp); }; /** * Cleans up event listeners attached by this action. * * Removes the 'keyup' event listener from the document and the 'input' event listener * from the Quill editor's root element to prevent memory leaks and unintended behavior * after the action is destroyed. */ onDestroy = () => { document.removeEventListener("keyup", this.onKeyUp), this.formatter.quill.root.removeEventListener("input", this.onKeyUp); }; /** * Handles the keyup event for caret navigation around a target blot in the editor. * * - If a modal is open or there is no current formatting specification, the handler exits early. * - If the left arrow key is pressed, places the caret before the target blot and hides the formatter UI. * - If the right arrow key is pressed, places the caret after the target blot and hides the formatter UI. * * @param e - The keyboard event triggered by the user's keyup action. */ onKeyUp = (t) => { const e = !!document.querySelector("[data-blot-formatter-modal]"); if (!this.formatter.currentSpec || e) return; const i = this.formatter.currentSpec.getTargetBlot(); i && (t.code === "ArrowLeft" ? (v.placeCaretBeforeBlot(this.formatter.quill, i, this.debug), this.formatter.hide()) : t.code === "ArrowRight" && (v.placeCaretAfterBlot(this.formatter.quill, i, this.debug), this.formatter.hide())); }; } function R(h) { return h && h.__esModule && Object.prototype.hasOwnProperty.call(h, "default") ? h.default : h; } var S, I; function O() { if (I) return S; I = 1; var h = function(c) { return t(c) && !e(c); }; function t(a) { return !!a && typeof a == "object"; } function e(a) { var c = Object.prototype.toString.call(a); return c === "[object RegExp]" || c === "[object Date]" || s(a); } var i = typeof Symbol == "function" && Symbol.for, o = i ? /* @__PURE__ */ Symbol.for("react.element") : 60103; function s(a) { return a.$$typeof === o; } function n(a) { return Array.isArray(a) ? [] : {}; } function r(a, c) { return c.clone !== !1 && c.isMergeableObject(a) ? w(n(a), a, c) : a; } function l(a, c, d) { return a.concat(c).map(function(y) { return r(y, d); }); } function p(a, c) { if (!c.customMerge) return w; var d = c.customMerge(a); return typeof d == "function" ? d : w; } function m(a) { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(a).filter(function(c) { return Object.propertyIsEnumerable.call(a, c); }) : []; } function u(a) { return Object.keys(a).concat(m(a)); } function g(a, c) { try { return c in a; } catch { return !1; } } function b(a, c) { return g(a, c) && !(Object.hasOwnProperty.call(a, c) && Object.propertyIsEnumerable.call(a, c)); } function z(a, c, d) { var y = {}; return d.isMergeableObject(a) && u(a).forEach(function(f) { y[f] = r(a[f], d); }), u(c).forEach(function(f) { b(a, f) || (g(a, f) && d.isMergeableObject(c[f]) ? y[f] = p(f, d)(a[f], c[f], d) : y[f] = r(c[f], d)); }), y; } function w(a, c, d) { d = d || {}, d.arrayMerge = d.arrayMerge || l, d.isMergeableObject = d.isMergeableObject || h, d.cloneUnlessOtherwiseSpecified = r; var y = Array.isArray(c), f = Array.isArray(a), B = y === f; return B ? y ? d.arrayMerge(a, c, d) : z(a, c, d) : r(c, d); } w.all = function(c, d) { if (!Array.isArray(c)) throw new Error("first argument should be an array"); return c.reduce(function(y, f) { return w(y, f, d); }, {}); }; var T = w; return S = T, S; } var H = O(); const N = /* @__PURE__ */ R(H); class q { formatter; element; buttons = {}; constructor(t) { this.formatter = t, this.element = document.createElement("div"), this.element.classList.add(this.formatter.options.toolbar.mainClassName), this.element.addEventListener("mousedown", (e) => { e.stopPropagation(); }), this.formatter.options.toolbar.mainStyle && Object.assign(this.element.style, this.formatter.options.toolbar.mainStyle); } /** * Creates and appends toolbar action buttons to the toolbar element. * Called by BlotFormatter.show() to initialize the toolbar. * * Iterates through all actions registered in the formatter, collects their toolbar buttons, * stores each button in the `buttons` map by its action name, and appends the created button elements * to the toolbar's DOM element. Finally, appends the toolbar element to the formatter's overlay. */ create = () => { const t = []; this.formatter.actions.forEach((e) => { e.toolbarButtons.forEach((i) => { this.buttons[i.action] = i, t.push(i.create()); }); }), this.element.append(...t), this.formatter.overlay.append(this.element), this.formatter.options.debug && console.debug("Toolbar created with buttons:", Object.keys(this.buttons), t); }; /** * Cleans up the toolbar by removing its element from the overlay, * destroying all associated buttons, and clearing internal references. * Called by BlotFormatter.hide() to remove the toolbar from the DOM. * * This should be called when the toolbar is no longer needed to prevent memory leaks. */ destroy = () => { this.element && this.formatter.overlay.removeChild(this.element); for (const t of Object.values(this.buttons)) t.destroy(); this.buttons = {}, this.element.innerHTML = "", this.formatter.options.debug && console.debug("Toolbar destroyed"); }; } class E { /** * Initializes the tooltip adjustment watcher when the action is created. * Searches for the tooltip element within the Quill container and, if found, * sets up observation for tooltip adjustments. Logs a warning if the tooltip * element is not present. * * @remarks * This method should be called during the creation lifecycle of the action. */ constructor(t, e = !1) { this.quill = t, this.debug = e; const i = t.container.querySelector(".ql-tooltip"); console.debug("tooltip:", i), i ? (E.watchTooltip(t, e), e && console.debug("Tooltip watcher initialized for:", i)) : console.warn("No tooltip found to watch for adjustments."); } quill; debug; /** * Repositions a tooltip element within a given container to ensure it does not overflow * the container's boundaries. Adjusts the tooltip's `top` and `left` CSS properties if * necessary to keep it fully visible. Optionally logs debug information about the repositioning. * * @param tooltip - The tooltip HTMLDivElement to reposition. * @param container - The container HTMLElement within which the tooltip should remain visible. * @param debug - If true, logs debug information to the console. Defaults to false. */ static _repositionTooltip = (t, e, i = !1) => { const o = t.getBoundingClientRect(), s = e.getBoundingClientRect(); let n = o.left - s.left, r = o.top - s.top; const l = o.width, p = o.height, m = e.clientWidth, u = e.clientHeight; let g = !1; const b = {}; r < 0 && (b.top = `${o.height}px`, g = !0), r + p > u && (b.top = `${u - p}px`, g = !0), n < 0 && (b.left = "0px", g = !0), n + l > m && (b.left = `${m - l}px`, g = !0), g ? (i && console.debug("Repositioning tooltip", b), b.top !== void 0 && (t.style.top = b.top), b.left !== void 0 && (t.style.left = b.left), t.classList.contains("ql-flip") && t.classList.remove("ql-flip")) : i && console.debug("Tooltip position is fine, no changes needed"); }; // Static property to store observers static observers = /* @__PURE__ */ new WeakMap(); /** * Observes changes to the tooltip's attributes and triggers repositioning when necessary. * * @param quill - The Quill editor instance containing the tooltip. * @param debug - Optional flag to enable debug logging of attribute mutations. * * @remarks * Uses a MutationObserver to monitor changes to the tooltip's `style` and `class` attributes. * When a mutation is detected, the tooltip is repositioned within the container. * If `debug` is true, mutation details are logged to the console. */ static watchTooltip(t, e = !1) { const i = t.container.querySelector(".ql-tooltip"), o = t.container; if (!i) { console.warn("No tooltip found to watch for adjustments."); return; } this.removeTooltipWatcher(i, e); let s = !1; const n = new MutationObserver((r) => { if (!s) { if (e) for (const l of r) console.debug("Tooltip mutation:", l.attributeName, i.getAttribute(l.attributeName)); s = !0, this._repositionTooltip(i, o, e), setTimeout(() => { s = !1; }, 0); } }); n.observe(i, { attributes: !0, attributeFilter: ["style", "class"] }), this.observers.set(i, n); } /** * Removes the MutationObserver for the specified tooltip element. * * @param tooltip - The HTMLDivElement or Quill instance to stop watching. * If a Quill instance is provided, finds the tooltip within its container. */ static removeTooltipWatcher(t, e = !1) { let i = null; t instanceof HTMLDivElement ? i = t : i = t.container.querySelector(".ql-tooltip"), i && this.observers.has(i) && (this.observers.get(i)?.disconnect(), this.observers.delete(i), e && console.debug("Tooltip watcher removed for:", i)); } /** * Cleans up resources when the action is destroyed. * Specifically, it finds the tooltip element within the Quill editor container * and removes its associated watcher if the tooltip exists. */ destroy = () => { this.quill.container.querySelector(".ql-tooltip") && (E.removeTooltipWatcher(this.quill, this.debug), this.debug && console.debug("Tooltip watcher removed on destroy")); }; } const W = (h) => { const t = h.import("formats/image"), e = ["alt", "height", "width", "title"]; return class extends t { static blotName = "image"; static formats(o) { return e.reduce( (s, n) => (o.hasAttribute(n) && (s[n] = o.getAttribute(n)), s), {} ); } format(o, s) { e.indexOf(o) > -1 ? s || o === "alt" ? this.domNode.setAttribute(o, s) : this.domNode.removeAttribute(o) : super.format(o, s); } }; }, D = (h) => { const t = h.import("parchment"), { ClassAttributor: e, Scope: i } = t; return class extends e { constructor(s = !1) { super("iframeAlign", "ql-iframe-align", { scope: i.BLOCK, whitelist: ["left", "center", "right"] }), this.debug = s; } debug; static attrName = "iframeAlign"; /** * Adds alignment and width-related formatting to the specified HTML element node. * * - Sets the alignment using the provided value, which can be either a string or an object containing an `align` property. * - Stores the alignment value in the element's `data-blot-align` attribute. * - Handles the element's `width` attribute: * - If present, ensures the width value includes units (appends 'px' if numeric only). * - Sets the CSS custom property `--resize-width` to the processed width value. * - Sets the `data-relative-size` attribute to `'true'` if the width ends with '%', otherwise `'false'`. * - If no width is specified, removes the `--resize-width` property and sets `data-relative-size` to `'false'`. * * @param node - The DOM element to which alignment and width formatting will be applied. * @param value - The alignment value, either as a string or an object with an `align` property. * @returns `true` if the formatting was successfully applied to an HTMLElement, otherwise `false`. */ add(s, n) { if (this.debug && console.debug("IframeAlignAttributor.add", s, n), s instanceof HTMLElement) { typeof n == "object" ? (super.add(s, n.align), s.dataset.blotAlign = n.align) : (super.add(s, n), s.dataset.blotAlign = n); let r = s.getAttribute("width"); return r ? (isNaN(Number(r.trim().slice(-1))) || (r = `${r}px`), s.style.setProperty("--resize-width", r), s.dataset.relativeSize = `${r.endsWith("%")}`) : (s.style.removeProperty("--resize-width"), s.dataset.relativeSize = "false"), this.debug && console.debug("IframeAlignAttributor.add - node:", s, "aligned with:", n), !0; } else return this.debug && console.debug("IframeAlignAttributor.add - node is not an HTMLElement, skipping alignment"), !1; } /** * Removes the alignment formatting from the specified DOM element. * * If the provided node is an instance of HTMLElement, this method first calls the * parent class's `remove` method to perform any additional removal logic, and then * deletes the `data-blot-align` attribute from the element's dataset. * * @param node - The DOM element from which to remove the alignment formatting. */ remove(s) { this.debug && console.debug("IframeAlignAttributor.remove", s), s instanceof HTMLElement && (super.remove(s), delete s.dataset.blotAlign); } /** * Extracts alignment and width information from a given DOM element. * * @param node - The DOM element from which to extract alignment and width values. * @returns An object containing: * - `align`: The alignment class name derived from the superclass's value method. * - `width`: The width value, determined from the element's CSS custom property '--resize-width', * its 'width' attribute, or an empty string if not present. * - `relativeSize`: A string indicating whether the width ends with a '%' character, representing a relative size. */ value(s) { const n = super.value(s), r = s instanceof HTMLElement && (s.style.getPropertyValue("--resize-width") || s.getAttribute("width")) || "", l = { align: n, width: r, relativeSize: `${r.endsWith("%")}` }; return this.debug && console.debug("IframeAlignAttributor.value", s, l), l; } }; }, j = (h) => { const t = h.import("parchment"), { ClassAttributor: e, Scope: i } = t; return class extends e { constructor(s = !1) { super("imageAlign", "ql-image-align", { scope: i.INLINE, whitelist: ["left", "center", "right"] }), this.debug = s; } debug; static tagName = "SPAN"; static attrName = "imageAlign"; /** * Adds or updates alignment and related formatting for an image wrapper node. * * This method applies alignment, caption, and width formatting to a given node containing an image. * It handles both object-based and string-based alignment values, updates relevant attributes, * and ensures the wrapper is styled correctly for Quill's image formatting. * * - If the node is an HTMLSpanElement, it sets alignment, caption (data-title), and width attributes. * - If the node is not a span, it attempts to find an image child and reapply the imageAlign format. * - Handles Quill's inline style merging quirks to avoid redundant wrappers. * * @param node - The DOM element (typically a span or container) to apply formatting to. * @param value - The alignment value, which can be a string or an object containing alignment and optional title. * @returns `true` if formatting was applied or handled, `false` otherwise. */ add(s, n) { if (this.debug && console.debug("ImageAlignAttributor.add", s, n), s instanceof HTMLSpanElement && n) { let r = s.querySelector("img"); if (typeof n == "object" && n.align) super.add(s, n.align), s.setAttribute("contenteditable", "false"), n.title ? s.setAttribute("data-title", n.title) : s.removeAttribute("data-title"), n.align && (r.dataset.blotAlign = n.align), this.debug && console.debug("ImageAlignAttributor.add - imageElement:", r, "aligned with:", n.align); else if (typeof n == "string") super.add(s, n), r.dataset.blotAlign = n, this.debug && console.debug("ImageAlignAttributor.add - imageElement:", r, "aligned with:", n); else return this.debug && console.debug("ImageAlignAttributor.add - no value provided, skipping alignment"), !1; let l = this.getImageWidth(r); return s.setAttribute("data-relative-size", `${l?.endsWith("%")}`), !0; } else { const r = s instanceof HTMLImageElement ? s : s.querySelector("img"); if (this.debug && console.debug(`ImageAlignAttributor.add - ${s.tagName} is not a span, checking for image:`, r), r instanceof HTMLImageElement) { const l = h.find(r); return this.debug && console.debug("ImageAlignAttributor.add - found image blot:", l), l && (s.firstChild instanceof HTMLSpanElement || !r.parentElement?.matches('span[class^="ql-image-align-"]')) && (l.format("imageAlign", n), this.debug && console.debug("ImageAlignAttributor.add - reapplying imageAlign format to image blot:", n, l)), !0; } return !1; } } /** * Removes alignment formatting from the given DOM node. * * If the node is an HTMLElement, it first calls the parent class's remove method. * Then, if the node's first child is also an HTMLElement, it deletes the `blotAlign` * data attribute from that child element. * * @param node - The DOM element from which to remove alignment formatting. */ remove(s) { this.debug && console.debug("ImageAlignAttributor.remove", s), s instanceof HTMLElement && (super.remove(s), s.firstChild && s.firstChild instanceof HTMLElement && delete s.firstChild.dataset.blotAlign); } /** * Retrieves alignment and metadata information for an image element within a given DOM node. * * This method attempts to find an `<img>` element within the provided node, then extracts its alignment, * title, and width attributes. If the width attribute is missing or invalid, it tries to determine the width * either immediately (if the image is loaded) or by setting an `onload` handler. The method returns an object * containing the alignment, title, width, a `contenteditable` flag, and a boolean string indicating if the width * is specified as a percentage. * * @param node - The DOM element to search for an image and extract alignment and metadata from. * @returns An object containing the image's alignment, title, width, contenteditable status, and relative size flag. */ value(s) { const n = s.querySelector("img"); if (!n) return null; const r = n.parentElement, l = super.value(r), p = n.getAttribute("title") || ""; let m = n.getAttribute("width") || ""; parseFloat(m) || (n.complete ? m = this.getImageWidth(n) : n.onload = (g) => { m = this.getImageWidth(g.target); }); const u = { align: l, title: p, width: m, contenteditable: "false", relativeSize: `${m.endsWith("%")}` }; return this.debug && console.debug("ImageAlignAttributor.value", s, u), u; } /** * Retrieves the width of the given HTMLImageElement, ensuring it is set as an attribute and formatted with 'px' units. * * - If the 'width' attribute is not present, it uses the image's natural width and sets it as the 'width' attribute in pixels. * - If the 'width' attribute exists but does not end with a non-numeric character (i.e., is a number), it appends 'px' to the value. * - Updates the parent element's CSS variable '--resize-width' with the computed width. * * @param imageElement - The HTMLImageElement whose width is to be retrieved and set. * @returns The width of the image as a string with 'px' units. */ getImageWidth(s) { let n = s.getAttribute("width"); return n ? isNaN(Number(n.trim().slice(-1))) || (n = `${n}px`, s.setAttribute("width", n)) : (n = `${s.naturalWidth}px`, s.setAttribute("width", n)), s.parentElement.style.setProperty("--resize-width", n), n; } }; }, $ = (h) => { const t = h.import("formats/video"); return class extends t { static blotName = "video"; static aspectRatio = "16 / 9 auto"; static create(i) { const o = super.create(i); return o.setAttribute("width", "100%"), o.style.aspectRatio = this.aspectRatio, o; } html() { return this.domNode.outerHTML; } }; }; class P { alignments = {}; options; formatter; debug; Scope; constructor(t) { this.formatter = t, this.debug = t.options.debug ?? !1; const e = t.Quill.import("parchment"); this.Scope = e.Scope, this.options = t.options, this.options.align.alignments.forEach((i) => { this.alignments[i] = { name: i, apply: (o) => { this.setAlignment(o, i); } }; }), this.debug && console.debug("DefaultAligner created with alignments:", this.alignments); } /** * Retrieves all available alignment options. * * @returns {Alignment[]} An array of alignment objects defined in the `alignments` property. */ getAlignments = () => Object.keys(this.alignments).map((t) => this.alignments[t]); /** * Clears alignment formatting from the given blot if it is an image or iframe. * * - For image blots (`IMG`), if the parent is a `SPAN`, removes the alignment attribute from the parent. * - For iframe blots (`IFRAME`), removes the alignment attribute directly from the blot. * * @param blot - The blot to clear alignment formatting from, or `null` if none. */ clear = (t) => { t != null && (t.domNode.tagName === "IMG" ? t.parent !== null && t.parent.domNode.tagName === "SPAN" && (t.parent.format(this.formatter.ImageAlign.attrName, !1), this.debug && console.debug("Cleared image alignment from parent span:", t.parent)) : t.domNode.tagName === "IFRAME" && (t.format(this.formatter.IframeAlign.attrName, !1), this.debug && console.debug("Cleared iframe alignment:", t))); }; /** * Determines whether the given blot is an inline blot. * * Checks if the provided `blot` has a scope that matches the inline blot scope. * * @param blot - The blot instance to check. * @returns `true` if the blot is an inline blot; otherwise, `false`. */ isInlineBlot = (t) => (t.statics?.scope & this.Scope.INLINE) === this.Scope.INLINE_BLOT; /** * Determines if the provided blot is a block-level blot. * * Checks the blot's static scope against the BLOCK scope constant, * and returns true if it matches the BLOCK_BLOT type. * * @param blot - The blot instance to check. * @returns True if the blot is a block blot; otherwise, false. */ isBlockBlot = (t) => (t.statics?.scope & this.Scope.BLOCK) === this.Scope.BLOCK_BLOT; /** * Determines whether the given blot has an inline scope. * * @param blot - The blot instance to check. * @returns `true` if the blot's scope includes the inline scope; otherwise, `false`. */ hasInlineScope = (t) => (t.statics.scope & this.Scope.INLINE) === this.Scope.INLINE; /** * Determines whether the given blot has block-level scope. * * @param blot - The blot instance to check. * @returns `true` if the blot's scope includes block-level formatting; otherwise, `false`. */ hasBlockScope = (t) => (t.statics.scope & this.Scope.BLOCK) === this.Scope.BLOCK; /** * Determines whether the given blot is aligned. * * If an alignment is specified, returns `true` only if the blot's alignment matches the specified alignment. * If no alignment is specified, returns `true` if the blot has any alignment. * * @param blot - The blot to check for alignment. * @param alignment - The alignment to compare against, or `null` to check for any alignment. * @returns `true` if the blot is aligned (and matches the specified alignment, if provided); otherwise, `false`. */ isAligned = (t, e) => { const i = this.getAlignment(t); return e ? i === e.name : !!i; }; /** * Retrieves the alignment value from the given blot's DOM node. * * @param blot - The blot instance whose alignment is to be determined. * @returns The alignment value as a string if present, otherwise `undefined`. */ getAlignment = (t) => t.domNode.dataset.blotAlign; /** * Sets the alignment for a given blot (content element) in the editor. * * This method checks if the blot is already aligned as requested. If not, it clears any existing alignment, * and applies the new alignment based on the blot type (inline or block). For inline blots (such as images), * it may also set a relative width attribute if required by the configuration. For block blots (such as iframes), * it applies the alignment directly. * * Additionally, if the editor contains only an image, it ensures a new paragraph is added underneath to maintain * editor usability. * * @param blot - The blot (content element) to align. Can be `null`, in which case no action is taken. * @param alignment - The alignment to apply (e.g., 'left', 'center', 'right'). Must correspond to a key in `this.alignments`. */ setAlignment = (t, e) => { if (t === null) { this.debug && console.debug("DefaultAligner.setAlignment called with null blot, no action taken"); return; } const i = this.isAligned(t, this.alignments[e]); if (this.debug && console.debug("hasAlignment", i), this.clear(t), !i) if (this.isInlineBlot(t) || this.hasInlineScope(t)) { if (this.debug && console.debug("setting alignment", this.isInlineBlot(t) || this.hasInlineScope(t)), !t.domNode.getAttribute("width") && this.options.resize.useRelativeSize && !this.options.resize.allowResizeModeChange) try { const o = getComputedStyle(this.formatter.quill.root), s = this.formatter.quill.root.clientWidth - parseFloat(o.paddingLeft) - parseFloat(o.paddingRight); t.domNode.setAttribute( "width", `${Math.min(Math.round(100 * t.domNode.naturalWidth / s), 100)}%` ); } catch { this.debug && console.debug("DefaultAligner.setAlignment Error setting image width:", t); } this.debug && console.debug( "DefaultAligner.setAlignment formatting image with", this.formatter.ImageAlign.attrName, { align: this.alignments[e].name, title: t.domNode.getAttribute("title") || "" } ), t.format( this.formatter.ImageAlign.attrName, { align: this.alignments[e].name, title: t.domNode.getAttribute("title") || "" } ); try { const o = this.formatter.quill.getContents().ops; o.length === 2 && o[1].insert === ` ` && this.formatter.quill.insertText(this.formatter.quill.getLength(), ` `, "user"); } catch { } } else (this.isBlockBlot(t) || this.hasBlockScope(t)) && (this.debug && console.debug( "DefaultAligner.setAlignment formatting iframe with", this.formatter.IframeAlign.attrName, { align: this.alignments[e].name } ), t.format( this.formatter.IframeAlign.attrName, this.alignments[e].name )); }; } class k { action; icon; onClick; options; element = null; initialVisibility = !0; // preset visibility before button is created constructor(t, e, i) { this.action = t, this.icon = i.icons[t], this.onClick = e, this.options = i; } /** * Creates and initializes the toolbar button element. * * This method constructs a `span` element, sets its inner HTML to the provided icon, * assigns the appropriate class name and action data attribute, and attaches the click handler. * If tooltips are configured for the action, it sets the tooltip text. * The button's selected and visible states are initialized, and custom styling is applied. * * @returns {HTMLElement} The created and configured toolbar button element. */ create = () => (this.element = document.createElement("span"), this.element.innerHTML = this.icon, this.element.className = this.options.buttonClassName, this.element.dataset.action = this.action, this.element.onclick = this.onClick, this.options.tooltips && this.options.tooltips[this.action] && (this.element.title = this.options.tooltips[this.action]), this.selected = this.preselect(), this.visible = this.initialVisibility, this._styleButton(), this.element); /** * Cleans up the toolbar button by removing its click event handler, * detaching it from the DOM, and clearing the reference to the element. * This method should be called when the button is no longer needed to * prevent memory leaks. */ destroy = () => { this.element && (this.element.onclick = null, this.element.remove(), this.element = null); }; /** * Determines whether the toolbar button should appear as selected (active) when loaded. * Override this method to provide custom logic for button selection state. * * @returns {boolean} `true` if the button should be preselected; otherwise, `false`. */ preselect = () => !1; /** * Indicates whether the toolbar button is currently selected. * * Returns `true` if the underlying element's `data-selected` attribute is set to `'true'`, otherwise returns `false`. */ get selected() { return this.element?.dataset.selected === "true"; } /** * Sets the selected state of the toolbar button. * * When set to `true`, applies the selected class and style to the button element. * When set to `false`, removes the selected class and style, and reapplies the default button style if provided. * Also updates the `data-selected` attribute on the element. * * @param value - Indicates whether the button should be in the selected state. */ set selected(t) { this.element && (this.element.dataset.selected = t.toString(), t ? (this.element.classList.add(this.options.buttonSelectedClassName), this.options.buttonSelectedStyle && Object.assign(this.element.style, this.options.buttonSelectedStyle)) : (this.element.classList.remove(this.options.buttonSelectedClassName), this.options.buttonSelectedStyle && (this.element.removeAttribute("style"), this.options.buttonStyle && Object.assign(this.element.style, this.options.buttonStyle)))); } /** * Indicates whether the toolbar button is currently visible. * Returns `true` if the button's element is not hidden (`display` is not set to `'none'`), otherwise returns `false`. */ get visible() { return this.element?.style.display !== "none"; } /** * Sets the visibility of the toolbar button element. * Accepts a CSS display value as a string or a boolean. * If a boolean is provided, `true` sets the display to 'inline-block', and `false` sets it to 'none'. * If a string is provided, it is used directly as the CSS display value. * * @param style - The desired visibility state, either as a CSS display string or a boolean. */ set visible(t) { this.element && (typeof t == "boolean" && (t = t ? "inline-block" : "none"), this.element.style.display = t); } /** * Applies custom styles to the toolbar button and its SVG child element, if provided in the options. * * - If `options.buttonStyle` is defined, it merges the style properties into the button's element. * - If `options.svgStyle` is defined, it merges the style properties into the first child element (assumed to be an SVG). * * @private */ _styleButton = () => { if (this.element && (this.options.buttonStyle && Object.assign(this.element.style, this.options.buttonStyle), this.options.svgStyle)) { const t = this.element.children[0]; t && Object.assign(t.style, this.options.svgStyle); } }; } class F extends x { aligner; alignButtons = {}; constructor(t) { super(t), this.aligner = new P(t), t.options.debug && console.debug("AlignAction Aligner created:", this.aligner); } /** * Creates alignment toolbar buttons for each available alignment option. * * Iterates over the alignments provided by the aligner and creates a `ToolbarButton` * for each alignment, storing them in the `alignButtons` map. If there is a currently * selected blot, it checks its alignment and preselects the corresponding button. * Optionally logs debug information about the created buttons and the current alignment. * * @private */ _createAlignmentButtons = () => { for (const e of Object.values(this.aligner.alignments)) this.alignButtons[e.name] = new k( e.name, this.onClickHandler, this.formatter.options.toolbar ); const t = this.formatter.currentSpec?.getTargetBlot(); if (t) { const e = this.aligner.getAlignment(t); e && this.alignButtons[e] && (this.alignButtons[e].preselect = () => !0), this.debug && (console.debug("AlignAction alignment buttons created:", this.alignButtons), console.debug("Blot alignment on load:", e)); } }; /** * Clears the selection state of all alignment buttons. * * Iterates through all buttons in the `alignButtons` collection and sets their * `selected` property to `false`. If debugging is enabled, logs a message to the console. * * @private */ _clearButtons = () => { for (const t of Object.values(this.alignButtons)) t.selected = !1; this.debug && console.debug("AlignAction alignment buttons cleared"); }; /** * Handles click events on alignment toolbar buttons. * * This event handler determines which alignment action was triggered by the user, * retrieves the corresponding alignment configuration, and applies or clears the alignment * on the currently selected blot in the editor. It also updates the toolbar button states * and logs debug information if enabled. * * @param event - The click event triggered by the user on a toolbar button. */ onClickHandler = (t) => { const e = t.target.closest(`span.${this.formatter.options.toolbar.buttonClassName}`); if (e) { const i = e.dataset.action || "", o = this.formatter.currentSpec?.getTargetBlot(); if (i && o) { const s = this.aligner.alignments[i]; this._clearButtons(), this.aligner.isAligned(o, s) ? (this.aligner.clear(o), this.debug && console.debug("AlignAction clear alignment:", i, o)) : (this.aligner.setAlignment(o, i), this.alignButtons[i].selected = !0, this.debug && console.debug("AlignAction set alignment:", i, o)); } } this.formatter.update(); }; /** * Initializes the alignment action by creating alignment buttons and storing them in the toolbar. * If debug mode is enabled in the formatter options, logs the created alignment buttons to the console. * * @returns {void} */ onCreate = () => { this._createAlignmentButtons(), this.toolbarButtons = Object.values(this.alignButtons), this.formatter.options.debug && console.debug("AlignAction alignment buttons created:", this.toolbarButtons); }; /** * Cleans up resources used by the alignment action. * * This method resets the `alignButtons` object and clears the `toolbarButtons` array. * If debug mode is enabled in the formatter options, a debug message is logged to the console. * * @returns {void} */ onDestroy = () => { this.alignButtons = {}, this.toolbarButtons = [], this.formatter.options.debug && console.debug("AlignAction alignment buttons destroyed"); }; } class U extends x { /** * Initializes event listeners for the delete action. * * - Adds a 'keyup' event listener to the document that triggers `_onKeyUp`. * - Adds an 'input' event listener to the Quill editor's root element that also triggers `_onKeyUp`. * * This method should be called when the delete action is created to ensure * proper handling of keyboard and input events. */ onCreate = () => { document.addEventListener("keyup", this._onKeyUp), this.formatter.quill.root.addEventListener("input", this._onKeyUp); }; /** * Cleans up event listeners associated with the action. * * Removes the 'keyup' event listener from the document and the 'input' event listener * from the Quill editor's root element to prevent memory leaks and unintended behavior * after the action is destroyed. */ onDestroy = () => { document.removeEventListener("keyup", this._onKeyUp), this.formatter.quill.root.removeEventListener("input", this._onKeyUp); }; /** * Handles the keyup event for delete and backspace actions. * * If no modal is open and a current spec is selected, checks if the pressed key is * 'Delete' or 'Backspace'. If so, finds the target blot element in the Quill editor, * determines its index, and deletes one character at that index. Afterwards, hides the formatter UI. * * @param e - The keyboard event triggered by the user. */ _onKeyUp = (t) => { const e = !!document.querySelector("[data-blot-formatter-modal]"); if (!(!this.formatter.currentSpec || e) && (t.code === "Delete" || t.code === "Backspace")) { this.debug && console.debug("DeleteAction keyup detected:", t.code); const i = this.formatter.currentSpec.getTargetElement(); if (i) { const o = this.formatter.Quill.find(i); if (o) { const s = this.formatter.quill.getIndex(o); this.formatter.quill.deleteText(s, 1, "user"); } } this.formatter.hide(); } }; } class V extends x { _topLeftHandle; _topRightHandle; _bottomRightHandle; _bottomLeftHandle; _dragHandle = null; _dragStartX = 0; _dragCursorStyle; _preDragWidth = 0; _pinchStartDistance = 0; _calculatedAspectRatio = 0; _computedAspectRatio = void 0; _target; _editorStyle; _editorWidth = 0; _useRelativeSize; _resizeModeButton = null; _isUnclickable = !1; _hasResized = !1; _formattedWidth = ""; _sizeInfoTimerId = null; _isImage = !1; _isSVG = !1; _naturalWidth = void 0; constructor(t) { super(t), this._topLeftHandle = this._createHandle("top-left", "nwse-resize"), this._topRightHandle = this._createHandle("top-right", "nesw-resize"), this._bottomRightHandle = this._createHandle("bottom-right", "nwse-resize"), this._bottomLeftHandle = this._createHandle("bottom-left", "nesw-resize"), this._dragCursorStyle = document.createElement("style"), this._useRelativeSize = this.formatter.options.resize.useRelativeSize, t.options.resize.allowResizeModeChange && (this._resizeModeButton = this._createResizeModeButton(), this.toolbarButtons = [ this._resizeModeButton ]); } /** * Initializes the resize action by setting up the target element, determining its type, * and appending resize handles to the overlay. Also attaches mouse and touch event listeners * to the overlay for handling user interactions. Finally, positions the handles according to * the specified style options. * * @remarks * This method should be called when the resize action is created to ensure all necessary * DOM elements and event listeners are properly initialized. */ onCreate = () => { this._target = this.formatter.currentSpec?.getTargetElement(), this._isUnclickable = this.formatter.currentSpec?.isUnclickable || !1, this._isImage = this._target instanceof HTMLImageElement, this._isImage && (this._isSVG = this._isSvgImage()), this.formatter.overlay.append( this._topLeftHandle, this._topRightHandle, this._bottomRightHandle, this._bottomLeftHandle ), this.formatter.overlay.addEventListener("mousedown", this._onOverlayMouseDown), this.formatter.overlay.addEventListener("mouseup", this._onOverlayMouseUp); const t = { passive: !1 }; this.formatter.overlay.addEventListener("touchstart", this._onOverlayTouchStart, t), this.formatter.overlay.addEventListener("touchmove", this._onOverlayTouchMove, t), this.formatter.overlay.addEventListener("touchend", this._onOverlayTouchEnd, t); const e = this.formatter.options.resize.handleStyle || {}; this._repositionHandles(e), this.debug && console.debug("ResizeAction created with target:", this._target, "isUnclickable:", this._isUnclickable); }; /** * Cleans up resources and event listeners associated with the resize action. * * This method resets internal state, removes resize handles from the overlay, * detaches mouse and touch event listeners, and triggers an update on the formatter. * * Should be called when the resize action is no longer needed to prevent memory leaks * and unintended behavior. */ onDestroy = () => { this._target = null, this._isUnclickable = !1, this._isImage = !1, this._naturalWidth = void 0, this._isSVG = !1, this._setCursor(""), [ this._topLeftHandle, this._topRightHandle, this._bottomRightHandle, this._bottomLeftHandle ].forEach((e) => { e.remove(); }), this.formatter.overlay.removeEventListener("mousedown", this._onOverlayMouseDown), this.formatter.overlay.removeEventListener("mouseup", this._onOverlayMouseUp); const t = { passive: !1 }; this.formatter.overlay.removeEventListener("touchstart", this._onOverlayTouchStart, t), this.formatter.overlay.removeEventListener("touchmove", this._onOverlayTouchMove, t), this.formatter.overlay.removeEventListener("touchend", this._onOverlayTouchEnd, t), this.formatter.update(); }; /** * Creates a resize handle element for the specified position with the given cursor style. * * The handle is styled using the class name and optional style provided in the formatter's options. * It also sets a `data-position` attribute and attaches a pointer down event listener. * * @param position - The position identifier for the handle (e.g., 'top-left', 'bottom-right'). * @param cursor - The CSS cursor style to apply when hovering over the handle. * @returns The created HTMLElement representing the resize handle. */ _createHandle = (t, e) => { const i = document.createElement("div"); return i.classList.add(this.formatter.options.resize.handleClassName), i.setAttribute("data-position", t), i.style.cursor = e, this.formatter.options.resize.handleStyle && Object.assign(i.style, this.formatter.options.resize.handleStyle), i.addEventListener("pointerdown", this._onHandlePointerDown), i; }; /** * Repositions the resize handles around an element based on the provided handle style. * * @param handleStyle - Optional style object containing width and height properties for the handles. * If provided, the handles are offset by half their width and height to center them. * If not provided, default offsets of '0px' are used. * * The method updates the `left`, `right`, `top`, and `bottom` CSS properties of the four handles * (`_topLeftHandle`, `_topRightHandle`, `_bottomRightHandle`, `_bottomLeftHandle`) to ensure they are * correctly positioned relative to the element being resized. */ _repositionHandles = (t) => { const e = t?.width ? `${-parseFloat(t.width) / 2}px` : "0px", i = t?.height ? `${-parseFloat(t.height) / 2}px` : "0px", { style: o } = this._topLeftHandle; o.left = e, o.top = i; const { style: s } = this._topRightHandle; s.right = e, s.top = i; const { style: n } = this._bottomRightHandle; n.right = e, n.bottom = i; const { style: r } = this._bottomLeftHandle; r.left = e, r.bottom = i; }; /** * Sets the cursor style for the document body and all its children. * When a non-empty value is provided, it applies the specified cursor style * globally by injecting a style element into the document head. * When an empty value is provided, it removes the previously injected style element, * reverting the cursor to its default behavior. * * @param value - The CSS cursor value to apply (e.g., 'pointer', 'col-resize'). */ _setCursor = (t) => { if (!document.body) { console.warn("ResizeAction: Cannot set cursor - document.body is null"); return; } try { t ? (this._dragCursorStyle.innerHTML = `body, body * { cursor: ${t} !important; }`, document.head.contains(this._dragCursorStyle) || document.head.appendChild(this._dragCursorStyle)) : document.head.contains(this._dragCursorStyle) && document.head.removeChild(this._dragCursorStyle); } catch (e) { console.error("ResizeAction: Error setting cursor style:", e); } }; /** * Activates or deactivates the resize mode for the target element. * * When activated, prepares the target for resizing by determining the resize mode (absolute or relative), * calculating editor and target dimensions, handling aspect ratio logic, and displaying size information. * When deactivated, applies the finalized width to the _target, updates toolbar button states, sets style attributes, * clears cached natural width, updates the formatter, and hides the size info box. * * @param activate - If `true`, activates resize mode; if `false`, finalizes and deactivates resize mode. */ _resizeMode = (t) => { if (t) { if (this._hasResized = !1, this._formattedWidth = "", this._target) { this._useRelativeSize = this.formatter._useRelative(this._target), this._editorStyle = getComputedStyle(this.formatter.quill.root), this._editorWidth = this.formatter.quill.root.clientWidth - parseFloat(this._editorStyle.paddingLeft) - parseFloat(this._editorStyle.paddingRight); const e = this._target.getBoundingClientRect(); if ((e.height === void 0 || e.height === 0) && (e.height = this._target.clientHeight + 1), this._preDragWidth = e.width, this._computedAspectRatio = getComputedStyle(this._target).aspectRatio || "auto", this._calculatedAspectRatio = e.width / e.height, this._useRelativeSize) this._isUnclickable && this._computedAspectRatio === "auto" && (this._target.style.aspectRatio = this.formatter.options.video.defaultAspectRatio, console.warn