elementdrawing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/elementdrawing.min.js +3 -0
- package/dist/elementdrawing.min.js.LICENSE.txt +8 -0
- package/dist/elementdrawing.min.js.map +1 -0
- package/dist/index.html +1 -0
- package/package.json +127 -0
- package/src/core/bridge.h +855 -0
- package/src/core/diff.c +900 -0
- package/src/core/element.c +1078 -0
- package/src/core/event.c +813 -0
- package/src/core/fiber.c +1027 -0
- package/src/core/hooks.c +919 -0
- package/src/core/renderer.c +963 -0
- package/src/core/scheduler.c +702 -0
- package/src/core/state.c +803 -0
- package/src/css/animations.css +779 -0
- package/src/css/base.css +615 -0
- package/src/css/components.css +1311 -0
- package/src/css/tailwind.css +370 -0
- package/src/css/themes.css +517 -0
- package/src/css/utilities.css +475 -0
- package/src/index.js +746 -0
- package/src/js/animation.js +655 -0
- package/src/js/dom.js +665 -0
- package/src/js/events.js +585 -0
- package/src/js/http.js +446 -0
- package/src/js/index.js +26 -0
- package/src/js/router.js +483 -0
- package/src/js/store.js +539 -0
- package/src/js/utils.js +593 -0
- package/src/js/validator.js +529 -0
- package/src/jsx/components/Accordion.jsx +210 -0
- package/src/jsx/components/Alert.jsx +169 -0
- package/src/jsx/components/Avatar.jsx +214 -0
- package/src/jsx/components/Badge.jsx +136 -0
- package/src/jsx/components/Breadcrumb.jsx +200 -0
- package/src/jsx/components/Button.jsx +188 -0
- package/src/jsx/components/Card.jsx +192 -0
- package/src/jsx/components/Carousel.jsx +278 -0
- package/src/jsx/components/Checkbox.jsx +215 -0
- package/src/jsx/components/Dialog.jsx +242 -0
- package/src/jsx/components/Drawer.jsx +190 -0
- package/src/jsx/components/Dropdown.jsx +268 -0
- package/src/jsx/components/Form.jsx +274 -0
- package/src/jsx/components/Input.jsx +285 -0
- package/src/jsx/components/Menu.jsx +276 -0
- package/src/jsx/components/Modal.jsx +274 -0
- package/src/jsx/components/Navbar.jsx +292 -0
- package/src/jsx/components/Pagination.jsx +268 -0
- package/src/jsx/components/Progress.jsx +252 -0
- package/src/jsx/components/Radio.jsx +208 -0
- package/src/jsx/components/Select.jsx +397 -0
- package/src/jsx/components/Sidebar.jsx +250 -0
- package/src/jsx/components/Slider.jsx +310 -0
- package/src/jsx/components/Spinner.jsx +198 -0
- package/src/jsx/components/Switch.jsx +201 -0
- package/src/jsx/components/Table.jsx +332 -0
- package/src/jsx/components/Tabs.jsx +227 -0
- package/src/jsx/components/Textarea.jsx +212 -0
- package/src/jsx/components/Toast.jsx +270 -0
- package/src/jsx/components/Tooltip.jsx +178 -0
- package/src/jsx/components/Typography.jsx +299 -0
- package/src/jsx/components/index.jsx +70 -0
- package/src/jsx/core/element.js +3 -0
- package/src/jsx/hooks/index.js +356 -0
- package/src/jsx/hooks/useCallback.js +472 -0
- package/src/jsx/hooks/useContext.js +586 -0
- package/src/jsx/hooks/useEffect.js +704 -0
- package/src/jsx/hooks/useLayoutEffect.js +508 -0
- package/src/jsx/hooks/useMemo.js +689 -0
- package/src/jsx/hooks/useReducer.js +729 -0
- package/src/jsx/hooks/useRef.js +542 -0
- package/src/jsx/hooks/useState.js +854 -0
- package/src/jsx/runtime/commit.js +903 -0
- package/src/jsx/runtime/createElement.js +860 -0
- package/src/jsx/runtime/index.js +356 -0
- package/src/jsx/runtime/reconcile.js +687 -0
- package/src/jsx/runtime/render.js +914 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commit - Commit Phase Operations
|
|
3
|
+
* ElementDrawing Framework - Applies reconciliation results to the DOM with
|
|
4
|
+
* insertions, deletions, updates, move operations, effect execution,
|
|
5
|
+
* ref management, error handling, snapshots, batch commit, and
|
|
6
|
+
* priority-based commit scheduling.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { INSERT, MOVE, REMOVE, UPDATE, REPLACE, SKIP, REORDER } = require('./reconcile');
|
|
12
|
+
|
|
13
|
+
// ─── Commit Priority ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const COMMIT_PRIORITY = {
|
|
16
|
+
IMMEDIATE: 0,
|
|
17
|
+
NORMAL: 1,
|
|
18
|
+
LOW: 2,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ─── Internal State ───────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
let isCommitting = false;
|
|
24
|
+
const commitQueue = [];
|
|
25
|
+
const pendingLayoutEffects = [];
|
|
26
|
+
const pendingPassiveEffects = [];
|
|
27
|
+
const pendingInsertionEffects = [];
|
|
28
|
+
const pendingRefCleanups = [];
|
|
29
|
+
const pendingRefAssignments = [];
|
|
30
|
+
const errorRecoveryStack = [];
|
|
31
|
+
let snapshotBeforeMutations = null;
|
|
32
|
+
let currentCommitPriority = COMMIT_PRIORITY.NORMAL;
|
|
33
|
+
|
|
34
|
+
// ─── Commit Statistics ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
let commitStats = {
|
|
37
|
+
totalCommits: 0,
|
|
38
|
+
totalOperations: 0,
|
|
39
|
+
totalInsertions: 0,
|
|
40
|
+
totalDeletions: 0,
|
|
41
|
+
totalUpdates: 0,
|
|
42
|
+
totalReplacements: 0,
|
|
43
|
+
totalMoves: 0,
|
|
44
|
+
lastCommitTime: 0,
|
|
45
|
+
lastCommitDuration: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get commit statistics.
|
|
50
|
+
* @returns {Object}
|
|
51
|
+
*/
|
|
52
|
+
function getCommitStats() {
|
|
53
|
+
return Object.assign({}, commitStats);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reset commit statistics.
|
|
58
|
+
*/
|
|
59
|
+
function resetCommitStats() {
|
|
60
|
+
commitStats = {
|
|
61
|
+
totalCommits: 0,
|
|
62
|
+
totalOperations: 0,
|
|
63
|
+
totalInsertions: 0,
|
|
64
|
+
totalDeletions: 0,
|
|
65
|
+
totalUpdates: 0,
|
|
66
|
+
totalReplacements: 0,
|
|
67
|
+
totalMoves: 0,
|
|
68
|
+
lastCommitTime: 0,
|
|
69
|
+
lastCommitDuration: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Commit Phase Entry ───────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Commit reconciliation results to the DOM.
|
|
77
|
+
* Processes all diff operations in the correct order:
|
|
78
|
+
* 1. Snapshot before mutations
|
|
79
|
+
* 2. DOM deletions
|
|
80
|
+
* 3. DOM insertions
|
|
81
|
+
* 4. DOM updates
|
|
82
|
+
* 5. DOM moves
|
|
83
|
+
* 6. DOM replacements
|
|
84
|
+
* 7. Layout effects
|
|
85
|
+
* 8. Ref cleanups and assignments
|
|
86
|
+
* 9. Passive effects
|
|
87
|
+
*
|
|
88
|
+
* @param {Array} operations - Diff operations from reconciliation
|
|
89
|
+
* @param {HTMLElement} container - Root DOM container
|
|
90
|
+
* @param {Object} [options] - Commit options
|
|
91
|
+
* @param {boolean} [options.batchCommit=false]
|
|
92
|
+
* @param {Function} [options.onComplete]
|
|
93
|
+
* @param {number} [options.priority]
|
|
94
|
+
* @returns {boolean} True if commit succeeded
|
|
95
|
+
*/
|
|
96
|
+
function commitWork(operations, container, options = {}) {
|
|
97
|
+
if (isCommitting && !options.batchCommit) {
|
|
98
|
+
commitQueue.push({ operations, container, options });
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const commitStart = Date.now();
|
|
103
|
+
isCommitting = true;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// ─── Phase 1: Snapshot before mutations ──────────────────────────────
|
|
107
|
+
if (operations.some((op) => op.type === REMOVE || op.type === REPLACE)) {
|
|
108
|
+
snapshotBeforeMutations = captureSnapshot(container);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Phase 2: Process deletions ──────────────────────────────────────
|
|
112
|
+
const deletions = operations.filter((op) => op.type === REMOVE);
|
|
113
|
+
processDeletions(deletions, container);
|
|
114
|
+
commitStats.totalDeletions += deletions.length;
|
|
115
|
+
|
|
116
|
+
// ─── Phase 3: Process insertions ─────────────────────────────────────
|
|
117
|
+
const insertions = operations.filter((op) => op.type === INSERT);
|
|
118
|
+
processInsertions(insertions, container);
|
|
119
|
+
commitStats.totalInsertions += insertions.length;
|
|
120
|
+
|
|
121
|
+
// ─── Phase 4: Process updates ────────────────────────────────────────
|
|
122
|
+
const updates = operations.filter((op) => op.type === UPDATE);
|
|
123
|
+
processUpdates(updates, container);
|
|
124
|
+
commitStats.totalUpdates += updates.length;
|
|
125
|
+
|
|
126
|
+
// ─── Phase 5: Process moves ──────────────────────────────────────────
|
|
127
|
+
const moves = operations.filter((op) => op.type === MOVE);
|
|
128
|
+
processMoves(moves, container);
|
|
129
|
+
commitStats.totalMoves += moves.length;
|
|
130
|
+
|
|
131
|
+
// ─── Phase 6: Process replacements ───────────────────────────────────
|
|
132
|
+
const replacements = operations.filter((op) => op.type === REPLACE);
|
|
133
|
+
processReplacements(replacements, container);
|
|
134
|
+
commitStats.totalReplacements += replacements.length;
|
|
135
|
+
|
|
136
|
+
// ─── Phase 7: Execute insertion effects ──────────────────────────────
|
|
137
|
+
flushPendingInsertionEffects();
|
|
138
|
+
|
|
139
|
+
// ─── Phase 8: Execute layout effects ─────────────────────────────────
|
|
140
|
+
flushPendingLayoutEffects();
|
|
141
|
+
|
|
142
|
+
// ─── Phase 9: Ref cleanups and assignments ───────────────────────────
|
|
143
|
+
processRefCleanups();
|
|
144
|
+
processRefAssignments();
|
|
145
|
+
|
|
146
|
+
// ─── Phase 10: Schedule passive effects ──────────────────────────────
|
|
147
|
+
schedulePassiveEffects();
|
|
148
|
+
|
|
149
|
+
// ─── Update stats ────────────────────────────────────────────────────
|
|
150
|
+
commitStats.totalCommits++;
|
|
151
|
+
commitStats.totalOperations += operations.length;
|
|
152
|
+
commitStats.lastCommitTime = commitStart;
|
|
153
|
+
commitStats.lastCommitDuration = Date.now() - commitStart;
|
|
154
|
+
|
|
155
|
+
// ─── Phase 11: Notify completion ──────────────────────────────────────
|
|
156
|
+
if (typeof options.onComplete === 'function') {
|
|
157
|
+
options.onComplete();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('[commit] Commit phase error:', error);
|
|
163
|
+
handleErrorDuringCommit(error, container);
|
|
164
|
+
return false;
|
|
165
|
+
} finally {
|
|
166
|
+
isCommitting = false;
|
|
167
|
+
|
|
168
|
+
// Process queued commits
|
|
169
|
+
if (commitQueue.length > 0) {
|
|
170
|
+
const next = commitQueue.shift();
|
|
171
|
+
commitWork(next.operations, next.container, next.options);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── DOM Insertions ───────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Process INSERT operations from the diff.
|
|
180
|
+
*/
|
|
181
|
+
function processInsertions(operations, container) {
|
|
182
|
+
operations.forEach((op) => {
|
|
183
|
+
if (op.type !== INSERT) return;
|
|
184
|
+
|
|
185
|
+
const { vnode, parentVNode, index } = op.payload;
|
|
186
|
+
if (!vnode) return;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const domNode = createDOMNodeFromVNode(vnode);
|
|
190
|
+
if (!domNode) return;
|
|
191
|
+
|
|
192
|
+
// Find the parent DOM node
|
|
193
|
+
const parentDOMNode = findParentDOMNode(container, parentVNode);
|
|
194
|
+
if (!parentDOMNode) {
|
|
195
|
+
container.appendChild(domNode);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Insert at the correct position
|
|
200
|
+
if (index !== undefined && index !== null) {
|
|
201
|
+
const referenceNode = parentDOMNode.childNodes[index];
|
|
202
|
+
if (referenceNode) {
|
|
203
|
+
parentDOMNode.insertBefore(domNode, referenceNode);
|
|
204
|
+
} else {
|
|
205
|
+
parentDOMNode.appendChild(domNode);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
parentDOMNode.appendChild(domNode);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Queue layout effects for the inserted node
|
|
212
|
+
queueLayoutEffects(vnode, 'mount');
|
|
213
|
+
|
|
214
|
+
// Queue ref assignments
|
|
215
|
+
if (vnode.ref) {
|
|
216
|
+
pendingRefAssignments.push({ ref: vnode.ref, domNode });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Recursively insert children
|
|
220
|
+
if (vnode.props && vnode.props.children) {
|
|
221
|
+
insertChildren(vnode, domNode);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('[commit] Insertion error:', error);
|
|
225
|
+
handleErrorDuringCommit(error, container);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Recursively insert children of a vnode.
|
|
232
|
+
*/
|
|
233
|
+
function insertChildren(vnode, parentDOMNode) {
|
|
234
|
+
if (!vnode.props || !vnode.props.children) return;
|
|
235
|
+
|
|
236
|
+
const children = Array.isArray(vnode.props.children)
|
|
237
|
+
? vnode.props.children
|
|
238
|
+
: [vnode.props.children];
|
|
239
|
+
|
|
240
|
+
children.forEach((child) => {
|
|
241
|
+
if (!child) return;
|
|
242
|
+
|
|
243
|
+
const childDOMNode = createDOMNodeFromVNode(child);
|
|
244
|
+
if (childDOMNode) {
|
|
245
|
+
parentDOMNode.appendChild(childDOMNode);
|
|
246
|
+
|
|
247
|
+
if (child.ref) {
|
|
248
|
+
pendingRefAssignments.push({ ref: child.ref, domNode: childDOMNode });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
insertChildren(child, childDOMNode);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── DOM Deletions ────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Process REMOVE operations from the diff.
|
|
260
|
+
*/
|
|
261
|
+
function processDeletions(operations, container) {
|
|
262
|
+
operations.forEach((op) => {
|
|
263
|
+
if (op.type !== REMOVE) return;
|
|
264
|
+
|
|
265
|
+
const { vnode, parentVNode } = op.payload;
|
|
266
|
+
if (!vnode) return;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const domNode = findDOMNodeForVNode(container, vnode);
|
|
270
|
+
if (!domNode) return;
|
|
271
|
+
|
|
272
|
+
// Queue ref cleanups before removal
|
|
273
|
+
if (vnode.ref) {
|
|
274
|
+
pendingRefCleanups.push({ ref: vnode.ref });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Queue unmount layout effects
|
|
278
|
+
queueLayoutEffects(vnode, 'unmount');
|
|
279
|
+
|
|
280
|
+
// Queue unmount passive effects
|
|
281
|
+
queuePassiveEffects(vnode, 'unmount');
|
|
282
|
+
|
|
283
|
+
// Recursively clean up children
|
|
284
|
+
cleanupChildVNodes(vnode);
|
|
285
|
+
|
|
286
|
+
// Remove the DOM node
|
|
287
|
+
if (domNode.parentNode) {
|
|
288
|
+
domNode.parentNode.removeChild(domNode);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Clean up stored vnode reference
|
|
292
|
+
if (domNode.__ed_vnode) {
|
|
293
|
+
delete domNode.__ed_vnode;
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error('[commit] Deletion error:', error);
|
|
297
|
+
handleErrorDuringCommit(error, container);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Recursively clean up child vnodes during deletion.
|
|
304
|
+
*/
|
|
305
|
+
function cleanupChildVNodes(vnode) {
|
|
306
|
+
if (!vnode.props || !vnode.props.children) return;
|
|
307
|
+
|
|
308
|
+
const children = Array.isArray(vnode.props.children)
|
|
309
|
+
? vnode.props.children
|
|
310
|
+
: [vnode.props.children];
|
|
311
|
+
|
|
312
|
+
children.forEach((child) => {
|
|
313
|
+
if (!child || typeof child !== 'object') return;
|
|
314
|
+
|
|
315
|
+
if (child.ref) {
|
|
316
|
+
pendingRefCleanups.push({ ref: child.ref });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
queueLayoutEffects(child, 'unmount');
|
|
320
|
+
queuePassiveEffects(child, 'unmount');
|
|
321
|
+
cleanupChildVNodes(child);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── DOM Updates ──────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Process UPDATE operations from the diff.
|
|
329
|
+
*/
|
|
330
|
+
function processUpdates(operations, container) {
|
|
331
|
+
operations.forEach((op) => {
|
|
332
|
+
if (op.type !== UPDATE) return;
|
|
333
|
+
|
|
334
|
+
const { vnode, changes, changeType } = op.payload;
|
|
335
|
+
if (!vnode) return;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const domNode = findDOMNodeForVNode(container, vnode);
|
|
339
|
+
if (!domNode) return;
|
|
340
|
+
|
|
341
|
+
switch (changeType) {
|
|
342
|
+
case 'props':
|
|
343
|
+
applyPropChanges(domNode, changes);
|
|
344
|
+
break;
|
|
345
|
+
case 'style':
|
|
346
|
+
applyStyleChanges(domNode, changes);
|
|
347
|
+
break;
|
|
348
|
+
case 'events':
|
|
349
|
+
applyEventChanges(domNode, changes);
|
|
350
|
+
break;
|
|
351
|
+
case 'ref':
|
|
352
|
+
applyRefChanges(domNode, changes);
|
|
353
|
+
break;
|
|
354
|
+
case 'text':
|
|
355
|
+
applyTextChanges(domNode, changes);
|
|
356
|
+
break;
|
|
357
|
+
case 'innerHTML':
|
|
358
|
+
applyInnerHTMLChanges(domNode, changes);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Queue update layout effects
|
|
363
|
+
queueLayoutEffects(vnode, 'update');
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('[commit] Update error:', error);
|
|
366
|
+
handleErrorDuringCommit(error, container);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Apply prop changes to a DOM node.
|
|
373
|
+
*/
|
|
374
|
+
function applyPropChanges(domNode, changes) {
|
|
375
|
+
changes.forEach((change) => {
|
|
376
|
+
switch (change.type) {
|
|
377
|
+
case 'set':
|
|
378
|
+
if (change.key === 'className') {
|
|
379
|
+
domNode.className = change.value || '';
|
|
380
|
+
} else if (change.key === 'dangerouslySetInnerHTML') {
|
|
381
|
+
if (change.value && change.value.__html) {
|
|
382
|
+
domNode.innerHTML = change.value.__html;
|
|
383
|
+
}
|
|
384
|
+
} else if (change.key === 'value') {
|
|
385
|
+
domNode.value = change.value;
|
|
386
|
+
} else if (change.key === 'checked') {
|
|
387
|
+
domNode.checked = change.value;
|
|
388
|
+
} else if (typeof change.value === 'boolean') {
|
|
389
|
+
if (change.value) {
|
|
390
|
+
domNode.setAttribute(change.key, '');
|
|
391
|
+
} else {
|
|
392
|
+
domNode.removeAttribute(change.key);
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
domNode.setAttribute(change.key, change.value);
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
case 'remove':
|
|
400
|
+
if (change.key === 'className') {
|
|
401
|
+
domNode.className = '';
|
|
402
|
+
} else if (change.key === 'value') {
|
|
403
|
+
domNode.value = '';
|
|
404
|
+
} else if (change.key === 'checked') {
|
|
405
|
+
domNode.checked = false;
|
|
406
|
+
} else if (change.key === 'dangerouslySetInnerHTML') {
|
|
407
|
+
domNode.innerHTML = '';
|
|
408
|
+
} else {
|
|
409
|
+
domNode.removeAttribute(change.key);
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Apply style changes to a DOM node.
|
|
418
|
+
*/
|
|
419
|
+
function applyStyleChanges(domNode, changes) {
|
|
420
|
+
changes.forEach((change) => {
|
|
421
|
+
switch (change.type) {
|
|
422
|
+
case 'replace':
|
|
423
|
+
domNode.style.cssText = change.value;
|
|
424
|
+
break;
|
|
425
|
+
case 'set':
|
|
426
|
+
domNode.style.setProperty(change.key, change.value);
|
|
427
|
+
break;
|
|
428
|
+
case 'remove':
|
|
429
|
+
domNode.style.removeProperty(change.key);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Apply event handler changes.
|
|
437
|
+
*/
|
|
438
|
+
function applyEventChanges(domNode, changes) {
|
|
439
|
+
changes.forEach((change) => {
|
|
440
|
+
const eventType = change.key.slice(2).toLowerCase();
|
|
441
|
+
const isCapture = change.key.endsWith('Capture');
|
|
442
|
+
const handlerKey = isCapture ? '__ed_capture_' + eventType : '__ed_handler_' + eventType;
|
|
443
|
+
|
|
444
|
+
switch (change.type) {
|
|
445
|
+
case 'set':
|
|
446
|
+
if (domNode[handlerKey]) {
|
|
447
|
+
domNode.removeEventListener(eventType, domNode[handlerKey], isCapture);
|
|
448
|
+
}
|
|
449
|
+
if (typeof change.value === 'function') {
|
|
450
|
+
domNode[handlerKey] = change.value;
|
|
451
|
+
domNode.addEventListener(eventType, change.value, isCapture);
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case 'remove':
|
|
456
|
+
if (domNode[handlerKey]) {
|
|
457
|
+
domNode.removeEventListener(eventType, domNode[handlerKey], isCapture);
|
|
458
|
+
domNode[handlerKey] = null;
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Apply ref changes.
|
|
467
|
+
*/
|
|
468
|
+
function applyRefChanges(domNode, changes) {
|
|
469
|
+
changes.forEach((change) => {
|
|
470
|
+
if (change.oldRef) {
|
|
471
|
+
if (typeof change.oldRef === 'function') {
|
|
472
|
+
change.oldRef(null);
|
|
473
|
+
} else if (typeof change.oldRef === 'object') {
|
|
474
|
+
change.oldRef.current = null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (change.newRef) {
|
|
479
|
+
if (typeof change.newRef === 'function') {
|
|
480
|
+
change.newRef(domNode);
|
|
481
|
+
} else if (typeof change.newRef === 'object') {
|
|
482
|
+
change.newRef.current = domNode;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Apply text content changes.
|
|
490
|
+
*/
|
|
491
|
+
function applyTextChanges(domNode, changes) {
|
|
492
|
+
changes.forEach((change) => {
|
|
493
|
+
if (change.key === 'nodeValue' && domNode.nodeType === 3) {
|
|
494
|
+
domNode.textContent = change.value;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Apply innerHTML changes.
|
|
501
|
+
*/
|
|
502
|
+
function applyInnerHTMLChanges(domNode, changes) {
|
|
503
|
+
changes.forEach((change) => {
|
|
504
|
+
if (change.type === 'set' && change.value) {
|
|
505
|
+
domNode.innerHTML = change.value.__html || '';
|
|
506
|
+
} else if (change.type === 'remove') {
|
|
507
|
+
domNode.innerHTML = '';
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ─── DOM Moves ────────────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Process MOVE operations (child reordering).
|
|
516
|
+
*/
|
|
517
|
+
function processMoves(operations, container) {
|
|
518
|
+
operations.forEach((op) => {
|
|
519
|
+
if (op.type !== MOVE) return;
|
|
520
|
+
|
|
521
|
+
const { vnode, fromIndex, toIndex } = op.payload;
|
|
522
|
+
if (!vnode) return;
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const domNode = findDOMNodeForVNode(container, vnode);
|
|
526
|
+
if (!domNode || !domNode.parentNode) return;
|
|
527
|
+
|
|
528
|
+
const parent = domNode.parentNode;
|
|
529
|
+
|
|
530
|
+
// Remove from old position
|
|
531
|
+
parent.removeChild(domNode);
|
|
532
|
+
|
|
533
|
+
// Insert at new position
|
|
534
|
+
const referenceNode = parent.childNodes[toIndex];
|
|
535
|
+
if (referenceNode) {
|
|
536
|
+
parent.insertBefore(domNode, referenceNode);
|
|
537
|
+
} else {
|
|
538
|
+
parent.appendChild(domNode);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Queue update effects
|
|
542
|
+
queueLayoutEffects(vnode, 'update');
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.error('[commit] Move error:', error);
|
|
545
|
+
handleErrorDuringCommit(error, container);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ─── DOM Replacements ─────────────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Process REPLACE operations from the diff.
|
|
554
|
+
*/
|
|
555
|
+
function processReplacements(operations, container) {
|
|
556
|
+
operations.forEach((op) => {
|
|
557
|
+
if (op.type !== REPLACE) return;
|
|
558
|
+
|
|
559
|
+
const { oldVNode, newVNode } = op.payload;
|
|
560
|
+
if (!oldVNode || !newVNode) return;
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const oldDOMNode = findDOMNodeForVNode(container, oldVNode);
|
|
564
|
+
if (!oldDOMNode) return;
|
|
565
|
+
|
|
566
|
+
// Clean up old ref
|
|
567
|
+
if (oldVNode.ref) {
|
|
568
|
+
pendingRefCleanups.push({ ref: oldVNode.ref });
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Queue unmount effects for old node
|
|
572
|
+
queueLayoutEffects(oldVNode, 'unmount');
|
|
573
|
+
queuePassiveEffects(oldVNode, 'unmount');
|
|
574
|
+
cleanupChildVNodes(oldVNode);
|
|
575
|
+
|
|
576
|
+
// Create new DOM node
|
|
577
|
+
const newDOMNode = createDOMNodeFromVNode(newVNode);
|
|
578
|
+
if (!newDOMNode) return;
|
|
579
|
+
|
|
580
|
+
// Replace in DOM
|
|
581
|
+
if (oldDOMNode.parentNode) {
|
|
582
|
+
oldDOMNode.parentNode.replaceChild(newDOMNode, oldDOMNode);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Queue mount effects for new node
|
|
586
|
+
queueLayoutEffects(newVNode, 'mount');
|
|
587
|
+
queuePassiveEffects(newVNode, 'mount');
|
|
588
|
+
|
|
589
|
+
// Assign new ref
|
|
590
|
+
if (newVNode.ref) {
|
|
591
|
+
pendingRefAssignments.push({ ref: newVNode.ref, domNode: newDOMNode });
|
|
592
|
+
}
|
|
593
|
+
} catch (error) {
|
|
594
|
+
console.error('[commit] Replacement error:', error);
|
|
595
|
+
handleErrorDuringCommit(error, container);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ─── Effect Execution ─────────────────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Queue a layout effect for execution.
|
|
604
|
+
*/
|
|
605
|
+
function queueLayoutEffects(vnode, phase) {
|
|
606
|
+
if (!vnode || !vnode._hooks) return;
|
|
607
|
+
|
|
608
|
+
vnode._hooks.forEach((hook) => {
|
|
609
|
+
if (hook && (hook.type === 'layout-effect' || hook.type === 'insertion-effect')) {
|
|
610
|
+
pendingLayoutEffects.push({ hook, phase });
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Queue a passive effect for execution.
|
|
617
|
+
*/
|
|
618
|
+
function queuePassiveEffects(vnode, phase) {
|
|
619
|
+
if (!vnode || !vnode._hooks) return;
|
|
620
|
+
|
|
621
|
+
vnode._hooks.forEach((hook) => {
|
|
622
|
+
if (hook && hook.type === 'passive-effect') {
|
|
623
|
+
pendingPassiveEffects.push({ hook, phase });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Flush pending insertion effects (synchronous, before layout).
|
|
630
|
+
*/
|
|
631
|
+
function flushPendingInsertionEffects() {
|
|
632
|
+
while (pendingInsertionEffects.length > 0) {
|
|
633
|
+
const { hook, phase } = pendingInsertionEffects.shift();
|
|
634
|
+
try {
|
|
635
|
+
if (phase === 'unmount' && typeof hook.cleanup === 'function') {
|
|
636
|
+
hook.cleanup();
|
|
637
|
+
hook.cleanup = null;
|
|
638
|
+
} else if (hook._shouldRun !== false) {
|
|
639
|
+
const cleanup = hook.create();
|
|
640
|
+
if (typeof cleanup === 'function') {
|
|
641
|
+
hook.cleanup = cleanup;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error('[commit] Insertion effect error:', error);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Flush pending layout effects (synchronous).
|
|
652
|
+
*/
|
|
653
|
+
function flushPendingLayoutEffects() {
|
|
654
|
+
while (pendingLayoutEffects.length > 0) {
|
|
655
|
+
const { hook, phase } = pendingLayoutEffects.shift();
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
if (phase === 'unmount' && typeof hook.cleanup === 'function') {
|
|
659
|
+
hook.cleanup();
|
|
660
|
+
hook.cleanup = null;
|
|
661
|
+
} else if ((phase === 'mount' || phase === 'update') && hook._shouldRun !== false) {
|
|
662
|
+
const cleanup = hook.create();
|
|
663
|
+
if (typeof cleanup === 'function') {
|
|
664
|
+
hook.cleanup = cleanup;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error('[commit] Layout effect error:', error);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Schedule passive effects for async execution after paint.
|
|
675
|
+
*/
|
|
676
|
+
function schedulePassiveEffects() {
|
|
677
|
+
if (pendingPassiveEffects.length === 0) return;
|
|
678
|
+
|
|
679
|
+
const effects = pendingPassiveEffects.splice(0);
|
|
680
|
+
requestAnimationFrame(() => {
|
|
681
|
+
setTimeout(() => {
|
|
682
|
+
effects.forEach(({ hook, phase }) => {
|
|
683
|
+
try {
|
|
684
|
+
if (phase === 'unmount' && typeof hook.cleanup === 'function') {
|
|
685
|
+
hook.cleanup();
|
|
686
|
+
hook.cleanup = null;
|
|
687
|
+
} else if ((phase === 'mount' || phase === 'update') && hook._shouldRun !== false) {
|
|
688
|
+
const cleanup = hook.create();
|
|
689
|
+
if (typeof cleanup === 'function') {
|
|
690
|
+
hook.cleanup = cleanup;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
console.error('[commit] Passive effect error:', error);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}, 0);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ─── Ref Management ───────────────────────────────────────────────────────────
|
|
702
|
+
|
|
703
|
+
function processRefCleanups() {
|
|
704
|
+
while (pendingRefCleanups.length > 0) {
|
|
705
|
+
const { ref } = pendingRefCleanups.shift();
|
|
706
|
+
try {
|
|
707
|
+
if (typeof ref === 'function') {
|
|
708
|
+
ref(null);
|
|
709
|
+
} else if (typeof ref === 'object' && ref !== null) {
|
|
710
|
+
ref.current = null;
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
console.error('[commit] Ref cleanup error:', error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function processRefAssignments() {
|
|
719
|
+
while (pendingRefAssignments.length > 0) {
|
|
720
|
+
const { ref, domNode } = pendingRefAssignments.shift();
|
|
721
|
+
try {
|
|
722
|
+
if (typeof ref === 'function') {
|
|
723
|
+
ref(domNode);
|
|
724
|
+
} else if (typeof ref === 'object' && ref !== null) {
|
|
725
|
+
ref.current = domNode;
|
|
726
|
+
}
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error('[commit] Ref assignment error:', error);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ─── Error Handling ───────────────────────────────────────────────────────────
|
|
734
|
+
|
|
735
|
+
function handleErrorDuringCommit(error, container) {
|
|
736
|
+
errorRecoveryStack.push({ error, container, timestamp: Date.now() });
|
|
737
|
+
|
|
738
|
+
if (container && container.__ed_errorBoundary) {
|
|
739
|
+
const boundary = container.__ed_errorBoundary;
|
|
740
|
+
if (typeof boundary._handleError === 'function') {
|
|
741
|
+
try {
|
|
742
|
+
boundary._handleError(error);
|
|
743
|
+
} catch (recoveryError) {
|
|
744
|
+
console.error('[commit] Error boundary also failed:', recoveryError);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ─── Snapshot ─────────────────────────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
function captureSnapshot(container) {
|
|
753
|
+
if (!container) return null;
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
return {
|
|
757
|
+
scrollTop: container.scrollTop,
|
|
758
|
+
scrollLeft: container.scrollLeft,
|
|
759
|
+
scrollHeight: container.scrollHeight,
|
|
760
|
+
scrollWidth: container.scrollWidth,
|
|
761
|
+
clientHeight: container.clientHeight,
|
|
762
|
+
clientWidth: container.clientWidth,
|
|
763
|
+
timestamp: Date.now(),
|
|
764
|
+
};
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function getSnapshot() {
|
|
771
|
+
return snapshotBeforeMutations;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ─── Batch Commit Operations ──────────────────────────────────────────────────
|
|
775
|
+
|
|
776
|
+
function batchCommitWork(callback) {
|
|
777
|
+
isCommitting = true;
|
|
778
|
+
const batchedOperations = [];
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
const originalCommitWork = commitWork;
|
|
782
|
+
const collectOps = (ops, container, opts) => {
|
|
783
|
+
batchedOperations.push({ ops, container, opts });
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
callback(collectOps);
|
|
787
|
+
|
|
788
|
+
batchedOperations.forEach(({ ops, container, opts }) => {
|
|
789
|
+
originalCommitWork(ops, container, opts);
|
|
790
|
+
});
|
|
791
|
+
} finally {
|
|
792
|
+
isCommitting = false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ─── Utility Functions ────────────────────────────────────────────────────────
|
|
797
|
+
|
|
798
|
+
function findParentDOMNode(container, parentVNode) {
|
|
799
|
+
if (!parentVNode) return container;
|
|
800
|
+
|
|
801
|
+
if (container.__ed_vnode === parentVNode) return container;
|
|
802
|
+
|
|
803
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
804
|
+
let node;
|
|
805
|
+
while ((node = walker.nextNode())) {
|
|
806
|
+
if (node.__ed_vnode === parentVNode) return node;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return container;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function findDOMNodeForVNode(container, vnode) {
|
|
813
|
+
if (!vnode || !container) return null;
|
|
814
|
+
|
|
815
|
+
// Fast path: text node lookup
|
|
816
|
+
if (vnode.type === '#text') {
|
|
817
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
818
|
+
let node;
|
|
819
|
+
while ((node = walker.nextNode())) {
|
|
820
|
+
if (node.__ed_vnode === vnode) return node;
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT);
|
|
826
|
+
let node;
|
|
827
|
+
while ((node = walker.nextNode())) {
|
|
828
|
+
if (node.__ed_vnode === vnode) return node;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function createDOMNodeFromVNode(vnode) {
|
|
835
|
+
if (!vnode) return null;
|
|
836
|
+
|
|
837
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') {
|
|
838
|
+
return document.createTextNode(String(vnode));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (vnode.type === '#text') {
|
|
842
|
+
return document.createTextNode(vnode.props.nodeValue || '');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (typeof vnode.type === 'string') {
|
|
846
|
+
const domNode = document.createElement(vnode.type);
|
|
847
|
+
|
|
848
|
+
if (vnode.props) {
|
|
849
|
+
Object.keys(vnode.props).forEach((key) => {
|
|
850
|
+
if (key === 'children') return;
|
|
851
|
+
if (key === 'dangerouslySetInnerHTML') return;
|
|
852
|
+
|
|
853
|
+
const value = vnode.props[key];
|
|
854
|
+
if (key === 'className') {
|
|
855
|
+
domNode.className = value || '';
|
|
856
|
+
} else if (key === 'style' && typeof value === 'object') {
|
|
857
|
+
Object.assign(domNode.style, value);
|
|
858
|
+
} else if (key === 'style' && typeof value === 'string') {
|
|
859
|
+
domNode.style.cssText = value;
|
|
860
|
+
} else if (value === true) {
|
|
861
|
+
domNode.setAttribute(key, '');
|
|
862
|
+
} else if (value !== false && value !== null && value !== undefined) {
|
|
863
|
+
domNode.setAttribute(key, value);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
if (vnode.props.dangerouslySetInnerHTML) {
|
|
868
|
+
domNode.innerHTML = vnode.props.dangerouslySetInnerHTML.__html || '';
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
domNode.__ed_vnode = vnode;
|
|
873
|
+
return domNode;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
880
|
+
|
|
881
|
+
module.exports = {
|
|
882
|
+
commitWork,
|
|
883
|
+
batchCommitWork,
|
|
884
|
+
processInsertions,
|
|
885
|
+
processDeletions,
|
|
886
|
+
processUpdates,
|
|
887
|
+
processMoves,
|
|
888
|
+
processReplacements,
|
|
889
|
+
flushPendingLayoutEffects,
|
|
890
|
+
flushPendingInsertionEffects,
|
|
891
|
+
schedulePassiveEffects,
|
|
892
|
+
processRefCleanups,
|
|
893
|
+
processRefAssignments,
|
|
894
|
+
handleErrorDuringCommit,
|
|
895
|
+
captureSnapshot,
|
|
896
|
+
getSnapshot,
|
|
897
|
+
findDOMNodeForVNode,
|
|
898
|
+
findParentDOMNode,
|
|
899
|
+
createDOMNodeFromVNode,
|
|
900
|
+
getCommitStats,
|
|
901
|
+
resetCommitStats,
|
|
902
|
+
COMMIT_PRIORITY,
|
|
903
|
+
};
|