返回 presentation-ai
updateSiblingsForcefully.ts
根目录 / src / components / notebook / presentation / editor / dnd / utils / updateSiblingsForcefully.ts
1 import {
2 NodeApi,
3 PathApi,
4 type NodeEntry,
5 type TElement,
6 type TText,
7 } from "platejs";
8 import { type PlateEditor } from "platejs/react";
9
10 /**
11 * Components that require force full sibling updates when their siblings change
12 * These components depend on sibling indexes or count for their layout/styling
13 */
14 const COMPONENTS_REQUIRING_SIBLING_UPDATES = [
15 "pyramid-item",
16 "cycle-item",
17 "stair-item",
18 "before-after-side",
19 "compare-side",
20 "timeline-item",
21 "arrow-vertical-item",
22 "box-item",
23 "bullet",
24 "cons-item",
25 "pros-item",
26 "slope-item",
27 "connected-circle-item",
28 "circular-grid-item",
29 "snake-item",
30 ] as const;
31
32 type ComponentRequiringSiblingUpdates =
33 (typeof COMPONENTS_REQUIRING_SIBLING_UPDATES)[number];
34
35 function requiresSiblingUpdates(element: { type?: unknown } | undefined) {
36 const type = element?.type;
37
38 return (
39 typeof type === "string" &&
40 COMPONENTS_REQUIRING_SIBLING_UPDATES.includes(
41 type as ComponentRequiringSiblingUpdates,
42 )
43 );
44 }
45
46 let siblingUpdateSequence = 0;
47
48 function getNextSiblingUpdateToken(): number {
49 siblingUpdateSequence += 1;
50
51 return Date.now() + siblingUpdateSequence;
52 }
53
54 function getSiblingUpdateToken(): number {
55 return getNextSiblingUpdateToken();
56 }
57
58 /**
59 * Forces all sibling nodes under the same parent to re-render by touching
60 * a `lastUpdate` property on each sibling node. Useful when UI depends on
61 * sibling indexes or count (e.g., alternating layouts).
62 *
63 * Always reads the fresh parent from the editor to avoid stale children counts.
64 */
65 export function updateSiblingsForcefully(
66 editor: PlateEditor,
67 _parentElement: NodeEntry<TElement | TText>[0] | null,
68 parentPath: number[],
69 ) {
70 // Always read the fresh parent from the editor to avoid stale children counts
71 const freshParent = NodeApi.get(editor, parentPath) as TElement | undefined;
72
73 if (!freshParent?.children || freshParent.children.length === 0) {
74 return;
75 }
76
77 const childCount = freshParent.children.length;
78 const updateTimestamp = getNextSiblingUpdateToken();
79
80 try {
81 editor.tf.withoutNormalizing(() => {
82 for (let childIndex = 0; childIndex < childCount; childIndex++) {
83 const siblingPath = [...parentPath, childIndex];
84 try {
85 editor.tf.setNodes(
86 { lastUpdate: updateTimestamp },
87 { at: siblingPath },
88 );
89 } catch {
90 // ignore errors for siblings that might be mid-edit
91 }
92 }
93 });
94 } catch {
95 // ignore
96 }
97 }
98
99 /**
100 * Updates siblings for all components that require it after a drop operation.
101 * The dropped element's type determines whether sibling updates are needed.
102 * When they are, ALL siblings under the same parent get a fresh `lastUpdate` token.
103 */
104 export function updateSiblingsAfterDrop(
105 editor: PlateEditor,
106 droppedElement: { type: string; id?: string },
107 dropPath: number[],
108 ) {
109 if (!requiresSiblingUpdates(droppedElement)) return;
110
111 // Get the parent path and read the fresh parent from the editor
112 const parentPath = PathApi.parent(dropPath);
113 const freshParent = NodeApi.get(editor, parentPath) as TElement | undefined;
114
115 if (!freshParent) return;
116
117 updateSiblingsForcefully(editor, freshParent, parentPath);
118 }
119
120 export function updateDroppedElementAfterDrop(
121 editor: PlateEditor,
122 dropPath: number[],
123 ) {
124 const droppedEntry = editor.api.node({ at: dropPath }) as
125 | NodeEntry<TElement>
126 | undefined;
127
128 if (!droppedEntry) return;
129
130 editor.tf.setNodes({ lastUpdate: getSiblingUpdateToken() }, { at: dropPath });
131 updateSiblingsAfterDrop(editor, droppedEntry[0], droppedEntry[1]);
132 }
133
134 export function updateSiblingsAfterDropById(
135 editor: PlateEditor,
136 droppedElementId: string,
137 ) {
138 const droppedEntry = editor.api.node({
139 id: droppedElementId,
140 at: [],
141 }) as NodeEntry<TElement> | undefined;
142
143 if (!droppedEntry) return;
144
145 updateSiblingsAfterDrop(editor, droppedEntry[0], droppedEntry[1]);
146 }
147
147 lines TYPESCRIPT