kiru 0.44.4

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.
Files changed (70) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +5 -0
  3. package/package.json +81 -0
  4. package/src/appContext.ts +186 -0
  5. package/src/cloneVNode.ts +14 -0
  6. package/src/constants.ts +146 -0
  7. package/src/context.ts +56 -0
  8. package/src/dom.ts +712 -0
  9. package/src/element.ts +54 -0
  10. package/src/env.ts +6 -0
  11. package/src/error.ts +85 -0
  12. package/src/flags.ts +15 -0
  13. package/src/form/index.ts +662 -0
  14. package/src/form/types.ts +261 -0
  15. package/src/form/utils.ts +19 -0
  16. package/src/generateId.ts +19 -0
  17. package/src/globalContext.ts +161 -0
  18. package/src/globals.ts +21 -0
  19. package/src/hmr.ts +178 -0
  20. package/src/hooks/index.ts +14 -0
  21. package/src/hooks/useAsync.ts +136 -0
  22. package/src/hooks/useCallback.ts +31 -0
  23. package/src/hooks/useContext.ts +79 -0
  24. package/src/hooks/useEffect.ts +44 -0
  25. package/src/hooks/useEffectEvent.ts +24 -0
  26. package/src/hooks/useId.ts +42 -0
  27. package/src/hooks/useLayoutEffect.ts +47 -0
  28. package/src/hooks/useMemo.ts +33 -0
  29. package/src/hooks/useReducer.ts +50 -0
  30. package/src/hooks/useRef.ts +40 -0
  31. package/src/hooks/useState.ts +62 -0
  32. package/src/hooks/useSyncExternalStore.ts +59 -0
  33. package/src/hooks/useViewTransition.ts +26 -0
  34. package/src/hooks/utils.ts +259 -0
  35. package/src/hydration.ts +67 -0
  36. package/src/index.ts +61 -0
  37. package/src/jsx.ts +11 -0
  38. package/src/lazy.ts +238 -0
  39. package/src/memo.ts +48 -0
  40. package/src/portal.ts +43 -0
  41. package/src/profiling.ts +105 -0
  42. package/src/props.ts +36 -0
  43. package/src/reconciler.ts +531 -0
  44. package/src/renderToString.ts +91 -0
  45. package/src/router/index.ts +2 -0
  46. package/src/router/route.ts +51 -0
  47. package/src/router/router.ts +275 -0
  48. package/src/router/routerUtils.ts +49 -0
  49. package/src/scheduler.ts +522 -0
  50. package/src/signals/base.ts +237 -0
  51. package/src/signals/computed.ts +139 -0
  52. package/src/signals/effect.ts +60 -0
  53. package/src/signals/globals.ts +11 -0
  54. package/src/signals/index.ts +12 -0
  55. package/src/signals/jsx.ts +45 -0
  56. package/src/signals/types.ts +10 -0
  57. package/src/signals/utils.ts +12 -0
  58. package/src/signals/watch.ts +151 -0
  59. package/src/ssr/client.ts +29 -0
  60. package/src/ssr/hydrationBoundary.ts +63 -0
  61. package/src/ssr/index.ts +1 -0
  62. package/src/ssr/server.ts +124 -0
  63. package/src/store.ts +241 -0
  64. package/src/swr.ts +360 -0
  65. package/src/transition.ts +80 -0
  66. package/src/types.dom.ts +1250 -0
  67. package/src/types.ts +209 -0
  68. package/src/types.utils.ts +39 -0
  69. package/src/utils.ts +581 -0
  70. package/src/warning.ts +9 -0
