From 2143fd136735dcf16d628b910e6fb86777a6756d Mon Sep 17 00:00:00 2001 From: Artem Nistuley Date: Wed, 14 Jan 2026 15:36:03 +0200 Subject: [PATCH] fix: annotation selection --- .../layout-engine/painters/dom/src/styles.ts | 18 +++++- .../src/components/SuperEditor.vue | 8 ++- .../src/components/toolbar/super-toolbar.js | 8 +++ .../presentation-editor/PresentationEditor.ts | 60 ++++++++++++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index 3e35ae7554..0ea2ce6e88 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -492,8 +492,22 @@ const SDT_CONTAINER_STYLES = ` const FIELD_ANNOTATION_STYLES = ` /* Field annotation draggable styles */ .superdoc-layout .annotation[data-draggable="true"] { - user-select: none; - -webkit-user-select: none; + user-select: text; +} + +.superdoc-layout .annotation::selection, +.superdoc-layout .annotation *::selection { + background: transparent; +} + +.superdoc-layout .annotation::-moz-selection, +.superdoc-layout .annotation *::-moz-selection { + background: transparent; +} + +.superdoc-layout .annotation, +.superdoc-layout .annotation * { + caret-color: transparent; } .superdoc-layout .annotation[data-draggable="true"]:hover { diff --git a/packages/super-editor/src/components/SuperEditor.vue b/packages/super-editor/src/components/SuperEditor.vue index 726c8a32a7..2d83252c9d 100644 --- a/packages/super-editor/src/components/SuperEditor.vue +++ b/packages/super-editor/src/components/SuperEditor.vue @@ -969,7 +969,13 @@ const handleMarginClick = (event) => { if (event.ctrlKey && isMacOS()) { return; } - if (event.target.classList.contains('ProseMirror')) return; + const target = event.target; + if (target?.classList?.contains('ProseMirror')) return; + + // Causes issues with node selection. + if (target?.closest?.('.presentation-editor, .superdoc-layout')) { + return; + } onMarginClickCursorChange(event, activeEditor.value); }; diff --git a/packages/super-editor/src/components/toolbar/super-toolbar.js b/packages/super-editor/src/components/toolbar/super-toolbar.js index ad626b11d9..9bd092b9bc 100644 --- a/packages/super-editor/src/components/toolbar/super-toolbar.js +++ b/packages/super-editor/src/components/toolbar/super-toolbar.js @@ -1059,6 +1059,14 @@ export class SuperToolbar extends EventEmitter { const isMarkToggle = this.isMarkToggle(item); const shouldRestoreFocus = Boolean(item?.restoreEditorFocus); + const hasArgument = argument !== null && argument !== undefined; + const isDropdownOpen = item?.type === 'dropdown' && !hasArgument; + const isFontCommand = item?.command === 'setFontFamily' || item?.command === 'setFontSize'; + if (isDropdownOpen && isFontCommand) { + // Opening/closing a dropdown should not shift editor focus or alter selection state. + return; + } + // If the editor wasn't focused and this is a mark toggle, queue it and keep the button active // until the next selection update (after the user clicks into the editor). if (!wasFocused && isMarkToggle) { diff --git a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts index dcaedc81bc..61389e938a 100644 --- a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts @@ -303,6 +303,10 @@ export class PresentationEditor extends EventEmitter { #lastClickTime = 0; #lastClickPosition: { x: number; y: number } = { x: 0, y: 0 }; #lastSelectedImageBlockId: string | null = null; + #lastSelectedFieldAnnotation: { + element: HTMLElement; + pmStart: number; + } | null = null; // Drag selection state #dragAnchor: number | null = null; @@ -4320,6 +4324,53 @@ export class PresentationEditor extends EventEmitter { this.#selectionSync.requestRender(options); } + #clearSelectedFieldAnnotationClass() { + if (this.#lastSelectedFieldAnnotation?.element?.classList?.contains('ProseMirror-selectednode')) { + this.#lastSelectedFieldAnnotation.element.classList.remove('ProseMirror-selectednode'); + } + this.#lastSelectedFieldAnnotation = null; + } + + #setSelectedFieldAnnotationClass(element: HTMLElement, pmStart: number) { + if (this.#lastSelectedFieldAnnotation?.element && this.#lastSelectedFieldAnnotation.element !== element) { + this.#lastSelectedFieldAnnotation.element.classList.remove('ProseMirror-selectednode'); + } + element.classList.add('ProseMirror-selectednode'); + this.#lastSelectedFieldAnnotation = { element, pmStart }; + } + + #syncSelectedFieldAnnotationClass(selection: Selection | null | undefined) { + if (!selection || !(selection instanceof NodeSelection)) { + this.#clearSelectedFieldAnnotationClass(); + return; + } + + const node = selection.node; + if (!node || node.type?.name !== 'fieldAnnotation') { + this.#clearSelectedFieldAnnotationClass(); + return; + } + + if (!this.#painterHost) { + this.#clearSelectedFieldAnnotationClass(); + return; + } + + const pmStart = selection.from; + if (this.#lastSelectedFieldAnnotation?.pmStart === pmStart && this.#lastSelectedFieldAnnotation.element) { + return; + } + + const selector = `.annotation[data-pm-start="${pmStart}"]`; + const element = this.#painterHost.querySelector(selector) as HTMLElement | null; + if (!element) { + this.#clearSelectedFieldAnnotationClass(); + return; + } + + this.#setSelectedFieldAnnotationClass(element, pmStart); + } + /** * Updates the visual cursor/selection overlay to match the current editor selection. * @@ -4350,6 +4401,7 @@ export class PresentationEditor extends EventEmitter { // In header/footer mode, the ProseMirror editor handles its own caret const sessionMode = this.#headerFooterSession?.session?.mode ?? 'body'; if (sessionMode !== 'body') { + this.#clearSelectedFieldAnnotationClass(); return; } @@ -4361,6 +4413,7 @@ export class PresentationEditor extends EventEmitter { // In viewing mode, don't render caret or selection highlights if (this.#isViewLocked()) { try { + this.#clearSelectedFieldAnnotationClass(); this.#localSelectionLayer.innerHTML = ''; } catch (error) { // DOM manipulation can fail if element is detached or in invalid state @@ -4377,6 +4430,7 @@ export class PresentationEditor extends EventEmitter { if (!selection) { try { + this.#clearSelectedFieldAnnotationClass(); this.#localSelectionLayer.innerHTML = ''; } catch (error) { if (process.env.NODE_ENV === 'development') { @@ -4399,6 +4453,8 @@ export class PresentationEditor extends EventEmitter { return; } + this.#syncSelectedFieldAnnotationClass(selection); + // Ensure selection endpoints remain mounted under virtualization so DOM-first // caret/selection rendering stays available during cross-page selection. this.#updateSelectionVirtualizationPins({ includeDragBuffer: this.#isDragging }); @@ -4446,7 +4502,9 @@ export class PresentationEditor extends EventEmitter { try { this.#localSelectionLayer.innerHTML = ''; - if (domRects.length > 0) { + const isFieldAnnotationSelection = + selection instanceof NodeSelection && selection.node?.type?.name === 'fieldAnnotation'; + if (domRects.length > 0 && !isFieldAnnotationSelection) { renderSelectionRects({ localSelectionLayer: this.#localSelectionLayer, rects: domRects,