From a0e1b2998e433170c684fc9e92e0b49750f83f5b Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 31 May 2025 13:54:04 +0200 Subject: [PATCH 01/11] Add basic image resizer --- .../src/extensions/image/image.js | 313 ++++++++-------- .../image/imageHelpers/imageResize.css | 94 +++++ .../image/imageHelpers/imageResizePlugin.js | 340 ++++++++++++++++++ .../extensions/image/imageHelpers/index.js | 12 +- .../src/extensions/image/index.js | 3 +- 5 files changed, 614 insertions(+), 148 deletions(-) create mode 100644 packages/super-editor/src/extensions/image/imageHelpers/imageResize.css create mode 100644 packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js diff --git a/packages/super-editor/src/extensions/image/image.js b/packages/super-editor/src/extensions/image/image.js index 2cc5c8190a..b59798e91f 100644 --- a/packages/super-editor/src/extensions/image/image.js +++ b/packages/super-editor/src/extensions/image/image.js @@ -1,145 +1,174 @@ -import { Node, Attribute } from '@core/index.js'; -import { ImagePlaceholderPlugin } from './imageHelpers/imagePlaceholderPlugin.js'; -import { ImagePositionPlugin } from './imageHelpers/imagePositionPlugin.js'; +import { Attribute, Node } from "@core/index.js"; +import { ImagePlaceholderPlugin } from "./imageHelpers/imagePlaceholderPlugin.js"; +import { ImagePositionPlugin } from "./imageHelpers/imagePositionPlugin.js"; +import { ImageResizePlugin } from "./imageHelpers/imageResizePlugin.js"; +import "./imageHelpers/imageResize.css"; export const Image = Node.create({ - name: 'image', - - group: 'inline', - - inline: true, - - draggable: true, - - addOptions() { - return { - allowBase64: true, - htmlAttributes: { - style: 'display: inline-block;', - }, - }; - }, - - addStorage() { - return { - media: {}, - }; - }, - - addAttributes() { - return { - src: { - default: null, - renderDOM: ({ src }) => { - return { - src: this.storage.media[src] ?? src, - }; - }, - }, - - alt: { - default: null, - }, - - id: { rendered: false }, - - title: { - default: null, - }, - - rId: { - default: null, - rendered: false, - }, - - originalPadding: { - default: null, - rendered: false, - }, - originalAttributes: { rendered: false }, - wrapTopAndBottom: { rendered: false }, - - anchorData: { - default: null, - rendered: false, - }, - - isAnchor: { rendered: false }, - simplePos: { rendered: false }, - wrapText: { rendered: false }, - - size: { - default: {}, - renderDOM: ({ size }) => { - let style = ''; - let { width, height } = size ?? {}; - if (width) style += `width: ${width}px;`; - if (height) style += `height: auto;`; - return { style }; - }, - }, - - padding: { - default: {}, - renderDOM: ({ padding, marginOffset }) => { - let { left = 0, top = 0, bottom = 0, right = 0 } = padding ?? {}; - let style = ''; - if (left && !marginOffset?.left) style += `margin-left: ${left}px;`; - if (top && !marginOffset?.top) style += `margin-top: ${top}px;`; - if (bottom) style += `margin-bottom: ${bottom}px;`; - if (right) style += `margin-right: ${right}px;`; - return { style }; - }, - }, - - marginOffset: { - default: {}, - renderDOM: ({ marginOffset }) => { - let { left = 0, top = 0 } = marginOffset ?? {}; - let style = ''; - if (left) style += `margin-left: ${left}px;`; - if (top) style += `margin-top: ${top}px;`; - return { style }; - }, - }, - - style: { - default: null, - rendered: true, - renderDOM: ({ style }) => { - if (!style) return {}; - return { style }; - }, - }, - }; - }, - - parseDOM() { - return [ - { - tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])', - }, - ]; - }, - - renderDOM({ htmlAttributes }) { - return ['img', Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes)]; - }, - - addCommands() { - return { - setImage: - (options) => - ({ commands }) => { - return commands.insertContent({ - type: this.name, - attrs: options, - }); - }, - }; - }, - - addPmPlugins() { - return [ImagePlaceholderPlugin(), ImagePositionPlugin({editor: this.editor })]; - }, + name: "image", + + group: "inline", + + inline: true, + + draggable: true, + + addOptions() { + return { + allowBase64: true, + htmlAttributes: { + style: "display: inline-block;", + }, + }; + }, + + addStorage() { + return { + media: {}, + }; + }, + + addAttributes() { + return { + src: { + default: null, + renderDOM: ({ src }) => { + return { + src: this.storage.media[src] ?? src, + }; + }, + }, + + alt: { + default: null, + }, + + id: { rendered: false }, + + title: { + default: null, + }, + + rId: { + default: null, + rendered: false, + }, + + originalPadding: { + default: null, + rendered: false, + }, + originalAttributes: { rendered: false }, + wrapTopAndBottom: { rendered: false }, + + anchorData: { + default: null, + rendered: false, + }, + + isAnchor: { rendered: false }, + simplePos: { rendered: false }, + wrapText: { rendered: false }, + + size: { + default: {}, + renderDOM: ({ size }) => { + let style = ""; + const { width, height } = size ?? {}; + if (width) style += `width: ${width}px;`; + if (height) style += "height: auto;"; + return { style }; + }, + }, + + padding: { + default: {}, + renderDOM: ({ padding, marginOffset }) => { + const { left = 0, top = 0, bottom = 0, right = 0 } = padding ?? {}; + let style = ""; + if (left && !marginOffset?.left) style += `margin-left: ${left}px;`; + if (top && !marginOffset?.top) style += `margin-top: ${top}px;`; + if (bottom) style += `margin-bottom: ${bottom}px;`; + if (right) style += `margin-right: ${right}px;`; + return { style }; + }, + }, + + marginOffset: { + default: {}, + renderDOM: ({ marginOffset }) => { + const { left = 0, top = 0 } = marginOffset ?? {}; + let style = ""; + if (left) style += `margin-left: ${left}px;`; + if (top) style += `margin-top: ${top}px;`; + return { style }; + }, + }, + + style: { + default: null, + rendered: true, + renderDOM: ({ style }) => { + if (!style) return {}; + return { style }; + }, + }, + }; + }, + + parseDOM() { + return [ + { + tag: this.options.allowBase64 + ? "img[src]" + : 'img[src]:not([src^="data:"])', + }, + ]; + }, + + renderDOM({ htmlAttributes }) { + return [ + "img", + Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes), + ]; + }, + + addCommands() { + return { + setImage: + (options) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }); + }, + resizeImage: + (pos, width, height) => + ({ tr }) => { + const node = tr.doc.nodeAt(pos); + if (node && node.type.name === "image") { + const attrs = { + ...node.attrs, + size: { + ...node.attrs.size, + width, + height, + }, + }; + tr.setNodeMarkup(pos, null, attrs); + return true; + } + return false; + }, + }; + }, + + addPmPlugins() { + return [ + ImagePlaceholderPlugin(), + ImagePositionPlugin({ editor: this.editor }), + ImageResizePlugin(), + ]; + }, }); diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageResize.css b/packages/super-editor/src/extensions/image/imageHelpers/imageResize.css new file mode 100644 index 0000000000..9ca440add2 --- /dev/null +++ b/packages/super-editor/src/extensions/image/imageHelpers/imageResize.css @@ -0,0 +1,94 @@ +/* Image resize styles */ +.ProseMirror .resizable-image-wrapper img { + display: block; + max-width: 100%; + height: auto; +} + +/* Resize handles container */ +.image-resize-container { + position: absolute; + pointer-events: none; + z-index: 1000; +} + +/* Resize handles */ +.resize-handle { + position: absolute; + width: 12px; + height: 12px; + background-color: #4dabf7; + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); + pointer-events: auto; + transition: all 0.1s ease; +} + +.resize-handle:hover { + background-color: #228be6; + transform: scale(1.1); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.4); +} + +/* Handle positions */ +.resize-handle-nw { + top: -6px; + left: -6px; + cursor: nwse-resize; +} + +.resize-handle-ne { + top: -6px; + right: -6px; + cursor: nesw-resize; +} + +.resize-handle-sw { + bottom: -6px; + left: -6px; + cursor: nesw-resize; +} + +.resize-handle-se { + bottom: -6px; + right: -6px; + cursor: nwse-resize; +} + +/* Hide handles when editor loses focus */ +.ProseMirror:not(.ProseMirror-focused) .image-resize-container { + display: none; +} + +/* Smooth transitions for image resizing */ +.resizable-image-wrapper img { + transition: none; +} + +.resizable-image-wrapper img:not([style*="width"]) { + transition: all 0.2s ease; +} + +/* Resize feedback indicator */ +.resizable-image-wrapper::after { + content: "Drag corners to resize"; + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(77, 171, 247, 0.9); + color: white; + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 1001; +} + +.resizable-image-wrapper:hover::after { + opacity: 1; +} diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js b/packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js new file mode 100644 index 0000000000..2a21980850 --- /dev/null +++ b/packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js @@ -0,0 +1,340 @@ +import { Plugin, PluginKey } from "prosemirror-state"; +import { Decoration, DecorationSet } from "prosemirror-view"; + +/** + * Plugin key for the image resize plugin + */ +export const ImageResizePluginKey = new PluginKey("imageResize"); + +/** + * Plugin for handling image resizing functionality + * + * @returns {Plugin} ProseMirror plugin for image resizing + */ +export const ImageResizePlugin = () => { + // Track the resize state + let resizeState = { + dragging: false, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + handle: null, + imagePos: null, + imageNode: null, + imageElement: null, + aspectRatio: 1, + }; + + // Store references to resize handles container and editor view + let resizeContainer = null; + let editorView = null; + let globalClickHandler = null; + let globalMousedownHandler = null; + + return new Plugin({ + key: ImageResizePluginKey, + + state: { + init() { + return DecorationSet.empty; + }, + + apply(tr, oldState, _, newState) { + // Skip if the transaction is from this plugin + if (tr.getMeta(ImageResizePluginKey)) { + return oldState; + } + + const decorations = []; + const { selection } = newState; + + // Only create decoration if an image node is selected + if (selection.node && selection.node.type.name === "image") { + decorations.push( + Decoration.node(selection.from, selection.to, { + nodeName: "span", + class: "resizable-image-wrapper image-selected", + "data-image-pos": selection.from, + }), + ); + } + + return DecorationSet.create(newState.doc, decorations); + }, + }, + + props: { + decorations(state) { + return this.getState(state); + }, + }, + + view(view) { + editorView = view; + + // Add global click handler + globalClickHandler = (event) => { + if ( + !event.target.closest(".resizable-image-wrapper") && + !event.target.closest(".image-resize-container") + ) { + hideResizeHandles(); + } + }; + + document.addEventListener("click", globalClickHandler); + + // Add global mousedown handler + globalMousedownHandler = (event) => { + if (event.target.closest(".resize-handle")) { + event.preventDefault(); + event.stopPropagation(); + startResize(editorView, event, event.target); + return true; + } + }; + + document.addEventListener("mousedown", globalMousedownHandler); + + return { + update(view, prevState) { + // Show/hide resize handles based on selection changes + const { selection } = view.state; + const prevSelection = prevState.selection; + + if ( + selection.from !== prevSelection.from || + selection.to !== prevSelection.to + ) { + setTimeout(() => { + const selectedImageWrapper = document.querySelector( + ".resizable-image-wrapper.image-selected", + ); + console.log({ selectedImageWrapper }); + if (selectedImageWrapper) { + const img = selectedImageWrapper.querySelector("img"); + console.log({ img }); + if (img) { + showResizeHandles(view, img); + } else { + hideResizeHandles(); + } + } else { + hideResizeHandles(); + } + }, 10); + } + }, + + destroy() { + hideResizeHandles(); + cleanupEventListeners(); + if (globalClickHandler) { + document.removeEventListener("click", globalClickHandler); + globalClickHandler = null; + } + if (globalMousedownHandler) { + document.removeEventListener("mousedown", globalMousedownHandler); + globalMousedownHandler = null; + } + editorView = null; + }, + }; + }, + }); + + function showResizeHandles(view, imageElement) { + hideResizeHandles(); + + const wrapper = imageElement.closest(".resizable-image-wrapper"); + if (!wrapper) return; + + const imagePos = Number.parseInt( + wrapper.getAttribute("data-image-pos"), + 10, + ); + const node = view.state.doc.nodeAt(imagePos); + if (!node || node.type.name !== "image") return; + + // Create resize container + resizeContainer = document.createElement("div"); + resizeContainer.className = "image-resize-container"; + resizeContainer.style.position = "absolute"; + resizeContainer.style.pointerEvents = "none"; + resizeContainer.style.zIndex = "1000"; + + // Create handles + const handles = ["nw", "ne", "sw", "se"]; + for (const handle of handles) { + const handleEl = document.createElement("div"); + handleEl.className = `resize-handle resize-handle-${handle}`; + handleEl.setAttribute("data-handle", handle); + handleEl.setAttribute("data-image-pos", imagePos); + handleEl.style.pointerEvents = "auto"; + resizeContainer.appendChild(handleEl); + } + + // Position the container relative to the image + document.body.appendChild(resizeContainer); + updateHandlePositions(imageElement); + } + + function hideResizeHandles() { + if (resizeContainer?.parentNode) { + resizeContainer.parentNode.removeChild(resizeContainer); + resizeContainer = null; + } + } + + function updateHandlePositions(imageElement) { + if (!resizeContainer || !imageElement) return; + + const rect = imageElement.getBoundingClientRect(); + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = + window.pageXOffset || document.documentElement.scrollLeft; + + resizeContainer.style.left = `${rect.left + scrollLeft}px`; + resizeContainer.style.top = `${rect.top + scrollTop}px`; + resizeContainer.style.width = `${rect.width}px`; + resizeContainer.style.height = `${rect.height}px`; + } + + function startResize(view, event, handleElement) { + const handle = handleElement.getAttribute("data-handle"); + const imagePos = Number.parseInt( + handleElement.getAttribute("data-image-pos"), + 10, + ); + const node = view.state.doc.nodeAt(imagePos); + + if (!node || node.type.name !== "image") return; + + const imageElement = view.nodeDOM(imagePos); + + if (!imageElement) return; + + const rect = imageElement.getBoundingClientRect(); + + resizeState = { + dragging: true, + startX: event.clientX, + startY: event.clientY, + startWidth: rect.width, + startHeight: rect.height, + handle, + imagePos, + imageNode: node, + imageElement, + aspectRatio: rect.width / rect.height, + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.cursor = getResizeCursor(handle); + document.body.style.userSelect = "none"; + } + + function handleMouseMove(event) { + if (!resizeState.dragging) return; + + event.preventDefault(); + event.stopPropagation(); + + let deltaX = event.clientX - resizeState.startX; + let deltaY = event.clientY - resizeState.startY; + + // Adjust delta based on handle position + if (resizeState.handle.includes("w")) deltaX = -deltaX; + if (resizeState.handle.includes("n")) deltaY = -deltaY; + + // Calculate new dimensions maintaining aspect ratio + const newWidth = Math.max(20, resizeState.startWidth + deltaX); + const newHeight = newWidth / resizeState.aspectRatio; + + // Apply the new size immediately for visual feedback + if (resizeState.imageElement) { + resizeState.imageElement.style.width = `${newWidth}px`; + resizeState.imageElement.style.height = "auto"; + + // Update handle positions + updateHandlePositions(resizeState.imageElement); + } + } + + function handleMouseUp(event) { + if (!resizeState.dragging) return; + + cleanupEventListeners(); + + let deltaX = event.clientX - resizeState.startX; + let deltaY = event.clientY - resizeState.startY; + + // Adjust delta based on handle position + if (resizeState.handle.includes("w")) deltaX = -deltaX; + if (resizeState.handle.includes("n")) deltaY = -deltaY; + + // Calculate final dimensions + const newWidth = Math.max(20, resizeState.startWidth + deltaX); + const newHeight = newWidth / resizeState.aspectRatio; + + // Update the document + if ( + editorView && + resizeState.imagePos < editorView.state.doc.content.size + ) { + const tr = editorView.state.tr; + const node = tr.doc.nodeAt(resizeState.imagePos); + + if (node && node.type.name === "image") { + const attrs = { + ...node.attrs, + size: { + ...node.attrs.size, + width: Math.round(newWidth), + height: Math.round(newHeight), + }, + }; + + tr.setNodeMarkup(resizeState.imagePos, null, attrs); + tr.setMeta(ImageResizePluginKey, { action: "resize" }); + editorView.dispatch(tr); + } + } + + // Reset resize state + resizeState = { + dragging: false, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + handle: null, + imagePos: null, + imageNode: null, + imageElement: null, + aspectRatio: 1, + }; + } + + function cleanupEventListeners() { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + + function getResizeCursor(handle) { + switch (handle) { + case "nw": + case "se": + return "nwse-resize"; + case "ne": + case "sw": + return "nesw-resize"; + default: + return "default"; + } + } +}; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/index.js b/packages/super-editor/src/extensions/image/imageHelpers/index.js index 10863837bc..8fd09c49e5 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/index.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/index.js @@ -1,5 +1,7 @@ -export * from './getFileOpener.js'; -export * from './startImageUpload.js'; -export * from './handleImageUpload.js'; -export * from './imagePlaceholderPlugin.js'; -export * from './processUploadedImage.js'; +export * from "./getFileOpener.js"; +export * from "./startImageUpload.js"; +export * from "./handleImageUpload.js"; +export * from "./imagePlaceholderPlugin.js"; +export * from "./processUploadedImage.js"; +export * from "./imagePositionPlugin.js"; +export * from "./imageResizePlugin.js"; diff --git a/packages/super-editor/src/extensions/image/index.js b/packages/super-editor/src/extensions/image/index.js index cf09068aaa..e974ea2f3a 100644 --- a/packages/super-editor/src/extensions/image/index.js +++ b/packages/super-editor/src/extensions/image/index.js @@ -1 +1,2 @@ -export * from './image'; +export * from "./image"; +export * from "./imageHelpers"; From 350711d965a8af9edfa8796503d4482dd0a57bf4 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sun, 1 Jun 2025 08:22:35 +0200 Subject: [PATCH 02/11] make resize plugin generic --- .../src/extensions/image/image.js | 23 +--- .../extensions/image/imageHelpers/index.js | 1 - packages/super-editor/src/extensions/index.js | 4 + .../popover-plugin/popover-plugin.js | 2 +- .../src/extensions/resize-plugin/index.js | 2 + .../resize-plugin.js} | 127 ++++++++---------- .../resize.css} | 21 +-- 7 files changed, 74 insertions(+), 106 deletions(-) create mode 100644 packages/super-editor/src/extensions/resize-plugin/index.js rename packages/super-editor/src/extensions/{image/imageHelpers/imageResizePlugin.js => resize-plugin/resize-plugin.js} (69%) rename packages/super-editor/src/extensions/{image/imageHelpers/imageResize.css => resize-plugin/resize.css} (75%) diff --git a/packages/super-editor/src/extensions/image/image.js b/packages/super-editor/src/extensions/image/image.js index b59798e91f..d2eb62fffe 100644 --- a/packages/super-editor/src/extensions/image/image.js +++ b/packages/super-editor/src/extensions/image/image.js @@ -1,8 +1,6 @@ import { Attribute, Node } from "@core/index.js"; import { ImagePlaceholderPlugin } from "./imageHelpers/imagePlaceholderPlugin.js"; import { ImagePositionPlugin } from "./imageHelpers/imagePositionPlugin.js"; -import { ImageResizePlugin } from "./imageHelpers/imageResizePlugin.js"; -import "./imageHelpers/imageResize.css"; export const Image = Node.create({ name: "image", @@ -142,25 +140,7 @@ export const Image = Node.create({ type: this.name, attrs: options, }); - }, - resizeImage: - (pos, width, height) => - ({ tr }) => { - const node = tr.doc.nodeAt(pos); - if (node && node.type.name === "image") { - const attrs = { - ...node.attrs, - size: { - ...node.attrs.size, - width, - height, - }, - }; - tr.setNodeMarkup(pos, null, attrs); - return true; - } - return false; - }, + } }; }, @@ -168,7 +148,6 @@ export const Image = Node.create({ return [ ImagePlaceholderPlugin(), ImagePositionPlugin({ editor: this.editor }), - ImageResizePlugin(), ]; }, }); diff --git a/packages/super-editor/src/extensions/image/imageHelpers/index.js b/packages/super-editor/src/extensions/image/imageHelpers/index.js index 8fd09c49e5..9d27a501aa 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/index.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/index.js @@ -4,4 +4,3 @@ export * from "./handleImageUpload.js"; export * from "./imagePlaceholderPlugin.js"; export * from "./processUploadedImage.js"; export * from "./imagePositionPlugin.js"; -export * from "./imageResizePlugin.js"; diff --git a/packages/super-editor/src/extensions/index.js b/packages/super-editor/src/extensions/index.js index be0f8e5038..0128390ff5 100644 --- a/packages/super-editor/src/extensions/index.js +++ b/packages/super-editor/src/extensions/index.js @@ -58,6 +58,7 @@ import { TrackChanges } from './track-changes/index.js'; import { Pagination } from './pagination/index.js'; import { LinkedStyles } from './linked-styles/linked-styles.js'; import { Search } from './search/index.js'; +import { ResizePlugin } from './resize-plugin/index.js'; // Helpers import { trackChangesHelpers } from './track-changes/index.js'; @@ -100,6 +101,7 @@ const getRichTextExtensions = () => { TrackFormat, AiPlugin, Image, + ResizePlugin, ]; }; @@ -165,6 +167,7 @@ const getStarterExtensions = () => { ContentBlock, Search, StructuredContent, + ResizePlugin, ]; }; @@ -227,4 +230,5 @@ export { AiPlugin, Search, StructuredContent, + ResizePlugin, }; diff --git a/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js b/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js index 3ea0b38b66..3c9c053572 100644 --- a/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js +++ b/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js @@ -1,5 +1,5 @@ import { createApp } from 'vue'; -import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'; +import { Plugin, PluginKey } from 'prosemirror-state'; import { Extension } from '@core/Extension.js'; import tippy from 'tippy.js'; diff --git a/packages/super-editor/src/extensions/resize-plugin/index.js b/packages/super-editor/src/extensions/resize-plugin/index.js new file mode 100644 index 0000000000..5104b837be --- /dev/null +++ b/packages/super-editor/src/extensions/resize-plugin/index.js @@ -0,0 +1,2 @@ +export * from "./resize-plugin.js"; +import "./resize.css"; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js b/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js similarity index 69% rename from packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js rename to packages/super-editor/src/extensions/resize-plugin/resize-plugin.js index 2a21980850..00d0aa7bc2 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/imageResizePlugin.js +++ b/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js @@ -1,17 +1,13 @@ import { Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; +import { Extension } from '@core/Extension.js'; /** - * Plugin key for the image resize plugin + * Plugin key for the resize plugin */ -export const ImageResizePluginKey = new PluginKey("imageResize"); +export const ResizePluginKey = new PluginKey("resize-nodes"); -/** - * Plugin for handling image resizing functionality - * - * @returns {Plugin} ProseMirror plugin for image resizing - */ -export const ImageResizePlugin = () => { +const resize = (nodeNames = ["image"]) => { // Track the resize state let resizeState = { dragging: false, @@ -20,9 +16,8 @@ export const ImageResizePlugin = () => { startWidth: 0, startHeight: 0, handle: null, - imagePos: null, - imageNode: null, - imageElement: null, + pos: null, + resizableElement: null, aspectRatio: 1, }; @@ -33,7 +28,7 @@ export const ImageResizePlugin = () => { let globalMousedownHandler = null; return new Plugin({ - key: ImageResizePluginKey, + key: ResizePluginKey, state: { init() { @@ -42,20 +37,20 @@ export const ImageResizePlugin = () => { apply(tr, oldState, _, newState) { // Skip if the transaction is from this plugin - if (tr.getMeta(ImageResizePluginKey)) { + if (tr.getMeta(ResizePluginKey)) { return oldState; } const decorations = []; const { selection } = newState; - // Only create decoration if an image node is selected - if (selection.node && selection.node.type.name === "image") { + // Only create decoration if one of the resizable nodes is selected + if (nodeNames.includes(selection.node?.type.name)) { decorations.push( Decoration.node(selection.from, selection.to, { nodeName: "span", - class: "resizable-image-wrapper image-selected", - "data-image-pos": selection.from, + class: "resizable-wrapper", + "data-pos": selection.from, }), ); } @@ -76,8 +71,8 @@ export const ImageResizePlugin = () => { // Add global click handler globalClickHandler = (event) => { if ( - !event.target.closest(".resizable-image-wrapper") && - !event.target.closest(".image-resize-container") + !event.target.closest(".resizable-wrapper") && + !event.target.closest(".resize-container") ) { hideResizeHandles(); } @@ -108,18 +103,11 @@ export const ImageResizePlugin = () => { selection.to !== prevSelection.to ) { setTimeout(() => { - const selectedImageWrapper = document.querySelector( - ".resizable-image-wrapper.image-selected", + const selectedResizableWrapper = document.querySelector( + ".resizable-wrapper", ); - console.log({ selectedImageWrapper }); - if (selectedImageWrapper) { - const img = selectedImageWrapper.querySelector("img"); - console.log({ img }); - if (img) { - showResizeHandles(view, img); - } else { - hideResizeHandles(); - } + if (selectedResizableWrapper) { + showResizeHandles(view, selectedResizableWrapper); } else { hideResizeHandles(); } @@ -144,22 +132,20 @@ export const ImageResizePlugin = () => { }, }); - function showResizeHandles(view, imageElement) { + function showResizeHandles(view, wrapper) { hideResizeHandles(); - const wrapper = imageElement.closest(".resizable-image-wrapper"); - if (!wrapper) return; - - const imagePos = Number.parseInt( - wrapper.getAttribute("data-image-pos"), + const pos = Number.parseInt( + wrapper.getAttribute("data-pos"), 10, ); - const node = view.state.doc.nodeAt(imagePos); - if (!node || node.type.name !== "image") return; + + const node = view.state.doc.nodeAt(pos); + if (!nodeNames.includes(node?.type.name)) return; // Create resize container resizeContainer = document.createElement("div"); - resizeContainer.className = "image-resize-container"; + resizeContainer.className = "resize-container"; resizeContainer.style.position = "absolute"; resizeContainer.style.pointerEvents = "none"; resizeContainer.style.zIndex = "1000"; @@ -170,14 +156,14 @@ export const ImageResizePlugin = () => { const handleEl = document.createElement("div"); handleEl.className = `resize-handle resize-handle-${handle}`; handleEl.setAttribute("data-handle", handle); - handleEl.setAttribute("data-image-pos", imagePos); + handleEl.setAttribute("data-pos", pos); handleEl.style.pointerEvents = "auto"; resizeContainer.appendChild(handleEl); } - // Position the container relative to the image + // Position the container relative to the resizable element document.body.appendChild(resizeContainer); - updateHandlePositions(imageElement); + updateHandlePositions(wrapper.firstElementChild); } function hideResizeHandles() { @@ -187,10 +173,10 @@ export const ImageResizePlugin = () => { } } - function updateHandlePositions(imageElement) { - if (!resizeContainer || !imageElement) return; + function updateHandlePositions(resizableElement) { + if (!resizeContainer || !resizableElement) return; - const rect = imageElement.getBoundingClientRect(); + const rect = resizableElement.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; @@ -203,19 +189,19 @@ export const ImageResizePlugin = () => { function startResize(view, event, handleElement) { const handle = handleElement.getAttribute("data-handle"); - const imagePos = Number.parseInt( - handleElement.getAttribute("data-image-pos"), + const pos = Number.parseInt( + handleElement.getAttribute("data-pos"), 10, ); - const node = view.state.doc.nodeAt(imagePos); + const node = view.state.doc.nodeAt(pos); - if (!node || node.type.name !== "image") return; + if (!nodeNames.includes(node?.type.name)) return; - const imageElement = view.nodeDOM(imagePos); + const resizableElement = view.nodeDOM(pos); - if (!imageElement) return; + if (!resizableElement) return; - const rect = imageElement.getBoundingClientRect(); + const rect = resizableElement.getBoundingClientRect(); resizeState = { dragging: true, @@ -224,9 +210,8 @@ export const ImageResizePlugin = () => { startWidth: rect.width, startHeight: rect.height, handle, - imagePos, - imageNode: node, - imageElement, + pos, + resizableElement, aspectRatio: rect.width / rect.height, }; @@ -251,15 +236,14 @@ export const ImageResizePlugin = () => { // Calculate new dimensions maintaining aspect ratio const newWidth = Math.max(20, resizeState.startWidth + deltaX); - const newHeight = newWidth / resizeState.aspectRatio; // Apply the new size immediately for visual feedback - if (resizeState.imageElement) { - resizeState.imageElement.style.width = `${newWidth}px`; - resizeState.imageElement.style.height = "auto"; + if (resizeState.resizableElement) { + resizeState.resizableElement.style.width = `${newWidth}px`; + resizeState.resizableElement.style.height = "auto"; // Update handle positions - updateHandlePositions(resizeState.imageElement); + updateHandlePositions(resizeState.resizableElement); } } @@ -282,12 +266,12 @@ export const ImageResizePlugin = () => { // Update the document if ( editorView && - resizeState.imagePos < editorView.state.doc.content.size + resizeState.pos < editorView.state.doc.content.size ) { const tr = editorView.state.tr; - const node = tr.doc.nodeAt(resizeState.imagePos); + const node = tr.doc.nodeAt(resizeState.pos); - if (node && node.type.name === "image") { + if (nodeNames.includes(node?.type.name)) { const attrs = { ...node.attrs, size: { @@ -297,8 +281,8 @@ export const ImageResizePlugin = () => { }, }; - tr.setNodeMarkup(resizeState.imagePos, null, attrs); - tr.setMeta(ImageResizePluginKey, { action: "resize" }); + tr.setNodeMarkup(resizeState.pos, null, attrs); + tr.setMeta(ResizePluginKey, { action: "resize" }); editorView.dispatch(tr); } } @@ -311,9 +295,8 @@ export const ImageResizePlugin = () => { startWidth: 0, startHeight: 0, handle: null, - imagePos: null, - imageNode: null, - imageElement: null, + pos: null, + resizableElement: null, aspectRatio: 1, }; } @@ -338,3 +321,11 @@ export const ImageResizePlugin = () => { } } }; + +export const ResizePlugin = Extension.create({ + name: 'resizePlugin', + + addPmPlugins() { + return [resize(["image"])] + } +}); diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageResize.css b/packages/super-editor/src/extensions/resize-plugin/resize.css similarity index 75% rename from packages/super-editor/src/extensions/image/imageHelpers/imageResize.css rename to packages/super-editor/src/extensions/resize-plugin/resize.css index 9ca440add2..467cfbe60b 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/imageResize.css +++ b/packages/super-editor/src/extensions/resize-plugin/resize.css @@ -1,12 +1,5 @@ -/* Image resize styles */ -.ProseMirror .resizable-image-wrapper img { - display: block; - max-width: 100%; - height: auto; -} - /* Resize handles container */ -.image-resize-container { +.resize-container { position: absolute; pointer-events: none; z-index: 1000; @@ -57,21 +50,21 @@ } /* Hide handles when editor loses focus */ -.ProseMirror:not(.ProseMirror-focused) .image-resize-container { +.ProseMirror:not(.ProseMirror-focused) .resize-container { display: none; } -/* Smooth transitions for image resizing */ -.resizable-image-wrapper img { +/* Smooth transitions for resizing */ +.resizable-wrapper * { transition: none; } -.resizable-image-wrapper img:not([style*="width"]) { +.resizable-wrapper *:not([style*="width"]) { transition: all 0.2s ease; } /* Resize feedback indicator */ -.resizable-image-wrapper::after { +.resizable-wrapper::after { content: "Drag corners to resize"; position: absolute; bottom: -25px; @@ -89,6 +82,6 @@ z-index: 1001; } -.resizable-image-wrapper:hover::after { +.resizable-wrapper:hover::after { opacity: 1; } From 701f6fac406df1f2f98caee5c636f3d351f8d07f Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 3 Jun 2025 15:38:23 +0200 Subject: [PATCH 03/11] run prettier on selected files --- .../src/extensions/image/image.js | 292 ++++----- .../image/imageHelpers/imagePositionPlugin.js | 22 +- .../extensions/image/imageHelpers/index.js | 12 +- .../imageHelpers/processUploadedImage.js | 16 +- .../src/extensions/image/index.js | 4 +- .../src/extensions/resize-plugin/index.js | 4 +- .../extensions/resize-plugin/resize-plugin.js | 620 +++++++++--------- .../src/extensions/resize-plugin/resize.css | 94 +-- 8 files changed, 516 insertions(+), 548 deletions(-) diff --git a/packages/super-editor/src/extensions/image/image.js b/packages/super-editor/src/extensions/image/image.js index d2eb62fffe..73bc8789b2 100644 --- a/packages/super-editor/src/extensions/image/image.js +++ b/packages/super-editor/src/extensions/image/image.js @@ -1,153 +1,145 @@ -import { Attribute, Node } from "@core/index.js"; -import { ImagePlaceholderPlugin } from "./imageHelpers/imagePlaceholderPlugin.js"; -import { ImagePositionPlugin } from "./imageHelpers/imagePositionPlugin.js"; +import { Attribute, Node } from '@core/index.js'; +import { ImagePlaceholderPlugin } from './imageHelpers/imagePlaceholderPlugin.js'; +import { ImagePositionPlugin } from './imageHelpers/imagePositionPlugin.js'; export const Image = Node.create({ - name: "image", - - group: "inline", - - inline: true, - - draggable: true, - - addOptions() { - return { - allowBase64: true, - htmlAttributes: { - style: "display: inline-block;", - }, - }; - }, - - addStorage() { - return { - media: {}, - }; - }, - - addAttributes() { - return { - src: { - default: null, - renderDOM: ({ src }) => { - return { - src: this.storage.media[src] ?? src, - }; - }, - }, - - alt: { - default: null, - }, - - id: { rendered: false }, - - title: { - default: null, - }, - - rId: { - default: null, - rendered: false, - }, - - originalPadding: { - default: null, - rendered: false, - }, - originalAttributes: { rendered: false }, - wrapTopAndBottom: { rendered: false }, - - anchorData: { - default: null, - rendered: false, - }, - - isAnchor: { rendered: false }, - simplePos: { rendered: false }, - wrapText: { rendered: false }, - - size: { - default: {}, - renderDOM: ({ size }) => { - let style = ""; - const { width, height } = size ?? {}; - if (width) style += `width: ${width}px;`; - if (height) style += "height: auto;"; - return { style }; - }, - }, - - padding: { - default: {}, - renderDOM: ({ padding, marginOffset }) => { - const { left = 0, top = 0, bottom = 0, right = 0 } = padding ?? {}; - let style = ""; - if (left && !marginOffset?.left) style += `margin-left: ${left}px;`; - if (top && !marginOffset?.top) style += `margin-top: ${top}px;`; - if (bottom) style += `margin-bottom: ${bottom}px;`; - if (right) style += `margin-right: ${right}px;`; - return { style }; - }, - }, - - marginOffset: { - default: {}, - renderDOM: ({ marginOffset }) => { - const { left = 0, top = 0 } = marginOffset ?? {}; - let style = ""; - if (left) style += `margin-left: ${left}px;`; - if (top) style += `margin-top: ${top}px;`; - return { style }; - }, - }, - - style: { - default: null, - rendered: true, - renderDOM: ({ style }) => { - if (!style) return {}; - return { style }; - }, - }, - }; - }, - - parseDOM() { - return [ - { - tag: this.options.allowBase64 - ? "img[src]" - : 'img[src]:not([src^="data:"])', - }, - ]; - }, - - renderDOM({ htmlAttributes }) { - return [ - "img", - Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes), - ]; - }, - - addCommands() { - return { - setImage: - (options) => - ({ commands }) => { - return commands.insertContent({ - type: this.name, - attrs: options, - }); - } - }; - }, - - addPmPlugins() { - return [ - ImagePlaceholderPlugin(), - ImagePositionPlugin({ editor: this.editor }), - ]; - }, + name: 'image', + + group: 'inline', + + inline: true, + + draggable: true, + + addOptions() { + return { + allowBase64: true, + htmlAttributes: { + style: 'display: inline-block;', + }, + }; + }, + + addStorage() { + return { + media: {}, + }; + }, + + addAttributes() { + return { + src: { + default: null, + renderDOM: ({ src }) => { + return { + src: this.storage.media[src] ?? src, + }; + }, + }, + + alt: { + default: null, + }, + + id: { rendered: false }, + + title: { + default: null, + }, + + rId: { + default: null, + rendered: false, + }, + + originalPadding: { + default: null, + rendered: false, + }, + originalAttributes: { rendered: false }, + wrapTopAndBottom: { rendered: false }, + + anchorData: { + default: null, + rendered: false, + }, + + isAnchor: { rendered: false }, + simplePos: { rendered: false }, + wrapText: { rendered: false }, + + size: { + default: {}, + renderDOM: ({ size }) => { + let style = ''; + const { width, height } = size ?? {}; + if (width) style += `width: ${width}px;`; + if (height) style += 'height: auto;'; + return { style }; + }, + }, + + padding: { + default: {}, + renderDOM: ({ padding, marginOffset }) => { + const { left = 0, top = 0, bottom = 0, right = 0 } = padding ?? {}; + let style = ''; + if (left && !marginOffset?.left) style += `margin-left: ${left}px;`; + if (top && !marginOffset?.top) style += `margin-top: ${top}px;`; + if (bottom) style += `margin-bottom: ${bottom}px;`; + if (right) style += `margin-right: ${right}px;`; + return { style }; + }, + }, + + marginOffset: { + default: {}, + renderDOM: ({ marginOffset }) => { + const { left = 0, top = 0 } = marginOffset ?? {}; + let style = ''; + if (left) style += `margin-left: ${left}px;`; + if (top) style += `margin-top: ${top}px;`; + return { style }; + }, + }, + + style: { + default: null, + rendered: true, + renderDOM: ({ style }) => { + if (!style) return {}; + return { style }; + }, + }, + }; + }, + + parseDOM() { + return [ + { + tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])', + }, + ]; + }, + + renderDOM({ htmlAttributes }) { + return ['img', Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes)]; + }, + + addCommands() { + return { + setImage: + (options) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options, + }); + }, + }; + }, + + addPmPlugins() { + return [ImagePlaceholderPlugin(), ImagePositionPlugin({ editor: this.editor })]; + }, }); diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imagePositionPlugin.js b/packages/super-editor/src/extensions/image/imageHelpers/imagePositionPlugin.js index c720088cfb..6f30c1f76f 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/imagePositionPlugin.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/imagePositionPlugin.js @@ -28,10 +28,7 @@ export const ImagePositionPlugin = ({ editor }) => { if (shouldUpdate) { shouldUpdate = false; const decorations = getImagePositionDecorations(lastState, view); - const updateTransaction = view.state.tr.setMeta( - ImagePositionPluginKey, - { decorations } - ); + const updateTransaction = view.state.tr.setMeta(ImagePositionPluginKey, { decorations }); view.dispatch(updateTransaction); } if (pagination?.isReadyToInit) { @@ -57,7 +54,7 @@ const getImagePositionDecorations = (state, view) => { let className = ''; const { vRelativeFrom, alignH } = node.attrs.anchorData; const { size, padding } = node.attrs; - + const pageBreak = findPreviousDomNodeWithClass(view, pos, 'pagination-break-wrapper'); if (pageBreak) { switch (alignH) { @@ -71,8 +68,11 @@ const getImagePositionDecorations = (state, view) => { style += 'display: block; margin-left: auto; margin-right: auto; '; break; } - style += vRelativeFrom === 'margin' ? `position: absolute; top: ${pageBreak?.offsetTop + pageBreak?.offsetHeight}px; ` : ''; - + style += + vRelativeFrom === 'margin' + ? `position: absolute; top: ${pageBreak?.offsetTop + pageBreak?.offsetHeight}px; ` + : ''; + if (vRelativeFrom === 'margin') { const nextPos = view.posAtDOM(pageBreak, 1); const imageBlock = document.createElement('div'); @@ -84,9 +84,7 @@ const getImagePositionDecorations = (state, view) => { } } - decorations.push( - Decoration.node(pos, pos + node.nodeSize, { style, class: className }), - ); + decorations.push(Decoration.node(pos, pos + node.nodeSize, { style, class: className })); } }); return decorations; @@ -94,7 +92,7 @@ const getImagePositionDecorations = (state, view) => { const findPreviousDomNodeWithClass = (view, pos, className) => { let { node } = view.domAtPos(pos); - + // If you get a text node, go to its parent if (node.nodeType === 3) { node = node.parentNode; @@ -117,4 +115,4 @@ const findPreviousDomNodeWithClass = (view, pos, className) => { } return null; // Not found -} +}; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/index.js b/packages/super-editor/src/extensions/image/imageHelpers/index.js index 9d27a501aa..61bf200623 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/index.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/index.js @@ -1,6 +1,6 @@ -export * from "./getFileOpener.js"; -export * from "./startImageUpload.js"; -export * from "./handleImageUpload.js"; -export * from "./imagePlaceholderPlugin.js"; -export * from "./processUploadedImage.js"; -export * from "./imagePositionPlugin.js"; +export * from './getFileOpener.js'; +export * from './startImageUpload.js'; +export * from './handleImageUpload.js'; +export * from './imagePlaceholderPlugin.js'; +export * from './processUploadedImage.js'; +export * from './imagePositionPlugin.js'; diff --git a/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js b/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js index 30d93a4248..5d918f8fb4 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/processUploadedImage.js @@ -9,18 +9,14 @@ export const processUploadedImage = (fileData, editor) => { img.onload = () => { const canvas = document.createElement('canvas'); - const { width, height } = getAllowedImageDimensions( - img.width, - img.height, - editor, - ); + const { width, height } = getAllowedImageDimensions(img.width, img.height, editor); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); - if (typeof fileData === 'string' ) { + if (typeof fileData === 'string') { const resizedBase64 = canvas.toDataURL(); resolve(resizedBase64); } else { @@ -47,13 +43,13 @@ export const getAllowedImageDimensions = (width, height, editor) => { const aspectRatio = width / height; if (height > maxHeight) { - adjustedHeight = maxHeight; - adjustedWidth = Math.round(maxHeight * aspectRatio); + adjustedHeight = maxHeight; + adjustedWidth = Math.round(maxHeight * aspectRatio); } if (adjustedWidth > maxWidth) { - adjustedWidth = maxWidth; - adjustedHeight = Math.round(maxWidth / aspectRatio); + adjustedWidth = maxWidth; + adjustedHeight = Math.round(maxWidth / aspectRatio); } return { width: adjustedWidth, height: adjustedHeight }; diff --git a/packages/super-editor/src/extensions/image/index.js b/packages/super-editor/src/extensions/image/index.js index e974ea2f3a..4ba10ca8a2 100644 --- a/packages/super-editor/src/extensions/image/index.js +++ b/packages/super-editor/src/extensions/image/index.js @@ -1,2 +1,2 @@ -export * from "./image"; -export * from "./imageHelpers"; +export * from './image'; +export * from './imageHelpers'; diff --git a/packages/super-editor/src/extensions/resize-plugin/index.js b/packages/super-editor/src/extensions/resize-plugin/index.js index 5104b837be..2f37653e53 100644 --- a/packages/super-editor/src/extensions/resize-plugin/index.js +++ b/packages/super-editor/src/extensions/resize-plugin/index.js @@ -1,2 +1,2 @@ -export * from "./resize-plugin.js"; -import "./resize.css"; +export * from './resize-plugin.js'; +import './resize.css'; diff --git a/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js b/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js index 00d0aa7bc2..26b855e0fe 100644 --- a/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js +++ b/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js @@ -1,331 +1,313 @@ -import { Plugin, PluginKey } from "prosemirror-state"; -import { Decoration, DecorationSet } from "prosemirror-view"; +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; import { Extension } from '@core/Extension.js'; /** * Plugin key for the resize plugin */ -export const ResizePluginKey = new PluginKey("resize-nodes"); - -const resize = (nodeNames = ["image"]) => { - // Track the resize state - let resizeState = { - dragging: false, - startX: 0, - startY: 0, - startWidth: 0, - startHeight: 0, - handle: null, - pos: null, - resizableElement: null, - aspectRatio: 1, - }; - - // Store references to resize handles container and editor view - let resizeContainer = null; - let editorView = null; - let globalClickHandler = null; - let globalMousedownHandler = null; - - return new Plugin({ - key: ResizePluginKey, - - state: { - init() { - return DecorationSet.empty; - }, - - apply(tr, oldState, _, newState) { - // Skip if the transaction is from this plugin - if (tr.getMeta(ResizePluginKey)) { - return oldState; - } - - const decorations = []; - const { selection } = newState; - - // Only create decoration if one of the resizable nodes is selected - if (nodeNames.includes(selection.node?.type.name)) { - decorations.push( - Decoration.node(selection.from, selection.to, { - nodeName: "span", - class: "resizable-wrapper", - "data-pos": selection.from, - }), - ); - } - - return DecorationSet.create(newState.doc, decorations); - }, - }, - - props: { - decorations(state) { - return this.getState(state); - }, - }, - - view(view) { - editorView = view; - - // Add global click handler - globalClickHandler = (event) => { - if ( - !event.target.closest(".resizable-wrapper") && - !event.target.closest(".resize-container") - ) { - hideResizeHandles(); - } - }; - - document.addEventListener("click", globalClickHandler); - - // Add global mousedown handler - globalMousedownHandler = (event) => { - if (event.target.closest(".resize-handle")) { - event.preventDefault(); - event.stopPropagation(); - startResize(editorView, event, event.target); - return true; - } - }; - - document.addEventListener("mousedown", globalMousedownHandler); - - return { - update(view, prevState) { - // Show/hide resize handles based on selection changes - const { selection } = view.state; - const prevSelection = prevState.selection; - - if ( - selection.from !== prevSelection.from || - selection.to !== prevSelection.to - ) { - setTimeout(() => { - const selectedResizableWrapper = document.querySelector( - ".resizable-wrapper", - ); - if (selectedResizableWrapper) { - showResizeHandles(view, selectedResizableWrapper); - } else { - hideResizeHandles(); - } - }, 10); - } - }, - - destroy() { - hideResizeHandles(); - cleanupEventListeners(); - if (globalClickHandler) { - document.removeEventListener("click", globalClickHandler); - globalClickHandler = null; - } - if (globalMousedownHandler) { - document.removeEventListener("mousedown", globalMousedownHandler); - globalMousedownHandler = null; - } - editorView = null; - }, - }; - }, - }); - - function showResizeHandles(view, wrapper) { - hideResizeHandles(); - - const pos = Number.parseInt( - wrapper.getAttribute("data-pos"), - 10, - ); - - const node = view.state.doc.nodeAt(pos); - if (!nodeNames.includes(node?.type.name)) return; - - // Create resize container - resizeContainer = document.createElement("div"); - resizeContainer.className = "resize-container"; - resizeContainer.style.position = "absolute"; - resizeContainer.style.pointerEvents = "none"; - resizeContainer.style.zIndex = "1000"; - - // Create handles - const handles = ["nw", "ne", "sw", "se"]; - for (const handle of handles) { - const handleEl = document.createElement("div"); - handleEl.className = `resize-handle resize-handle-${handle}`; - handleEl.setAttribute("data-handle", handle); - handleEl.setAttribute("data-pos", pos); - handleEl.style.pointerEvents = "auto"; - resizeContainer.appendChild(handleEl); - } - - // Position the container relative to the resizable element - document.body.appendChild(resizeContainer); - updateHandlePositions(wrapper.firstElementChild); - } - - function hideResizeHandles() { - if (resizeContainer?.parentNode) { - resizeContainer.parentNode.removeChild(resizeContainer); - resizeContainer = null; - } - } - - function updateHandlePositions(resizableElement) { - if (!resizeContainer || !resizableElement) return; - - const rect = resizableElement.getBoundingClientRect(); - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const scrollLeft = - window.pageXOffset || document.documentElement.scrollLeft; - - resizeContainer.style.left = `${rect.left + scrollLeft}px`; - resizeContainer.style.top = `${rect.top + scrollTop}px`; - resizeContainer.style.width = `${rect.width}px`; - resizeContainer.style.height = `${rect.height}px`; - } - - function startResize(view, event, handleElement) { - const handle = handleElement.getAttribute("data-handle"); - const pos = Number.parseInt( - handleElement.getAttribute("data-pos"), - 10, - ); - const node = view.state.doc.nodeAt(pos); - - if (!nodeNames.includes(node?.type.name)) return; - - const resizableElement = view.nodeDOM(pos); - - if (!resizableElement) return; - - const rect = resizableElement.getBoundingClientRect(); - - resizeState = { - dragging: true, - startX: event.clientX, - startY: event.clientY, - startWidth: rect.width, - startHeight: rect.height, - handle, - pos, - resizableElement, - aspectRatio: rect.width / rect.height, - }; - - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - document.body.style.cursor = getResizeCursor(handle); - document.body.style.userSelect = "none"; - } - - function handleMouseMove(event) { - if (!resizeState.dragging) return; - - event.preventDefault(); - event.stopPropagation(); - - let deltaX = event.clientX - resizeState.startX; - let deltaY = event.clientY - resizeState.startY; - - // Adjust delta based on handle position - if (resizeState.handle.includes("w")) deltaX = -deltaX; - if (resizeState.handle.includes("n")) deltaY = -deltaY; - - // Calculate new dimensions maintaining aspect ratio - const newWidth = Math.max(20, resizeState.startWidth + deltaX); - - // Apply the new size immediately for visual feedback - if (resizeState.resizableElement) { - resizeState.resizableElement.style.width = `${newWidth}px`; - resizeState.resizableElement.style.height = "auto"; - - // Update handle positions - updateHandlePositions(resizeState.resizableElement); - } - } - - function handleMouseUp(event) { - if (!resizeState.dragging) return; - - cleanupEventListeners(); - - let deltaX = event.clientX - resizeState.startX; - let deltaY = event.clientY - resizeState.startY; - - // Adjust delta based on handle position - if (resizeState.handle.includes("w")) deltaX = -deltaX; - if (resizeState.handle.includes("n")) deltaY = -deltaY; - - // Calculate final dimensions - const newWidth = Math.max(20, resizeState.startWidth + deltaX); - const newHeight = newWidth / resizeState.aspectRatio; - - // Update the document - if ( - editorView && - resizeState.pos < editorView.state.doc.content.size - ) { - const tr = editorView.state.tr; - const node = tr.doc.nodeAt(resizeState.pos); - - if (nodeNames.includes(node?.type.name)) { - const attrs = { - ...node.attrs, - size: { - ...node.attrs.size, - width: Math.round(newWidth), - height: Math.round(newHeight), - }, - }; - - tr.setNodeMarkup(resizeState.pos, null, attrs); - tr.setMeta(ResizePluginKey, { action: "resize" }); - editorView.dispatch(tr); - } - } - - // Reset resize state - resizeState = { - dragging: false, - startX: 0, - startY: 0, - startWidth: 0, - startHeight: 0, - handle: null, - pos: null, - resizableElement: null, - aspectRatio: 1, - }; - } - - function cleanupEventListeners() { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - document.body.style.cursor = ""; - document.body.style.userSelect = ""; - } - - function getResizeCursor(handle) { - switch (handle) { - case "nw": - case "se": - return "nwse-resize"; - case "ne": - case "sw": - return "nesw-resize"; - default: - return "default"; - } - } +export const ResizePluginKey = new PluginKey('resize-nodes'); + +const resize = (nodeNames = ['image']) => { + // Track the resize state + let resizeState = { + dragging: false, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + handle: null, + pos: null, + resizableElement: null, + aspectRatio: 1, + }; + + // Store references to resize handles container and editor view + let resizeContainer = null; + let editorView = null; + let globalClickHandler = null; + let globalMousedownHandler = null; + + return new Plugin({ + key: ResizePluginKey, + + state: { + init() { + return DecorationSet.empty; + }, + + apply(tr, oldState, _, newState) { + // Skip if the transaction is from this plugin + if (tr.getMeta(ResizePluginKey)) { + return oldState; + } + + const decorations = []; + const { selection } = newState; + + // Only create decoration if one of the resizable nodes is selected + if (nodeNames.includes(selection.node?.type.name)) { + decorations.push( + Decoration.node(selection.from, selection.to, { + nodeName: 'span', + class: 'resizable-wrapper', + 'data-pos': selection.from, + }), + ); + } + + return DecorationSet.create(newState.doc, decorations); + }, + }, + + props: { + decorations(state) { + return this.getState(state); + }, + }, + + view(view) { + editorView = view; + + // Add global click handler + globalClickHandler = (event) => { + if (!event.target.closest('.resizable-wrapper') && !event.target.closest('.resize-container')) { + hideResizeHandles(); + } + }; + + document.addEventListener('click', globalClickHandler); + + // Add global mousedown handler + globalMousedownHandler = (event) => { + if (event.target.closest('.resize-handle')) { + event.preventDefault(); + event.stopPropagation(); + startResize(editorView, event, event.target); + return true; + } + }; + + document.addEventListener('mousedown', globalMousedownHandler); + + return { + update(view, prevState) { + // Show/hide resize handles based on selection changes + const { selection } = view.state; + const prevSelection = prevState.selection; + + if (selection.from !== prevSelection.from || selection.to !== prevSelection.to) { + setTimeout(() => { + const selectedResizableWrapper = document.querySelector('.resizable-wrapper'); + if (selectedResizableWrapper) { + showResizeHandles(view, selectedResizableWrapper); + } else { + hideResizeHandles(); + } + }, 10); + } + }, + + destroy() { + hideResizeHandles(); + cleanupEventListeners(); + if (globalClickHandler) { + document.removeEventListener('click', globalClickHandler); + globalClickHandler = null; + } + if (globalMousedownHandler) { + document.removeEventListener('mousedown', globalMousedownHandler); + globalMousedownHandler = null; + } + editorView = null; + }, + }; + }, + }); + + function showResizeHandles(view, wrapper) { + hideResizeHandles(); + + const pos = Number.parseInt(wrapper.getAttribute('data-pos'), 10); + + const node = view.state.doc.nodeAt(pos); + if (!nodeNames.includes(node?.type.name)) return; + + // Create resize container + resizeContainer = document.createElement('div'); + resizeContainer.className = 'resize-container'; + resizeContainer.style.position = 'absolute'; + resizeContainer.style.pointerEvents = 'none'; + resizeContainer.style.zIndex = '1000'; + + // Create handles + const handles = ['nw', 'ne', 'sw', 'se']; + for (const handle of handles) { + const handleEl = document.createElement('div'); + handleEl.className = `resize-handle resize-handle-${handle}`; + handleEl.setAttribute('data-handle', handle); + handleEl.setAttribute('data-pos', pos); + handleEl.style.pointerEvents = 'auto'; + resizeContainer.appendChild(handleEl); + } + + // Position the container relative to the resizable element + document.body.appendChild(resizeContainer); + updateHandlePositions(wrapper.firstElementChild); + } + + function hideResizeHandles() { + if (resizeContainer?.parentNode) { + resizeContainer.parentNode.removeChild(resizeContainer); + resizeContainer = null; + } + } + + function updateHandlePositions(resizableElement) { + if (!resizeContainer || !resizableElement) return; + + const rect = resizableElement.getBoundingClientRect(); + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + + resizeContainer.style.left = `${rect.left + scrollLeft}px`; + resizeContainer.style.top = `${rect.top + scrollTop}px`; + resizeContainer.style.width = `${rect.width}px`; + resizeContainer.style.height = `${rect.height}px`; + } + + function startResize(view, event, handleElement) { + const handle = handleElement.getAttribute('data-handle'); + const pos = Number.parseInt(handleElement.getAttribute('data-pos'), 10); + const node = view.state.doc.nodeAt(pos); + + if (!nodeNames.includes(node?.type.name)) return; + + const resizableElement = view.nodeDOM(pos); + + if (!resizableElement) return; + + const rect = resizableElement.getBoundingClientRect(); + + resizeState = { + dragging: true, + startX: event.clientX, + startY: event.clientY, + startWidth: rect.width, + startHeight: rect.height, + handle, + pos, + resizableElement, + aspectRatio: rect.width / rect.height, + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.cursor = getResizeCursor(handle); + document.body.style.userSelect = 'none'; + } + + function handleMouseMove(event) { + if (!resizeState.dragging) return; + + event.preventDefault(); + event.stopPropagation(); + + let deltaX = event.clientX - resizeState.startX; + let deltaY = event.clientY - resizeState.startY; + + // Adjust delta based on handle position + if (resizeState.handle.includes('w')) deltaX = -deltaX; + if (resizeState.handle.includes('n')) deltaY = -deltaY; + + // Calculate new dimensions maintaining aspect ratio + const newWidth = Math.max(20, resizeState.startWidth + deltaX); + + // Apply the new size immediately for visual feedback + if (resizeState.resizableElement) { + resizeState.resizableElement.style.width = `${newWidth}px`; + resizeState.resizableElement.style.height = 'auto'; + + // Update handle positions + updateHandlePositions(resizeState.resizableElement); + } + } + + function handleMouseUp(event) { + if (!resizeState.dragging) return; + + cleanupEventListeners(); + + let deltaX = event.clientX - resizeState.startX; + let deltaY = event.clientY - resizeState.startY; + + // Adjust delta based on handle position + if (resizeState.handle.includes('w')) deltaX = -deltaX; + if (resizeState.handle.includes('n')) deltaY = -deltaY; + + // Calculate final dimensions + const newWidth = Math.max(20, resizeState.startWidth + deltaX); + const newHeight = newWidth / resizeState.aspectRatio; + + // Update the document + if (editorView && resizeState.pos < editorView.state.doc.content.size) { + const tr = editorView.state.tr; + const node = tr.doc.nodeAt(resizeState.pos); + + if (nodeNames.includes(node?.type.name)) { + const attrs = { + ...node.attrs, + size: { + ...node.attrs.size, + width: Math.round(newWidth), + height: Math.round(newHeight), + }, + }; + + tr.setNodeMarkup(resizeState.pos, null, attrs); + tr.setMeta(ResizePluginKey, { action: 'resize' }); + editorView.dispatch(tr); + } + } + + // Reset resize state + resizeState = { + dragging: false, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + handle: null, + pos: null, + resizableElement: null, + aspectRatio: 1, + }; + } + + function cleanupEventListeners() { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } + + function getResizeCursor(handle) { + switch (handle) { + case 'nw': + case 'se': + return 'nwse-resize'; + case 'ne': + case 'sw': + return 'nesw-resize'; + default: + return 'default'; + } + } }; export const ResizePlugin = Extension.create({ name: 'resizePlugin', addPmPlugins() { - return [resize(["image"])] - } + return [resize(['image'])]; + }, }); diff --git a/packages/super-editor/src/extensions/resize-plugin/resize.css b/packages/super-editor/src/extensions/resize-plugin/resize.css index 467cfbe60b..83e5995ba6 100644 --- a/packages/super-editor/src/extensions/resize-plugin/resize.css +++ b/packages/super-editor/src/extensions/resize-plugin/resize.css @@ -1,87 +1,87 @@ /* Resize handles container */ .resize-container { - position: absolute; - pointer-events: none; - z-index: 1000; + position: absolute; + pointer-events: none; + z-index: 1000; } /* Resize handles */ .resize-handle { - position: absolute; - width: 12px; - height: 12px; - background-color: #4dabf7; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); - pointer-events: auto; - transition: all 0.1s ease; + position: absolute; + width: 12px; + height: 12px; + background-color: #4dabf7; + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); + pointer-events: auto; + transition: all 0.1s ease; } .resize-handle:hover { - background-color: #228be6; - transform: scale(1.1); - box-shadow: 0 0 6px rgba(0, 0, 0, 0.4); + background-color: #228be6; + transform: scale(1.1); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.4); } /* Handle positions */ .resize-handle-nw { - top: -6px; - left: -6px; - cursor: nwse-resize; + top: -6px; + left: -6px; + cursor: nwse-resize; } .resize-handle-ne { - top: -6px; - right: -6px; - cursor: nesw-resize; + top: -6px; + right: -6px; + cursor: nesw-resize; } .resize-handle-sw { - bottom: -6px; - left: -6px; - cursor: nesw-resize; + bottom: -6px; + left: -6px; + cursor: nesw-resize; } .resize-handle-se { - bottom: -6px; - right: -6px; - cursor: nwse-resize; + bottom: -6px; + right: -6px; + cursor: nwse-resize; } /* Hide handles when editor loses focus */ .ProseMirror:not(.ProseMirror-focused) .resize-container { - display: none; + display: none; } /* Smooth transitions for resizing */ .resizable-wrapper * { - transition: none; + transition: none; } -.resizable-wrapper *:not([style*="width"]) { - transition: all 0.2s ease; +.resizable-wrapper *:not([style*='width']) { + transition: all 0.2s ease; } /* Resize feedback indicator */ .resizable-wrapper::after { - content: "Drag corners to resize"; - position: absolute; - bottom: -25px; - left: 50%; - transform: translateX(-50%); - background-color: rgba(77, 171, 247, 0.9); - color: white; - font-size: 11px; - padding: 4px 8px; - border-radius: 4px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.3s ease; - z-index: 1001; + content: 'Drag corners to resize'; + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(77, 171, 247, 0.9); + color: white; + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 1001; } .resizable-wrapper:hover::after { - opacity: 1; + opacity: 1; } From 1b07f4f7adcaf114e7830dc69e18e9509818f1cc Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 3 Jun 2025 16:11:04 +0200 Subject: [PATCH 04/11] move css files tio assets and rename resize-plugin to noderesizer --- .../src/assets/styles/extensions/_all.css | 1 + .../assets/styles/extensions/noderesizer.css | 87 +++++ packages/super-editor/src/extensions/index.js | 8 +- .../src/extensions/noderesizer/index.js | 1 + .../src/extensions/noderesizer/noderesizer.js | 316 ++++++++++++++++++ 5 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 packages/super-editor/src/assets/styles/extensions/noderesizer.css create mode 100644 packages/super-editor/src/extensions/noderesizer/index.js create mode 100644 packages/super-editor/src/extensions/noderesizer/noderesizer.js diff --git a/packages/super-editor/src/assets/styles/extensions/_all.css b/packages/super-editor/src/assets/styles/extensions/_all.css index 7ea72b9a82..2435cec8ec 100644 --- a/packages/super-editor/src/assets/styles/extensions/_all.css +++ b/packages/super-editor/src/assets/styles/extensions/_all.css @@ -3,3 +3,4 @@ @import './placeholder.css'; @import './mention.css'; @import './comments.css'; +@import './noderesizer.css'; diff --git a/packages/super-editor/src/assets/styles/extensions/noderesizer.css b/packages/super-editor/src/assets/styles/extensions/noderesizer.css new file mode 100644 index 0000000000..8718e05d1f --- /dev/null +++ b/packages/super-editor/src/assets/styles/extensions/noderesizer.css @@ -0,0 +1,87 @@ +/* Resize handles container */ +.sd-editor-resize-container { + position: absolute; + pointer-events: none; + z-index: 1000; +} + +/* Resize handles */ +.sd-editor-resize-handle { + position: absolute; + width: 12px; + height: 12px; + background-color: #4dabf7; + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); + pointer-events: auto; + transition: all 0.1s ease; +} + +.sd-editor-resize-handle:hover { + background-color: #228be6; + transform: scale(1.1); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.4); +} + +/* Handle positions */ +.sd-editor-resize-handle-nw { + top: -6px; + left: -6px; + cursor: nwse-resize; +} + +.sd-editor-resize-handle-ne { + top: -6px; + right: -6px; + cursor: nesw-resize; +} + +.sd-editor-resize-handle-sw { + bottom: -6px; + left: -6px; + cursor: nesw-resize; +} + +.sd-editor-resize-handle-se { + bottom: -6px; + right: -6px; + cursor: nwse-resize; +} + +/* Hide handles when editor loses focus */ +.ProseMirror:not(.ProseMirror-focused) .sd-editor-resize-container { + display: none; +} + +/* Smooth transitions for resizing */ +.sd-editor-resizable-wrapper * { + transition: none; +} + +.sd-editor-resizable-wrapper *:not([style*='width']) { + transition: all 0.2s ease; +} + +/* Resize feedback indicator */ +.sd-editor-resizable-wrapper::after { + content: 'Drag corners to resize'; + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(77, 171, 247, 0.9); + color: white; + font-size: 11px; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + z-index: 1001; +} + +.sd-editor-resizable-wrapper:hover::after { + opacity: 1; +} diff --git a/packages/super-editor/src/extensions/index.js b/packages/super-editor/src/extensions/index.js index 0128390ff5..360a56b7e7 100644 --- a/packages/super-editor/src/extensions/index.js +++ b/packages/super-editor/src/extensions/index.js @@ -58,7 +58,7 @@ import { TrackChanges } from './track-changes/index.js'; import { Pagination } from './pagination/index.js'; import { LinkedStyles } from './linked-styles/linked-styles.js'; import { Search } from './search/index.js'; -import { ResizePlugin } from './resize-plugin/index.js'; +import { NodeResizer } from './noderesizer/index.js'; // Helpers import { trackChangesHelpers } from './track-changes/index.js'; @@ -101,7 +101,7 @@ const getRichTextExtensions = () => { TrackFormat, AiPlugin, Image, - ResizePlugin, + NodeResizer, ]; }; @@ -167,7 +167,7 @@ const getStarterExtensions = () => { ContentBlock, Search, StructuredContent, - ResizePlugin, + NodeResizer, ]; }; @@ -230,5 +230,5 @@ export { AiPlugin, Search, StructuredContent, - ResizePlugin, + NodeResizer, }; diff --git a/packages/super-editor/src/extensions/noderesizer/index.js b/packages/super-editor/src/extensions/noderesizer/index.js new file mode 100644 index 0000000000..91fd9cee04 --- /dev/null +++ b/packages/super-editor/src/extensions/noderesizer/index.js @@ -0,0 +1 @@ +export * from './noderesizer.js'; diff --git a/packages/super-editor/src/extensions/noderesizer/noderesizer.js b/packages/super-editor/src/extensions/noderesizer/noderesizer.js new file mode 100644 index 0000000000..1a4ceee03a --- /dev/null +++ b/packages/super-editor/src/extensions/noderesizer/noderesizer.js @@ -0,0 +1,316 @@ +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; +import { Extension } from '@core/Extension.js'; + +/** + * Plugin key for the resize plugin + */ +export const ResizePluginKey = new PluginKey('node-resizer'); + +const nodeResizer = (nodeNames = ['image']) => { + // Track the resize state + let resizeState = { + dragging: false, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + handle: null, + pos: null, + resizableElement: null, + aspectRatio: 1, + }; + + // Store references to resize handles container and editor view + let resizeContainer = null; + let editorView = null; + let globalClickHandler = null; + let globalMousedownHandler = null; + + return new Plugin({ + key: ResizePluginKey, + + state: { + init() { + return DecorationSet.empty; + }, + + apply(tr, oldState, _, newState) { + // Skip if the transaction is from this plugin + if (tr.getMeta(ResizePluginKey)) { + return oldState; + } + + const decorations = []; + const { selection } = newState; + + // Only create decoration if one of the resizable nodes is selected + if (nodeNames.includes(selection.node?.type.name)) { + decorations.push( + Decoration.node(selection.from, selection.to, { + nodeName: 'span', + class: 'sd-editor-resizable-wrapper', + 'data-pos': selection.from, + }), + ); + } + + return DecorationSet.create(newState.doc, decorations); + }, + }, + + props: { + decorations(state) { + return this.getState(state); + }, + }, + + view(view) { + editorView = view; + + // Add global click handler + globalClickHandler = (event) => { + if ( + !event.target.closest('.sd-editor-resizable-wrapper') && + !event.target.closest('.sd-editor-resize-container') + ) { + hideResizeHandles(); + } + }; + + document.addEventListener('click', globalClickHandler); + + // Add global mousedown handler + globalMousedownHandler = (event) => { + if (event.target.closest('.sd-editor-resize-handle')) { + event.preventDefault(); + event.stopPropagation(); + startResize(editorView, event, event.target); + return true; + } + }; + + document.addEventListener('mousedown', globalMousedownHandler); + + return { + update(view, prevState) { + // Show/hide resize handles based on selection changes + const { selection } = view.state; + const prevSelection = prevState.selection; + + if (selection.from !== prevSelection.from || selection.to !== prevSelection.to) { + setTimeout(() => { + const selectedResizableWrapper = document.querySelector('.sd-editor-resizable-wrapper'); + if (selectedResizableWrapper) { + showResizeHandles(view, selectedResizableWrapper); + } else { + hideResizeHandles(); + } + }, 10); + } + }, + + destroy() { + hideResizeHandles(); + cleanupEventListeners(); + if (globalClickHandler) { + document.removeEventListener('click', globalClickHandler); + globalClickHandler = null; + } + if (globalMousedownHandler) { + document.removeEventListener('mousedown', globalMousedownHandler); + globalMousedownHandler = null; + } + editorView = null; + }, + }; + }, + }); + + function showResizeHandles(view, wrapper) { + hideResizeHandles(); + + const pos = Number.parseInt(wrapper.getAttribute('data-pos'), 10); + + const node = view.state.doc.nodeAt(pos); + if (!nodeNames.includes(node?.type.name)) return; + + // Create resize container + resizeContainer = document.createElement('div'); + resizeContainer.className = 'sd-editor-resize-container'; + resizeContainer.style.position = 'absolute'; + resizeContainer.style.pointerEvents = 'none'; + resizeContainer.style.zIndex = '1000'; + + // Create handles + const handles = ['nw', 'ne', 'sw', 'se']; + for (const handle of handles) { + const handleEl = document.createElement('div'); + handleEl.className = `sd-editor-resize-handle sd-editor-resize-handle-${handle}`; + handleEl.setAttribute('data-handle', handle); + handleEl.setAttribute('data-pos', pos); + handleEl.style.pointerEvents = 'auto'; + resizeContainer.appendChild(handleEl); + } + + // Position the container relative to the resizable element + document.body.appendChild(resizeContainer); + updateHandlePositions(wrapper.firstElementChild); + } + + function hideResizeHandles() { + if (resizeContainer?.parentNode) { + resizeContainer.parentNode.removeChild(resizeContainer); + resizeContainer = null; + } + } + + function updateHandlePositions(resizableElement) { + if (!resizeContainer || !resizableElement) return; + + const rect = resizableElement.getBoundingClientRect(); + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + + resizeContainer.style.left = `${rect.left + scrollLeft}px`; + resizeContainer.style.top = `${rect.top + scrollTop}px`; + resizeContainer.style.width = `${rect.width}px`; + resizeContainer.style.height = `${rect.height}px`; + } + + function startResize(view, event, handleElement) { + const handle = handleElement.getAttribute('data-handle'); + const pos = Number.parseInt(handleElement.getAttribute('data-pos'), 10); + const node = view.state.doc.nodeAt(pos); + + if (!nodeNames.includes(node?.type.name)) return; + + const resizableElement = view.nodeDOM(pos); + + if (!resizableElement) return; + + const rect = resizableElement.getBoundingClientRect(); + + resizeState = { + dragging: true, + startX: event.clientX, + startY: event.clientY, + startWidth: rect.width, + startHeight: rect.height, + handle, + pos, + resizableElement, + aspectRatio: rect.width / rect.height, + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.cursor = getResizeCursor(handle); + document.body.style.userSelect = 'none'; + } + + function handleMouseMove(event) { + if (!resizeState.dragging) return; + + event.preventDefault(); + event.stopPropagation(); + + let deltaX = event.clientX - resizeState.startX; + let deltaY = event.clientY - resizeState.startY; + + // Adjust delta based on handle position + if (resizeState.handle.includes('w')) deltaX = -deltaX; + if (resizeState.handle.includes('n')) deltaY = -deltaY; + + // Calculate new dimensions maintaining aspect ratio + const newWidth = Math.max(20, resizeState.startWidth + deltaX); + + // Apply the new size immediately for visual feedback + if (resizeState.resizableElement) { + resizeState.resizableElement.style.width = `${newWidth}px`; + resizeState.resizableElement.style.height = 'auto'; + + // Update handle positions + updateHandlePositions(resizeState.resizableElement); + } + } + + function handleMouseUp(event) { + if (!resizeState.dragging) return; + + cleanupEventListeners(); + + let deltaX = event.clientX - resizeState.startX; + let deltaY = event.clientY - resizeState.startY; + + // Adjust delta based on handle position + if (resizeState.handle.includes('w')) deltaX = -deltaX; + if (resizeState.handle.includes('n')) deltaY = -deltaY; + + // Calculate final dimensions + const newWidth = Math.max(20, resizeState.startWidth + deltaX); + const newHeight = newWidth / resizeState.aspectRatio; + + // Update the document + if (editorView && resizeState.pos < editorView.state.doc.content.size) { + const tr = editorView.state.tr; + const node = tr.doc.nodeAt(resizeState.pos); + + if (nodeNames.includes(node?.type.name)) { + const attrs = { + ...node.attrs, + size: { + ...node.attrs.size, + width: Math.round(newWidth), + height: Math.round(newHeight), + }, + }; + + tr.setNodeMarkup(resizeState.pos, null, attrs); + tr.setMeta(ResizePluginKey, { action: 'resize' }); + editorView.dispatch(tr); + } + } + + // Reset resize state + resizeState = { + dragging: false, + startX: 0, + startY: 0, + startWidth: 0, + startHeight: 0, + handle: null, + pos: null, + resizableElement: null, + aspectRatio: 1, + }; + } + + function cleanupEventListeners() { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } + + function getResizeCursor(handle) { + switch (handle) { + case 'nw': + case 'se': + return 'nwse-resize'; + case 'ne': + case 'sw': + return 'nesw-resize'; + default: + return 'default'; + } + } +}; + +export const NodeResizer = Extension.create({ + name: 'nodeResizer', + + addPmPlugins() { + return [nodeResizer(['image'])]; + }, +}); From fe006421acb6ecc0888abd0eeefb53020a30e75b Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 4 Jun 2025 00:19:08 +0200 Subject: [PATCH 05/11] remove old files --- .../src/extensions/noderesizer/noderesizer.js | 8 +- .../src/extensions/resize-plugin/index.js | 2 - .../extensions/resize-plugin/resize-plugin.js | 313 ------------------ .../src/extensions/resize-plugin/resize.css | 87 ----- 4 files changed, 4 insertions(+), 406 deletions(-) delete mode 100644 packages/super-editor/src/extensions/resize-plugin/index.js delete mode 100644 packages/super-editor/src/extensions/resize-plugin/resize-plugin.js delete mode 100644 packages/super-editor/src/extensions/resize-plugin/resize.css diff --git a/packages/super-editor/src/extensions/noderesizer/noderesizer.js b/packages/super-editor/src/extensions/noderesizer/noderesizer.js index 1a4ceee03a..c10624f551 100644 --- a/packages/super-editor/src/extensions/noderesizer/noderesizer.js +++ b/packages/super-editor/src/extensions/noderesizer/noderesizer.js @@ -5,7 +5,7 @@ import { Extension } from '@core/Extension.js'; /** * Plugin key for the resize plugin */ -export const ResizePluginKey = new PluginKey('node-resizer'); +export const NodeResizerKey = new PluginKey('node-resizer'); const nodeResizer = (nodeNames = ['image']) => { // Track the resize state @@ -28,7 +28,7 @@ const nodeResizer = (nodeNames = ['image']) => { let globalMousedownHandler = null; return new Plugin({ - key: ResizePluginKey, + key: NodeResizerKey, state: { init() { @@ -37,7 +37,7 @@ const nodeResizer = (nodeNames = ['image']) => { apply(tr, oldState, _, newState) { // Skip if the transaction is from this plugin - if (tr.getMeta(ResizePluginKey)) { + if (tr.getMeta(NodeResizerKey)) { return oldState; } @@ -267,7 +267,7 @@ const nodeResizer = (nodeNames = ['image']) => { }; tr.setNodeMarkup(resizeState.pos, null, attrs); - tr.setMeta(ResizePluginKey, { action: 'resize' }); + tr.setMeta(NodeResizerKey, { action: 'resize' }); editorView.dispatch(tr); } } diff --git a/packages/super-editor/src/extensions/resize-plugin/index.js b/packages/super-editor/src/extensions/resize-plugin/index.js deleted file mode 100644 index 2f37653e53..0000000000 --- a/packages/super-editor/src/extensions/resize-plugin/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './resize-plugin.js'; -import './resize.css'; diff --git a/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js b/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js deleted file mode 100644 index 26b855e0fe..0000000000 --- a/packages/super-editor/src/extensions/resize-plugin/resize-plugin.js +++ /dev/null @@ -1,313 +0,0 @@ -import { Plugin, PluginKey } from 'prosemirror-state'; -import { Decoration, DecorationSet } from 'prosemirror-view'; -import { Extension } from '@core/Extension.js'; - -/** - * Plugin key for the resize plugin - */ -export const ResizePluginKey = new PluginKey('resize-nodes'); - -const resize = (nodeNames = ['image']) => { - // Track the resize state - let resizeState = { - dragging: false, - startX: 0, - startY: 0, - startWidth: 0, - startHeight: 0, - handle: null, - pos: null, - resizableElement: null, - aspectRatio: 1, - }; - - // Store references to resize handles container and editor view - let resizeContainer = null; - let editorView = null; - let globalClickHandler = null; - let globalMousedownHandler = null; - - return new Plugin({ - key: ResizePluginKey, - - state: { - init() { - return DecorationSet.empty; - }, - - apply(tr, oldState, _, newState) { - // Skip if the transaction is from this plugin - if (tr.getMeta(ResizePluginKey)) { - return oldState; - } - - const decorations = []; - const { selection } = newState; - - // Only create decoration if one of the resizable nodes is selected - if (nodeNames.includes(selection.node?.type.name)) { - decorations.push( - Decoration.node(selection.from, selection.to, { - nodeName: 'span', - class: 'resizable-wrapper', - 'data-pos': selection.from, - }), - ); - } - - return DecorationSet.create(newState.doc, decorations); - }, - }, - - props: { - decorations(state) { - return this.getState(state); - }, - }, - - view(view) { - editorView = view; - - // Add global click handler - globalClickHandler = (event) => { - if (!event.target.closest('.resizable-wrapper') && !event.target.closest('.resize-container')) { - hideResizeHandles(); - } - }; - - document.addEventListener('click', globalClickHandler); - - // Add global mousedown handler - globalMousedownHandler = (event) => { - if (event.target.closest('.resize-handle')) { - event.preventDefault(); - event.stopPropagation(); - startResize(editorView, event, event.target); - return true; - } - }; - - document.addEventListener('mousedown', globalMousedownHandler); - - return { - update(view, prevState) { - // Show/hide resize handles based on selection changes - const { selection } = view.state; - const prevSelection = prevState.selection; - - if (selection.from !== prevSelection.from || selection.to !== prevSelection.to) { - setTimeout(() => { - const selectedResizableWrapper = document.querySelector('.resizable-wrapper'); - if (selectedResizableWrapper) { - showResizeHandles(view, selectedResizableWrapper); - } else { - hideResizeHandles(); - } - }, 10); - } - }, - - destroy() { - hideResizeHandles(); - cleanupEventListeners(); - if (globalClickHandler) { - document.removeEventListener('click', globalClickHandler); - globalClickHandler = null; - } - if (globalMousedownHandler) { - document.removeEventListener('mousedown', globalMousedownHandler); - globalMousedownHandler = null; - } - editorView = null; - }, - }; - }, - }); - - function showResizeHandles(view, wrapper) { - hideResizeHandles(); - - const pos = Number.parseInt(wrapper.getAttribute('data-pos'), 10); - - const node = view.state.doc.nodeAt(pos); - if (!nodeNames.includes(node?.type.name)) return; - - // Create resize container - resizeContainer = document.createElement('div'); - resizeContainer.className = 'resize-container'; - resizeContainer.style.position = 'absolute'; - resizeContainer.style.pointerEvents = 'none'; - resizeContainer.style.zIndex = '1000'; - - // Create handles - const handles = ['nw', 'ne', 'sw', 'se']; - for (const handle of handles) { - const handleEl = document.createElement('div'); - handleEl.className = `resize-handle resize-handle-${handle}`; - handleEl.setAttribute('data-handle', handle); - handleEl.setAttribute('data-pos', pos); - handleEl.style.pointerEvents = 'auto'; - resizeContainer.appendChild(handleEl); - } - - // Position the container relative to the resizable element - document.body.appendChild(resizeContainer); - updateHandlePositions(wrapper.firstElementChild); - } - - function hideResizeHandles() { - if (resizeContainer?.parentNode) { - resizeContainer.parentNode.removeChild(resizeContainer); - resizeContainer = null; - } - } - - function updateHandlePositions(resizableElement) { - if (!resizeContainer || !resizableElement) return; - - const rect = resizableElement.getBoundingClientRect(); - const scrollTop = window.pageYOffset || document.documentElement.scrollTop; - const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - - resizeContainer.style.left = `${rect.left + scrollLeft}px`; - resizeContainer.style.top = `${rect.top + scrollTop}px`; - resizeContainer.style.width = `${rect.width}px`; - resizeContainer.style.height = `${rect.height}px`; - } - - function startResize(view, event, handleElement) { - const handle = handleElement.getAttribute('data-handle'); - const pos = Number.parseInt(handleElement.getAttribute('data-pos'), 10); - const node = view.state.doc.nodeAt(pos); - - if (!nodeNames.includes(node?.type.name)) return; - - const resizableElement = view.nodeDOM(pos); - - if (!resizableElement) return; - - const rect = resizableElement.getBoundingClientRect(); - - resizeState = { - dragging: true, - startX: event.clientX, - startY: event.clientY, - startWidth: rect.width, - startHeight: rect.height, - handle, - pos, - resizableElement, - aspectRatio: rect.width / rect.height, - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - document.body.style.cursor = getResizeCursor(handle); - document.body.style.userSelect = 'none'; - } - - function handleMouseMove(event) { - if (!resizeState.dragging) return; - - event.preventDefault(); - event.stopPropagation(); - - let deltaX = event.clientX - resizeState.startX; - let deltaY = event.clientY - resizeState.startY; - - // Adjust delta based on handle position - if (resizeState.handle.includes('w')) deltaX = -deltaX; - if (resizeState.handle.includes('n')) deltaY = -deltaY; - - // Calculate new dimensions maintaining aspect ratio - const newWidth = Math.max(20, resizeState.startWidth + deltaX); - - // Apply the new size immediately for visual feedback - if (resizeState.resizableElement) { - resizeState.resizableElement.style.width = `${newWidth}px`; - resizeState.resizableElement.style.height = 'auto'; - - // Update handle positions - updateHandlePositions(resizeState.resizableElement); - } - } - - function handleMouseUp(event) { - if (!resizeState.dragging) return; - - cleanupEventListeners(); - - let deltaX = event.clientX - resizeState.startX; - let deltaY = event.clientY - resizeState.startY; - - // Adjust delta based on handle position - if (resizeState.handle.includes('w')) deltaX = -deltaX; - if (resizeState.handle.includes('n')) deltaY = -deltaY; - - // Calculate final dimensions - const newWidth = Math.max(20, resizeState.startWidth + deltaX); - const newHeight = newWidth / resizeState.aspectRatio; - - // Update the document - if (editorView && resizeState.pos < editorView.state.doc.content.size) { - const tr = editorView.state.tr; - const node = tr.doc.nodeAt(resizeState.pos); - - if (nodeNames.includes(node?.type.name)) { - const attrs = { - ...node.attrs, - size: { - ...node.attrs.size, - width: Math.round(newWidth), - height: Math.round(newHeight), - }, - }; - - tr.setNodeMarkup(resizeState.pos, null, attrs); - tr.setMeta(ResizePluginKey, { action: 'resize' }); - editorView.dispatch(tr); - } - } - - // Reset resize state - resizeState = { - dragging: false, - startX: 0, - startY: 0, - startWidth: 0, - startHeight: 0, - handle: null, - pos: null, - resizableElement: null, - aspectRatio: 1, - }; - } - - function cleanupEventListeners() { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - } - - function getResizeCursor(handle) { - switch (handle) { - case 'nw': - case 'se': - return 'nwse-resize'; - case 'ne': - case 'sw': - return 'nesw-resize'; - default: - return 'default'; - } - } -}; - -export const ResizePlugin = Extension.create({ - name: 'resizePlugin', - - addPmPlugins() { - return [resize(['image'])]; - }, -}); diff --git a/packages/super-editor/src/extensions/resize-plugin/resize.css b/packages/super-editor/src/extensions/resize-plugin/resize.css deleted file mode 100644 index 83e5995ba6..0000000000 --- a/packages/super-editor/src/extensions/resize-plugin/resize.css +++ /dev/null @@ -1,87 +0,0 @@ -/* Resize handles container */ -.resize-container { - position: absolute; - pointer-events: none; - z-index: 1000; -} - -/* Resize handles */ -.resize-handle { - position: absolute; - width: 12px; - height: 12px; - background-color: #4dabf7; - border: 2px solid #fff; - border-radius: 50%; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); - pointer-events: auto; - transition: all 0.1s ease; -} - -.resize-handle:hover { - background-color: #228be6; - transform: scale(1.1); - box-shadow: 0 0 6px rgba(0, 0, 0, 0.4); -} - -/* Handle positions */ -.resize-handle-nw { - top: -6px; - left: -6px; - cursor: nwse-resize; -} - -.resize-handle-ne { - top: -6px; - right: -6px; - cursor: nesw-resize; -} - -.resize-handle-sw { - bottom: -6px; - left: -6px; - cursor: nesw-resize; -} - -.resize-handle-se { - bottom: -6px; - right: -6px; - cursor: nwse-resize; -} - -/* Hide handles when editor loses focus */ -.ProseMirror:not(.ProseMirror-focused) .resize-container { - display: none; -} - -/* Smooth transitions for resizing */ -.resizable-wrapper * { - transition: none; -} - -.resizable-wrapper *:not([style*='width']) { - transition: all 0.2s ease; -} - -/* Resize feedback indicator */ -.resizable-wrapper::after { - content: 'Drag corners to resize'; - position: absolute; - bottom: -25px; - left: 50%; - transform: translateX(-50%); - background-color: rgba(77, 171, 247, 0.9); - color: white; - font-size: 11px; - padding: 4px 8px; - border-radius: 4px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.3s ease; - z-index: 1001; -} - -.resizable-wrapper:hover::after { - opacity: 1; -} From 9bd22fcc446c8be4eafa217b23512df92bdd68d7 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 4 Jun 2025 23:02:12 +0200 Subject: [PATCH 06/11] Make work if there are multiple editors on the same page --- .../super-editor/src/extensions/noderesizer/noderesizer.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/super-editor/src/extensions/noderesizer/noderesizer.js b/packages/super-editor/src/extensions/noderesizer/noderesizer.js index c10624f551..42658b63f9 100644 --- a/packages/super-editor/src/extensions/noderesizer/noderesizer.js +++ b/packages/super-editor/src/extensions/noderesizer/noderesizer.js @@ -181,9 +181,7 @@ const nodeResizer = (nodeNames = ['image']) => { function startResize(view, event, handleElement) { const handle = handleElement.getAttribute('data-handle'); const pos = Number.parseInt(handleElement.getAttribute('data-pos'), 10); - const node = view.state.doc.nodeAt(pos); - - if (!nodeNames.includes(node?.type.name)) return; + if (view.state.selection.from !== pos || !nodeNames.includes(view.state.selection.node?.type.name)) return; const resizableElement = view.nodeDOM(pos); From fbc9d86d1fd3173c3c3a9e9dd87332744eb30133 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Fri, 6 Jun 2025 11:03:57 -0700 Subject: [PATCH 07/11] Tweaks to GHA --- .github/workflows/npm-publish-next.yml | 2 +- .github/workflows/npm-publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npm-publish-next.yml b/.github/workflows/npm-publish-next.yml index 1e2bb0f142..492abd228f 100644 --- a/.github/workflows/npm-publish-next.yml +++ b/.github/workflows/npm-publish-next.yml @@ -1,4 +1,4 @@ -name: Publish SuperDoc Package to npm +name: Publish develop branch to npm ("next" tag) on: push: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 78920a290d..adc1ff716c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,4 +1,4 @@ -name: Publish SuperDoc Package to npm +name: Release "latest" tag SuperDoc Package to npm on: push: From 1f0575c66c893037dc57bba6e346936416dbee66 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Fri, 6 Jun 2025 11:17:02 -0700 Subject: [PATCH 08/11] update version --- packages/superdoc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index b1b74620f3..25493dd314 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -1,7 +1,7 @@ { "name": "@harbour-enterprises/superdoc", "type": "module", - "version": "0.12.31-next.5", + "version": "0.12.32", "license": "AGPL-3.0", "readme": "../../README.md", "files": [ From ba9de69877c00c9bf67314124bc878c87a7fff6d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Jun 2025 18:20:27 +0000 Subject: [PATCH 09/11] chore: release v0.12.33 [skip ci] --- packages/superdoc/CHANGELOG.md | 2 ++ packages/superdoc/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/superdoc/CHANGELOG.md b/packages/superdoc/CHANGELOG.md index 367586ffe4..18a4175e91 100644 --- a/packages/superdoc/CHANGELOG.md +++ b/packages/superdoc/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [0.12.33](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.31-next.5...v0.12.33) (2025-06-06) + ## [0.12.31-next.5](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.31-next.4...v0.12.31-next.5) (2025-06-06) ## [0.12.31-next.4](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.31-next.3...v0.12.31-next.4) (2025-06-06) diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index 25493dd314..35cb9c6ace 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -1,7 +1,7 @@ { "name": "@harbour-enterprises/superdoc", "type": "module", - "version": "0.12.32", + "version": "0.12.33", "license": "AGPL-3.0", "readme": "../../README.md", "files": [ From d0dc2308c4beba59d8e1b4405d04d0b377beba05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Jun 2025 22:12:19 +0000 Subject: [PATCH 10/11] chore: release v0.12.35 [skip ci] --- packages/superdoc/CHANGELOG.md | 18 ++++++++++++++++++ packages/superdoc/package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/superdoc/CHANGELOG.md b/packages/superdoc/CHANGELOG.md index 18a4175e91..f4005c9eae 100644 --- a/packages/superdoc/CHANGELOG.md +++ b/packages/superdoc/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.12.35](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.34-next.2...v0.12.35) (2025-06-06) + + +### Features + +* added extra accessibility features ([6a0f9d8](https://github.com/Harbour-Enterprises/SuperDoc/commit/6a0f9d80ae281f21ebc39beb8a14be027ef24619)) + + + +## [0.12.33](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.34-next.2...v0.12.35) (2025-06-06) + + +### Features + +* (accessibility) simulate click with enter ([f9cd0b4](https://github.com/Harbour-Enterprises/SuperDoc/commit/f9cd0b4f5f8148ad0d0afad091288b32333f4cd3)) +* added initial toolbar keyboard navigation ([e37d79b](https://github.com/Harbour-Enterprises/SuperDoc/commit/e37d79bd284265f9b22f9a1ec23d05e58c21263a)) +* added roving tabindex ([6c77015](https://github.com/Harbour-Enterprises/SuperDoc/commit/6c770155532b805888d37bc91cb4d120129d541f)) + ## [0.12.33](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.31-next.5...v0.12.33) (2025-06-06) ## [0.12.31-next.5](https://github.com/Harbour-Enterprises/SuperDoc/compare/v0.12.31-next.4...v0.12.31-next.5) (2025-06-06) diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index 6fb3b74b6f..cc7f3eb8e7 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -1,7 +1,7 @@ { "name": "@harbour-enterprises/superdoc", "type": "module", - "version": "0.12.34", + "version": "0.12.35", "license": "AGPL-3.0", "readme": "../../README.md", "files": [ From 756a7d3ac9b0719d826830c23f219e609077fd09 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 7 Jun 2025 20:54:08 +0200 Subject: [PATCH 11/11] adjust z-index --- .../super-editor/src/assets/styles/extensions/noderesizer.css | 4 ++-- .../super-editor/src/extensions/noderesizer/noderesizer.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/super-editor/src/assets/styles/extensions/noderesizer.css b/packages/super-editor/src/assets/styles/extensions/noderesizer.css index 8718e05d1f..3f9990925a 100644 --- a/packages/super-editor/src/assets/styles/extensions/noderesizer.css +++ b/packages/super-editor/src/assets/styles/extensions/noderesizer.css @@ -2,7 +2,7 @@ .sd-editor-resize-container { position: absolute; pointer-events: none; - z-index: 1000; + z-index: 11; } /* Resize handles */ @@ -79,7 +79,7 @@ pointer-events: none; opacity: 0; transition: opacity 0.3s ease; - z-index: 1001; + z-index: 12; } .sd-editor-resizable-wrapper:hover::after { diff --git a/packages/super-editor/src/extensions/noderesizer/noderesizer.js b/packages/super-editor/src/extensions/noderesizer/noderesizer.js index 42658b63f9..a6375d0e56 100644 --- a/packages/super-editor/src/extensions/noderesizer/noderesizer.js +++ b/packages/super-editor/src/extensions/noderesizer/noderesizer.js @@ -179,6 +179,7 @@ const nodeResizer = (nodeNames = ['image']) => { } function startResize(view, event, handleElement) { + if (!view.hasFocus()) return; const handle = handleElement.getAttribute('data-handle'); const pos = Number.parseInt(handleElement.getAttribute('data-pos'), 10); if (view.state.selection.from !== pos || !nodeNames.includes(view.state.selection.node?.type.name)) return;