kiru 1.0.1 → 1.1.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.
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 +165 -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 +204 -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
@@ -0,0 +1,818 @@
1
+ import {
2
+ propToHtmlAttr,
3
+ getVNodeApp,
4
+ setRef,
5
+ registerVNodeCleanup,
6
+ } from "../utils/index.js"
7
+ import { Signal } from "../signals/base.js"
8
+ import { unwrap } from "../signals/utils.js"
9
+ import { booleanAttributes, EVENT_PREFIX_REGEX } from "../constants.js"
10
+ import { __DEV__, isBrowser } from "../env.js"
11
+ import { wrapFocusEventHandler } from "./focus.js"
12
+ import type { StyleObject } from "../types.dom.js"
13
+ import type { DomVNode, SomeDom, SomeElement } from "../types.utils.js"
14
+
15
+ export { updateDomProps, unmountDomProps, setSignalProp }
16
+
17
+ type VNode = Kiru.VNode
18
+
19
+ interface VNodeEventListenerObjects {
20
+ [key: string]: EventListenerObject
21
+ }
22
+
23
+ const eventListenerObjects = new WeakMap<VNode, VNodeEventListenerObjects>()
24
+ const skippedProps = new Set(["children", "ref", "key"])
25
+
26
+ // Reusable buckets for maybeOrderPropKeys — avoids per-call allocation.
27
+ // Safe because JS is single-threaded.
28
+ const _buckets: string[][] = [[], [], [], [], [], [], []]
29
+
30
+ const bindAttrToEventMap: Record<string, string> = {
31
+ value: "input",
32
+ checked: "change",
33
+ open: "toggle",
34
+ volume: "volumechange",
35
+ playbackRate: "ratechange",
36
+ currentTime: "timeupdate",
37
+ }
38
+
39
+ const numericValueInputTypes = new Set(["progress", "meter", "number", "range"])
40
+
41
+ // Reuse a Set for explicit-value element tag lookup (faster than indexOf on array)
42
+ const explicitValueElementTags = new Set(["INPUT", "TEXTAREA"])
43
+
44
+ function updateDomProps(vNode: DomVNode) {
45
+ const { dom, prev, props, cleanups } = vNode
46
+ const prevProps = prev?.props ?? {}
47
+ const nextProps = props ?? {}
48
+
49
+ if (isTextNode(dom)) {
50
+ const nextVal = nextProps.nodeValue
51
+ if (!Signal.isSignal(nextVal) && dom.nodeValue !== nextVal) {
52
+ dom.nodeValue = nextVal
53
+ }
54
+ return
55
+ }
56
+
57
+ // Fast-path for first commit: no previous props, so we can skip diffing and
58
+ // just apply all props (with ordering) directly.
59
+ if (!prev) {
60
+ mountDomProps(vNode, dom, nextProps, cleanups)
61
+ const nextRef = nextProps.ref
62
+ if (nextRef) setRef(nextRef, dom)
63
+ return
64
+ }
65
+
66
+ // Use a Set to deduplicate keys that appear in both prevProps and nextProps.
67
+ const execKeySet = new Set<string>()
68
+ let styleKeyToSignal: Map<string, Signal<unknown>> | undefined
69
+ let events: VNodeEventListenerObjects | undefined
70
+
71
+ // Handle prevProps keys that may have been removed or changed.
72
+ for (const key in prevProps) {
73
+ const prevVal = prevProps[key]
74
+ const nextVal = nextProps[key]
75
+
76
+ if (prevVal === nextVal) continue // unchanged
77
+
78
+ // Event removal
79
+ if (
80
+ key.length >= 2 &&
81
+ key.charCodeAt(0) === 111 &&
82
+ key.charCodeAt(1) === 110
83
+ ) {
84
+ // "on"
85
+ if (!nextVal) {
86
+ events ??= eventListenerObjects.get(vNode) ?? {}
87
+ eventListenerObjects.set(vNode, events)
88
+
89
+ const evtName = key.replace(EVENT_PREFIX_REGEX, "")
90
+ const evtObj = events[evtName]
91
+ if (evtObj) {
92
+ dom.removeEventListener(evtName, evtObj)
93
+ delete events[evtName]
94
+ }
95
+ continue
96
+ }
97
+ }
98
+
99
+ // Cleanup previous signals
100
+ if (Signal.isSignal(prevVal) && cleanups?.[key]) {
101
+ cleanups[key]()
102
+ delete cleanups[key]
103
+ }
104
+
105
+ execKeySet.add(key)
106
+ }
107
+
108
+ // Handle nextProps keys that are new or changed.
109
+ for (const key in nextProps) {
110
+ if (!(key in prevProps) || prevProps[key] !== nextProps[key]) {
111
+ execKeySet.add(key)
112
+ }
113
+ }
114
+
115
+ const execKeys = Array.from(execKeySet)
116
+
117
+ // Analyze for constraint/value hazards only if multiple keys changed.
118
+ if (isElementNode(dom) && execKeys.length > 1) {
119
+ const changedSet = execKeySet // reuse the same set
120
+ let seenConstraint = false
121
+ let constraintChanged = false
122
+ const valueLikeKeys: string[] = []
123
+ let hasEvents = false
124
+ let hasNonEvent = false
125
+
126
+ for (const key in nextProps) {
127
+ const isEvent =
128
+ key.length >= 2 &&
129
+ key.charCodeAt(0) === 111 &&
130
+ key.charCodeAt(1) === 110 // "on"
131
+ if (isEvent) hasEvents = true
132
+ else hasNonEvent = true
133
+
134
+ const baseKey = !isEvent && key.startsWith("bind:") ? key.slice(5) : key
135
+ const priority = getBasePropPriority(baseKey, isEvent)
136
+
137
+ if (priority === 1) {
138
+ seenConstraint = true
139
+ if (changedSet.has(key)) constraintChanged = true
140
+ } else if (priority === 5) {
141
+ valueLikeKeys.push(key)
142
+ }
143
+ }
144
+
145
+ if (seenConstraint && constraintChanged) {
146
+ for (const vk of valueLikeKeys) {
147
+ if (!changedSet.has(vk) && vk in nextProps) {
148
+ execKeys.push(vk)
149
+ changedSet.add(vk)
150
+ }
151
+ }
152
+ }
153
+
154
+ const needsOrdering =
155
+ (seenConstraint && constraintChanged) || (hasEvents && hasNonEvent)
156
+ if (needsOrdering && execKeys.length > 1) maybeOrderPropKeys(execKeys)
157
+ }
158
+
159
+ // Apply updates
160
+ for (let i = 0; i < execKeys.length; i++) {
161
+ const key = execKeys[i]
162
+ const prevVal = prevProps[key]
163
+ const nextVal = nextProps[key]
164
+
165
+ // Skip structural props early
166
+ if (skippedProps.has(key)) continue
167
+
168
+ // Events — charcode check is faster than startsWith for hot path
169
+ if (
170
+ key.length >= 2 &&
171
+ key.charCodeAt(0) === 111 &&
172
+ key.charCodeAt(1) === 110
173
+ ) {
174
+ // "on"
175
+ events ??= eventListenerObjects.get(vNode) ?? {}
176
+ eventListenerObjects.set(vNode, events)
177
+
178
+ const evtName = key.replace(EVENT_PREFIX_REGEX, "")
179
+ const evtObj = events[evtName]
180
+
181
+ if (!nextVal) {
182
+ if (evtObj) {
183
+ dom.removeEventListener(evtName, evtObj)
184
+ delete events[evtName]
185
+ }
186
+ continue
187
+ }
188
+
189
+ let handleEvent = nextVal.bind(void 0)
190
+ if (evtName === "focus" || evtName === "blur")
191
+ handleEvent = wrapFocusEventHandler(handleEvent)
192
+
193
+ if (evtObj) {
194
+ evtObj.handleEvent = handleEvent
195
+ continue
196
+ }
197
+
198
+ dom.addEventListener(evtName, (events[evtName] = { handleEvent }))
199
+ continue
200
+ }
201
+
202
+ // Signal
203
+ if (Signal.isSignal(nextVal)) {
204
+ setSignalProp(vNode, dom, key, nextVal, prevVal)
205
+ continue
206
+ }
207
+
208
+ // Style
209
+ if (key === "style" && typeof nextVal === "object" && nextVal !== null) {
210
+ if (cleanups?.style) {
211
+ cleanups.style()
212
+ delete cleanups.style
213
+ }
214
+ if (!styleKeyToSignal) {
215
+ styleKeyToSignal = new Map<string, Signal<unknown>>()
216
+ }
217
+ setStyleProp(dom, nextVal, prevVal, true)
218
+ if (styleKeyToSignal.size > 0) {
219
+ const unsubs: (() => void)[] = []
220
+ for (const [k, sig] of styleKeyToSignal.entries()) {
221
+ unsubs.push(
222
+ sig.subscribe(
223
+ k.startsWith("--")
224
+ ? (v) => setCustomCSSStyleDecValue(dom, k, v)
225
+ : (v) => setCSSStyleDecValue(dom, k, v)
226
+ )
227
+ )
228
+ }
229
+ styleKeyToSignal.clear()
230
+ registerVNodeCleanup(vNode, "style", () => unsubs.forEach((u) => u()))
231
+ }
232
+ continue
233
+ }
234
+
235
+ setProp(dom, key, nextVal, prevVal)
236
+ }
237
+
238
+ // Ref
239
+ const prevRef = prevProps.ref
240
+ const nextRef = nextProps.ref
241
+ if (prevRef !== nextRef) {
242
+ if (prevRef) setRef(prevRef, null)
243
+ if (nextRef) setRef(nextRef, dom)
244
+ }
245
+ }
246
+
247
+ function unmountDomProps(
248
+ vNode: DomVNode,
249
+ dom: SomeDom,
250
+ prevProps: Record<string, any>,
251
+ cleanups?: DomVNode["cleanups"]
252
+ ) {
253
+ let events: VNodeEventListenerObjects | undefined
254
+
255
+ for (const key in prevProps) {
256
+ const prevVal = prevProps[key]
257
+
258
+ // Skip structural props early
259
+ if (skippedProps.has(key)) continue
260
+
261
+ // Events
262
+ if (
263
+ key.length >= 2 &&
264
+ key.charCodeAt(0) === 111 &&
265
+ key.charCodeAt(1) === 110
266
+ ) {
267
+ // "on"
268
+ events ??= eventListenerObjects.get(vNode) ?? {}
269
+ eventListenerObjects.set(vNode, events)
270
+
271
+ const evtName = key.replace(EVENT_PREFIX_REGEX, "")
272
+ const evtObj = events[evtName]
273
+ if (evtObj) {
274
+ dom.removeEventListener(evtName, evtObj)
275
+ delete events[evtName]
276
+ }
277
+ continue
278
+ }
279
+
280
+ // Signals (including bind: props) – invoke their registered cleanups.
281
+ if (Signal.isSignal(prevVal) && cleanups?.[key]) {
282
+ cleanups[key]!()
283
+ delete cleanups[key]
284
+ continue
285
+ }
286
+
287
+ // Style object: clear any style listeners and remove the style attribute.
288
+ if (key === "style") {
289
+ if (cleanups?.style) {
290
+ cleanups.style()
291
+ delete cleanups.style
292
+ }
293
+ if (isElementNode(dom)) {
294
+ setStyleProp(dom as SomeElement, undefined, prevVal)
295
+ }
296
+ continue
297
+ }
298
+
299
+ // Other props: remove/reset attributes based on previous value.
300
+ if (isElementNode(dom)) {
301
+ setProp(dom as SomeElement, key, undefined, prevVal)
302
+ }
303
+ }
304
+
305
+ // Clear previous ref
306
+ const prevRef = prevProps.ref
307
+ if (prevRef) setRef(prevRef, null)
308
+ }
309
+
310
+ const styleKeyToSignal = new Map<string, Signal<unknown>>()
311
+ function mountDomProps(
312
+ vNode: DomVNode,
313
+ dom: SomeDom,
314
+ props: Record<string, any>,
315
+ cleanups?: DomVNode["cleanups"]
316
+ ) {
317
+ const keys = Object.keys(props)
318
+ if (isElementNode(dom) && keys.length > 1) {
319
+ maybeOrderPropKeys(keys)
320
+ }
321
+
322
+ let events: VNodeEventListenerObjects | undefined
323
+
324
+ for (let i = 0; i < keys.length; i++) {
325
+ const key = keys[i]
326
+ const value = props[key]
327
+
328
+ if (skippedProps.has(key)) continue
329
+
330
+ // Events
331
+ if (
332
+ key.length >= 2 &&
333
+ key.charCodeAt(0) === 111 &&
334
+ key.charCodeAt(1) === 110
335
+ ) {
336
+ // "on"
337
+ if (!value) continue
338
+
339
+ events ??= eventListenerObjects.get(vNode) ?? {}
340
+ eventListenerObjects.set(vNode, events)
341
+
342
+ const evtName = key.replace(EVENT_PREFIX_REGEX, "")
343
+ const evtObj = events[evtName]
344
+
345
+ let handleEvent = value.bind(void 0)
346
+ if (evtName === "focus" || evtName === "blur") {
347
+ handleEvent = wrapFocusEventHandler(handleEvent)
348
+ }
349
+
350
+ if (evtObj) {
351
+ evtObj.handleEvent = handleEvent
352
+ } else {
353
+ dom.addEventListener(evtName, (events[evtName] = { handleEvent }))
354
+ }
355
+ continue
356
+ }
357
+
358
+ // Signals
359
+ if (Signal.isSignal(value)) {
360
+ setSignalProp(vNode, dom as Exclude<SomeDom, Text>, key, value, undefined)
361
+ continue
362
+ }
363
+
364
+ // Style
365
+ if (key === "style" && typeof value === "object" && value !== null) {
366
+ setStyleProp(dom as SomeElement, value, undefined, true)
367
+ if (styleKeyToSignal.size > 0) {
368
+ cleanups ??= {}
369
+ for (const [k, sig] of styleKeyToSignal.entries()) {
370
+ const cleanupKey = `style-${k}`
371
+ cleanups[cleanupKey]?.()
372
+ cleanups[cleanupKey] = sig.subscribe(
373
+ k.startsWith("--")
374
+ ? (v) => setCustomCSSStyleDecValue(dom as SomeElement, k, v)
375
+ : (v) => setCSSStyleDecValue(dom as SomeElement, k, v)
376
+ )
377
+ }
378
+ styleKeyToSignal.clear()
379
+ }
380
+ continue
381
+ }
382
+
383
+ setProp(dom as SomeElement, key, value, undefined)
384
+ }
385
+ }
386
+
387
+ function maybeOrderPropKeys(keys: string[]) {
388
+ if (keys.length <= 1) return
389
+
390
+ // Clear reusable buckets
391
+ for (let b = 0; b < 7; b++) _buckets[b].length = 0
392
+
393
+ for (let i = 0; i < keys.length; i++) {
394
+ const key = keys[i]
395
+ const isEvent =
396
+ key.length >= 2 && key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 // "on"
397
+
398
+ let baseKey = key
399
+ if (!isEvent && key.length > 5 && key.charCodeAt(4) === 58) {
400
+ // "bind:"
401
+ baseKey = key.slice(5)
402
+ }
403
+
404
+ const priority = getBasePropPriority(baseKey, isEvent)
405
+
406
+ let bucketIdx: number
407
+ if (priority <= 0) bucketIdx = 0
408
+ else if (priority === 1) bucketIdx = 1
409
+ else if (priority === 2) bucketIdx = 2
410
+ else if (priority === 3) bucketIdx = 3
411
+ else if (priority === 5) bucketIdx = 4
412
+ else if (priority >= 9) bucketIdx = 6
413
+ else bucketIdx = 5
414
+
415
+ _buckets[bucketIdx].push(key)
416
+ }
417
+
418
+ let outIdx = 0
419
+ for (let b = 0; b < 7; b++) {
420
+ const bucket = _buckets[b]
421
+ for (let i = 0; i < bucket.length; i++) {
422
+ keys[outIdx++] = bucket[i]
423
+ }
424
+ }
425
+ }
426
+
427
+ function getBasePropPriority(baseKey: string, isEvent: boolean): number {
428
+ if (isEvent) return 9
429
+
430
+ // Use first char to narrow comparisons
431
+ switch (baseKey) {
432
+ case "innerHTML":
433
+ case "type":
434
+ case "muted":
435
+ case "autoplay":
436
+ case "loop":
437
+ return 0
438
+ case "min":
439
+ case "max":
440
+ case "step":
441
+ case "pattern":
442
+ case "accept":
443
+ case "multiple":
444
+ case "preload":
445
+ case "minLength":
446
+ case "maxLength":
447
+ case "crossOrigin":
448
+ case "decoding":
449
+ case "loading":
450
+ case "referrerPolicy":
451
+ return 1
452
+ case "style":
453
+ case "className":
454
+ return 3
455
+ case "value":
456
+ case "checked":
457
+ case "selected":
458
+ case "open":
459
+ case "src":
460
+ return 5
461
+ default:
462
+ return 4
463
+ }
464
+ }
465
+
466
+ function setSelectElementValue(dom: HTMLSelectElement, value: any) {
467
+ if (!dom.multiple || value === undefined || value === null || value === "") {
468
+ dom.value = value
469
+ return
470
+ }
471
+ const options = dom.options
472
+ const len = options.length
473
+ for (let i = 0; i < len; i++) {
474
+ const option = options[i]
475
+ option.selected = value.indexOf(option.value) > -1
476
+ }
477
+ }
478
+
479
+ function setSignalProp(
480
+ vNode: VNode,
481
+ dom: Exclude<SomeDom, Text>,
482
+ key: string,
483
+ signal: Signal<any>,
484
+ prevValue: unknown
485
+ ) {
486
+ const colonIdx = key.indexOf(":")
487
+ const modifier = colonIdx === -1 ? key : key.slice(0, colonIdx)
488
+ const attr = colonIdx === -1 ? undefined : key.slice(colonIdx + 1)
489
+
490
+ if (modifier === "bind") {
491
+ const evtName = bindAttrToEventMap[attr!]
492
+ if (!evtName) {
493
+ if (__DEV__) {
494
+ console.error(
495
+ `[kiru]: ${attr} is not a valid element binding attribute.`
496
+ )
497
+ }
498
+ return
499
+ }
500
+ const value = signal.peek()
501
+ const cleanup = bindElementProp(vNode, dom, attr!, evtName, signal, value)
502
+ registerVNodeCleanup(vNode, key, cleanup)
503
+ } else {
504
+ const unsub = signal.subscribe((value, prev) => {
505
+ if (value === prev) return
506
+ setProp(dom, key, value, prev)
507
+ if (__DEV__) {
508
+ emitSignalAttrUpdate(vNode)
509
+ }
510
+ })
511
+ registerVNodeCleanup(vNode, key, unsub)
512
+ }
513
+
514
+ const value = signal.peek()
515
+ const prev = unwrap(prevValue)
516
+ if (modifier !== "bind" && value !== prev) {
517
+ setProp(dom, attr ?? modifier, value, prev)
518
+ }
519
+ }
520
+
521
+ function bindElementProp(
522
+ vNode: VNode,
523
+ dom: Exclude<SomeDom, Text>,
524
+ attr: string,
525
+ evtName: string,
526
+ signal: Signal<any>,
527
+ initialValue?: any
528
+ ): () => void {
529
+ const writeToSignal = (val: any) => {
530
+ signal.sneak(val)
531
+ signal.notify((sub) => sub !== updateFromSignal)
532
+ }
533
+
534
+ const writeToElement =
535
+ dom.nodeName === "SELECT" && attr === "value"
536
+ ? (value: any) => setSelectElementValue(dom as HTMLSelectElement, value)
537
+ : (value: any) => ((dom as any)[attr] = value)
538
+
539
+ const updateFromSignal = (value: any) => {
540
+ writeToElement(value)
541
+ if (__DEV__) {
542
+ emitSignalAttrUpdate(vNode)
543
+ }
544
+ }
545
+
546
+ let readValue: (() => any) | undefined
547
+ let evtHandler: EventListener
548
+ if (attr === "value") {
549
+ readValue = createElementValueReader(dom)
550
+ evtHandler = () => writeToSignal(readValue!())
551
+ } else {
552
+ evtHandler = () => {
553
+ const val = (dom as any)[attr]
554
+ if (attr === "currentTime" && signal.peek() === val) return
555
+ writeToSignal(val)
556
+ }
557
+ }
558
+
559
+ if (initialValue !== undefined) {
560
+ updateFromSignal(initialValue)
561
+ }
562
+
563
+ // After binding is established, always reconcile the signal with the
564
+ // element's current value. This ensures that any browser coercion
565
+ // (clamping, defaulting, etc.) is reflected back into the signal,
566
+ // even when the initial signal value was undefined.
567
+ let domVal: any
568
+ if (attr === "value" && readValue) {
569
+ domVal = readValue()
570
+ } else {
571
+ domVal = (dom as any)[attr]
572
+ }
573
+
574
+ const current = signal.peek()
575
+ if (domVal !== current) {
576
+ writeToSignal(domVal)
577
+ }
578
+
579
+ dom.addEventListener(evtName, evtHandler)
580
+ const unsub = signal.subscribe(updateFromSignal)
581
+
582
+ return () => {
583
+ dom.removeEventListener(evtName, evtHandler)
584
+ unsub()
585
+ }
586
+ }
587
+
588
+ function setProp(
589
+ element: SomeElement,
590
+ key: string,
591
+ value: unknown,
592
+ prev: unknown
593
+ ) {
594
+ switch (key) {
595
+ case "style":
596
+ return setStyleProp(element, value, prev)
597
+ case "className":
598
+ return setClassName(element, value)
599
+ case "innerHTML":
600
+ return setInnerHTML(element, value)
601
+ case "muted":
602
+ ;(element as HTMLMediaElement).muted = Boolean(value)
603
+ return
604
+ case "value":
605
+ if (element.nodeName === "SELECT") {
606
+ return setSelectElementValue(element as HTMLSelectElement, value)
607
+ }
608
+ const strVal = value === undefined || value === null ? "" : String(value)
609
+ if (explicitValueElementTags.has(element.nodeName)) {
610
+ ;(element as HTMLInputElement | HTMLTextAreaElement).value = strVal
611
+ return
612
+ }
613
+ element.setAttribute("value", strVal)
614
+ return
615
+ case "checked":
616
+ if (element.nodeName === "INPUT") {
617
+ ;(element as HTMLInputElement).checked = Boolean(value)
618
+ return
619
+ }
620
+ element.setAttribute("checked", String(value))
621
+ return
622
+ default:
623
+ return setDomAttribute(element, propToHtmlAttr(key), value)
624
+ }
625
+ }
626
+
627
+ function setDomAttribute(element: Element, key: string, value: unknown) {
628
+ const isBoolAttr = booleanAttributes.has(key)
629
+
630
+ if (handleAttributeRemoval(element, key, value, isBoolAttr)) return
631
+
632
+ element.setAttribute(
633
+ key,
634
+ isBoolAttr && typeof value === "boolean" ? "" : String(value)
635
+ )
636
+ }
637
+
638
+ function setInnerHTML(element: SomeElement, value: unknown) {
639
+ if (value === null || value === undefined || typeof value === "boolean") {
640
+ element.innerHTML = ""
641
+ return
642
+ }
643
+ element.innerHTML = String(value)
644
+ }
645
+
646
+ function setClassName(element: SomeElement, value: unknown) {
647
+ const val = unwrap(value)
648
+ if (!val) {
649
+ return element.removeAttribute("class")
650
+ }
651
+ element.setAttribute("class", val as string)
652
+ }
653
+
654
+ function setCustomCSSStyleDecValue(
655
+ element: SomeElement,
656
+ key: string,
657
+ value: unknown
658
+ ): void {
659
+ if (value === undefined || value === null) {
660
+ element.style.removeProperty(key)
661
+ return
662
+ }
663
+ element.style.setProperty(key, String(value))
664
+ }
665
+
666
+ function setCSSStyleDecValue(
667
+ element: SomeElement,
668
+ key: string,
669
+ value: unknown
670
+ ): void {
671
+ element.style[key as any] =
672
+ value !== undefined && value !== null ? String(value) : ""
673
+ }
674
+
675
+ function setStyleProp(
676
+ element: SomeElement,
677
+ value: unknown,
678
+ prev: unknown,
679
+ trackSignals = false
680
+ ): void {
681
+ if (handleAttributeRemoval(element, "style", value)) return
682
+
683
+ const raw = unwrap(value)
684
+ if (raw === null || raw === undefined) {
685
+ element.removeAttribute("style")
686
+ return
687
+ }
688
+ if (typeof raw === "string") {
689
+ element.setAttribute("style", raw)
690
+ return
691
+ }
692
+
693
+ let prevStyle: StyleObject = {}
694
+ const rawPrev = unwrap(prev)
695
+ if (typeof rawPrev === "string") {
696
+ element.setAttribute("style", "")
697
+ } else if (typeof rawPrev === "object" && rawPrev !== null) {
698
+ prevStyle = rawPrev as StyleObject
699
+ }
700
+
701
+ const nextStyle = raw as StyleObject
702
+ const prevKeys = Object.keys(prevStyle)
703
+ const nextKeys = Object.keys(nextStyle)
704
+
705
+ // Avoid Set allocation for the common case where prevStyle is empty
706
+ if (prevKeys.length === 0) {
707
+ for (let i = 0; i < nextKeys.length; i++) {
708
+ const k = nextKeys[i] as keyof StyleObject
709
+ const rawNext = nextStyle[k]
710
+ const nextVal = unwrap(rawNext)
711
+ if (trackSignals && Signal.isSignal(rawNext)) {
712
+ styleKeyToSignal.set(k, rawNext)
713
+ }
714
+ if (k.startsWith("--")) {
715
+ setCustomCSSStyleDecValue(element, k, nextVal)
716
+ } else {
717
+ setCSSStyleDecValue(element, k, nextVal)
718
+ }
719
+ }
720
+ return
721
+ }
722
+
723
+ // Full merge path: iterate prevKeys for removals, nextKeys for additions/changes
724
+ const nextStyleKeys = new Set(nextKeys)
725
+ for (let i = 0; i < prevKeys.length; i++) {
726
+ const k = prevKeys[i] as keyof StyleObject
727
+ if (!nextStyleKeys.has(k)) {
728
+ // Property was removed
729
+ if ((k as string).startsWith("--")) {
730
+ setCustomCSSStyleDecValue(element, k as string, undefined)
731
+ } else {
732
+ setCSSStyleDecValue(element, k as string, undefined)
733
+ }
734
+ }
735
+ }
736
+
737
+ for (let i = 0; i < nextKeys.length; i++) {
738
+ const k = nextKeys[i] as keyof StyleObject
739
+ const rawNext = nextStyle[k]
740
+ const prevVal = unwrap(prevStyle[k])
741
+ const nextVal = unwrap(rawNext)
742
+ if (trackSignals && Signal.isSignal(rawNext)) {
743
+ styleKeyToSignal.set(k, rawNext)
744
+ }
745
+ if (prevVal === nextVal) continue
746
+ if ((k as string).startsWith("--")) {
747
+ setCustomCSSStyleDecValue(element, k as string, nextVal)
748
+ } else {
749
+ setCSSStyleDecValue(element, k as string, nextVal)
750
+ }
751
+ }
752
+ }
753
+
754
+ function handleAttributeRemoval(
755
+ element: Element,
756
+ key: string,
757
+ value: unknown,
758
+ isBoolAttr = false
759
+ ) {
760
+ if (value === null) {
761
+ element.removeAttribute(key)
762
+ return true
763
+ }
764
+ switch (typeof value) {
765
+ case "undefined":
766
+ case "function":
767
+ case "symbol": {
768
+ element.removeAttribute(key)
769
+ return true
770
+ }
771
+ case "boolean": {
772
+ if (isBoolAttr && !value) {
773
+ element.removeAttribute(key)
774
+ return true
775
+ }
776
+ }
777
+ }
778
+
779
+ return false
780
+ }
781
+
782
+ function createElementValueReader(dom: Exclude<SomeDom, Text>) {
783
+ if (dom.nodeName === "INPUT") {
784
+ return createInputValueReader(dom as HTMLInputElement)
785
+ }
786
+ if (dom.nodeName === "SELECT") {
787
+ return () => getSelectElementValue(dom as HTMLSelectElement)
788
+ }
789
+ return () => (dom as any).value
790
+ }
791
+
792
+ function createInputValueReader(dom: HTMLInputElement): () => any {
793
+ const t = dom.type
794
+ if (numericValueInputTypes.has(t)) {
795
+ return () => dom.valueAsNumber
796
+ }
797
+ return () => dom.value
798
+ }
799
+
800
+ function getSelectElementValue(dom: HTMLSelectElement) {
801
+ if (dom.multiple) {
802
+ return Array.from(dom.selectedOptions).map((option) => option.value)
803
+ }
804
+ return dom.value
805
+ }
806
+
807
+ function emitSignalAttrUpdate(vNode: VNode) {
808
+ if (!isBrowser) return
809
+ window.__kiru?.profilingContext?.emit("signalAttrUpdate", getVNodeApp(vNode)!)
810
+ }
811
+
812
+ function isElementNode(dom: SomeDom): dom is HTMLElement | SVGElement {
813
+ return dom.nodeType === 1
814
+ }
815
+
816
+ function isTextNode(dom: SomeDom): dom is Text {
817
+ return dom.nodeType === 3
818
+ }