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.
- package/LICENSE +7 -0
- package/README.md +5 -0
- package/package.json +81 -0
- package/src/appContext.ts +186 -0
- package/src/cloneVNode.ts +14 -0
- package/src/constants.ts +146 -0
- package/src/context.ts +56 -0
- package/src/dom.ts +712 -0
- package/src/element.ts +54 -0
- package/src/env.ts +6 -0
- package/src/error.ts +85 -0
- package/src/flags.ts +15 -0
- package/src/form/index.ts +662 -0
- package/src/form/types.ts +261 -0
- package/src/form/utils.ts +19 -0
- package/src/generateId.ts +19 -0
- package/src/globalContext.ts +161 -0
- package/src/globals.ts +21 -0
- package/src/hmr.ts +178 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAsync.ts +136 -0
- package/src/hooks/useCallback.ts +31 -0
- package/src/hooks/useContext.ts +79 -0
- package/src/hooks/useEffect.ts +44 -0
- package/src/hooks/useEffectEvent.ts +24 -0
- package/src/hooks/useId.ts +42 -0
- package/src/hooks/useLayoutEffect.ts +47 -0
- package/src/hooks/useMemo.ts +33 -0
- package/src/hooks/useReducer.ts +50 -0
- package/src/hooks/useRef.ts +40 -0
- package/src/hooks/useState.ts +62 -0
- package/src/hooks/useSyncExternalStore.ts +59 -0
- package/src/hooks/useViewTransition.ts +26 -0
- package/src/hooks/utils.ts +259 -0
- package/src/hydration.ts +67 -0
- package/src/index.ts +61 -0
- package/src/jsx.ts +11 -0
- package/src/lazy.ts +238 -0
- package/src/memo.ts +48 -0
- package/src/portal.ts +43 -0
- package/src/profiling.ts +105 -0
- package/src/props.ts +36 -0
- package/src/reconciler.ts +531 -0
- package/src/renderToString.ts +91 -0
- package/src/router/index.ts +2 -0
- package/src/router/route.ts +51 -0
- package/src/router/router.ts +275 -0
- package/src/router/routerUtils.ts +49 -0
- package/src/scheduler.ts +522 -0
- package/src/signals/base.ts +237 -0
- package/src/signals/computed.ts +139 -0
- package/src/signals/effect.ts +60 -0
- package/src/signals/globals.ts +11 -0
- package/src/signals/index.ts +12 -0
- package/src/signals/jsx.ts +45 -0
- package/src/signals/types.ts +10 -0
- package/src/signals/utils.ts +12 -0
- package/src/signals/watch.ts +151 -0
- package/src/ssr/client.ts +29 -0
- package/src/ssr/hydrationBoundary.ts +63 -0
- package/src/ssr/index.ts +1 -0
- package/src/ssr/server.ts +124 -0
- package/src/store.ts +241 -0
- package/src/swr.ts +360 -0
- package/src/transition.ts +80 -0
- package/src/types.dom.ts +1250 -0
- package/src/types.ts +209 -0
- package/src/types.utils.ts +39 -0
- package/src/utils.ts +581 -0
- 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
|
+
}
|