@notectl/core 2.2.5 → 2.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{AlignmentPlugin-BCHxo16v.mjs → AlignmentPlugin-DeD4C4EP.mjs} +2 -2
- package/dist/chunks/{AlignmentPlugin-BCHxo16v.mjs.map → AlignmentPlugin-DeD4C4EP.mjs.map} +1 -1
- package/dist/chunks/{CodeBlockPlugin-DC0-BnNy.mjs → CodeBlockPlugin-3V2K3RBe.mjs} +2 -2
- package/dist/chunks/{CodeBlockPlugin-DC0-BnNy.mjs.map → CodeBlockPlugin-3V2K3RBe.mjs.map} +1 -1
- package/dist/chunks/{ColorGrid-CXRdpP_n.mjs → ColorGrid-DPNiXxpD.mjs} +2 -2
- package/dist/chunks/{ColorGrid-CXRdpP_n.mjs.map → ColorGrid-DPNiXxpD.mjs.map} +1 -1
- package/dist/chunks/{ColorPickerPopup-C_74bqid.mjs → ColorPickerPopup-lcf3jS6G.mjs} +2 -2
- package/dist/chunks/{ColorPickerPopup-C_74bqid.mjs.map → ColorPickerPopup-lcf3jS6G.mjs.map} +1 -1
- package/dist/chunks/FontPlugin-DQMj65Bc.mjs +2 -0
- package/dist/chunks/{FontPlugin-DB1_bgam.mjs.map → FontPlugin-DQMj65Bc.mjs.map} +1 -1
- package/dist/chunks/{FontSizePlugin-DFvp7Ki9.mjs → FontSizePlugin-9StCjEfN.mjs} +2 -2
- package/dist/chunks/{FontSizePlugin-DFvp7Ki9.mjs.map → FontSizePlugin-9StCjEfN.mjs.map} +1 -1
- package/dist/chunks/{FormulaPlugin-BLEcyazj.mjs → FormulaPlugin-CV5VUxkb.mjs} +2 -2
- package/dist/chunks/{FormulaPlugin-BLEcyazj.mjs.map → FormulaPlugin-CV5VUxkb.mjs.map} +1 -1
- package/dist/chunks/{HeadingPlugin-D9yvG2Aw.mjs → HeadingPlugin-BuGoM9Rz.mjs} +2 -2
- package/dist/chunks/{HeadingPlugin-D9yvG2Aw.mjs.map → HeadingPlugin-BuGoM9Rz.mjs.map} +1 -1
- package/dist/chunks/{HighlightPlugin-D4gh4RP7.mjs → HighlightPlugin-C6pHifVz.mjs} +2 -2
- package/dist/chunks/{HighlightPlugin-D4gh4RP7.mjs.map → HighlightPlugin-C6pHifVz.mjs.map} +1 -1
- package/dist/chunks/{ImagePlugin-Cp_RINtc.mjs → ImagePlugin-KpkuE2LH.mjs} +2 -2
- package/dist/chunks/{ImagePlugin-Cp_RINtc.mjs.map → ImagePlugin-KpkuE2LH.mjs.map} +1 -1
- package/dist/chunks/{InlineStyleMarkSpec-4R6DC7ij.mjs → InlineStyleMarkSpec-C6LJT7kp.mjs} +2 -2
- package/dist/chunks/{InlineStyleMarkSpec-4R6DC7ij.mjs.map → InlineStyleMarkSpec-C6LJT7kp.mjs.map} +1 -1
- package/dist/chunks/{ListPlugin-CVId2dE0.mjs → ListPlugin-2yl6KiTK.mjs} +2 -2
- package/dist/chunks/{ListPlugin-CVId2dE0.mjs.map → ListPlugin-2yl6KiTK.mjs.map} +1 -1
- package/dist/chunks/{PopupManager-CyOfZUab.mjs → PopupManager-DUtaK3WX.mjs} +2 -2
- package/dist/chunks/{PopupManager-CyOfZUab.mjs.map → PopupManager-DUtaK3WX.mjs.map} +1 -1
- package/dist/chunks/{PopupPositioning-DtUrY9bh.mjs → PopupPositioning-BB97-__N.mjs} +2 -2
- package/dist/chunks/{PopupPositioning-DtUrY9bh.mjs.map → PopupPositioning-BB97-__N.mjs.map} +1 -1
- package/dist/chunks/PrintPlugin-DyIZDY3Q.mjs +2 -0
- package/dist/chunks/PrintPlugin-DyIZDY3Q.mjs.map +1 -0
- package/dist/chunks/{PrintTypes-WdtgSj74.mjs → PrintTypes-CxU69qu3.mjs} +3 -2
- package/dist/chunks/PrintTypes-CxU69qu3.mjs.map +1 -0
- package/dist/chunks/StyleRuntime-BFKu52mS.mjs +2 -0
- package/dist/chunks/StyleRuntime-BFKu52mS.mjs.map +1 -0
- package/dist/chunks/{TablePlugin-D93RR5xH.mjs → TablePlugin-BTH9VRZq.mjs} +2 -2
- package/dist/chunks/{TablePlugin-D93RR5xH.mjs.map → TablePlugin-BTH9VRZq.mjs.map} +1 -1
- package/dist/chunks/{TextColorPlugin-C1qpzEOa.mjs → TextColorPlugin-CEfZQ6Md.mjs} +2 -2
- package/dist/chunks/{TextColorPlugin-C1qpzEOa.mjs.map → TextColorPlugin-CEfZQ6Md.mjs.map} +1 -1
- package/dist/chunks/{ToolbarPlugin-CLz2wa2c.mjs → ToolbarPlugin-DVGrFf8v.mjs} +2 -2
- package/dist/chunks/{ToolbarPlugin-CLz2wa2c.mjs.map → ToolbarPlugin-DVGrFf8v.mjs.map} +1 -1
- package/dist/chunks/{VideoPlugin-D9rQZol6.mjs → VideoPlugin-B3uf1Dwp.mjs} +2 -2
- package/dist/chunks/{VideoPlugin-D9rQZol6.mjs.map → VideoPlugin-B3uf1Dwp.mjs.map} +1 -1
- package/dist/chunks/{full-Cah3qjCT.mjs → full-BBl7mvwH.mjs} +2 -2
- package/dist/chunks/{full-Cah3qjCT.mjs.map → full-BBl7mvwH.mjs.map} +1 -1
- package/dist/chunks/minimal-CtnR4dqp.mjs +2 -0
- package/dist/chunks/{minimal-DqATlnAL.mjs.map → minimal-CtnR4dqp.mjs.map} +1 -1
- package/dist/chunks/src-C89Cw7ni.mjs +2 -0
- package/dist/chunks/{src-YY6n2SmV.mjs.map → src-C89Cw7ni.mjs.map} +1 -1
- package/dist/editor/NotectlEditor.d.ts +22 -3
- package/dist/editor/NotectlEditor.d.ts.map +1 -1
- package/dist/full.mjs +1 -1
- package/dist/notectl-core.mjs +1 -1
- package/dist/notectl-core.umd.js +1 -1
- package/dist/notectl-core.umd.js.map +1 -1
- package/dist/plugins/alignment.mjs +1 -1
- package/dist/plugins/code-block.mjs +1 -1
- package/dist/plugins/font-size.mjs +1 -1
- package/dist/plugins/font.mjs +1 -1
- package/dist/plugins/formula.mjs +1 -1
- package/dist/plugins/heading.mjs +1 -1
- package/dist/plugins/highlight.mjs +1 -1
- package/dist/plugins/image.mjs +1 -1
- package/dist/plugins/list.mjs +1 -1
- package/dist/plugins/print/PrintHostStyles.d.ts +50 -0
- package/dist/plugins/print/PrintHostStyles.d.ts.map +1 -0
- package/dist/plugins/print/PrintPlugin.d.ts.map +1 -1
- package/dist/plugins/print/PrintServiceImpl.d.ts +72 -3
- package/dist/plugins/print/PrintServiceImpl.d.ts.map +1 -1
- package/dist/plugins/print/PrintStyleCollector.d.ts +150 -21
- package/dist/plugins/print/PrintStyleCollector.d.ts.map +1 -1
- package/dist/plugins/print/StaticHostMarker.d.ts +18 -0
- package/dist/plugins/print/StaticHostMarker.d.ts.map +1 -0
- package/dist/plugins/print.mjs +1 -1
- package/dist/plugins/shared.mjs +1 -1
- package/dist/plugins/table.mjs +1 -1
- package/dist/plugins/text-color.mjs +1 -1
- package/dist/plugins/toolbar.mjs +1 -1
- package/dist/plugins/video.mjs +1 -1
- package/dist/presets/full.mjs +1 -1
- package/dist/presets/minimal.mjs +1 -1
- package/dist/presets.mjs +1 -1
- package/dist/style/StyleRuntime.d.ts +7 -0
- package/dist/style/StyleRuntime.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/FontPlugin-DB1_bgam.mjs +0 -2
- package/dist/chunks/PrintPlugin-C6gOJT6d.mjs +0 -2
- package/dist/chunks/PrintPlugin-C6gOJT6d.mjs.map +0 -1
- package/dist/chunks/PrintTypes-WdtgSj74.mjs.map +0 -1
- package/dist/chunks/StyleRuntime-BAutwDZT.mjs +0 -2
- package/dist/chunks/StyleRuntime-BAutwDZT.mjs.map +0 -1
- package/dist/chunks/minimal-DqATlnAL.mjs +0 -2
- package/dist/chunks/src-YY6n2SmV.mjs +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImagePlugin-Cp_RINtc.mjs","names":[],"sources":["../../src/editor/styles/image.ts","../../src/plugins/image/ImageCommands.ts","../../src/plugins/image/ImageLocale.ts","../../src/plugins/image/ImageNodeView.ts","../../src/plugins/image/ImagePopup.ts","../../src/plugins/image/ImagePopupStyles.ts","../../src/plugins/image/ImageUpload.ts","../../src/plugins/image/ImagePlugin.ts"],"sourcesContent":["/** Image styles — display, alignment, resize handles, upload overlay. */\nexport const IMAGE_CSS = `\n/* Image */\n.notectl-image {\n\tmargin: 8px 0;\n\tuser-select: none;\n\tline-height: 0;\n}\n\n.notectl-image--center {\n\ttext-align: center;\n}\n\n.notectl-image--start {\n\ttext-align: start;\n}\n\n.notectl-image--end {\n\ttext-align: end;\n}\n\n.notectl-image__container {\n\tdisplay: inline-block;\n\tposition: relative;\n\tline-height: 0;\n}\n\n.notectl-image__img {\n\tdisplay: block;\n\tmax-width: 100%;\n\tborder-radius: 4px;\n}\n\n.notectl-image--selected .notectl-image__container {\n\toutline: 2px solid var(--notectl-primary);\n\toutline-offset: 2px;\n\tborder-radius: 6px;\n}\n\n/* Alignment toolbar (appears on image selection) */\n.notectl-image__align-toolbar {\n\tdisplay: flex;\n\tgap: 2px;\n\tposition: absolute;\n\ttop: -36px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tbackground: var(--notectl-surface-overlay);\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 6px;\n\tpadding: 2px;\n\tbox-shadow: 0 2px 8px var(--notectl-shadow);\n\tz-index: 10;\n}\n\n.notectl-image__align-btn {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 28px;\n\theight: 28px;\n\tborder: none;\n\tbackground: transparent;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\tcolor: var(--notectl-fg);\n}\n\n.notectl-image__align-btn:hover {\n\tbackground: var(--notectl-hover-bg);\n}\n\n.notectl-image__align-btn--active {\n\tbackground: var(--notectl-active-bg);\n\tcolor: var(--notectl-primary-fg);\n}\n\n.notectl-image__align-btn svg {\n\twidth: 16px;\n\theight: 16px;\n\tfill: currentColor;\n}\n\n/* Upload overlay */\n.notectl-image__overlay {\n\tposition: absolute;\n\tinset: 0;\n\tdisplay: none;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground: rgba(255, 255, 255, 0.8);\n\tborder-radius: 4px;\n\tfont-size: 13px;\n\tcolor: var(--notectl-fg-muted);\n}\n\n.notectl-image__overlay--uploading {\n\tdisplay: flex;\n}\n\n.notectl-image__overlay--error {\n\tdisplay: flex;\n\tbackground: var(--notectl-danger-muted);\n\tcolor: var(--notectl-danger);\n}\n\n/* Resize overlay + handles */\n.notectl-image__resize-overlay {\n\tposition: absolute;\n\tinset: 0;\n\tpointer-events: none;\n}\n\n.notectl-image__resize-handle {\n\tposition: absolute;\n\twidth: 10px;\n\theight: 10px;\n\tbackground: var(--notectl-surface-raised);\n\tborder: 2px solid var(--notectl-primary);\n\tborder-radius: 50%;\n\tpointer-events: all;\n\tz-index: 2;\n\ttransition: transform 0.1s, background 0.1s;\n}\n\n.notectl-image__resize-handle:hover {\n\tbackground: var(--notectl-primary);\n\ttransform: scale(1.3);\n}\n\n.notectl-image--resizing .notectl-image__resize-handle {\n\tbackground: var(--notectl-primary);\n}\n\n.notectl-image__resize-handle--nw {\n\ttop: -5px;\n\tinset-inline-start: -5px;\n\tcursor: nwse-resize;\n}\n\n.notectl-image__resize-handle--ne {\n\ttop: -5px;\n\tinset-inline-end: -5px;\n\tcursor: nesw-resize;\n}\n\n.notectl-image__resize-handle--sw {\n\tbottom: -5px;\n\tinset-inline-start: -5px;\n\tcursor: nesw-resize;\n}\n\n.notectl-image__resize-handle--se {\n\tbottom: -5px;\n\tinset-inline-end: -5px;\n\tcursor: nwse-resize;\n}\n\n/* RTL cursor overrides — flip diagonal cursors when in RTL context */\n[dir=\"rtl\"] .notectl-image__resize-handle--nw {\n\tcursor: nesw-resize;\n}\n\n[dir=\"rtl\"] .notectl-image__resize-handle--ne {\n\tcursor: nwse-resize;\n}\n\n[dir=\"rtl\"] .notectl-image__resize-handle--sw {\n\tcursor: nwse-resize;\n}\n\n[dir=\"rtl\"] .notectl-image__resize-handle--se {\n\tcursor: nesw-resize;\n}\n\n/* Size indicator tooltip */\n.notectl-image__size-indicator {\n\tposition: absolute;\n\tbottom: -28px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tpadding: 2px 8px;\n\tbackground: var(--notectl-tooltip-bg);\n\tcolor: var(--notectl-tooltip-fg);\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tborder-radius: 4px;\n\twhite-space: nowrap;\n\tpointer-events: none;\n\topacity: 0;\n\ttransition: opacity 0.15s;\n\tz-index: 3;\n}\n\n.notectl-image__size-indicator--visible {\n\topacity: 1;\n}\n\n/* Keyboard resize hint (shown on selection, hidden during drag) */\n.notectl-image__keyboard-hint {\n\tdisplay: none;\n\tposition: absolute;\n\tbottom: -28px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tpadding: 2px 8px;\n\tfont-size: 11px;\n\tcolor: var(--notectl-fg-muted);\n\twhite-space: nowrap;\n\tpointer-events: none;\n\tz-index: 3;\n}\n\n.notectl-image--selected .notectl-image__keyboard-hint {\n\tdisplay: block;\n}\n\n.notectl-image--resizing .notectl-image__keyboard-hint {\n\tdisplay: none;\n}\n\n/* Resizing feedback */\n.notectl-image--resizing .notectl-image__img {\n\topacity: 0.85;\n}\n`;\n","/**\n * Image plugin commands: insert, remove, and attribute updates.\n * Supports both root-level and nested contexts (e.g. images inside table cells).\n */\n\nimport { insertBlockObjectOnOwnLine } from '../../commands/BlockInsertion.js';\nimport { deleteNodeSelection } from '../../commands/NodeSelectionCommands.js';\nimport type { BlockAttrs, BlockNode } from '../../model/Document.js';\nimport { createBlockNode, getBlockChildren, isBlockNode } from '../../model/Document.js';\nimport { createNodeSelection, isGapCursor, isNodeSelection } from '../../model/Selection.js';\nimport type { BlockId } from '../../model/TypeBrands.js';\nimport { nodeType } from '../../model/TypeBrands.js';\nimport type { EditorState } from '../../state/EditorState.js';\nimport type { PluginContext } from '../Plugin.js';\nimport type { ImageAttrs } from './ImageUpload.js';\n\n/**\n * Inserts an image block. When the cursor is inside a table cell,\n * the image is inserted as a child of the cell. Otherwise it is\n * inserted after the current block at document root.\n */\nexport function insertImage(\n\tcontext: PluginContext,\n\tattrs: Partial<ImageAttrs> & { readonly src: string },\n): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\n\tconst imageAttrs: BlockAttrs = {\n\t\tsrc: attrs.src,\n\t\talt: attrs.alt ?? '',\n\t\talign: attrs.align ?? 'center',\n\t\t...(attrs.width !== undefined ? { width: attrs.width } : {}),\n\t\t...(attrs.height !== undefined ? { height: attrs.height } : {}),\n\t};\n\n\tif (isGapCursor(sel)) return false;\n\n\tconst anchorBlockId: BlockId = isNodeSelection(sel) ? sel.nodeId : sel.anchor.blockId;\n\n\t// Check if we're inside a table cell\n\tconst cellId: BlockId | undefined = findTableCellAncestor(state, anchorBlockId);\n\tif (cellId) {\n\t\treturn insertImageIntoCell(state, context, cellId, imageAttrs);\n\t}\n\n\treturn insertImageAtRoot(state, context, anchorBlockId, imageAttrs);\n}\n\n/** Finds a table_cell ancestor for the given block (or the block itself). */\nfunction findTableCellAncestor(state: EditorState, bid: BlockId): BlockId | undefined {\n\tconst block: BlockNode | undefined = state.getBlock(bid);\n\tif (block?.type === 'table_cell') return bid;\n\n\tconst path: BlockId[] | undefined = state.getNodePath(bid);\n\tif (!path) return undefined;\n\n\tfor (const id of path) {\n\t\tconst node: BlockNode | undefined = state.getBlock(id);\n\t\tif (node?.type === 'table_cell') return id;\n\t}\n\treturn undefined;\n}\n\n/** Inserts an image on its own line at document root, after the anchor block. */\nfunction insertImageAtRoot(\n\tstate: EditorState,\n\tcontext: PluginContext,\n\tanchorBlockId: BlockId,\n\timageAttrs: BlockAttrs,\n): boolean {\n\tconst imageBlock: BlockNode = createBlockNode(nodeType('image'), [], undefined, imageAttrs);\n\tconst builder = state.transaction('command');\n\tconst trailing = insertBlockObjectOnOwnLine(state, builder, anchorBlockId, imageBlock);\n\tif (!trailing) return false;\n\n\tbuilder.setSelection(createNodeSelection(imageBlock.id, []));\n\tcontext.dispatch(builder.build());\n\treturn true;\n}\n\n/** Inserts an image as a child of a table cell. */\nfunction insertImageIntoCell(\n\tstate: EditorState,\n\tcontext: PluginContext,\n\tcellId: BlockId,\n\timageAttrs: BlockAttrs,\n): boolean {\n\tconst cellPath: BlockId[] | undefined = state.getNodePath(cellId);\n\tif (!cellPath) return false;\n\n\tconst imageBlock: BlockNode = createBlockNode(nodeType('image'), [], undefined, imageAttrs);\n\n\t// Remove existing block children first (if any)\n\tconst cell: BlockNode | undefined = state.getBlock(cellId);\n\tif (!cell) return false;\n\n\tconst builder = state.transaction('command');\n\tconst blockChildren: readonly BlockNode[] = getBlockChildren(cell);\n\tfor (let i = blockChildren.length - 1; i >= 0; i--) {\n\t\t// Find the raw index in children array\n\t\tconst rawIndex: number = cell.children.findIndex(\n\t\t\t(c) => isBlockNode(c) && c.id === blockChildren[i]?.id,\n\t\t);\n\t\tif (rawIndex !== -1) {\n\t\t\tbuilder.removeNode(cellPath, rawIndex);\n\t\t}\n\t}\n\n\t// Insert image at index 0 (before any text nodes)\n\tbuilder.insertNode(cellPath, 0, imageBlock);\n\tbuilder.setSelection(createNodeSelection(imageBlock.id, [...cellPath, imageBlock.id]));\n\n\tcontext.dispatch(builder.build());\n\treturn true;\n}\n\n/** Removes the currently selected image block (works for nested images too). */\nexport function removeImage(context: PluginContext): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst nodePath: BlockId[] | undefined = state.getNodePath(sel.nodeId);\n\tif (!nodePath || nodePath.length === 0) return false;\n\n\tconst tr = deleteNodeSelection(state, createNodeSelection(sel.nodeId, nodePath));\n\tif (!tr) return false;\n\tcontext.dispatch(tr);\n\treturn true;\n}\n\n/** Updates attributes on the currently selected image block. */\nexport function setImageAttr(context: PluginContext, attrs: Partial<ImageAttrs>): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst path: BlockId[] | undefined = state.getNodePath(sel.nodeId);\n\tif (!path) return false;\n\n\tconst merged: BlockAttrs = { ...(block.attrs ?? {}), ...attrs };\n\tconst tr = state.transaction('command').setNodeAttr(path, merged).build();\n\n\tcontext.dispatch(tr);\n\treturn true;\n}\n\n/** Minimum image width in pixels (shared with NodeView). */\nexport const MIN_IMAGE_WIDTH = 50;\n\n/**\n * Resizes the currently selected image by a pixel delta, preserving aspect ratio.\n * Returns false if no image is selected or the image has no width/height attributes.\n */\nexport function resizeImageByDelta(\n\tcontext: PluginContext,\n\tdelta: number,\n\tmaxWidth: number,\n): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst currentWidth: number | undefined = block.attrs?.width as number | undefined;\n\tconst currentHeight: number | undefined = block.attrs?.height as number | undefined;\n\tif (currentWidth === undefined || currentHeight === undefined) return false;\n\n\tconst aspectRatio: number = currentHeight > 0 ? currentWidth / currentHeight : 1;\n\tconst clampedWidth: number = Math.max(MIN_IMAGE_WIDTH, Math.min(maxWidth, currentWidth + delta));\n\tconst newHeight: number = Math.round(clampedWidth / aspectRatio);\n\n\tif (clampedWidth === currentWidth && newHeight === currentHeight) return false;\n\n\treturn setImageAttr(context, { width: clampedWidth, height: newHeight });\n}\n\n/** Resets the currently selected image to its natural size by removing width/height. */\nexport function resetImageSize(context: PluginContext): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst path: BlockId[] | undefined = state.getNodePath(sel.nodeId);\n\tif (!path) return false;\n\n\tconst attrs: BlockAttrs = { ...(block.attrs ?? {}) };\n\tconst cleaned: BlockAttrs = Object.fromEntries(\n\t\tObject.entries(attrs).filter(([k]) => k !== 'width' && k !== 'height'),\n\t);\n\n\tconst tr = state.transaction('command').setNodeAttr(path, cleaned).build();\n\tcontext.dispatch(tr);\n\treturn true;\n}\n\n/** Registers all image commands on the plugin context. */\nexport function registerImageCommands(context: PluginContext): void {\n\tcontext.registerCommand('insertImage', () => {\n\t\treturn insertImage(context, { src: '' });\n\t});\n\n\tcontext.registerCommand('removeImage', () => {\n\t\treturn removeImage(context);\n\t});\n}\n","/** Locale interface and default English locale for the ImagePlugin. */\n\nimport { type LocaleModuleMap, loadLocaleModule } from '../shared/LocaleLoader.js';\n\n// --- Locale Interface ---\n\nexport interface ImageLocale {\n\treadonly insertImage: string;\n\treadonly insertImageTooltip: string;\n\treadonly uploadFromComputer: string;\n\treadonly uploadAria: string;\n\treadonly separator: string;\n\treadonly urlPlaceholder: string;\n\treadonly urlAria: string;\n\treadonly insertButton: string;\n\treadonly insertAria: string;\n\treadonly imageAria: (alt: string, width?: number, height?: number) => string;\n\treadonly resetToNaturalSize: string;\n\treadonly uploadFailed: string;\n\treadonly uploading: string;\n\treadonly resizeHandleLabel: (position: string) => string;\n\treadonly imageSelected: string;\n\treadonly altTextPrefix: string;\n\treadonly imageSizeAnnounce: (w: number, h: number) => string;\n\treadonly resizeHint: (shrink: string, grow: string) => string;\n\treadonly imageResized: (w: number, h: number) => string;\n\treadonly keyboardResizeHint: (shrink: string, grow: string) => string;\n}\n\n// --- Default English Locale ---\n\nexport const IMAGE_LOCALE_EN: ImageLocale = {\n\tinsertImage: 'Insert Image',\n\tinsertImageTooltip: 'Insert Image',\n\tuploadFromComputer: 'Upload from computer',\n\tuploadAria: 'Upload image from computer',\n\tseparator: 'or',\n\turlPlaceholder: 'https://...',\n\turlAria: 'Image URL',\n\tinsertButton: 'Insert',\n\tinsertAria: 'Insert image',\n\timageAria: (alt: string, width?: number, height?: number) => {\n\t\tconst parts: string[] = [alt || 'Image'];\n\t\tif (width !== undefined && height !== undefined) {\n\t\t\tparts.push(`${width} by ${height} pixels`);\n\t\t}\n\t\treturn parts.join(', ');\n\t},\n\tresetToNaturalSize: 'Image reset to natural size.',\n\tuploadFailed: 'Upload failed.',\n\tuploading: 'Uploading...',\n\tresizeHandleLabel: (position: string) => `Resize ${position}`,\n\timageSelected: 'Image selected.',\n\taltTextPrefix: 'Alt text: ',\n\timageSizeAnnounce: (w: number, h: number) => `Size: ${w} by ${h} pixels.`,\n\tresizeHint: (shrink: string, grow: string) => `${shrink} / ${grow} to resize.`,\n\timageResized: (w: number, h: number) => `Image resized to ${w} by ${h} pixels.`,\n\tkeyboardResizeHint: (shrink: string, grow: string) => `${shrink} / ${grow} to resize`,\n};\n\n// --- Lazy Locale Loader ---\n\nconst localeModules: LocaleModuleMap<ImageLocale> = import.meta.glob<{\n\tdefault: ImageLocale;\n}>('./locales/*.ts', { eager: false });\n\nexport async function loadImageLocale(lang: string): Promise<ImageLocale> {\n\treturn loadLocaleModule(localeModules, lang, IMAGE_LOCALE_EN);\n}\n","/**\n * NodeView factory for image blocks.\n * Renders <figure> with <img>, handles selection state, upload overlay,\n * resize with 4 corner handles + live size indicator, and a keyboard-hint\n * showing resize shortcuts when the image is selected.\n */\n\nimport type { BlockAttrs, BlockNode } from '../../model/Document.js';\nimport type { BlockId } from '../../model/TypeBrands.js';\nimport { getTextDirection } from '../../platform/Platform.js';\nimport type { EditorState } from '../../state/EditorState.js';\nimport type { Transaction } from '../../state/Transaction.js';\nimport {\n\tgetStyleNonceForNode,\n\tsetStyleProperties,\n\tsetStyleProperty,\n} from '../../style/StyleRuntime.js';\nimport type { NodeView, NodeViewFactory } from '../../view/NodeView.js';\nimport { formatShortcut } from '../shared/ShortcutFormatting.js';\nimport { IMAGE_LOCALE_EN, type ImageLocale } from './ImageLocale.js';\nimport type { ImageKeymap, ImagePluginConfig, UploadState } from './ImageUpload.js';\n\nconst ALIGNMENT_CLASSES: Record<string, string> = {\n\tstart: 'notectl-image--start',\n\tcenter: 'notectl-image--center',\n\tend: 'notectl-image--end',\n};\n\nconst MIN_IMAGE_WIDTH = 50;\n\ntype HandlePosition = 'nw' | 'ne' | 'sw' | 'se';\n\nconst HANDLE_CURSORS: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'nwse-resize',\n\tne: 'nesw-resize',\n\tsw: 'nesw-resize',\n\tse: 'nwse-resize',\n};\n\n/** +1 means dragging right increases width; -1 means dragging left does. */\nconst HANDLE_X_SIGN: Readonly<Record<HandlePosition, 1 | -1>> = {\n\tnw: -1,\n\tne: 1,\n\tsw: -1,\n\tse: 1,\n};\n\nconst HANDLE_POSITIONS_LTR: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'top-left',\n\tne: 'top-right',\n\tsw: 'bottom-left',\n\tse: 'bottom-right',\n};\n\nconst HANDLE_POSITIONS_RTL: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'top-right',\n\tne: 'top-left',\n\tsw: 'bottom-right',\n\tse: 'bottom-left',\n};\n\nconst RTL_CURSOR_MAP: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'nesw-resize',\n\tne: 'nwse-resize',\n\tsw: 'nwse-resize',\n\tse: 'nesw-resize',\n};\n\n/** Returns the direction-aware ARIA label for a resize handle. */\nexport function getHandleLabel(\n\tposition: HandlePosition,\n\tisRtl: boolean,\n\tlocale: ImageLocale = IMAGE_LOCALE_EN,\n): string {\n\tconst positionName: string = (isRtl ? HANDLE_POSITIONS_RTL : HANDLE_POSITIONS_LTR)[position];\n\treturn locale.resizeHandleLabel(positionName);\n}\n\n/** Returns the X-sign for resize drag, flipped in RTL. */\nexport function getResizeXSign(position: HandlePosition, isRtl: boolean): 1 | -1 {\n\tconst sign: 1 | -1 = HANDLE_X_SIGN[position];\n\treturn isRtl ? (-sign as 1 | -1) : sign;\n}\n\n// --- Global cursor override during resize ---\n\nlet activeCursorStyle: HTMLStyleElement | null = null;\n\nfunction setGlobalResizeCursor(cursor: string, referenceNode?: Node): void {\n\tclearGlobalResizeCursor();\n\tactiveCursorStyle = document.createElement('style');\n\tconst nonce = referenceNode ? getStyleNonceForNode(referenceNode) : undefined;\n\tif (nonce) {\n\t\tactiveCursorStyle.setAttribute('nonce', nonce);\n\t}\n\tactiveCursorStyle.textContent = `*{cursor:${cursor}!important;user-select:none!important}`;\n\tdocument.head.appendChild(activeCursorStyle);\n}\n\nfunction clearGlobalResizeCursor(): void {\n\tif (activeCursorStyle) {\n\t\tactiveCursorStyle.remove();\n\t\tactiveCursorStyle = null;\n\t}\n}\n\n/** Builds the keyboard-hint text from the resolved keymap. */\nfunction buildKeyboardHintText(\n\tresolvedKeymap: Readonly<Record<keyof ImageKeymap, string | null>>,\n\tlocale: ImageLocale,\n): string {\n\tconst shrink: string | null = resolvedKeymap.shrinkWidth ?? null;\n\tconst grow: string | null = resolvedKeymap.growWidth ?? null;\n\tif (!shrink || !grow) return '';\n\treturn locale.keyboardResizeHint(formatShortcut(shrink), formatShortcut(grow));\n}\n\n/** Creates a NodeViewFactory for image blocks. */\nexport function createImageNodeViewFactory(\n\tconfig: ImagePluginConfig,\n\tuploadStates: Map<BlockId, UploadState>,\n\tresolvedKeymap?: Readonly<Record<keyof ImageKeymap, string | null>>,\n\tlocale: ImageLocale = IMAGE_LOCALE_EN,\n): NodeViewFactory {\n\treturn (\n\t\tnode: BlockNode,\n\t\tgetState: () => EditorState,\n\t\tdispatch: (tr: Transaction) => void,\n\t): NodeView => {\n\t\t// --- DOM Construction ---\n\t\tconst figure: HTMLElement = document.createElement('figure');\n\t\tfigure.className = 'notectl-image';\n\t\tfigure.setAttribute('data-block-id', node.id);\n\t\tfigure.setAttribute('data-void', 'true');\n\t\tfigure.setAttribute('data-selectable', 'true');\n\n\t\tconst container: HTMLDivElement = document.createElement('div');\n\t\tcontainer.className = 'notectl-image__container';\n\n\t\tconst img: HTMLImageElement = document.createElement('img');\n\t\timg.className = 'notectl-image__img';\n\t\timg.draggable = false;\n\n\t\tconst overlay: HTMLDivElement = document.createElement('div');\n\t\toverlay.className = 'notectl-image__overlay';\n\n\t\tcontainer.appendChild(img);\n\t\tcontainer.appendChild(overlay);\n\t\tfigure.appendChild(container);\n\n\t\tlet currentNodeId: BlockId = node.id;\n\t\tlet resizeOverlay: HTMLDivElement | null = null;\n\n\t\t// --- ARIA Label ---\n\n\t\tfunction updateAriaLabel(n: BlockNode): void {\n\t\t\tconst alt: string = (n.attrs?.alt as string | undefined) ?? '';\n\t\t\tconst width: number | undefined = n.attrs?.width as number | undefined;\n\t\t\tconst height: number | undefined = n.attrs?.height as number | undefined;\n\t\t\tfigure.setAttribute('aria-label', locale.imageAria(alt, width, height));\n\t\t}\n\n\t\t// --- Attribute Application ---\n\n\t\tfunction applyAttrs(n: BlockNode): void {\n\t\t\tconst src: string = (n.attrs?.src as string | undefined) ?? '';\n\t\t\tconst alt: string = (n.attrs?.alt as string | undefined) ?? '';\n\t\t\tconst width: number | undefined = n.attrs?.width as number | undefined;\n\t\t\tconst height: number | undefined = n.attrs?.height as number | undefined;\n\t\t\tconst align: string = (n.attrs?.align as string | undefined) ?? 'center';\n\n\t\t\t// Only update src when it changes to avoid re-triggering load events\n\t\t\tif (img.src !== src) img.src = src;\n\t\t\timg.alt = alt;\n\n\t\t\tif (width !== undefined) {\n\t\t\t\tsetStyleProperty(img, 'width', `${width}px`);\n\t\t\t} else {\n\t\t\t\tsetStyleProperty(img, 'width', '');\n\t\t\t}\n\t\t\tif (height !== undefined) {\n\t\t\t\tsetStyleProperty(img, 'height', `${height}px`);\n\t\t\t} else {\n\t\t\t\tsetStyleProperty(img, 'height', '');\n\t\t\t}\n\n\t\t\tfor (const cls of Object.values(ALIGNMENT_CLASSES)) {\n\t\t\t\tfigure.classList.remove(cls);\n\t\t\t}\n\t\t\tconst alignClass: string | undefined = ALIGNMENT_CLASSES[align];\n\t\t\tif (alignClass) {\n\t\t\t\tfigure.classList.add(alignClass);\n\t\t\t}\n\n\t\t\tconst uploadState: UploadState = uploadStates.get(n.id) ?? 'idle';\n\t\t\tconst isUploading: boolean = uploadState === 'uploading';\n\t\t\toverlay.classList.toggle('notectl-image__overlay--uploading', isUploading);\n\t\t\toverlay.classList.toggle('notectl-image__overlay--error', uploadState === 'error');\n\n\t\t\tif (isUploading) {\n\t\t\t\toverlay.textContent = locale.uploading;\n\t\t\t} else if (uploadState === 'error') {\n\t\t\t\toverlay.textContent = locale.uploadFailed;\n\t\t\t} else {\n\t\t\t\toverlay.textContent = '';\n\t\t\t}\n\n\t\t\tif (isUploading) {\n\t\t\t\tfigure.setAttribute('aria-busy', 'true');\n\t\t\t} else {\n\t\t\t\tfigure.removeAttribute('aria-busy');\n\t\t\t}\n\n\t\t\tupdateAriaLabel(n);\n\t\t}\n\n\t\tapplyAttrs(node);\n\n\t\t// --- Commit natural dimensions on first load ---\n\n\t\tlet dimensionsCommitted = false;\n\n\t\timg.addEventListener('load', () => {\n\t\t\tif (dimensionsCommitted) return;\n\n\t\t\tconst state: EditorState = getState();\n\t\t\tconst block: BlockNode | undefined = state.getBlock(currentNodeId);\n\t\t\tif (!block || block.type !== 'image') return;\n\n\t\t\tconst hasWidth: boolean = block.attrs?.width !== undefined;\n\t\t\tconst hasHeight: boolean = block.attrs?.height !== undefined;\n\t\t\tif (hasWidth && hasHeight) {\n\t\t\t\tdimensionsCommitted = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst naturalW: number = img.naturalWidth;\n\t\t\tconst naturalH: number = img.naturalHeight;\n\t\t\tif (naturalW === 0 || naturalH === 0) return;\n\n\t\t\tconst clampedW: number = Math.min(naturalW, config.maxWidth);\n\t\t\tconst ratio: number = naturalW > 0 ? naturalH / naturalW : 1;\n\t\t\tconst clampedH: number = Math.round(clampedW * ratio);\n\n\t\t\tconst path: BlockId[] | undefined = state.getNodePath(currentNodeId);\n\t\t\tif (!path) return;\n\n\t\t\tconst merged: BlockAttrs = {\n\t\t\t\t...(block.attrs ?? {}),\n\t\t\t\twidth: clampedW,\n\t\t\t\theight: clampedH,\n\t\t\t};\n\n\t\t\tconst tr: Transaction = state.transaction('command').setNodeAttr(path, merged).build();\n\t\t\tdispatch(tr);\n\t\t\tdimensionsCommitted = true;\n\t\t});\n\n\t\t// --- Resize Logic ---\n\n\t\tfunction clampWidth(width: number): number {\n\t\t\treturn Math.max(MIN_IMAGE_WIDTH, Math.min(config.maxWidth, width));\n\t\t}\n\n\t\tfunction commitResize(nodeId: BlockId, width: number, height: number): void {\n\t\t\tconst state: EditorState = getState();\n\t\t\tconst block: BlockNode | undefined = state.getBlock(nodeId);\n\t\t\tif (!block) return;\n\n\t\t\tconst prevWidth: number | undefined = block.attrs?.width as number | undefined;\n\t\t\tconst prevHeight: number | undefined = block.attrs?.height as number | undefined;\n\t\t\tif (prevWidth === width && prevHeight === height) return;\n\n\t\t\tconst path: BlockId[] | undefined = state.getNodePath(nodeId);\n\t\t\tif (!path) return;\n\n\t\t\tconst merged: BlockAttrs = {\n\t\t\t\t...(block.attrs ?? {}),\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t};\n\n\t\t\tconst tr: Transaction = state.transaction('command').setNodeAttr(path, merged).build();\n\t\t\tdispatch(tr);\n\t\t}\n\n\t\tfunction attachHandleListeners(\n\t\t\thandle: HTMLDivElement,\n\t\t\tposition: HandlePosition,\n\t\t\tnodeId: BlockId,\n\t\t\tsizeIndicator: HTMLDivElement,\n\t\t): void {\n\t\t\tlet startX = 0;\n\t\t\tlet startWidth = 0;\n\t\t\tlet aspectRatio = 1;\n\t\t\tlet xSign: 1 | -1 = HANDLE_X_SIGN[position];\n\n\t\t\tconst onPointerMove = (e: PointerEvent): void => {\n\t\t\t\tconst deltaX: number = e.clientX - startX;\n\t\t\t\tconst newWidth: number = clampWidth(startWidth + deltaX * xSign);\n\t\t\t\tconst newHeight: number = Math.round(newWidth / aspectRatio);\n\n\t\t\t\tsetStyleProperties(img, {\n\t\t\t\t\twidth: `${newWidth}px`,\n\t\t\t\t\theight: `${newHeight}px`,\n\t\t\t\t});\n\n\t\t\t\tsizeIndicator.textContent = `${Math.round(newWidth)} \\u00D7 ${newHeight}`;\n\t\t\t};\n\n\t\t\tconst onPointerUp = (e: PointerEvent): void => {\n\t\t\t\tdocument.removeEventListener('pointermove', onPointerMove);\n\t\t\t\tdocument.removeEventListener('pointerup', onPointerUp);\n\n\t\t\t\tfigure.classList.remove('notectl-image--resizing');\n\t\t\t\tsizeIndicator.classList.remove('notectl-image__size-indicator--visible');\n\t\t\t\tclearGlobalResizeCursor();\n\n\t\t\t\tconst finalWidth: number = Math.round(img.getBoundingClientRect().width);\n\t\t\t\tconst finalHeight: number = Math.round(finalWidth / aspectRatio);\n\n\t\t\t\tcommitResize(nodeId, finalWidth, finalHeight);\n\n\t\t\t\t(e.target as HTMLElement | null)?.releasePointerCapture?.(e.pointerId);\n\t\t\t};\n\n\t\t\thandle.addEventListener('pointerdown', (e: PointerEvent) => {\n\t\t\t\te.preventDefault();\n\t\t\t\te.stopPropagation();\n\n\t\t\t\tconst isRtl: boolean = getTextDirection(figure) === 'rtl';\n\t\t\t\txSign = getResizeXSign(position, isRtl);\n\n\t\t\t\tstartX = e.clientX;\n\t\t\t\tstartWidth = img.getBoundingClientRect().width;\n\t\t\t\tconst imgHeight: number = img.getBoundingClientRect().height;\n\t\t\t\taspectRatio = imgHeight > 0 ? startWidth / imgHeight : 1;\n\n\t\t\t\tfigure.classList.add('notectl-image--resizing');\n\t\t\t\tsizeIndicator.textContent = `${Math.round(startWidth)} \\u00D7 ${Math.round(imgHeight)}`;\n\t\t\t\tsizeIndicator.classList.add('notectl-image__size-indicator--visible');\n\t\t\t\tconst cursor: string = isRtl ? RTL_CURSOR_MAP[position] : HANDLE_CURSORS[position];\n\t\t\t\tsetGlobalResizeCursor(cursor, figure);\n\n\t\t\t\t(e.target as HTMLElement).setPointerCapture(e.pointerId);\n\t\t\t\tdocument.addEventListener('pointermove', onPointerMove);\n\t\t\t\tdocument.addEventListener('pointerup', onPointerUp);\n\t\t\t});\n\t\t}\n\n\t\tfunction createResizeOverlay(nodeId: BlockId): void {\n\t\t\tif (!config.resizable || resizeOverlay) return;\n\n\t\t\tresizeOverlay = document.createElement('div');\n\t\t\tresizeOverlay.className = 'notectl-image__resize-overlay';\n\t\t\tresizeOverlay.setAttribute('data-notectl-no-print', '');\n\n\t\t\tconst sizeIndicator: HTMLDivElement = document.createElement('div');\n\t\t\tsizeIndicator.className = 'notectl-image__size-indicator';\n\t\t\tresizeOverlay.appendChild(sizeIndicator);\n\n\t\t\tconst positions: readonly HandlePosition[] = ['nw', 'ne', 'sw', 'se'];\n\t\t\tfor (const pos of positions) {\n\t\t\t\tconst handle: HTMLDivElement = document.createElement('div');\n\t\t\t\thandle.className = `notectl-image__resize-handle notectl-image__resize-handle--${pos}`;\n\t\t\t\t// Pointer-only visual grips. Keyboard resize is provided via shortcuts\n\t\t\t\t// with live-region announcements (see registerResizeKeymaps), so these\n\t\t\t\t// decorative handles are hidden from assistive technology rather than\n\t\t\t\t// exposed with a misleading `separator` role.\n\t\t\t\thandle.setAttribute('aria-hidden', 'true');\n\t\t\t\tattachHandleListeners(handle, pos, nodeId, sizeIndicator);\n\t\t\t\tresizeOverlay.appendChild(handle);\n\t\t\t}\n\n\t\t\t// Keyboard hint (hidden from screenreaders, visual only)\n\t\t\tif (resolvedKeymap) {\n\t\t\t\tconst hintText: string = buildKeyboardHintText(resolvedKeymap, locale);\n\t\t\t\tif (hintText) {\n\t\t\t\t\tconst hint: HTMLDivElement = document.createElement('div');\n\t\t\t\t\thint.className = 'notectl-image__keyboard-hint';\n\t\t\t\t\thint.setAttribute('aria-hidden', 'true');\n\t\t\t\t\thint.textContent = hintText;\n\t\t\t\t\tresizeOverlay.appendChild(hint);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontainer.appendChild(resizeOverlay);\n\t\t}\n\n\t\tfunction removeResizeOverlay(): void {\n\t\t\tclearGlobalResizeCursor();\n\t\t\tif (resizeOverlay) {\n\t\t\t\tresizeOverlay.remove();\n\t\t\t\tresizeOverlay = null;\n\t\t\t}\n\t\t}\n\n\t\t// --- NodeView Interface ---\n\n\t\treturn {\n\t\t\tdom: figure,\n\t\t\tcontentDOM: null,\n\n\t\t\tupdate(updatedNode: BlockNode): boolean {\n\t\t\t\tif (updatedNode.type !== 'image') return false;\n\t\t\t\tcurrentNodeId = updatedNode.id;\n\t\t\t\tfigure.setAttribute('data-block-id', updatedNode.id);\n\t\t\t\tapplyAttrs(updatedNode);\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\tselectNode(): void {\n\t\t\t\tfigure.classList.add('notectl-image--selected');\n\t\t\t\tcreateResizeOverlay(currentNodeId);\n\t\t\t},\n\n\t\t\tdeselectNode(): void {\n\t\t\t\tfigure.classList.remove('notectl-image--selected');\n\t\t\t\tremoveResizeOverlay();\n\t\t\t},\n\n\t\t\tdestroy(): void {\n\t\t\t\tremoveResizeOverlay();\n\t\t\t\t// Do NOT revoke blob URLs here — the URL may still be referenced\n\t\t\t\t// by the clipboard (cut/paste) or undo history. Blob URLs are\n\t\t\t\t// automatically released when the document is unloaded.\n\t\t\t},\n\t\t};\n\t};\n}\n","/**\n * Renders the image insert popup with file upload and URL input,\n * following the same pattern as FontSizePopup.\n */\n\nimport type { PluginContext } from '../Plugin.js';\nimport { insertImage } from './ImageCommands.js';\nimport type { ImageLocale } from './ImageLocale.js';\n\n// --- Config ---\n\nexport interface ImagePopupConfig {\n\treadonly acceptedTypes: readonly string[];\n\treadonly locale: ImageLocale;\n\treadonly onFileInsert: (context: PluginContext, file: File) => void;\n\treadonly onClose: () => void;\n}\n\n// --- Public Entry Point ---\n\n/** Builds the image insert popup DOM inside the given container. */\nexport function renderImagePopup(\n\tcontainer: HTMLElement,\n\tcontext: PluginContext,\n\tconfig: ImagePopupConfig,\n): void {\n\tcontainer.classList.add('notectl-image-popup');\n\n\tappendFileUploadSection(container, context, config);\n\tappendSeparator(container, config.locale);\n\tappendUrlSection(container, context, config);\n}\n\n// --- File Upload Section ---\n\nfunction appendFileUploadSection(\n\tcontainer: HTMLElement,\n\tcontext: PluginContext,\n\tconfig: ImagePopupConfig,\n): void {\n\tconst fileInput: HTMLInputElement = document.createElement('input');\n\tfileInput.type = 'file';\n\tfileInput.accept = config.acceptedTypes.join(',');\n\tfileInput.className = 'notectl-image-popup__file-input';\n\n\tconst uploadBtn: HTMLButtonElement = document.createElement('button');\n\tuploadBtn.type = 'button';\n\tuploadBtn.textContent = config.locale.uploadFromComputer;\n\tuploadBtn.setAttribute('aria-label', config.locale.uploadAria);\n\tuploadBtn.className = 'notectl-image-popup__upload-btn';\n\n\tuploadBtn.addEventListener('mousedown', (e: MouseEvent) => {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\tfileInput.click();\n\t});\n\n\tfileInput.addEventListener('change', () => {\n\t\tconst file: File | undefined = fileInput.files?.[0];\n\t\tif (file) {\n\t\t\tconfig.onFileInsert(context, file);\n\t\t\tconfig.onClose();\n\t\t\tcontext.getContainer().focus();\n\t\t}\n\t});\n\n\tcontainer.appendChild(fileInput);\n\tcontainer.appendChild(uploadBtn);\n}\n\n// --- Separator ---\n\nfunction appendSeparator(container: HTMLElement, locale: ImageLocale): void {\n\tconst separator: HTMLDivElement = document.createElement('div');\n\tseparator.className = 'notectl-image-popup__separator';\n\n\tconst line1: HTMLSpanElement = document.createElement('span');\n\tline1.className = 'notectl-image-popup__separator-line';\n\n\tconst orText: HTMLSpanElement = document.createElement('span');\n\torText.textContent = locale.separator;\n\torText.className = 'notectl-image-popup__separator-text';\n\n\tconst line2: HTMLSpanElement = document.createElement('span');\n\tline2.className = 'notectl-image-popup__separator-line';\n\n\tseparator.appendChild(line1);\n\tseparator.appendChild(orText);\n\tseparator.appendChild(line2);\n\tcontainer.appendChild(separator);\n}\n\n// --- URL Input Section ---\n\nfunction appendUrlSection(\n\tcontainer: HTMLElement,\n\tcontext: PluginContext,\n\tconfig: ImagePopupConfig,\n): void {\n\tconst urlInput: HTMLInputElement = document.createElement('input');\n\turlInput.type = 'url';\n\turlInput.placeholder = config.locale.urlPlaceholder;\n\turlInput.setAttribute('aria-label', config.locale.urlAria);\n\turlInput.className = 'notectl-image-popup__url-input';\n\n\tconst insertBtn: HTMLButtonElement = document.createElement('button');\n\tinsertBtn.type = 'button';\n\tinsertBtn.textContent = config.locale.insertButton;\n\tinsertBtn.setAttribute('aria-label', config.locale.insertAria);\n\tinsertBtn.className = 'notectl-image-popup__insert-btn';\n\n\tconst applyUrl = (): void => {\n\t\tconst src: string = urlInput.value.trim();\n\t\tif (src) {\n\t\t\tinsertImage(context, { src });\n\t\t\tconfig.onClose();\n\t\t\tcontext.getContainer().focus();\n\t\t}\n\t};\n\n\tinsertBtn.addEventListener('mousedown', (e: MouseEvent) => {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\tapplyUrl();\n\t});\n\n\turlInput.addEventListener('keydown', (e: KeyboardEvent) => {\n\t\tif (e.key === 'Enter') {\n\t\t\te.preventDefault();\n\t\t\tapplyUrl();\n\t\t}\n\t});\n\n\tcontainer.appendChild(urlInput);\n\tcontainer.appendChild(insertBtn);\n\n\trequestAnimationFrame(() => urlInput.focus());\n}\n","/** Image insert popup styles (file upload + URL input). */\nexport const IMAGE_POPUP_CSS = `\n.notectl-image-popup {\n\tpadding: 8px;\n\tmin-width: 240px;\n}\n\n.notectl-image-popup__file-input {\n\tposition: absolute;\n\twidth: 0;\n\theight: 0;\n\toverflow: hidden;\n\topacity: 0;\n}\n\n.notectl-image-popup__upload-btn {\n\tdisplay: block;\n\twidth: 100%;\n\tpadding: 8px 12px;\n\tcursor: pointer;\n\ttext-align: center;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 4px;\n\tbackground: var(--notectl-surface-raised);\n\tcolor: var(--notectl-fg);\n}\n\n.notectl-image-popup__separator {\n\tdisplay: flex;\n\talign-items: center;\n\tmargin: 8px 0;\n\tcolor: var(--notectl-fg-muted);\n\tfont-size: 12px;\n}\n\n.notectl-image-popup__separator-line {\n\tflex: 1;\n\theight: 1px;\n\tbackground: var(--notectl-border);\n}\n\n.notectl-image-popup__separator-text {\n\tpadding: 0 8px;\n}\n\n.notectl-image-popup__url-input {\n\twidth: 100%;\n\tpadding: 6px 8px;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 4px;\n\tbackground: var(--notectl-surface-overlay);\n\tcolor: var(--notectl-fg);\n}\n\n.notectl-image-popup__insert-btn {\n\twidth: 100%;\n\tpadding: 8px 12px;\n\tmargin-top: 4px;\n\tcursor: pointer;\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 4px;\n\tbackground: var(--notectl-surface-raised);\n\tcolor: var(--notectl-fg);\n}\n`;\n","/**\n * Image plugin types, configuration, and service key.\n */\n\nimport { ServiceKey } from '../Plugin.js';\nimport type { ImageLocale } from './ImageLocale.js';\n\n// --- Image Attributes ---\n\nexport interface ImageAttrs {\n\treadonly src: string;\n\treadonly alt: string;\n\treadonly width?: number;\n\treadonly height?: number;\n\treadonly align: 'start' | 'center' | 'end';\n}\n\n// --- Upload Types ---\n\nexport interface ImageUploadResult {\n\treadonly url: string;\n\treadonly width?: number;\n\treadonly height?: number;\n}\n\nexport interface ImageUploadService {\n\tupload(file: File): Promise<ImageUploadResult>;\n}\n\nexport const IMAGE_UPLOAD_SERVICE = new ServiceKey<ImageUploadService>('image:upload');\n\nexport type UploadState = 'idle' | 'uploading' | 'complete' | 'error';\n\n// --- Keyboard Bindings ---\n\n/**\n * Configurable keyboard bindings for image resize actions.\n * Omit a slot to use the default; set to `null` to disable the binding.\n *\n * Key descriptor format: `'Mod-Shift-ArrowRight'`, etc.\n * `Mod` resolves to Cmd on macOS, Ctrl on Windows/Linux.\n */\nexport interface ImageKeymap {\n\treadonly growWidth?: string | null;\n\treadonly shrinkWidth?: string | null;\n\treadonly growWidthLarge?: string | null;\n\treadonly shrinkWidthLarge?: string | null;\n\treadonly resetSize?: string | null;\n}\n\nexport const DEFAULT_IMAGE_KEYMAP: Readonly<Record<keyof ImageKeymap, string>> = {\n\tgrowWidth: 'Mod-Shift-ArrowRight',\n\tshrinkWidth: 'Mod-Shift-ArrowLeft',\n\tgrowWidthLarge: 'Mod-Shift-Alt-ArrowRight',\n\tshrinkWidthLarge: 'Mod-Shift-Alt-ArrowLeft',\n\tresetSize: 'Mod-Shift-0',\n};\n\n// --- Configuration ---\n\nexport interface ImagePluginConfig {\n\treadonly maxWidth: number;\n\treadonly maxFileSize: number;\n\treadonly acceptedTypes: readonly string[];\n\treadonly resizable: boolean;\n\t/** Pixels to grow/shrink per small resize step. @default 10 */\n\treadonly resizeStep?: number;\n\t/** Pixels to grow/shrink per large resize step. @default 50 */\n\treadonly resizeStepLarge?: number;\n\t/** Customize keyboard bindings for image resize actions. */\n\treadonly keymap?: ImageKeymap;\n\t/** Locale override for user-facing strings. */\n\treadonly locale?: ImageLocale;\n}\n\nexport const DEFAULT_IMAGE_CONFIG: ImagePluginConfig = {\n\tmaxWidth: 800,\n\tmaxFileSize: 10 * 1024 * 1024,\n\tacceptedTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'],\n\tresizable: true,\n\tresizeStep: 10,\n\tresizeStepLarge: 50,\n};\n","/**\n * ImagePlugin: registers an image void block type with NodeSpec,\n * NodeView, commands, file handler, toolbar button, and accessible\n * keyboard resize with screenreader announcements.\n */\n\nimport { IMAGE_CSS } from '../../editor/styles/image.js';\nimport type { BlockAttrs, BlockNode } from '../../model/Document.js';\nimport { escapeHTML } from '../../model/HTMLUtils.js';\nimport { isNodeSelection } from '../../model/Selection.js';\nimport type { BlockId } from '../../model/TypeBrands.js';\nimport type { EditorState } from '../../state/EditorState.js';\nimport type { Transaction } from '../../state/Transaction.js';\nimport { setStyleProperty } from '../../style/StyleRuntime.js';\nimport { createBlockElement } from '../../view/DomUtils.js';\nimport type { Plugin, PluginContext } from '../Plugin.js';\nimport { resolveLocale } from '../shared/PluginHelpers.js';\nimport { formatShortcut } from '../shared/ShortcutFormatting.js';\nimport {\n\tinsertImage,\n\tregisterImageCommands,\n\tresetImageSize,\n\tresizeImageByDelta,\n} from './ImageCommands.js';\nimport { IMAGE_LOCALE_EN, type ImageLocale, loadImageLocale } from './ImageLocale.js';\nimport { createImageNodeViewFactory } from './ImageNodeView.js';\nimport { renderImagePopup } from './ImagePopup.js';\nimport { IMAGE_POPUP_CSS } from './ImagePopupStyles.js';\nimport {\n\tDEFAULT_IMAGE_CONFIG,\n\tDEFAULT_IMAGE_KEYMAP,\n\tIMAGE_UPLOAD_SERVICE,\n\ttype ImageKeymap,\n\ttype ImagePluginConfig,\n\ttype UploadState,\n} from './ImageUpload.js';\n\n// --- Attribute Registry Augmentation ---\n\ndeclare module '../../model/AttrRegistry.js' {\n\tinterface NodeAttrRegistry {\n\t\timage: {\n\t\t\tsrc: string;\n\t\t\talt: string;\n\t\t\twidth?: number;\n\t\t\theight?: number;\n\t\t\talign: 'start' | 'center' | 'end';\n\t\t};\n\t}\n}\n\n// --- Plugin ---\n\nexport class ImagePlugin implements Plugin {\n\treadonly id = 'image';\n\treadonly name = 'Image';\n\treadonly priority = 45;\n\n\tprivate readonly config: ImagePluginConfig;\n\tprivate readonly resolvedKeymap: Readonly<Record<keyof ImageKeymap, string | null>>;\n\tprivate readonly uploadStates = new Map<BlockId, UploadState>();\n\tprivate readonly blobUrls = new Set<string>();\n\tprivate context: PluginContext | null = null;\n\tprivate locale!: ImageLocale;\n\n\tconstructor(config?: Partial<ImagePluginConfig>) {\n\t\tthis.config = { ...DEFAULT_IMAGE_CONFIG, ...config };\n\t\tthis.resolvedKeymap = { ...DEFAULT_IMAGE_KEYMAP, ...config?.keymap };\n\t}\n\n\tasync init(context: PluginContext): Promise<void> {\n\t\tthis.locale = await resolveLocale(\n\t\t\tcontext,\n\t\t\tthis.config.locale,\n\t\t\tIMAGE_LOCALE_EN,\n\t\t\tloadImageLocale,\n\t\t);\n\t\tcontext.registerStyleSheet(IMAGE_CSS);\n\t\tcontext.registerStyleSheet(IMAGE_POPUP_CSS);\n\t\tthis.context = context;\n\t\tthis.registerNodeSpec(context);\n\t\tthis.registerNodeView(context);\n\t\tregisterImageCommands(context);\n\t\tthis.registerResizeCommands(context);\n\t\tthis.registerResizeKeymaps(context);\n\t\tthis.registerFileHandler(context);\n\t\tthis.registerToolbarItem(context);\n\t}\n\n\tdestroy(): void {\n\t\tfor (const url of this.blobUrls) {\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t\tthis.blobUrls.clear();\n\t\tthis.uploadStates.clear();\n\t\tthis.context = null;\n\t}\n\n\tonStateChange(oldState: EditorState, newState: EditorState, _tr: Transaction): void {\n\t\tif (!this.context) return;\n\n\t\t// Clean up upload states for removed blocks (searches full tree)\n\t\tfor (const id of this.uploadStates.keys()) {\n\t\t\tif (!newState.getBlock(id)) {\n\t\t\t\tthis.uploadStates.delete(id);\n\t\t\t}\n\t\t}\n\n\t\t// Announce image selection for screenreaders\n\t\tconst oldIsImage: boolean = this.isImageSelected(oldState);\n\t\tconst nowIsImage: boolean = this.isImageSelected(newState);\n\n\t\tif (!oldIsImage && nowIsImage) {\n\t\t\tthis.announceImageSelection(newState);\n\t\t}\n\t}\n\n\tprivate registerNodeSpec(context: PluginContext): void {\n\t\tconst locale = this.locale;\n\t\tcontext.registerNodeSpec({\n\t\t\ttype: 'image',\n\t\t\tgroup: 'block',\n\t\t\tisVoid: true,\n\t\t\tselectable: true,\n\t\t\tattrs: {\n\t\t\t\tsrc: { default: '' },\n\t\t\t\talt: { default: '' },\n\t\t\t\talign: { default: 'center' },\n\t\t\t},\n\t\t\ttoDOM(node) {\n\t\t\t\tconst figure = createBlockElement('figure', node.id);\n\t\t\t\tfigure.className = 'notectl-image';\n\t\t\t\tfigure.setAttribute('data-void', 'true');\n\t\t\t\tfigure.setAttribute('data-selectable', 'true');\n\n\t\t\t\tconst imgContainer: HTMLDivElement = document.createElement('div');\n\t\t\t\timgContainer.className = 'notectl-image__container';\n\n\t\t\t\tconst alt: string = (node.attrs?.alt as string | undefined) ?? '';\n\t\t\t\tconst width: number | undefined = node.attrs?.width as number | undefined;\n\t\t\t\tconst height: number | undefined = node.attrs?.height as number | undefined;\n\n\t\t\t\tconst img: HTMLImageElement = document.createElement('img');\n\t\t\t\timg.className = 'notectl-image__img';\n\t\t\t\timg.src = (node.attrs?.src as string | undefined) ?? '';\n\t\t\t\timg.alt = alt;\n\t\t\t\timg.draggable = false;\n\n\t\t\t\tif (width !== undefined) setStyleProperty(img, 'width', `${width}px`);\n\t\t\t\tif (height !== undefined) setStyleProperty(img, 'height', `${height}px`);\n\n\t\t\t\tconst align: string = (node.attrs?.align as string | undefined) ?? 'center';\n\t\t\t\tconst alignClass: string | undefined = {\n\t\t\t\t\tstart: 'notectl-image--start',\n\t\t\t\t\tcenter: 'notectl-image--center',\n\t\t\t\t\tend: 'notectl-image--end',\n\t\t\t\t}[align];\n\t\t\t\tif (alignClass) figure.classList.add(alignClass);\n\n\t\t\t\tfigure.setAttribute('aria-label', locale.imageAria(alt, width, height));\n\n\t\t\t\timgContainer.appendChild(img);\n\t\t\t\tfigure.appendChild(imgContainer);\n\t\t\t\treturn figure;\n\t\t\t},\n\t\t\ttoHTML(node) {\n\t\t\t\tconst src: string = escapeHTML((node.attrs?.src as string | undefined) ?? '');\n\t\t\t\tconst alt: string = escapeHTML((node.attrs?.alt as string | undefined) ?? '');\n\t\t\t\tconst width: number | undefined = node.attrs?.width as number | undefined;\n\t\t\t\tconst height: number | undefined = node.attrs?.height as number | undefined;\n\n\t\t\t\tconst sizeAttrs: string =\n\t\t\t\t\t(width !== undefined ? ` width=\"${width}\"` : '') +\n\t\t\t\t\t(height !== undefined ? ` height=\"${height}\"` : '');\n\n\t\t\t\t// Alignment is handled by the serializer's alignment injection,\n\t\t\t\t// which works in both inline-style and CSS-class modes.\n\t\t\t\treturn `<figure><img src=\"${src}\" alt=\"${alt}\"${sizeAttrs}></figure>`;\n\t\t\t},\n\t\t\tparseHTML: [\n\t\t\t\t{\n\t\t\t\t\ttag: 'figure',\n\t\t\t\t\tgetAttrs(el) {\n\t\t\t\t\t\tconst img: HTMLImageElement | null = el.querySelector('img');\n\t\t\t\t\t\tif (!img) return false;\n\t\t\t\t\t\tconst attrs: Record<string, string | number | boolean> = {\n\t\t\t\t\t\t\tsrc: img.getAttribute('src') ?? '',\n\t\t\t\t\t\t\talt: img.getAttribute('alt') ?? '',\n\t\t\t\t\t\t\talign: 'center',\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst width: string | null = img.getAttribute('width');\n\t\t\t\t\t\tconst height: string | null = img.getAttribute('height');\n\t\t\t\t\t\tif (width) attrs.width = Number.parseInt(width, 10);\n\t\t\t\t\t\tif (height) attrs.height = Number.parseInt(height, 10);\n\n\t\t\t\t\t\t// Check inline style first, then class names (new + legacy)\n\t\t\t\t\t\tconst textAlign: string = el.style?.textAlign ?? '';\n\t\t\t\t\t\tif (textAlign === 'start' || textAlign === 'end' || textAlign === 'center') {\n\t\t\t\t\t\t\tattrs.align = textAlign;\n\t\t\t\t\t\t} else if (textAlign === 'left') {\n\t\t\t\t\t\t\tattrs.align = 'start';\n\t\t\t\t\t\t} else if (textAlign === 'right') {\n\t\t\t\t\t\t\tattrs.align = 'end';\n\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-start') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-left') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--start') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--left')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tattrs.align = 'start';\n\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-end') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-right') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--end') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--right')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tattrs.align = 'end';\n\t\t\t\t\t\t} else if (el.classList.contains('notectl-align-center')) {\n\t\t\t\t\t\t\tattrs.align = 'center';\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn attrs;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttag: 'img',\n\t\t\t\t\tgetAttrs(el) {\n\t\t\t\t\t\tconst attrs: Record<string, string | number | boolean> = {\n\t\t\t\t\t\t\tsrc: el.getAttribute('src') ?? '',\n\t\t\t\t\t\t\talt: el.getAttribute('alt') ?? '',\n\t\t\t\t\t\t\talign: 'center',\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst width: string | null = el.getAttribute('width');\n\t\t\t\t\t\tconst height: string | null = el.getAttribute('height');\n\t\t\t\t\t\tif (width) attrs.width = Number.parseInt(width, 10);\n\t\t\t\t\t\tif (height) attrs.height = Number.parseInt(height, 10);\n\t\t\t\t\t\treturn attrs;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\tsanitize: {\n\t\t\t\ttags: ['figure', 'img'],\n\t\t\t\tattrs: ['src', 'alt', 'width', 'height', 'class', 'style'],\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate registerNodeView(context: PluginContext): void {\n\t\tcontext.registerNodeView(\n\t\t\t'image',\n\t\t\tcreateImageNodeViewFactory(this.config, this.uploadStates, this.resolvedKeymap, this.locale),\n\t\t);\n\t}\n\n\tprivate registerResizeCommands(context: PluginContext): void {\n\t\tconst step: number = this.config.resizeStep ?? 10;\n\t\tconst stepLarge: number = this.config.resizeStepLarge ?? 50;\n\t\tconst maxWidth: number = this.config.maxWidth;\n\n\t\tcontext.registerCommand('resizeImageGrow', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, step, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resizeImageShrink', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, -step, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resizeImageGrowLarge', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, stepLarge, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resizeImageShrinkLarge', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, -stepLarge, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resetImageSize', () => {\n\t\t\tconst result: boolean = resetImageSize(context);\n\t\t\tif (result) context.announce(this.locale.resetToNaturalSize);\n\t\t\treturn result;\n\t\t});\n\t}\n\n\tprivate registerResizeKeymaps(context: PluginContext): void {\n\t\tconst bindings: Record<string, () => boolean> = {};\n\t\tconst commands: Record<keyof ImageKeymap, string> = {\n\t\t\tgrowWidth: 'resizeImageGrow',\n\t\t\tshrinkWidth: 'resizeImageShrink',\n\t\t\tgrowWidthLarge: 'resizeImageGrowLarge',\n\t\t\tshrinkWidthLarge: 'resizeImageShrinkLarge',\n\t\t\tresetSize: 'resetImageSize',\n\t\t};\n\n\t\tfor (const [slot, commandName] of Object.entries(commands)) {\n\t\t\tconst binding: string | null = this.resolvedKeymap[slot as keyof ImageKeymap] ?? null;\n\t\t\tif (binding) {\n\t\t\t\tbindings[binding] = () => context.executeCommand(commandName);\n\t\t\t}\n\t\t}\n\n\t\tif (Object.keys(bindings).length > 0) {\n\t\t\tcontext.registerKeymap(bindings);\n\t\t}\n\t}\n\n\tprivate registerFileHandler(context: PluginContext): void {\n\t\tcontext.registerFileHandler('image/*', async (file, _position) => {\n\t\t\tif (!this.isAcceptedType(file.type)) return false;\n\t\t\tthis.handleFileInsert(context, file);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tprivate handleFileInsert(context: PluginContext, file: File): void {\n\t\tif (file.size > this.config.maxFileSize) return;\n\n\t\tconst blobUrl: string = URL.createObjectURL(file);\n\t\tthis.blobUrls.add(blobUrl);\n\n\t\tconst inserted: boolean = insertImage(context, { src: blobUrl });\n\t\tif (!inserted) {\n\t\t\tURL.revokeObjectURL(blobUrl);\n\t\t\tthis.blobUrls.delete(blobUrl);\n\t\t\treturn;\n\t\t}\n\n\t\t// Find the newly inserted image block\n\t\tconst state: EditorState = context.getState();\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return;\n\t\tconst imageBlockId: BlockId = sel.nodeId;\n\n\t\tthis.uploadStates.set(imageBlockId, 'uploading');\n\n\t\t// Upload if service is registered\n\t\tconst uploadService = context.getService(IMAGE_UPLOAD_SERVICE);\n\t\tif (uploadService) {\n\t\t\tthis.uploadFile(context, file, imageBlockId, blobUrl);\n\t\t} else {\n\t\t\tthis.uploadStates.set(imageBlockId, 'complete');\n\t\t}\n\t}\n\n\tprivate async uploadFile(\n\t\tcontext: PluginContext,\n\t\tfile: File,\n\t\timageBlockId: BlockId,\n\t\tblobUrl: string,\n\t): Promise<void> {\n\t\tconst uploadService = context.getService(IMAGE_UPLOAD_SERVICE);\n\t\tif (!uploadService) return;\n\n\t\ttry {\n\t\t\tconst result = await uploadService.upload(file);\n\t\t\tthis.uploadStates.set(imageBlockId, 'complete');\n\n\t\t\t// Replace blob URL with uploaded URL\n\t\t\tconst state: EditorState = context.getState();\n\t\t\tconst block: BlockNode | undefined = state.getBlock(imageBlockId);\n\t\t\tif (!block) return;\n\n\t\t\tconst path: BlockId[] | undefined = state.getNodePath(imageBlockId);\n\t\t\tif (!path) return;\n\n\t\t\tconst merged: BlockAttrs = {\n\t\t\t\t...(block.attrs ?? {}),\n\t\t\t\tsrc: result.url,\n\t\t\t\t...(result.width !== undefined ? { width: result.width } : {}),\n\t\t\t\t...(result.height !== undefined ? { height: result.height } : {}),\n\t\t\t};\n\n\t\t\tconst tr: Transaction = state.transaction('command').setNodeAttr(path, merged).build();\n\t\t\tcontext.dispatch(tr);\n\n\t\t\t// Clean up blob URL\n\t\t\tURL.revokeObjectURL(blobUrl);\n\t\t\tthis.blobUrls.delete(blobUrl);\n\t\t} catch {\n\t\t\tthis.uploadStates.set(imageBlockId, 'error');\n\t\t\tcontext.announce(this.locale.uploadFailed);\n\t\t}\n\t}\n\n\tprivate registerToolbarItem(context: PluginContext): void {\n\t\tconst icon =\n\t\t\t'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z\"/></svg>';\n\n\t\tcontext.registerToolbarItem({\n\t\t\tid: 'image',\n\t\t\tgroup: 'insert',\n\t\t\ticon,\n\t\t\tlabel: this.locale.insertImage,\n\t\t\ttooltip: this.locale.insertImageTooltip,\n\t\t\tcommand: 'insertImage',\n\t\t\tpopupType: 'custom',\n\t\t\trenderPopup: (container, ctx, onClose) => {\n\t\t\t\trenderImagePopup(container, ctx, {\n\t\t\t\t\tacceptedTypes: this.config.acceptedTypes,\n\t\t\t\t\tlocale: this.locale,\n\t\t\t\t\tonFileInsert: (c, file) => this.handleFileInsert(c, file),\n\t\t\t\t\tonClose,\n\t\t\t\t});\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate isAcceptedType(mimeType: string): boolean {\n\t\treturn this.config.acceptedTypes.some(\n\t\t\t(accepted) =>\n\t\t\t\taccepted === mimeType ||\n\t\t\t\t(accepted.endsWith('/*') && mimeType.startsWith(accepted.slice(0, -1))),\n\t\t);\n\t}\n\n\tprivate isImageSelected(state: EditorState): boolean {\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return false;\n\t\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\t\treturn block?.type === 'image';\n\t}\n\n\tprivate announceImageSelection(state: EditorState): void {\n\t\tif (!this.context) return;\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return;\n\n\t\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\t\tif (!block || block.type !== 'image') return;\n\n\t\tconst alt: string = (block.attrs?.alt as string | undefined) ?? '';\n\t\tconst width: number | undefined = block.attrs?.width as number | undefined;\n\t\tconst height: number | undefined = block.attrs?.height as number | undefined;\n\n\t\tconst parts: string[] = [this.locale.imageSelected];\n\t\tif (alt) parts.push(`${this.locale.altTextPrefix}${alt}.`);\n\t\tif (width !== undefined && height !== undefined) {\n\t\t\tparts.push(this.locale.imageSizeAnnounce(width, height));\n\t\t}\n\n\t\tconst shrinkKey: string | null = this.resolvedKeymap.shrinkWidth ?? null;\n\t\tconst growKey: string | null = this.resolvedKeymap.growWidth ?? null;\n\t\tif (shrinkKey && growKey) {\n\t\t\tparts.push(this.locale.resizeHint(formatShortcut(shrinkKey), formatShortcut(growKey)));\n\t\t}\n\n\t\tthis.context.announce(parts.join(' '));\n\t}\n\n\tprivate announceCurrentSize(context: PluginContext): void {\n\t\tconst state = context.getState();\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return;\n\n\t\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\t\tif (!block || block.type !== 'image') return;\n\n\t\tconst width: number | undefined = block.attrs?.width as number | undefined;\n\t\tconst height: number | undefined = block.attrs?.height as number | undefined;\n\t\tif (width !== undefined && height !== undefined) {\n\t\t\tcontext.announce(this.locale.imageResized(width, height));\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImagePlugin-KpkuE2LH.mjs","names":[],"sources":["../../src/editor/styles/image.ts","../../src/plugins/image/ImageCommands.ts","../../src/plugins/image/ImageLocale.ts","../../src/plugins/image/ImageNodeView.ts","../../src/plugins/image/ImagePopup.ts","../../src/plugins/image/ImagePopupStyles.ts","../../src/plugins/image/ImageUpload.ts","../../src/plugins/image/ImagePlugin.ts"],"sourcesContent":["/** Image styles — display, alignment, resize handles, upload overlay. */\nexport const IMAGE_CSS = `\n/* Image */\n.notectl-image {\n\tmargin: 8px 0;\n\tuser-select: none;\n\tline-height: 0;\n}\n\n.notectl-image--center {\n\ttext-align: center;\n}\n\n.notectl-image--start {\n\ttext-align: start;\n}\n\n.notectl-image--end {\n\ttext-align: end;\n}\n\n.notectl-image__container {\n\tdisplay: inline-block;\n\tposition: relative;\n\tline-height: 0;\n}\n\n.notectl-image__img {\n\tdisplay: block;\n\tmax-width: 100%;\n\tborder-radius: 4px;\n}\n\n.notectl-image--selected .notectl-image__container {\n\toutline: 2px solid var(--notectl-primary);\n\toutline-offset: 2px;\n\tborder-radius: 6px;\n}\n\n/* Alignment toolbar (appears on image selection) */\n.notectl-image__align-toolbar {\n\tdisplay: flex;\n\tgap: 2px;\n\tposition: absolute;\n\ttop: -36px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tbackground: var(--notectl-surface-overlay);\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 6px;\n\tpadding: 2px;\n\tbox-shadow: 0 2px 8px var(--notectl-shadow);\n\tz-index: 10;\n}\n\n.notectl-image__align-btn {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 28px;\n\theight: 28px;\n\tborder: none;\n\tbackground: transparent;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\tcolor: var(--notectl-fg);\n}\n\n.notectl-image__align-btn:hover {\n\tbackground: var(--notectl-hover-bg);\n}\n\n.notectl-image__align-btn--active {\n\tbackground: var(--notectl-active-bg);\n\tcolor: var(--notectl-primary-fg);\n}\n\n.notectl-image__align-btn svg {\n\twidth: 16px;\n\theight: 16px;\n\tfill: currentColor;\n}\n\n/* Upload overlay */\n.notectl-image__overlay {\n\tposition: absolute;\n\tinset: 0;\n\tdisplay: none;\n\talign-items: center;\n\tjustify-content: center;\n\tbackground: rgba(255, 255, 255, 0.8);\n\tborder-radius: 4px;\n\tfont-size: 13px;\n\tcolor: var(--notectl-fg-muted);\n}\n\n.notectl-image__overlay--uploading {\n\tdisplay: flex;\n}\n\n.notectl-image__overlay--error {\n\tdisplay: flex;\n\tbackground: var(--notectl-danger-muted);\n\tcolor: var(--notectl-danger);\n}\n\n/* Resize overlay + handles */\n.notectl-image__resize-overlay {\n\tposition: absolute;\n\tinset: 0;\n\tpointer-events: none;\n}\n\n.notectl-image__resize-handle {\n\tposition: absolute;\n\twidth: 10px;\n\theight: 10px;\n\tbackground: var(--notectl-surface-raised);\n\tborder: 2px solid var(--notectl-primary);\n\tborder-radius: 50%;\n\tpointer-events: all;\n\tz-index: 2;\n\ttransition: transform 0.1s, background 0.1s;\n}\n\n.notectl-image__resize-handle:hover {\n\tbackground: var(--notectl-primary);\n\ttransform: scale(1.3);\n}\n\n.notectl-image--resizing .notectl-image__resize-handle {\n\tbackground: var(--notectl-primary);\n}\n\n.notectl-image__resize-handle--nw {\n\ttop: -5px;\n\tinset-inline-start: -5px;\n\tcursor: nwse-resize;\n}\n\n.notectl-image__resize-handle--ne {\n\ttop: -5px;\n\tinset-inline-end: -5px;\n\tcursor: nesw-resize;\n}\n\n.notectl-image__resize-handle--sw {\n\tbottom: -5px;\n\tinset-inline-start: -5px;\n\tcursor: nesw-resize;\n}\n\n.notectl-image__resize-handle--se {\n\tbottom: -5px;\n\tinset-inline-end: -5px;\n\tcursor: nwse-resize;\n}\n\n/* RTL cursor overrides — flip diagonal cursors when in RTL context */\n[dir=\"rtl\"] .notectl-image__resize-handle--nw {\n\tcursor: nesw-resize;\n}\n\n[dir=\"rtl\"] .notectl-image__resize-handle--ne {\n\tcursor: nwse-resize;\n}\n\n[dir=\"rtl\"] .notectl-image__resize-handle--sw {\n\tcursor: nwse-resize;\n}\n\n[dir=\"rtl\"] .notectl-image__resize-handle--se {\n\tcursor: nesw-resize;\n}\n\n/* Size indicator tooltip */\n.notectl-image__size-indicator {\n\tposition: absolute;\n\tbottom: -28px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tpadding: 2px 8px;\n\tbackground: var(--notectl-tooltip-bg);\n\tcolor: var(--notectl-tooltip-fg);\n\tfont-size: 11px;\n\tfont-weight: 500;\n\tborder-radius: 4px;\n\twhite-space: nowrap;\n\tpointer-events: none;\n\topacity: 0;\n\ttransition: opacity 0.15s;\n\tz-index: 3;\n}\n\n.notectl-image__size-indicator--visible {\n\topacity: 1;\n}\n\n/* Keyboard resize hint (shown on selection, hidden during drag) */\n.notectl-image__keyboard-hint {\n\tdisplay: none;\n\tposition: absolute;\n\tbottom: -28px;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\tpadding: 2px 8px;\n\tfont-size: 11px;\n\tcolor: var(--notectl-fg-muted);\n\twhite-space: nowrap;\n\tpointer-events: none;\n\tz-index: 3;\n}\n\n.notectl-image--selected .notectl-image__keyboard-hint {\n\tdisplay: block;\n}\n\n.notectl-image--resizing .notectl-image__keyboard-hint {\n\tdisplay: none;\n}\n\n/* Resizing feedback */\n.notectl-image--resizing .notectl-image__img {\n\topacity: 0.85;\n}\n`;\n","/**\n * Image plugin commands: insert, remove, and attribute updates.\n * Supports both root-level and nested contexts (e.g. images inside table cells).\n */\n\nimport { insertBlockObjectOnOwnLine } from '../../commands/BlockInsertion.js';\nimport { deleteNodeSelection } from '../../commands/NodeSelectionCommands.js';\nimport type { BlockAttrs, BlockNode } from '../../model/Document.js';\nimport { createBlockNode, getBlockChildren, isBlockNode } from '../../model/Document.js';\nimport { createNodeSelection, isGapCursor, isNodeSelection } from '../../model/Selection.js';\nimport type { BlockId } from '../../model/TypeBrands.js';\nimport { nodeType } from '../../model/TypeBrands.js';\nimport type { EditorState } from '../../state/EditorState.js';\nimport type { PluginContext } from '../Plugin.js';\nimport type { ImageAttrs } from './ImageUpload.js';\n\n/**\n * Inserts an image block. When the cursor is inside a table cell,\n * the image is inserted as a child of the cell. Otherwise it is\n * inserted after the current block at document root.\n */\nexport function insertImage(\n\tcontext: PluginContext,\n\tattrs: Partial<ImageAttrs> & { readonly src: string },\n): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\n\tconst imageAttrs: BlockAttrs = {\n\t\tsrc: attrs.src,\n\t\talt: attrs.alt ?? '',\n\t\talign: attrs.align ?? 'center',\n\t\t...(attrs.width !== undefined ? { width: attrs.width } : {}),\n\t\t...(attrs.height !== undefined ? { height: attrs.height } : {}),\n\t};\n\n\tif (isGapCursor(sel)) return false;\n\n\tconst anchorBlockId: BlockId = isNodeSelection(sel) ? sel.nodeId : sel.anchor.blockId;\n\n\t// Check if we're inside a table cell\n\tconst cellId: BlockId | undefined = findTableCellAncestor(state, anchorBlockId);\n\tif (cellId) {\n\t\treturn insertImageIntoCell(state, context, cellId, imageAttrs);\n\t}\n\n\treturn insertImageAtRoot(state, context, anchorBlockId, imageAttrs);\n}\n\n/** Finds a table_cell ancestor for the given block (or the block itself). */\nfunction findTableCellAncestor(state: EditorState, bid: BlockId): BlockId | undefined {\n\tconst block: BlockNode | undefined = state.getBlock(bid);\n\tif (block?.type === 'table_cell') return bid;\n\n\tconst path: BlockId[] | undefined = state.getNodePath(bid);\n\tif (!path) return undefined;\n\n\tfor (const id of path) {\n\t\tconst node: BlockNode | undefined = state.getBlock(id);\n\t\tif (node?.type === 'table_cell') return id;\n\t}\n\treturn undefined;\n}\n\n/** Inserts an image on its own line at document root, after the anchor block. */\nfunction insertImageAtRoot(\n\tstate: EditorState,\n\tcontext: PluginContext,\n\tanchorBlockId: BlockId,\n\timageAttrs: BlockAttrs,\n): boolean {\n\tconst imageBlock: BlockNode = createBlockNode(nodeType('image'), [], undefined, imageAttrs);\n\tconst builder = state.transaction('command');\n\tconst trailing = insertBlockObjectOnOwnLine(state, builder, anchorBlockId, imageBlock);\n\tif (!trailing) return false;\n\n\tbuilder.setSelection(createNodeSelection(imageBlock.id, []));\n\tcontext.dispatch(builder.build());\n\treturn true;\n}\n\n/** Inserts an image as a child of a table cell. */\nfunction insertImageIntoCell(\n\tstate: EditorState,\n\tcontext: PluginContext,\n\tcellId: BlockId,\n\timageAttrs: BlockAttrs,\n): boolean {\n\tconst cellPath: BlockId[] | undefined = state.getNodePath(cellId);\n\tif (!cellPath) return false;\n\n\tconst imageBlock: BlockNode = createBlockNode(nodeType('image'), [], undefined, imageAttrs);\n\n\t// Remove existing block children first (if any)\n\tconst cell: BlockNode | undefined = state.getBlock(cellId);\n\tif (!cell) return false;\n\n\tconst builder = state.transaction('command');\n\tconst blockChildren: readonly BlockNode[] = getBlockChildren(cell);\n\tfor (let i = blockChildren.length - 1; i >= 0; i--) {\n\t\t// Find the raw index in children array\n\t\tconst rawIndex: number = cell.children.findIndex(\n\t\t\t(c) => isBlockNode(c) && c.id === blockChildren[i]?.id,\n\t\t);\n\t\tif (rawIndex !== -1) {\n\t\t\tbuilder.removeNode(cellPath, rawIndex);\n\t\t}\n\t}\n\n\t// Insert image at index 0 (before any text nodes)\n\tbuilder.insertNode(cellPath, 0, imageBlock);\n\tbuilder.setSelection(createNodeSelection(imageBlock.id, [...cellPath, imageBlock.id]));\n\n\tcontext.dispatch(builder.build());\n\treturn true;\n}\n\n/** Removes the currently selected image block (works for nested images too). */\nexport function removeImage(context: PluginContext): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst nodePath: BlockId[] | undefined = state.getNodePath(sel.nodeId);\n\tif (!nodePath || nodePath.length === 0) return false;\n\n\tconst tr = deleteNodeSelection(state, createNodeSelection(sel.nodeId, nodePath));\n\tif (!tr) return false;\n\tcontext.dispatch(tr);\n\treturn true;\n}\n\n/** Updates attributes on the currently selected image block. */\nexport function setImageAttr(context: PluginContext, attrs: Partial<ImageAttrs>): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst path: BlockId[] | undefined = state.getNodePath(sel.nodeId);\n\tif (!path) return false;\n\n\tconst merged: BlockAttrs = { ...(block.attrs ?? {}), ...attrs };\n\tconst tr = state.transaction('command').setNodeAttr(path, merged).build();\n\n\tcontext.dispatch(tr);\n\treturn true;\n}\n\n/** Minimum image width in pixels (shared with NodeView). */\nexport const MIN_IMAGE_WIDTH = 50;\n\n/**\n * Resizes the currently selected image by a pixel delta, preserving aspect ratio.\n * Returns false if no image is selected or the image has no width/height attributes.\n */\nexport function resizeImageByDelta(\n\tcontext: PluginContext,\n\tdelta: number,\n\tmaxWidth: number,\n): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst currentWidth: number | undefined = block.attrs?.width as number | undefined;\n\tconst currentHeight: number | undefined = block.attrs?.height as number | undefined;\n\tif (currentWidth === undefined || currentHeight === undefined) return false;\n\n\tconst aspectRatio: number = currentHeight > 0 ? currentWidth / currentHeight : 1;\n\tconst clampedWidth: number = Math.max(MIN_IMAGE_WIDTH, Math.min(maxWidth, currentWidth + delta));\n\tconst newHeight: number = Math.round(clampedWidth / aspectRatio);\n\n\tif (clampedWidth === currentWidth && newHeight === currentHeight) return false;\n\n\treturn setImageAttr(context, { width: clampedWidth, height: newHeight });\n}\n\n/** Resets the currently selected image to its natural size by removing width/height. */\nexport function resetImageSize(context: PluginContext): boolean {\n\tconst state = context.getState();\n\tconst sel = state.selection;\n\tif (!isNodeSelection(sel)) return false;\n\n\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\tif (!block || block.type !== 'image') return false;\n\n\tconst path: BlockId[] | undefined = state.getNodePath(sel.nodeId);\n\tif (!path) return false;\n\n\tconst attrs: BlockAttrs = { ...(block.attrs ?? {}) };\n\tconst cleaned: BlockAttrs = Object.fromEntries(\n\t\tObject.entries(attrs).filter(([k]) => k !== 'width' && k !== 'height'),\n\t);\n\n\tconst tr = state.transaction('command').setNodeAttr(path, cleaned).build();\n\tcontext.dispatch(tr);\n\treturn true;\n}\n\n/** Registers all image commands on the plugin context. */\nexport function registerImageCommands(context: PluginContext): void {\n\tcontext.registerCommand('insertImage', () => {\n\t\treturn insertImage(context, { src: '' });\n\t});\n\n\tcontext.registerCommand('removeImage', () => {\n\t\treturn removeImage(context);\n\t});\n}\n","/** Locale interface and default English locale for the ImagePlugin. */\n\nimport { type LocaleModuleMap, loadLocaleModule } from '../shared/LocaleLoader.js';\n\n// --- Locale Interface ---\n\nexport interface ImageLocale {\n\treadonly insertImage: string;\n\treadonly insertImageTooltip: string;\n\treadonly uploadFromComputer: string;\n\treadonly uploadAria: string;\n\treadonly separator: string;\n\treadonly urlPlaceholder: string;\n\treadonly urlAria: string;\n\treadonly insertButton: string;\n\treadonly insertAria: string;\n\treadonly imageAria: (alt: string, width?: number, height?: number) => string;\n\treadonly resetToNaturalSize: string;\n\treadonly uploadFailed: string;\n\treadonly uploading: string;\n\treadonly resizeHandleLabel: (position: string) => string;\n\treadonly imageSelected: string;\n\treadonly altTextPrefix: string;\n\treadonly imageSizeAnnounce: (w: number, h: number) => string;\n\treadonly resizeHint: (shrink: string, grow: string) => string;\n\treadonly imageResized: (w: number, h: number) => string;\n\treadonly keyboardResizeHint: (shrink: string, grow: string) => string;\n}\n\n// --- Default English Locale ---\n\nexport const IMAGE_LOCALE_EN: ImageLocale = {\n\tinsertImage: 'Insert Image',\n\tinsertImageTooltip: 'Insert Image',\n\tuploadFromComputer: 'Upload from computer',\n\tuploadAria: 'Upload image from computer',\n\tseparator: 'or',\n\turlPlaceholder: 'https://...',\n\turlAria: 'Image URL',\n\tinsertButton: 'Insert',\n\tinsertAria: 'Insert image',\n\timageAria: (alt: string, width?: number, height?: number) => {\n\t\tconst parts: string[] = [alt || 'Image'];\n\t\tif (width !== undefined && height !== undefined) {\n\t\t\tparts.push(`${width} by ${height} pixels`);\n\t\t}\n\t\treturn parts.join(', ');\n\t},\n\tresetToNaturalSize: 'Image reset to natural size.',\n\tuploadFailed: 'Upload failed.',\n\tuploading: 'Uploading...',\n\tresizeHandleLabel: (position: string) => `Resize ${position}`,\n\timageSelected: 'Image selected.',\n\taltTextPrefix: 'Alt text: ',\n\timageSizeAnnounce: (w: number, h: number) => `Size: ${w} by ${h} pixels.`,\n\tresizeHint: (shrink: string, grow: string) => `${shrink} / ${grow} to resize.`,\n\timageResized: (w: number, h: number) => `Image resized to ${w} by ${h} pixels.`,\n\tkeyboardResizeHint: (shrink: string, grow: string) => `${shrink} / ${grow} to resize`,\n};\n\n// --- Lazy Locale Loader ---\n\nconst localeModules: LocaleModuleMap<ImageLocale> = import.meta.glob<{\n\tdefault: ImageLocale;\n}>('./locales/*.ts', { eager: false });\n\nexport async function loadImageLocale(lang: string): Promise<ImageLocale> {\n\treturn loadLocaleModule(localeModules, lang, IMAGE_LOCALE_EN);\n}\n","/**\n * NodeView factory for image blocks.\n * Renders <figure> with <img>, handles selection state, upload overlay,\n * resize with 4 corner handles + live size indicator, and a keyboard-hint\n * showing resize shortcuts when the image is selected.\n */\n\nimport type { BlockAttrs, BlockNode } from '../../model/Document.js';\nimport type { BlockId } from '../../model/TypeBrands.js';\nimport { getTextDirection } from '../../platform/Platform.js';\nimport type { EditorState } from '../../state/EditorState.js';\nimport type { Transaction } from '../../state/Transaction.js';\nimport {\n\tgetStyleNonceForNode,\n\tsetStyleProperties,\n\tsetStyleProperty,\n} from '../../style/StyleRuntime.js';\nimport type { NodeView, NodeViewFactory } from '../../view/NodeView.js';\nimport { formatShortcut } from '../shared/ShortcutFormatting.js';\nimport { IMAGE_LOCALE_EN, type ImageLocale } from './ImageLocale.js';\nimport type { ImageKeymap, ImagePluginConfig, UploadState } from './ImageUpload.js';\n\nconst ALIGNMENT_CLASSES: Record<string, string> = {\n\tstart: 'notectl-image--start',\n\tcenter: 'notectl-image--center',\n\tend: 'notectl-image--end',\n};\n\nconst MIN_IMAGE_WIDTH = 50;\n\ntype HandlePosition = 'nw' | 'ne' | 'sw' | 'se';\n\nconst HANDLE_CURSORS: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'nwse-resize',\n\tne: 'nesw-resize',\n\tsw: 'nesw-resize',\n\tse: 'nwse-resize',\n};\n\n/** +1 means dragging right increases width; -1 means dragging left does. */\nconst HANDLE_X_SIGN: Readonly<Record<HandlePosition, 1 | -1>> = {\n\tnw: -1,\n\tne: 1,\n\tsw: -1,\n\tse: 1,\n};\n\nconst HANDLE_POSITIONS_LTR: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'top-left',\n\tne: 'top-right',\n\tsw: 'bottom-left',\n\tse: 'bottom-right',\n};\n\nconst HANDLE_POSITIONS_RTL: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'top-right',\n\tne: 'top-left',\n\tsw: 'bottom-right',\n\tse: 'bottom-left',\n};\n\nconst RTL_CURSOR_MAP: Readonly<Record<HandlePosition, string>> = {\n\tnw: 'nesw-resize',\n\tne: 'nwse-resize',\n\tsw: 'nwse-resize',\n\tse: 'nesw-resize',\n};\n\n/** Returns the direction-aware ARIA label for a resize handle. */\nexport function getHandleLabel(\n\tposition: HandlePosition,\n\tisRtl: boolean,\n\tlocale: ImageLocale = IMAGE_LOCALE_EN,\n): string {\n\tconst positionName: string = (isRtl ? HANDLE_POSITIONS_RTL : HANDLE_POSITIONS_LTR)[position];\n\treturn locale.resizeHandleLabel(positionName);\n}\n\n/** Returns the X-sign for resize drag, flipped in RTL. */\nexport function getResizeXSign(position: HandlePosition, isRtl: boolean): 1 | -1 {\n\tconst sign: 1 | -1 = HANDLE_X_SIGN[position];\n\treturn isRtl ? (-sign as 1 | -1) : sign;\n}\n\n// --- Global cursor override during resize ---\n\nlet activeCursorStyle: HTMLStyleElement | null = null;\n\nfunction setGlobalResizeCursor(cursor: string, referenceNode?: Node): void {\n\tclearGlobalResizeCursor();\n\tactiveCursorStyle = document.createElement('style');\n\tconst nonce = referenceNode ? getStyleNonceForNode(referenceNode) : undefined;\n\tif (nonce) {\n\t\tactiveCursorStyle.setAttribute('nonce', nonce);\n\t}\n\tactiveCursorStyle.textContent = `*{cursor:${cursor}!important;user-select:none!important}`;\n\tdocument.head.appendChild(activeCursorStyle);\n}\n\nfunction clearGlobalResizeCursor(): void {\n\tif (activeCursorStyle) {\n\t\tactiveCursorStyle.remove();\n\t\tactiveCursorStyle = null;\n\t}\n}\n\n/** Builds the keyboard-hint text from the resolved keymap. */\nfunction buildKeyboardHintText(\n\tresolvedKeymap: Readonly<Record<keyof ImageKeymap, string | null>>,\n\tlocale: ImageLocale,\n): string {\n\tconst shrink: string | null = resolvedKeymap.shrinkWidth ?? null;\n\tconst grow: string | null = resolvedKeymap.growWidth ?? null;\n\tif (!shrink || !grow) return '';\n\treturn locale.keyboardResizeHint(formatShortcut(shrink), formatShortcut(grow));\n}\n\n/** Creates a NodeViewFactory for image blocks. */\nexport function createImageNodeViewFactory(\n\tconfig: ImagePluginConfig,\n\tuploadStates: Map<BlockId, UploadState>,\n\tresolvedKeymap?: Readonly<Record<keyof ImageKeymap, string | null>>,\n\tlocale: ImageLocale = IMAGE_LOCALE_EN,\n): NodeViewFactory {\n\treturn (\n\t\tnode: BlockNode,\n\t\tgetState: () => EditorState,\n\t\tdispatch: (tr: Transaction) => void,\n\t): NodeView => {\n\t\t// --- DOM Construction ---\n\t\tconst figure: HTMLElement = document.createElement('figure');\n\t\tfigure.className = 'notectl-image';\n\t\tfigure.setAttribute('data-block-id', node.id);\n\t\tfigure.setAttribute('data-void', 'true');\n\t\tfigure.setAttribute('data-selectable', 'true');\n\n\t\tconst container: HTMLDivElement = document.createElement('div');\n\t\tcontainer.className = 'notectl-image__container';\n\n\t\tconst img: HTMLImageElement = document.createElement('img');\n\t\timg.className = 'notectl-image__img';\n\t\timg.draggable = false;\n\n\t\tconst overlay: HTMLDivElement = document.createElement('div');\n\t\toverlay.className = 'notectl-image__overlay';\n\n\t\tcontainer.appendChild(img);\n\t\tcontainer.appendChild(overlay);\n\t\tfigure.appendChild(container);\n\n\t\tlet currentNodeId: BlockId = node.id;\n\t\tlet resizeOverlay: HTMLDivElement | null = null;\n\n\t\t// --- ARIA Label ---\n\n\t\tfunction updateAriaLabel(n: BlockNode): void {\n\t\t\tconst alt: string = (n.attrs?.alt as string | undefined) ?? '';\n\t\t\tconst width: number | undefined = n.attrs?.width as number | undefined;\n\t\t\tconst height: number | undefined = n.attrs?.height as number | undefined;\n\t\t\tfigure.setAttribute('aria-label', locale.imageAria(alt, width, height));\n\t\t}\n\n\t\t// --- Attribute Application ---\n\n\t\tfunction applyAttrs(n: BlockNode): void {\n\t\t\tconst src: string = (n.attrs?.src as string | undefined) ?? '';\n\t\t\tconst alt: string = (n.attrs?.alt as string | undefined) ?? '';\n\t\t\tconst width: number | undefined = n.attrs?.width as number | undefined;\n\t\t\tconst height: number | undefined = n.attrs?.height as number | undefined;\n\t\t\tconst align: string = (n.attrs?.align as string | undefined) ?? 'center';\n\n\t\t\t// Only update src when it changes to avoid re-triggering load events\n\t\t\tif (img.src !== src) img.src = src;\n\t\t\timg.alt = alt;\n\n\t\t\tif (width !== undefined) {\n\t\t\t\tsetStyleProperty(img, 'width', `${width}px`);\n\t\t\t} else {\n\t\t\t\tsetStyleProperty(img, 'width', '');\n\t\t\t}\n\t\t\tif (height !== undefined) {\n\t\t\t\tsetStyleProperty(img, 'height', `${height}px`);\n\t\t\t} else {\n\t\t\t\tsetStyleProperty(img, 'height', '');\n\t\t\t}\n\n\t\t\tfor (const cls of Object.values(ALIGNMENT_CLASSES)) {\n\t\t\t\tfigure.classList.remove(cls);\n\t\t\t}\n\t\t\tconst alignClass: string | undefined = ALIGNMENT_CLASSES[align];\n\t\t\tif (alignClass) {\n\t\t\t\tfigure.classList.add(alignClass);\n\t\t\t}\n\n\t\t\tconst uploadState: UploadState = uploadStates.get(n.id) ?? 'idle';\n\t\t\tconst isUploading: boolean = uploadState === 'uploading';\n\t\t\toverlay.classList.toggle('notectl-image__overlay--uploading', isUploading);\n\t\t\toverlay.classList.toggle('notectl-image__overlay--error', uploadState === 'error');\n\n\t\t\tif (isUploading) {\n\t\t\t\toverlay.textContent = locale.uploading;\n\t\t\t} else if (uploadState === 'error') {\n\t\t\t\toverlay.textContent = locale.uploadFailed;\n\t\t\t} else {\n\t\t\t\toverlay.textContent = '';\n\t\t\t}\n\n\t\t\tif (isUploading) {\n\t\t\t\tfigure.setAttribute('aria-busy', 'true');\n\t\t\t} else {\n\t\t\t\tfigure.removeAttribute('aria-busy');\n\t\t\t}\n\n\t\t\tupdateAriaLabel(n);\n\t\t}\n\n\t\tapplyAttrs(node);\n\n\t\t// --- Commit natural dimensions on first load ---\n\n\t\tlet dimensionsCommitted = false;\n\n\t\timg.addEventListener('load', () => {\n\t\t\tif (dimensionsCommitted) return;\n\n\t\t\tconst state: EditorState = getState();\n\t\t\tconst block: BlockNode | undefined = state.getBlock(currentNodeId);\n\t\t\tif (!block || block.type !== 'image') return;\n\n\t\t\tconst hasWidth: boolean = block.attrs?.width !== undefined;\n\t\t\tconst hasHeight: boolean = block.attrs?.height !== undefined;\n\t\t\tif (hasWidth && hasHeight) {\n\t\t\t\tdimensionsCommitted = true;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst naturalW: number = img.naturalWidth;\n\t\t\tconst naturalH: number = img.naturalHeight;\n\t\t\tif (naturalW === 0 || naturalH === 0) return;\n\n\t\t\tconst clampedW: number = Math.min(naturalW, config.maxWidth);\n\t\t\tconst ratio: number = naturalW > 0 ? naturalH / naturalW : 1;\n\t\t\tconst clampedH: number = Math.round(clampedW * ratio);\n\n\t\t\tconst path: BlockId[] | undefined = state.getNodePath(currentNodeId);\n\t\t\tif (!path) return;\n\n\t\t\tconst merged: BlockAttrs = {\n\t\t\t\t...(block.attrs ?? {}),\n\t\t\t\twidth: clampedW,\n\t\t\t\theight: clampedH,\n\t\t\t};\n\n\t\t\tconst tr: Transaction = state.transaction('command').setNodeAttr(path, merged).build();\n\t\t\tdispatch(tr);\n\t\t\tdimensionsCommitted = true;\n\t\t});\n\n\t\t// --- Resize Logic ---\n\n\t\tfunction clampWidth(width: number): number {\n\t\t\treturn Math.max(MIN_IMAGE_WIDTH, Math.min(config.maxWidth, width));\n\t\t}\n\n\t\tfunction commitResize(nodeId: BlockId, width: number, height: number): void {\n\t\t\tconst state: EditorState = getState();\n\t\t\tconst block: BlockNode | undefined = state.getBlock(nodeId);\n\t\t\tif (!block) return;\n\n\t\t\tconst prevWidth: number | undefined = block.attrs?.width as number | undefined;\n\t\t\tconst prevHeight: number | undefined = block.attrs?.height as number | undefined;\n\t\t\tif (prevWidth === width && prevHeight === height) return;\n\n\t\t\tconst path: BlockId[] | undefined = state.getNodePath(nodeId);\n\t\t\tif (!path) return;\n\n\t\t\tconst merged: BlockAttrs = {\n\t\t\t\t...(block.attrs ?? {}),\n\t\t\t\twidth,\n\t\t\t\theight,\n\t\t\t};\n\n\t\t\tconst tr: Transaction = state.transaction('command').setNodeAttr(path, merged).build();\n\t\t\tdispatch(tr);\n\t\t}\n\n\t\tfunction attachHandleListeners(\n\t\t\thandle: HTMLDivElement,\n\t\t\tposition: HandlePosition,\n\t\t\tnodeId: BlockId,\n\t\t\tsizeIndicator: HTMLDivElement,\n\t\t): void {\n\t\t\tlet startX = 0;\n\t\t\tlet startWidth = 0;\n\t\t\tlet aspectRatio = 1;\n\t\t\tlet xSign: 1 | -1 = HANDLE_X_SIGN[position];\n\n\t\t\tconst onPointerMove = (e: PointerEvent): void => {\n\t\t\t\tconst deltaX: number = e.clientX - startX;\n\t\t\t\tconst newWidth: number = clampWidth(startWidth + deltaX * xSign);\n\t\t\t\tconst newHeight: number = Math.round(newWidth / aspectRatio);\n\n\t\t\t\tsetStyleProperties(img, {\n\t\t\t\t\twidth: `${newWidth}px`,\n\t\t\t\t\theight: `${newHeight}px`,\n\t\t\t\t});\n\n\t\t\t\tsizeIndicator.textContent = `${Math.round(newWidth)} \\u00D7 ${newHeight}`;\n\t\t\t};\n\n\t\t\tconst onPointerUp = (e: PointerEvent): void => {\n\t\t\t\tdocument.removeEventListener('pointermove', onPointerMove);\n\t\t\t\tdocument.removeEventListener('pointerup', onPointerUp);\n\n\t\t\t\tfigure.classList.remove('notectl-image--resizing');\n\t\t\t\tsizeIndicator.classList.remove('notectl-image__size-indicator--visible');\n\t\t\t\tclearGlobalResizeCursor();\n\n\t\t\t\tconst finalWidth: number = Math.round(img.getBoundingClientRect().width);\n\t\t\t\tconst finalHeight: number = Math.round(finalWidth / aspectRatio);\n\n\t\t\t\tcommitResize(nodeId, finalWidth, finalHeight);\n\n\t\t\t\t(e.target as HTMLElement | null)?.releasePointerCapture?.(e.pointerId);\n\t\t\t};\n\n\t\t\thandle.addEventListener('pointerdown', (e: PointerEvent) => {\n\t\t\t\te.preventDefault();\n\t\t\t\te.stopPropagation();\n\n\t\t\t\tconst isRtl: boolean = getTextDirection(figure) === 'rtl';\n\t\t\t\txSign = getResizeXSign(position, isRtl);\n\n\t\t\t\tstartX = e.clientX;\n\t\t\t\tstartWidth = img.getBoundingClientRect().width;\n\t\t\t\tconst imgHeight: number = img.getBoundingClientRect().height;\n\t\t\t\taspectRatio = imgHeight > 0 ? startWidth / imgHeight : 1;\n\n\t\t\t\tfigure.classList.add('notectl-image--resizing');\n\t\t\t\tsizeIndicator.textContent = `${Math.round(startWidth)} \\u00D7 ${Math.round(imgHeight)}`;\n\t\t\t\tsizeIndicator.classList.add('notectl-image__size-indicator--visible');\n\t\t\t\tconst cursor: string = isRtl ? RTL_CURSOR_MAP[position] : HANDLE_CURSORS[position];\n\t\t\t\tsetGlobalResizeCursor(cursor, figure);\n\n\t\t\t\t(e.target as HTMLElement).setPointerCapture(e.pointerId);\n\t\t\t\tdocument.addEventListener('pointermove', onPointerMove);\n\t\t\t\tdocument.addEventListener('pointerup', onPointerUp);\n\t\t\t});\n\t\t}\n\n\t\tfunction createResizeOverlay(nodeId: BlockId): void {\n\t\t\tif (!config.resizable || resizeOverlay) return;\n\n\t\t\tresizeOverlay = document.createElement('div');\n\t\t\tresizeOverlay.className = 'notectl-image__resize-overlay';\n\t\t\tresizeOverlay.setAttribute('data-notectl-no-print', '');\n\n\t\t\tconst sizeIndicator: HTMLDivElement = document.createElement('div');\n\t\t\tsizeIndicator.className = 'notectl-image__size-indicator';\n\t\t\tresizeOverlay.appendChild(sizeIndicator);\n\n\t\t\tconst positions: readonly HandlePosition[] = ['nw', 'ne', 'sw', 'se'];\n\t\t\tfor (const pos of positions) {\n\t\t\t\tconst handle: HTMLDivElement = document.createElement('div');\n\t\t\t\thandle.className = `notectl-image__resize-handle notectl-image__resize-handle--${pos}`;\n\t\t\t\t// Pointer-only visual grips. Keyboard resize is provided via shortcuts\n\t\t\t\t// with live-region announcements (see registerResizeKeymaps), so these\n\t\t\t\t// decorative handles are hidden from assistive technology rather than\n\t\t\t\t// exposed with a misleading `separator` role.\n\t\t\t\thandle.setAttribute('aria-hidden', 'true');\n\t\t\t\tattachHandleListeners(handle, pos, nodeId, sizeIndicator);\n\t\t\t\tresizeOverlay.appendChild(handle);\n\t\t\t}\n\n\t\t\t// Keyboard hint (hidden from screenreaders, visual only)\n\t\t\tif (resolvedKeymap) {\n\t\t\t\tconst hintText: string = buildKeyboardHintText(resolvedKeymap, locale);\n\t\t\t\tif (hintText) {\n\t\t\t\t\tconst hint: HTMLDivElement = document.createElement('div');\n\t\t\t\t\thint.className = 'notectl-image__keyboard-hint';\n\t\t\t\t\thint.setAttribute('aria-hidden', 'true');\n\t\t\t\t\thint.textContent = hintText;\n\t\t\t\t\tresizeOverlay.appendChild(hint);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontainer.appendChild(resizeOverlay);\n\t\t}\n\n\t\tfunction removeResizeOverlay(): void {\n\t\t\tclearGlobalResizeCursor();\n\t\t\tif (resizeOverlay) {\n\t\t\t\tresizeOverlay.remove();\n\t\t\t\tresizeOverlay = null;\n\t\t\t}\n\t\t}\n\n\t\t// --- NodeView Interface ---\n\n\t\treturn {\n\t\t\tdom: figure,\n\t\t\tcontentDOM: null,\n\n\t\t\tupdate(updatedNode: BlockNode): boolean {\n\t\t\t\tif (updatedNode.type !== 'image') return false;\n\t\t\t\tcurrentNodeId = updatedNode.id;\n\t\t\t\tfigure.setAttribute('data-block-id', updatedNode.id);\n\t\t\t\tapplyAttrs(updatedNode);\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\tselectNode(): void {\n\t\t\t\tfigure.classList.add('notectl-image--selected');\n\t\t\t\tcreateResizeOverlay(currentNodeId);\n\t\t\t},\n\n\t\t\tdeselectNode(): void {\n\t\t\t\tfigure.classList.remove('notectl-image--selected');\n\t\t\t\tremoveResizeOverlay();\n\t\t\t},\n\n\t\t\tdestroy(): void {\n\t\t\t\tremoveResizeOverlay();\n\t\t\t\t// Do NOT revoke blob URLs here — the URL may still be referenced\n\t\t\t\t// by the clipboard (cut/paste) or undo history. Blob URLs are\n\t\t\t\t// automatically released when the document is unloaded.\n\t\t\t},\n\t\t};\n\t};\n}\n","/**\n * Renders the image insert popup with file upload and URL input,\n * following the same pattern as FontSizePopup.\n */\n\nimport type { PluginContext } from '../Plugin.js';\nimport { insertImage } from './ImageCommands.js';\nimport type { ImageLocale } from './ImageLocale.js';\n\n// --- Config ---\n\nexport interface ImagePopupConfig {\n\treadonly acceptedTypes: readonly string[];\n\treadonly locale: ImageLocale;\n\treadonly onFileInsert: (context: PluginContext, file: File) => void;\n\treadonly onClose: () => void;\n}\n\n// --- Public Entry Point ---\n\n/** Builds the image insert popup DOM inside the given container. */\nexport function renderImagePopup(\n\tcontainer: HTMLElement,\n\tcontext: PluginContext,\n\tconfig: ImagePopupConfig,\n): void {\n\tcontainer.classList.add('notectl-image-popup');\n\n\tappendFileUploadSection(container, context, config);\n\tappendSeparator(container, config.locale);\n\tappendUrlSection(container, context, config);\n}\n\n// --- File Upload Section ---\n\nfunction appendFileUploadSection(\n\tcontainer: HTMLElement,\n\tcontext: PluginContext,\n\tconfig: ImagePopupConfig,\n): void {\n\tconst fileInput: HTMLInputElement = document.createElement('input');\n\tfileInput.type = 'file';\n\tfileInput.accept = config.acceptedTypes.join(',');\n\tfileInput.className = 'notectl-image-popup__file-input';\n\n\tconst uploadBtn: HTMLButtonElement = document.createElement('button');\n\tuploadBtn.type = 'button';\n\tuploadBtn.textContent = config.locale.uploadFromComputer;\n\tuploadBtn.setAttribute('aria-label', config.locale.uploadAria);\n\tuploadBtn.className = 'notectl-image-popup__upload-btn';\n\n\tuploadBtn.addEventListener('mousedown', (e: MouseEvent) => {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\tfileInput.click();\n\t});\n\n\tfileInput.addEventListener('change', () => {\n\t\tconst file: File | undefined = fileInput.files?.[0];\n\t\tif (file) {\n\t\t\tconfig.onFileInsert(context, file);\n\t\t\tconfig.onClose();\n\t\t\tcontext.getContainer().focus();\n\t\t}\n\t});\n\n\tcontainer.appendChild(fileInput);\n\tcontainer.appendChild(uploadBtn);\n}\n\n// --- Separator ---\n\nfunction appendSeparator(container: HTMLElement, locale: ImageLocale): void {\n\tconst separator: HTMLDivElement = document.createElement('div');\n\tseparator.className = 'notectl-image-popup__separator';\n\n\tconst line1: HTMLSpanElement = document.createElement('span');\n\tline1.className = 'notectl-image-popup__separator-line';\n\n\tconst orText: HTMLSpanElement = document.createElement('span');\n\torText.textContent = locale.separator;\n\torText.className = 'notectl-image-popup__separator-text';\n\n\tconst line2: HTMLSpanElement = document.createElement('span');\n\tline2.className = 'notectl-image-popup__separator-line';\n\n\tseparator.appendChild(line1);\n\tseparator.appendChild(orText);\n\tseparator.appendChild(line2);\n\tcontainer.appendChild(separator);\n}\n\n// --- URL Input Section ---\n\nfunction appendUrlSection(\n\tcontainer: HTMLElement,\n\tcontext: PluginContext,\n\tconfig: ImagePopupConfig,\n): void {\n\tconst urlInput: HTMLInputElement = document.createElement('input');\n\turlInput.type = 'url';\n\turlInput.placeholder = config.locale.urlPlaceholder;\n\turlInput.setAttribute('aria-label', config.locale.urlAria);\n\turlInput.className = 'notectl-image-popup__url-input';\n\n\tconst insertBtn: HTMLButtonElement = document.createElement('button');\n\tinsertBtn.type = 'button';\n\tinsertBtn.textContent = config.locale.insertButton;\n\tinsertBtn.setAttribute('aria-label', config.locale.insertAria);\n\tinsertBtn.className = 'notectl-image-popup__insert-btn';\n\n\tconst applyUrl = (): void => {\n\t\tconst src: string = urlInput.value.trim();\n\t\tif (src) {\n\t\t\tinsertImage(context, { src });\n\t\t\tconfig.onClose();\n\t\t\tcontext.getContainer().focus();\n\t\t}\n\t};\n\n\tinsertBtn.addEventListener('mousedown', (e: MouseEvent) => {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\tapplyUrl();\n\t});\n\n\turlInput.addEventListener('keydown', (e: KeyboardEvent) => {\n\t\tif (e.key === 'Enter') {\n\t\t\te.preventDefault();\n\t\t\tapplyUrl();\n\t\t}\n\t});\n\n\tcontainer.appendChild(urlInput);\n\tcontainer.appendChild(insertBtn);\n\n\trequestAnimationFrame(() => urlInput.focus());\n}\n","/** Image insert popup styles (file upload + URL input). */\nexport const IMAGE_POPUP_CSS = `\n.notectl-image-popup {\n\tpadding: 8px;\n\tmin-width: 240px;\n}\n\n.notectl-image-popup__file-input {\n\tposition: absolute;\n\twidth: 0;\n\theight: 0;\n\toverflow: hidden;\n\topacity: 0;\n}\n\n.notectl-image-popup__upload-btn {\n\tdisplay: block;\n\twidth: 100%;\n\tpadding: 8px 12px;\n\tcursor: pointer;\n\ttext-align: center;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 4px;\n\tbackground: var(--notectl-surface-raised);\n\tcolor: var(--notectl-fg);\n}\n\n.notectl-image-popup__separator {\n\tdisplay: flex;\n\talign-items: center;\n\tmargin: 8px 0;\n\tcolor: var(--notectl-fg-muted);\n\tfont-size: 12px;\n}\n\n.notectl-image-popup__separator-line {\n\tflex: 1;\n\theight: 1px;\n\tbackground: var(--notectl-border);\n}\n\n.notectl-image-popup__separator-text {\n\tpadding: 0 8px;\n}\n\n.notectl-image-popup__url-input {\n\twidth: 100%;\n\tpadding: 6px 8px;\n\tbox-sizing: border-box;\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 4px;\n\tbackground: var(--notectl-surface-overlay);\n\tcolor: var(--notectl-fg);\n}\n\n.notectl-image-popup__insert-btn {\n\twidth: 100%;\n\tpadding: 8px 12px;\n\tmargin-top: 4px;\n\tcursor: pointer;\n\tborder: 1px solid var(--notectl-border);\n\tborder-radius: 4px;\n\tbackground: var(--notectl-surface-raised);\n\tcolor: var(--notectl-fg);\n}\n`;\n","/**\n * Image plugin types, configuration, and service key.\n */\n\nimport { ServiceKey } from '../Plugin.js';\nimport type { ImageLocale } from './ImageLocale.js';\n\n// --- Image Attributes ---\n\nexport interface ImageAttrs {\n\treadonly src: string;\n\treadonly alt: string;\n\treadonly width?: number;\n\treadonly height?: number;\n\treadonly align: 'start' | 'center' | 'end';\n}\n\n// --- Upload Types ---\n\nexport interface ImageUploadResult {\n\treadonly url: string;\n\treadonly width?: number;\n\treadonly height?: number;\n}\n\nexport interface ImageUploadService {\n\tupload(file: File): Promise<ImageUploadResult>;\n}\n\nexport const IMAGE_UPLOAD_SERVICE = new ServiceKey<ImageUploadService>('image:upload');\n\nexport type UploadState = 'idle' | 'uploading' | 'complete' | 'error';\n\n// --- Keyboard Bindings ---\n\n/**\n * Configurable keyboard bindings for image resize actions.\n * Omit a slot to use the default; set to `null` to disable the binding.\n *\n * Key descriptor format: `'Mod-Shift-ArrowRight'`, etc.\n * `Mod` resolves to Cmd on macOS, Ctrl on Windows/Linux.\n */\nexport interface ImageKeymap {\n\treadonly growWidth?: string | null;\n\treadonly shrinkWidth?: string | null;\n\treadonly growWidthLarge?: string | null;\n\treadonly shrinkWidthLarge?: string | null;\n\treadonly resetSize?: string | null;\n}\n\nexport const DEFAULT_IMAGE_KEYMAP: Readonly<Record<keyof ImageKeymap, string>> = {\n\tgrowWidth: 'Mod-Shift-ArrowRight',\n\tshrinkWidth: 'Mod-Shift-ArrowLeft',\n\tgrowWidthLarge: 'Mod-Shift-Alt-ArrowRight',\n\tshrinkWidthLarge: 'Mod-Shift-Alt-ArrowLeft',\n\tresetSize: 'Mod-Shift-0',\n};\n\n// --- Configuration ---\n\nexport interface ImagePluginConfig {\n\treadonly maxWidth: number;\n\treadonly maxFileSize: number;\n\treadonly acceptedTypes: readonly string[];\n\treadonly resizable: boolean;\n\t/** Pixels to grow/shrink per small resize step. @default 10 */\n\treadonly resizeStep?: number;\n\t/** Pixels to grow/shrink per large resize step. @default 50 */\n\treadonly resizeStepLarge?: number;\n\t/** Customize keyboard bindings for image resize actions. */\n\treadonly keymap?: ImageKeymap;\n\t/** Locale override for user-facing strings. */\n\treadonly locale?: ImageLocale;\n}\n\nexport const DEFAULT_IMAGE_CONFIG: ImagePluginConfig = {\n\tmaxWidth: 800,\n\tmaxFileSize: 10 * 1024 * 1024,\n\tacceptedTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'],\n\tresizable: true,\n\tresizeStep: 10,\n\tresizeStepLarge: 50,\n};\n","/**\n * ImagePlugin: registers an image void block type with NodeSpec,\n * NodeView, commands, file handler, toolbar button, and accessible\n * keyboard resize with screenreader announcements.\n */\n\nimport { IMAGE_CSS } from '../../editor/styles/image.js';\nimport type { BlockAttrs, BlockNode } from '../../model/Document.js';\nimport { escapeHTML } from '../../model/HTMLUtils.js';\nimport { isNodeSelection } from '../../model/Selection.js';\nimport type { BlockId } from '../../model/TypeBrands.js';\nimport type { EditorState } from '../../state/EditorState.js';\nimport type { Transaction } from '../../state/Transaction.js';\nimport { setStyleProperty } from '../../style/StyleRuntime.js';\nimport { createBlockElement } from '../../view/DomUtils.js';\nimport type { Plugin, PluginContext } from '../Plugin.js';\nimport { resolveLocale } from '../shared/PluginHelpers.js';\nimport { formatShortcut } from '../shared/ShortcutFormatting.js';\nimport {\n\tinsertImage,\n\tregisterImageCommands,\n\tresetImageSize,\n\tresizeImageByDelta,\n} from './ImageCommands.js';\nimport { IMAGE_LOCALE_EN, type ImageLocale, loadImageLocale } from './ImageLocale.js';\nimport { createImageNodeViewFactory } from './ImageNodeView.js';\nimport { renderImagePopup } from './ImagePopup.js';\nimport { IMAGE_POPUP_CSS } from './ImagePopupStyles.js';\nimport {\n\tDEFAULT_IMAGE_CONFIG,\n\tDEFAULT_IMAGE_KEYMAP,\n\tIMAGE_UPLOAD_SERVICE,\n\ttype ImageKeymap,\n\ttype ImagePluginConfig,\n\ttype UploadState,\n} from './ImageUpload.js';\n\n// --- Attribute Registry Augmentation ---\n\ndeclare module '../../model/AttrRegistry.js' {\n\tinterface NodeAttrRegistry {\n\t\timage: {\n\t\t\tsrc: string;\n\t\t\talt: string;\n\t\t\twidth?: number;\n\t\t\theight?: number;\n\t\t\talign: 'start' | 'center' | 'end';\n\t\t};\n\t}\n}\n\n// --- Plugin ---\n\nexport class ImagePlugin implements Plugin {\n\treadonly id = 'image';\n\treadonly name = 'Image';\n\treadonly priority = 45;\n\n\tprivate readonly config: ImagePluginConfig;\n\tprivate readonly resolvedKeymap: Readonly<Record<keyof ImageKeymap, string | null>>;\n\tprivate readonly uploadStates = new Map<BlockId, UploadState>();\n\tprivate readonly blobUrls = new Set<string>();\n\tprivate context: PluginContext | null = null;\n\tprivate locale!: ImageLocale;\n\n\tconstructor(config?: Partial<ImagePluginConfig>) {\n\t\tthis.config = { ...DEFAULT_IMAGE_CONFIG, ...config };\n\t\tthis.resolvedKeymap = { ...DEFAULT_IMAGE_KEYMAP, ...config?.keymap };\n\t}\n\n\tasync init(context: PluginContext): Promise<void> {\n\t\tthis.locale = await resolveLocale(\n\t\t\tcontext,\n\t\t\tthis.config.locale,\n\t\t\tIMAGE_LOCALE_EN,\n\t\t\tloadImageLocale,\n\t\t);\n\t\tcontext.registerStyleSheet(IMAGE_CSS);\n\t\tcontext.registerStyleSheet(IMAGE_POPUP_CSS);\n\t\tthis.context = context;\n\t\tthis.registerNodeSpec(context);\n\t\tthis.registerNodeView(context);\n\t\tregisterImageCommands(context);\n\t\tthis.registerResizeCommands(context);\n\t\tthis.registerResizeKeymaps(context);\n\t\tthis.registerFileHandler(context);\n\t\tthis.registerToolbarItem(context);\n\t}\n\n\tdestroy(): void {\n\t\tfor (const url of this.blobUrls) {\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t\tthis.blobUrls.clear();\n\t\tthis.uploadStates.clear();\n\t\tthis.context = null;\n\t}\n\n\tonStateChange(oldState: EditorState, newState: EditorState, _tr: Transaction): void {\n\t\tif (!this.context) return;\n\n\t\t// Clean up upload states for removed blocks (searches full tree)\n\t\tfor (const id of this.uploadStates.keys()) {\n\t\t\tif (!newState.getBlock(id)) {\n\t\t\t\tthis.uploadStates.delete(id);\n\t\t\t}\n\t\t}\n\n\t\t// Announce image selection for screenreaders\n\t\tconst oldIsImage: boolean = this.isImageSelected(oldState);\n\t\tconst nowIsImage: boolean = this.isImageSelected(newState);\n\n\t\tif (!oldIsImage && nowIsImage) {\n\t\t\tthis.announceImageSelection(newState);\n\t\t}\n\t}\n\n\tprivate registerNodeSpec(context: PluginContext): void {\n\t\tconst locale = this.locale;\n\t\tcontext.registerNodeSpec({\n\t\t\ttype: 'image',\n\t\t\tgroup: 'block',\n\t\t\tisVoid: true,\n\t\t\tselectable: true,\n\t\t\tattrs: {\n\t\t\t\tsrc: { default: '' },\n\t\t\t\talt: { default: '' },\n\t\t\t\talign: { default: 'center' },\n\t\t\t},\n\t\t\ttoDOM(node) {\n\t\t\t\tconst figure = createBlockElement('figure', node.id);\n\t\t\t\tfigure.className = 'notectl-image';\n\t\t\t\tfigure.setAttribute('data-void', 'true');\n\t\t\t\tfigure.setAttribute('data-selectable', 'true');\n\n\t\t\t\tconst imgContainer: HTMLDivElement = document.createElement('div');\n\t\t\t\timgContainer.className = 'notectl-image__container';\n\n\t\t\t\tconst alt: string = (node.attrs?.alt as string | undefined) ?? '';\n\t\t\t\tconst width: number | undefined = node.attrs?.width as number | undefined;\n\t\t\t\tconst height: number | undefined = node.attrs?.height as number | undefined;\n\n\t\t\t\tconst img: HTMLImageElement = document.createElement('img');\n\t\t\t\timg.className = 'notectl-image__img';\n\t\t\t\timg.src = (node.attrs?.src as string | undefined) ?? '';\n\t\t\t\timg.alt = alt;\n\t\t\t\timg.draggable = false;\n\n\t\t\t\tif (width !== undefined) setStyleProperty(img, 'width', `${width}px`);\n\t\t\t\tif (height !== undefined) setStyleProperty(img, 'height', `${height}px`);\n\n\t\t\t\tconst align: string = (node.attrs?.align as string | undefined) ?? 'center';\n\t\t\t\tconst alignClass: string | undefined = {\n\t\t\t\t\tstart: 'notectl-image--start',\n\t\t\t\t\tcenter: 'notectl-image--center',\n\t\t\t\t\tend: 'notectl-image--end',\n\t\t\t\t}[align];\n\t\t\t\tif (alignClass) figure.classList.add(alignClass);\n\n\t\t\t\tfigure.setAttribute('aria-label', locale.imageAria(alt, width, height));\n\n\t\t\t\timgContainer.appendChild(img);\n\t\t\t\tfigure.appendChild(imgContainer);\n\t\t\t\treturn figure;\n\t\t\t},\n\t\t\ttoHTML(node) {\n\t\t\t\tconst src: string = escapeHTML((node.attrs?.src as string | undefined) ?? '');\n\t\t\t\tconst alt: string = escapeHTML((node.attrs?.alt as string | undefined) ?? '');\n\t\t\t\tconst width: number | undefined = node.attrs?.width as number | undefined;\n\t\t\t\tconst height: number | undefined = node.attrs?.height as number | undefined;\n\n\t\t\t\tconst sizeAttrs: string =\n\t\t\t\t\t(width !== undefined ? ` width=\"${width}\"` : '') +\n\t\t\t\t\t(height !== undefined ? ` height=\"${height}\"` : '');\n\n\t\t\t\t// Alignment is handled by the serializer's alignment injection,\n\t\t\t\t// which works in both inline-style and CSS-class modes.\n\t\t\t\treturn `<figure><img src=\"${src}\" alt=\"${alt}\"${sizeAttrs}></figure>`;\n\t\t\t},\n\t\t\tparseHTML: [\n\t\t\t\t{\n\t\t\t\t\ttag: 'figure',\n\t\t\t\t\tgetAttrs(el) {\n\t\t\t\t\t\tconst img: HTMLImageElement | null = el.querySelector('img');\n\t\t\t\t\t\tif (!img) return false;\n\t\t\t\t\t\tconst attrs: Record<string, string | number | boolean> = {\n\t\t\t\t\t\t\tsrc: img.getAttribute('src') ?? '',\n\t\t\t\t\t\t\talt: img.getAttribute('alt') ?? '',\n\t\t\t\t\t\t\talign: 'center',\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst width: string | null = img.getAttribute('width');\n\t\t\t\t\t\tconst height: string | null = img.getAttribute('height');\n\t\t\t\t\t\tif (width) attrs.width = Number.parseInt(width, 10);\n\t\t\t\t\t\tif (height) attrs.height = Number.parseInt(height, 10);\n\n\t\t\t\t\t\t// Check inline style first, then class names (new + legacy)\n\t\t\t\t\t\tconst textAlign: string = el.style?.textAlign ?? '';\n\t\t\t\t\t\tif (textAlign === 'start' || textAlign === 'end' || textAlign === 'center') {\n\t\t\t\t\t\t\tattrs.align = textAlign;\n\t\t\t\t\t\t} else if (textAlign === 'left') {\n\t\t\t\t\t\t\tattrs.align = 'start';\n\t\t\t\t\t\t} else if (textAlign === 'right') {\n\t\t\t\t\t\t\tattrs.align = 'end';\n\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-start') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-left') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--start') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--left')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tattrs.align = 'start';\n\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-end') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-align-right') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--end') ||\n\t\t\t\t\t\t\tel.classList.contains('notectl-image--right')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tattrs.align = 'end';\n\t\t\t\t\t\t} else if (el.classList.contains('notectl-align-center')) {\n\t\t\t\t\t\t\tattrs.align = 'center';\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn attrs;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttag: 'img',\n\t\t\t\t\tgetAttrs(el) {\n\t\t\t\t\t\tconst attrs: Record<string, string | number | boolean> = {\n\t\t\t\t\t\t\tsrc: el.getAttribute('src') ?? '',\n\t\t\t\t\t\t\talt: el.getAttribute('alt') ?? '',\n\t\t\t\t\t\t\talign: 'center',\n\t\t\t\t\t\t};\n\t\t\t\t\t\tconst width: string | null = el.getAttribute('width');\n\t\t\t\t\t\tconst height: string | null = el.getAttribute('height');\n\t\t\t\t\t\tif (width) attrs.width = Number.parseInt(width, 10);\n\t\t\t\t\t\tif (height) attrs.height = Number.parseInt(height, 10);\n\t\t\t\t\t\treturn attrs;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\tsanitize: {\n\t\t\t\ttags: ['figure', 'img'],\n\t\t\t\tattrs: ['src', 'alt', 'width', 'height', 'class', 'style'],\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate registerNodeView(context: PluginContext): void {\n\t\tcontext.registerNodeView(\n\t\t\t'image',\n\t\t\tcreateImageNodeViewFactory(this.config, this.uploadStates, this.resolvedKeymap, this.locale),\n\t\t);\n\t}\n\n\tprivate registerResizeCommands(context: PluginContext): void {\n\t\tconst step: number = this.config.resizeStep ?? 10;\n\t\tconst stepLarge: number = this.config.resizeStepLarge ?? 50;\n\t\tconst maxWidth: number = this.config.maxWidth;\n\n\t\tcontext.registerCommand('resizeImageGrow', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, step, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resizeImageShrink', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, -step, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resizeImageGrowLarge', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, stepLarge, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resizeImageShrinkLarge', () => {\n\t\t\tconst result: boolean = resizeImageByDelta(context, -stepLarge, maxWidth);\n\t\t\tif (result) this.announceCurrentSize(context);\n\t\t\treturn result;\n\t\t});\n\n\t\tcontext.registerCommand('resetImageSize', () => {\n\t\t\tconst result: boolean = resetImageSize(context);\n\t\t\tif (result) context.announce(this.locale.resetToNaturalSize);\n\t\t\treturn result;\n\t\t});\n\t}\n\n\tprivate registerResizeKeymaps(context: PluginContext): void {\n\t\tconst bindings: Record<string, () => boolean> = {};\n\t\tconst commands: Record<keyof ImageKeymap, string> = {\n\t\t\tgrowWidth: 'resizeImageGrow',\n\t\t\tshrinkWidth: 'resizeImageShrink',\n\t\t\tgrowWidthLarge: 'resizeImageGrowLarge',\n\t\t\tshrinkWidthLarge: 'resizeImageShrinkLarge',\n\t\t\tresetSize: 'resetImageSize',\n\t\t};\n\n\t\tfor (const [slot, commandName] of Object.entries(commands)) {\n\t\t\tconst binding: string | null = this.resolvedKeymap[slot as keyof ImageKeymap] ?? null;\n\t\t\tif (binding) {\n\t\t\t\tbindings[binding] = () => context.executeCommand(commandName);\n\t\t\t}\n\t\t}\n\n\t\tif (Object.keys(bindings).length > 0) {\n\t\t\tcontext.registerKeymap(bindings);\n\t\t}\n\t}\n\n\tprivate registerFileHandler(context: PluginContext): void {\n\t\tcontext.registerFileHandler('image/*', async (file, _position) => {\n\t\t\tif (!this.isAcceptedType(file.type)) return false;\n\t\t\tthis.handleFileInsert(context, file);\n\t\t\treturn true;\n\t\t});\n\t}\n\n\tprivate handleFileInsert(context: PluginContext, file: File): void {\n\t\tif (file.size > this.config.maxFileSize) return;\n\n\t\tconst blobUrl: string = URL.createObjectURL(file);\n\t\tthis.blobUrls.add(blobUrl);\n\n\t\tconst inserted: boolean = insertImage(context, { src: blobUrl });\n\t\tif (!inserted) {\n\t\t\tURL.revokeObjectURL(blobUrl);\n\t\t\tthis.blobUrls.delete(blobUrl);\n\t\t\treturn;\n\t\t}\n\n\t\t// Find the newly inserted image block\n\t\tconst state: EditorState = context.getState();\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return;\n\t\tconst imageBlockId: BlockId = sel.nodeId;\n\n\t\tthis.uploadStates.set(imageBlockId, 'uploading');\n\n\t\t// Upload if service is registered\n\t\tconst uploadService = context.getService(IMAGE_UPLOAD_SERVICE);\n\t\tif (uploadService) {\n\t\t\tthis.uploadFile(context, file, imageBlockId, blobUrl);\n\t\t} else {\n\t\t\tthis.uploadStates.set(imageBlockId, 'complete');\n\t\t}\n\t}\n\n\tprivate async uploadFile(\n\t\tcontext: PluginContext,\n\t\tfile: File,\n\t\timageBlockId: BlockId,\n\t\tblobUrl: string,\n\t): Promise<void> {\n\t\tconst uploadService = context.getService(IMAGE_UPLOAD_SERVICE);\n\t\tif (!uploadService) return;\n\n\t\ttry {\n\t\t\tconst result = await uploadService.upload(file);\n\t\t\tthis.uploadStates.set(imageBlockId, 'complete');\n\n\t\t\t// Replace blob URL with uploaded URL\n\t\t\tconst state: EditorState = context.getState();\n\t\t\tconst block: BlockNode | undefined = state.getBlock(imageBlockId);\n\t\t\tif (!block) return;\n\n\t\t\tconst path: BlockId[] | undefined = state.getNodePath(imageBlockId);\n\t\t\tif (!path) return;\n\n\t\t\tconst merged: BlockAttrs = {\n\t\t\t\t...(block.attrs ?? {}),\n\t\t\t\tsrc: result.url,\n\t\t\t\t...(result.width !== undefined ? { width: result.width } : {}),\n\t\t\t\t...(result.height !== undefined ? { height: result.height } : {}),\n\t\t\t};\n\n\t\t\tconst tr: Transaction = state.transaction('command').setNodeAttr(path, merged).build();\n\t\t\tcontext.dispatch(tr);\n\n\t\t\t// Clean up blob URL\n\t\t\tURL.revokeObjectURL(blobUrl);\n\t\t\tthis.blobUrls.delete(blobUrl);\n\t\t} catch {\n\t\t\tthis.uploadStates.set(imageBlockId, 'error');\n\t\t\tcontext.announce(this.locale.uploadFailed);\n\t\t}\n\t}\n\n\tprivate registerToolbarItem(context: PluginContext): void {\n\t\tconst icon =\n\t\t\t'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path d=\"M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z\"/></svg>';\n\n\t\tcontext.registerToolbarItem({\n\t\t\tid: 'image',\n\t\t\tgroup: 'insert',\n\t\t\ticon,\n\t\t\tlabel: this.locale.insertImage,\n\t\t\ttooltip: this.locale.insertImageTooltip,\n\t\t\tcommand: 'insertImage',\n\t\t\tpopupType: 'custom',\n\t\t\trenderPopup: (container, ctx, onClose) => {\n\t\t\t\trenderImagePopup(container, ctx, {\n\t\t\t\t\tacceptedTypes: this.config.acceptedTypes,\n\t\t\t\t\tlocale: this.locale,\n\t\t\t\t\tonFileInsert: (c, file) => this.handleFileInsert(c, file),\n\t\t\t\t\tonClose,\n\t\t\t\t});\n\t\t\t},\n\t\t});\n\t}\n\n\tprivate isAcceptedType(mimeType: string): boolean {\n\t\treturn this.config.acceptedTypes.some(\n\t\t\t(accepted) =>\n\t\t\t\taccepted === mimeType ||\n\t\t\t\t(accepted.endsWith('/*') && mimeType.startsWith(accepted.slice(0, -1))),\n\t\t);\n\t}\n\n\tprivate isImageSelected(state: EditorState): boolean {\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return false;\n\t\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\t\treturn block?.type === 'image';\n\t}\n\n\tprivate announceImageSelection(state: EditorState): void {\n\t\tif (!this.context) return;\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return;\n\n\t\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\t\tif (!block || block.type !== 'image') return;\n\n\t\tconst alt: string = (block.attrs?.alt as string | undefined) ?? '';\n\t\tconst width: number | undefined = block.attrs?.width as number | undefined;\n\t\tconst height: number | undefined = block.attrs?.height as number | undefined;\n\n\t\tconst parts: string[] = [this.locale.imageSelected];\n\t\tif (alt) parts.push(`${this.locale.altTextPrefix}${alt}.`);\n\t\tif (width !== undefined && height !== undefined) {\n\t\t\tparts.push(this.locale.imageSizeAnnounce(width, height));\n\t\t}\n\n\t\tconst shrinkKey: string | null = this.resolvedKeymap.shrinkWidth ?? null;\n\t\tconst growKey: string | null = this.resolvedKeymap.growWidth ?? null;\n\t\tif (shrinkKey && growKey) {\n\t\t\tparts.push(this.locale.resizeHint(formatShortcut(shrinkKey), formatShortcut(growKey)));\n\t\t}\n\n\t\tthis.context.announce(parts.join(' '));\n\t}\n\n\tprivate announceCurrentSize(context: PluginContext): void {\n\t\tconst state = context.getState();\n\t\tconst sel = state.selection;\n\t\tif (!isNodeSelection(sel)) return;\n\n\t\tconst block: BlockNode | undefined = state.getBlock(sel.nodeId);\n\t\tif (!block || block.type !== 'image') return;\n\n\t\tconst width: number | undefined = block.attrs?.width as number | undefined;\n\t\tconst height: number | undefined = block.attrs?.height as number | undefined;\n\t\tif (width !== undefined && height !== undefined) {\n\t\t\tcontext.announce(this.locale.imageResized(width, height));\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as t}from"./HTMLUtils-C6B4UT00.mjs";import{
|
|
2
|
-
//# sourceMappingURL=InlineStyleMarkSpec-
|
|
1
|
+
import{r as t}from"./HTMLUtils-C6B4UT00.mjs";import{c as r}from"./StyleRuntime-BFKu52mS.mjs";function e(e){const{type:n,rank:s,valueAttr:a,domStyleProperty:o,cssProperty:l,validate:i,validateOnParse:u,transformParsed:p}=e,c=t=>String(t.attrs?.[a]??"");return{type:n,rank:s,attrs:{[a]:{default:""}},toDOM(t){const e=document.createElement("span"),n=c(t);return n&&r(e,o,n),e},toHTMLString:(r,e,n)=>{const s=c(r);if(!s||!i(s))return e;const a=`${l}: ${t(s)}`;return`<span${n?.styleAttr(a)??` style="${a}"`}>${e}</span>`},toHTMLStyle:r=>{const e=c(r);return e&&i(e)?`${l}: ${t(e)}`:null},parseHTML:[{tag:"span",getAttrs:t=>{const r=t.style[o]??"";if(!r)return!1;if(u&&!i(r))return!1;const e=p?p(r):r;return{[a]:e}}}],sanitize:{tags:["span"]}}}export{e as t};
|
|
2
|
+
//# sourceMappingURL=InlineStyleMarkSpec-C6LJT7kp.mjs.map
|
package/dist/chunks/{InlineStyleMarkSpec-4R6DC7ij.mjs.map → InlineStyleMarkSpec-C6LJT7kp.mjs.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InlineStyleMarkSpec-
|
|
1
|
+
{"version":3,"file":"InlineStyleMarkSpec-C6LJT7kp.mjs","names":[],"sources":["../../src/plugins/shared/InlineStyleMarkSpec.ts"],"sourcesContent":["/**\n * Factory for inline marks that carry a single CSS style declaration (text\n * color, highlight, font family, font size). These marks share the same DOM\n * rendering, HTML import/export, and `<span>`-based parse shape and differ only\n * in the attribute name, CSS property, and validation.\n */\n\nimport type { Mark } from '../../model/Document.js';\nimport { escapeHTML } from '../../model/HTMLUtils.js';\nimport type { MarkSpec } from '../../model/MarkSpec.js';\nimport type { HTMLExportContext } from '../../model/NodeSpec.js';\nimport { setStyleProperty } from '../../style/StyleRuntime.js';\n\nexport interface InlineStyleMarkConfig {\n\t/** Mark type name, e.g. `'textColor'`. */\n\treadonly type: string;\n\t/** Nesting rank (lower renders closer to the text). */\n\treadonly rank: number;\n\t/** Attribute key holding the value, e.g. `'color'` / `'family'` / `'size'`. */\n\treadonly valueAttr: string;\n\t/** camelCase DOM style property, e.g. `'backgroundColor'`. */\n\treadonly domStyleProperty: string;\n\t/** kebab-case CSS property for HTML export, e.g. `'background-color'`. */\n\treadonly cssProperty: string;\n\t/** Validates a value for HTML export (and optionally on parse). */\n\treadonly validate: (value: string) => boolean;\n\t/** When true, `validate` is also enforced while parsing HTML. */\n\treadonly validateOnParse?: boolean;\n\t/** Optional transform applied to a parsed value (e.g. CSS quote normalization). */\n\treadonly transformParsed?: (value: string) => string;\n}\n\n/** Builds a MarkSpec for a single-CSS-property inline mark. */\nexport function createInlineStyleMarkSpec(config: InlineStyleMarkConfig): MarkSpec {\n\tconst {\n\t\ttype,\n\t\trank,\n\t\tvalueAttr,\n\t\tdomStyleProperty,\n\t\tcssProperty,\n\t\tvalidate,\n\t\tvalidateOnParse,\n\t\ttransformParsed,\n\t} = config;\n\n\tconst readValue = (mark: Mark): string => String(mark.attrs?.[valueAttr] ?? '');\n\n\treturn {\n\t\ttype,\n\t\trank,\n\t\tattrs: { [valueAttr]: { default: '' } },\n\t\ttoDOM(mark) {\n\t\t\tconst span: HTMLElement = document.createElement('span');\n\t\t\tconst value: string = readValue(mark);\n\t\t\tif (value) {\n\t\t\t\tsetStyleProperty(span, domStyleProperty, value);\n\t\t\t}\n\t\t\treturn span;\n\t\t},\n\t\ttoHTMLString: (mark: Mark, content: string, ctx?: HTMLExportContext) => {\n\t\t\tconst value: string = readValue(mark);\n\t\t\tif (!value || !validate(value)) return content;\n\t\t\tconst decl: string = `${cssProperty}: ${escapeHTML(value)}`;\n\t\t\tconst attr: string = ctx?.styleAttr(decl) ?? ` style=\"${decl}\"`;\n\t\t\treturn `<span${attr}>${content}</span>`;\n\t\t},\n\t\ttoHTMLStyle: (mark: Mark) => {\n\t\t\tconst value: string = readValue(mark);\n\t\t\tif (!value || !validate(value)) return null;\n\t\t\treturn `${cssProperty}: ${escapeHTML(value)}`;\n\t\t},\n\t\tparseHTML: [\n\t\t\t{\n\t\t\t\ttag: 'span',\n\t\t\t\tgetAttrs: (el: HTMLElement) => {\n\t\t\t\t\tconst raw: string =\n\t\t\t\t\t\t(el.style as unknown as Record<string, string>)[domStyleProperty] ?? '';\n\t\t\t\t\tif (!raw) return false;\n\t\t\t\t\tif (validateOnParse && !validate(raw)) return false;\n\t\t\t\t\tconst value: string = transformParsed ? transformParsed(raw) : raw;\n\t\t\t\t\treturn { [valueAttr]: value };\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t\tsanitize: { tags: ['span'] },\n\t};\n}\n"],"mappings":"6FAiCA,SAAgB,EAA0B,GACzC,MAAM,KACL,EAAA,KACA,EAAA,UACA,EAAA,iBACA,EAAA,YACA,EAAA,SACA,EAAA,gBACA,EAAA,gBACA,GACG,EAEE,EAAa,GAAuB,OAAO,EAAK,QAAQ,IAAc,IAE5E,MAAO,CACN,OACA,OACA,MAAO,CAAA,CAAG,GAAY,CAAE,QAAS,KACjC,KAAA,CAAM,GACL,MAAM,EAAoB,SAAS,cAAc,QAC3C,EAAgB,EAAU,GAIhC,OAHI,GACH,EAAiB,EAAM,EAAkB,GAEnC,CACR,EACA,aAAA,CAAe,EAAY,EAAiB,KAC3C,MAAM,EAAgB,EAAU,GAChC,IAAK,IAAU,EAAS,GAAQ,OAAO,EACvC,MAAM,EAAe,GAAG,MAAgB,EAAW,KAEnD,MAAO,QADc,GAAK,UAAU,IAAS,WAAW,QACjC,YAExB,YAAc,IACb,MAAM,EAAgB,EAAU,GAChC,OAAK,GAAU,EAAS,GACjB,GAAG,MAAgB,EAAW,KADE,MAGxC,UAAW,CACV,CACC,IAAK,OACL,SAAW,IACV,MAAM,EACJ,EAAG,MAA4C,IAAqB,GACtE,IAAK,EAAK,OAAO,EACjB,GAAI,IAAoB,EAAS,GAAM,OAAO,EAC9C,MAAM,EAAgB,EAAkB,EAAgB,GAAO,EAC/D,MAAO,CAAA,CAAG,GAAY,MAIzB,SAAU,CAAE,KAAM,CAAC,SAErB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{h as t,u as e}from"./Document-r7vLOAGA.mjs";import{d as n,o as i,t as c,u as s}from"./Selection-HYoVFpAx.mjs";import{a as o,t as r}from"./TypeBrands-Ch9856Gb.mjs";import{r as l}from"./AttrRegistry-DcDVyzLm.mjs";import{n as a}from"./DomUtils-Bf7JLFlx.mjs";import{t as d}from"./RangeIterator-o8v3msQe.mjs";import{s as u}from"./StyleRuntime-BAutwDZT.mjs";import{c as h,o as m}from"./PluginHelpers-KDuTlmXm.mjs";function p(t,e,n){return"checklist"===t?{listType:t,indent:e,checked:n??!1}:{listType:t,indent:e}}function k(t,e){const i=t.getState(),c=i.selection;return!!s(c)&&(c.anchor.blockId!==c.head.blockId?function(t,e,i){const c=e.selection;if(!s(c))return!1;const r=n(c,e.getBlockOrder()),a=g(e,r,i),u=e.transaction("command");return d(e,r,t=>{const n=e.getBlock(t);if(n)if(a)u.setBlockType(t,o("paragraph"));else{const e=l(n,"list_item")?n.attrs.indent:0,c=l(n,"list_item")&&"checklist"===n.attrs.listType&&n.attrs.checked;u.setBlockType(t,o("list_item"),p(i,e,c))}}),u.setSelection(c),t.dispatch(u.build()),!0}(t,i,e):function(t,e,n){const i=e.selection;if(!s(i))return!1;const c=e.getBlock(i.anchor.blockId);if(!c)return!1;if("list_item"===c.type&&c.attrs?.listType===n){const n=e.transaction("command").setBlockType(i.anchor.blockId,o("paragraph")).setSelection(i).build();return t.dispatch(n),!0}const r=p(n,l(c,"list_item")?c.attrs.indent:0),a=e.transaction("command").setBlockType(i.anchor.blockId,o("list_item"),r).setSelection(i).build();return t.dispatch(a),!0}(t,i,e))}function b(t,e,i){const c=t.getState();if(!s(c.selection))return!1;if(c.selection.anchor.blockId!==c.selection.head.blockId)return function(t,e,i,c){const r=e.selection;if(!s(r))return!1;const a=n(r,e.getBlockOrder()),u=e.transaction("command");let h=!1;return d(e,a,t=>{const n=e.getBlock(t);if(!n||!l(n,"list_item"))return;const s=n.attrs.indent+i;s<0||s>c||(u.setBlockType(t,o("list_item"),p(n.attrs.listType,s,n.attrs.checked)),h=!0)}),!!h&&(u.setSelection(r),t.dispatch(u.build()),!0)}(t,c,e,i);const r=c.selection,a=c.getBlock(r.anchor.blockId);if(!a||!l(a,"list_item"))return!1;const u=a.attrs.indent+e;if(u<0||u>i)return!1;const h=p(a.attrs.listType,u,a.attrs.checked),m=c.transaction("command").setBlockType(r.anchor.blockId,o("list_item"),h).setSelection(r).build();return t.dispatch(m),!0}function f(t,e){const{interactiveCheckboxes:n,targetId:i,locale:c}=e;if(t.isReadOnly()&&!n)return!1;const r=t.getState(),a=i??(s(r.selection)?r.selection.anchor.blockId:null);if(!a)return!1;const d=r.getBlock(a);if(!d||!l(d,"list_item")||"checklist"!==d.attrs.listType)return!1;const u=!d.attrs.checked,h=p(d.attrs.listType,d.attrs.indent,u),m=r.transaction("command").setBlockType(a,o("list_item"),h).setSelection(r.selection).build();return t.dispatch(m),t.announce(u?c.checkedAnnouncement:c.uncheckedAnnouncement),!0}function g(t,e,n){let i=!0;return d(t,e,e=>{const c=t.getBlock(e);c&&"list_item"===c.type&&c.attrs?.listType===n||(i=!1)}),i}var y=[{type:"bullet",icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>',inputPattern:/^[-*] $/,inputPrefix:"- "},{type:"ordered",icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>',inputPattern:/^\d+\. $/,inputPrefix:"1. "},{type:"checklist",icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 7h-9v2h9V7zm0 8h-9v2h9v-2zM5.54 11L2 7.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L5.54 11zm0 8L2 15.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L5.54 19z"/></svg>',inputPattern:/^\[[ x]] $/,inputPrefix:"[ ] "}];function v(t,e){const n=t.getState();if(!s(n.selection))return!1;const c=n.selection;if(!i(c))return!1;const o=n.getBlock(c.anchor.blockId);return!(!o||"list_item"!==o.type)&&e({state:n,block:o,blockId:c.anchor.blockId,offset:c.anchor.offset})}var x={bulletList:"Bullet List",numberedList:"Numbered List",checklist:"Checklist",checkboxLabel:"To-do item",checkedAnnouncement:"Checked",uncheckedAnnouncement:"Unchecked"},I=/* #__PURE__ */Object.assign({"./locales/ar.ts":()=>import("./ar-Cy_77yRf.mjs"),"./locales/de.ts":()=>import("./de-CaeEIyzV.mjs"),"./locales/es.ts":()=>import("./es-DFokqQst.mjs"),"./locales/fr.ts":()=>import("./fr-5HkMazAf.mjs"),"./locales/hi.ts":()=>import("./hi-BzbMD_cP.mjs"),"./locales/pt.ts":()=>import("./pt-CR4j8gtE.mjs"),"./locales/ru.ts":()=>import("./ru-DiwQV30V.mjs"),"./locales/zh.ts":()=>import("./zh-BQqf3wJZ.mjs")});async function T(t){return h(I,t,x)}var B="notectl-checklist-marker";function L(t,e){return{bullet:e.bulletList,ordered:e.numberedList,checklist:e.checklist}[t]}function w(t,e){if(!s(t.selection))return!1;if(t.selection.anchor.blockId!==t.selection.head.blockId)return g(t,n(t.selection,t.getBlockOrder()),e);const i=t.getBlock(t.selection.anchor.blockId);return"list_item"===i?.type&&i.attrs?.listType===e}var S={types:["bullet","ordered","checklist"],maxIndent:4},C=class{id="list";name="List";priority=35;config;locale;context=null;checkboxClickHandler=null;constructor(t){this.config={...S,...t}}async init(n){this.context=n,this.locale=await m(n,this.config.locale,x,T);const r=this.getEnabledTypes();n.registerStyleSheet("\n/* List Wrappers — suppress native markers, we use ::before pseudo-elements */\n.notectl-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n/* Ordered List — each <ol> wrapper resets the counter */\n.notectl-list--ordered {\n\tcounter-reset: notectl-ordered;\n}\n\n/* List Items */\n.notectl-list-item {\n\tmargin: 0;\n\tpadding: 2px 0;\n\tpadding-inline-start: 24px;\n\tmin-height: 1.6em;\n\tposition: relative;\n}\n\n.notectl-list-item::before {\n\tposition: absolute;\n\tinset-inline-start: 0;\n\tdisplay: inline-block;\n\twidth: 24px;\n\ttext-align: center;\n\t-webkit-user-select: none;\n\tuser-select: none;\n\tpointer-events: none;\n}\n\n/* Bullet List */\n.notectl-list-item--bullet::before {\n\tcontent: '\\2022';\n\tcolor: var(--notectl-fg);\n}\n\n/* Ordered List — counter increment per item */\n.notectl-list-item--ordered {\n\tcounter-increment: notectl-ordered;\n}\n\n.notectl-list-item--ordered::before {\n\tcontent: counter(notectl-ordered) '.';\n\tcolor: var(--notectl-fg);\n\tfont-size: 14px;\n}\n\n/* Checklist — the marker is a real role=\"checkbox\" element (see ListMarker.ts)\n positioned over the reserved marker zone. inline-block keeps the parent's\n line-through (on checked items) from striking the glyph. */\n.notectl-checklist-marker {\n\tposition: absolute;\n\tinset-inline-start: 0;\n\tdisplay: inline-block;\n\twidth: 24px;\n\ttext-align: center;\n\tfont-size: 16px;\n\tcolor: var(--notectl-fg-muted);\n\tcursor: pointer;\n\ttext-decoration: none;\n\t-webkit-user-select: none;\n\tuser-select: none;\n}\n\n.notectl-checklist-marker::before {\n\tcontent: '\\2610';\n}\n\n.notectl-list-item--checklist[data-checked=\"true\"] .notectl-checklist-marker {\n\tcolor: var(--notectl-success);\n}\n\n.notectl-list-item--checklist[data-checked=\"true\"] .notectl-checklist-marker::before {\n\tcontent: '\\2611';\n}\n\n.notectl-list-item--checklist[data-checked=\"true\"] {\n\tcolor: var(--notectl-fg-muted);\n\ttext-decoration: line-through;\n}\n"),this.registerNodeSpec(n),function(t,e,n,i){for(const c of n)t.registerCommand(`toggleList:${c.type}`,()=>k(t,c.type));if(t.registerCommand("indentListItem",()=>b(t,1,e.maxIndent)),t.registerCommand("outdentListItem",()=>b(t,-1,e.maxIndent)),e.types.includes("checklist")){const n=()=>f(t,{interactiveCheckboxes:e.interactiveCheckboxes,locale:i});t.registerCommand("toggleChecklistItem",n,{readonlyAllowed:!0}),t.registerKeymap({"Mod-Enter":n},{priority:"navigation"})}t.registerKeymap({Tab:()=>b(t,1,e.maxIndent),"Shift-Tab":()=>b(t,-1,e.maxIndent)})}(n,this.config,r,this.locale),function(n){n.registerKeymap({Enter:()=>function(n){return v(n,i=>{if(""===t(i.block)){const t=i.state.transaction("input").setBlockType(i.blockId,o("paragraph")).setSelection(i.state.selection).build();return n.dispatch(t),!0}const s=e(),r=p(l(i.block,"list_item")?i.block.attrs.listType:"bullet",l(i.block,"list_item")?i.block.attrs.indent:0),a=i.state.transaction("input").splitBlock(i.blockId,i.offset,s).setBlockType(s,o("list_item"),r).setSelection(c(s,0)).build();return n.dispatch(a),!0})}(n),Backspace:()=>function(t){return v(t,e=>{if(0!==e.offset)return!1;const n=e.state.transaction("input").setBlockType(e.blockId,o("paragraph")).setSelection(e.state.selection).build();return t.dispatch(n),!0})}(n)})}(n),function(t,e){for(const n of e)t.registerInputRule({pattern:n.inputPattern,handler:(t,e,r,l)=>{const a=t.selection;if(!s(a))return null;if(!i(a))return null;const d=t.getBlock(a.anchor.blockId);if(!d||"paragraph"!==d.type)return null;const u=e[0]??"",h=u.length,m=p(n.type,0,u.includes("[x]"));return t.transaction("input").deleteTextAt(a.anchor.blockId,r,r+h).setBlockType(a.anchor.blockId,o("list_item"),m).setSelection(c(a.anchor.blockId,0)).build()}})}(n,r),function(t,e,n){for(const i of e)t.registerToolbarItem({id:`list-${i.type}`,group:"block",icon:i.icon,label:L(i.type,n),command:`toggleList:${i.type}`,isActive:t=>w(t,i.type)})}(n,r,this.locale),this.registerCheckboxClickHandler(n)}destroy(){this.checkboxClickHandler&&this.context&&this.context.getContainer().removeEventListener("mousedown",this.checkboxClickHandler),this.checkboxClickHandler=null,this.context=null}registerNodeSpec(t){const e=this.locale.checkboxLabel;t.registerNodeSpec({type:"list_item",group:"block",content:{allow:["text"]},attrs:{listType:{default:"bullet"},indent:{default:0},checked:{default:!1}},toDOM(t){const n=t.attrs?.listType??"bullet",i=t.attrs?.indent??0,c=t.attrs?.checked??!1,s=a("li",t.id);return s.setAttribute("role","listitem"),s.setAttribute("data-list-type",n),s.setAttribute("data-indent",String(i)),s.className=`notectl-list-item notectl-list-item--${n}`,i>0&&u(s,"marginLeft",24*i+"px"),"checklist"===n&&(s.setAttribute("data-checked",String(c)),s.appendChild(function(t,e){const n=document.createElement("span");return n.className=B,n.setAttribute("role","checkbox"),n.setAttribute("aria-checked",String(t)),n.setAttribute("aria-label",e),n.setAttribute("contenteditable","false"),n.setAttribute("data-widget","true"),n}(c,e))),s.setAttribute("aria-level",String(i+1)),s},wrapper(t){const e=t.attrs?.listType??"bullet";return{tag:"ordered"===e?"ol":"ul",key:`list-${e}`,className:`notectl-list notectl-list--${e}`,attrs:{role:"list"}}},toHTML(t,e){const n=t.attrs?.listType??"bullet",i=t.attrs?.checked??!1;return"checklist"===n?`<li role="checkbox" aria-checked="${String(i)}"><input type="checkbox" disabled${i?" checked":""}>${e||"<br>"}</li>`:`<li>${e||"<br>"}</li>`},parseHTML:[{tag:"li"}],sanitize:{tags:["ul","ol","li","input"],attrs:["type","disabled","checked","role","aria-checked"]}})}registerCheckboxClickHandler(t){this.config.types.includes("checklist")&&(this.checkboxClickHandler=e=>{if(t.isReadOnly()&&!this.config.interactiveCheckboxes)return;const n=e.target;if(!(n instanceof HTMLElement))return;const i=n.closest(`.${B}`);if(!i)return;const c=i.closest(".notectl-list-item--checklist");if(!c)return;e.preventDefault();const s=c.getAttribute("data-block-id");s&&f(t,{interactiveCheckboxes:this.config.interactiveCheckboxes,targetId:r(s),locale:this.locale})},t.getContainer().addEventListener("mousedown",this.checkboxClickHandler))}getEnabledTypes(){return y.filter(t=>this.config.types.includes(t.type))}};export{x as n,T as r,C as t};
|
|
2
|
-
//# sourceMappingURL=ListPlugin-
|
|
1
|
+
import{h as t,u as e}from"./Document-r7vLOAGA.mjs";import{d as n,o as i,t as c,u as s}from"./Selection-HYoVFpAx.mjs";import{a as o,t as r}from"./TypeBrands-Ch9856Gb.mjs";import{r as l}from"./AttrRegistry-DcDVyzLm.mjs";import{n as a}from"./DomUtils-Bf7JLFlx.mjs";import{t as d}from"./RangeIterator-o8v3msQe.mjs";import{c as u}from"./StyleRuntime-BFKu52mS.mjs";import{c as h,o as m}from"./PluginHelpers-KDuTlmXm.mjs";function p(t,e,n){return"checklist"===t?{listType:t,indent:e,checked:n??!1}:{listType:t,indent:e}}function k(t,e){const i=t.getState(),c=i.selection;return!!s(c)&&(c.anchor.blockId!==c.head.blockId?function(t,e,i){const c=e.selection;if(!s(c))return!1;const r=n(c,e.getBlockOrder()),a=g(e,r,i),u=e.transaction("command");return d(e,r,t=>{const n=e.getBlock(t);if(n)if(a)u.setBlockType(t,o("paragraph"));else{const e=l(n,"list_item")?n.attrs.indent:0,c=l(n,"list_item")&&"checklist"===n.attrs.listType&&n.attrs.checked;u.setBlockType(t,o("list_item"),p(i,e,c))}}),u.setSelection(c),t.dispatch(u.build()),!0}(t,i,e):function(t,e,n){const i=e.selection;if(!s(i))return!1;const c=e.getBlock(i.anchor.blockId);if(!c)return!1;if("list_item"===c.type&&c.attrs?.listType===n){const n=e.transaction("command").setBlockType(i.anchor.blockId,o("paragraph")).setSelection(i).build();return t.dispatch(n),!0}const r=p(n,l(c,"list_item")?c.attrs.indent:0),a=e.transaction("command").setBlockType(i.anchor.blockId,o("list_item"),r).setSelection(i).build();return t.dispatch(a),!0}(t,i,e))}function b(t,e,i){const c=t.getState();if(!s(c.selection))return!1;if(c.selection.anchor.blockId!==c.selection.head.blockId)return function(t,e,i,c){const r=e.selection;if(!s(r))return!1;const a=n(r,e.getBlockOrder()),u=e.transaction("command");let h=!1;return d(e,a,t=>{const n=e.getBlock(t);if(!n||!l(n,"list_item"))return;const s=n.attrs.indent+i;s<0||s>c||(u.setBlockType(t,o("list_item"),p(n.attrs.listType,s,n.attrs.checked)),h=!0)}),!!h&&(u.setSelection(r),t.dispatch(u.build()),!0)}(t,c,e,i);const r=c.selection,a=c.getBlock(r.anchor.blockId);if(!a||!l(a,"list_item"))return!1;const u=a.attrs.indent+e;if(u<0||u>i)return!1;const h=p(a.attrs.listType,u,a.attrs.checked),m=c.transaction("command").setBlockType(r.anchor.blockId,o("list_item"),h).setSelection(r).build();return t.dispatch(m),!0}function f(t,e){const{interactiveCheckboxes:n,targetId:i,locale:c}=e;if(t.isReadOnly()&&!n)return!1;const r=t.getState(),a=i??(s(r.selection)?r.selection.anchor.blockId:null);if(!a)return!1;const d=r.getBlock(a);if(!d||!l(d,"list_item")||"checklist"!==d.attrs.listType)return!1;const u=!d.attrs.checked,h=p(d.attrs.listType,d.attrs.indent,u),m=r.transaction("command").setBlockType(a,o("list_item"),h).setSelection(r.selection).build();return t.dispatch(m),t.announce(u?c.checkedAnnouncement:c.uncheckedAnnouncement),!0}function g(t,e,n){let i=!0;return d(t,e,e=>{const c=t.getBlock(e);c&&"list_item"===c.type&&c.attrs?.listType===n||(i=!1)}),i}var y=[{type:"bullet",icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>',inputPattern:/^[-*] $/,inputPrefix:"- "},{type:"ordered",icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>',inputPattern:/^\d+\. $/,inputPrefix:"1. "},{type:"checklist",icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 7h-9v2h9V7zm0 8h-9v2h9v-2zM5.54 11L2 7.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L5.54 11zm0 8L2 15.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L5.54 19z"/></svg>',inputPattern:/^\[[ x]] $/,inputPrefix:"[ ] "}];function v(t,e){const n=t.getState();if(!s(n.selection))return!1;const c=n.selection;if(!i(c))return!1;const o=n.getBlock(c.anchor.blockId);return!(!o||"list_item"!==o.type)&&e({state:n,block:o,blockId:c.anchor.blockId,offset:c.anchor.offset})}var x={bulletList:"Bullet List",numberedList:"Numbered List",checklist:"Checklist",checkboxLabel:"To-do item",checkedAnnouncement:"Checked",uncheckedAnnouncement:"Unchecked"},I=/* #__PURE__ */Object.assign({"./locales/ar.ts":()=>import("./ar-Cy_77yRf.mjs"),"./locales/de.ts":()=>import("./de-CaeEIyzV.mjs"),"./locales/es.ts":()=>import("./es-DFokqQst.mjs"),"./locales/fr.ts":()=>import("./fr-5HkMazAf.mjs"),"./locales/hi.ts":()=>import("./hi-BzbMD_cP.mjs"),"./locales/pt.ts":()=>import("./pt-CR4j8gtE.mjs"),"./locales/ru.ts":()=>import("./ru-DiwQV30V.mjs"),"./locales/zh.ts":()=>import("./zh-BQqf3wJZ.mjs")});async function T(t){return h(I,t,x)}var B="notectl-checklist-marker";function L(t,e){return{bullet:e.bulletList,ordered:e.numberedList,checklist:e.checklist}[t]}function w(t,e){if(!s(t.selection))return!1;if(t.selection.anchor.blockId!==t.selection.head.blockId)return g(t,n(t.selection,t.getBlockOrder()),e);const i=t.getBlock(t.selection.anchor.blockId);return"list_item"===i?.type&&i.attrs?.listType===e}var S={types:["bullet","ordered","checklist"],maxIndent:4},C=class{id="list";name="List";priority=35;config;locale;context=null;checkboxClickHandler=null;constructor(t){this.config={...S,...t}}async init(n){this.context=n,this.locale=await m(n,this.config.locale,x,T);const r=this.getEnabledTypes();n.registerStyleSheet("\n/* List Wrappers — suppress native markers, we use ::before pseudo-elements */\n.notectl-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n/* Ordered List — each <ol> wrapper resets the counter */\n.notectl-list--ordered {\n\tcounter-reset: notectl-ordered;\n}\n\n/* List Items */\n.notectl-list-item {\n\tmargin: 0;\n\tpadding: 2px 0;\n\tpadding-inline-start: 24px;\n\tmin-height: 1.6em;\n\tposition: relative;\n}\n\n.notectl-list-item::before {\n\tposition: absolute;\n\tinset-inline-start: 0;\n\tdisplay: inline-block;\n\twidth: 24px;\n\ttext-align: center;\n\t-webkit-user-select: none;\n\tuser-select: none;\n\tpointer-events: none;\n}\n\n/* Bullet List */\n.notectl-list-item--bullet::before {\n\tcontent: '\\2022';\n\tcolor: var(--notectl-fg);\n}\n\n/* Ordered List — counter increment per item */\n.notectl-list-item--ordered {\n\tcounter-increment: notectl-ordered;\n}\n\n.notectl-list-item--ordered::before {\n\tcontent: counter(notectl-ordered) '.';\n\tcolor: var(--notectl-fg);\n\tfont-size: 14px;\n}\n\n/* Checklist — the marker is a real role=\"checkbox\" element (see ListMarker.ts)\n positioned over the reserved marker zone. inline-block keeps the parent's\n line-through (on checked items) from striking the glyph. */\n.notectl-checklist-marker {\n\tposition: absolute;\n\tinset-inline-start: 0;\n\tdisplay: inline-block;\n\twidth: 24px;\n\ttext-align: center;\n\tfont-size: 16px;\n\tcolor: var(--notectl-fg-muted);\n\tcursor: pointer;\n\ttext-decoration: none;\n\t-webkit-user-select: none;\n\tuser-select: none;\n}\n\n.notectl-checklist-marker::before {\n\tcontent: '\\2610';\n}\n\n.notectl-list-item--checklist[data-checked=\"true\"] .notectl-checklist-marker {\n\tcolor: var(--notectl-success);\n}\n\n.notectl-list-item--checklist[data-checked=\"true\"] .notectl-checklist-marker::before {\n\tcontent: '\\2611';\n}\n\n.notectl-list-item--checklist[data-checked=\"true\"] {\n\tcolor: var(--notectl-fg-muted);\n\ttext-decoration: line-through;\n}\n"),this.registerNodeSpec(n),function(t,e,n,i){for(const c of n)t.registerCommand(`toggleList:${c.type}`,()=>k(t,c.type));if(t.registerCommand("indentListItem",()=>b(t,1,e.maxIndent)),t.registerCommand("outdentListItem",()=>b(t,-1,e.maxIndent)),e.types.includes("checklist")){const n=()=>f(t,{interactiveCheckboxes:e.interactiveCheckboxes,locale:i});t.registerCommand("toggleChecklistItem",n,{readonlyAllowed:!0}),t.registerKeymap({"Mod-Enter":n},{priority:"navigation"})}t.registerKeymap({Tab:()=>b(t,1,e.maxIndent),"Shift-Tab":()=>b(t,-1,e.maxIndent)})}(n,this.config,r,this.locale),function(n){n.registerKeymap({Enter:()=>function(n){return v(n,i=>{if(""===t(i.block)){const t=i.state.transaction("input").setBlockType(i.blockId,o("paragraph")).setSelection(i.state.selection).build();return n.dispatch(t),!0}const s=e(),r=p(l(i.block,"list_item")?i.block.attrs.listType:"bullet",l(i.block,"list_item")?i.block.attrs.indent:0),a=i.state.transaction("input").splitBlock(i.blockId,i.offset,s).setBlockType(s,o("list_item"),r).setSelection(c(s,0)).build();return n.dispatch(a),!0})}(n),Backspace:()=>function(t){return v(t,e=>{if(0!==e.offset)return!1;const n=e.state.transaction("input").setBlockType(e.blockId,o("paragraph")).setSelection(e.state.selection).build();return t.dispatch(n),!0})}(n)})}(n),function(t,e){for(const n of e)t.registerInputRule({pattern:n.inputPattern,handler:(t,e,r,l)=>{const a=t.selection;if(!s(a))return null;if(!i(a))return null;const d=t.getBlock(a.anchor.blockId);if(!d||"paragraph"!==d.type)return null;const u=e[0]??"",h=u.length,m=p(n.type,0,u.includes("[x]"));return t.transaction("input").deleteTextAt(a.anchor.blockId,r,r+h).setBlockType(a.anchor.blockId,o("list_item"),m).setSelection(c(a.anchor.blockId,0)).build()}})}(n,r),function(t,e,n){for(const i of e)t.registerToolbarItem({id:`list-${i.type}`,group:"block",icon:i.icon,label:L(i.type,n),command:`toggleList:${i.type}`,isActive:t=>w(t,i.type)})}(n,r,this.locale),this.registerCheckboxClickHandler(n)}destroy(){this.checkboxClickHandler&&this.context&&this.context.getContainer().removeEventListener("mousedown",this.checkboxClickHandler),this.checkboxClickHandler=null,this.context=null}registerNodeSpec(t){const e=this.locale.checkboxLabel;t.registerNodeSpec({type:"list_item",group:"block",content:{allow:["text"]},attrs:{listType:{default:"bullet"},indent:{default:0},checked:{default:!1}},toDOM(t){const n=t.attrs?.listType??"bullet",i=t.attrs?.indent??0,c=t.attrs?.checked??!1,s=a("li",t.id);return s.setAttribute("role","listitem"),s.setAttribute("data-list-type",n),s.setAttribute("data-indent",String(i)),s.className=`notectl-list-item notectl-list-item--${n}`,i>0&&u(s,"marginLeft",24*i+"px"),"checklist"===n&&(s.setAttribute("data-checked",String(c)),s.appendChild(function(t,e){const n=document.createElement("span");return n.className=B,n.setAttribute("role","checkbox"),n.setAttribute("aria-checked",String(t)),n.setAttribute("aria-label",e),n.setAttribute("contenteditable","false"),n.setAttribute("data-widget","true"),n}(c,e))),s.setAttribute("aria-level",String(i+1)),s},wrapper(t){const e=t.attrs?.listType??"bullet";return{tag:"ordered"===e?"ol":"ul",key:`list-${e}`,className:`notectl-list notectl-list--${e}`,attrs:{role:"list"}}},toHTML(t,e){const n=t.attrs?.listType??"bullet",i=t.attrs?.checked??!1;return"checklist"===n?`<li role="checkbox" aria-checked="${String(i)}"><input type="checkbox" disabled${i?" checked":""}>${e||"<br>"}</li>`:`<li>${e||"<br>"}</li>`},parseHTML:[{tag:"li"}],sanitize:{tags:["ul","ol","li","input"],attrs:["type","disabled","checked","role","aria-checked"]}})}registerCheckboxClickHandler(t){this.config.types.includes("checklist")&&(this.checkboxClickHandler=e=>{if(t.isReadOnly()&&!this.config.interactiveCheckboxes)return;const n=e.target;if(!(n instanceof HTMLElement))return;const i=n.closest(`.${B}`);if(!i)return;const c=i.closest(".notectl-list-item--checklist");if(!c)return;e.preventDefault();const s=c.getAttribute("data-block-id");s&&f(t,{interactiveCheckboxes:this.config.interactiveCheckboxes,targetId:r(s),locale:this.locale})},t.getContainer().addEventListener("mousedown",this.checkboxClickHandler))}getEnabledTypes(){return y.filter(t=>this.config.types.includes(t.type))}};export{x as n,T as r,C as t};
|
|
2
|
+
//# sourceMappingURL=ListPlugin-2yl6KiTK.mjs.map
|