Files
WRNexusJS/packages/block-editor/src/canvas.ts
T
Clintchiz 88207cdd48
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s
Add shared canvas annotation and drag handle runtime
2026-09-05 16:33:56 +05:30

52 lines
2.7 KiB
TypeScript

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<HTMLElement>("[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) value === null ? element.removeAttribute(name) : 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 = "⋮⋮";
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()); },
};
}