From a887f639922b4d3ca6134f4f28bd1124897d84a9 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Wed, 29 Jul 2026 08:04:40 -0700 Subject: [PATCH 1/2] feat(quick-edit): add content anchorType for SET_SELECTED_NODE Supports highlighting/scrolling to default-content nodes (paragraph, heading, list, code, quote) selected from da-live's outline panel, via a resolver that maps proseIndex to the rendered element by data-prose-index, mirroring the existing table/image resolvers. Co-Authored-By: Claude --- nx/public/plugins/quick-edit/src/selection.js | 12 +++++++++- nx/utils/message-types.js | 3 +++ .../plugins/quick-edit/selection.test.js | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/nx/public/plugins/quick-edit/src/selection.js b/nx/public/plugins/quick-edit/src/selection.js index 59829fe47..90b741072 100644 --- a/nx/public/plugins/quick-edit/src/selection.js +++ b/nx/public/plugins/quick-edit/src/selection.js @@ -1,5 +1,5 @@ import { - findBlock, findImageAtProseIndex, pictureSrc, srcPathsMatch, OVERLAY_SELECTOR, + findBlock, findImageAtProseIndex, findTextBlock, pictureSrc, srcPathsMatch, OVERLAY_SELECTOR, } from './dom-index.js'; import { parseIndex, positionBox } from './utils.js'; import { MESSAGE_TYPES } from '../../../../utils/message-types.js'; @@ -70,6 +70,13 @@ function findPictureBySrc(src, proseIndex, root = document) { .find((pic) => srcPathsMatch(pictureSrc(pic), src)) || null; } +function findContentByIndex(proseIndex, root = document) { + if (proseIndex == null) return null; + const el = findTextBlock(proseIndex, root); + const elIndex = parseIndex(el?.getAttribute?.('data-prose-index')); + return elIndex === proseIndex ? el : null; +} + function resolveSelectionElement(node, root) { if (node.anchorType === 'table') { const block = findBlock(node.proseIndex, root); @@ -81,6 +88,9 @@ function resolveSelectionElement(node, root) { || findImageAtProseIndex(node.proseIndex, root) || findPictureBySrc(node.src, node.proseIndex, root); } + if (node.anchorType === 'content') { + return findContentByIndex(node.proseIndex, root); + } return null; } diff --git a/nx/utils/message-types.js b/nx/utils/message-types.js index 20d25eff0..a2d0d32fe 100644 --- a/nx/utils/message-types.js +++ b/nx/utils/message-types.js @@ -13,6 +13,8 @@ export const MESSAGE_TYPES = Object.freeze({ SET_BODY: 'set-body', // { body: string } SET_EDITOR_STATE: 'set-editor-state', // { editorState: PM node JSON, cursorOffset: number } SET_CURSORS: 'set-cursors', // { cursors: [{ proseIndex, remote, color }] } + // node.anchorType: 'table' (block), 'image', or 'content' (loose default-content + // node — paragraph/heading/list/code/quote, resolved via data-prose-index) SET_SELECTED_NODE: 'set-selected-node', // { node: { anchorType, proseIndex, ... } | null, scrollIntoView } // Iframe -> host: ongoing @@ -20,6 +22,7 @@ export const MESSAGE_TYPES = Object.freeze({ RELOAD: 'reload', // no payload GET_EDITOR: 'get-editor', // { cursorOffset: number } NODE_UPDATE: 'node-update', // { node: PM node JSON, cursorOffset: number } + // node.anchorType: see SET_SELECTED_NODE above NODE_SELECT: 'node-select', // { node: { anchorType, proseIndex, src?, blockIndex? } | null } HISTORY: 'history', // { action: 'undo' | 'redo' } NEW_VERSION: 'new-version', // no payload diff --git a/test/nx/public/plugins/quick-edit/selection.test.js b/test/nx/public/plugins/quick-edit/selection.test.js index 223cdf009..617e3483d 100644 --- a/test/nx/public/plugins/quick-edit/selection.test.js +++ b/test/nx/public/plugins/quick-edit/selection.test.js @@ -13,6 +13,7 @@ function buildBody() { + '
other block
' + '
unindexed
' + '' + + '

a paragraph

' + ''; } @@ -93,6 +94,27 @@ describe('quick-edit selection overlay', () => { expect(overlay.querySelector('.qe-selected-pill')).to.equal(null); }); + it('setSelectedNode draws a content box without a pill', () => { + setSelectedNode({ anchorType: 'content', proseIndex: 71 }); + const overlay = document.getElementById('qe-selection-overlay'); + expect(overlay.querySelector('.qe-selected-box')).to.not.equal(null); + expect(overlay.querySelector('.qe-selected-pill')).to.equal(null); + }); + + it('setSelectedNode ignores a content index that resolves to no element', () => { + setSelectedNode({ anchorType: 'content', proseIndex: 9999 }); + const overlay = document.getElementById('qe-selection-overlay'); + expect(overlay?.querySelector('.qe-selected-box') ?? null).to.equal(null); + }); + + it('scrolls a content node into view when requested', () => { + const el = document.querySelector('[data-prose-index="71"]'); + let scrolled = false; + el.scrollIntoView = () => { scrolled = true; }; + setSelectedNode({ anchorType: 'content', proseIndex: 71 }, document, { scrollIntoView: true }); + expect(scrolled).to.equal(true); + }); + it('setSelectedNode with null clears the overlay', () => { setSelectedNode({ anchorType: 'table', proseIndex: 50 }); setSelectedNode(null); From d65a1e5a742cdaaa9160b27900a606bde556c8a3 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Wed, 29 Jul 2026 08:19:55 -0700 Subject: [PATCH 2/2] chore(quick-edit): drop anchorType comments from message-types.js Co-Authored-By: Claude --- nx/utils/message-types.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/nx/utils/message-types.js b/nx/utils/message-types.js index a2d0d32fe..20d25eff0 100644 --- a/nx/utils/message-types.js +++ b/nx/utils/message-types.js @@ -13,8 +13,6 @@ export const MESSAGE_TYPES = Object.freeze({ SET_BODY: 'set-body', // { body: string } SET_EDITOR_STATE: 'set-editor-state', // { editorState: PM node JSON, cursorOffset: number } SET_CURSORS: 'set-cursors', // { cursors: [{ proseIndex, remote, color }] } - // node.anchorType: 'table' (block), 'image', or 'content' (loose default-content - // node — paragraph/heading/list/code/quote, resolved via data-prose-index) SET_SELECTED_NODE: 'set-selected-node', // { node: { anchorType, proseIndex, ... } | null, scrollIntoView } // Iframe -> host: ongoing @@ -22,7 +20,6 @@ export const MESSAGE_TYPES = Object.freeze({ RELOAD: 'reload', // no payload GET_EDITOR: 'get-editor', // { cursorOffset: number } NODE_UPDATE: 'node-update', // { node: PM node JSON, cursorOffset: number } - // node.anchorType: see SET_SELECTED_NODE above NODE_SELECT: 'node-select', // { node: { anchorType, proseIndex, src?, blockIndex? } | null } HISTORY: 'history', // { action: 'undo' | 'redo' } NEW_VERSION: 'new-version', // no payload