solarite 0.5.2 → 0.7.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/Solarite-debug.js +2823 -1856
- package/dist/Solarite.js +2779 -1824
- package/dist/Solarite.min.js +2 -4
- package/package.json +16 -3
- package/readme.md +58 -11
- package/src/Globals.js +54 -72
- package/src/HtmlParser.js +90 -90
- package/src/MultiValueMap.js +57 -105
- package/src/NodeGroup.js +624 -470
- package/src/Path.js +224 -211
- package/src/PathToAttribValue.js +401 -261
- package/src/PathToAttribs.js +113 -80
- package/src/PathToComment.js +7 -7
- package/src/PathToComponent.js +183 -188
- package/src/PathToEvent.js +76 -64
- package/src/PathToKey.js +19 -0
- package/src/PathToNodes.js +1053 -566
- package/src/RootNodeGroup.js +120 -8
- package/src/Shell.js +570 -348
- package/src/Solarite.d.ts +134 -113
- package/src/Solarite.js +243 -286
- package/src/Template.js +195 -274
- package/src/Util.js +353 -351
- package/src/assert.js +10 -10
- package/src/assignAttributes.js +63 -0
- package/src/delve.js +55 -43
- package/src/h.js +220 -138
- package/src/jsx-dev-runtime.d.ts +1 -0
- package/src/jsx-dev-runtime.js +5 -0
- package/src/jsx-runtime.d.ts +21 -0
- package/src/jsx-runtime.js +84 -0
- package/src/jsx.js +194 -0
- package/src/toEl.js +77 -82
- package/dist/udomdiff-license.txt +0 -18
- package/src/getArg.js +0 -137
- package/src/hash.js +0 -89
- package/src/udomdiff.js +0 -176
- package/src/unused/FastLookupArray.js +0 -54
- package/src/unused/Hashes.js +0 -339
- package/src/unused/InUse.test.js +0 -92
- package/src/unused/InUseMap.js +0 -98
- package/src/unused/LinkedList.js +0 -117
- package/src/unused/LinkedList.test.js +0 -115
- package/src/unused/Misc.js +0 -13
- package/src/unused/Perf.js +0 -47
- package/src/unused/TrackedArray.js +0 -54
- package/src/unused/WeakArray.js +0 -33
- package/src/watch.js +0 -543
package/src/PathToNodes.js
CHANGED
|
@@ -1,566 +1,1053 @@
|
|
|
1
|
-
import Path from "./Path.js";
|
|
2
|
-
import assert from "./assert.js";
|
|
3
|
-
import NodeGroup from "./NodeGroup.js";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import Template from "./Template.js";
|
|
7
|
-
import Globals from "./Globals.js";
|
|
8
|
-
import MultiValueMap from "./MultiValueMap.js";
|
|
9
|
-
|
|
10
|
-
export default class PathToNodes extends Path {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
path
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let
|
|
260
|
-
let
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
for (let
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
1
|
+
import Path from "./Path.js";
|
|
2
|
+
import assert from "./assert.js";
|
|
3
|
+
import NodeGroup from "./NodeGroup.js";
|
|
4
|
+
import Shell from "./Shell.js";
|
|
5
|
+
import Util from "./Util.js";
|
|
6
|
+
import Template, {templatesSame, exprSame} from "./Template.js";
|
|
7
|
+
import Globals from "./Globals.js";
|
|
8
|
+
import MultiValueMap from "./MultiValueMap.js";
|
|
9
|
+
|
|
10
|
+
export default class PathToNodes extends Path {
|
|
11
|
+
|
|
12
|
+
/** @type {?NodeGroup[]} The NodeGroups created by this path's expression, in order.
|
|
13
|
+
* Lazily created; null when the path has only ever rendered a primitive (see textNode). */
|
|
14
|
+
nodeGroups = null;
|
|
15
|
+
|
|
16
|
+
/** @type {?Text} When the expression is a single primitive, its text node lives here
|
|
17
|
+
* with no Template or NodeGroup wrapper. Mutually exclusive with nodeGroups entries. */
|
|
18
|
+
textNode = null;
|
|
19
|
+
|
|
20
|
+
/** @type {?string} The current value of textNode. */
|
|
21
|
+
textValue = null;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Nodes that have been used during the current render().
|
|
27
|
+
* Used with getNodeGroup() and freeNodeGroups() on the generic path; the positional diff
|
|
28
|
+
* tracks in-use NodeGroups in this.nodeGroups instead.
|
|
29
|
+
* Lazily created since most paths never use it.
|
|
30
|
+
* @type {?NodeGroup[]} */
|
|
31
|
+
nodeGroupsRendered = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Nodes that were added to the web component during the last render(), but are available to be used again.
|
|
35
|
+
* Used with getNodeGroup() and freeNodeGroups(), keyed by close key.
|
|
36
|
+
* Lazily created since most paths never use it.
|
|
37
|
+
* @type {?MultiValueMap} */
|
|
38
|
+
nodeGroupsAttachedAvailable = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Nodes that were not added to the web component during the last render(), and available to be used again.
|
|
42
|
+
* Lazily created since most paths never use it.
|
|
43
|
+
* @type {?MultiValueMap} */
|
|
44
|
+
nodeGroupsDetachedAvailable = null;
|
|
45
|
+
|
|
46
|
+
constructor(nodeBefore, nodeMarker) {
|
|
47
|
+
super(nodeBefore, nodeMarker);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Insert/replace the nodes created by a single expression.
|
|
52
|
+
* Called by applyExprs()
|
|
53
|
+
* @param exprs {Expr[]} Only the first is used.
|
|
54
|
+
* @return {Node[]} New Nodes created. */
|
|
55
|
+
apply(exprs) {
|
|
56
|
+
//#IFDEV
|
|
57
|
+
assert(Array.isArray(exprs));
|
|
58
|
+
//#ENDIF
|
|
59
|
+
this.applySingle(exprs[0]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Make the DOM between nodeBefore and nodeMarker match the value of expr.
|
|
64
|
+
* This is the main entry point for rendering an expression's nodes, chosen from three strategies:
|
|
65
|
+
* 1. A primitive expr updating (or creating) a single text node is handled inline with no allocations.
|
|
66
|
+
* 2. Otherwise expr is flattened to a list of Templates, strings, and Nodes via collectItems(),
|
|
67
|
+
* then applyDiff() positionally diffs them against the previous render's NodeGroups.
|
|
68
|
+
* 3. If the items contain raw Nodes, now or on the previous render, applyGeneric() uses pooled
|
|
69
|
+
* close-key matching and reconcileNodes(), since this.nodeGroups can't track raw Nodes positionally.
|
|
70
|
+
* @param expr {Expr} */
|
|
71
|
+
applySingle(expr) {
|
|
72
|
+
|
|
73
|
+
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
74
|
+
|
|
75
|
+
// Fast path for a single primitive expression, the most common case in loops.
|
|
76
|
+
let exprType = typeof expr;
|
|
77
|
+
if ((exprType === 'string' || exprType === 'number') && !this.itemsHaveNodes) {
|
|
78
|
+
if (exprType !== 'string')
|
|
79
|
+
expr += '';
|
|
80
|
+
|
|
81
|
+
// Update the existing text node.
|
|
82
|
+
let tn = this.textNode;
|
|
83
|
+
if (tn !== null) {
|
|
84
|
+
if (this.textValue !== expr) {
|
|
85
|
+
tn.nodeValue = expr;
|
|
86
|
+
this.textValue = expr;
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let ngs = this.nodeGroups;
|
|
92
|
+
if (ngs === null || ngs.length === 0) {
|
|
93
|
+
|
|
94
|
+
// Create a bare text node in an empty path, with no Template or NodeGroup wrapper.
|
|
95
|
+
let node;
|
|
96
|
+
if (this.wholeParent) {
|
|
97
|
+
// A NodeGroup re-applied through a shared stamper (NodeGroup.applyStamp/rewriteStamp)
|
|
98
|
+
// can already hold a lone text child; update it in place. Node identity is
|
|
99
|
+
// unchanged then, so no caches need invalidation.
|
|
100
|
+
let fc = this.nodeMarker.firstChild;
|
|
101
|
+
if (fc !== null && fc.nodeType === 3 && fc === this.nodeMarker.lastChild) {
|
|
102
|
+
if (fc.nodeValue !== expr)
|
|
103
|
+
fc.nodeValue = expr;
|
|
104
|
+
this.textNode = fc;
|
|
105
|
+
this.textValue = expr;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// One native call; the browser creates the text node.
|
|
109
|
+
this.nodeMarker.textContent = expr;
|
|
110
|
+
node = this.nodeMarker.firstChild;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
node = Globals.doc.createTextNode(expr);
|
|
114
|
+
this.nodeMarker.parentNode.insertBefore(node, this.nodeMarker);
|
|
115
|
+
}
|
|
116
|
+
this.textNode = node;
|
|
117
|
+
this.textValue = expr;
|
|
118
|
+
|
|
119
|
+
// During a NodeGroup's first applyExprs(), no ancestor caches can reference its nodes yet.
|
|
120
|
+
if (!this.parentNg.firstApply) {
|
|
121
|
+
this.nodesCache = null;
|
|
122
|
+
if (this.parentNg.parentPath)
|
|
123
|
+
this.parentNg.parentPath.clearNodesCache();
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A single text NodeGroup left over from an array render.
|
|
129
|
+
if (ngs.length === 1) {
|
|
130
|
+
let ng = ngs[0], tpl = ng.template;
|
|
131
|
+
if (tpl.isText === true) {
|
|
132
|
+
if (tpl.html[0] !== expr) {
|
|
133
|
+
ng.startNode.nodeValue = expr;
|
|
134
|
+
tpl.html[0] = expr; // Text templates have their own html array, so this can't affect others.
|
|
135
|
+
ng.closeKey = expr;
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A previous primitive render stored a bare text node; wrap it in a NodeGroup so it can be diffed.
|
|
143
|
+
if (this.textNode !== null) {
|
|
144
|
+
let ng = new NodeGroup(textTemplate(this.textValue), this, this.textNode);
|
|
145
|
+
(this.nodeGroups ??= []).push(ng);
|
|
146
|
+
this.textNode = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 1. Flatten the expression to a list of Templates, strings and Nodes, evaluating functions along the way.
|
|
150
|
+
/** @type {(Template|string|Node)[]} */
|
|
151
|
+
let newItems = [];
|
|
152
|
+
let hasNodesNow = this.collectItems(expr, newItems, false);
|
|
153
|
+
|
|
154
|
+
// 2. Raw Nodes in the items (now or on the previous render) can't be diffed positionally
|
|
155
|
+
// because this.nodeGroups only tracks NodeGroups. Use the generic path for those.
|
|
156
|
+
if (hasNodesNow || this.itemsHaveNodes) {
|
|
157
|
+
this.itemsHaveNodes = hasNodesNow;
|
|
158
|
+
this.applyGeneric(newItems);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Templates with a key=${} attribute diff by key so node identity follows the data.
|
|
162
|
+
// An empty list also routes to applyKeyed when the previous render was keyed,
|
|
163
|
+
// so removed keyed NodeGroups are discarded instead of pooled.
|
|
164
|
+
let first = newItems.length !== 0 ? newItems[0] : null;
|
|
165
|
+
if (first !== null
|
|
166
|
+
? (typeof first !== 'string' && (first.key !== undefined || Shell.get(first.html, first.svgMode).keyIndex >= 0))
|
|
167
|
+
: (this.nodeGroups !== null && this.nodeGroups.length !== 0 && this.nodeGroups[0].key !== undefined))
|
|
168
|
+
this.applyKeyed(newItems);
|
|
169
|
+
else
|
|
170
|
+
this.applyDiff(newItems);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*#IFDEV*/this.verify();/*#ENDIF*/
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Positionally diff newItems (all Templates) against this.nodeGroups.
|
|
178
|
+
* Unchanged NodeGroups are kept without any hashing or map lookups.
|
|
179
|
+
* NodeGroups created from the same html are rewritten in place.
|
|
180
|
+
* Leftover items are removed/inserted with direct DOM operations.
|
|
181
|
+
* @param newItems {Template[]} */
|
|
182
|
+
applyDiff(newItems) {
|
|
183
|
+
let oldNgs = this.nodeGroups || emptyNodeGroups;
|
|
184
|
+
let oldLen = oldNgs.length, newLen = newItems.length;
|
|
185
|
+
let newNgs = new Array(newLen);
|
|
186
|
+
|
|
187
|
+
let start = 0;
|
|
188
|
+
let oldEnd = oldLen, newEnd = newLen;
|
|
189
|
+
|
|
190
|
+
// 1. Keep the matching prefix.
|
|
191
|
+
// This runs before the suffix scan so that removing one of several identical items keeps the first ones.
|
|
192
|
+
while (start < oldEnd && start < newEnd) {
|
|
193
|
+
let ng = oldNgs[start], t = newItems[start];
|
|
194
|
+
if (!itemSame(ng, t))
|
|
195
|
+
break;
|
|
196
|
+
if (ng.hasComponentPaths)
|
|
197
|
+
ng.applyExprs(t.exprs, false);
|
|
198
|
+
newNgs[start] = ng;
|
|
199
|
+
start++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 2. Keep the matching suffix. This makes removing items from the middle cheap.
|
|
203
|
+
while (oldEnd > start && newEnd > start) {
|
|
204
|
+
let ng = oldNgs[oldEnd-1], t = newItems[newEnd-1];
|
|
205
|
+
if (!itemSame(ng, t))
|
|
206
|
+
break;
|
|
207
|
+
if (ng.hasComponentPaths)
|
|
208
|
+
ng.applyExprs(t.exprs, false);
|
|
209
|
+
newNgs[--newEnd] = ng;
|
|
210
|
+
oldEnd--;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 3. Aligned middle scan: keep unchanged NodeGroups, rewrite same-shape ones in place.
|
|
214
|
+
while (start < oldEnd && start < newEnd) {
|
|
215
|
+
let ng = oldNgs[start], t = newItems[start];
|
|
216
|
+
if (itemSame(ng, t)) { // Can happen between changed rows, e.g. partial updates.
|
|
217
|
+
if (ng.hasComponentPaths)
|
|
218
|
+
ng.applyExprs(t.exprs, false);
|
|
219
|
+
}
|
|
220
|
+
else if (itemClose(ng, t))
|
|
221
|
+
this.rewriteNodeGroup(ng, t);
|
|
222
|
+
else
|
|
223
|
+
break; // Different html at this position. Remove/insert the remaining window below.
|
|
224
|
+
newNgs[start] = ng;
|
|
225
|
+
start++;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let oldRemain = oldEnd - start, newRemain = newEnd - start;
|
|
229
|
+
if (oldRemain || newRemain) {
|
|
230
|
+
|
|
231
|
+
// 4. Remove leftover old NodeGroups.
|
|
232
|
+
if (oldRemain) {
|
|
233
|
+
// Materialize node caches of multi-node groups while still attached,
|
|
234
|
+
// since detaching breaks sibling links. Single-node groups don't need it.
|
|
235
|
+
for (let i=start; i<oldEnd; i++) {
|
|
236
|
+
let ng = oldNgs[i];
|
|
237
|
+
if (ng.startNode !== ng.endNode)
|
|
238
|
+
ng.getNodes();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Fast clear when removing everything.
|
|
242
|
+
let cleared = newLen === 0 && start === 0 && this.fastClear();
|
|
243
|
+
let pool = this.nodeGroupsDetachedAvailable ??= new MultiValueMap();
|
|
244
|
+
for (let i=start; i<oldEnd; i++) {
|
|
245
|
+
let ng = oldNgs[i];
|
|
246
|
+
if (ng.startNode !== ng.endNode)
|
|
247
|
+
Util.saveOrphans(ng.getNodes()); // Moves the nodes out of the DOM, into their own fragment.
|
|
248
|
+
else if (!cleared)
|
|
249
|
+
ng.startNode.remove();
|
|
250
|
+
if (!ng.template.isText)
|
|
251
|
+
pool.addCapped(ng.closeKey, ng, maxPooledPerKey);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 5. Insert leftover new items.
|
|
256
|
+
if (newRemain) {
|
|
257
|
+
let wholeParent = this.wholeParent;
|
|
258
|
+
let anchor = newEnd < newLen ? newNgs[newEnd].startNode : (wholeParent ? null : this.nodeMarker);
|
|
259
|
+
let parent = wholeParent ? this.nodeMarker : this.nodeMarker.parentNode;
|
|
260
|
+
let target = parent, before = anchor;
|
|
261
|
+
let fragment = null;
|
|
262
|
+
if (newRemain > 1) { // Batch-insert through a fragment.
|
|
263
|
+
fragment = Globals.doc.createDocumentFragment();
|
|
264
|
+
target = fragment;
|
|
265
|
+
before = null;
|
|
266
|
+
}
|
|
267
|
+
for (let i=start; i<newEnd; i++) {
|
|
268
|
+
let ng = this.createOrReuse(newItems[i]);
|
|
269
|
+
newNgs[i] = ng;
|
|
270
|
+
let node = ng.startNode, end = ng.endNode;
|
|
271
|
+
if (node === end) // Single-node NodeGroups are the common case in loops.
|
|
272
|
+
target.insertBefore(node, before);
|
|
273
|
+
else while (true) {
|
|
274
|
+
let next = node.nextSibling;
|
|
275
|
+
target.insertBefore(node, before);
|
|
276
|
+
if (node === end)
|
|
277
|
+
break;
|
|
278
|
+
node = next;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (fragment)
|
|
282
|
+
parent.insertBefore(fragment, anchor);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 6. Node membership changed, so invalidate caches.
|
|
286
|
+
// During a NodeGroup's first applyExprs(), no ancestor caches can reference its nodes yet.
|
|
287
|
+
if (!this.parentNg.firstApply) {
|
|
288
|
+
this.nodesCache = null;
|
|
289
|
+
if (this.parentNg.parentPath)
|
|
290
|
+
this.parentNg.parentPath.clearNodesCache();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.nodeGroups = newNgs;
|
|
295
|
+
|
|
296
|
+
// Keep state used by the generic path from going stale.
|
|
297
|
+
if (this.nodeGroupsRendered)
|
|
298
|
+
this.nodeGroupsRendered = null;
|
|
299
|
+
if (this.nodeGroupsAttachedAvailable)
|
|
300
|
+
this.nodeGroupsAttachedAvailable = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Keyed reconciliation: match this.nodeGroups to newItems by their key=${} expressions,
|
|
305
|
+
* so NodeGroup (and DOM node) identity follows the data:
|
|
306
|
+
* 1. Prefix/suffix scans keep NodeGroups whose keys match in place, rewriting changed content.
|
|
307
|
+
* 2. The middle windows match through a key map, and kept NodeGroups outside a longest
|
|
308
|
+
* increasing subsequence of old positions are moved, so the fewest node ranges move.
|
|
309
|
+
* 3. Unmatched new items create fresh NodeGroups and unmatched old ones are discarded —
|
|
310
|
+
* never pooled — so replaced data always gets new nodes, as keyed semantics require.
|
|
311
|
+
* @param newItems {(Template|string)[]} */
|
|
312
|
+
applyKeyed(newItems) {
|
|
313
|
+
let oldNgs = this.nodeGroups || emptyNodeGroups;
|
|
314
|
+
let oldLen = oldNgs.length, newLen = newItems.length;
|
|
315
|
+
let newNgs = new Array(newLen);
|
|
316
|
+
|
|
317
|
+
// Resolve an item's key, caching the html->keyIndex lookup for same-template lists.
|
|
318
|
+
let keyHtml = null, keyIndex = -1;
|
|
319
|
+
const keyOf = t => {
|
|
320
|
+
if (t.key !== undefined) // JSX templates carry the key directly.
|
|
321
|
+
return t.key;
|
|
322
|
+
if (t.html !== keyHtml) {
|
|
323
|
+
keyHtml = t.html;
|
|
324
|
+
keyIndex = Shell.get(t.html, t.svgMode).keyIndex;
|
|
325
|
+
}
|
|
326
|
+
return keyIndex >= 0 ? t.exprs[keyIndex] : undefined;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
//#IFDEV
|
|
330
|
+
{
|
|
331
|
+
let seen = new Set();
|
|
332
|
+
for (let t of newItems) {
|
|
333
|
+
let k = typeof t === 'string' ? undefined : keyOf(t);
|
|
334
|
+
if (k === undefined)
|
|
335
|
+
console.warn('Unkeyed item in a keyed list; it will be rebuilt on every render:', t);
|
|
336
|
+
else if (seen.has(k))
|
|
337
|
+
console.warn('Duplicate key in keyed list:', k);
|
|
338
|
+
else
|
|
339
|
+
seen.add(k);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
//#ENDIF
|
|
343
|
+
|
|
344
|
+
let start = 0, oldEnd = oldLen, newEnd = newLen;
|
|
345
|
+
|
|
346
|
+
// 1. Keep the matching prefix in place, rewriting changed content.
|
|
347
|
+
while (start < oldEnd && start < newEnd) {
|
|
348
|
+
let ng = oldNgs[start], t = newItems[start];
|
|
349
|
+
// An identical Template instance (h.map) implies an identical key, so skip key extraction.
|
|
350
|
+
if (ng.template === t) {
|
|
351
|
+
if (ng.hasComponentPaths)
|
|
352
|
+
ng.applyExprs(t.exprs, false);
|
|
353
|
+
}
|
|
354
|
+
else if (typeof t === 'string' || ng.key !== keyOf(t) || !itemClose(ng, t))
|
|
355
|
+
break;
|
|
356
|
+
else if (itemSame(ng, t)) {
|
|
357
|
+
if (ng.hasComponentPaths)
|
|
358
|
+
ng.applyExprs(t.exprs, false);
|
|
359
|
+
}
|
|
360
|
+
else
|
|
361
|
+
this.rewriteNodeGroup(ng, t);
|
|
362
|
+
newNgs[start] = ng;
|
|
363
|
+
start++;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. Keep the matching suffix.
|
|
367
|
+
while (oldEnd > start && newEnd > start) {
|
|
368
|
+
let ng = oldNgs[oldEnd-1], t = newItems[newEnd-1];
|
|
369
|
+
if (ng.template === t) {
|
|
370
|
+
if (ng.hasComponentPaths)
|
|
371
|
+
ng.applyExprs(t.exprs, false);
|
|
372
|
+
}
|
|
373
|
+
else if (typeof t === 'string' || ng.key !== keyOf(t) || !itemClose(ng, t))
|
|
374
|
+
break;
|
|
375
|
+
else if (itemSame(ng, t)) {
|
|
376
|
+
if (ng.hasComponentPaths)
|
|
377
|
+
ng.applyExprs(t.exprs, false);
|
|
378
|
+
}
|
|
379
|
+
else
|
|
380
|
+
this.rewriteNodeGroup(ng, t);
|
|
381
|
+
newNgs[--newEnd] = ng;
|
|
382
|
+
oldEnd--;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let oldRemain = oldEnd - start, newRemain = newEnd - start;
|
|
386
|
+
if (oldRemain || newRemain) {
|
|
387
|
+
let wholeParent = this.wholeParent;
|
|
388
|
+
let parent = wholeParent ? this.nodeMarker : this.nodeMarker.parentNode;
|
|
389
|
+
|
|
390
|
+
// 3. Match the middle windows by key.
|
|
391
|
+
let kept = 0, moved = false;
|
|
392
|
+
let sources = null; // sources[i] = old index reused by new item start+i, or -1 to create fresh.
|
|
393
|
+
let removals = null;
|
|
394
|
+
if (oldRemain) {
|
|
395
|
+
if (newRemain) {
|
|
396
|
+
let keyToNewIndex = new Map();
|
|
397
|
+
for (let i=start; i<newEnd; i++) {
|
|
398
|
+
let t = newItems[i];
|
|
399
|
+
if (typeof t !== 'string')
|
|
400
|
+
keyToNewIndex.set(keyOf(t), i);
|
|
401
|
+
}
|
|
402
|
+
sources = new Array(newRemain).fill(-1);
|
|
403
|
+
let lastNewIndex = -1;
|
|
404
|
+
for (let i=start; i<oldEnd; i++) {
|
|
405
|
+
let ng = oldNgs[i];
|
|
406
|
+
let newIndex = ng.key === undefined ? undefined : keyToNewIndex.get(ng.key);
|
|
407
|
+
let t;
|
|
408
|
+
if (newIndex !== undefined && sources[newIndex-start] === -1 && itemClose(ng, t = newItems[newIndex])) {
|
|
409
|
+
sources[newIndex-start] = i;
|
|
410
|
+
kept++;
|
|
411
|
+
if (newIndex < lastNewIndex)
|
|
412
|
+
moved = true;
|
|
413
|
+
else
|
|
414
|
+
lastNewIndex = newIndex;
|
|
415
|
+
if (itemSame(ng, t)) {
|
|
416
|
+
if (ng.hasComponentPaths)
|
|
417
|
+
ng.applyExprs(t.exprs, false);
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
this.rewriteNodeGroup(ng, t);
|
|
421
|
+
newNgs[newIndex] = ng;
|
|
422
|
+
}
|
|
423
|
+
else
|
|
424
|
+
(removals ??= []).push(ng);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
removals = oldNgs.slice(start, oldEnd);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 4. Remove unmatched old NodeGroups. They're discarded, never pooled,
|
|
433
|
+
// so a later render with new keys always creates new nodes.
|
|
434
|
+
if (removals) {
|
|
435
|
+
// Materialize node caches of multi-node groups while attached, since detaching breaks sibling links.
|
|
436
|
+
for (let ng of removals)
|
|
437
|
+
if (ng.startNode !== ng.endNode)
|
|
438
|
+
ng.getNodes();
|
|
439
|
+
|
|
440
|
+
// Fast clear when nothing is kept anywhere; the whole region is removals.
|
|
441
|
+
let cleared = start === 0 && newEnd === newLen && kept === 0 && this.fastClear();
|
|
442
|
+
if (!cleared)
|
|
443
|
+
for (let ng of removals) {
|
|
444
|
+
if (ng.startNode !== ng.endNode)
|
|
445
|
+
Util.saveOrphans(ng.getNodes()); // Moves the nodes out of the DOM, into their own fragment.
|
|
446
|
+
else
|
|
447
|
+
ng.startNode.remove();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 5. Insert new NodeGroups and move kept ones.
|
|
452
|
+
if (newRemain) {
|
|
453
|
+
let anchor = newEnd < newLen ? newNgs[newEnd].startNode : (wholeParent ? null : this.nodeMarker);
|
|
454
|
+
|
|
455
|
+
// 5a. Nothing kept in the middle: batch-insert every new item through a fragment.
|
|
456
|
+
if (kept === 0) {
|
|
457
|
+
let target = parent, before = anchor;
|
|
458
|
+
let fragment = null;
|
|
459
|
+
if (newRemain > 1) {
|
|
460
|
+
fragment = Globals.doc.createDocumentFragment();
|
|
461
|
+
target = fragment;
|
|
462
|
+
before = null;
|
|
463
|
+
}
|
|
464
|
+
for (let i=start; i<newEnd; i++) {
|
|
465
|
+
let ng = this.createNew(newItems[i]);
|
|
466
|
+
newNgs[i] = ng;
|
|
467
|
+
let node = ng.startNode, end = ng.endNode;
|
|
468
|
+
if (node === end)
|
|
469
|
+
target.insertBefore(node, before);
|
|
470
|
+
else while (true) {
|
|
471
|
+
let next = node.nextSibling;
|
|
472
|
+
target.insertBefore(node, before);
|
|
473
|
+
if (node === end)
|
|
474
|
+
break;
|
|
475
|
+
node = next;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (fragment)
|
|
479
|
+
parent.insertBefore(fragment, anchor);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 5b. Mixed: iterate backwards so each item's anchor is already in place.
|
|
483
|
+
// Kept NodeGroups on a longest increasing subsequence of old positions stay still;
|
|
484
|
+
// everything else moves or is created.
|
|
485
|
+
else {
|
|
486
|
+
let lis = moved ? longestIncreasingSubsequence(sources) : null;
|
|
487
|
+
let lisPos = lis !== null ? lis.length - 1 : -1;
|
|
488
|
+
for (let i=newEnd-1; i>=start; i--) {
|
|
489
|
+
let ng = newNgs[i];
|
|
490
|
+
if (ng === undefined) { // Create and insert.
|
|
491
|
+
ng = this.createNew(newItems[i]);
|
|
492
|
+
newNgs[i] = ng;
|
|
493
|
+
insertNodesBefore(parent, ng, anchor);
|
|
494
|
+
}
|
|
495
|
+
else if (lis !== null) {
|
|
496
|
+
if (lisPos >= 0 && lis[lisPos] === i - start)
|
|
497
|
+
lisPos--; // Part of the stable subsequence; doesn't move.
|
|
498
|
+
else
|
|
499
|
+
insertNodesBefore(parent, ng, anchor);
|
|
500
|
+
}
|
|
501
|
+
anchor = ng.startNode;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 6. Node membership or order changed, so invalidate caches.
|
|
507
|
+
// During a NodeGroup's first applyExprs(), no ancestor caches can reference its nodes yet.
|
|
508
|
+
if (!this.parentNg.firstApply) {
|
|
509
|
+
this.nodesCache = null;
|
|
510
|
+
if (this.parentNg.parentPath)
|
|
511
|
+
this.parentNg.parentPath.clearNodesCache();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
this.nodeGroups = newNgs;
|
|
516
|
+
|
|
517
|
+
// Keep state used by the generic path from going stale.
|
|
518
|
+
if (this.nodeGroupsRendered)
|
|
519
|
+
this.nodeGroupsRendered = null;
|
|
520
|
+
if (this.nodeGroupsAttachedAvailable)
|
|
521
|
+
this.nodeGroupsAttachedAvailable = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Create a NodeGroup for an item in a keyed list. Never reuses pooled NodeGroups,
|
|
526
|
+
* because keyed semantics require new keys to get new nodes.
|
|
527
|
+
* @param item {Template|string}
|
|
528
|
+
* @return {NodeGroup} */
|
|
529
|
+
createNew(item) {
|
|
530
|
+
if (typeof item === 'string')
|
|
531
|
+
return new NodeGroup(textTemplate(item), this); // Text NodeGroups have no paths to apply.
|
|
532
|
+
let ng = new NodeGroup(item, this);
|
|
533
|
+
if (item.exprs.length || (ng.paths && ng.paths.length))
|
|
534
|
+
ng.applyExprs(item.exprs);
|
|
535
|
+
return ng;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Update an existing NodeGroup, created from the same html strings, with new values.
|
|
540
|
+
* @param ng {NodeGroup}
|
|
541
|
+
* @param item {Template|string} */
|
|
542
|
+
rewriteNodeGroup(ng, item) {
|
|
543
|
+
if (typeof item === 'string') { // Text content.
|
|
544
|
+
ng.startNode.nodeValue = item;
|
|
545
|
+
ng.template.html[0] = item; // Text templates have their own html array, so this can't affect others.
|
|
546
|
+
ng.closeKey = item;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
// When every path consumes exactly one expression, paths align 1:1 with exprs,
|
|
550
|
+
// so only the expressions that changed need to be applied.
|
|
551
|
+
if (ng.pathsSingleExpr) {
|
|
552
|
+
// Stamped groups (paths === null) rewrite through the shared stampers and stay
|
|
553
|
+
// path-less, unless a child-node expression stopped being primitive.
|
|
554
|
+
if (ng.paths !== null || !ng.rewriteStamp(item)) {
|
|
555
|
+
let oldExprs = ng.template.exprs, newExprs = item.exprs;
|
|
556
|
+
let paths = ng.paths ?? ng.materializePaths();
|
|
557
|
+
for (let i = paths.length - 1; i >= 0; i--)
|
|
558
|
+
if (!exprSame(oldExprs[i], newExprs[i]))
|
|
559
|
+
paths[i].applySingle(newExprs[i]);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (ng.styles)
|
|
563
|
+
ng.updateStyles();
|
|
564
|
+
ng.nodesCache = null;
|
|
565
|
+
ng.firstApply = false;
|
|
566
|
+
}
|
|
567
|
+
else
|
|
568
|
+
ng.applyExprs(item.exprs);
|
|
569
|
+
ng.template = item;
|
|
570
|
+
if (item.key !== undefined) // JSX keyed item; keep ng.key in sync with the new template.
|
|
571
|
+
ng.key = item.key;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Create a NodeGroup for an item, reusing a detached one with the same html if available.
|
|
577
|
+
* @param item {Template|string}
|
|
578
|
+
* @return {NodeGroup} */
|
|
579
|
+
createOrReuse(item) {
|
|
580
|
+
let ng;
|
|
581
|
+
if (typeof item === 'string') {
|
|
582
|
+
item = textTemplate(item);
|
|
583
|
+
return new NodeGroup(item, this); // Text NodeGroups have no paths to apply.
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let pool = this.nodeGroupsDetachedAvailable;
|
|
587
|
+
if (pool) {
|
|
588
|
+
ng = pool.deleteAny(item.getCloseKey());
|
|
589
|
+
if (ng) {
|
|
590
|
+
// rewriteNodeGroup compares expressions and writes only what changed,
|
|
591
|
+
// keeping stamped groups path-less. It also assigns ng.template.
|
|
592
|
+
this.rewriteNodeGroup(ng, item);
|
|
593
|
+
return ng;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
ng = new NodeGroup(item, this);
|
|
598
|
+
if (item.exprs.length || (ng.paths && ng.paths.length))
|
|
599
|
+
ng.applyExprs(item.exprs);
|
|
600
|
+
return ng;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Recursively flatten expr into items, evaluating functions and converting primitives to text Templates.
|
|
605
|
+
* @param expr
|
|
606
|
+
* @param items {(Template|Node)[]}
|
|
607
|
+
* @param hasNodes {boolean}
|
|
608
|
+
* @return {boolean} True if any raw Nodes were added to items. */
|
|
609
|
+
collectItems(expr, items, hasNodes) {
|
|
610
|
+
if (expr instanceof Template)
|
|
611
|
+
items.push(expr);
|
|
612
|
+
|
|
613
|
+
else if (Array.isArray(expr)) {
|
|
614
|
+
for (let subExpr of expr) {
|
|
615
|
+
if (subExpr instanceof Template) // Inline the most common case.
|
|
616
|
+
items.push(subExpr);
|
|
617
|
+
else
|
|
618
|
+
hasNodes = this.collectItems(subExpr, items, hasNodes);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
else if (typeof expr === 'function')
|
|
623
|
+
hasNodes = this.collectItems(expr(), items, hasNodes);
|
|
624
|
+
|
|
625
|
+
else if (expr instanceof NodeList) {
|
|
626
|
+
for (let node of expr)
|
|
627
|
+
items.push(node);
|
|
628
|
+
hasNodes = hasNodes || expr.length > 0;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
else if (expr?.nodeType) {
|
|
632
|
+
if (expr.nodeType === 11) { // DocumentFragment
|
|
633
|
+
for (let node of [...expr.childNodes])
|
|
634
|
+
items.push(node);
|
|
635
|
+
}
|
|
636
|
+
else
|
|
637
|
+
items.push(expr);
|
|
638
|
+
hasNodes = true;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// String/Number/Date/Boolean. Pushed as a plain string to avoid allocating a Template.
|
|
642
|
+
else {
|
|
643
|
+
if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
|
|
644
|
+
expr = '';
|
|
645
|
+
else if (typeof expr !== 'string')
|
|
646
|
+
expr += '';
|
|
647
|
+
|
|
648
|
+
items.push(expr);
|
|
649
|
+
}
|
|
650
|
+
return hasNodes;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Pool-based reconciliation using close keys and reconcileNodes(). Used when expressions
|
|
655
|
+
* contain raw Nodes, since those can't be tracked by the positional diff.
|
|
656
|
+
* @param items {(Template|string|Node)[]} */
|
|
657
|
+
applyGeneric(items) {
|
|
658
|
+
let path = this;
|
|
659
|
+
path.freeNodeGroups();
|
|
660
|
+
|
|
661
|
+
/** @type {Node[]} */
|
|
662
|
+
let newNodes = [];
|
|
663
|
+
let oldNodeGroups = path.nodeGroups || emptyNodeGroups;
|
|
664
|
+
/*#IFDEV*/assert(!oldNodeGroups.includes(null))/*#ENDIF*/
|
|
665
|
+
|
|
666
|
+
path.nodeGroups = [];
|
|
667
|
+
for (let item of items) {
|
|
668
|
+
if (typeof item === 'string')
|
|
669
|
+
item = textTemplate(item);
|
|
670
|
+
if (item instanceof Template) {
|
|
671
|
+
let ng = path.getNodeGroup(item);
|
|
672
|
+
newNodes.push(...ng.getNodes());
|
|
673
|
+
path.nodeGroups.push(ng);
|
|
674
|
+
}
|
|
675
|
+
else // A raw Node from an expression; collectItems() has already flattened fragments and NodeLists.
|
|
676
|
+
newNodes.push(item);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let oldNodes = path.getNodes();
|
|
680
|
+
|
|
681
|
+
// This pre-check makes it a few percent faster?
|
|
682
|
+
let same = Util.arraySame(oldNodes, newNodes);
|
|
683
|
+
if (!same) {
|
|
684
|
+
|
|
685
|
+
path.nodesCache = newNodes; // Replaces value set by path.getNodes()
|
|
686
|
+
|
|
687
|
+
if (this.parentNg.parentPath)
|
|
688
|
+
this.parentNg.parentPath.clearNodesCache();
|
|
689
|
+
|
|
690
|
+
// Fast clear method
|
|
691
|
+
let isNowEmpty = oldNodes.length && !newNodes.length;
|
|
692
|
+
if (!isNowEmpty || !path.fastClear()) {
|
|
693
|
+
|
|
694
|
+
// Rearrange nodes.
|
|
695
|
+
if (path.wholeParent)
|
|
696
|
+
reconcileNodes(path.nodeMarker, oldNodes, newNodes, null)
|
|
697
|
+
else
|
|
698
|
+
reconcileNodes(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// TODO: Put this in a remove() function of NodeGroup.
|
|
702
|
+
// Then only run it on the old nodeGroups that were actually removed.
|
|
703
|
+
//Util.saveOrphans(oldNodeGroups, oldNodes);
|
|
704
|
+
|
|
705
|
+
for (let ng of oldNodeGroups)
|
|
706
|
+
if (!ng.startNode.parentNode)
|
|
707
|
+
Util.saveOrphans(ng.getNodes());
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Clear the nodeCache of this Path, as well as all parent and child Paths that
|
|
714
|
+
* share the same DOM parent node. */
|
|
715
|
+
clearNodesCache() {
|
|
716
|
+
let path = this;
|
|
717
|
+
|
|
718
|
+
// Clear cache parent Paths that have the same parentNode
|
|
719
|
+
let parentNode = this.wholeParent ? this.nodeMarker : this.nodeMarker.parentNode;
|
|
720
|
+
while (path && (path.wholeParent ? path.nodeMarker : path.nodeMarker.parentNode) === parentNode) {
|
|
721
|
+
path.nodesCache = null;
|
|
722
|
+
path = path.parentNg?.parentPath
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Attempt to remove all of this Path's nodes from the DOM, if it can be done using a special fast method.
|
|
728
|
+
* @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
|
|
729
|
+
fastClear() {
|
|
730
|
+
if (this.wholeParent) {
|
|
731
|
+
this.nodeMarker.textContent = '';
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
let parent = this.nodeBefore.parentNode;
|
|
736
|
+
if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
|
|
737
|
+
|
|
738
|
+
// If parent is the only child of the grandparent, replace the whole parent.
|
|
739
|
+
// And if it has no siblings, it's not created by a NodeGroup/path.
|
|
740
|
+
// Commented out because this will break any references.
|
|
741
|
+
// And because I don't see much performance difference.
|
|
742
|
+
// let grandparent = parent.parentNode
|
|
743
|
+
// if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
|
|
744
|
+
// let replacement = document.createElement(parent.tagName)
|
|
745
|
+
// replacement.append(this.nodeBefore, this.nodeMarker)
|
|
746
|
+
// for (let attrib of parent.attributes)
|
|
747
|
+
// replacement.setAttribute(attrib.name, attrib.value)
|
|
748
|
+
// parent.replaceWith(replacement)
|
|
749
|
+
// }
|
|
750
|
+
// else {
|
|
751
|
+
parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
|
|
752
|
+
parent.append(this.nodeBefore, this.nodeMarker)
|
|
753
|
+
//}
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Get a NodeGroup with the same html as the template, reusing a pooled one if available.
|
|
761
|
+
* The first pooled NodeGroup with the same close key (html shape) is taken and its
|
|
762
|
+
* expressions are updated, skipping the update when its values are already identical.
|
|
763
|
+
*
|
|
764
|
+
* @param template {Template}
|
|
765
|
+
* @return {NodeGroup} */
|
|
766
|
+
getNodeGroup(template) {
|
|
767
|
+
let closeKey = template.getCloseKey();
|
|
768
|
+
let result = this.nodeGroupsAttachedAvailable?.deleteAny(closeKey)
|
|
769
|
+
|| this.nodeGroupsDetachedAvailable?.deleteAny(closeKey);
|
|
770
|
+
|
|
771
|
+
if (result) {
|
|
772
|
+
if (templatesSame(result.template, template)) {
|
|
773
|
+
// Components still render so changes deeper in the tree can surface.
|
|
774
|
+
if (result.hasComponentPaths)
|
|
775
|
+
result.applyExprs(template.exprs, false);
|
|
776
|
+
}
|
|
777
|
+
else
|
|
778
|
+
result.applyExprs(template.exprs);
|
|
779
|
+
result.template = template;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
result = new NodeGroup(template, this);
|
|
783
|
+
result.applyExprs(template.exprs);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
(this.nodeGroupsRendered ??= []).push(result);
|
|
787
|
+
|
|
788
|
+
/*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
|
|
795
|
+
* Called at the beginning of applyGeneric() so it can have NodeGroups to use.
|
|
796
|
+
* TODO: this could run as needed in getNodeGroup? */
|
|
797
|
+
freeNodeGroups() {
|
|
798
|
+
// Add nodes that weren't used during render() to nodeGroupsDetached
|
|
799
|
+
let previouslyAttached = this.nodeGroupsAttachedAvailable?.data;
|
|
800
|
+
if (previouslyAttached) {
|
|
801
|
+
let detached = (this.nodeGroupsDetachedAvailable ??= new MultiValueMap()).data;
|
|
802
|
+
for (let key in previouslyAttached) {
|
|
803
|
+
let src = previouslyAttached[key];
|
|
804
|
+
let from = src.head || 0; // Skip entries already consumed by deleteAny().
|
|
805
|
+
let array = detached[key];
|
|
806
|
+
if (!array) {
|
|
807
|
+
array = detached[key] = from ? src.slice(from) : src;
|
|
808
|
+
if (array.length > maxPooledPerKey)
|
|
809
|
+
array.length = maxPooledPerKey;
|
|
810
|
+
}
|
|
811
|
+
else
|
|
812
|
+
for (let i=from, max=maxPooledPerKey + (array.head || 0); i<src.length && array.length < max; i++)
|
|
813
|
+
array.push(src[i]);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Add nodes that were used during render() to nodeGroupsRendered.
|
|
818
|
+
// If the last render used the positional diff, the in-use NodeGroups are in
|
|
819
|
+
// this.nodeGroups instead of nodeGroupsRendered.
|
|
820
|
+
this.nodeGroupsAttachedAvailable = new MultiValueMap();
|
|
821
|
+
let nga = this.nodeGroupsAttachedAvailable;
|
|
822
|
+
let source = this.nodeGroupsRendered?.length ? this.nodeGroupsRendered : this.nodeGroups;
|
|
823
|
+
if (source)
|
|
824
|
+
for (let ng of source)
|
|
825
|
+
nga.add(ng.closeKey, ng);
|
|
826
|
+
|
|
827
|
+
this.nodeGroupsRendered = null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* @return {(Node|HTMLElement)[]} */
|
|
834
|
+
getNodes() {
|
|
835
|
+
|
|
836
|
+
// Why doesn't this work?
|
|
837
|
+
// let result2 = [];
|
|
838
|
+
// for (let ng of this.nodeGroups)
|
|
839
|
+
// result2.push(...ng.getNodes())
|
|
840
|
+
// return result2;
|
|
841
|
+
|
|
842
|
+
let result
|
|
843
|
+
|
|
844
|
+
// This shaves about 5ms off the partialUpdate benchmark.
|
|
845
|
+
result = this.nodesCache;
|
|
846
|
+
if (result) {
|
|
847
|
+
//#IFDEV
|
|
848
|
+
//this.checkNodesCache();
|
|
849
|
+
//#ENDIF
|
|
850
|
+
return result
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
result = [];
|
|
854
|
+
let current, stop = null;
|
|
855
|
+
if (this.wholeParent)
|
|
856
|
+
current = this.nodeMarker.firstChild;
|
|
857
|
+
else {
|
|
858
|
+
current = this.nodeBefore.nextSibling;
|
|
859
|
+
stop = this.nodeMarker;
|
|
860
|
+
}
|
|
861
|
+
while (current && current !== stop) {
|
|
862
|
+
result.push(current)
|
|
863
|
+
current = current.nextSibling
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
this.nodesCache = result;
|
|
867
|
+
return result;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
//#IFDEV
|
|
871
|
+
|
|
872
|
+
get debug() {
|
|
873
|
+
return [
|
|
874
|
+
`parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
|
|
875
|
+
'nodes:',
|
|
876
|
+
...setIndent(this.getNodes().map(item => {
|
|
877
|
+
if (item?.nodeType)
|
|
878
|
+
return item.outerHTML || item.textContent
|
|
879
|
+
else if (item instanceof NodeGroup)
|
|
880
|
+
return item.debug
|
|
881
|
+
}), 1).flat()
|
|
882
|
+
]
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
get debugNodes() {
|
|
886
|
+
// Clear nodesCache so that getNodes() manually gets the nodes.
|
|
887
|
+
let nc = this.nodesCache;
|
|
888
|
+
this.nodesCache = null;
|
|
889
|
+
let result = this.getNodes()
|
|
890
|
+
this.nodesCache = nc;
|
|
891
|
+
return result;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
checkNodesCache() {
|
|
895
|
+
return;
|
|
896
|
+
|
|
897
|
+
// Make sure cache is accurate.
|
|
898
|
+
// If this is invalid, then perhaps another component append()'d one of our nodes to itself.
|
|
899
|
+
// Or perhaps one of our nodes is used in an expression more than once.
|
|
900
|
+
// TODO: Find a way to check for and warn when this happens.
|
|
901
|
+
// MutationObserver is too slow since it's asynchronous.
|
|
902
|
+
// My own MutationWatcher has to modify DOM prototypes, which is rather invasive.
|
|
903
|
+
if (this.nodesCache) {
|
|
904
|
+
let nodes = [];
|
|
905
|
+
let current = this.nodeBefore.nextSibling;
|
|
906
|
+
let nodeMarker = this.nodeMarker;
|
|
907
|
+
while (current && current !== nodeMarker) {
|
|
908
|
+
nodes.push(current)
|
|
909
|
+
current = current.nextSibling
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (!Util.arraySame(this.nodesCache, nodes))
|
|
913
|
+
console.log(this.nodesCache, nodes)
|
|
914
|
+
assert(Util.arraySame(this.nodesCache, nodes) === true);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
//#ENDIF
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
// Shared empty array for paths whose nodeGroups were never created. Never mutated.
|
|
922
|
+
const emptyNodeGroups = [];
|
|
923
|
+
|
|
924
|
+
// Most detached NodeGroups kept per close key. Bounds memory growth after very large
|
|
925
|
+
// lists are cleared while keeping pooled rows for every typical re-create pattern.
|
|
926
|
+
// Lowering this (e.g. to 1000) cuts retained memory ~7x after clearing a 10k-row list,
|
|
927
|
+
// but makes re-creating such a list ~2x slower since most rows are built fresh.
|
|
928
|
+
const maxPooledPerKey = 10000;
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* @param text {string}
|
|
932
|
+
* @return {Template} */
|
|
933
|
+
function textTemplate(text) {
|
|
934
|
+
let result = new Template([text], []);
|
|
935
|
+
result.isText = true;
|
|
936
|
+
return result;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Does the NodeGroup already have content identical to item?
|
|
941
|
+
* @param ng {NodeGroup}
|
|
942
|
+
* @param item {Template|string}
|
|
943
|
+
* @return {boolean} */
|
|
944
|
+
function itemSame(ng, item) {
|
|
945
|
+
let tpl = ng.template;
|
|
946
|
+
if (tpl === item) // h.map() returns the same Template instance for an unchanged item.
|
|
947
|
+
return true;
|
|
948
|
+
if (typeof item === 'string')
|
|
949
|
+
return tpl.isText === true && tpl.html[0] === item;
|
|
950
|
+
return templatesSame(tpl, item);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Could ng be rewritten in place with the values of item?
|
|
955
|
+
* True when both come from the same html strings (and thus the same Shell), or both are text.
|
|
956
|
+
* @param ng {NodeGroup}
|
|
957
|
+
* @param item {Template|string}
|
|
958
|
+
* @return {boolean} */
|
|
959
|
+
function itemClose(ng, item) {
|
|
960
|
+
let tpl = ng.template;
|
|
961
|
+
if (typeof item === 'string')
|
|
962
|
+
return tpl.isText === true;
|
|
963
|
+
return tpl.html === item.html && tpl.svgMode === item.svgMode;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Insert all of ng's nodes before anchor within parent.
|
|
968
|
+
* @param parent {Node}
|
|
969
|
+
* @param ng {NodeGroup}
|
|
970
|
+
* @param anchor {?Node} Null appends at the end. */
|
|
971
|
+
function insertNodesBefore(parent, ng, anchor) {
|
|
972
|
+
let node = ng.startNode, end = ng.endNode;
|
|
973
|
+
if (node === end) // Single-node NodeGroups are the common case in loops.
|
|
974
|
+
parent.insertBefore(node, anchor);
|
|
975
|
+
else while (true) {
|
|
976
|
+
let next = node.nextSibling;
|
|
977
|
+
parent.insertBefore(node, anchor);
|
|
978
|
+
if (node === end)
|
|
979
|
+
break;
|
|
980
|
+
node = next;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Indices into arr whose values form a longest strictly increasing subsequence, skipping -1 entries.
|
|
986
|
+
* O(n log n) patience algorithm with predecessor backtracking, as used by Vue 3's keyed diff.
|
|
987
|
+
* @param arr {int[]}
|
|
988
|
+
* @return {int[]} */
|
|
989
|
+
function longestIncreasingSubsequence(arr) {
|
|
990
|
+
let result = []; // Indices of the smallest known tail for each subsequence length.
|
|
991
|
+
let prev = new Array(arr.length); // prev[i] = index that comes before i in the subsequence ending at i.
|
|
992
|
+
for (let i=0; i<arr.length; i++) {
|
|
993
|
+
let v = arr[i];
|
|
994
|
+
if (v === -1)
|
|
995
|
+
continue;
|
|
996
|
+
// Binary search for the first tail whose value >= v.
|
|
997
|
+
let lo = 0, hi = result.length;
|
|
998
|
+
while (lo < hi) {
|
|
999
|
+
let mid = (lo + hi) >> 1;
|
|
1000
|
+
if (arr[result[mid]] < v)
|
|
1001
|
+
lo = mid + 1;
|
|
1002
|
+
else
|
|
1003
|
+
hi = mid;
|
|
1004
|
+
}
|
|
1005
|
+
if (lo > 0)
|
|
1006
|
+
prev[i] = result[lo-1];
|
|
1007
|
+
if (lo === result.length)
|
|
1008
|
+
result.push(i);
|
|
1009
|
+
else
|
|
1010
|
+
result[lo] = i;
|
|
1011
|
+
}
|
|
1012
|
+
// Backtrack from the last tail to recover the subsequence's indices.
|
|
1013
|
+
let pos = result.length;
|
|
1014
|
+
if (pos) {
|
|
1015
|
+
let i = result[pos-1];
|
|
1016
|
+
while (pos-- > 0) {
|
|
1017
|
+
result[pos] = i;
|
|
1018
|
+
i = prev[i];
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return result;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Reconcile the children of parentNode so they become newNodes, in order, ending just before
|
|
1027
|
+
* `before` (or at the end when before is null). Reuses existing nodes by identity and skips
|
|
1028
|
+
* nodes already in their target position. Only the raw-Node fallback (applyGeneric) uses this;
|
|
1029
|
+
* the keyed/positional diffs never do.
|
|
1030
|
+
* @param parentNode {Node}
|
|
1031
|
+
* @param oldNodes {Node[]}
|
|
1032
|
+
* @param newNodes {Node[]}
|
|
1033
|
+
* @param before {?Node} */
|
|
1034
|
+
function reconcileNodes(parentNode, oldNodes, newNodes, before) {
|
|
1035
|
+
// 1. Remove old nodes that aren't in the new list.
|
|
1036
|
+
if (oldNodes.length) {
|
|
1037
|
+
let keep = new Set(newNodes);
|
|
1038
|
+
for (let node of oldNodes)
|
|
1039
|
+
if (!keep.has(node) && node.parentNode === parentNode)
|
|
1040
|
+
parentNode.removeChild(node);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// 2. Place new nodes in order, walking back to front so `next` is always the already-placed
|
|
1044
|
+
// node that should follow. insertBefore moves a node already in the DOM, so nodes already in
|
|
1045
|
+
// the right spot are skipped to avoid needless mutation.
|
|
1046
|
+
let next = before;
|
|
1047
|
+
for (let i=newNodes.length; i--; ) {
|
|
1048
|
+
let node = newNodes[i];
|
|
1049
|
+
if (node.nextSibling !== next || node.parentNode !== parentNode)
|
|
1050
|
+
parentNode.insertBefore(node, next);
|
|
1051
|
+
next = node;
|
|
1052
|
+
}
|
|
1053
|
+
}
|