fix(builder): correct move and sortable behavior
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 10:49:33 +05:30
1 parent 0bd2a6860d
commit edccf06658
5 files changed
+70 -5

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/block-editor", "name": "@wrnexus/block-editor",
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"description": "Hydrated accessible editor runtime for WRNexus block documents.", "description": "Hydrated accessible editor runtime for WRNexus block documents.",
"exports": { "exports": {
+5 -2
View File
@@ -49,8 +49,11 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions)
const parent = element.dataset.blockParent!; const parent = element.dataset.blockParent!;
const slot = element.dataset.blockSlot!; const slot = element.dataset.blockSlot!;
const siblings = [ const siblings = [
...element.parentElement!.querySelectorAll<HTMLElement>(":scope > [data-block-id]"), ...root.querySelectorAll<HTMLElement>("[data-block-id][data-block-parent][data-block-slot]"),
]; ].filter(
(candidate) =>
candidate.dataset.blockParent === parent && candidate.dataset.blockSlot === slot,
);
const handle = element.querySelector<HTMLElement>("[data-block-handle]") ?? undefined; const handle = element.querySelector<HTMLElement>("[data-block-handle]") ?? undefined;
cleanups.push( cleanups.push(
manager.registerSortable({ manager.registerSortable({
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/blocks", "name": "@wrnexus/blocks",
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"description": "Pure dynamic block document engine for WRNexus builders.", "description": "Pure dynamic block document engine for WRNexus builders.",
"exports": { "exports": {
+31 -1
View File
@@ -17,6 +17,7 @@ export type BlockCommand =
| { type: "insert"; location: BlockLocation; nodes: BlockNode[] } | { type: "insert"; location: BlockLocation; nodes: BlockNode[] }
| { type: "remove"; nodeIds: string[] } | { type: "remove"; nodeIds: string[] }
| { type: "move"; intent: MoveIntent } | { type: "move"; intent: MoveIntent }
| { type: "duplicate"; nodeIds: string[]; ids: string[] }
| { type: "updateProps"; nodeId: string; patch: JsonObject } | { type: "updateProps"; nodeId: string; patch: JsonObject }
| { type: "updateStyles"; nodeId: string; patch: BlockStyles } | { type: "updateStyles"; nodeId: string; patch: BlockStyles }
| { type: "replace"; nodeId: string; node: BlockNode } | { type: "replace"; nodeId: string; node: BlockNode }
@@ -152,9 +153,15 @@ export function applyCommand(
}, },
], ],
}; };
const removedBeforeDestination = command.intent.from.filter(
(location) =>
location.parentId === command.intent.to.parentId &&
location.slot === command.intent.to.slot &&
location.index < command.intent.to.index,
).length;
root = insert( root = insert(
remove(root, new Set(command.intent.nodeIds)), remove(root, new Set(command.intent.nodeIds)),
command.intent.to, { ...command.intent.to, index: Math.max(0, command.intent.to.index - removedBeforeDestination) },
moving as BlockNode[], moving as BlockNode[],
); );
affected = command.intent.nodeIds; affected = command.intent.nodeIds;
@@ -169,6 +176,29 @@ export function applyCommand(
}, },
})), })),
}; };
} else if (command.type === "duplicate") {
const originals = command.nodeIds.map((id) => findBlock(root, id));
const locations = command.nodeIds.map((id) => locationOf(root, id));
if (originals.some((node) => !node) || locations.some((location) => !location))
return { ok: false, errors: [{ code: "command.missing", message: "A duplicated block does not exist.", severity: "error" }] };
const countNodes = (node: BlockNode): number =>
1 + Object.values(node.slots).flat().reduce((total, child) => total + countNodes(child), 0);
const requiredIds = (originals as BlockNode[]).reduce(
(total, node) => total + countNodes(node),
0,
);
if (command.ids.length !== requiredIds || new Set(command.ids).size !== command.ids.length)
return { ok: false, errors: [{ code: "command.ids", message: "Duplicate requires one unique id for every copied node.", severity: "error" }] };
const copies = duplicateNodes(originals as BlockNode[], (() => {
let index = 0;
return () => command.ids[index++]!;
})());
const commands = copies.map((node, index) => ({
type: "insert" as const,
location: { ...locations[index]!, index: locations[index]!.index + 1 },
nodes: [node],
}));
return applyCommand(document, { type: "batch", commands }, kind, registry);
} else if (command.type === "updateProps") { } else if (command.type === "updateProps") {
const previous = findBlock(root, command.nodeId); const previous = findBlock(root, command.nodeId);
if (!previous) if (!previous)
+32
View File
@@ -137,4 +137,36 @@ describe("dynamic blocks engine", () => {
expect(decoded.ok && decoded.value.roots[0]?.id).not.toBe(node.id); expect(decoded.ok && decoded.value.roots[0]?.id).not.toBe(node.id);
expect(duplicateNodes([node], id)[0]?.props).toEqual(node.props); expect(duplicateNodes([node], id)[0]?.props).toEqual(node.props);
}); });
test("moves forward within one slot without an off-by-one error", () => {
const document = doc();
document.root.slots.content = ["a", "b", "c"].map((nodeId) => ({
...text.create({ id: () => nodeId }),
props: { text: nodeId },
}));
const moved = applyCommand(document, {
type: "move",
intent: {
nodeIds: ["a"],
from: [{ parentId: "root", slot: "content", index: 0 }],
to: { parentId: "root", slot: "content", index: 3 },
},
}, kind, registry);
expect(moved.ok && moved.value.document.root.slots.content.map((node) => node.id)).toEqual([
"b", "c", "a",
]);
});
test("duplicates blocks with deterministic injected ids", () => {
const document = doc();
document.root.slots.content = [{ ...text.create({ id: () => "a" }), props: { text: "A" } }];
const duplicated = applyCommand(document, {
type: "duplicate",
nodeIds: ["a"],
ids: ["a-copy"],
}, kind, registry);
expect(duplicated.ok && duplicated.value.document.root.slots.content.map((node) => node.id)).toEqual([
"a", "a-copy",
]);
});
}); });