import type { BlockNode } from "@wrnexus/blocks"; import { mountBlockEditor, type MountedEditor, type MountEditorOptions } from "./runtime.ts"; /** Annotates an editor-only rendered document; never changes the canonical tree. */ export function mountBlockCanvas( root: HTMLElement, tree: BlockNode, options: MountEditorOptions, ): MountedEditor { const elements = new Map( [...root.querySelectorAll("[data-block-id]")].map((element) => [ element.dataset.blockId!, element, ]), ); const cleanup: Array<() => void> = []; const visit = (node: BlockNode, parent?: BlockNode, slot?: string) => { const element = elements.get(node.id); if (element) { const old = ["data-block-parent", "data-block-slot", "data-block-type", "draggable"].map( (name) => [name, element.getAttribute(name)] as const, ); cleanup.push(() => { for (const [name, value] of old) { if (value === null) element.removeAttribute(name); else element.setAttribute(name, value); } }); element.setAttribute("draggable", "false"); if (parent && slot !== undefined) { element.dataset.blockParent = parent.id; element.dataset.blockSlot = slot; element.dataset.blockType = node.type; const handle = root.ownerDocument.createElement("button"); handle.type = "button"; handle.dataset.blockHandle = ""; handle.className = "wrn-canvas-drag-handle"; handle.setAttribute("aria-label", `Move ${node.type}`); handle.style.touchAction = "none"; handle.textContent = "⋮⋮"; // Clicks must not bubble into selection or inline-edit handlers. // Pointer/keyboard sensor events remain available to the DnD backend. for (const name of ["click", "dblclick"]) handle.addEventListener(name, (event) => { event.preventDefault(); event.stopPropagation(); }); element.prepend(handle); cleanup.push(() => handle.remove()); } for (const [name, children] of Object.entries(node.slots)) { if (children.length) continue; const target = root.ownerDocument.createElement("div"); target.dataset.blockParent = node.id; target.dataset.blockSlotTarget = name; target.className = "wrn-canvas-empty-slot"; target.textContent = `Drop blocks in ${name}`; // Root is a presentation table; keep editor decorations in a valid cell. const container = element.tagName === "TABLE" ? element.querySelector("td") : element; if (container) { container.append(target); cleanup.push(() => target.remove()); } } } for (const [name, children] of Object.entries(node.slots)) for (const child of children) visit(child, node, name); }; visit(tree); let mounted: MountedEditor; try { mounted = mountBlockEditor(root, options); } catch (error) { cleanup.reverse().forEach((fn) => fn()); throw error; } let destroyed = false; return { refresh: () => { if (!destroyed) mounted.refresh(); }, destroy() { if (destroyed) return; destroyed = true; mounted.destroy(); cleanup.reverse().forEach((fn) => fn()); }, }; }