@enzedonline/quill-blot-formatter2
Version:
An update for quill-blot-formatter to make quilljs v2 compatible.
1 lines • 318 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/actions/Action.ts","../src/actions/CaretAction.ts","../node_modules/deepmerge/dist/cjs.js","../src/actions/toolbar/Toolbar.ts","../src/tooltip/TooltipContainPosition.ts","../src/blots/Image.ts","../src/actions/align/AlignFormats.ts","../src/blots/Video.ts","../src/actions/align/DefaultAligner.ts","../src/actions/toolbar/ToolbarButton.ts","../src/actions/align/AlignAction.ts","../src/actions/DeleteAction.ts","../src/actions/ResizeAction.ts","../src/specs/BlotSpec.ts","../src/specs/UnclickableBlotSpec.ts","../src/specs/IframeVideoSpec.ts","../src/actions/AttributeAction.ts","../src/actions/CompressAction.ts","../src/actions/LinkAction.ts","../src/specs/ImageSpec.ts","../src/DefaultOptions.ts","../src/BlotFormatter.ts"],"sourcesContent":["import BlotFormatter from '../BlotFormatter.js';\nimport ToolbarButton from './toolbar/ToolbarButton.js';\n\n/**\n * Represents a base class for actions used within the BlotFormatter.\n * \n * This class provides a structure for actions that can be performed by the formatter,\n * including lifecycle hooks for creation, destruction, and updates. Subclasses should\n * override the lifecycle methods as needed.\n * \n * @remarks\n * - Each action holds a reference to the parent `BlotFormatter` instance.\n * - Actions can define their own toolbar buttons by populating the `toolbarButtons` array.\n * - Debug logging is available if the formatter's options enable it.\n * \n * @example\n * ```typescript\n * class CustomAction extends Action {\n * onCreate = (): void => {\n * // Custom initialization logic\n * }\n * onDestroy = (): void => {\n * // Custom destruction logic\n * }\n * onUpdate = (): void => {\n * // Custom update logic\n * }\n * }\n * ```\n * \n * @public\n */\nexport default class Action {\n formatter: BlotFormatter;\n toolbarButtons: ToolbarButton[] = [];\n debug: boolean;\n\n constructor(formatter: BlotFormatter) {\n this.formatter = formatter;\n this.debug = this.formatter.options.debug || false;\n if (this.debug) console.debug('Action created:', this.constructor.name);\n }\n\n /**\n * Called when the action is created.\n * Override this method to implement custom initialization logic.\n */\n onCreate = (): void => {}\n\n /**\n * Called when the action is being destroyed.\n * Override this method to implement custom cleanup logic.\n */\n onDestroy = (): void => {}\n\n /**\n * Called when the action should be updated.\n * Override this method to implement custom update logic.\n */\n onUpdate = (): void => {}\n\n}\n","import Action from './Action.js';\r\nimport { Blot } from '../specs/BlotSpec.js';\r\n\r\n/**\r\n * Provides caret (text cursor) manipulation actions for the Quill editor, including moving the caret\r\n * backward, placing the caret before or after a specified blot, and handling keyboard navigation events.\r\n *\r\n * This class is designed to work with the Quill editor and the Blot Formatter overlay, enabling precise\r\n * caret placement and navigation around custom blots (such as images or embeds) within the editor.\r\n *\r\n * @remarks\r\n * - Integrates with the Quill editor instance and its formatting specifications.\r\n * - Handles keyboard events to facilitate intuitive caret movement for users.\r\n * - Ensures proper event listener management to prevent memory leaks.\r\n *\r\n * @public\r\n */\r\nexport default class CaretAction extends Action {\r\n\r\n /**\r\n * Moves the caret (text cursor) backward by a specified number of characters within the current selection.\r\n *\r\n * If the caret is at the beginning of a text node, it attempts to move to the end of the previous sibling text node.\r\n * If there is no previous sibling or the selection is not valid, the caret position remains unchanged.\r\n *\r\n * @param n - The number of characters to move the caret back. Defaults to 1.\r\n */\r\n static sendCaretBack = (n = 1, debug = false): void => {\r\n const selection = window.getSelection();\r\n if (selection && selection.rangeCount > 0) {\r\n const range = selection.getRangeAt(0);\r\n const currentNode = range.startContainer;\r\n const currentOffset = range.startOffset;\r\n\r\n // Move the cursor back by n characters\r\n if (currentOffset > 0) {\r\n range.setStart(currentNode, currentOffset - n);\r\n } else if (currentNode.previousSibling) {\r\n // Move to end of previous text node\r\n const prevNode = currentNode.previousSibling;\r\n if (prevNode.nodeType === Node.TEXT_NODE) {\r\n range.setStart(prevNode, prevNode.textContent?.length || 0);\r\n }\r\n }\r\n\r\n range.collapse(true);\r\n selection.removeAllRanges();\r\n selection.addRange(range);\r\n if (debug) {\r\n console.debug('Caret moved back by', n, 'characters');\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Places the caret (text cursor) immediately before the specified blot in the Quill editor.\r\n *\r\n * @param quill - The Quill editor instance.\r\n * @param targetBlot - The blot before which the caret should be placed.\r\n */\r\n static placeCaretBeforeBlot = (quill: any, targetBlot: Blot, debug = false): void => {\r\n const index = quill.getIndex(targetBlot);\r\n quill.setSelection(index, 0, \"user\");\r\n if (debug) {\r\n console.debug('Caret placed before blot at index:', index, targetBlot);\r\n }\r\n }\r\n\r\n /**\r\n * Places the caret (text cursor) immediately after the specified blot in the Quill editor.\r\n *\r\n * This method first clears any existing selection and ensures the editor is focused.\r\n * It then calculates the index of the target blot and determines whether it is the last blot in the document.\r\n * - If the target blot is the last one, the caret is placed at the very end of the document.\r\n * - Otherwise, the caret is positioned just after the target blot, using a combination of Quill's selection API\r\n * and a native browser adjustment to avoid placing the caret inside a formatting span wrapper.\r\n *\r\n * @param quill - The Quill editor instance.\r\n * @param targetBlot - The blot after which the caret should be placed.\r\n */\r\n static placeCaretAfterBlot = (quill: any, targetBlot: Blot, debug = false): void => {\r\n quill.setSelection(null); // Clear selection first\r\n quill.root.focus(); // Ensure the editor is focused\r\n const index = quill.getIndex(targetBlot);\r\n const documentLength = quill.getLength();\r\n\r\n // Check if this is the last blot in the document\r\n if (index + 1 >= documentLength - 1) {\r\n // For the last blot, place cursor at the very end\r\n quill.setSelection(documentLength - 1, 0, \"user\");\r\n if (debug) {\r\n console.debug('Caret placed at the end of the document after blot:', targetBlot);\r\n }\r\n } else {\r\n // overshoot by one then use native browser API to send caret back one\r\n // without this, caret will be placed inside formatting span wrapper\r\n if (debug) {\r\n console.debug('Caret placed after character following blot at index:', index, targetBlot);\r\n } \r\n quill.setSelection(index + 2, 0, \"user\");\r\n this.sendCaretBack(1, debug); // Move cursor back by 1 character\r\n }\r\n }\r\n\r\n /**\r\n * Initializes event listeners for the CaretAction.\r\n *\r\n * Adds a 'keyup' event listener to the document and an 'input' event listener\r\n * to the Quill editor's root element. Both listeners trigger the `onKeyUp` handler.\r\n *\r\n * @remarks\r\n * This method should be called when the action is created to ensure proper\r\n * caret handling and formatting updates in response to user input.\r\n */\r\n onCreate = (): void => {\r\n document.addEventListener('keyup', this.onKeyUp);\r\n this.formatter.quill.root.addEventListener('input', this.onKeyUp);\r\n }\r\n\r\n /**\r\n * Cleans up event listeners attached by this action.\r\n *\r\n * Removes the 'keyup' event listener from the document and the 'input' event listener\r\n * from the Quill editor's root element to prevent memory leaks and unintended behavior\r\n * after the action is destroyed.\r\n */\r\n onDestroy = (): void => {\r\n document.removeEventListener('keyup', this.onKeyUp);\r\n this.formatter.quill.root.removeEventListener('input', this.onKeyUp);\r\n }\r\n\r\n /**\r\n * Handles the keyup event for caret navigation around a target blot in the editor.\r\n *\r\n * - If a modal is open or there is no current formatting specification, the handler exits early.\r\n * - If the left arrow key is pressed, places the caret before the target blot and hides the formatter UI.\r\n * - If the right arrow key is pressed, places the caret after the target blot and hides the formatter UI.\r\n *\r\n * @param e - The keyboard event triggered by the user's keyup action.\r\n */\r\n onKeyUp = (e: KeyboardEvent) => {\r\n const modalOpen: boolean = !!document.querySelector('[data-blot-formatter-modal]')\r\n if (!this.formatter.currentSpec || modalOpen) {\r\n return;\r\n }\r\n const targetBlot = this.formatter.currentSpec.getTargetBlot();\r\n if (!targetBlot) {\r\n return;\r\n }\r\n\r\n // if left arrow, place cursor before targetBlot\r\n // if right arrow, place cursor after targetBlot\r\n if (e.code === 'ArrowLeft') {\r\n CaretAction.placeCaretBeforeBlot(this.formatter.quill, targetBlot, this.debug);\r\n this.formatter.hide();\r\n } else if (e.code === 'ArrowRight') {\r\n CaretAction.placeCaretAfterBlot(this.formatter.quill, targetBlot, this.debug);\r\n this.formatter.hide();\r\n }\r\n };\r\n}\r\n","'use strict';\n\nvar isMergeableObject = function isMergeableObject(value) {\n\treturn isNonNullObject(value)\n\t\t&& !isSpecial(value)\n};\n\nfunction isNonNullObject(value) {\n\treturn !!value && typeof value === 'object'\n}\n\nfunction isSpecial(value) {\n\tvar stringValue = Object.prototype.toString.call(value);\n\n\treturn stringValue === '[object RegExp]'\n\t\t|| stringValue === '[object Date]'\n\t\t|| isReactElement(value)\n}\n\n// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25\nvar canUseSymbol = typeof Symbol === 'function' && Symbol.for;\nvar REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;\n\nfunction isReactElement(value) {\n\treturn value.$$typeof === REACT_ELEMENT_TYPE\n}\n\nfunction emptyTarget(val) {\n\treturn Array.isArray(val) ? [] : {}\n}\n\nfunction cloneUnlessOtherwiseSpecified(value, options) {\n\treturn (options.clone !== false && options.isMergeableObject(value))\n\t\t? deepmerge(emptyTarget(value), value, options)\n\t\t: value\n}\n\nfunction defaultArrayMerge(target, source, options) {\n\treturn target.concat(source).map(function(element) {\n\t\treturn cloneUnlessOtherwiseSpecified(element, options)\n\t})\n}\n\nfunction getMergeFunction(key, options) {\n\tif (!options.customMerge) {\n\t\treturn deepmerge\n\t}\n\tvar customMerge = options.customMerge(key);\n\treturn typeof customMerge === 'function' ? customMerge : deepmerge\n}\n\nfunction getEnumerableOwnPropertySymbols(target) {\n\treturn Object.getOwnPropertySymbols\n\t\t? Object.getOwnPropertySymbols(target).filter(function(symbol) {\n\t\t\treturn Object.propertyIsEnumerable.call(target, symbol)\n\t\t})\n\t\t: []\n}\n\nfunction getKeys(target) {\n\treturn Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))\n}\n\nfunction propertyIsOnObject(object, property) {\n\ttry {\n\t\treturn property in object\n\t} catch(_) {\n\t\treturn false\n\t}\n}\n\n// Protects from prototype poisoning and unexpected merging up the prototype chain.\nfunction propertyIsUnsafe(target, key) {\n\treturn propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,\n\t\t&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,\n\t\t\t&& Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.\n}\n\nfunction mergeObject(target, source, options) {\n\tvar destination = {};\n\tif (options.isMergeableObject(target)) {\n\t\tgetKeys(target).forEach(function(key) {\n\t\t\tdestination[key] = cloneUnlessOtherwiseSpecified(target[key], options);\n\t\t});\n\t}\n\tgetKeys(source).forEach(function(key) {\n\t\tif (propertyIsUnsafe(target, key)) {\n\t\t\treturn\n\t\t}\n\n\t\tif (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {\n\t\t\tdestination[key] = getMergeFunction(key, options)(target[key], source[key], options);\n\t\t} else {\n\t\t\tdestination[key] = cloneUnlessOtherwiseSpecified(source[key], options);\n\t\t}\n\t});\n\treturn destination\n}\n\nfunction deepmerge(target, source, options) {\n\toptions = options || {};\n\toptions.arrayMerge = options.arrayMerge || defaultArrayMerge;\n\toptions.isMergeableObject = options.isMergeableObject || isMergeableObject;\n\t// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()\n\t// implementations can use it. The caller may not replace it.\n\toptions.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;\n\n\tvar sourceIsArray = Array.isArray(source);\n\tvar targetIsArray = Array.isArray(target);\n\tvar sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;\n\n\tif (!sourceAndTargetTypesMatch) {\n\t\treturn cloneUnlessOtherwiseSpecified(source, options)\n\t} else if (sourceIsArray) {\n\t\treturn options.arrayMerge(target, source, options)\n\t} else {\n\t\treturn mergeObject(target, source, options)\n\t}\n}\n\ndeepmerge.all = function deepmergeAll(array, options) {\n\tif (!Array.isArray(array)) {\n\t\tthrow new Error('first argument should be an array')\n\t}\n\n\treturn array.reduce(function(prev, next) {\n\t\treturn deepmerge(prev, next, options)\n\t}, {})\n};\n\nvar deepmerge_1 = deepmerge;\n\nmodule.exports = deepmerge_1;\n","import BlotFormatter from '../../BlotFormatter.js';\r\nimport ToolbarButton from './ToolbarButton.js';\r\n\r\n/**\r\n * Manages the creation, display, and destruction of a toolbar for BlotFormatter actions.\r\n *\r\n * The `Toolbar` class is responsible for rendering toolbar buttons associated with registered actions,\r\n * handling their lifecycle, and appending/removing the toolbar element from the formatter's overlay.\r\n *\r\n * @remarks\r\n * - The toolbar is initialized and shown via the `create()` method, which collects all action buttons and appends them to the DOM.\r\n * - The `destroy()` method cleans up the toolbar, removes it from the DOM, and destroys all associated buttons to prevent memory leaks.\r\n *\r\n * @example\r\n * ```typescript\r\n * const toolbar = new Toolbar(formatter);\r\n * toolbar.create(); // Show toolbar\r\n * // ... later\r\n * toolbar.destroy(); // Hide and clean up toolbar\r\n * ```\r\n *\r\n * @public\r\n */\r\nexport default class Toolbar {\r\n formatter: BlotFormatter;\r\n element: HTMLElement;\r\n buttons: Record<string, ToolbarButton> = {};\r\n\r\n constructor(formatter: BlotFormatter) {\r\n this.formatter = formatter;\r\n this.element = document.createElement('div');\r\n this.element.classList.add(this.formatter.options.toolbar.mainClassName);\r\n this.element.addEventListener('mousedown', (event: MouseEvent) => {\r\n event.stopPropagation();\r\n });\r\n if (this.formatter.options.toolbar.mainStyle) {\r\n Object.assign(this.element.style, this.formatter.options.toolbar.mainStyle);\r\n }\r\n }\r\n\r\n /**\r\n * Creates and appends toolbar action buttons to the toolbar element. \r\n * Called by BlotFormatter.show() to initialize the toolbar.\r\n * \r\n * Iterates through all actions registered in the formatter, collects their toolbar buttons,\r\n * stores each button in the `buttons` map by its action name, and appends the created button elements\r\n * to the toolbar's DOM element. Finally, appends the toolbar element to the formatter's overlay.\r\n */\r\n create = (): void => {\r\n const actionButtons: HTMLElement[] = [];\r\n this.formatter.actions.forEach(action => {\r\n action.toolbarButtons.forEach(button => {\r\n this.buttons[button.action] = button;\r\n actionButtons.push(button.create());\r\n });\r\n });\r\n this.element.append(...actionButtons);\r\n this.formatter.overlay.append(this.element);\r\n if (this.formatter.options.debug) {\r\n console.debug('Toolbar created with buttons:', Object.keys(this.buttons), actionButtons);\r\n }\r\n }\r\n\r\n /**\r\n * Cleans up the toolbar by removing its element from the overlay,\r\n * destroying all associated buttons, and clearing internal references.\r\n * Called by BlotFormatter.hide() to remove the toolbar from the DOM.\r\n * \r\n * This should be called when the toolbar is no longer needed to prevent memory leaks.\r\n */\r\n destroy = (): void => {\r\n if (this.element) {\r\n this.formatter.overlay.removeChild(this.element);\r\n }\r\n for (const button of Object.values(this.buttons)) {\r\n button.destroy();\r\n }\r\n this.buttons = {};\r\n this.element.innerHTML = '';\r\n if (this.formatter.options.debug) {\r\n console.debug('Toolbar destroyed');\r\n }\r\n }\r\n}","import type Quill from 'quill';\r\n\r\nexport default class TooltipContainPosition {\r\n /**\r\n * Repositions a tooltip element within a given container to ensure it does not overflow\r\n * the container's boundaries. Adjusts the tooltip's `top` and `left` CSS properties if\r\n * necessary to keep it fully visible. Optionally logs debug information about the repositioning.\r\n *\r\n * @param tooltip - The tooltip HTMLDivElement to reposition.\r\n * @param container - The container HTMLElement within which the tooltip should remain visible.\r\n * @param debug - If true, logs debug information to the console. Defaults to false.\r\n */\r\n private static _repositionTooltip = (tooltip: HTMLDivElement, container: HTMLElement, debug = false) => {\r\n const tooltipRect = tooltip.getBoundingClientRect();\r\n const containerRect = container.getBoundingClientRect();\r\n\r\n // Calculate position relative to the container\r\n let left = tooltipRect.left - containerRect.left;\r\n let top = tooltipRect.top - containerRect.top;\r\n const width = tooltipRect.width;\r\n const height = tooltipRect.height;\r\n const maxWidth = container.clientWidth;\r\n const maxHeight = container.clientHeight;\r\n\r\n let changed = false;\r\n const newStyles: { top?: string; left?: string } = {};\r\n\r\n // 1) Top overflow \r\n if (top < 0) {\r\n newStyles.top = `${tooltipRect.height}px`;\r\n changed = true;\r\n }\r\n\r\n // 2) Bottom overflow\r\n if (top + height > maxHeight) {\r\n newStyles.top = `${maxHeight - height}px`;\r\n changed = true;\r\n }\r\n\r\n // 3) Left overflow\r\n if (left < 0) {\r\n newStyles.left = '0px';\r\n changed = true;\r\n }\r\n\r\n // 4) Right overflow\r\n if (left + width > maxWidth) {\r\n newStyles.left = `${maxWidth - width}px`;\r\n changed = true;\r\n }\r\n\r\n if (changed) {\r\n if (debug) {\r\n console.debug('Repositioning tooltip', newStyles);\r\n }\r\n\r\n // Apply all style changes at once to minimize mutations\r\n if (newStyles.top !== undefined) {\r\n tooltip.style.top = newStyles.top;\r\n }\r\n if (newStyles.left !== undefined) {\r\n tooltip.style.left = newStyles.left;\r\n }\r\n\r\n if (tooltip.classList.contains('ql-flip')) {\r\n tooltip.classList.remove('ql-flip');\r\n }\r\n } else {\r\n if (debug) {\r\n console.debug('Tooltip position is fine, no changes needed');\r\n }\r\n }\r\n }\r\n \r\n // Static property to store observers\r\n private static observers = new WeakMap<HTMLDivElement, MutationObserver>();\r\n\r\n /**\r\n * Observes changes to the tooltip's attributes and triggers repositioning when necessary.\r\n *\r\n * @param quill - The Quill editor instance containing the tooltip.\r\n * @param debug - Optional flag to enable debug logging of attribute mutations.\r\n *\r\n * @remarks\r\n * Uses a MutationObserver to monitor changes to the tooltip's `style` and `class` attributes.\r\n * When a mutation is detected, the tooltip is repositioned within the container.\r\n * If `debug` is true, mutation details are logged to the console.\r\n */\r\n static watchTooltip(quill: Quill, debug = false): void {\r\n const tooltip = quill.container.querySelector('.ql-tooltip') as HTMLDivElement;\r\n const container = quill.container;\r\n if (!tooltip) {\r\n console.warn('No tooltip found to watch for adjustments.');\r\n return;\r\n }\r\n // Clean up any existing observer for this tooltip\r\n this.removeTooltipWatcher(tooltip, debug);\r\n\r\n let isRepositioning = false;\r\n\r\n const observer = new MutationObserver((mutations: MutationRecord[]) => {\r\n // Ignore mutations caused by our own repositioning\r\n if (isRepositioning) return;\r\n\r\n if (debug) {\r\n for (const m of mutations) {\r\n console.debug('Tooltip mutation:', m.attributeName, tooltip.getAttribute(m.attributeName!));\r\n }\r\n }\r\n\r\n isRepositioning = true;\r\n this._repositionTooltip(tooltip, container, debug);\r\n // Use setTimeout to reset the flag after the current execution context\r\n setTimeout(() => {\r\n isRepositioning = false;\r\n }, 0);\r\n });\r\n\r\n observer.observe(tooltip, {\r\n attributes: true,\r\n attributeFilter: ['style', 'class'],\r\n });\r\n\r\n // Store the observer for later cleanup\r\n this.observers.set(tooltip, observer);\r\n }\r\n\r\n /**\r\n * Removes the MutationObserver for the specified tooltip element.\r\n *\r\n * @param tooltip - The HTMLDivElement or Quill instance to stop watching.\r\n * If a Quill instance is provided, finds the tooltip within its container.\r\n */\r\n static removeTooltipWatcher(tooltip: HTMLDivElement | Quill, debug = false): void {\r\n let tooltipElement: HTMLDivElement | null = null;\r\n\r\n if (tooltip instanceof HTMLDivElement) {\r\n tooltipElement = tooltip;\r\n } else {\r\n // Assume it's a Quill instance\r\n tooltipElement = tooltip.container.querySelector('.ql-tooltip') as HTMLDivElement;\r\n }\r\n\r\n if (tooltipElement && this.observers.has(tooltipElement)) {\r\n const observer = this.observers.get(tooltipElement);\r\n observer?.disconnect();\r\n this.observers.delete(tooltipElement);\r\n if (debug) {\r\n console.debug('Tooltip watcher removed for:', tooltipElement);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Initializes the tooltip adjustment watcher when the action is created.\r\n * Searches for the tooltip element within the Quill container and, if found,\r\n * sets up observation for tooltip adjustments. Logs a warning if the tooltip\r\n * element is not present.\r\n *\r\n * @remarks\r\n * This method should be called during the creation lifecycle of the action.\r\n */\r\n constructor(private readonly quill: Quill, private readonly debug = false) {\r\n const tooltip = quill.container.querySelector('.ql-tooltip') as HTMLDivElement;\r\n console.debug('tooltip:', tooltip);\r\n if (tooltip) {\r\n TooltipContainPosition.watchTooltip(quill, debug);\r\n if (debug) {\r\n console.debug('Tooltip watcher initialized for:', tooltip);\r\n }\r\n } else {\r\n console.warn('No tooltip found to watch for adjustments.');\r\n }\r\n }\r\n\r\n /**\r\n * Cleans up resources when the action is destroyed.\r\n * Specifically, it finds the tooltip element within the Quill editor container\r\n * and removes its associated watcher if the tooltip exists.\r\n */\r\n destroy = (): void => {\r\n const tooltip = this.quill.container.querySelector('.ql-tooltip') as HTMLDivElement;\r\n if (tooltip) {\r\n TooltipContainPosition.removeTooltipWatcher(this.quill, this.debug);\r\n if (this.debug) {\r\n console.debug('Tooltip watcher removed on destroy');\r\n }\r\n }\r\n }\r\n\r\n}\r\n","\r\n/**\r\n * Factory function to create a custom Quill Image blot class supporting additional attributes.\r\n *\r\n * This function returns a class extending Quill's native Image blot, adding support for the `title` attribute\r\n * (in addition to `alt`, `height`, and `width`). The returned class overrides the static `formats` method\r\n * to extract these attributes from the DOM node, and the instance `format` method to set or remove them.\r\n *\r\n * @param QuillConstructor - The Quill constructor or instance used to import the base Image blot.\r\n * @returns A custom Image blot class supporting `alt`, `height`, `width`, and `title` attributes.\r\n *\r\n * @remarks\r\n * - This is useful for enabling the `title` attribute on images in Quill editors, which is not supported by default.\r\n * - See https://github.com/slab/quill/pull/4350 for related discussion.\r\n *\r\n * @example\r\n * ```typescript\r\n * const CustomImageBlot = createAltTitleImageBlotClass(Quill);\r\n * Quill.register(CustomImageBlot);\r\n * ```\r\n */\r\nexport const createAltTitleImageBlotClass = (QuillConstructor: any): any => {\r\n const ImageBlot = QuillConstructor.import('formats/image') as any;\r\n\r\n const ATTRIBUTES = ['alt', 'height', 'width', 'title'];\r\n\r\n /**\r\n * Represents a custom image blot for Quill editor, extending the base `ImageBlot`.\r\n * \r\n * This class provides custom formatting logic to allow managing the `title` attribute missing from the native Quill image blot.\r\n * See PR https://github.com/slab/quill/pull/4350 for more details.\r\n * \r\n * @remarks\r\n * - The static `formats` method extracts supported attributes from a DOM node and returns them as a record.\r\n * - The `format` method sets or removes supported attributes on the image DOM node, delegating to the superclass for unsupported attributes.\r\n * \r\n * @example\r\n * ```typescript\r\n * // Extract formats from an image DOM node\r\n * const formats = Image.formats(domNode);\r\n * \r\n * // Format an image blot\r\n * imageBlot.format('alt', 'A description');\r\n * ```\r\n */\r\n return class Image extends ImageBlot {\r\n static blotName = 'image';\r\n static formats(domNode: Element) {\r\n return ATTRIBUTES.reduce(\r\n (formats: Record<string, string | null>, attribute) => {\r\n if (domNode.hasAttribute(attribute)) {\r\n formats[attribute] = domNode.getAttribute(attribute);\r\n }\r\n return formats;\r\n },\r\n {},\r\n );\r\n }\r\n\r\n format(name: string, value: string) {\r\n if (ATTRIBUTES.indexOf(name) > -1) {\r\n if (value || name === 'alt') {\r\n this.domNode.setAttribute(name, value);\r\n } else {\r\n this.domNode.removeAttribute(name);\r\n }\r\n } else {\r\n super.format(name, value);\r\n }\r\n }\r\n }\r\n}\r\n","interface IframeAlignValue {\r\n align: string;\r\n width: string;\r\n relativeSize: string;\r\n}\r\n\r\ninterface ImageAlignInputValue {\r\n align: string;\r\n title: string;\r\n}\r\n\r\ninterface ImageAlignValue extends ImageAlignInputValue {\r\n width: string;\r\n contenteditable: string;\r\n relativeSize: string;\r\n}\r\n\r\n/**\r\n * Represents a class type for Attributors, which are used to define and manage\r\n * custom attributes in Quill editors. This type describes a constructor signature\r\n * for Attributor classes, including their prototype and the attribute name they handle.\r\n *\r\n * @template T The instance type created by the constructor.\r\n * @property {string} attrName - The name of the attribute managed by the Attributor.\r\n */\r\nexport type AttributorClass = {\r\n new (...args: any[]): any;\r\n prototype: any;\r\n attrName: string;\r\n}\r\n\r\n/**\r\n * Creates a custom Quill Attributor class for handling iframe alignment.\r\n *\r\n * This attributor allows alignment of iframe elements within the Quill editor by\r\n * applying a CSS class and managing related dataset properties. It also handles\r\n * width styling and tracks whether the width is relative (percentage-based).\r\n *\r\n * @param QuillConstructor - The Quill constructor or Quill instance used to import Parchment.\r\n * @returns A class extending Quill's ClassAttributor, customized for iframe alignment.\r\n *\r\n * @example\r\n * this.Quill = this.quill.constructor\r\n * const IframeAlignClass = createIframeAlignAttributor(this.Quill);\r\n * this.IframeAlign = new IframeAlignClass();\r\n * this.Quill.register({\r\n * 'formats/iframeAlign': this.IframeAlign,\r\n * 'attributors/class/iframeAlign': this.IframeAlign,\r\n * }, true);\r\n * \r\n * @remarks\r\n * - Supported alignments: 'left', 'center', 'right'.\r\n * - Adds/removes the `ql-iframe-align` class and manages `data-blot-align` and `--resize-width` style.\r\n * - Handles both string and object values for alignment.\r\n */\r\nexport const createIframeAlignAttributor = (QuillConstructor: any): AttributorClass => {\r\n const parchment = QuillConstructor.import('parchment') as any;\r\n const { ClassAttributor, Scope } = parchment;\r\n\r\n return class IframeAlignAttributor extends ClassAttributor {\r\n static attrName = 'iframeAlign';\r\n\r\n constructor(private debug = false) {\r\n super('iframeAlign', 'ql-iframe-align', {\r\n scope: Scope.BLOCK,\r\n whitelist: ['left', 'center', 'right'],\r\n });\r\n }\r\n\r\n /**\r\n * Adds alignment and width-related formatting to the specified HTML element node.\r\n *\r\n * - Sets the alignment using the provided value, which can be either a string or an object containing an `align` property.\r\n * - Stores the alignment value in the element's `data-blot-align` attribute.\r\n * - Handles the element's `width` attribute:\r\n * - If present, ensures the width value includes units (appends 'px' if numeric only).\r\n * - Sets the CSS custom property `--resize-width` to the processed width value.\r\n * - Sets the `data-relative-size` attribute to `'true'` if the width ends with '%', otherwise `'false'`.\r\n * - If no width is specified, removes the `--resize-width` property and sets `data-relative-size` to `'false'`.\r\n *\r\n * @param node - The DOM element to which alignment and width formatting will be applied.\r\n * @param value - The alignment value, either as a string or an object with an `align` property.\r\n * @returns `true` if the formatting was successfully applied to an HTMLElement, otherwise `false`.\r\n */\r\n add(node: Element, value: IframeAlignValue): boolean {\r\n if (this.debug) {\r\n console.debug('IframeAlignAttributor.add', node, value);\r\n }\r\n if (node instanceof HTMLElement) {\r\n if (typeof value === 'object') {\r\n super.add(node, value.align);\r\n node.dataset.blotAlign = value.align;\r\n } else {\r\n super.add(node, value);\r\n node.dataset.blotAlign = value;\r\n }\r\n let width: string | null = node.getAttribute('width');\r\n if (width) {\r\n // width style value must include units, add 'px' if numeric only\r\n if (!isNaN(Number(width.trim().slice(-1)))) {\r\n width = `${width}px`\r\n }\r\n node.style.setProperty('--resize-width', width);\r\n node.dataset.relativeSize = `${width.endsWith('%')}`;\r\n } else {\r\n node.style.removeProperty('--resize-width');\r\n node.dataset.relativeSize = 'false';\r\n }\r\n if (this.debug) {\r\n console.debug('IframeAlignAttributor.add - node:', node, 'aligned with:', value);\r\n }\r\n return true;\r\n } else {\r\n if (this.debug) {\r\n console.debug('IframeAlignAttributor.add - node is not an HTMLElement, skipping alignment');\r\n }\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Removes the alignment formatting from the specified DOM element.\r\n * \r\n * If the provided node is an instance of HTMLElement, this method first calls the\r\n * parent class's `remove` method to perform any additional removal logic, and then\r\n * deletes the `data-blot-align` attribute from the element's dataset.\r\n *\r\n * @param node - The DOM element from which to remove the alignment formatting.\r\n */\r\n remove(node: Element): void {\r\n if (this.debug) {\r\n console.debug('IframeAlignAttributor.remove', node);\r\n }\r\n if (node instanceof HTMLElement) {\r\n super.remove(node);\r\n delete node.dataset.blotAlign;\r\n }\r\n }\r\n\r\n /**\r\n * Extracts alignment and width information from a given DOM element.\r\n *\r\n * @param node - The DOM element from which to extract alignment and width values.\r\n * @returns An object containing:\r\n * - `align`: The alignment class name derived from the superclass's value method.\r\n * - `width`: The width value, determined from the element's CSS custom property '--resize-width', \r\n * its 'width' attribute, or an empty string if not present.\r\n * - `relativeSize`: A string indicating whether the width ends with a '%' character, representing a relative size.\r\n */\r\n value(node: Element): IframeAlignValue {\r\n const className = super.value(node);\r\n const width = (node instanceof HTMLElement) ?\r\n node.style.getPropertyValue('--resize-width') || node.getAttribute('width') || '' :\r\n '';\r\n const value = {\r\n align: className,\r\n width: width,\r\n relativeSize: `${width.endsWith('%')}`\r\n };\r\n if (this.debug) {\r\n console.debug('IframeAlignAttributor.value', node, value);\r\n }\r\n return value;\r\n }\r\n\r\n }\r\n}\r\n\r\n/**\r\n * Creates a custom Quill Attributor class for handling image alignment within a span wrapper.\r\n * \r\n * This attributor enables alignment formatting (`left`, `center`, `right`) for images by wrapping them in a `<span>`\r\n * element with a specific class and managing related attributes such as `data-title` for captions and width handling.\r\n * It also ensures that the image's alignment and caption are properly reflected in the DOM and Quill's internal model.\r\n * \r\n * @param QuillConstructor - The Quill constructor or instance used to import Parchment and related classes.\r\n * @returns A custom AttributorClass for image alignment formatting.\r\n * \r\n * @example\r\n * this.Quill = this.quill.constructor\r\n * const ImageAlignClass = createImageAlignAttributor(this.Quill);\r\n * this.ImageAlign = new ImageAlignClass();\r\n * this.Quill.register({\r\n * 'formats/imageAlign': this.ImageAlign,\r\n * 'attributors/class/imageAlign': this.ImageAlign,\r\n * }, true);\r\n *\r\n * @remarks\r\n * - The attributor manages both string and object values for alignment, supporting additional metadata like `title`.\r\n * - It ensures the wrapper span is not editable and synchronizes width information for correct CSS rendering.\r\n * - Handles edge cases where Quill merges inline styles, ensuring the image alignment format is reapplied as needed.\r\n * - The `remove` and `value` methods ensure proper cleanup and retrieval of alignment state.\r\n */\r\nexport const createImageAlignAttributor = (QuillConstructor: any): AttributorClass => {\r\n const parchment = QuillConstructor.import('parchment') as any;\r\n const { ClassAttributor, Scope } = parchment;\r\n\r\n return class ImageAlignAttributor extends ClassAttributor {\r\n static tagName = 'SPAN';\r\n static attrName = 'imageAlign';\r\n\r\n constructor(private debug = false) {\r\n super('imageAlign', 'ql-image-align', {\r\n scope: Scope.INLINE,\r\n whitelist: ['left', 'center', 'right'],\r\n });\r\n }\r\n\r\n /**\r\n * Adds or updates alignment and related formatting for an image wrapper node.\r\n *\r\n * This method applies alignment, caption, and width formatting to a given node containing an image.\r\n * It handles both object-based and string-based alignment values, updates relevant attributes,\r\n * and ensures the wrapper is styled correctly for Quill's image formatting.\r\n *\r\n * - If the node is an HTMLSpanElement, it sets alignment, caption (data-title), and width attributes.\r\n * - If the node is not a span, it attempts to find an image child and reapply the imageAlign format.\r\n * - Handles Quill's inline style merging quirks to avoid redundant wrappers.\r\n *\r\n * @param node - The DOM element (typically a span or container) to apply formatting to.\r\n * @param value - The alignment value, which can be a string or an object containing alignment and optional title.\r\n * @returns `true` if formatting was applied or handled, `false` otherwise.\r\n */\r\n add(node: Element, value: ImageAlignInputValue | string): boolean {\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.add', node, value);\r\n }\r\n if (node instanceof HTMLSpanElement && value) {\r\n let imageElement = node.querySelector('img') as HTMLImageElement;\r\n if (typeof value === 'object' && value.align) {\r\n super.add(node, value.align);\r\n node.setAttribute('contenteditable', 'false');\r\n // data-title used to populate caption via ::after\r\n if (!!value.title) {\r\n node.setAttribute('data-title', value.title);\r\n } else {\r\n node.removeAttribute('data-title');\r\n }\r\n if (value.align) {\r\n imageElement.dataset.blotAlign = value.align;\r\n }\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.add - imageElement:', imageElement, 'aligned with:', value.align);\r\n }\r\n } else if (typeof value === 'string') {\r\n super.add(node, value);\r\n imageElement.dataset.blotAlign = value;\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.add - imageElement:', imageElement, 'aligned with:', value);\r\n }\r\n } else {\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.add - no value provided, skipping alignment');\r\n }\r\n return false;\r\n }\r\n // set width style property on wrapper if image and has imageAlign format\r\n // fallback to image natural width if width attribute missing (image not resized))\r\n // width needed to size wrapper correctly via css\r\n // width style value must include units, add 'px' if numeric only\r\n let width: string | null = this.getImageWidth(imageElement);\r\n node.setAttribute('data-relative-size', `${width?.endsWith('%')}`)\r\n return true;\r\n } else {\r\n // bug fix - Quill's inline styles merge elements and remove span element if styles nested\r\n // for the first outer style, reapply imageAlign format on image \r\n // for subsequent outer styles, skip reformat and just return true - will nest multiple span wrappers otherwise\r\n const imageElement = node instanceof HTMLImageElement ? node : node.querySelector('img');\r\n if (this.debug)\r\n console.debug(`ImageAlignAttributor.add - ${node.tagName} is not a span, checking for image:`, imageElement);\r\n if (imageElement instanceof HTMLImageElement) {\r\n // Use QuillConstructor.find to find the image blot, using global Quill static methods will always return null \r\n // in some environments such as vite, react, etc.\r\n const imageBlot = QuillConstructor.find(imageElement) as any;\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.add - found image blot:', imageBlot);\r\n }\r\n if (\r\n imageBlot &&\r\n (\r\n node.firstChild instanceof HTMLSpanElement ||\r\n !(imageElement.parentElement?.matches('span[class^=\"ql-image-align-\"]'))\r\n )\r\n ) {\r\n imageBlot.format('imageAlign', value);\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.add - reapplying imageAlign format to image blot:', value, imageBlot);\r\n }\r\n }\r\n return true;\r\n }\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Removes alignment formatting from the given DOM node.\r\n *\r\n * If the node is an HTMLElement, it first calls the parent class's remove method.\r\n * Then, if the node's first child is also an HTMLElement, it deletes the `blotAlign`\r\n * data attribute from that child element.\r\n *\r\n * @param node - The DOM element from which to remove alignment formatting.\r\n */\r\n remove(node: Element): void {\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.remove', node);\r\n }\r\n if (node instanceof HTMLElement) {\r\n super.remove(node);\r\n if (node.firstChild && (node.firstChild instanceof HTMLElement)) {\r\n delete node.firstChild.dataset.blotAlign;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Retrieves alignment and metadata information for an image element within a given DOM node.\r\n *\r\n * This method attempts to find an `<img>` element within the provided node, then extracts its alignment,\r\n * title, and width attributes. If the width attribute is missing or invalid, it tries to determine the width\r\n * either immediately (if the image is loaded) or by setting an `onload` handler. The method returns an object\r\n * containing the alignment, title, width, a `contenteditable` flag, and a boolean string indicating if the width\r\n * is specified as a percentage.\r\n *\r\n * @param node - The DOM element to search for an image and extract alignment and metadata from.\r\n * @returns An object containing the image's alignment, title, width, contenteditable status, and relative size flag.\r\n */\r\n value(node: Element): ImageAlignValue {\r\n // in case nested style, find image element and span wrapper\r\n const imageElement = node.querySelector('img') as HTMLImageElement;\r\n if (!imageElement) return null as any; // this can happen during certain 'undo' operations\r\n const parentElement = imageElement.parentElement as HTMLElement;\r\n const align = super.value(parentElement);\r\n const title: string = imageElement.getAttribute('title') || '';\r\n let width: string = imageElement.getAttribute('width') || '';\r\n // attempt to get width if missing or image not loaded\r\n if (!parseFloat(width)) {\r\n if (imageElement.complete) {\r\n width = this.getImageWidth(imageElement);\r\n } else {\r\n imageElement.onload = (event) => {\r\n width = this.getImageWidth(event.target as HTMLImageElement);\r\n }\r\n }\r\n }\r\n const value = {\r\n align: align,\r\n title: title,\r\n width: width,\r\n contenteditable: 'false',\r\n relativeSize: `${width.endsWith('%')}`\r\n };\r\n if (this.debug) {\r\n console.debug('ImageAlignAttributor.value', node, value);\r\n }\r\n return value;\r\n }\r\n\r\n /**\r\n * Retrieves the width of the given HTMLImageElement, ensuring it is set as an attribute and formatted with 'px' units.\r\n * \r\n * - If the 'width' attribute is not present, it uses the image's natural width and sets it as the 'width' attribute in pixels.\r\n * - 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.\r\n * - Updates the parent element's CSS variable '--resize-width' with the computed width.\r\n * \r\n * @param imageElement - The HTMLImageElement whose width is to be retrieved and set.\r\n * @returns The width of the image as a string with 'px' units.\r\n */\r\n getImageWidth(imageElement: HTMLImageElement) {\r\n let width = imageElement.getAttribute('width');\r\n if (!width) {\r\n width = `${imageElement.naturalWidth}px`;\r\n imageElement.setAttribute('width', width);\r\n } else {\r\n if (!isNaN(Number(width.trim().slice(-1)))) {\r\n width = `${width}px`\r\n imageElement.setAttribute('width', width);\r\n }\r\n }\r\n (imageElement.parentElement as HTMLElement).style.setProperty('--resize-width', width);\r\n return width;\r\n }\r\n\r\n }\r\n}\r\n","/**\r\n * Factory function to create a custom Quill video blot class with responsive styling.\r\n *\r\n * @param QuillConstructor - The Quill constructor or instance used to import the base video format.\r\n * @returns A class extending Quill's VideoEmbed, enforcing a 16:9 aspect ratio and full width for embedded videos.\r\n *\r\n * @remarks\r\n * The returned class, `VideoResponsive`, overrides the default video blot to ensure videos are displayed responsively.\r\n * The aspect ratio is controlled via the static `aspectRatio` property and applied to the video element's style.\r\n *\r\n * @example\r\n * ```typescript\r\n * const VideoResponsive = createResponsiveVideoBlotClass(Quill);\r\n * Quill.register(VideoResponsive);\r\n * ```\r\n */\r\nexport const createResponsiveVideoBlotClass = (QuillConstructor: any): any => {\r\n const VideoEmbed = QuillConstructor.import(\"formats/video\") as any;\r\n\r\n /**\r\n * A custom Quill blot for embedding responsive videos.\r\n * Extends `VideoEmbed` to ensure videos use a 16:9 aspect ratio and full width.\r\n *\r\n * @remarks\r\n * The aspect ratio is set via the `aspectRatio` static property and applied to the video element's style.\r\n *\r\n * @example\r\n * ```typescript\r\n * const videoBlot = VideoResponsive.create('https://example.com/video.mp4');\r\n * ```\r\n *\r\n * @extends VideoEmbed\r\n */\r\n return class VideoResponsive extends VideoEmbed {\r\n static blotName = 'video';\r\n static aspectRatio: string = \"16 / 9 auto\"\r\n static create(value: string) {\r\n const node = super.create(value);\r\n node.setAttribute('width', '100%');\r\n node.style.aspectRatio = this.aspectRatio;\r\n return node;\r\n }\r\n html() {\r\n return this.domNode.outerHTML;\r\n }\r\n }\r\n}\r\n","import BlotFormatter from '../../BlotFormatter.js';\r\nimport { Aligner } from './Aligner.js';\r\nimport type { Alignment } from './Alignment.js';\r\nimport type { Blot } from '../../specs/BlotSpec.js';\r\nimport type { Options } from '../../Options.js';\r\n\r\n/**\r\n * The `DefaultAligner` class provides alignment management for Quill editor blots (such as images and iframes).\r\n * It implements the `Aligner` interface and is responsible for applying, clearing, and querying alignment\r\n * formatting on supported blots within the editor.\r\n *\r\n * This class supports both inline and block-level blots, and can be configured with custom alignment options.\r\n * It interacts with Quill's Parchment module to determine blot types and scopes, and uses formatter options\r\n * to control alignment and resizing behaviors.\r\n *\r\n * Key features:\r\n * - Registers available alignments and exposes them via `getAlignments()`.\r\n * - Applies alignment to blots using `setAlignment()`, handling both inline (e.g., images) and block (e.g., iframes) elements.\r\n * - Clears alignment formatting from blots with `clear()`.\r\n * - Determines blot type and scope with utility methods (`isInlineBlot`, `isBlockBlot`, `hasInlineScope`, `hasBlockScope`).\r\n * - Checks and retrieves current alignment with `isAligned()` and `getAlignment()`.\r\n * - Optionally sets relative width for images if configured.\r\n * - Ensures editor usability by adding a new paragraph if the editor contains only an aligned image.\r\n *\r\n * @remarks\r\n * This class is intended for internal use by the BlotFormatter module and expects a properly configured\r\n * `BlotFormatter` instance with alignment and resize options.\r\n *\r\n * @example\r\n * ```typescript\r\n * const aligner = new DefaultAligner(formatter);\r\n * aligner.setAlignment(blot, 'center');\r\n * ```\r\n *\r\n * @see Aligner\r\n * @see BlotFormatter\r\n */\r\nexport default class DefaultAligner implements Aligner {\r\n alignments: Record<string, Alignment> = {};\r\n options: Options;\r\n formatter: BlotFormatter;\r\n private debug: boolean;\r\n private Scope: any;\r\n\r\n constructor(formatter: BlotFormatter) {\r\n this.formatter