diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 97bfceef..c57eb759 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/block-editor", - "version": "0.1.7", + "version": "0.1.8", "type": "module", "description": "Hydrated accessible editor runtime for WRNexus block documents.", "exports": { diff --git a/packages/block-editor/src/runtime.ts b/packages/block-editor/src/runtime.ts index c26019c9..0eb643a4 100644 --- a/packages/block-editor/src/runtime.ts +++ b/packages/block-editor/src/runtime.ts @@ -119,38 +119,35 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions) }); }, }); + const elements = [...root.querySelectorAll( + "[data-block-id][data-block-parent][data-block-slot]", + )]; + const groupSizes = new Map(); + const registrations = elements.map(element => { + const parentId = element.dataset.blockParent!; + const slot = element.dataset.blockSlot!; + const group = JSON.stringify([parentId, slot]); + const index = groupSizes.get(group) ?? 0; + groupSizes.set(group, index + 1); + groups.set(group, { parentId, slot }); + return { element, group, index }; + }); for (const element of root.querySelectorAll("[data-block-slot-target][data-block-parent]")) { const parentId = element.dataset.blockParent!; const slot = element.dataset.blockSlotTarget!; - const hasChildren = [...root.querySelectorAll("[data-block-id][data-block-parent][data-block-slot]")] - .some(child => child.dataset.blockParent === parentId && child.dataset.blockSlot === slot); - if (hasChildren) continue; const group = JSON.stringify([parentId, slot]); + if (groupSizes.has(group)) continue; groups.set(group, { parentId, slot }); cleanups.push(manager.registerDropZone({ id: `wrn-slot:${group}`, group, index: 0, element })); } - for (const element of root.querySelectorAll( - "[data-block-id][data-block-parent][data-block-slot]", - )) { + for (const { element, group, index } of registrations) { const id = element.dataset.blockId!; - const parent = element.dataset.blockParent!; - const slot = element.dataset.blockSlot!; - const group = JSON.stringify([parent, slot]); - groups.set(group, { parentId: parent, slot }); - const siblings = [ - ...root.querySelectorAll( - "[data-block-id][data-block-parent][data-block-slot]", - ), - ].filter( - (candidate) => - candidate.dataset.blockParent === parent && candidate.dataset.blockSlot === slot, - ); const handle = element.querySelector("[data-block-handle]") ?? undefined; cleanups.push( manager.registerSortable({ id, group, - index: siblings.indexOf(element), + index, element, handle, type: element.dataset.blockType, diff --git a/packages/block-editor/test/runtime.test.ts b/packages/block-editor/test/runtime.test.ts index e5f599ff..cf55e295 100644 --- a/packages/block-editor/test/runtime.test.ts +++ b/packages/block-editor/test/runtime.test.ts @@ -74,6 +74,38 @@ function fixture(onMove: MountEditorOptions["onMove"] = () => false) { }; } +test("large canvas registration scans blocks once and preserves interleaved slot order", () => { + const f = fixture(); + f.root.replaceChildren(); + for (let i = 0; i < 300; i++) { + const node = f.document.createElement("div"); + node.dataset.blockId = String(i); + node.dataset.blockParent = `parent:${i % 3}`; + node.dataset.blockSlot = "content"; + f.root.append(node); + } + for (let i = 0; i < 4; i++) { + const slot = f.document.createElement("div"); + slot.dataset.blockParent = `parent:${i}`; + slot.dataset.blockSlotTarget = "content"; + f.root.append(slot); + } + const query = f.root.querySelectorAll.bind(f.root); + let scans = 0; + f.root.querySelectorAll = ((selector: string) => { + if (selector === "[data-block-id][data-block-parent][data-block-slot]") scans++; + return query(selector); + }) as typeof f.root.querySelectorAll; + f.editor.refresh(); + const backend = f.backends.at(-1)!; + expect(scans).toBe(1); + expect(backend.registrations.map(({ id, group, index }) => ({ id, group, index }))).toEqual( + Array.from({ length: 300 }, (_, i) => ({ id: String(i), group: JSON.stringify([`parent:${i % 3}`, "content"]), index: Math.floor(i / 3) })), + ); + expect(backend.zones.map(zone => zone.group)).toEqual([JSON.stringify(["parent:3", "content"])]); + f.editor.destroy(); +}); + test("cancelled optimistic moves restore original DOM nodes across containers", async () => { const f = fixture(); const a = f.root.querySelector('[data-block-id="a"]')!;