diff --git a/blocks/canvas/editor-utils/blocks.js b/blocks/canvas/editor-utils/blocks.js index 992ed3074..9148383b6 100644 --- a/blocks/canvas/editor-utils/blocks.js +++ b/blocks/canvas/editor-utils/blocks.js @@ -1,4 +1,4 @@ -import { DOMParser as PMDOMParser, NodeSelection } from 'da-y-wrapper'; +import { DOMParser as PMDOMParser, NodeSelection, TextSelection } from 'da-y-wrapper'; const NON_BLOCK_TABLE_NAMES = new Set(['metadata', 'section metadata', 'section-metadata']); @@ -223,6 +223,36 @@ export function appendBlockRow(view, tablePos, rowDom) { view.dispatch(tr.scrollIntoView()); } +/** Doc range of each section, split on horizontal rules (see deleteSection). */ +export function getSectionRanges(view) { + const { doc, schema } = view.state; + const ranges = []; + let current = null; + doc.forEach((node, offset) => { + if (node.type === schema.nodes.horizontal_rule) { + current = null; + return; + } + if (!current) { + current = { from: offset, to: offset }; + ranges.push(current); + } + current.to = offset + node.nodeSize; + }); + return ranges; +} + +/** Select a whole section and scroll it into view. */ +export function selectSection(view, sectionIndex) { + if (!view) return false; + const range = getSectionRanges(view)[sectionIndex]; + if (!range) return false; + const { tr, doc } = view.state; + view.dispatch(tr.setSelection(TextSelection.create(doc, range.from, range.to)).scrollIntoView()); + view.focus(); + return true; +} + export function deleteSection(view, sectionIndex) { if (!view) return; const { doc, schema } = view.state; diff --git a/blocks/canvas/ew-panel-extensions/iframe-protocol.js b/blocks/canvas/ew-panel-extensions/iframe-protocol.js index a2af96bbe..852131dc8 100644 --- a/blocks/canvas/ew-panel-extensions/iframe-protocol.js +++ b/blocks/canvas/ew-panel-extensions/iframe-protocol.js @@ -1,4 +1,6 @@ import { insertText, insertHTML, getEditorSelection } from './helpers.js'; +import { selectSection } from '../editor-utils/blocks.js'; +import { canvasBus } from '../utils/canvas-bus.js'; import { getNx } from '../../../scripts/utils.js'; import { getPostMessageTargetOrigin, isValidHref } from '../../shared/utils.js'; @@ -61,6 +63,25 @@ export async function setupIframeChannel({ iframe, hashState, getView, onClose } ); } + // Navigation reuses the outline's own path — same blockIndex address, same + // scroll — rather than driving the view directly, so the outline highlight + // and doc state stay in step. + if (action === 'scrollToBlock') { + const ok = Number.isInteger(details?.blockIndex) && details.blockIndex >= 0; + if (ok) canvasBus.editorSelectState.emit({ blockIndex: details.blockIndex, source: 'plugin' }); + channel.port1.postMessage({ action: 'scrollToBlockResult', details: { ok } }); + return; + } + + // Separate from navigation: a section range is the only way to reach section + // metadata, which getBlockPositions deliberately excludes from blockIndex. + // Leaves a replaceable range under sendHTML. + if (action === 'selectSection') { + const ok = editorView ? selectSection(editorView, details?.index) : false; + channel.port1.postMessage({ action: 'selectSectionResult', details: { ok } }); + return; + } + if (action === 'getSelection') { if (!editorView) { channel.port1.postMessage({ action: 'error', details: 'No editor view' });