kiru 1.0.1 → 1.1.1

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 (145) hide show
  1. package/dist/appHandle.js +2 -2
  2. package/dist/appHandle.js.map +1 -1
  3. package/dist/components/lazy.d.ts.map +1 -1
  4. package/dist/components/lazy.js +2 -2
  5. package/dist/components/lazy.js.map +1 -1
  6. package/dist/components/transition.js +1 -5
  7. package/dist/components/transition.js.map +1 -1
  8. package/dist/devtools.d.ts.map +1 -1
  9. package/dist/devtools.js +6 -2
  10. package/dist/devtools.js.map +1 -1
  11. package/dist/dom/commit.d.ts +5 -0
  12. package/dist/dom/commit.d.ts.map +1 -0
  13. package/dist/dom/commit.js +94 -0
  14. package/dist/dom/commit.js.map +1 -0
  15. package/dist/dom/focus.d.ts +4 -0
  16. package/dist/dom/focus.d.ts.map +1 -0
  17. package/dist/dom/focus.js +32 -0
  18. package/dist/dom/focus.js.map +1 -0
  19. package/dist/dom/index.d.ts +4 -0
  20. package/dist/dom/index.d.ts.map +1 -0
  21. package/dist/dom/index.js +4 -0
  22. package/dist/dom/index.js.map +1 -0
  23. package/dist/dom/nodes.d.ts +12 -0
  24. package/dist/dom/nodes.d.ts.map +1 -0
  25. package/dist/dom/nodes.js +166 -0
  26. package/dist/dom/nodes.js.map +1 -0
  27. package/dist/dom/props.d.ts +8 -0
  28. package/dist/dom/props.d.ts.map +1 -0
  29. package/dist/dom/props.js +675 -0
  30. package/dist/dom/props.js.map +1 -0
  31. package/dist/env.d.ts +2 -0
  32. package/dist/env.d.ts.map +1 -1
  33. package/dist/env.js +2 -0
  34. package/dist/env.js.map +1 -1
  35. package/dist/globalContext.d.ts +3 -8
  36. package/dist/globalContext.d.ts.map +1 -1
  37. package/dist/globalContext.js +4 -16
  38. package/dist/globalContext.js.map +1 -1
  39. package/dist/globals.d.ts +21 -1
  40. package/dist/globals.d.ts.map +1 -1
  41. package/dist/globals.js +22 -2
  42. package/dist/globals.js.map +1 -1
  43. package/dist/hmr.d.ts +17 -2
  44. package/dist/hmr.d.ts.map +1 -1
  45. package/dist/hmr.js +31 -5
  46. package/dist/hmr.js.map +1 -1
  47. package/dist/hooks/index.d.ts +1 -0
  48. package/dist/hooks/index.d.ts.map +1 -1
  49. package/dist/hooks/index.js +1 -0
  50. package/dist/hooks/index.js.map +1 -1
  51. package/dist/hooks/onBeforeMount.d.ts +1 -1
  52. package/dist/hooks/onBeforeMount.d.ts.map +1 -1
  53. package/dist/hooks/onBeforeMount.js +10 -3
  54. package/dist/hooks/onBeforeMount.js.map +1 -1
  55. package/dist/hooks/onCleanup.d.ts +1 -1
  56. package/dist/hooks/onCleanup.d.ts.map +1 -1
  57. package/dist/hooks/onCleanup.js +7 -4
  58. package/dist/hooks/onCleanup.js.map +1 -1
  59. package/dist/hooks/onMount.d.ts +2 -2
  60. package/dist/hooks/onMount.d.ts.map +1 -1
  61. package/dist/hooks/onMount.js +11 -4
  62. package/dist/hooks/onMount.js.map +1 -1
  63. package/dist/hooks/setup.d.ts +13 -0
  64. package/dist/hooks/setup.d.ts.map +1 -0
  65. package/dist/hooks/setup.js +54 -0
  66. package/dist/hooks/setup.js.map +1 -0
  67. package/dist/hooks/utils.d.ts +2 -3
  68. package/dist/hooks/utils.d.ts.map +1 -1
  69. package/dist/hooks/utils.js +9 -14
  70. package/dist/hooks/utils.js.map +1 -1
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +4 -3
  74. package/dist/index.js.map +1 -1
  75. package/dist/reconciler.js +3 -3
  76. package/dist/reconciler.js.map +1 -1
  77. package/dist/router/head.js +2 -2
  78. package/dist/router/head.js.map +1 -1
  79. package/dist/router/pageConfig.js +2 -2
  80. package/dist/router/pageConfig.js.map +1 -1
  81. package/dist/scheduler.js +62 -57
  82. package/dist/scheduler.js.map +1 -1
  83. package/dist/signals/base.js +3 -3
  84. package/dist/signals/base.js.map +1 -1
  85. package/dist/signals/effect.d.ts.map +1 -1
  86. package/dist/signals/effect.js +6 -6
  87. package/dist/signals/effect.js.map +1 -1
  88. package/dist/signals/tracking.d.ts +3 -2
  89. package/dist/signals/tracking.d.ts.map +1 -1
  90. package/dist/signals/tracking.js.map +1 -1
  91. package/dist/statefulPromise.js +2 -2
  92. package/dist/statefulPromise.js.map +1 -1
  93. package/dist/types.d.ts +5 -1
  94. package/dist/types.d.ts.map +1 -1
  95. package/dist/types.dom.d.ts +1 -1
  96. package/dist/types.dom.d.ts.map +1 -1
  97. package/dist/utils/format.d.ts.map +1 -1
  98. package/dist/utils/format.js +4 -1
  99. package/dist/utils/format.js.map +1 -1
  100. package/dist/utils/vdom.d.ts +2 -2
  101. package/dist/utils/vdom.d.ts.map +1 -1
  102. package/dist/utils/vdom.js +2 -2
  103. package/dist/utils/vdom.js.map +1 -1
  104. package/dist/viewTransitions.d.ts.map +1 -1
  105. package/dist/viewTransitions.js +2 -1
  106. package/dist/viewTransitions.js.map +1 -1
  107. package/package.json +1 -1
  108. package/src/appHandle.ts +2 -2
  109. package/src/components/lazy.ts +5 -6
  110. package/src/components/transition.ts +2 -6
  111. package/src/devtools.ts +4 -2
  112. package/src/dom/commit.ts +133 -0
  113. package/src/dom/focus.ts +34 -0
  114. package/src/dom/index.ts +3 -0
  115. package/src/dom/nodes.ts +205 -0
  116. package/src/dom/props.ts +818 -0
  117. package/src/env.ts +3 -0
  118. package/src/globalContext.ts +7 -24
  119. package/src/globals.ts +25 -2
  120. package/src/hmr.ts +32 -5
  121. package/src/hooks/index.ts +1 -0
  122. package/src/hooks/onBeforeMount.ts +9 -3
  123. package/src/hooks/onCleanup.ts +10 -4
  124. package/src/hooks/onMount.ts +10 -4
  125. package/src/hooks/setup.ts +70 -0
  126. package/src/hooks/utils.ts +14 -19
  127. package/src/index.ts +4 -2
  128. package/src/reconciler.ts +3 -3
  129. package/src/router/head.ts +2 -2
  130. package/src/router/pageConfig.ts +2 -2
  131. package/src/scheduler.ts +79 -64
  132. package/src/signals/base.ts +3 -3
  133. package/src/signals/effect.ts +5 -7
  134. package/src/signals/tracking.ts +3 -2
  135. package/src/statefulPromise.ts +2 -2
  136. package/src/types.dom.ts +2 -2
  137. package/src/types.ts +7 -1
  138. package/src/utils/format.ts +3 -1
  139. package/src/utils/vdom.ts +2 -2
  140. package/src/viewTransitions.ts +2 -1
  141. package/dist/dom.d.ts +0 -10
  142. package/dist/dom.d.ts.map +0 -1
  143. package/dist/dom.js +0 -601
  144. package/dist/dom.js.map +0 -1
  145. package/src/dom.ts +0 -775