package/src/dom.ts ADDED
@@ -0,0 +1,712 @@
1
+ import {
2
+ traverseApply,
3
+ commitSnapshot,
4
+ propFilters,
5
+ propToHtmlAttr,
6
+ postOrderApply,
7
+ } from "./utils.js"
8
+ import { booleanAttributes, FLAG, svgTags } from "./constants.js"
9
+ import { Signal, unwrap } from "./signals/index.js"
10
+ import { ctx, renderMode } from "./globals.js"
11
+ import { hydrationStack } from "./hydration.js"
12
+ import { StyleObject } from "./types.dom.js"
13
+ import { isPortal } from "./portal.js"
14
+ import { __DEV__ } from "./env.js"
15
+ import { KaiokenError } from "./error.js"
16
+ import { flags } from "./flags.js"
17
+ import type {
18
+ DomVNode,
19
+ ElementVNode,
20
+ MaybeDom,
21
+ SomeDom,
22
+ SomeElement,
23
+ } from "./types.utils"
24
+
25
+ export { commitWork, createDom, updateDom, hydrateDom }
26
+
27
+ type VNode = Kaioken.VNode
28
+ type HostNode = {
29
+ node: ElementVNode
30
+ lastChild?: DomVNode
31
+ }
32
+ type PlacementScope = {
33
+ parent: VNode
34
+ active: boolean
35
+ child?: VNode
36
+ }
37
+
38
+ function setDomRef(ref: Kaioken.Ref<SomeDom | null>, value: SomeDom | null) {
39
+ if (typeof ref === "function") {
40
+ ref(value)
41
+ return
42
+ }
43
+ if (Signal.isSignal(ref)) {
44
+ ref.sneak(value)
45
+ ref.notify({
46
+ filter: (sub) => typeof sub === "function",
47
+ })
48
+ return
49
+ }
50
+ ;(ref as Kaioken.MutableRefObject<SomeDom | null>).current = value
51
+ }
52
+
53
+ function createDom(vNode: DomVNode): SomeDom {
54
+ const t = vNode.type
55
+ const dom =
56
+ t == "#text"
57
+ ? createTextNode(vNode)
58
+ : svgTags.has(t)
59
+ ? document.createElementNS("http://www.w3.org/2000/svg", t)
60
+ : document.createElement(t)
61
+
62
+ return dom
63
+ }
64
+ function createTextNode(vNode: VNode): Text {
65
+ const prop = vNode.props.nodeValue
66
+ const value = unwrap(prop)
67
+ const textNode = document.createTextNode(value)
68
+ if (Signal.isSignal(prop)) {
69
+ subTextNode(vNode, textNode, prop)
70
+ }
71
+ return textNode
72
+ }
73
+
74
+ /**
75
+ * toggled when we fire a focus event on an element
76
+ * persist focus when the currently focused element is moved.
77
+ */
78
+ let persistingFocus = false
79
+
80
+ // gets set prior to dom commits
81
+ let currentActiveElement: Element | null = null
82
+
83
+ let didBlurActiveElement = false
84
+ const placementBlurHandler = (event: Event) => {
85
+ event.preventDefault()
86
+ event.stopPropagation()
87
+ didBlurActiveElement = true
88
+ }
89
+
90
+ function handlePrePlacementFocusPersistence() {
91
+ persistingFocus = true
92
+ currentActiveElement = document.activeElement
93
+ if (currentActiveElement && currentActiveElement !== document.body) {
94
+ currentActiveElement.addEventListener("blur", placementBlurHandler)
95
+ }
96
+ }
97
+
98
+ function handlePostPlacementFocusPersistence() {
99
+ if (!didBlurActiveElement) {
100
+ persistingFocus = false
101
+ return
102
+ }
103
+ currentActiveElement?.removeEventListener("blur", placementBlurHandler)
104
+ if (currentActiveElement?.isConnected) {
105
+ if ("focus" in currentActiveElement) (currentActiveElement as any).focus()
106
+ }
107
+ persistingFocus = false
108
+ }
109
+
110
+ function wrapFocusEventHandler(
111
+ vNode: VNode,
112
+ evtName: "focus" | "blur",
113
+ callback: (event: FocusEvent) => void
114
+ ) {
115
+ const wrappedHandlers = vNodeToWrappedFocusEventHandlersMap.get(vNode) ?? {}
116
+ const handler = (wrappedHandlers[evtName] = (event: FocusEvent) => {
117
+ if (persistingFocus) {
118
+ event.preventDefault()
119
+ event.stopPropagation()
120
+ return
121
+ }
122
+ callback(event)
123
+ })
124
+ vNodeToWrappedFocusEventHandlersMap.set(vNode, wrappedHandlers)
125
+ return handler
126
+ }
127
+
128
+ type WrappedFocusEventMap = {
129
+ focus?: (event: FocusEvent) => void
130
+ blur?: (event: FocusEvent) => void
131
+ }
132
+
133
+ const vNodeToWrappedFocusEventHandlersMap = new WeakMap<
134
+ VNode,
135
+ WrappedFocusEventMap
136
+ >()
137
+
138
+ function updateDom(vNode: VNode) {
139
+ if (isPortal(vNode)) return
140
+ const dom = vNode.dom as SomeDom
141
+ const prevProps: Record<string, any> = vNode.prev?.props ?? {}
142
+ const nextProps: Record<string, any> = vNode.props ?? {}
143
+ const keys = new Set([...Object.keys(prevProps), ...Object.keys(nextProps)])
144
+ const isHydration = renderMode.current === "hydrate"
145
+
146
+ keys.forEach((key) => {
147
+ const prev = prevProps[key],
148
+ next = nextProps[key]
149
+ if (propFilters.internalProps.includes(key) && key !== "innerHTML") {
150
+ if (key === "ref" && prev !== next) {
151
+ if (prev) {
152
+ setDomRef(prev, null)
153
+ }
154
+ if (next) {
155
+ setDomRef(next, dom)
156
+ }
157
+ }
158
+ return
159
+ }
160
+
161
+ if (propFilters.isEvent(key)) {
162
+ if (prev !== next || renderMode.current === "hydrate") {
163
+ const evtName = key.toLowerCase().substring(2)
164
+ const isFocusEvent = evtName === "focus" || evtName === "blur"
165
+ if (key in prevProps) {
166
+ dom.removeEventListener(
167
+ evtName,
168
+ isFocusEvent
169
+ ? vNodeToWrappedFocusEventHandlersMap.get(vNode)?.[evtName]
170
+ : prev
171
+ )
172
+ }
173
+ if (key in nextProps) {
174
+ dom.addEventListener(
175
+ evtName,
176
+ isFocusEvent ? wrapFocusEventHandler(vNode, evtName, next) : next
177
+ )
178
+ }
179
+ }
180
+ return
181
+ }
182
+
183
+ if (!(dom instanceof Text)) {
184
+ if (prev === next || (isHydration && dom.getAttribute(key) === next)) {
185
+ return
186
+ }
187
+
188
+ if (Signal.isSignal(prev) && vNode.cleanups) {
189
+ const v = vNode.cleanups[key]
190
+ v && (v(), delete vNode.cleanups[key])
191
+ }
192
+ if (Signal.isSignal(next)) {
193
+ return setSignalProp(vNode, dom, key, next, prev)
194
+ }
195
+ setProp(dom, key, next, prev)
196
+ return
197
+ }
198
+ if (Signal.isSignal(next)) {
199
+ // signal textNodes are handled via 'subTextNode'.
200
+ return
201
+ }
202
+ // text node
203
+ if (dom.nodeValue !== next) {
204
+ dom.nodeValue = next
205
+ }
206
+ })
207
+ }
208
+
209
+ function deriveSelectElementValue(dom: HTMLSelectElement) {
210
+ if (dom.multiple) {
211
+ return Array.from(dom.selectedOptions).map((option) => option.value)
212
+ }
213
+ return dom.value
214
+ }
215
+
216
+ function setSelectElementValue(dom: HTMLSelectElement, value: any) {
217
+ if (!dom.multiple || value === undefined || value === null || value === "") {
218
+ dom.value = value
219
+ return
220
+ }
221
+ Array.from(dom.options).forEach((option) => {
222
+ option.selected = value.indexOf(option.value) > -1
223
+ })
224
+ }
225
+
226
+ const bindAttrToEventMap: Record<string, string> = {
227
+ value: "input",
228
+ checked: "change",
229
+ open: "toggle",
230
+ volume: "volumechange",
231
+ playbackRate: "ratechange",
232
+ currentTime: "timeupdate",
233
+ }
234
+ const numericValueElements = ["progress", "meter", "number", "range"]
235
+
236
+ function setSignalProp(
237
+ vNode: VNode,
238
+ dom: Exclude<SomeDom, Text>,
239
+ key: string,
240
+ signal: Signal<any>,
241
+ prevValue: unknown
242
+ ) {
243
+ const _ctx = ctx.current
244
+ const cleanups = (vNode.cleanups ??= {})
245
+ const [modifier, attr] = key.split(":")
246
+ if (modifier !== "bind") {
247
+ cleanups[key] = signal.subscribe((value) => {
248
+ setProp(dom, key, value, null)
249
+ if (__DEV__) {
250
+ window.__kaioken?.profilingContext?.emit("signalAttrUpdate", _ctx)
251
+ }
252
+ })
253
+
254
+ return setProp(dom, key, signal.peek(), unwrap(prevValue))
255
+ }
256
+
257
+ const evtName = bindAttrToEventMap[attr]
258
+ if (!evtName) {
259
+ if (__DEV__) {
260
+ console.error(
261
+ `[kaioken]: ${attr} is not a valid element binding attribute.`
262
+ )
263
+ }
264
+ return
265
+ }
266
+
267
+ const isSelect = dom instanceof HTMLSelectElement
268
+ const setAttr = isSelect
269
+ ? (value: any) => setSelectElementValue(dom, value)
270
+ : (value: any) => ((dom as any)[attr] = value)
271
+
272
+ const signalUpdateCallback = (value: any) => {
273
+ setAttr(value)
274
+ if (__DEV__) {
275
+ window.__kaioken?.profilingContext?.emit("signalAttrUpdate", _ctx)
276
+ }
277
+ }
278
+
279
+ const setSigFromElement = (val: any) => {
280
+ signal.sneak(val)
281
+ signal.notify({ filter: (sub) => sub !== signalUpdateCallback })
282
+ }
283
+
284
+ let evtHandler: (evt: Event) => void
285
+ if (attr === "value") {
286
+ const useNumericValue =
287
+ numericValueElements.indexOf((dom as HTMLInputElement).type) !== -1
288
+ evtHandler = () => {
289
+ let val: any = (dom as HTMLInputElement | HTMLSelectElement).value
290
+ if (isSelect) {
291
+ val = deriveSelectElementValue(dom)
292
+ } else if (typeof signal.peek() === "number" && useNumericValue) {
293
+ val = (dom as HTMLInputElement).valueAsNumber
294
+ }
295
+ setSigFromElement(val)
296
+ }
297
+ } else {
298
+ evtHandler = (e: Event) => {
299
+ const val = (e.target as any)[attr]
300
+ /**
301
+ * the 'timeupdate' event is fired when the currentTime property is
302
+ * set (from code OR playback), so we need to prevent unnecessary
303
+ * signal updates to avoid a feedback loop when there are multiple
304
+ * elements with the same signal bound to 'currentTime'
305
+ */
306
+ if (attr === "currentTime" && signal.peek() === val) return
307
+ setSigFromElement(val)
308
+ }
309
+ }
310
+
311
+ dom.addEventListener(evtName, evtHandler)
312
+ const unsub = signal.subscribe(signalUpdateCallback)
313
+
314
+ cleanups[key] = () => {
315
+ dom.removeEventListener(evtName, evtHandler)
316
+ unsub()
317
+ }
318
+
319
+ return setProp(dom, attr, signal.peek(), unwrap(prevValue))
320
+ }
321
+
322
+ function subTextNode(vNode: VNode, textNode: Text, signal: Signal<string>) {
323
+ const _ctx = ctx.current
324
+ ;(vNode.cleanups ??= {}).nodeValue = signal.subscribe((v) => {
325
+ textNode.nodeValue = v
326
+ if (__DEV__) {
327
+ window.__kaioken?.profilingContext?.emit("signalTextUpdate", _ctx)
328
+ }
329
+ })
330
+ }
331
+
332
+ function hydrateDom(vNode: VNode) {
333
+ const dom = hydrationStack.nextChild()
334
+ if (!dom)
335
+ throw new KaiokenError({
336
+ message: `Hydration mismatch - no node found`,
337
+ vNode,
338
+ })
339
+ let nodeName = dom.nodeName
340
+ if (!svgTags.has(nodeName)) {
341
+ nodeName = nodeName.toLowerCase()
342
+ }
343
+ if ((vNode.type as string) !== nodeName) {
344
+ throw new KaiokenError({
345
+ message: `Hydration mismatch - expected node of type ${vNode.type.toString()} but received ${nodeName}`,
346
+ vNode,
347
+ })
348
+ }
349
+ vNode.dom = dom
350
+ if (vNode.type !== "#text") {
351
+ updateDom(vNode)
352
+ return
353
+ }
354
+ if (Signal.isSignal(vNode.props.nodeValue)) {
355
+ subTextNode(vNode, dom as Text, vNode.props.nodeValue)
356
+ }
357
+
358
+ let prev = vNode
359
+ let sibling = vNode.sibling
360
+ while (sibling && sibling.type === "#text") {
361
+ const sib = sibling
362
+ hydrationStack.bumpChildIndex()
363
+ const prevText = String(unwrap(prev.props.nodeValue) ?? "")
364
+ const dom = (prev.dom as Text).splitText(prevText.length)
365
+ sib.dom = dom
366
+ if (Signal.isSignal(sib.props.nodeValue)) {
367
+ subTextNode(sib, dom, sib.props.nodeValue)
368
+ }
369
+ prev = sibling
370
+ sibling = sibling.sibling
371
+ }
372
+ }
373
+
374
+ function handleAttributeRemoval(
375
+ element: Element,
376
+ key: string,
377
+ value: unknown,
378
+ isBoolAttr = false
379
+ ) {
380
+ if (value === null) {
381
+ element.removeAttribute(key)
382
+ return true
383
+ }
384
+ switch (typeof value) {
385
+ case "undefined":
386
+ case "function":
387
+ case "symbol": {
388
+ element.removeAttribute(key)
389
+ return true
390
+ }
391
+ case "boolean": {
392
+ if (isBoolAttr && !value) {
393
+ element.removeAttribute(key)
394
+ return true
395
+ }
396
+ }
397
+ }
398
+
399
+ return false
400
+ }
401
+
402
+ function setDomAttribute(element: Element, key: string, value: unknown) {
403
+ const isBoolAttr = booleanAttributes.has(key)
404
+
405
+ if (handleAttributeRemoval(element, key, value, isBoolAttr)) return
406
+
407
+ element.setAttribute(
408
+ key,
409
+ isBoolAttr && typeof value === "boolean" ? "" : String(value)
410
+ )
411
+ }
412
+
413
+ const explicitValueElementTags = ["INPUT", "TEXTAREA"]
414
+
415
+ const needsExplicitValueSet = (
416
+ element: SomeElement
417
+ ): element is HTMLInputElement | HTMLTextAreaElement => {
418
+ return explicitValueElementTags.indexOf(element.nodeName) > -1
419
+ }
420
+
421
+ function setProp(
422
+ element: SomeElement,
423
+ key: string,
424
+ value: unknown,
425
+ prev: unknown
426
+ ) {
427
+ if (value === prev) return
428
+ switch (key) {
429
+ case "style":
430
+ return setStyleProp(element, value, prev)
431
+ case "className":
432
+ return setClassName(element, value)
433
+ case "innerHTML":
434
+ return setInnerHTML(element, value)
435
+ case "muted":
436
+ ;(element as HTMLMediaElement).muted = Boolean(value)
437
+ return
438
+ case "value":
439
+ if (element.nodeName === "SELECT") {
440
+ return setSelectElementValue(element as HTMLSelectElement, value)
441
+ }
442
+ const strVal = value === undefined || value === null ? "" : String(value)
443
+ if (needsExplicitValueSet(element)) {
444
+ element.value = strVal
445
+ return
446
+ }
447
+ element.setAttribute("value", strVal)
448
+ return
449
+ case "checked":
450
+ if (element.nodeName === "INPUT") {
451
+ ;(element as HTMLInputElement).checked = Boolean(value)
452
+ return
453
+ }
454
+ element.setAttribute("checked", String(value))
455
+ return
456
+ default:
457
+ return setDomAttribute(element, propToHtmlAttr(key), value)
458
+ }
459
+ }
460
+
461
+ function setInnerHTML(element: SomeElement, value: unknown) {
462
+ if (value === null || value === undefined || typeof value === "boolean") {
463
+ element.innerHTML = ""
464
+ return
465
+ }
466
+ element.innerHTML = String(value)
467
+ }
468
+
469
+ function setClassName(element: SomeElement, value: unknown) {
470
+ const val = unwrap(value)
471
+ if (!val) {
472
+ return element.removeAttribute("class")
473
+ }
474
+ element.setAttribute("class", val as string)
475
+ }
476
+
477
+ function setStyleProp(element: SomeElement, value: unknown, prev: unknown) {
478
+ if (handleAttributeRemoval(element, "style", value)) return
479
+
480
+ if (typeof value === "string") {
481
+ element.setAttribute("style", value)
482
+ return
483
+ }
484
+
485
+ let prevStyle: StyleObject = {}
486
+ if (typeof prev === "string") {
487
+ element.setAttribute("style", "")
488
+ } else if (typeof prev === "object" && !!prev) {
489
+ prevStyle = prev as StyleObject
490
+ }
491
+
492
+ const nextStyle = value as StyleObject
493
+ const keys = new Set([
494
+ ...Object.keys(prevStyle),
495
+ ...Object.keys(nextStyle),
496
+ ]) as Set<keyof StyleObject>
497
+
498
+ keys.forEach((k) => {
499
+ const prev = prevStyle[k]
500
+ const next = nextStyle[k]
501
+ if (prev === next) return
502
+
503
+ if (next === undefined) {
504
+ element.style[k as any] = ""
505
+ return
506
+ }
507
+
508
+ element.style[k as any] = next as any
509
+ })
510
+ }
511
+
512
+ function getDomParent(vNode: VNode): ElementVNode {
513
+ let parentNode: VNode | null = vNode.parent
514
+ let parentNodeElement = parentNode?.dom
515
+ while (parentNode && !parentNodeElement) {
516
+ parentNode = parentNode.parent
517
+ parentNodeElement = parentNode?.dom
518
+ }
519
+
520
+ if (!parentNodeElement || !parentNode) {
521
+ // handle app entry
522
+ if (!vNode.parent && vNode.dom) {
523
+ return vNode as ElementVNode
524
+ }
525
+
526
+ throw new KaiokenError({
527
+ message: "No DOM parent found while attempting to place node.",
528
+ vNode: vNode,
529
+ })
530
+ }
531
+ return parentNode as ElementVNode
532
+ }
533
+
534
+ function placeDom(vNode: DomVNode, hostNode: HostNode) {
535
+ const { node: parentVNodeWithDom, lastChild } = hostNode
536
+ const dom = vNode.dom
537
+ if (lastChild) {
538
+ lastChild.dom.after(dom)
539
+ return
540
+ }
541
+ // TODO: we can probably skip the 'next sibling search' if we're appending
542
+ const nextSiblingDom = getNextSiblingDom(vNode, parentVNodeWithDom)
543
+ if (nextSiblingDom) {
544
+ parentVNodeWithDom.dom.insertBefore(dom, nextSiblingDom)
545
+ return
546
+ }
547
+
548
+ parentVNodeWithDom.dom.appendChild(dom)
549
+ }
550
+
551
+ function getNextSiblingDom(vNode: VNode, parent: ElementVNode): MaybeDom {
552
+ let node: VNode | null = vNode
553
+
554
+ while (node) {
555
+ let sibling = node.sibling
556
+
557
+ while (sibling) {
558
+ // Skip unmounted, to-be-placed & portal nodes
559
+ if (!flags.get(sibling.flags, FLAG.PLACEMENT) && !isPortal(sibling)) {
560
+ // Descend into the child to find host dom
561
+ const dom = findFirstHostDom(sibling)
562
+ if (dom?.isConnected) return dom
563
+ }
564
+ sibling = sibling.sibling
565
+ }
566
+
567
+ // Move up to parent — but don't escape portal boundary
568
+ node = node.parent
569
+ if (!node || isPortal(node) || node === parent) {
570
+ return
571
+ }
572
+ }
573
+
574
+ return
575
+ }
576
+
577
+ function findFirstHostDom(vNode: VNode): MaybeDom {
578
+ let node: VNode | null = vNode
579
+
580
+ while (node) {
581
+ if (node.dom) return node.dom
582
+ if (isPortal(node)) return // Don't descend into portals
583
+ node = node.child
584
+ }
585
+ return
586
+ }
587
+
588
+ function commitWork(vNode: VNode) {
589
+ if (renderMode.current === "hydrate") {
590
+ return traverseApply(vNode, commitSnapshot)
591
+ }
592
+ if (flags.get(vNode.flags, FLAG.DELETION)) {
593
+ return commitDeletion(vNode)
594
+ }
595
+ handlePrePlacementFocusPersistence()
596
+
597
+ const hostNodes: HostNode[] = []
598
+ let currentHostNode: HostNode
599
+ const placementScopes: PlacementScope[] = []
600
+ let currentPlacementScope: PlacementScope | undefined
601
+
602
+ postOrderApply(vNode, {
603
+ onDescent: (node) => {
604
+ if (node.dom) {
605
+ // collect host nodes as we go
606
+ currentHostNode = { node: node as ElementVNode }
607
+ hostNodes.push(currentHostNode)
608
+
609
+ if (node.prev && "innerHTML" in node.prev.props) {
610
+ /**
611
+ * We need to update innerHTML during descent in cases
612
+ * where we previously set innerHTML on this element but
613
+ * now we provide children. Setting innerHTML _after_
614
+ * appending children will yeet em into the abyss.
615
+ */
616
+ delete node.props.innerHTML
617
+ setInnerHTML(node.dom as SomeElement, "")
618
+ // remove innerHTML from prev to prevent our ascension pass from doing this again
619
+ delete node.prev.props.innerHTML
620
+ }
621
+
622
+ if (currentPlacementScope?.active) {
623
+ currentPlacementScope.child = node
624
+ // prevent scope applying to descendants of this element node
625
+ currentPlacementScope.active = false
626
+ }
627
+ } else if (flags.get(node.flags, FLAG.PLACEMENT)) {
628
+ currentPlacementScope = { parent: node, active: true }
629
+ placementScopes.push(currentPlacementScope)
630
+ }
631
+ },
632
+ onAscent: (node) => {
633
+ let inheritsPlacement = false
634
+ if (currentPlacementScope?.child === node) {
635
+ currentPlacementScope.active = true
636
+ inheritsPlacement = true
637
+ }
638
+ if (flags.get(node.flags, FLAG.DELETION)) {
639
+ return commitDeletion(node)
640
+ }
641
+ if (node.dom) {
642
+ if (!currentHostNode) {
643
+ currentHostNode = { node: getDomParent(node) }
644
+ hostNodes.push(currentHostNode)
645
+ }
646
+ commitDom(node as DomVNode, currentHostNode, inheritsPlacement)
647
+ }
648
+ commitSnapshot(node)
649
+ },
650
+ onBeforeAscent(node) {
651
+ if (currentPlacementScope?.parent === node) {
652
+ placementScopes.pop()
653
+ currentPlacementScope = placementScopes[placementScopes.length - 1]
654
+ }
655
+ if (currentHostNode?.node === node.parent) {
656
+ hostNodes.pop()
657
+ currentHostNode = hostNodes[hostNodes.length - 1]
658
+ }
659
+ },
660
+ })
661
+
662
+ handlePostPlacementFocusPersistence()
663
+ }
664
+
665
+ function commitDom(
666
+ vNode: DomVNode,
667
+ hostNode: HostNode,
668
+ inheritsPlacement: boolean
669
+ ) {
670
+ if (isPortal(vNode)) return
671
+ if (
672
+ inheritsPlacement ||
673
+ !vNode.dom.isConnected ||
674
+ flags.get(vNode.flags, FLAG.PLACEMENT)
675
+ ) {
676
+ placeDom(vNode, hostNode)
677
+ }
678
+ if (!vNode.prev || flags.get(vNode.flags, FLAG.UPDATE)) {
679
+ updateDom(vNode)
680
+ }
681
+ hostNode.lastChild = vNode
682
+ }
683
+
684
+ function commitDeletion(vNode: VNode) {
685
+ if (vNode === vNode.parent?.child) {
686
+ vNode.parent.child = vNode.sibling
687
+ }
688
+ traverseApply(vNode, (node) => {
689
+ const {
690
+ hooks,
691
+ subs,
692
+ cleanups,
693
+ dom,
694
+ props: { ref },
695
+ } = node
696
+ while (hooks?.length) hooks.pop()!.cleanup?.()
697
+ subs?.forEach((sub) => Signal.unsubscribe(node, sub))
698
+ if (cleanups) Object.values(cleanups).forEach((c) => c())
699
+
700
+ if (__DEV__) {
701
+ window.__kaioken?.profilingContext?.emit("removeNode", ctx.current)
702
+ }
703
+
704
+ if (dom) {
705
+ if (ref) setDomRef(ref as Kaioken.Ref<SomeDom>, null)
706
+ if (dom.isConnected && !isPortal(node)) {
707
+ dom.remove()
708
+ }
709
+ delete node.dom
710
+ }
711
+ })
712
+ }