fix(block-editor): restore DOM order after rejected moves
This commit is contained in:
1 parent
4ac31d750e
commit
6b35f522b9
3 files changed
+176
-9
No files matched your search
@@ -16,7 +16,11 @@ export interface MountEditorOptions {
|
||||
export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions): MountedEditor {
|
||||
let manager: DndManager | undefined;
|
||||
let cleanups: Array<() => void> = [];
|
||||
const live = document.createElement("div");
|
||||
let destroyed = false;
|
||||
let generation = 0;
|
||||
const owner = root.ownerDocument;
|
||||
const view = owner.defaultView!;
|
||||
const live = owner.createElement("div");
|
||||
live.setAttribute("role", "status");
|
||||
live.setAttribute("aria-live", "polite");
|
||||
live.setAttribute("aria-atomic", "true");
|
||||
@@ -24,21 +28,65 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions)
|
||||
"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";
|
||||
root.append(live);
|
||||
const refresh = () => {
|
||||
if (destroyed) return;
|
||||
const currentGeneration = ++generation;
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
cleanups = [];
|
||||
manager?.destroy();
|
||||
let originalChildren = new Map<Node, Node[]>();
|
||||
let gesture = 0;
|
||||
const groups = new Map<string, { parentId: string; slot: string }>();
|
||||
manager = createDndManager({
|
||||
direction: options.direction ?? (getComputedStyle(root).direction === "rtl" ? "rtl" : "ltr"),
|
||||
reducedMotion: matchMedia("(prefers-reduced-motion: reduce)").matches,
|
||||
direction:
|
||||
options.direction ?? (view.getComputedStyle(root).direction === "rtl" ? "rtl" : "ltr"),
|
||||
reducedMotion: view.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
||||
announce(announcement) {
|
||||
if (destroyed || generation !== currentGeneration) return;
|
||||
if (announcement.type === "pickedUp") {
|
||||
gesture++;
|
||||
originalChildren = new Map();
|
||||
for (const element of root.querySelectorAll(
|
||||
"[data-block-id][data-block-parent][data-block-slot]",
|
||||
)) {
|
||||
const parent = element.parentNode;
|
||||
if (parent && !originalChildren.has(parent))
|
||||
originalChildren.set(parent, [...parent.childNodes]);
|
||||
}
|
||||
} else if (announcement.type === "cancelled") {
|
||||
// Let the backend finish its own cancellation work before reconciliation.
|
||||
// Keep the actual nodes (and their listeners), not a stale innerHTML copy.
|
||||
const snapshot = originalChildren;
|
||||
const cancelledGesture = gesture;
|
||||
view.setTimeout(() => {
|
||||
if (
|
||||
destroyed ||
|
||||
generation !== currentGeneration ||
|
||||
gesture !== cancelledGesture ||
|
||||
!root.isConnected
|
||||
)
|
||||
return;
|
||||
for (const [parent, children] of snapshot) {
|
||||
if (parent !== root && !root.contains(parent)) continue;
|
||||
let anchor: Node | null = null;
|
||||
for (const child of [...children].reverse()) {
|
||||
// Never resurrect nodes removed by an application rerender.
|
||||
if (!root.contains(child)) continue;
|
||||
parent.insertBefore(child, anchor);
|
||||
anchor = child;
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
}, 0);
|
||||
} else if (announcement.type === "dropped") originalChildren.clear();
|
||||
live.textContent = options.announce?.(announcement) ?? announcement.type;
|
||||
},
|
||||
async onMoveIntent(intent) {
|
||||
const [parentId, slot] = String(intent.to.group).split(":", 2);
|
||||
if (!parentId || !slot) return false;
|
||||
if (destroyed || generation !== currentGeneration) return false;
|
||||
const destination = groups.get(String(intent.to.group));
|
||||
if (!destination) return false;
|
||||
return options.onMove({
|
||||
nodeIds: intent.ids.map(String),
|
||||
to: { parentId, slot, index: intent.to.index },
|
||||
to: { ...destination, index: intent.to.index },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -48,8 +96,12 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions)
|
||||
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<HTMLElement>("[data-block-id][data-block-parent][data-block-slot]"),
|
||||
...root.querySelectorAll<HTMLElement>(
|
||||
"[data-block-id][data-block-parent][data-block-slot]",
|
||||
),
|
||||
].filter(
|
||||
(candidate) =>
|
||||
candidate.dataset.blockParent === parent && candidate.dataset.blockSlot === slot,
|
||||
@@ -58,7 +110,7 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions)
|
||||
cleanups.push(
|
||||
manager.registerSortable({
|
||||
id,
|
||||
group: `${parent}:${slot}`,
|
||||
group,
|
||||
index: siblings.indexOf(element),
|
||||
element,
|
||||
handle,
|
||||
@@ -71,7 +123,11 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions)
|
||||
return {
|
||||
refresh,
|
||||
destroy() {
|
||||
if (destroyed) return;
|
||||
destroyed = true;
|
||||
generation++;
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
cleanups = [];
|
||||
manager?.destroy();
|
||||
live.remove();
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user