90 lines
3.2 KiB
TypeScript
90 lines
3.2 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) {
|
|
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());
|
|
},
|
|
};
|
|
}
|