package/src/dom.ts DELETED
@@ -1,775 +0,0 @@
1
- import {
2
- traverseApply,
3
- commitSnapshot,
4
- propFilters,
5
- propToHtmlAttr,
6
- getVNodeApp,
7
- setRef,
8
- isValidTextChild,
9
- registerVNodeCleanup,
10
- call,
11
- } from "./utils/index.js"
12
- import {
13
- booleanAttributes,
14
- FLAG_PLACEMENT,
15
- FLAG_UPDATE,
16
- FLAG_STATIC_DOM,
17
- svgTags,
18
- EVENT_PREFIX_REGEX,
19
- } from "./constants.js"
20
- import { Signal } from "./signals/base.js"
21
- import { unwrap } from "./signals/utils.js"
22
- import { renderMode } from "./globals.js"
23
- import { hydrationStack } from "./hydration.js"
24
- import { StyleObject } from "./types.dom.js"
25
- import { __DEV__ } from "./env.js"
26
- import { KiruError } from "./error.js"
27
- import type {
28
- DomVNode,
29
- ElementVNode,
30
- MaybeDom,
31
- SomeDom,
32
- SomeElement,
33
- } from "./types.utils"
34
- import type { AppHandle } from "./appHandle.js"
35
-
36
- export {
37
- commitWork,
38
- onBeforeFlushDomChanges,
39
- onAfterFlushDomChanges,
40
- commitDeletion,
41
- createDom,
42
- hydrateDom,
43
- }
44
-
45
- type VNode = Kiru.VNode
46
- type HostNode = {
47
- node: ElementVNode
48
- lastChild?: SomeDom
49
- }
50
-
51
- let persistingFocus = false
52
- let didBlurActiveElement = false
53
- const postHookCleanups: (() => void)[] = []
54
-
55
- const placementBlurHandler = (event: Event) => {
56
- event.preventDefault()
57
- event.stopPropagation()
58
- didBlurActiveElement = true
59
- }
60
-
61
- let currentActiveElement: Element | null = null
62
- function onBeforeFlushDomChanges() {
63
- persistingFocus = true
64
- currentActiveElement = document.activeElement
65
- if (currentActiveElement && currentActiveElement !== document.body) {
66
- currentActiveElement.addEventListener("blur", placementBlurHandler)
67
- }
68
- }
69
-
70
- function onAfterFlushDomChanges() {
71
- if (didBlurActiveElement) {
72
- currentActiveElement!.removeEventListener("blur", placementBlurHandler)
73
- if (currentActiveElement!.isConnected) {
74
- ;(currentActiveElement as any).focus()
75
- }
76
- didBlurActiveElement = false
77
- }
78
- persistingFocus = false
79
- queueMicrotask(() => {
80
- postHookCleanups.forEach(call)
81
- postHookCleanups.length = 0
82
- })
83
- }
84
-
85
- function createDom(vNode: DomVNode): SomeDom {
86
- const t = vNode.type
87
- const dom =
88
- t == "#text"
89
- ? createTextNode(vNode)
90
- : svgTags.has(t)
91
- ? document.createElementNS("http://www.w3.org/2000/svg", t)
92
- : document.createElement(t)
93
-
94
- return dom
95
- }
96
- function createTextNode(vNode: VNode): Text {
97
- const { nodeValue } = vNode.props
98
- if (Signal.isSignal(nodeValue)) {
99
- return createSignalTextNode(vNode, nodeValue)
100
- }
101
-
102
- return document.createTextNode(nodeValue)
103
- }
104
-
105
- function createSignalTextNode(vNode: VNode, nodeValue: Signal<string>): Text {
106
- const value = nodeValue.peek() ?? ""
107
- const textNode = document.createTextNode(value)
108
- subTextNode(vNode, textNode, nodeValue)
109
- return textNode
110
- }
111
-
112
- function wrapFocusEventHandler(callback: (event: FocusEvent) => void) {
113
- return (event: FocusEvent) => {
114
- if (persistingFocus) {
115
- event.preventDefault()
116
- event.stopPropagation()
117
- return
118
- }
119
- callback(event)
120
- }
121
- }
122
-
123
- interface VNodeEventListenerObjects {
124
- [key: string]: EventListenerObject
125
- }
126
- const eventListenerObjects = new WeakMap<VNode, VNodeEventListenerObjects>()
127
-
128
- function updateDom(vNode: DomVNode) {
129
- const { dom, prev, props, cleanups } = vNode
130
- const prevProps = prev?.props ?? {}
131
- const nextProps = props ?? {}
132
- const isHydration = renderMode.current === "hydrate"
133
-
134
- // TEXT NODE SHORT-PATH
135
- if (dom instanceof Text) {
136
- const nextVal = nextProps.nodeValue
137
- if (!Signal.isSignal(nextVal) && dom.nodeValue !== nextVal) {
138
- dom.nodeValue = nextVal
139
- }
140
- return
141
- }
142
-
143
- const keys: string[] = []
144
- for (const k in prevProps) keys.push(k)
145
- for (const k in nextProps) {
146
- if (!(k in prevProps)) keys.push(k)
147
- }
148
-
149
- let events: VNodeEventListenerObjects | undefined
150
- for (let i = 0; i < keys.length; i++) {
151
- const key = keys[i]
152
- const prevVal = prevProps[key]
153
- const nextVal = nextProps[key]
154
-
155
- if (propFilters.isEvent(key)) {
156
- events ??= eventListenerObjects.get(vNode)
157
- if (!events) eventListenerObjects.set(vNode, (events = {}))
158
-
159
- if (prevVal !== nextVal || isHydration) {
160
- const evtName = key.replace(EVENT_PREFIX_REGEX, "")
161
- const evtListenerObj = events[evtName]
162
-
163
- if (!nextVal) {
164
- if (evtListenerObj) {
165
- dom.removeEventListener(evtName, evtListenerObj)
166
- delete events[evtName]
167
- }
168
- continue
169
- }
170
-
171
- let handleEvent = nextVal.bind(void 0)
172
- if (evtName === "focus" || evtName === "blur") {
173
- handleEvent = wrapFocusEventHandler(handleEvent)
174
- }
175
-
176
- if (evtListenerObj) {
177
- evtListenerObj.handleEvent = handleEvent
178
- continue
179
- }
180
-
181
- dom.addEventListener(evtName, (events[evtName] = { handleEvent }))
182
- }
183
- continue
184
- }
185
-
186
- if (propFilters.isInternalProp(key) && key !== "innerHTML") {
187
- continue
188
- }
189
-
190
- if (prevVal === nextVal) {
191
- continue
192
- }
193
-
194
- if (Signal.isSignal(prevVal) && cleanups) {
195
- const disposer = cleanups[key]
196
- if (disposer) {
197
- disposer()
198
- delete cleanups[key]
199
- }
200
- }
201
-
202
- if (Signal.isSignal(nextVal)) {
203
- setSignalProp(vNode, dom, key, nextVal, prevVal)
204
- continue
205
- }
206
-
207
- setProp(dom, key, nextVal, prevVal)
208
- }
209
-
210
- const prevRef = prevProps.ref
211
- const nextRef = nextProps.ref
212
- if (prevRef !== nextRef) {
213
- if (prevRef) setRef(prevRef, null)
214
- if (nextRef) setRef(nextRef, dom)
215
- }
216
- }
217
-
218
- function getSelectElementValue(dom: HTMLSelectElement) {
219
- if (dom.multiple) {
220
- return Array.from(dom.selectedOptions).map((option) => option.value)
221
- }
222
- return dom.value
223
- }
224
-
225
- function setSelectElementValue(dom: HTMLSelectElement, value: any) {
226
- if (!dom.multiple || value === undefined || value === null || value === "") {
227
- dom.value = value
228
- return
229
- }
230
- Array.from(dom.options).forEach((option) => {
231
- option.selected = value.indexOf(option.value) > -1
232
- })
233
- }
234
-
235
- const bindAttrToEventMap: Record<string, string> = {
236
- value: "input",
237
- checked: "change",
238
- open: "toggle",
239
- volume: "volumechange",
240
- playbackRate: "ratechange",
241
- currentTime: "timeupdate",
242
- }
243
- const numericValueInputTypes = new Set(["progress", "meter", "number", "range"])
244
-
245
- function setSignalProp(
246
- vNode: VNode,
247
- dom: Exclude<SomeDom, Text>,
248
- key: string,
249
- signal: Signal<any>,
250
- prevValue: unknown
251
- ) {
252
- const [modifier, attr] = key.split(":")
253
- if (modifier !== "bind") {
254
- const unsub = signal.subscribe((value, prev) => {
255
- if (value === prev) return
256
- setProp(dom, key, value, prev)
257
- if (__DEV__) {
258
- emitSignalAttrUpdate(vNode)
259
- }
260
- })
261
- registerVNodeCleanup(vNode, key, unsub)
262
- } else {
263
- const evtName = bindAttrToEventMap[attr]
264
- if (!evtName) {
265
- if (__DEV__) {
266
- console.error(
267
- `[kiru]: ${attr} is not a valid element binding attribute.`
268
- )
269
- }
270
- return
271
- }
272
- const cleanup = bindElementProp(vNode, dom, attr, evtName, signal)
273
- registerVNodeCleanup(vNode, key, cleanup)
274
- }
275
-
276
- const value = signal.peek()
277
- const prev = unwrap(prevValue)
278
- if (value !== prev) {
279
- setProp(dom, attr ?? modifier, value, prev)
280
- }
281
- }
282
-
283
- function createElementValueReader(
284
- dom: Exclude<SomeDom, Text>,
285
- signal: Signal<any>
286
- ) {
287
- if (dom instanceof HTMLInputElement) {
288
- return createInputValueReader(dom, signal)
289
- }
290
- if (dom instanceof HTMLSelectElement) {
291
- return () => getSelectElementValue(dom)
292
- }
293
- return () => (dom as any).value
294
- }
295
-
296
- function bindElementProp(
297
- vNode: VNode,
298
- dom: Exclude<SomeDom, Text>,
299
- attr: string,
300
- evtName: string,
301
- signal: Signal<any>
302
- ): () => void {
303
- const writeToSignal = (val: any) => {
304
- signal.sneak(val)
305
- signal.notify((sub) => sub !== updateFromSignal)
306
- }
307
-
308
- const writeToElement =
309
- dom instanceof HTMLSelectElement && attr === "value"
310
- ? (value: any) => setSelectElementValue(dom, value)
311
- : (value: any) => ((dom as any)[attr] = value)
312
-
313
- const updateFromSignal = (value: any) => {
314
- writeToElement(value)
315
- if (__DEV__) {
316
- emitSignalAttrUpdate(vNode)
317
- }
318
- }
319
-
320
- let evtHandler: EventListener
321
- if (attr === "value") {
322
- const readValue = createElementValueReader(dom, signal)
323
- evtHandler = () => writeToSignal(readValue())
324
- } else {
325
- evtHandler = () => {
326
- const val = (dom as any)[attr]
327
- /**
328
- * the 'timeupdate' event is fired when the currentTime property is
329
- * set (from code OR playback), so we need to prevent unnecessary
330
- * signal updates to avoid a feedback loop when there are multiple
331
- * elements with the same signal bound to 'currentTime'
332
- */
333
- if (attr === "currentTime" && signal.peek() === val) return
334
- writeToSignal(val)
335
- }
336
- }
337
-
338
- dom.addEventListener(evtName, evtHandler)
339
- const unsub = signal.subscribe(updateFromSignal)
340
-
341
- return () => {
342
- dom.removeEventListener(evtName, evtHandler)
343
- unsub()
344
- }
345
- }
346
-
347
- function createInputValueReader(
348
- dom: HTMLInputElement,
349
- signal: Signal<any>
350
- ): () => any {
351
- const t = dom.type
352
- const v = signal.peek()
353
-
354
- if (t === "date" && v instanceof Date) {
355
- return () => dom.valueAsDate
356
- }
357
-
358
- if (numericValueInputTypes.has(t) && typeof v === "number") {
359
- return () => dom.valueAsNumber
360
- }
361
-
362
- return () => dom.value
363
- }
364
-
365
- function emitSignalAttrUpdate(vNode: VNode) {
366
- window.__kiru.profilingContext?.emit("signalAttrUpdate", getVNodeApp(vNode)!)
367
- }
368
-
369
- function subTextNode(vNode: VNode, textNode: Text, signal: Signal<string>) {
370
- const cleanup = signal.subscribe((value, prev) => {
371
- if (value === prev) return
372
- textNode.nodeValue = value
373
- if (__DEV__) {
374
- window.__kiru.profilingContext?.emit(
375
- "signalTextUpdate",
376
- getVNodeApp(vNode)!
377
- )
378
- }
379
- })
380
- registerVNodeCleanup(vNode, "nodeValue", cleanup)
381
- }
382
-
383
- /**
384
- * Creates and inserts an empty signal-bound text node into
385
- * the dom tree if the signal value is null or undefined.
386
- */
387
- function getOrCreateTextNode(vNode: VNode): MaybeDom {
388
- const sig = vNode.props.nodeValue
389
- if (!Signal.isSignal(sig)) {
390
- return hydrationStack.getCurrentChild()
391
- }
392
-
393
- const value = sig.peek()
394
- if (isValidTextChild(value)) {
395
- return hydrationStack.getCurrentChild()
396
- }
397
-
398
- const dom = createSignalTextNode(vNode, sig)
399
- const currentChild = hydrationStack.getCurrentChild()
400
-
401
- if (!currentChild) {
402
- return hydrationStack.getCurrentParent().appendChild(dom)
403
- }
404
-
405
- currentChild.before(dom)
406
- return dom
407
- }
408
-
409
- function hydrateDom(vNode: VNode) {
410
- const dom =
411
- vNode.type === "#text"
412
- ? getOrCreateTextNode(vNode)
413
- : hydrationStack.getCurrentChild()
414
-
415
- hydrationStack.bumpChildIndex()
416
-
417
- if (!dom) {
418
- throw new KiruError({
419
- message: `Hydration mismatch - no node found`,
420
- vNode,
421
- })
422
- }
423
- let nodeName = dom.nodeName
424
- if (!svgTags.has(nodeName)) {
425
- nodeName = nodeName.toLowerCase()
426
- }
427
- if ((vNode.type as string) !== nodeName) {
428
- throw new KiruError({
429
- message: `Hydration mismatch - expected node of type ${vNode.type.toString()} but received ${nodeName}`,
430
- vNode,
431
- })
432
- }
433
- vNode.dom = dom
434
- if (vNode.type !== "#text" && !(vNode.flags & FLAG_STATIC_DOM)) {
435
- updateDom(vNode as DomVNode)
436
- return
437
- }
438
- if (Signal.isSignal(vNode.props.nodeValue)) {
439
- subTextNode(vNode, dom as Text, vNode.props.nodeValue)
440
- }
441
-
442
- let prev = vNode
443
- let sibling = vNode.sibling
444
- while (sibling && sibling.type === "#text") {
445
- const sib = sibling
446
- hydrationStack.bumpChildIndex()
447
- const prevText = String(unwrap(prev.props.nodeValue) ?? "")
448
- const dom = (prev.dom as Text).splitText(prevText.length)
449
- sib.dom = dom
450
- if (Signal.isSignal(sib.props.nodeValue)) {
451
- subTextNode(sib, dom, sib.props.nodeValue)
452
- }
453
- prev = sibling
454
- sibling = sibling.sibling
455
- }
456
- }
457
-
458
- function handleAttributeRemoval(
459
- element: Element,
460
- key: string,
461
- value: unknown,
462
- isBoolAttr = false
463
- ) {
464
- if (value === null) {
465
- element.removeAttribute(key)
466
- return true
467
- }
468
- switch (typeof value) {
469
- case "undefined":
470
- case "function":
471
- case "symbol": {
472
- element.removeAttribute(key)
473
- return true
474
- }
475
- case "boolean": {
476
- if (isBoolAttr && !value) {
477
- element.removeAttribute(key)
478
- return true
479
- }
480
- }
481
- }
482
-
483
- return false
484
- }
485
-
486
- function setDomAttribute(element: Element, key: string, value: unknown) {
487
- const isBoolAttr = booleanAttributes.has(key)
488
-
489
- if (handleAttributeRemoval(element, key, value, isBoolAttr)) return
490
-
491
- element.setAttribute(
492
- key,
493
- isBoolAttr && typeof value === "boolean" ? "" : String(value)
494
- )
495
- }
496
-
497
- const explicitValueElementTags = ["INPUT", "TEXTAREA"]
498
-
499
- const needsExplicitValueSet = (
500
- element: SomeElement
501
- ): element is HTMLInputElement | HTMLTextAreaElement => {
502
- return explicitValueElementTags.indexOf(element.nodeName) > -1
503
- }
504
-
505
- function setProp(
506
- element: SomeElement,
507
- key: string,
508
- value: unknown,
509
- prev: unknown
510
- ) {
511
- switch (key) {
512
- case "style":
513
- return setStyleProp(element, value, prev)
514
- case "className":
515
- return setClassName(element, value)
516
- case "innerHTML":
517
- return setInnerHTML(element, value)
518
- case "muted":
519
- ;(element as HTMLMediaElement).muted = Boolean(value)
520
- return
521
- case "value":
522
- if (element.nodeName === "SELECT") {
523
- return setSelectElementValue(element as HTMLSelectElement, value)
524
- }
525
- const strVal = value === undefined || value === null ? "" : String(value)
526
- if (needsExplicitValueSet(element)) {
527
- element.value = strVal
528
- return
529
- }
530
- element.setAttribute("value", strVal)
531
- return
532
- case "checked":
533
- if (element.nodeName === "INPUT") {
534
- ;(element as HTMLInputElement).checked = Boolean(value)
535
- return
536
- }
537
- element.setAttribute("checked", String(value))
538
- return
539
- default:
540
- return setDomAttribute(element, propToHtmlAttr(key), value)
541
- }
542
- }
543
-
544
- function setInnerHTML(element: SomeElement, value: unknown) {
545
- if (value === null || value === undefined || typeof value === "boolean") {
546
- element.innerHTML = ""
547
- return
548
- }
549
- element.innerHTML = String(value)
550
- }
551
-
552
- function setClassName(element: SomeElement, value: unknown) {
553
- const val = unwrap(value)
554
- if (!val) {
555
- return element.removeAttribute("class")
556
- }
557
- element.setAttribute("class", val as string)
558
- }
559
-
560
- function setStyleProp(element: SomeElement, value: unknown, prev: unknown) {
561
- if (handleAttributeRemoval(element, "style", value)) return
562
-
563
- if (typeof value === "string") {
564
- element.setAttribute("style", value)
565
- return
566
- }
567
-
568
- let prevStyle: StyleObject = {}
569
- if (typeof prev === "string") {
570
- element.setAttribute("style", "")
571
- } else if (typeof prev === "object" && !!prev) {
572
- prevStyle = prev as StyleObject
573
- }
574
-
575
- const nextStyle = value as StyleObject
576
- const keys = new Set([
577
- ...Object.keys(prevStyle),
578
- ...Object.keys(nextStyle),
579
- ]) as Set<keyof StyleObject>
580
-
581
- keys.forEach((k) => {
582
- const prev = prevStyle[k]
583
- const next = nextStyle[k]
584
- if (prev === next) return
585
-
586
- if (next === undefined) {
587
- element.style[k as any] = ""
588
- return
589
- }
590
-
591
- element.style[k as any] = next as any
592
- })
593
- }
594
-
595
- function getDomParent(vNode: VNode): ElementVNode {
596
- let parentNode: VNode | null = vNode.parent
597
- let parentNodeElement = parentNode?.dom
598
- while (parentNode && !parentNodeElement) {
599
- parentNode = parentNode.parent
600
- parentNodeElement = parentNode?.dom
601
- }
602
-
603
- if (!parentNodeElement || !parentNode) {
604
- // handle app entry
605
- if (!vNode.parent && vNode.dom) {
606
- return vNode as ElementVNode
607
- }
608
-
609
- throw new KiruError({
610
- message: "No DOM parent found while attempting to place node.",
611
- vNode: vNode,
612
- })
613
- }
614
- return parentNode as ElementVNode
615
- }
616
-
617
- function placeDom(vNode: DomVNode, hostNode: HostNode) {
618
- const { node: parentVNodeWithDom, lastChild } = hostNode
619
- const dom = vNode.dom
620
- if (lastChild) {
621
- lastChild.after(dom)
622
- return
623
- }
624
- // TODO: we can probably skip the 'next sibling search' if we're appending
625
- const nextSiblingDom = getNextSiblingDom(vNode, parentVNodeWithDom)
626
- if (nextSiblingDom) {
627
- parentVNodeWithDom.dom.insertBefore(dom, nextSiblingDom)
628
- return
629
- }
630
-
631
- parentVNodeWithDom.dom.appendChild(dom)
632
- }
633
-
634
- function getNextSiblingDom(vNode: VNode, parent: ElementVNode): MaybeDom {
635
- let node: VNode | null = vNode
636
-
637
- while (node) {
638
- let sibling = node.sibling
639
-
640
- while (sibling) {
641
- // Skip unmounted, to-be-placed & static nodes
642
- if (!(sibling.flags & (FLAG_PLACEMENT | FLAG_STATIC_DOM))) {
643
- // Descend into the child to find host dom
644
- const dom = findFirstHostDom(sibling)
645
- if (dom?.isConnected) return dom
646
- }
647
- sibling = sibling.sibling
648
- }
649
-
650
- // Move up to parent — but don't escape portal boundary
651
- node = node.parent
652
- if (!node || node.flags & FLAG_STATIC_DOM || node === parent) {
653
- return
654
- }
655
- }
656
-
657
- return
658
- }
659
-
660
- function findFirstHostDom(vNode: VNode): MaybeDom {
661
- let node: VNode | null = vNode
662
-
663
- while (node) {
664
- if (node.dom) return node.dom
665
- if (node.flags & FLAG_STATIC_DOM) return // Don't descend into portals
666
- node = node.child
667
- }
668
- return
669
- }
670
-
671
- function commitWork(vNode: VNode) {
672
- if (renderMode.current === "hydrate") {
673
- return traverseApply(vNode, commitSnapshot)
674
- }
675
-
676
- const host: HostNode = {
677
- node: vNode.dom ? (vNode as ElementVNode) : getDomParent(vNode),
678
- }
679
- commitWork_impl(vNode, host, (vNode.flags & FLAG_PLACEMENT) > 0)
680
- if (vNode.dom && !(vNode.flags & FLAG_STATIC_DOM)) {
681
- commitDom(vNode as DomVNode, host, false)
682
- }
683
- commitSnapshot(vNode)
684
- }
685
-
686
- function commitWork_impl(
687
- vNode: VNode,
688
- currentHostNode: HostNode,
689
- inheritsPlacement: boolean
690
- ) {
691
- let child: VNode | null = vNode.child
692
- while (child) {
693
- if (child.dom) {
694
- commitWork_impl(child, { node: child as ElementVNode }, false)
695
- if (!(child.flags & FLAG_STATIC_DOM)) {
696
- commitDom(child as DomVNode, currentHostNode, inheritsPlacement)
697
- }
698
- } else {
699
- commitWork_impl(
700
- child,
701
- currentHostNode,
702
- (child.flags & FLAG_PLACEMENT) > 0 || inheritsPlacement
703
- )
704
- }
705
-
706
- commitSnapshot(child)
707
- child = child.sibling
708
- }
709
- }
710
-
711
- function commitDom(
712
- vNode: DomVNode,
713
- hostNode: HostNode,
714
- inheritsPlacement: boolean
715
- ) {
716
- if (
717
- inheritsPlacement ||
718
- !vNode.dom.isConnected ||
719
- vNode.flags & FLAG_PLACEMENT
720
- ) {
721
- placeDom(vNode, hostNode)
722
- }
723
- if (!vNode.prev || vNode.flags & FLAG_UPDATE) {
724
- updateDom(vNode)
725
- }
726
- hostNode.lastChild = vNode.dom
727
- }
728
-
729
- function commitDeletion(vNode: VNode) {
730
- if (vNode === vNode.parent?.child) {
731
- vNode.parent.child = vNode.sibling
732
- }
733
- let app: AppHandle
734
- if (__DEV__) {
735
- app = getVNodeApp(vNode)!
736
- }
737
- traverseApply(vNode, (node) => {
738
- const {
739
- subs,
740
- cleanups,
741
- dom,
742
- props: { ref },
743
- hooks,
744
- } = node
745
-
746
- subs?.forEach((unsub) => unsub())
747
- if (cleanups) Object.values(cleanups).forEach((c) => c())
748
- if (hooks) {
749
- const { preCleanups, postCleanups } = hooks
750
-
751
- preCleanups.forEach(call)
752
- postHookCleanups.push(...postCleanups)
753
- preCleanups.length = postCleanups.length = 0
754
- }
755
-
756
- if (__DEV__) {
757
- window.__kiru.profilingContext?.emit("removeNode", app)
758
- if (dom instanceof Element) {
759
- delete dom.__kiruNode
760
- }
761
- }
762
-
763
- if (dom) {
764
- if (dom.isConnected && !(node.flags & FLAG_STATIC_DOM)) {
765
- dom.remove()
766
- }
767
- if (ref) {
768
- setRef(ref, null)
769
- }
770
- delete node.dom
771
- }
772
- })
773
-
774
- vNode.parent = null
775
- }