@enzedonline/quill-blot-formatter2
Version:
An update for quill-blot-formatter to make quilljs v2 compatible.
1,038 lines • 156 kB
JavaScript
class v {
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 _ extends v {
/**
* 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 s = i.getRangeAt(0), o = s.startContainer, n = s.startOffset;
if (n > 0)
s.setStart(o, n - t);
else if (o.previousSibling) {
const r = o.previousSibling;
r.nodeType === Node.TEXT_NODE && s.setStart(r, r.textContent?.length || 0);
}
s.collapse(!0), i.removeAllRanges(), i.addRange(s), 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 s = t.getIndex(e);
t.setSelection(s, 0, "user"), i && console.debug("Caret placed before blot at index:", s, 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 s = t.getIndex(e), o = t.getLength();
s + 1 >= o - 1 ? (t.setSelection(o - 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:", s, e), t.setSelection(s + 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" ? (_.placeCaretBeforeBlot(this.formatter.quill, i, this.debug), this.formatter.hide()) : t.code === "ArrowRight" && (_.placeCaretAfterBlot(this.formatter.quill, i, this.debug), this.formatter.hide()));
};
}
function O(h) {
return h && h.__esModule && Object.prototype.hasOwnProperty.call(h, "default") ? h.default : h;
}
var C, S;
function H() {
if (S) return C;
S = 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]" || o(a);
}
var i = typeof Symbol == "function" && Symbol.for, s = i ? Symbol.for("react.element") : 60103;
function o(a) {
return a.$$typeof === s;
}
function n(a) {
return Array.isArray(a) ? [] : {};
}
function r(a, c) {
return c.clone !== !1 && c.isMergeableObject(a) ? x(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 x;
var d = c.customMerge(a);
return typeof d == "function" ? d : x;
}
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 B(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 x(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), R = y === f;
return R ? y ? d.arrayMerge(a, c, d) : B(a, c, d) : r(c, d);
}
x.all = function(c, d) {
if (!Array.isArray(c))
throw new Error("first argument should be an array");
return c.reduce(function(y, f) {
return x(y, f, d);
}, {});
};
var T = x;
return C = T, C;
}
var N = H();
const q = /* @__PURE__ */ O(N);
class I {
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 k {
/**
* 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 ? (k.watchTooltip(t, e), e && console.debug("Tooltip watcher initialized for:", i)) : console.warn("No tooltip found to watch for adjustments.");
}
/**
* 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 s = t.getBoundingClientRect(), o = e.getBoundingClientRect();
let n = s.left - o.left, r = s.top - o.top;
const l = s.width, p = s.height, m = e.clientWidth, u = e.clientHeight;
let g = !1;
const b = {};
r < 0 && (b.top = `${s.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"), s = t.container;
if (!i) {
console.warn("No tooltip found to watch for adjustments.");
return;
}
this.removeTooltipWatcher(i, e);
let o = !1;
const n = new MutationObserver((r) => {
if (!o) {
if (e)
for (const l of r)
console.debug("Tooltip mutation:", l.attributeName, i.getAttribute(l.attributeName));
o = !0, this._repositionTooltip(i, s, e), setTimeout(() => {
o = !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") && (k.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(s) {
return e.reduce(
(o, n) => (s.hasAttribute(n) && (o[n] = s.getAttribute(n)), o),
{}
);
}
format(s, o) {
e.indexOf(s) > -1 ? o || s === "alt" ? this.domNode.setAttribute(s, o) : this.domNode.removeAttribute(s) : super.format(s, o);
}
};
}, D = (h) => {
const t = h.import("parchment"), { ClassAttributor: e, Scope: i } = t;
return class extends e {
constructor(o = !1) {
super("iframeAlign", "ql-iframe-align", {
scope: i.BLOCK,
whitelist: ["left", "center", "right"]
}), this.debug = o;
}
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(o, n) {
if (this.debug && console.debug("IframeAlignAttributor.add", o, n), o instanceof HTMLElement) {
typeof n == "object" ? (super.add(o, n.align), o.dataset.blotAlign = n.align) : (super.add(o, n), o.dataset.blotAlign = n);
let r = o.getAttribute("width");
return r ? (isNaN(Number(r.trim().slice(-1))) || (r = `${r}px`), o.style.setProperty("--resize-width", r), o.dataset.relativeSize = `${r.endsWith("%")}`) : (o.style.removeProperty("--resize-width"), o.dataset.relativeSize = "false"), this.debug && console.debug("IframeAlignAttributor.add - node:", o, "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(o) {
this.debug && console.debug("IframeAlignAttributor.remove", o), o instanceof HTMLElement && (super.remove(o), delete o.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(o) {
const n = super.value(o), r = o instanceof HTMLElement && (o.style.getPropertyValue("--resize-width") || o.getAttribute("width")) || "", l = {
align: n,
width: r,
relativeSize: `${r.endsWith("%")}`
};
return this.debug && console.debug("IframeAlignAttributor.value", o, l), l;
}
};
}, j = (h) => {
const t = h.import("parchment"), { ClassAttributor: e, Scope: i } = t;
return class extends e {
constructor(o = !1) {
super("imageAlign", "ql-image-align", {
scope: i.INLINE,
whitelist: ["left", "center", "right"]
}), this.debug = o;
}
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(o, n) {
if (this.debug && console.debug("ImageAlignAttributor.add", o, n), o instanceof HTMLSpanElement && n) {
let r = o.querySelector("img");
if (typeof n == "object" && n.align)
super.add(o, n.align), o.setAttribute("contenteditable", "false"), n.title ? o.setAttribute("data-title", n.title) : o.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(o, 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 o.setAttribute("data-relative-size", `${l?.endsWith("%")}`), !0;
} else {
const r = o instanceof HTMLImageElement ? o : o.querySelector("img");
if (this.debug && console.debug(`ImageAlignAttributor.add - ${o.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 && (o.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(o) {
this.debug && console.debug("ImageAlignAttributor.remove", o), o instanceof HTMLElement && (super.remove(o), o.firstChild && o.firstChild instanceof HTMLElement && delete o.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(o) {
const n = o.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", o, 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(o) {
let n = o.getAttribute("width");
return n ? isNaN(Number(n.trim().slice(-1))) || (n = `${n}px`, o.setAttribute("width", n)) : (n = `${o.naturalWidth}px`, o.setAttribute("width", n)), o.parentElement.style.setProperty("--resize-width", n), n;
}
};
}, P = (h) => {
const t = h.import("formats/video");
return class extends t {
static blotName = "video";
static aspectRatio = "16 / 9 auto";
static create(i) {
const s = super.create(i);
return s.setAttribute("width", "100%"), s.style.aspectRatio = this.aspectRatio, s;
}
html() {
return this.domNode.outerHTML;
}
};
};
class $ {
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: (s) => {
this.setAlignment(s, 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 s = getComputedStyle(this.formatter.quill.root), o = this.formatter.quill.root.clientWidth - parseFloat(s.paddingLeft) - parseFloat(s.paddingRight);
t.domNode.setAttribute(
"width",
`${Math.min(Math.round(100 * t.domNode.naturalWidth / o), 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 s = this.formatter.quill.getContents().ops;
s.length === 2 && s[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 A {
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 v {
aligner;
alignButtons = {};
constructor(t) {
super(t), this.aligner = new $(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 A(
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 || "", s = this.formatter.currentSpec?.getTargetBlot();
if (i && s) {
const o = this.aligner.alignments[i];
this._clearButtons(), this.aligner.isAligned(s, o) ? (this.aligner.clear(s), this.debug && console.debug("AlignAction clear alignment:", i, s)) : (this.aligner.setAlignment(s, i), this.alignButtons[i].selected = !0, this.debug && console.debug("AlignAction set alignment:", i, s));
}
}
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 v {
/**
* 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 s = this.formatter.Quill.find(i);
if (s) {
const o = this.formatter.quill.getIndex(s);
this.formatter.quill.deleteText(o, 1, "user");
}
}
this.formatter.hide();
}
};
}
class V extends v {
_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: s } = this._topLeftHandle;
s.left = e, s.top = i;
const { style: o } = this._topRightHandle;
o.right = e, o.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(
`No iframe aspect-ratio set. Set an aspect