fusee-framework 2.7.1 → 2.7.2
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/README.md +42 -24
- package/framework/core/h.js +632 -588
- package/package.json +1 -1
package/framework/core/h.js
CHANGED
|
@@ -1,589 +1,633 @@
|
|
|
1
|
-
import { effect, batch, signal, microEffect, untrack } from '../core/signal.js'
|
|
2
|
-
import { createEventHandler, registerDelegatedEvent, isDelegatedEvent } from '../core/event-delegation.js'
|
|
3
|
-
import { resolveElementProperty } from '../core/element-props.js'
|
|
4
|
-
|
|
5
|
-
export { effect as _effect, batch as _batch }
|
|
6
|
-
|
|
7
|
-
/** Reports whether a function should be evaluated as a plain getter. */
|
|
8
|
-
function isGetter(v) {
|
|
9
|
-
return typeof v === 'function' && !v.isSignal
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const SENSITIVE_ATTRS = new Set(['href', 'src', 'srcset', 'formaction', 'xlink:href', 'data'])
|
|
13
|
-
const DANGEROUS_SCHEME = /^(javascript|data|vbscript|file):/i
|
|
14
|
-
|
|
15
|
-
/** Sanitizes URL-bearing DOM properties before they reach the browser. */
|
|
16
|
-
function sanitize(name, value) {
|
|
17
|
-
if (SENSITIVE_ATTRS.has(name.toLowerCase())) {
|
|
18
|
-
const trimmed = String(value).trim()
|
|
19
|
-
if (DANGEROUS_SCHEME.test(trimmed)) {
|
|
20
|
-
console.warn(`[fusée] Blocked potential XSS on "${name}": "${value}"`)
|
|
21
|
-
return 'about:blank'
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return value
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Creates a framework node for an element and mounts its child nodes. */
|
|
28
|
-
export function h(tag, props = {}, children = [], isStatic = false) {
|
|
29
|
-
const el = document.createElement(tag)
|
|
30
|
-
const effects = []
|
|
31
|
-
|
|
32
|
-
if (!isStatic) {
|
|
33
|
-
_applyProps(el, props, effects)
|
|
34
|
-
} else {
|
|
35
|
-
_applyStaticProps(el, props)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const childNodes = _mountChildren(el, children, effects)
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
node: el,
|
|
42
|
-
effects,
|
|
43
|
-
props,
|
|
44
|
-
children: childNodes,
|
|
45
|
-
_tag: tag,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Creates a text node that optionally updates from a reactive getter. */
|
|
50
|
-
export function hText(value) {
|
|
51
|
-
const node = document.createTextNode('')
|
|
52
|
-
const effects = []
|
|
53
|
-
|
|
54
|
-
if (typeof value === 'function') {
|
|
55
|
-
const cleanup = microEffect(() => {
|
|
56
|
-
node.textContent = String(value() ?? '')
|
|
57
|
-
})
|
|
58
|
-
effects.push(cleanup)
|
|
59
|
-
} else {
|
|
60
|
-
node.textContent = String(value ?? '')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { node, effects }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Creates a reactive conditional anchored by a comment node. */
|
|
67
|
-
export function hIf(branches) {
|
|
68
|
-
const anchor = document.createComment('f-if')
|
|
69
|
-
const effects = []
|
|
70
|
-
|
|
71
|
-
let currentNodes = []
|
|
72
|
-
let currentBranch = -1
|
|
73
|
-
const mounted = _createMountTracker()
|
|
74
|
-
|
|
75
|
-
const cleanup = effect(() => {
|
|
76
|
-
mounted.track()
|
|
77
|
-
let matchIdx = -1
|
|
78
|
-
for (let i = 0; i < branches.length; i++) {
|
|
79
|
-
const [cond] = branches[i]
|
|
80
|
-
if (cond === null || cond()) { matchIdx = i; break }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const parent = anchor.parentNode
|
|
84
|
-
if (!parent) return mounted.defer()
|
|
85
|
-
|
|
86
|
-
if (matchIdx === currentBranch) return
|
|
87
|
-
|
|
88
|
-
_unmountNodes(currentNodes)
|
|
89
|
-
currentNodes = []
|
|
90
|
-
currentBranch = matchIdx
|
|
91
|
-
|
|
92
|
-
if (matchIdx === -1) return
|
|
93
|
-
|
|
94
|
-
const [, getChildren] = branches[matchIdx]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
const
|
|
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
|
-
continue
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
continue
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
function
|
|
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
|
-
el
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
value
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1
|
+
import { effect, batch, signal, microEffect, untrack, runDetached } from '../core/signal.js'
|
|
2
|
+
import { createEventHandler, registerDelegatedEvent, isDelegatedEvent } from '../core/event-delegation.js'
|
|
3
|
+
import { resolveElementProperty } from '../core/element-props.js'
|
|
4
|
+
|
|
5
|
+
export { effect as _effect, batch as _batch }
|
|
6
|
+
|
|
7
|
+
/** Reports whether a function should be evaluated as a plain getter. */
|
|
8
|
+
function isGetter(v) {
|
|
9
|
+
return typeof v === 'function' && !v.isSignal
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SENSITIVE_ATTRS = new Set(['href', 'src', 'srcset', 'formaction', 'xlink:href', 'data'])
|
|
13
|
+
const DANGEROUS_SCHEME = /^(javascript|data|vbscript|file):/i
|
|
14
|
+
|
|
15
|
+
/** Sanitizes URL-bearing DOM properties before they reach the browser. */
|
|
16
|
+
function sanitize(name, value) {
|
|
17
|
+
if (SENSITIVE_ATTRS.has(name.toLowerCase())) {
|
|
18
|
+
const trimmed = String(value).trim()
|
|
19
|
+
if (DANGEROUS_SCHEME.test(trimmed)) {
|
|
20
|
+
console.warn(`[fusée] Blocked potential XSS on "${name}": "${value}"`)
|
|
21
|
+
return 'about:blank'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Creates a framework node for an element and mounts its child nodes. */
|
|
28
|
+
export function h(tag, props = {}, children = [], isStatic = false) {
|
|
29
|
+
const el = document.createElement(tag)
|
|
30
|
+
const effects = []
|
|
31
|
+
|
|
32
|
+
if (!isStatic) {
|
|
33
|
+
_applyProps(el, props, effects)
|
|
34
|
+
} else {
|
|
35
|
+
_applyStaticProps(el, props)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const childNodes = _mountChildren(el, children, effects)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
node: el,
|
|
42
|
+
effects,
|
|
43
|
+
props,
|
|
44
|
+
children: childNodes,
|
|
45
|
+
_tag: tag,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Creates a text node that optionally updates from a reactive getter. */
|
|
50
|
+
export function hText(value) {
|
|
51
|
+
const node = document.createTextNode('')
|
|
52
|
+
const effects = []
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'function') {
|
|
55
|
+
const cleanup = microEffect(() => {
|
|
56
|
+
node.textContent = String(value() ?? '')
|
|
57
|
+
})
|
|
58
|
+
effects.push(cleanup)
|
|
59
|
+
} else {
|
|
60
|
+
node.textContent = String(value ?? '')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { node, effects }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Creates a reactive conditional anchored by a comment node. */
|
|
67
|
+
export function hIf(branches) {
|
|
68
|
+
const anchor = document.createComment('f-if')
|
|
69
|
+
const effects = []
|
|
70
|
+
|
|
71
|
+
let currentNodes = []
|
|
72
|
+
let currentBranch = -1
|
|
73
|
+
const mounted = _createMountTracker()
|
|
74
|
+
|
|
75
|
+
const cleanup = effect(() => {
|
|
76
|
+
mounted.track()
|
|
77
|
+
let matchIdx = -1
|
|
78
|
+
for (let i = 0; i < branches.length; i++) {
|
|
79
|
+
const [cond] = branches[i]
|
|
80
|
+
if (cond === null || cond()) { matchIdx = i; break }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parent = anchor.parentNode
|
|
84
|
+
if (!parent) return mounted.defer()
|
|
85
|
+
|
|
86
|
+
if (matchIdx === currentBranch) return
|
|
87
|
+
|
|
88
|
+
_unmountNodes(currentNodes)
|
|
89
|
+
currentNodes = []
|
|
90
|
+
currentBranch = matchIdx
|
|
91
|
+
|
|
92
|
+
if (matchIdx === -1) return
|
|
93
|
+
|
|
94
|
+
const [, getChildren] = branches[matchIdx]
|
|
95
|
+
// Detached: branch effects belong to the branch, which is unmounted explicitly below.
|
|
96
|
+
const children = runDetached(getChildren)
|
|
97
|
+
|
|
98
|
+
for (const fnode of children) {
|
|
99
|
+
if (!fnode?.node) continue
|
|
100
|
+
insertFNode(parent, fnode, anchor)
|
|
101
|
+
currentNodes.push(fnode)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
effects.push(() => {
|
|
106
|
+
cleanup()
|
|
107
|
+
_unmountNodes(currentNodes)
|
|
108
|
+
currentNodes = []
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
node: anchor,
|
|
113
|
+
effects,
|
|
114
|
+
_ifBranchNodes: () => currentNodes,
|
|
115
|
+
_isIf: true,
|
|
116
|
+
_afterInsert: mounted.resume,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Creates a keyed reactive list anchored by a comment node. */
|
|
121
|
+
export function hFor(sourceGetter, renderItem, keyFn) {
|
|
122
|
+
const anchor = document.createComment('f-for')
|
|
123
|
+
const effects = []
|
|
124
|
+
|
|
125
|
+
let keyedMap = new Map()
|
|
126
|
+
let oldKeys = []
|
|
127
|
+
let _keyEvalCurrent = null
|
|
128
|
+
const keyEvaluatorGetter = () => _keyEvalCurrent
|
|
129
|
+
keyEvaluatorGetter.isSignal = true
|
|
130
|
+
const mounted = _createMountTracker()
|
|
131
|
+
|
|
132
|
+
const cleanup = effect(() => {
|
|
133
|
+
mounted.track()
|
|
134
|
+
const list = sourceGetter() ?? []
|
|
135
|
+
const parent = anchor.parentNode
|
|
136
|
+
|
|
137
|
+
if (!parent) return mounted.defer()
|
|
138
|
+
|
|
139
|
+
const newKeys = new Array(list.length)
|
|
140
|
+
for (let i = 0; i < list.length; i++) {
|
|
141
|
+
_keyEvalCurrent = list[i]
|
|
142
|
+
const key = keyFn(keyEvaluatorGetter, i)
|
|
143
|
+
// Loops without :key compile to a key of undefined; key them by position, as SSR does.
|
|
144
|
+
newKeys[i] = key === undefined ? i : key
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (newKeys.length === 0 && keyedMap.size > 0) {
|
|
148
|
+
_clearRows(parent, keyedMap)
|
|
149
|
+
keyedMap = new Map()
|
|
150
|
+
oldKeys = newKeys
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (oldKeys.length === newKeys.length) {
|
|
155
|
+
let identical = true
|
|
156
|
+
for (let i = 0; i < newKeys.length; i++) {
|
|
157
|
+
if (oldKeys[i] !== newKeys[i]) { identical = false; break }
|
|
158
|
+
}
|
|
159
|
+
if (identical) {
|
|
160
|
+
for (let i = 0; i < list.length; i++) {
|
|
161
|
+
_setItem(keyedMap.get(newKeys[i]), list[i])
|
|
162
|
+
}
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (oldKeys.length === newKeys.length) {
|
|
168
|
+
let diffA = -1, diffB = -1, diffCount = 0
|
|
169
|
+
for (let i = 0; i < newKeys.length; i++) {
|
|
170
|
+
if (oldKeys[i] !== newKeys[i]) {
|
|
171
|
+
if (diffCount === 0) {
|
|
172
|
+
diffA = i
|
|
173
|
+
} else if (diffCount === 1) {
|
|
174
|
+
diffB = i
|
|
175
|
+
}
|
|
176
|
+
diffCount++
|
|
177
|
+
if (diffCount > 2)
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (diffCount === 2 && oldKeys[diffA] === newKeys[diffB] && oldKeys[diffB] === newKeys[diffA]) {
|
|
182
|
+
const entryA = keyedMap.get(oldKeys[diffA])
|
|
183
|
+
const entryB = keyedMap.get(oldKeys[diffB])
|
|
184
|
+
const nodeA = entryA.fnode.node
|
|
185
|
+
const nodeB = entryB.fnode.node
|
|
186
|
+
const refA = nodeA.nextSibling
|
|
187
|
+
const parentEl = nodeA.parentNode
|
|
188
|
+
parentEl.insertBefore(nodeA, nodeB.nextSibling)
|
|
189
|
+
parentEl.insertBefore(nodeB, refA)
|
|
190
|
+
// The two rows traded places: each keeps its key and receives the item now at its new index.
|
|
191
|
+
_setItem(entryA, list[diffB])
|
|
192
|
+
_setItem(entryB, list[diffA])
|
|
193
|
+
oldKeys = newKeys
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const newMap = new Map()
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < list.length; i++) {
|
|
201
|
+
const item = list[i]
|
|
202
|
+
const key = newKeys[i]
|
|
203
|
+
|
|
204
|
+
const exists = keyedMap.get(key)
|
|
205
|
+
if (exists) {
|
|
206
|
+
_setItem(exists, item)
|
|
207
|
+
newMap.set(key, exists)
|
|
208
|
+
} else {
|
|
209
|
+
const itemSignal = signal(item)
|
|
210
|
+
// Detached: otherwise every row's effects are registered as children of this
|
|
211
|
+
// list effect and stay reachable after the row is removed.
|
|
212
|
+
const fnode = runDetached(() => renderItem(itemSignal, i))
|
|
213
|
+
newMap.set(key, { fnode, key, itemSignal })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const [key, entry] of keyedMap) {
|
|
218
|
+
if (!newMap.has(key)) {
|
|
219
|
+
_unmountNodes([entry.fnode])
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const newEntries = newKeys.map(k => newMap.get(k))
|
|
224
|
+
const oldIndexOf = new Map()
|
|
225
|
+
oldKeys.forEach((k, i) => oldIndexOf.set(k, i))
|
|
226
|
+
|
|
227
|
+
const posMap = newEntries.map(entry => {
|
|
228
|
+
const idx = oldIndexOf.get(entry.key)
|
|
229
|
+
return idx === undefined ? -1 : idx
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const lisIndices = _getLIS(posMap)
|
|
233
|
+
let lisPtr = lisIndices.length - 1
|
|
234
|
+
|
|
235
|
+
for (let i = newEntries.length - 1; i >= 0; i--) {
|
|
236
|
+
const entry = newEntries[i]
|
|
237
|
+
const ref = i === newEntries.length - 1 ? anchor : newEntries[i + 1].fnode.node
|
|
238
|
+
const inLis = lisPtr >= 0 && i === lisIndices[lisPtr]
|
|
239
|
+
|
|
240
|
+
if (inLis) {
|
|
241
|
+
lisPtr--
|
|
242
|
+
if (!entry.fnode.node.parentNode) {
|
|
243
|
+
insertFNode(parent, entry.fnode, ref)
|
|
244
|
+
}
|
|
245
|
+
} else if (entry.fnode.node.nextSibling !== ref || !entry.fnode.node.parentNode) {
|
|
246
|
+
insertFNode(parent, entry.fnode, ref)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
keyedMap = newMap
|
|
251
|
+
oldKeys = newKeys
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
effects.push(() => {
|
|
255
|
+
cleanup()
|
|
256
|
+
_unmountNodes([...keyedMap.values()].map(e => e.fnode))
|
|
257
|
+
keyedMap = new Map()
|
|
258
|
+
oldKeys = []
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
node: anchor,
|
|
263
|
+
effects,
|
|
264
|
+
_isFor: true,
|
|
265
|
+
_getNodes: () => [...keyedMap.values()].map(e => e.fnode),
|
|
266
|
+
_afterInsert: mounted.resume,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Unmounts every row. When the rows are the only elements in the parent, the parent is emptied
|
|
272
|
+
* in one operation instead of one removeChild per row, and its other nodes (the anchor, template
|
|
273
|
+
* whitespace, text) are put back in order.
|
|
274
|
+
*/
|
|
275
|
+
function _clearRows(parent, keyedMap) {
|
|
276
|
+
const fnodes = [...keyedMap.values()].map(e => e.fnode)
|
|
277
|
+
const rowsAreElements = fnodes.every(f => f.node.nodeType === 1)
|
|
278
|
+
|
|
279
|
+
if (rowsAreElements && parent.childElementCount === fnodes.length) {
|
|
280
|
+
const kept = []
|
|
281
|
+
for (let n = parent.firstChild; n; n = n.nextSibling) {
|
|
282
|
+
if (n.nodeType !== 1) kept.push(n)
|
|
283
|
+
}
|
|
284
|
+
for (const fnode of fnodes) _disposeEffects(fnode)
|
|
285
|
+
parent.textContent = ''
|
|
286
|
+
parent.append(...kept)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
_unmountNodes(fnodes)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Updates a keyed row's item signal without subscribing the list effect to it. */
|
|
293
|
+
function _setItem(entry, item) {
|
|
294
|
+
if (untrack(entry.itemSignal) !== item) entry.itemSignal(item)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* hIf/hFor are built before their anchor has a parent (children are created before the
|
|
299
|
+
* element that holds them), so their first render waits until insertFNode() places the
|
|
300
|
+
* anchor and calls _afterInsert.
|
|
301
|
+
*/
|
|
302
|
+
function _createMountTracker() {
|
|
303
|
+
const tick = signal(0)
|
|
304
|
+
let pending = false
|
|
305
|
+
return {
|
|
306
|
+
track: () => tick(),
|
|
307
|
+
defer: () => { pending = true },
|
|
308
|
+
resume: () => {
|
|
309
|
+
if (!pending) return
|
|
310
|
+
pending = false
|
|
311
|
+
tick(untrack(tick) + 1)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Inserts a framework node (and slot nodes it carries) before ref, then lets anchor-based
|
|
318
|
+
* nodes (hIf/hFor) render now that they have a parent.
|
|
319
|
+
*/
|
|
320
|
+
export function insertFNode(parent, fnode, ref = null) {
|
|
321
|
+
parent.insertBefore(fnode.node, ref)
|
|
322
|
+
if (fnode._isSlot && fnode._slotNodes) {
|
|
323
|
+
for (const sn of fnode._slotNodes) {
|
|
324
|
+
if (sn?.node) insertFNode(parent, sn, ref)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (typeof fnode._afterInsert === 'function') fnode._afterInsert()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function _getLIS(arr) {
|
|
331
|
+
const p = arr.slice()
|
|
332
|
+
const result = []
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < arr.length; i++) {
|
|
335
|
+
if (arr[i] === -1) continue
|
|
336
|
+
|
|
337
|
+
if (result.length === 0 || arr[result[result.length - 1]] < arr[i]) {
|
|
338
|
+
p[i] = result.length ? result[result.length - 1] : -1
|
|
339
|
+
result.push(i)
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let lo = 0, hi = result.length - 1
|
|
344
|
+
while (lo < hi) {
|
|
345
|
+
const mid = (lo + hi) >> 1
|
|
346
|
+
if (arr[result[mid]] < arr[i]) lo = mid + 1
|
|
347
|
+
else hi = mid
|
|
348
|
+
}
|
|
349
|
+
if (arr[i] < arr[result[lo]]) {
|
|
350
|
+
p[i] = lo > 0 ? result[lo - 1] : -1
|
|
351
|
+
result[lo] = i
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let u = result.length
|
|
356
|
+
let v = u > 0 ? result[u - 1] : -1
|
|
357
|
+
const seq = new Array(u)
|
|
358
|
+
while (u-- > 0) {
|
|
359
|
+
seq[u] = v
|
|
360
|
+
v = p[v]
|
|
361
|
+
}
|
|
362
|
+
return seq
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Marks a slots object whose outlets must stay native `<slot>` elements (shadow DOM). */
|
|
366
|
+
export const NATIVE_SLOTS = Symbol.for('fusee.nativeSlots')
|
|
367
|
+
|
|
368
|
+
export function hSlot(slots, name, fallback = []) {
|
|
369
|
+
if (slots?.[NATIVE_SLOTS]) {
|
|
370
|
+
return h('slot', name === 'default' ? {} : { name }, fallback)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const anchor = document.createComment(`slot:${name}`)
|
|
374
|
+
const effects = []
|
|
375
|
+
const slotFn = slots?.[name]
|
|
376
|
+
const nodes = typeof slotFn === 'function' ? slotFn() : fallback
|
|
377
|
+
return {
|
|
378
|
+
node: anchor,
|
|
379
|
+
effects,
|
|
380
|
+
_slotNodes: nodes,
|
|
381
|
+
_isSlot: true,
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function createComponent(name, ComponentFn, rawProps = {}, rawSlots = {}) {
|
|
386
|
+
if (!ComponentFn) {
|
|
387
|
+
console.warn(`[fusée] Component "${name}" is not registered`)
|
|
388
|
+
const placeholder = document.createComment(`missing:${name}`)
|
|
389
|
+
return { node: placeholder, effects: [] }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const resolvedProps = _resolveComponentProps(rawProps)
|
|
393
|
+
const listeners = {}
|
|
394
|
+
const slots = {}
|
|
395
|
+
|
|
396
|
+
for (const [key, value] of Object.entries(rawProps)) {
|
|
397
|
+
if (key.startsWith('on:')) {
|
|
398
|
+
listeners[key.slice(3)] = value
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const [slotName, slotFn] of Object.entries(rawSlots)) {
|
|
403
|
+
slots[slotName] = slotFn
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const container = document.createElement('div')
|
|
407
|
+
container.setAttribute('data-fusee-component', name)
|
|
408
|
+
|
|
409
|
+
const api = ComponentFn(resolvedProps, { listeners, slots })
|
|
410
|
+
api.render(container)
|
|
411
|
+
const root = container.firstElementChild ?? container
|
|
412
|
+
|
|
413
|
+
const effects = []
|
|
414
|
+
effects.push(() => api.unmount())
|
|
415
|
+
|
|
416
|
+
return { node: root, effects, _component: api }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function _applyProps(el, props, effects) {
|
|
420
|
+
for (const key in props) {
|
|
421
|
+
const value = props[key]
|
|
422
|
+
if (key.startsWith('@')) {
|
|
423
|
+
const eventName = key.slice(1)
|
|
424
|
+
const { handler, modifiers = [] } = value
|
|
425
|
+
const handlerState = { timeoutId: null, throttleTimeoutId: null, lastRun: 0 }
|
|
426
|
+
const wrappedHandler = createEventHandler(handler, modifiers, null, null, handlerState)
|
|
427
|
+
|
|
428
|
+
let cleanup
|
|
429
|
+
if (isDelegatedEvent(eventName)) {
|
|
430
|
+
cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, { modifiers, handlerState })
|
|
431
|
+
} else {
|
|
432
|
+
el.addEventListener(eventName, wrappedHandler)
|
|
433
|
+
cleanup = () => el.removeEventListener(eventName, wrappedHandler)
|
|
434
|
+
}
|
|
435
|
+
effects.push(cleanup)
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (key === 'f-show') {
|
|
440
|
+
const cleanup = effect(() => {
|
|
441
|
+
el.style.display = value() ? '' : 'none'
|
|
442
|
+
})
|
|
443
|
+
effects.push(cleanup)
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (key === 'f-html') {
|
|
448
|
+
const cleanup = effect(() => {
|
|
449
|
+
el.innerHTML = String(value() ?? '')
|
|
450
|
+
})
|
|
451
|
+
effects.push(cleanup)
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (key === 'f-model') {
|
|
456
|
+
const sig = value()
|
|
457
|
+
const cleanup = effect(() => {
|
|
458
|
+
const v = typeof sig === 'function' ? sig() : sig
|
|
459
|
+
if (el.value !== String(v ?? '')) el.value = String(v ?? '')
|
|
460
|
+
})
|
|
461
|
+
effects.push(cleanup)
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (key === 'f-ref') {
|
|
466
|
+
el.dispatchEvent(new CustomEvent('fusee:ref', {
|
|
467
|
+
detail: { name: value },
|
|
468
|
+
bubbles: true,
|
|
469
|
+
}))
|
|
470
|
+
continue
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (key === 'f-once') continue
|
|
474
|
+
|
|
475
|
+
if (typeof value !== 'function') {
|
|
476
|
+
_applyAttr(el, key, value)
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const attrName = key
|
|
481
|
+
const getter = value
|
|
482
|
+
const cleanup = effect(() => {
|
|
483
|
+
_applyAttr(el, attrName, getter())
|
|
484
|
+
})
|
|
485
|
+
effects.push(cleanup)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function _applyStaticProps(el, props) {
|
|
490
|
+
for (const key in props) {
|
|
491
|
+
const value = props[key]
|
|
492
|
+
if (key.startsWith('@') || key.startsWith('f-'))
|
|
493
|
+
continue
|
|
494
|
+
if (typeof value !== 'function')
|
|
495
|
+
_applyAttr(el, key, value)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function _applyAttr(el, name, value) {
|
|
500
|
+
if (name === 'class') {
|
|
501
|
+
const cls = _resolveClass(value)
|
|
502
|
+
if (cls) el.className = cls
|
|
503
|
+
else el.removeAttribute('class')
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (name === 'style') {
|
|
508
|
+
if (value !== null && typeof value === 'object') {
|
|
509
|
+
for (const k in value) el.style[k] = value[k]
|
|
510
|
+
} else {
|
|
511
|
+
el.style.cssText = String(value ?? '')
|
|
512
|
+
}
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const elementProp = resolveElementProperty(el, name, value)
|
|
517
|
+
if (elementProp) {
|
|
518
|
+
el[elementProp] = value
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (name in el && typeof el[name] !== 'undefined' && name !== 'type') {
|
|
523
|
+
try {
|
|
524
|
+
el[name] = value
|
|
525
|
+
return
|
|
526
|
+
} catch (_) {
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (typeof value === 'boolean') {
|
|
532
|
+
value ? el.setAttribute(name, '') : el.removeAttribute(name)
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (value == null) {
|
|
537
|
+
el.removeAttribute(name)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
el.setAttribute(name, sanitize(name, String(value)))
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function _resolveClass(value) {
|
|
545
|
+
if (typeof value === 'string')
|
|
546
|
+
return value
|
|
547
|
+
if (Array.isArray(value))
|
|
548
|
+
return value.filter(Boolean).join(' ')
|
|
549
|
+
if (value && typeof value === 'object')
|
|
550
|
+
return Object.entries(value).filter(([, v]) => !!v).map(([k]) => k).join(' ')
|
|
551
|
+
return String(value ?? '')
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function _mountChildren(parent, children, parentEffects) {
|
|
555
|
+
const mounted = []
|
|
556
|
+
|
|
557
|
+
for (const fnode of children) {
|
|
558
|
+
if (!fnode || !fnode.node) continue
|
|
559
|
+
|
|
560
|
+
insertFNode(parent, fnode)
|
|
561
|
+
|
|
562
|
+
if (fnode.effects) parentEffects.push(...fnode.effects)
|
|
563
|
+
mounted.push(fnode)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return mounted
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function _disposeEffects(fnode) {
|
|
570
|
+
if (!fnode.effects) return
|
|
571
|
+
for (let i = fnode.effects.length - 1; i >= 0; i--) {
|
|
572
|
+
const cleanup = fnode.effects[i]
|
|
573
|
+
if (typeof cleanup === 'function') cleanup()
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function _unmountNodes(fnodes) {
|
|
578
|
+
for (const fnode of fnodes) {
|
|
579
|
+
if (!fnode) continue
|
|
580
|
+
|
|
581
|
+
_disposeEffects(fnode)
|
|
582
|
+
|
|
583
|
+
if (fnode.node?.parentNode) {
|
|
584
|
+
fnode.node.parentNode.removeChild(fnode.node)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (fnode._ifBranchNodes) {
|
|
588
|
+
_unmountNodes(fnode._ifBranchNodes())
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (fnode._getNodes) {
|
|
592
|
+
_unmountNodes(fnode._getNodes())
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function _resolveComponentProps(rawProps) {
|
|
598
|
+
const resolved = {}
|
|
599
|
+
|
|
600
|
+
for (const [key, value] of Object.entries(rawProps)) {
|
|
601
|
+
if (key.startsWith('on:')) continue
|
|
602
|
+
|
|
603
|
+
if (isGetter(value)) {
|
|
604
|
+
Object.defineProperty(resolved, key, {
|
|
605
|
+
get: value,
|
|
606
|
+
enumerable: true,
|
|
607
|
+
configurable: true,
|
|
608
|
+
})
|
|
609
|
+
} else {
|
|
610
|
+
resolved[key] = value
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return resolved
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function mount(renderFn, ctx, components, container) {
|
|
618
|
+
const fnodes = renderFn(ctx, components)
|
|
619
|
+
const effects = []
|
|
620
|
+
|
|
621
|
+
for (const fnode of fnodes) {
|
|
622
|
+
if (!fnode?.node) continue
|
|
623
|
+
insertFNode(container, fnode)
|
|
624
|
+
if (fnode.effects) effects.push(...fnode.effects)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
unmount() {
|
|
629
|
+
_unmountNodes(fnodes)
|
|
630
|
+
container.innerHTML = ''
|
|
631
|
+
}
|
|
632
|
+
}
|
|
589
633
|
}
|