@remix-run/ui 0.0.0 → 0.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.
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/animation/animate-layout-mixin.d.ts +12 -0
- package/dist/animation/animate-layout-mixin.js +214 -0
- package/dist/animation/animate-layout-mixin.js.map +1 -0
- package/dist/animation/animate-mixins.d.ts +29 -0
- package/dist/animation/animate-mixins.js +235 -0
- package/dist/animation/animate-mixins.js.map +1 -0
- package/dist/animation/index.d.ts +6 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/spring.d.ts +75 -0
- package/dist/animation/spring.js +183 -0
- package/dist/animation/spring.js.map +1 -0
- package/dist/animation/tween.d.ts +70 -0
- package/dist/animation/tween.js +93 -0
- package/dist/animation/tween.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +92 -0
- package/dist/components/accordion/accordion.js +337 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/anchor/anchor.d.ts +13 -0
- package/dist/components/anchor/anchor.js +558 -0
- package/dist/components/anchor/anchor.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/button/button.d.ts +23 -0
- package/dist/components/button/button.js +147 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +101 -0
- package/dist/components/combobox/combobox.js +708 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/glyph/glyph.d.ts +14 -0
- package/dist/components/glyph/glyph.js +65 -0
- package/dist/components/glyph/glyph.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +67 -0
- package/dist/components/listbox/listbox.js +340 -0
- package/dist/components/listbox/listbox.js.map +1 -0
- package/dist/components/menu/hover-aim.d.ts +5 -0
- package/dist/components/menu/hover-aim.js +308 -0
- package/dist/components/menu/hover-aim.js.map +1 -0
- package/dist/components/menu/menu.d.ts +164 -0
- package/dist/components/menu/menu.js +1106 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/popover/popover.d.ts +35 -0
- package/dist/components/popover/popover.js +138 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.js +397 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/separator/separator.d.ts +2 -0
- package/dist/components/separator/separator.js +15 -0
- package/dist/components/separator/separator.js.map +1 -0
- package/dist/components/tabs/tabs.d.ts +78 -0
- package/dist/components/tabs/tabs.js +341 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/keydown/keydown.d.ts +1 -0
- package/dist/interactions/keydown/keydown.js +8 -0
- package/dist/interactions/keydown/keydown.js.map +1 -0
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
- package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
- package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
- package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
- package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +3 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/runtime/client-entries.d.ts +91 -0
- package/dist/runtime/client-entries.js +40 -0
- package/dist/runtime/client-entries.js.map +1 -0
- package/dist/runtime/component.d.ts +223 -0
- package/dist/runtime/component.js +152 -0
- package/dist/runtime/component.js.map +1 -0
- package/dist/runtime/create-element.d.ts +10 -0
- package/dist/runtime/create-element.js +29 -0
- package/dist/runtime/create-element.js.map +1 -0
- package/dist/runtime/diff-dom.d.ts +2 -0
- package/dist/runtime/diff-dom.js +364 -0
- package/dist/runtime/diff-dom.js.map +1 -0
- package/dist/runtime/diff-props.d.ts +9 -0
- package/dist/runtime/diff-props.js +195 -0
- package/dist/runtime/diff-props.js.map +1 -0
- package/dist/runtime/document-state.d.ts +11 -0
- package/dist/runtime/document-state.js +106 -0
- package/dist/runtime/document-state.js.map +1 -0
- package/dist/runtime/dom.d.ts +3189 -0
- package/dist/runtime/dom.js +2 -0
- package/dist/runtime/dom.js.map +1 -0
- package/dist/runtime/error-event.d.ts +20 -0
- package/dist/runtime/error-event.js +19 -0
- package/dist/runtime/error-event.js.map +1 -0
- package/dist/runtime/event-listeners.d.ts +50 -0
- package/dist/runtime/event-listeners.js +31 -0
- package/dist/runtime/event-listeners.js.map +1 -0
- package/dist/runtime/frame.d.ts +119 -0
- package/dist/runtime/frame.js +915 -0
- package/dist/runtime/frame.js.map +1 -0
- package/dist/runtime/invariant.d.ts +23 -0
- package/dist/runtime/invariant.js +33 -0
- package/dist/runtime/invariant.js.map +1 -0
- package/dist/runtime/jsx.d.ts +320 -0
- package/dist/runtime/jsx.js +31 -0
- package/dist/runtime/jsx.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
- package/dist/runtime/mixins/attrs-mixin.js +23 -0
- package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.d.ts +17 -0
- package/dist/runtime/mixins/link-mixin.js +101 -0
- package/dist/runtime/mixins/link-mixin.js.map +1 -0
- package/dist/runtime/mixins/mixin.d.ts +145 -0
- package/dist/runtime/mixins/mixin.js +570 -0
- package/dist/runtime/mixins/mixin.js.map +1 -0
- package/dist/runtime/mixins/on-mixin.d.ts +16 -0
- package/dist/runtime/mixins/on-mixin.js +49 -0
- package/dist/runtime/mixins/on-mixin.js.map +1 -0
- package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
- package/dist/runtime/mixins/ref-mixin.js +21 -0
- package/dist/runtime/mixins/ref-mixin.js.map +1 -0
- package/dist/runtime/navigation.d.ts +22 -0
- package/dist/runtime/navigation.js +102 -0
- package/dist/runtime/navigation.js.map +1 -0
- package/dist/runtime/reconcile.d.ts +11 -0
- package/dist/runtime/reconcile.js +1240 -0
- package/dist/runtime/reconcile.js.map +1 -0
- package/dist/runtime/render.d.ts +54 -0
- package/dist/runtime/render.js +50 -0
- package/dist/runtime/render.js.map +1 -0
- package/dist/runtime/run.d.ts +45 -0
- package/dist/runtime/run.js +68 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +30 -0
- package/dist/runtime/scheduler.js +185 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/svg-attributes.d.ts +5 -0
- package/dist/runtime/svg-attributes.js +124 -0
- package/dist/runtime/svg-attributes.js.map +1 -0
- package/dist/runtime/to-vnode.d.ts +3 -0
- package/dist/runtime/to-vnode.js +43 -0
- package/dist/runtime/to-vnode.js.map +1 -0
- package/dist/runtime/typed-event-target.d.ts +50 -0
- package/dist/runtime/typed-event-target.js +6 -0
- package/dist/runtime/typed-event-target.js.map +1 -0
- package/dist/runtime/vdom.d.ts +55 -0
- package/dist/runtime/vdom.js +198 -0
- package/dist/runtime/vdom.js.map +1 -0
- package/dist/runtime/vnode.d.ts +79 -0
- package/dist/runtime/vnode.js +38 -0
- package/dist/runtime/vnode.js.map +1 -0
- package/dist/server/stream.d.ts +57 -0
- package/dist/server/stream.js +1007 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/style/css-mixin.d.ts +8 -0
- package/dist/style/css-mixin.js +54 -0
- package/dist/style/css-mixin.js.map +1 -0
- package/dist/style/index.d.ts +5 -0
- package/dist/style/index.js +4 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/style.d.ts +24 -0
- package/dist/style/style.js +269 -0
- package/dist/style/style.js.map +1 -0
- package/dist/style/stylesheet.d.ts +11 -0
- package/dist/style/stylesheet.js +174 -0
- package/dist/style/stylesheet.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +2 -0
- package/dist/test.js.map +1 -0
- package/dist/theme/contract.d.ts +259 -0
- package/dist/theme/contract.js +131 -0
- package/dist/theme/contract.js.map +1 -0
- package/dist/theme/glyph-contract.d.ts +13 -0
- package/dist/theme/glyph-contract.js +34 -0
- package/dist/theme/glyph-contract.js.map +1 -0
- package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
- package/dist/theme/presets/rmx-01/glyphs.js +252 -0
- package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
- package/dist/theme/presets/rmx-01/index.d.ts +2 -0
- package/dist/theme/presets/rmx-01/index.js +119 -0
- package/dist/theme/presets/rmx-01/index.js.map +1 -0
- package/dist/theme/runtime.d.ts +2 -0
- package/dist/theme/runtime.js +81 -0
- package/dist/theme/runtime.js.map +1 -0
- package/dist/theme/theme.d.ts +6 -0
- package/dist/theme/theme.js +5 -0
- package/dist/theme/theme.js.map +1 -0
- package/dist/utils/flash-attribute.d.ts +1 -0
- package/dist/utils/flash-attribute.js +11 -0
- package/dist/utils/flash-attribute.js.map +1 -0
- package/dist/utils/scroll-lock.d.ts +3 -0
- package/dist/utils/scroll-lock.js +69 -0
- package/dist/utils/scroll-lock.js.map +1 -0
- package/dist/utils/wait-for-css-transition.d.ts +1 -0
- package/dist/utils/wait-for-css-transition.js +58 -0
- package/dist/utils/wait-for-css-transition.js.map +1 -0
- package/dist/utils/wait.d.ts +1 -0
- package/dist/utils/wait.js +6 -0
- package/dist/utils/wait.js.map +1 -0
- package/package.json +125 -5
- package/src/animation/animate-layout-mixin.ts +266 -0
- package/src/animation/animate-mixins.ts +292 -0
- package/src/animation/index.ts +6 -0
- package/src/animation/spring.ts +299 -0
- package/src/animation/tween.ts +134 -0
- package/src/components/accordion/accordion.tsx +565 -0
- package/src/components/anchor/anchor.ts +904 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
- package/src/components/button/README.md +44 -0
- package/src/components/button/button.tsx +188 -0
- package/src/components/combobox/README.md +145 -0
- package/src/components/combobox/combobox.tsx +1014 -0
- package/src/components/glyph/glyph.tsx +110 -0
- package/src/components/listbox/listbox.ts +447 -0
- package/src/components/menu/hover-aim.ts +428 -0
- package/src/components/menu/menu.tsx +1547 -0
- package/src/components/popover/README.md +122 -0
- package/src/components/popover/popover.ts +201 -0
- package/src/components/select/select.tsx +585 -0
- package/src/components/separator/separator.ts +19 -0
- package/src/components/tabs/README.md +105 -0
- package/src/components/tabs/tabs.tsx +499 -0
- package/src/index.ts +72 -0
- package/src/interactions/keydown/keydown.ts +14 -0
- package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
- package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/runtime/client-entries.ts +137 -0
- package/src/runtime/component.ts +402 -0
- package/src/runtime/create-element.ts +38 -0
- package/src/runtime/diff-dom.ts +404 -0
- package/src/runtime/diff-props.ts +209 -0
- package/src/runtime/document-state.ts +130 -0
- package/src/runtime/dom.ts +3971 -0
- package/src/runtime/error-event.ts +26 -0
- package/src/runtime/event-listeners.ts +171 -0
- package/src/runtime/frame.ts +1211 -0
- package/src/runtime/invariant.ts +31 -0
- package/src/runtime/jsx.ts +398 -0
- package/src/runtime/mixins/attrs-mixin.ts +35 -0
- package/src/runtime/mixins/link-mixin.ts +131 -0
- package/src/runtime/mixins/mixin.ts +908 -0
- package/src/runtime/mixins/on-mixin.ts +89 -0
- package/src/runtime/mixins/ref-mixin.ts +32 -0
- package/src/runtime/navigation.ts +136 -0
- package/src/runtime/reconcile.ts +1722 -0
- package/src/runtime/render.ts +101 -0
- package/src/runtime/run.ts +100 -0
- package/src/runtime/scheduler.ts +262 -0
- package/src/runtime/svg-attributes.ts +133 -0
- package/src/runtime/to-vnode.ts +50 -0
- package/src/runtime/typed-event-target.ts +67 -0
- package/src/runtime/vdom.ts +295 -0
- package/src/runtime/vnode.ts +137 -0
- package/src/server/stream.ts +1342 -0
- package/src/style/css-mixin.ts +82 -0
- package/src/style/index.ts +10 -0
- package/src/style/style.ts +312 -0
- package/src/style/stylesheet.ts +209 -0
- package/src/test/setup.ts +7 -0
- package/src/test/utils.ts +45 -0
- package/src/test.ts +2 -0
- package/src/theme/contract.ts +183 -0
- package/src/theme/glyph-contract.ts +57 -0
- package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
- package/src/theme/presets/rmx-01/index.ts +121 -0
- package/src/theme/runtime.ts +116 -0
- package/src/theme/theme.ts +15 -0
- package/src/utils/flash-attribute.ts +11 -0
- package/src/utils/scroll-lock.ts +97 -0
- package/src/utils/wait-for-css-transition.ts +66 -0
- package/src/utils/wait.ts +5 -0
|
@@ -0,0 +1,1722 @@
|
|
|
1
|
+
import type { Component, ComponentHandle, FrameContent, FrameHandle } from './component.ts'
|
|
2
|
+
import { createComponent, Frame } from './component.ts'
|
|
3
|
+
import type { Frame as FrameInstance, FrameRuntime } from './frame.ts'
|
|
4
|
+
import { createFrame } from './frame.ts'
|
|
5
|
+
import { createRangeRoot } from './vdom.ts'
|
|
6
|
+
import type {
|
|
7
|
+
ComponentNode,
|
|
8
|
+
CommittedComponentNode,
|
|
9
|
+
CommittedHostNode,
|
|
10
|
+
CommittedTextNode,
|
|
11
|
+
HostNode,
|
|
12
|
+
TextNode,
|
|
13
|
+
VNode,
|
|
14
|
+
VNodeType,
|
|
15
|
+
} from './vnode.ts'
|
|
16
|
+
import {
|
|
17
|
+
isCommittedComponentNode,
|
|
18
|
+
isComponentNode,
|
|
19
|
+
isCommittedHostNode,
|
|
20
|
+
isCommittedTextNode,
|
|
21
|
+
isFragmentNode,
|
|
22
|
+
isHostNode,
|
|
23
|
+
isTextNode,
|
|
24
|
+
findContextFromAncestry,
|
|
25
|
+
} from './vnode.ts'
|
|
26
|
+
import { invariant } from './invariant.ts'
|
|
27
|
+
import { diffHostProps } from './diff-props.ts'
|
|
28
|
+
import type { StyleManager } from '../style/index.ts'
|
|
29
|
+
import type { ElementProps } from './jsx.ts'
|
|
30
|
+
import { skipComments, logHydrationMismatch } from './client-entries.ts'
|
|
31
|
+
import type { Scheduler } from './scheduler.ts'
|
|
32
|
+
import { toVNode } from './to-vnode.ts'
|
|
33
|
+
import {
|
|
34
|
+
bindMixinRuntime,
|
|
35
|
+
cancelPendingMixinRemoval,
|
|
36
|
+
dispatchMixinBeforeUpdate,
|
|
37
|
+
dispatchMixinCommit,
|
|
38
|
+
getMixinRuntimeSignal,
|
|
39
|
+
prepareMixinRemoval,
|
|
40
|
+
resolveMixedProps,
|
|
41
|
+
teardownMixins,
|
|
42
|
+
type MixinRuntimeBinding,
|
|
43
|
+
type MixinRuntimeState,
|
|
44
|
+
} from './mixins/mixin.ts'
|
|
45
|
+
|
|
46
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
47
|
+
|
|
48
|
+
// Internal diffing flags (modeled after Preact)
|
|
49
|
+
const INSERT_VNODE = 1 << 0
|
|
50
|
+
const MATCHED = 1 << 1
|
|
51
|
+
|
|
52
|
+
let idCounter = 0
|
|
53
|
+
let persistedRemovalToken = 0
|
|
54
|
+
const persistedMixinNodes = new Set<CommittedHostNode>()
|
|
55
|
+
let activeSchedulerUpdateParents: ParentNode[] | undefined
|
|
56
|
+
|
|
57
|
+
// Compute SVG context for a node based on its parent and type.
|
|
58
|
+
// Returns true if the node is within an SVG subtree, false otherwise.
|
|
59
|
+
function getSvgContext(vParent: VNode, nodeType: VNodeType): boolean {
|
|
60
|
+
// Only host elements (strings) can affect SVG context
|
|
61
|
+
if (typeof nodeType === 'string') {
|
|
62
|
+
// svg element creates SVG context
|
|
63
|
+
if (nodeType === 'svg') return true
|
|
64
|
+
// foreignObject switches back to HTML context
|
|
65
|
+
if (nodeType === 'foreignObject') return false
|
|
66
|
+
}
|
|
67
|
+
// Otherwise inherit from parent
|
|
68
|
+
return vParent._svg ?? false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getHostProps(node: HostNode | CommittedHostNode): ElementProps {
|
|
72
|
+
return node._mixedProps ?? node.props
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function markNodePersistedByMixins(node: CommittedHostNode, domParent: ParentNode, token: number) {
|
|
76
|
+
node._persistedByMixins = true
|
|
77
|
+
node._persistedParentByMixins = domParent
|
|
78
|
+
node._persistedRemovalToken = token
|
|
79
|
+
persistedMixinNodes.add(node)
|
|
80
|
+
bindMixinRuntime(node._mixState as MixinRuntimeState | undefined, undefined)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function unmarkNodePersistedByMixins(node: CommittedHostNode) {
|
|
84
|
+
node._persistedByMixins = false
|
|
85
|
+
node._persistedParentByMixins = undefined
|
|
86
|
+
node._persistedRemovalToken = undefined
|
|
87
|
+
persistedMixinNodes.delete(node)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findMatchingPersistedMixinNode(
|
|
91
|
+
type: string,
|
|
92
|
+
key: string | undefined,
|
|
93
|
+
domParent: ParentNode,
|
|
94
|
+
): CommittedHostNode | null {
|
|
95
|
+
if (key == null) return null
|
|
96
|
+
for (let node of persistedMixinNodes) {
|
|
97
|
+
if (node._persistedParentByMixins !== domParent) continue
|
|
98
|
+
if (node.type !== type) continue
|
|
99
|
+
if (node.key !== key) continue
|
|
100
|
+
return node
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type ControlledReflectionState = {
|
|
106
|
+
disposed: boolean
|
|
107
|
+
listenersAttached: boolean
|
|
108
|
+
pendingRestoreVersion: number
|
|
109
|
+
managesValue: boolean
|
|
110
|
+
managesChecked: boolean
|
|
111
|
+
hasControlledValue: boolean
|
|
112
|
+
controlledValue: unknown
|
|
113
|
+
hasControlledChecked: boolean
|
|
114
|
+
controlledChecked: unknown
|
|
115
|
+
onInput: () => void
|
|
116
|
+
onChange: () => void
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function shouldRestoreControlledReflectionOnInput(
|
|
120
|
+
node: CommittedHostNode,
|
|
121
|
+
state: ControlledReflectionState,
|
|
122
|
+
): boolean {
|
|
123
|
+
// Some controls dispatch `input` before `change` for the same interaction.
|
|
124
|
+
// When checked/value state is typically handled on `change`, restoring on the
|
|
125
|
+
// earlier `input` can race and clobber the value observed by app handlers.
|
|
126
|
+
if (state.hasControlledChecked) return false
|
|
127
|
+
if (node.type === 'select') return false
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ensureControlledReflection(
|
|
132
|
+
node: CommittedHostNode,
|
|
133
|
+
scheduler: Scheduler,
|
|
134
|
+
): ControlledReflectionState {
|
|
135
|
+
let existing = node._controlledState as ControlledReflectionState | undefined
|
|
136
|
+
if (existing) return existing
|
|
137
|
+
|
|
138
|
+
let state: ControlledReflectionState = {
|
|
139
|
+
disposed: false,
|
|
140
|
+
listenersAttached: false,
|
|
141
|
+
pendingRestoreVersion: 0,
|
|
142
|
+
managesValue: false,
|
|
143
|
+
managesChecked: false,
|
|
144
|
+
hasControlledValue: false,
|
|
145
|
+
controlledValue: undefined,
|
|
146
|
+
hasControlledChecked: false,
|
|
147
|
+
controlledChecked: undefined,
|
|
148
|
+
onInput: () => {
|
|
149
|
+
if (!shouldRestoreControlledReflectionOnInput(node, state)) return
|
|
150
|
+
scheduleControlledRestore(node, state)
|
|
151
|
+
},
|
|
152
|
+
onChange: () => {
|
|
153
|
+
scheduleControlledRestore(node, state)
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
node._controlledState = state
|
|
158
|
+
scheduler.enqueueTasks([
|
|
159
|
+
() => {
|
|
160
|
+
if (state.disposed) return
|
|
161
|
+
node._dom.addEventListener('input', state.onInput)
|
|
162
|
+
node._dom.addEventListener('change', state.onChange)
|
|
163
|
+
state.listenersAttached = true
|
|
164
|
+
},
|
|
165
|
+
])
|
|
166
|
+
return state
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function syncControlledReflection(node: CommittedHostNode, props: ElementProps): void {
|
|
170
|
+
let state = node._controlledState as ControlledReflectionState | undefined
|
|
171
|
+
if (!state || state.disposed) return
|
|
172
|
+
|
|
173
|
+
state.managesValue = canManageValue(node.type, node._dom)
|
|
174
|
+
state.managesChecked = canReflectProperty(node._dom, 'checked')
|
|
175
|
+
state.hasControlledValue = state.managesValue && hasControlledValueProp(props)
|
|
176
|
+
state.controlledValue = props.value
|
|
177
|
+
state.hasControlledChecked = state.managesChecked && hasControlledCheckedProp(props)
|
|
178
|
+
state.controlledChecked = props.checked
|
|
179
|
+
state.pendingRestoreVersion++
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function shouldTrackControlledReflection(props: ElementProps): boolean {
|
|
183
|
+
return hasControlledValueProp(props) || hasControlledCheckedProp(props)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function scheduleControlledRestore(
|
|
187
|
+
node: CommittedHostNode,
|
|
188
|
+
state: ControlledReflectionState,
|
|
189
|
+
): void {
|
|
190
|
+
if (state.disposed) return
|
|
191
|
+
let version = ++state.pendingRestoreVersion
|
|
192
|
+
queueMicrotask(() => {
|
|
193
|
+
if (state.disposed) return
|
|
194
|
+
if (state.pendingRestoreVersion !== version) return
|
|
195
|
+
restoreControlledReflections(node, state)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function restoreControlledReflections(
|
|
200
|
+
node: CommittedHostNode,
|
|
201
|
+
state: ControlledReflectionState,
|
|
202
|
+
): void {
|
|
203
|
+
let element = node._dom
|
|
204
|
+
if (state.hasControlledValue && readDomProp(element, 'value') !== state.controlledValue) {
|
|
205
|
+
setPropertyReflection(element, 'value', state.controlledValue)
|
|
206
|
+
}
|
|
207
|
+
if (state.hasControlledChecked && readDomProp(element, 'checked') !== state.controlledChecked) {
|
|
208
|
+
setPropertyReflection(element, 'checked', state.controlledChecked)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function teardownControlledReflection(node: CommittedHostNode): void {
|
|
213
|
+
let state = node._controlledState as ControlledReflectionState | undefined
|
|
214
|
+
if (!state) return
|
|
215
|
+
state.disposed = true
|
|
216
|
+
state.pendingRestoreVersion++
|
|
217
|
+
if (state.listenersAttached) {
|
|
218
|
+
node._dom.removeEventListener('input', state.onInput)
|
|
219
|
+
node._dom.removeEventListener('change', state.onChange)
|
|
220
|
+
state.listenersAttached = false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function canManageValue(type: string, element: Element): boolean {
|
|
225
|
+
if (type === 'progress') return false
|
|
226
|
+
return canReflectProperty(element, 'value')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hasControlledValueProp(props: ElementProps): boolean {
|
|
230
|
+
return 'value' in props && props.value !== undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasControlledCheckedProp(props: ElementProps): boolean {
|
|
234
|
+
return 'checked' in props && props.checked !== undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function canReflectProperty(
|
|
238
|
+
element: Element,
|
|
239
|
+
key: string,
|
|
240
|
+
): element is Element & Record<string, unknown> {
|
|
241
|
+
return key in element && !key.includes('-')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readDomProp(element: Element, key: string): unknown {
|
|
245
|
+
if (!canReflectProperty(element, key)) return undefined
|
|
246
|
+
return element[key]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function setPropertyReflection(element: Element, key: string, value: unknown): void {
|
|
250
|
+
if (!canReflectProperty(element, key)) return
|
|
251
|
+
element[key] = value == null ? '' : value
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveNodeMixProps(
|
|
255
|
+
node: HostNode,
|
|
256
|
+
frame: FrameHandle,
|
|
257
|
+
scheduler: Scheduler,
|
|
258
|
+
state?: MixinRuntimeState,
|
|
259
|
+
): ElementProps {
|
|
260
|
+
let mix = node.props.mix
|
|
261
|
+
if (state == null && (mix == null || (Array.isArray(mix) && mix.length === 0))) {
|
|
262
|
+
node._mixState = undefined
|
|
263
|
+
node._mixedProps = node.props
|
|
264
|
+
return node.props
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let resolved = resolveMixedProps({
|
|
268
|
+
hostType: node.type,
|
|
269
|
+
frame,
|
|
270
|
+
scheduler,
|
|
271
|
+
getContext: (type: Component | string | symbol) => {
|
|
272
|
+
if (typeof type !== 'function') {
|
|
273
|
+
return undefined
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return findContextFromAncestry(node, type as Component)
|
|
277
|
+
},
|
|
278
|
+
props: node.props,
|
|
279
|
+
state,
|
|
280
|
+
})
|
|
281
|
+
node._mixState = resolved.state
|
|
282
|
+
node._mixedProps = resolved.props
|
|
283
|
+
return resolved.props
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function enqueueMixinBindingUpdate(
|
|
287
|
+
this: MixinRuntimeBinding,
|
|
288
|
+
done: (signal: AbortSignal) => void,
|
|
289
|
+
): void {
|
|
290
|
+
let node = this.target as CommittedHostNode
|
|
291
|
+
let state = node._mixState as MixinRuntimeState | undefined
|
|
292
|
+
this.scheduler.enqueueWork([
|
|
293
|
+
() => {
|
|
294
|
+
if (state?.aborted) {
|
|
295
|
+
done(getMixinRuntimeSignal(state))
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
dispatchMixinBeforeUpdate(state)
|
|
300
|
+
let prevProps = getHostProps(node)
|
|
301
|
+
let nextProps = resolveNodeMixProps(node, this.frame, this.scheduler, state)
|
|
302
|
+
diffHostProps(prevProps, nextProps, this.node)
|
|
303
|
+
|
|
304
|
+
dispatchMixinCommit(state)
|
|
305
|
+
done(state ? getMixinRuntimeSignal(state) : AbortSignal.abort())
|
|
306
|
+
},
|
|
307
|
+
])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function bindNodeMixRuntime(
|
|
311
|
+
node: CommittedHostNode,
|
|
312
|
+
frame: FrameHandle,
|
|
313
|
+
scheduler: Scheduler,
|
|
314
|
+
styles: StyleManager,
|
|
315
|
+
reclaimed: boolean = false,
|
|
316
|
+
parent?: ParentNode,
|
|
317
|
+
) {
|
|
318
|
+
let state = node._mixState as MixinRuntimeState | undefined
|
|
319
|
+
bindMixinRuntime(
|
|
320
|
+
state,
|
|
321
|
+
{
|
|
322
|
+
node: node._dom,
|
|
323
|
+
parent: parent ?? (node._dom.parentNode as ParentNode),
|
|
324
|
+
key: node.key,
|
|
325
|
+
target: node,
|
|
326
|
+
frame,
|
|
327
|
+
scheduler,
|
|
328
|
+
enqueueUpdate: enqueueMixinBindingUpdate,
|
|
329
|
+
},
|
|
330
|
+
{ dispatchReclaimed: reclaimed },
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isHeadHostNode(node: HostNode): boolean {
|
|
335
|
+
return node.type.toLowerCase() === 'head'
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getDocumentHead(domParent: ParentNode): HTMLHeadElement | null {
|
|
339
|
+
if (domParent instanceof Document) {
|
|
340
|
+
return domParent.head
|
|
341
|
+
}
|
|
342
|
+
if (domParent instanceof Node) {
|
|
343
|
+
return domParent.ownerDocument?.head ?? null
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function diffVNodes(
|
|
349
|
+
curr: VNode | null,
|
|
350
|
+
next: VNode,
|
|
351
|
+
domParent: ParentNode,
|
|
352
|
+
frame: FrameHandle,
|
|
353
|
+
scheduler: Scheduler,
|
|
354
|
+
styles: StyleManager,
|
|
355
|
+
vParent: VNode,
|
|
356
|
+
rootTarget: EventTarget,
|
|
357
|
+
anchor?: Node,
|
|
358
|
+
rootCursor?: Node | null,
|
|
359
|
+
): Node | null | undefined {
|
|
360
|
+
next._parent = vParent // set parent for initial render context lookups
|
|
361
|
+
next._svg = getSvgContext(vParent, next.type)
|
|
362
|
+
|
|
363
|
+
// new
|
|
364
|
+
if (curr === null) {
|
|
365
|
+
return insert(
|
|
366
|
+
next,
|
|
367
|
+
domParent,
|
|
368
|
+
frame,
|
|
369
|
+
scheduler,
|
|
370
|
+
styles,
|
|
371
|
+
vParent,
|
|
372
|
+
rootTarget,
|
|
373
|
+
anchor,
|
|
374
|
+
rootCursor,
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (curr.type !== next.type) {
|
|
379
|
+
replace(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)
|
|
380
|
+
return rootCursor
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (isCommittedTextNode(curr) && isTextNode(next)) {
|
|
384
|
+
diffText(curr, next, vParent)
|
|
385
|
+
return rootCursor
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (isCommittedHostNode(curr) && isHostNode(next)) {
|
|
389
|
+
diffHost(curr, next, frame, scheduler, styles, vParent, rootTarget)
|
|
390
|
+
return rootCursor
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (isCommittedComponentNode(curr) && isComponentNode(next)) {
|
|
394
|
+
diffComponent(curr, next, frame, scheduler, styles, domParent, vParent, rootTarget)
|
|
395
|
+
return rootCursor
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (isFragmentNode(curr) && isFragmentNode(next)) {
|
|
399
|
+
diffChildren(
|
|
400
|
+
curr._children,
|
|
401
|
+
next._children,
|
|
402
|
+
domParent,
|
|
403
|
+
frame,
|
|
404
|
+
scheduler,
|
|
405
|
+
styles,
|
|
406
|
+
vParent,
|
|
407
|
+
rootTarget,
|
|
408
|
+
undefined,
|
|
409
|
+
anchor,
|
|
410
|
+
)
|
|
411
|
+
return rootCursor
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (curr.type === Frame && next.type === Frame) {
|
|
415
|
+
diffFrame(curr, next, domParent, frame, scheduler, styles, vParent, rootTarget, anchor)
|
|
416
|
+
return rootCursor
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
invariant(false, 'Unexpected diff case')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function replace(
|
|
423
|
+
curr: VNode,
|
|
424
|
+
next: VNode,
|
|
425
|
+
domParent: ParentNode,
|
|
426
|
+
frame: FrameHandle,
|
|
427
|
+
scheduler: Scheduler,
|
|
428
|
+
styles: StyleManager,
|
|
429
|
+
vParent: VNode,
|
|
430
|
+
rootTarget: EventTarget,
|
|
431
|
+
anchor?: Node,
|
|
432
|
+
) {
|
|
433
|
+
let currAnchor = findFirstDomAnchor(curr)
|
|
434
|
+
if (currAnchor && currAnchor.parentNode === domParent) {
|
|
435
|
+
let replacementAnchor = document.createComment('rmx:replace')
|
|
436
|
+
domParent.insertBefore(replacementAnchor, currAnchor)
|
|
437
|
+
try {
|
|
438
|
+
remove(curr, domParent, scheduler, styles)
|
|
439
|
+
insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
|
|
440
|
+
} finally {
|
|
441
|
+
replacementAnchor.parentNode?.removeChild(replacementAnchor)
|
|
442
|
+
}
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let replacementAnchor = findNextSiblingDomAnchor(curr, vParent) ?? anchor
|
|
447
|
+
remove(curr, domParent, scheduler, styles)
|
|
448
|
+
insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replacementAnchor)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function diffHost(
|
|
452
|
+
curr: CommittedHostNode,
|
|
453
|
+
next: HostNode,
|
|
454
|
+
frame: FrameHandle,
|
|
455
|
+
scheduler: Scheduler,
|
|
456
|
+
styles: StyleManager,
|
|
457
|
+
vParent: VNode,
|
|
458
|
+
rootTarget: EventTarget,
|
|
459
|
+
) {
|
|
460
|
+
let mixState = curr._mixState as MixinRuntimeState | undefined
|
|
461
|
+
let currProps = getHostProps(curr)
|
|
462
|
+
let nextProps = resolveNodeMixProps(next, frame, scheduler, mixState)
|
|
463
|
+
if (shouldDispatchInlineMixinLifecycle(curr._dom)) {
|
|
464
|
+
dispatchMixinBeforeUpdate(next._mixState as MixinRuntimeState | undefined)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Handle innerHTML prop BEFORE diffChildren to avoid clearing children
|
|
468
|
+
if (nextProps.innerHTML != null) {
|
|
469
|
+
// innerHTML is set, update it if changed
|
|
470
|
+
if (currProps.innerHTML !== nextProps.innerHTML) {
|
|
471
|
+
curr._dom.innerHTML = nextProps.innerHTML as string
|
|
472
|
+
}
|
|
473
|
+
} else if (currProps.innerHTML != null) {
|
|
474
|
+
// innerHTML was removed, clear it before adding children
|
|
475
|
+
curr._dom.innerHTML = ''
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
diffChildren(
|
|
479
|
+
curr._children,
|
|
480
|
+
next._children,
|
|
481
|
+
curr._dom,
|
|
482
|
+
frame,
|
|
483
|
+
scheduler,
|
|
484
|
+
styles,
|
|
485
|
+
next,
|
|
486
|
+
rootTarget,
|
|
487
|
+
)
|
|
488
|
+
diffHostProps(currProps, nextProps, curr._dom)
|
|
489
|
+
|
|
490
|
+
next._dom = curr._dom
|
|
491
|
+
next._parent = vParent
|
|
492
|
+
next._controller = curr._controller
|
|
493
|
+
next._controlledState = curr._controlledState
|
|
494
|
+
|
|
495
|
+
if (next._controlledState || shouldTrackControlledReflection(nextProps)) {
|
|
496
|
+
ensureControlledReflection(next as CommittedHostNode, scheduler)
|
|
497
|
+
syncControlledReflection(next as CommittedHostNode, nextProps)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
bindNodeMixRuntime(next as CommittedHostNode, frame, scheduler, styles)
|
|
501
|
+
if (shouldDispatchInlineMixinLifecycle(curr._dom)) {
|
|
502
|
+
scheduler.enqueueCommitPhase([
|
|
503
|
+
() => dispatchMixinCommit(next._mixState as MixinRuntimeState | undefined),
|
|
504
|
+
])
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function setupHostNode(node: HostNode, dom: Element, scheduler: Scheduler): void {
|
|
511
|
+
node._dom = dom
|
|
512
|
+
let props = getHostProps(node)
|
|
513
|
+
let committedNode = node as CommittedHostNode
|
|
514
|
+
|
|
515
|
+
if (shouldTrackControlledReflection(props)) {
|
|
516
|
+
ensureControlledReflection(committedNode, scheduler)
|
|
517
|
+
syncControlledReflection(committedNode, props)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function diffText(curr: CommittedTextNode, next: TextNode, vParent: VNode) {
|
|
522
|
+
if (curr._text !== next._text) {
|
|
523
|
+
curr._dom.textContent = next._text
|
|
524
|
+
}
|
|
525
|
+
next._dom = curr._dom
|
|
526
|
+
next._parent = vParent
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function insert(
|
|
530
|
+
node: VNode,
|
|
531
|
+
domParent: ParentNode,
|
|
532
|
+
frame: FrameHandle,
|
|
533
|
+
scheduler: Scheduler,
|
|
534
|
+
styles: StyleManager,
|
|
535
|
+
vParent: VNode,
|
|
536
|
+
rootTarget: EventTarget,
|
|
537
|
+
anchor?: Node,
|
|
538
|
+
cursor?: Node | null,
|
|
539
|
+
): Node | null | undefined {
|
|
540
|
+
node._parent = vParent // set parent for initial render context lookups
|
|
541
|
+
node._svg = getSvgContext(vParent, node.type)
|
|
542
|
+
|
|
543
|
+
// Stop hydration if cursor has reached the anchor (end boundary)
|
|
544
|
+
// Check BEFORE skipComments to prevent escaping range root markers
|
|
545
|
+
if (cursor && anchor && cursor === anchor) {
|
|
546
|
+
cursor = null
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
cursor =
|
|
550
|
+
node.type === Frame
|
|
551
|
+
? skipCommentsExceptFrameStart(cursor ?? null)
|
|
552
|
+
: skipComments(cursor ?? null)
|
|
553
|
+
|
|
554
|
+
// Also check after skipComments in case we skipped past the anchor
|
|
555
|
+
if (cursor && anchor && cursor === anchor) {
|
|
556
|
+
cursor = null
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let doInsert = anchor
|
|
560
|
+
? (dom: Node) => domParent.insertBefore(dom, anchor)
|
|
561
|
+
: (dom: Node) => domParent.appendChild(dom)
|
|
562
|
+
|
|
563
|
+
if (isTextNode(node)) {
|
|
564
|
+
if (cursor instanceof Text) {
|
|
565
|
+
node._parent = vParent
|
|
566
|
+
// Handle text node consolidation: server renders adjacent text as single node
|
|
567
|
+
// e.g., <span>Hello {world}</span> → server: "Hello world", client: ["Hello ", "world"]
|
|
568
|
+
if (cursor.data !== node._text) {
|
|
569
|
+
if (cursor.data.startsWith(node._text) && node._text.length < cursor.data.length) {
|
|
570
|
+
// Consolidation case: split the text node at the boundary
|
|
571
|
+
// cursor becomes the first part (node._text), remainder is returned for next vnode
|
|
572
|
+
let remainder = cursor.splitText(node._text.length)
|
|
573
|
+
node._dom = cursor
|
|
574
|
+
return remainder
|
|
575
|
+
}
|
|
576
|
+
// Genuine mismatch - correct it
|
|
577
|
+
logHydrationMismatch('text mismatch', cursor.data, node._text)
|
|
578
|
+
cursor.data = node._text
|
|
579
|
+
}
|
|
580
|
+
node._dom = cursor
|
|
581
|
+
return cursor.nextSibling
|
|
582
|
+
}
|
|
583
|
+
let dom = document.createTextNode(node._text)
|
|
584
|
+
node._dom = dom
|
|
585
|
+
node._parent = vParent
|
|
586
|
+
doInsert(dom)
|
|
587
|
+
return cursor
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (isHostNode(node)) {
|
|
591
|
+
let hostProps = resolveNodeMixProps(node, frame, scheduler)
|
|
592
|
+
|
|
593
|
+
if (isHeadHostNode(node)) {
|
|
594
|
+
let targetHead = getDocumentHead(domParent)
|
|
595
|
+
if (targetHead) {
|
|
596
|
+
let childCursor = cursor
|
|
597
|
+
if (cursor instanceof Element && cursor.tagName.toLowerCase() === 'head') {
|
|
598
|
+
childCursor = cursor.firstChild
|
|
599
|
+
let nextCursor = cursor.nextSibling
|
|
600
|
+
if (cursor !== targetHead) {
|
|
601
|
+
while (cursor.firstChild) {
|
|
602
|
+
targetHead.appendChild(cursor.firstChild)
|
|
603
|
+
}
|
|
604
|
+
cursor.remove()
|
|
605
|
+
}
|
|
606
|
+
cursor = nextCursor
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Render explicit <head> children directly into document.head.
|
|
610
|
+
diffChildren(
|
|
611
|
+
null,
|
|
612
|
+
node._children,
|
|
613
|
+
targetHead,
|
|
614
|
+
frame,
|
|
615
|
+
scheduler,
|
|
616
|
+
styles,
|
|
617
|
+
node,
|
|
618
|
+
rootTarget,
|
|
619
|
+
childCursor,
|
|
620
|
+
)
|
|
621
|
+
diffHostProps({}, hostProps, targetHead)
|
|
622
|
+
setupHostNode(node, targetHead, scheduler)
|
|
623
|
+
bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
|
|
624
|
+
return cursor
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Check for matching mixin-persisted node that can be reclaimed
|
|
629
|
+
let persistedNode = findMatchingPersistedMixinNode(node.type, node.key, domParent)
|
|
630
|
+
if (persistedNode) {
|
|
631
|
+
reclaimPersistedMixinNode(persistedNode, node, frame, scheduler, styles, vParent, rootTarget)
|
|
632
|
+
return cursor
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (cursor instanceof Element) {
|
|
636
|
+
// SVG elements have case-sensitive tag names (e.g. linearGradient, clipPath)
|
|
637
|
+
// HTML elements are case-insensitive, so we lowercase for comparison
|
|
638
|
+
let cursorTag = node._svg ? cursor.tagName : cursor.tagName.toLowerCase()
|
|
639
|
+
if (cursorTag === node.type) {
|
|
640
|
+
let nextCursor = cursor.nextSibling
|
|
641
|
+
diffHostProps({}, hostProps, cursor)
|
|
642
|
+
|
|
643
|
+
// Handle innerHTML prop
|
|
644
|
+
if (hostProps.innerHTML != null) {
|
|
645
|
+
cursor.innerHTML = hostProps.innerHTML as string
|
|
646
|
+
} else {
|
|
647
|
+
let childCursor = cursor.firstChild
|
|
648
|
+
// Ignore excess nodes - browser extensions may inject content
|
|
649
|
+
diffChildren(
|
|
650
|
+
null,
|
|
651
|
+
node._children,
|
|
652
|
+
cursor,
|
|
653
|
+
frame,
|
|
654
|
+
scheduler,
|
|
655
|
+
styles,
|
|
656
|
+
node,
|
|
657
|
+
rootTarget,
|
|
658
|
+
childCursor,
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
setupHostNode(node, cursor, scheduler)
|
|
663
|
+
bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
|
|
664
|
+
return nextCursor
|
|
665
|
+
} else {
|
|
666
|
+
// Type mismatch - try single-advance retry to handle browser extension injections
|
|
667
|
+
// at the start of containers. Skip this node and try the next sibling once.
|
|
668
|
+
let nextSibling = skipComments(cursor.nextSibling)
|
|
669
|
+
if (nextSibling instanceof Element) {
|
|
670
|
+
let nextTag = node._svg ? nextSibling.tagName : nextSibling.tagName.toLowerCase()
|
|
671
|
+
if (nextTag === node.type) {
|
|
672
|
+
let nextCursor = nextSibling.nextSibling
|
|
673
|
+
// Found a match after skipping - adopt it and leave skipped node in place
|
|
674
|
+
diffHostProps({}, hostProps, nextSibling)
|
|
675
|
+
|
|
676
|
+
if (hostProps.innerHTML != null) {
|
|
677
|
+
nextSibling.innerHTML = hostProps.innerHTML as string
|
|
678
|
+
} else {
|
|
679
|
+
let childCursor = nextSibling.firstChild
|
|
680
|
+
diffChildren(
|
|
681
|
+
null,
|
|
682
|
+
node._children,
|
|
683
|
+
nextSibling,
|
|
684
|
+
frame,
|
|
685
|
+
scheduler,
|
|
686
|
+
styles,
|
|
687
|
+
node,
|
|
688
|
+
rootTarget,
|
|
689
|
+
childCursor,
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
setupHostNode(node, nextSibling, scheduler)
|
|
694
|
+
bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles)
|
|
695
|
+
return nextCursor
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Retry failed - log mismatch and create new element (don't remove mismatched nodes)
|
|
699
|
+
logHydrationMismatch('tag', cursorTag, node.type)
|
|
700
|
+
cursor = undefined // stop hydration for this tree
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
let dom = node._svg
|
|
704
|
+
? document.createElementNS(SVG_NS, node.type)
|
|
705
|
+
: document.createElement(node.type)
|
|
706
|
+
diffHostProps({}, hostProps, dom)
|
|
707
|
+
|
|
708
|
+
// Handle innerHTML prop
|
|
709
|
+
if (hostProps.innerHTML != null) {
|
|
710
|
+
dom.innerHTML = hostProps.innerHTML as string
|
|
711
|
+
} else {
|
|
712
|
+
diffChildren(null, node._children, dom, frame, scheduler, styles, node, rootTarget)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
setupHostNode(node, dom, scheduler)
|
|
716
|
+
bindNodeMixRuntime(node as CommittedHostNode, frame, scheduler, styles, false, domParent)
|
|
717
|
+
doInsert(dom)
|
|
718
|
+
return cursor
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (isFragmentNode(node)) {
|
|
722
|
+
// Insert fragment children in order before the same anchor
|
|
723
|
+
for (let child of node._children) {
|
|
724
|
+
cursor = insert(
|
|
725
|
+
child,
|
|
726
|
+
domParent,
|
|
727
|
+
frame,
|
|
728
|
+
scheduler,
|
|
729
|
+
styles,
|
|
730
|
+
vParent,
|
|
731
|
+
rootTarget,
|
|
732
|
+
anchor,
|
|
733
|
+
cursor,
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
return cursor
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (isComponentNode(node)) {
|
|
740
|
+
return diffComponent(
|
|
741
|
+
null,
|
|
742
|
+
node,
|
|
743
|
+
frame,
|
|
744
|
+
scheduler,
|
|
745
|
+
styles,
|
|
746
|
+
domParent,
|
|
747
|
+
vParent,
|
|
748
|
+
rootTarget,
|
|
749
|
+
anchor,
|
|
750
|
+
cursor,
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (node.type === Frame) {
|
|
755
|
+
return insertFrame(
|
|
756
|
+
node,
|
|
757
|
+
domParent,
|
|
758
|
+
frame,
|
|
759
|
+
scheduler,
|
|
760
|
+
styles,
|
|
761
|
+
vParent,
|
|
762
|
+
rootTarget,
|
|
763
|
+
anchor,
|
|
764
|
+
cursor,
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
invariant(false, 'Unexpected node type')
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function diffFrame(
|
|
772
|
+
curr: VNode,
|
|
773
|
+
next: VNode,
|
|
774
|
+
domParent: ParentNode,
|
|
775
|
+
frame: FrameHandle,
|
|
776
|
+
scheduler: Scheduler,
|
|
777
|
+
styles: StyleManager,
|
|
778
|
+
vParent: VNode,
|
|
779
|
+
rootTarget: EventTarget,
|
|
780
|
+
anchor?: Node,
|
|
781
|
+
): void {
|
|
782
|
+
let currSrc = getFrameSrc(curr)
|
|
783
|
+
let nextSrc = getFrameSrc(next)
|
|
784
|
+
let currName = getFrameName(curr)
|
|
785
|
+
let nextName = getFrameName(next)
|
|
786
|
+
|
|
787
|
+
if (currName !== nextName) {
|
|
788
|
+
let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor
|
|
789
|
+
remove(curr, domParent, scheduler, styles)
|
|
790
|
+
insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// If the frame hasn't resolved yet, preserve existing cancel/remount behavior
|
|
795
|
+
// so pending streams from the old src cannot take over the new src.
|
|
796
|
+
if (currSrc !== nextSrc && !curr._frameResolved) {
|
|
797
|
+
let replaceAnchor = curr._rangeEnd?.nextSibling ?? anchor
|
|
798
|
+
remove(curr, domParent, scheduler, styles)
|
|
799
|
+
insert(next, domParent, frame, scheduler, styles, vParent, rootTarget, replaceAnchor)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
next._rangeStart = curr._rangeStart
|
|
804
|
+
next._rangeEnd = curr._rangeEnd
|
|
805
|
+
next._frameInstance = curr._frameInstance
|
|
806
|
+
next._frameFallbackRoot = curr._frameFallbackRoot
|
|
807
|
+
next._frameResolveToken = curr._frameResolveToken
|
|
808
|
+
next._frameResolved = curr._frameResolved
|
|
809
|
+
next._parent = vParent
|
|
810
|
+
|
|
811
|
+
if (currSrc !== nextSrc) {
|
|
812
|
+
let frameInstance = next._frameInstance as FrameInstance | undefined
|
|
813
|
+
if (frameInstance) {
|
|
814
|
+
frameInstance.handle.src = nextSrc
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let runtime = getFrameRuntime(frame)
|
|
818
|
+
if (runtime) {
|
|
819
|
+
resolveClientFrame(next, runtime)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (!next._frameResolved && next._frameFallbackRoot) {
|
|
824
|
+
next._frameFallbackRoot.render(next.props?.fallback ?? null)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function insertFrame(
|
|
829
|
+
node: VNode,
|
|
830
|
+
domParent: ParentNode,
|
|
831
|
+
frame: FrameHandle,
|
|
832
|
+
scheduler: Scheduler,
|
|
833
|
+
styles: StyleManager,
|
|
834
|
+
vParent: VNode,
|
|
835
|
+
rootTarget: EventTarget,
|
|
836
|
+
anchor?: Node,
|
|
837
|
+
cursor?: Node | null,
|
|
838
|
+
): Node | null | undefined {
|
|
839
|
+
let runtime = getFrameRuntime(frame)
|
|
840
|
+
if (!runtime || (runtime as { canResolveFrames?: boolean }).canResolveFrames === false) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
'Cannot render <Frame /> without frame runtime. Use run() or pass frameInit to createRoot/createRangeRoot.',
|
|
843
|
+
)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Hydration path: adopt server-rendered frame markers and reuse the existing
|
|
847
|
+
// frame instance created during createSubFrames().
|
|
848
|
+
if (isFrameStartComment(cursor)) {
|
|
849
|
+
let start = cursor
|
|
850
|
+
let end = findFrameEndComment(start)
|
|
851
|
+
if (end) {
|
|
852
|
+
node._rangeStart = start
|
|
853
|
+
node._rangeEnd = end
|
|
854
|
+
node._parent = vParent
|
|
855
|
+
node._frameResolveToken = 0
|
|
856
|
+
node._frameResolveController = undefined
|
|
857
|
+
node._frameFallbackRoot = undefined
|
|
858
|
+
node._frameResolved = true
|
|
859
|
+
|
|
860
|
+
let frameId = getFrameIdFromComment(start)
|
|
861
|
+
let marker = frameId ? runtime.data.f?.[frameId] : undefined
|
|
862
|
+
let src = marker?.src ?? getFrameSrc(node)
|
|
863
|
+
let instance = runtime.frameInstances.get(start)
|
|
864
|
+
if (!instance) {
|
|
865
|
+
instance = createFrame([start, end], {
|
|
866
|
+
name: getFrameName(node),
|
|
867
|
+
src,
|
|
868
|
+
marker: frameId && marker ? { ...marker, id: frameId } : undefined,
|
|
869
|
+
errorTarget: runtime.errorTarget,
|
|
870
|
+
loadModule: runtime.loadModule,
|
|
871
|
+
resolveFrame: runtime.resolveFrame,
|
|
872
|
+
pendingClientEntries: runtime.pendingClientEntries,
|
|
873
|
+
scheduler: runtime.scheduler,
|
|
874
|
+
data: runtime.data,
|
|
875
|
+
moduleCache: runtime.moduleCache,
|
|
876
|
+
moduleLoads: runtime.moduleLoads,
|
|
877
|
+
frameInstances: runtime.frameInstances,
|
|
878
|
+
namedFrames: runtime.namedFrames,
|
|
879
|
+
})
|
|
880
|
+
runtime.frameInstances.set(start, instance)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
node._frameInstance = instance
|
|
884
|
+
|
|
885
|
+
return end.nextSibling
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
let start = document.createComment(` rmx:f:${randomFrameId()} `)
|
|
890
|
+
let end = document.createComment(' /rmx:f ')
|
|
891
|
+
let doInsert = anchor
|
|
892
|
+
? (dom: Node) => domParent.insertBefore(dom, anchor)
|
|
893
|
+
: (dom: Node) => domParent.appendChild(dom)
|
|
894
|
+
|
|
895
|
+
doInsert(start)
|
|
896
|
+
doInsert(end)
|
|
897
|
+
|
|
898
|
+
node._rangeStart = start
|
|
899
|
+
node._rangeEnd = end
|
|
900
|
+
node._parent = vParent
|
|
901
|
+
|
|
902
|
+
let fallbackRoot = createRangeRoot([start, end], {
|
|
903
|
+
frame,
|
|
904
|
+
styleManager: styles,
|
|
905
|
+
})
|
|
906
|
+
fallbackRoot.render(node.props?.fallback ?? null)
|
|
907
|
+
node._frameFallbackRoot = fallbackRoot
|
|
908
|
+
node._frameResolved = false
|
|
909
|
+
node._frameResolveToken = 0
|
|
910
|
+
|
|
911
|
+
let instance = createFrame([start, end], {
|
|
912
|
+
name: getFrameName(node),
|
|
913
|
+
src: getFrameSrc(node),
|
|
914
|
+
errorTarget: runtime.errorTarget,
|
|
915
|
+
loadModule: runtime.loadModule,
|
|
916
|
+
resolveFrame: runtime.resolveFrame,
|
|
917
|
+
pendingClientEntries: runtime.pendingClientEntries,
|
|
918
|
+
scheduler: runtime.scheduler,
|
|
919
|
+
data: runtime.data,
|
|
920
|
+
moduleCache: runtime.moduleCache,
|
|
921
|
+
moduleLoads: runtime.moduleLoads,
|
|
922
|
+
frameInstances: runtime.frameInstances,
|
|
923
|
+
namedFrames: runtime.namedFrames,
|
|
924
|
+
})
|
|
925
|
+
node._frameInstance = instance
|
|
926
|
+
runtime.frameInstances.set(start, instance)
|
|
927
|
+
|
|
928
|
+
resolveClientFrame(node, runtime)
|
|
929
|
+
|
|
930
|
+
return cursor
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function resolveClientFrame(node: VNode, runtime: FrameRuntime): void {
|
|
934
|
+
let frameSrc = getFrameSrc(node)
|
|
935
|
+
let instance = node._frameInstance as FrameInstance | undefined
|
|
936
|
+
if (!instance) return
|
|
937
|
+
|
|
938
|
+
let token = (node._frameResolveToken ?? 0) + 1
|
|
939
|
+
node._frameResolveToken = token
|
|
940
|
+
node._frameResolveController?.abort()
|
|
941
|
+
let resolveController = new AbortController()
|
|
942
|
+
node._frameResolveController = resolveController
|
|
943
|
+
|
|
944
|
+
Promise.resolve(runtime.resolveFrame(frameSrc, resolveController.signal))
|
|
945
|
+
.then(async (content) => {
|
|
946
|
+
if (node._frameResolveToken !== token || resolveController.signal.aborted) return
|
|
947
|
+
node._frameFallbackRoot?.dispose()
|
|
948
|
+
node._frameFallbackRoot = undefined
|
|
949
|
+
let nextContent = asAbortableFrameContent(content, resolveController.signal)
|
|
950
|
+
await instance.render(nextContent, { signal: resolveController.signal })
|
|
951
|
+
if (node._frameResolveToken !== token || resolveController.signal.aborted) return
|
|
952
|
+
node._frameResolved = true
|
|
953
|
+
})
|
|
954
|
+
.catch(() => {})
|
|
955
|
+
.finally(() => {
|
|
956
|
+
if (node._frameResolveController === resolveController) {
|
|
957
|
+
node._frameResolveController = undefined
|
|
958
|
+
}
|
|
959
|
+
})
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function disposeFrameResources(node: VNode): void {
|
|
963
|
+
node._frameResolveToken = (node._frameResolveToken ?? 0) + 1
|
|
964
|
+
node._frameResolveController?.abort()
|
|
965
|
+
node._frameResolveController = undefined
|
|
966
|
+
node._frameFallbackRoot?.dispose()
|
|
967
|
+
node._frameFallbackRoot = undefined
|
|
968
|
+
|
|
969
|
+
let frameInstance = node._frameInstance as FrameInstance | undefined
|
|
970
|
+
if (frameInstance) {
|
|
971
|
+
frameInstance.dispose()
|
|
972
|
+
node._frameInstance = undefined
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function asAbortableFrameContent(content: FrameContent, signal: AbortSignal): FrameContent {
|
|
977
|
+
if (!(content instanceof ReadableStream)) return content
|
|
978
|
+
return createAbortableReadableStream(content, signal)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function createAbortableReadableStream(
|
|
982
|
+
source: ReadableStream<Uint8Array>,
|
|
983
|
+
signal: AbortSignal,
|
|
984
|
+
): ReadableStream<Uint8Array> {
|
|
985
|
+
let reader = source.getReader()
|
|
986
|
+
let aborted = false
|
|
987
|
+
|
|
988
|
+
let onAbort = () => {
|
|
989
|
+
aborted = true
|
|
990
|
+
void reader.cancel(signal.reason)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (signal.aborted) onAbort()
|
|
994
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
995
|
+
|
|
996
|
+
return new ReadableStream<Uint8Array>({
|
|
997
|
+
async pull(controller) {
|
|
998
|
+
if (aborted) {
|
|
999
|
+
controller.close()
|
|
1000
|
+
return
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let removeAbortReadListener: undefined | (() => void)
|
|
1004
|
+
let abortRead = new Promise<ReadableStreamReadResult<Uint8Array>>((resolve) => {
|
|
1005
|
+
if (signal.aborted) {
|
|
1006
|
+
resolve({ done: true, value: undefined as unknown as Uint8Array })
|
|
1007
|
+
return
|
|
1008
|
+
}
|
|
1009
|
+
let onAbortRead = () => {
|
|
1010
|
+
resolve({ done: true, value: undefined as unknown as Uint8Array })
|
|
1011
|
+
}
|
|
1012
|
+
removeAbortReadListener = () => signal.removeEventListener('abort', onAbortRead)
|
|
1013
|
+
signal.addEventListener('abort', onAbortRead, { once: true })
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
let { done, value } = await Promise.race([reader.read(), abortRead])
|
|
1017
|
+
removeAbortReadListener?.()
|
|
1018
|
+
|
|
1019
|
+
if (done) {
|
|
1020
|
+
controller.close()
|
|
1021
|
+
return
|
|
1022
|
+
}
|
|
1023
|
+
controller.enqueue(value)
|
|
1024
|
+
},
|
|
1025
|
+
cancel(reason) {
|
|
1026
|
+
signal.removeEventListener('abort', onAbort)
|
|
1027
|
+
return reader.cancel(reason)
|
|
1028
|
+
},
|
|
1029
|
+
})
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function removeFrameDomRange(node: VNode, domParent: ParentNode): void {
|
|
1033
|
+
let start = node._rangeStart
|
|
1034
|
+
let end = node._rangeEnd
|
|
1035
|
+
if (!(start instanceof Comment) || !(end instanceof Comment)) return
|
|
1036
|
+
|
|
1037
|
+
let cursor: Node | null = start
|
|
1038
|
+
while (cursor) {
|
|
1039
|
+
let nextSibling: Node | null = cursor.nextSibling
|
|
1040
|
+
if (cursor.parentNode === domParent) {
|
|
1041
|
+
domParent.removeChild(cursor)
|
|
1042
|
+
}
|
|
1043
|
+
if (cursor === end) break
|
|
1044
|
+
cursor = nextSibling
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
node._rangeStart = undefined
|
|
1048
|
+
node._rangeEnd = undefined
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function getFrameRuntime(frame: FrameHandle): FrameRuntime | undefined {
|
|
1052
|
+
return frame.$runtime as FrameRuntime | undefined
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function getFrameSrc(node: VNode): string {
|
|
1056
|
+
let src = node.props?.src
|
|
1057
|
+
invariant(typeof src === 'string' && src.length > 0, '<Frame /> requires a src prop')
|
|
1058
|
+
return src
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function getFrameName(node: VNode): string | undefined {
|
|
1062
|
+
let name = node.props?.name
|
|
1063
|
+
return typeof name === 'string' && name.length > 0 ? name : undefined
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function randomFrameId(): string {
|
|
1067
|
+
return `f${crypto.randomUUID().slice(0, 8)}`
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function skipCommentsExceptFrameStart(cursor: Node | null): Node | null {
|
|
1071
|
+
while (cursor && cursor.nodeType === Node.COMMENT_NODE) {
|
|
1072
|
+
if (isFrameStartComment(cursor)) return cursor
|
|
1073
|
+
cursor = cursor.nextSibling
|
|
1074
|
+
}
|
|
1075
|
+
return cursor
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function isFrameStartComment(node: Node | null | undefined): node is Comment {
|
|
1079
|
+
return node instanceof Comment && node.data.trim().startsWith('rmx:f:')
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function isFrameEndComment(node: Node | null | undefined): node is Comment {
|
|
1083
|
+
return node instanceof Comment && node.data.trim() === '/rmx:f'
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function getFrameIdFromComment(comment: Comment): string | undefined {
|
|
1087
|
+
let text = comment.data.trim()
|
|
1088
|
+
if (!text.startsWith('rmx:f:')) return undefined
|
|
1089
|
+
return text.slice('rmx:f:'.length)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function findFrameEndComment(start: Comment): Comment | null {
|
|
1093
|
+
let depth = 1
|
|
1094
|
+
let node: Node | null = start.nextSibling
|
|
1095
|
+
|
|
1096
|
+
while (node) {
|
|
1097
|
+
if (isFrameStartComment(node)) depth++
|
|
1098
|
+
else if (isFrameEndComment(node)) {
|
|
1099
|
+
depth--
|
|
1100
|
+
if (depth === 0) return node
|
|
1101
|
+
}
|
|
1102
|
+
node = node.nextSibling
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return null
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export function renderComponent(
|
|
1109
|
+
handle: ComponentHandle,
|
|
1110
|
+
currContent: VNode | null,
|
|
1111
|
+
next: ComponentNode,
|
|
1112
|
+
domParent: ParentNode,
|
|
1113
|
+
frame: FrameHandle,
|
|
1114
|
+
scheduler: Scheduler,
|
|
1115
|
+
styles: StyleManager,
|
|
1116
|
+
rootTarget: EventTarget,
|
|
1117
|
+
vParent?: VNode,
|
|
1118
|
+
anchor?: Node,
|
|
1119
|
+
cursor?: Node | null,
|
|
1120
|
+
): Node | null | undefined {
|
|
1121
|
+
let [element, tasks] = handle.render(next.props)
|
|
1122
|
+
let content = toVNode(element)
|
|
1123
|
+
let newCursor = diffVNodes(
|
|
1124
|
+
currContent,
|
|
1125
|
+
content,
|
|
1126
|
+
domParent,
|
|
1127
|
+
frame,
|
|
1128
|
+
scheduler,
|
|
1129
|
+
styles,
|
|
1130
|
+
next,
|
|
1131
|
+
rootTarget,
|
|
1132
|
+
anchor,
|
|
1133
|
+
cursor,
|
|
1134
|
+
)
|
|
1135
|
+
next._content = content
|
|
1136
|
+
next._handle = handle
|
|
1137
|
+
next._parent = vParent
|
|
1138
|
+
|
|
1139
|
+
let committed = next as CommittedComponentNode
|
|
1140
|
+
handle.setScheduleUpdate(() => {
|
|
1141
|
+
scheduler.enqueue(committed, domParent)
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
scheduler.enqueueTasks(tasks)
|
|
1145
|
+
|
|
1146
|
+
return newCursor
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function diffComponent(
|
|
1150
|
+
curr: CommittedComponentNode | null,
|
|
1151
|
+
next: ComponentNode,
|
|
1152
|
+
frame: FrameHandle,
|
|
1153
|
+
scheduler: Scheduler,
|
|
1154
|
+
styles: StyleManager,
|
|
1155
|
+
domParent: ParentNode,
|
|
1156
|
+
vParent: VNode,
|
|
1157
|
+
rootTarget: EventTarget,
|
|
1158
|
+
anchor?: Node,
|
|
1159
|
+
cursor?: Node | null,
|
|
1160
|
+
): Node | null | undefined {
|
|
1161
|
+
if (curr === null) {
|
|
1162
|
+
let componentId = vParent._pendingHydrationComponentId
|
|
1163
|
+
if (componentId) {
|
|
1164
|
+
vParent._pendingHydrationComponentId = undefined
|
|
1165
|
+
} else {
|
|
1166
|
+
componentId = `c${++idCounter}`
|
|
1167
|
+
}
|
|
1168
|
+
next._handle = createComponent({
|
|
1169
|
+
id: componentId,
|
|
1170
|
+
frame,
|
|
1171
|
+
type: next.type,
|
|
1172
|
+
getContext: (type: Component) => findContextFromAncestry(vParent, type),
|
|
1173
|
+
getFrameByName(name: string) {
|
|
1174
|
+
let runtime = getFrameRuntime(frame)
|
|
1175
|
+
return runtime?.namedFrames.get(name)
|
|
1176
|
+
},
|
|
1177
|
+
getTopFrame() {
|
|
1178
|
+
let runtime = getFrameRuntime(frame)
|
|
1179
|
+
return runtime?.topFrame
|
|
1180
|
+
},
|
|
1181
|
+
})
|
|
1182
|
+
|
|
1183
|
+
return renderComponent(
|
|
1184
|
+
next._handle,
|
|
1185
|
+
null,
|
|
1186
|
+
next,
|
|
1187
|
+
domParent,
|
|
1188
|
+
frame,
|
|
1189
|
+
scheduler,
|
|
1190
|
+
styles,
|
|
1191
|
+
rootTarget,
|
|
1192
|
+
vParent,
|
|
1193
|
+
anchor,
|
|
1194
|
+
cursor,
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
next._handle = curr._handle
|
|
1198
|
+
let { _content, _handle } = curr
|
|
1199
|
+
return renderComponent(
|
|
1200
|
+
_handle,
|
|
1201
|
+
_content,
|
|
1202
|
+
next,
|
|
1203
|
+
domParent,
|
|
1204
|
+
frame,
|
|
1205
|
+
scheduler,
|
|
1206
|
+
styles,
|
|
1207
|
+
rootTarget,
|
|
1208
|
+
vParent,
|
|
1209
|
+
anchor,
|
|
1210
|
+
cursor,
|
|
1211
|
+
)
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Cleanup without DOM removal - used for descendants when parent DOM node is removed
|
|
1215
|
+
function cleanupDescendants(node: VNode, scheduler: Scheduler, styles: StyleManager): void {
|
|
1216
|
+
if (isCommittedTextNode(node)) {
|
|
1217
|
+
return
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (isCommittedHostNode(node)) {
|
|
1221
|
+
for (let child of node._children) {
|
|
1222
|
+
cleanupDescendants(child, scheduler, styles)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
teardownMixins(node._mixState as MixinRuntimeState | undefined)
|
|
1226
|
+
teardownControlledReflection(node)
|
|
1227
|
+
if (node._controller) node._controller.abort()
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (isFragmentNode(node)) {
|
|
1232
|
+
for (let child of node._children) {
|
|
1233
|
+
cleanupDescendants(child, scheduler, styles)
|
|
1234
|
+
}
|
|
1235
|
+
return
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (isCommittedComponentNode(node)) {
|
|
1239
|
+
cleanupDescendants(node._content, scheduler, styles)
|
|
1240
|
+
let tasks = node._handle.remove()
|
|
1241
|
+
scheduler.enqueueTasks(tasks)
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (node.type === Frame) {
|
|
1246
|
+
disposeFrameResources(node)
|
|
1247
|
+
return
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
export function remove(
|
|
1252
|
+
node: VNode,
|
|
1253
|
+
domParent: ParentNode,
|
|
1254
|
+
scheduler: Scheduler,
|
|
1255
|
+
styles: StyleManager,
|
|
1256
|
+
) {
|
|
1257
|
+
if (isCommittedTextNode(node)) {
|
|
1258
|
+
node._dom.parentNode?.removeChild(node._dom)
|
|
1259
|
+
return
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (isCommittedHostNode(node)) {
|
|
1263
|
+
if (node._persistedByMixins) return
|
|
1264
|
+
|
|
1265
|
+
let persistedRemoval = prepareMixinRemoval(node._mixState as MixinRuntimeState | undefined)
|
|
1266
|
+
if (persistedRemoval) {
|
|
1267
|
+
let token = ++persistedRemovalToken
|
|
1268
|
+
markNodePersistedByMixins(node, domParent, token)
|
|
1269
|
+
void persistedRemoval
|
|
1270
|
+
.catch(() => {})
|
|
1271
|
+
.finally(() => {
|
|
1272
|
+
if (!node._persistedByMixins) return
|
|
1273
|
+
if (node._persistedRemovalToken !== token) return
|
|
1274
|
+
unmarkNodePersistedByMixins(node)
|
|
1275
|
+
performHostNodeRemoval(node, domParent, scheduler, styles)
|
|
1276
|
+
})
|
|
1277
|
+
return
|
|
1278
|
+
}
|
|
1279
|
+
performHostNodeRemoval(node, domParent, scheduler, styles)
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (isFragmentNode(node)) {
|
|
1284
|
+
for (let child of node._children) {
|
|
1285
|
+
remove(child, domParent, scheduler, styles)
|
|
1286
|
+
}
|
|
1287
|
+
return
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (isCommittedComponentNode(node)) {
|
|
1291
|
+
remove(node._content, domParent, scheduler, styles)
|
|
1292
|
+
let tasks = node._handle.remove()
|
|
1293
|
+
scheduler.enqueueTasks(tasks)
|
|
1294
|
+
return
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (node.type === Frame) {
|
|
1298
|
+
disposeFrameResources(node)
|
|
1299
|
+
removeFrameDomRange(node, domParent)
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Actually remove a host node from DOM and clean up
|
|
1305
|
+
function performHostNodeRemoval(
|
|
1306
|
+
node: CommittedHostNode,
|
|
1307
|
+
domParent: ParentNode,
|
|
1308
|
+
scheduler: Scheduler,
|
|
1309
|
+
styles: StyleManager,
|
|
1310
|
+
) {
|
|
1311
|
+
if (isHeadHostNode(node)) {
|
|
1312
|
+
for (let child of node._children) {
|
|
1313
|
+
remove(child, node._dom, scheduler, styles)
|
|
1314
|
+
}
|
|
1315
|
+
} else {
|
|
1316
|
+
// Clean up all descendants first (before removing DOM subtree)
|
|
1317
|
+
for (let child of node._children) {
|
|
1318
|
+
cleanupDescendants(child, scheduler, styles)
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
teardownMixins(node._mixState as MixinRuntimeState | undefined)
|
|
1323
|
+
teardownControlledReflection(node)
|
|
1324
|
+
// Never remove the real document.head node when reconciling a <head> vnode.
|
|
1325
|
+
if (!isHeadHostNode(node)) {
|
|
1326
|
+
node._dom.parentNode?.removeChild(node._dom)
|
|
1327
|
+
}
|
|
1328
|
+
if (node._controller) node._controller.abort()
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function diffChildren(
|
|
1332
|
+
curr: VNode[] | null,
|
|
1333
|
+
next: VNode[],
|
|
1334
|
+
domParent: ParentNode,
|
|
1335
|
+
frame: FrameHandle,
|
|
1336
|
+
scheduler: Scheduler,
|
|
1337
|
+
styles: StyleManager,
|
|
1338
|
+
vParent: VNode,
|
|
1339
|
+
rootTarget: EventTarget,
|
|
1340
|
+
cursor?: Node | null,
|
|
1341
|
+
anchor?: Node,
|
|
1342
|
+
) {
|
|
1343
|
+
let nextLength = next.length
|
|
1344
|
+
|
|
1345
|
+
// Warn when duplicate keys are present among siblings. Duplicate keys are
|
|
1346
|
+
// still processed (last one wins), but they make keyed diffing ambiguous.
|
|
1347
|
+
let hasKeys = false
|
|
1348
|
+
let seenKeys: Set<string> | undefined
|
|
1349
|
+
let duplicateKeys: Set<string> | undefined
|
|
1350
|
+
for (let i = 0; i < nextLength; i++) {
|
|
1351
|
+
let node = next[i]
|
|
1352
|
+
if (node && node.key != null) {
|
|
1353
|
+
hasKeys = true
|
|
1354
|
+
if (!seenKeys) {
|
|
1355
|
+
seenKeys = new Set([node.key])
|
|
1356
|
+
continue
|
|
1357
|
+
}
|
|
1358
|
+
if (seenKeys.has(node.key)) {
|
|
1359
|
+
if (!duplicateKeys) {
|
|
1360
|
+
duplicateKeys = new Set()
|
|
1361
|
+
}
|
|
1362
|
+
duplicateKeys.add(node.key)
|
|
1363
|
+
} else {
|
|
1364
|
+
seenKeys.add(node.key)
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (duplicateKeys?.size) {
|
|
1370
|
+
let quotedKeys = Array.from(duplicateKeys, (key) => `"${key}"`)
|
|
1371
|
+
console.warn(
|
|
1372
|
+
`Duplicate keys detected in siblings: ${quotedKeys.join(', ')}. Keys should be unique.`,
|
|
1373
|
+
)
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Initial mount / hydration: delegate to insert() for each child so that
|
|
1377
|
+
// hydration cursors and creation logic remain centralized there.
|
|
1378
|
+
if (curr === null) {
|
|
1379
|
+
for (let node of next) {
|
|
1380
|
+
cursor = insert(
|
|
1381
|
+
node,
|
|
1382
|
+
domParent,
|
|
1383
|
+
frame,
|
|
1384
|
+
scheduler,
|
|
1385
|
+
styles,
|
|
1386
|
+
vParent,
|
|
1387
|
+
rootTarget,
|
|
1388
|
+
anchor,
|
|
1389
|
+
cursor,
|
|
1390
|
+
)
|
|
1391
|
+
}
|
|
1392
|
+
vParent._children = next
|
|
1393
|
+
return cursor
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
let currLength = curr.length
|
|
1397
|
+
|
|
1398
|
+
// Detect if any keys are present in the new children. If not, we can fall
|
|
1399
|
+
// back to the simpler index-based diff which is cheaper and matches
|
|
1400
|
+
// pre-existing behavior.
|
|
1401
|
+
if (!hasKeys) {
|
|
1402
|
+
for (let i = 0; i < nextLength; i++) {
|
|
1403
|
+
let currentNode = i < currLength ? curr[i] : null
|
|
1404
|
+
diffVNodes(
|
|
1405
|
+
currentNode,
|
|
1406
|
+
next[i],
|
|
1407
|
+
domParent,
|
|
1408
|
+
frame,
|
|
1409
|
+
scheduler,
|
|
1410
|
+
styles,
|
|
1411
|
+
vParent,
|
|
1412
|
+
rootTarget,
|
|
1413
|
+
anchor,
|
|
1414
|
+
cursor,
|
|
1415
|
+
)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (currLength > nextLength) {
|
|
1419
|
+
for (let i = nextLength; i < currLength; i++) {
|
|
1420
|
+
let node = curr[i]
|
|
1421
|
+
if (node) remove(node, domParent, scheduler, styles)
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
vParent._children = next
|
|
1426
|
+
return
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// --- O(n + m) keyed diff with Map-based lookup ------------------------------
|
|
1430
|
+
|
|
1431
|
+
let oldChildren = curr
|
|
1432
|
+
let oldChildrenLength = currLength
|
|
1433
|
+
let remainingOldChildren = oldChildrenLength
|
|
1434
|
+
|
|
1435
|
+
// Build key → index map for O(1) lookup: O(m)
|
|
1436
|
+
let oldKeyMap = new Map<string, number>()
|
|
1437
|
+
for (let i = 0; i < oldChildrenLength; i++) {
|
|
1438
|
+
let c = oldChildren[i]
|
|
1439
|
+
if (c) {
|
|
1440
|
+
c._flags = 0
|
|
1441
|
+
if (c.key != null) {
|
|
1442
|
+
oldKeyMap.set(c.key, i)
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
let skew = 0
|
|
1448
|
+
let newChildren: VNode[] = new Array(nextLength)
|
|
1449
|
+
|
|
1450
|
+
// First pass: match new children to old ones using Map lookup: O(n)
|
|
1451
|
+
for (let i = 0; i < nextLength; i++) {
|
|
1452
|
+
let childVNode = next[i]
|
|
1453
|
+
if (!childVNode) {
|
|
1454
|
+
newChildren[i] = childVNode
|
|
1455
|
+
continue
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
newChildren[i] = childVNode
|
|
1459
|
+
childVNode._parent = vParent
|
|
1460
|
+
|
|
1461
|
+
let skewedIndex = i + skew
|
|
1462
|
+
let matchingIndex = -1
|
|
1463
|
+
|
|
1464
|
+
let key = childVNode.key
|
|
1465
|
+
let type = childVNode.type
|
|
1466
|
+
|
|
1467
|
+
if (key != null) {
|
|
1468
|
+
// O(1) Map lookup for keyed children
|
|
1469
|
+
let mapIndex = oldKeyMap.get(key)
|
|
1470
|
+
if (mapIndex !== undefined) {
|
|
1471
|
+
let candidate = oldChildren[mapIndex]
|
|
1472
|
+
let candidateFlags = candidate?._flags ?? 0
|
|
1473
|
+
if (candidate && (candidateFlags & MATCHED) === 0 && candidate.type === type) {
|
|
1474
|
+
matchingIndex = mapIndex
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
} else {
|
|
1478
|
+
// Non-keyed children use positional identity only - no searching
|
|
1479
|
+
let searchVNode = oldChildren[skewedIndex]
|
|
1480
|
+
let searchFlags = searchVNode?._flags ?? 0
|
|
1481
|
+
let available = searchVNode != null && (searchFlags & MATCHED) === 0
|
|
1482
|
+
if (available && searchVNode.key == null && type === searchVNode.type) {
|
|
1483
|
+
matchingIndex = skewedIndex
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
childVNode._index = matchingIndex
|
|
1488
|
+
|
|
1489
|
+
let matchedOldVNode: VNode | null = null
|
|
1490
|
+
if (matchingIndex !== -1) {
|
|
1491
|
+
matchedOldVNode = oldChildren[matchingIndex]
|
|
1492
|
+
remainingOldChildren--
|
|
1493
|
+
if (matchedOldVNode) {
|
|
1494
|
+
matchedOldVNode._flags = (matchedOldVNode._flags ?? 0) | MATCHED
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Determine whether this is a mount vs move and mark INSERT_VNODE
|
|
1499
|
+
let oldDom = matchedOldVNode && findFirstDomAnchor(matchedOldVNode)
|
|
1500
|
+
let isMounting = !matchedOldVNode || !oldDom
|
|
1501
|
+
if (isMounting) {
|
|
1502
|
+
if (matchingIndex === -1) {
|
|
1503
|
+
// Adjust skew similar to Preact when lengths differ
|
|
1504
|
+
if (nextLength > oldChildrenLength) {
|
|
1505
|
+
skew--
|
|
1506
|
+
} else if (nextLength < oldChildrenLength) {
|
|
1507
|
+
skew++
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
|
|
1512
|
+
} else if (matchingIndex !== i + skew) {
|
|
1513
|
+
if (matchingIndex === i + skew - 1) {
|
|
1514
|
+
skew--
|
|
1515
|
+
} else if (matchingIndex === i + skew + 1) {
|
|
1516
|
+
skew++
|
|
1517
|
+
} else {
|
|
1518
|
+
if (matchingIndex! > i + skew) skew--
|
|
1519
|
+
else skew++
|
|
1520
|
+
childVNode._flags = (childVNode._flags ?? 0) | INSERT_VNODE
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Unmount any old children that weren't matched
|
|
1526
|
+
if (remainingOldChildren) {
|
|
1527
|
+
for (let i = 0; i < oldChildrenLength; i++) {
|
|
1528
|
+
let oldVNode = oldChildren[i]
|
|
1529
|
+
if (oldVNode && ((oldVNode._flags ?? 0) & MATCHED) === 0) {
|
|
1530
|
+
remove(oldVNode, domParent, scheduler, styles)
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Second pass: diff matched pairs and place/move DOM nodes in the correct
|
|
1536
|
+
// order, similar to Preact's diffChildren + insert.
|
|
1537
|
+
vParent._children = newChildren
|
|
1538
|
+
|
|
1539
|
+
let lastPlaced: Node | null = null
|
|
1540
|
+
|
|
1541
|
+
for (let i = 0; i < nextLength; i++) {
|
|
1542
|
+
let childVNode = newChildren[i]
|
|
1543
|
+
if (!childVNode) continue
|
|
1544
|
+
|
|
1545
|
+
let idx = childVNode._index ?? -1
|
|
1546
|
+
let oldVNode = idx >= 0 ? oldChildren[idx] : null
|
|
1547
|
+
|
|
1548
|
+
diffVNodes(
|
|
1549
|
+
oldVNode,
|
|
1550
|
+
childVNode,
|
|
1551
|
+
domParent,
|
|
1552
|
+
frame,
|
|
1553
|
+
scheduler,
|
|
1554
|
+
styles,
|
|
1555
|
+
vParent,
|
|
1556
|
+
rootTarget,
|
|
1557
|
+
anchor,
|
|
1558
|
+
cursor,
|
|
1559
|
+
)
|
|
1560
|
+
|
|
1561
|
+
let shouldPlace = (childVNode._flags ?? 0) & INSERT_VNODE
|
|
1562
|
+
let firstDom = findFirstDomAnchor(childVNode)
|
|
1563
|
+
let lastDom = firstDom ? findLastDomAnchor(childVNode) : null
|
|
1564
|
+
if (shouldPlace && firstDom && lastDom && firstDom.parentNode === domParent) {
|
|
1565
|
+
let target: Node | null
|
|
1566
|
+
if (lastPlaced === null) {
|
|
1567
|
+
if (vParent._rangeStart && vParent._rangeStart.parentNode === domParent) {
|
|
1568
|
+
target = vParent._rangeStart.nextSibling
|
|
1569
|
+
} else {
|
|
1570
|
+
target = domParent.firstChild
|
|
1571
|
+
}
|
|
1572
|
+
} else {
|
|
1573
|
+
target = lastPlaced.nextSibling
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (target === null && anchor) target = anchor
|
|
1577
|
+
|
|
1578
|
+
// If target lies within the range we're moving, skip the move.
|
|
1579
|
+
if (target && domRangeContainsNode(firstDom, lastDom, target)) {
|
|
1580
|
+
// no-op
|
|
1581
|
+
} else if (firstDom !== target) {
|
|
1582
|
+
moveDomRange(domParent, firstDom, lastDom, target)
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (lastDom) lastPlaced = lastDom
|
|
1587
|
+
|
|
1588
|
+
// Clear internal flags for next diff
|
|
1589
|
+
childVNode._flags = 0
|
|
1590
|
+
childVNode._index = undefined
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
export function findFirstDomAnchor(node: VNode | null | undefined): Node | null {
|
|
1597
|
+
if (!node) return null
|
|
1598
|
+
if (isCommittedTextNode(node)) return node._dom
|
|
1599
|
+
if (isCommittedHostNode(node)) return node._dom
|
|
1600
|
+
if (isCommittedComponentNode(node)) return findFirstDomAnchor(node._content)
|
|
1601
|
+
if (node.type === Frame) return node._rangeStart ?? null
|
|
1602
|
+
if (isFragmentNode(node)) {
|
|
1603
|
+
for (let child of node._children) {
|
|
1604
|
+
let dom = findFirstDomAnchor(child)
|
|
1605
|
+
if (dom) return dom
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return null
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
export function findLastDomAnchor(node: VNode | null | undefined): Node | null {
|
|
1612
|
+
if (!node) return null
|
|
1613
|
+
if (isCommittedTextNode(node)) return node._dom
|
|
1614
|
+
if (isCommittedHostNode(node)) return node._dom
|
|
1615
|
+
if (isCommittedComponentNode(node)) return findLastDomAnchor(node._content)
|
|
1616
|
+
if (node.type === Frame) return node._rangeEnd ?? null
|
|
1617
|
+
if (isFragmentNode(node)) {
|
|
1618
|
+
for (let i = node._children.length - 1; i >= 0; i--) {
|
|
1619
|
+
let dom = findLastDomAnchor(node._children[i])
|
|
1620
|
+
if (dom) return dom
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return null
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function domRangeContainsNode(first: Node, last: Node, node: Node): boolean {
|
|
1627
|
+
let current: Node | null = first
|
|
1628
|
+
while (current) {
|
|
1629
|
+
if (current === node) return true
|
|
1630
|
+
if (current === last) break
|
|
1631
|
+
current = current.nextSibling
|
|
1632
|
+
}
|
|
1633
|
+
return false
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function moveDomRange(domParent: ParentNode, first: Node, last: Node, before: Node | null): void {
|
|
1637
|
+
let current: Node | null = first
|
|
1638
|
+
while (current) {
|
|
1639
|
+
let next: Node | null = current === last ? null : current.nextSibling
|
|
1640
|
+
domParent.insertBefore(current, before)
|
|
1641
|
+
if (current === last) break
|
|
1642
|
+
current = next
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
export function setActiveSchedulerUpdateParents(parents: ParentNode[] | undefined) {
|
|
1647
|
+
activeSchedulerUpdateParents = parents
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function shouldDispatchInlineMixinLifecycle(node: Node): boolean {
|
|
1651
|
+
let parents = activeSchedulerUpdateParents
|
|
1652
|
+
if (!parents?.length) return true
|
|
1653
|
+
for (let parent of parents) {
|
|
1654
|
+
let parentNode = parent as Node
|
|
1655
|
+
if (parentNode === node) return false
|
|
1656
|
+
if (parentNode.contains(node)) return false
|
|
1657
|
+
}
|
|
1658
|
+
return true
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
export function findNextSiblingDomAnchor(curr: VNode, vParent?: VNode): Node | null {
|
|
1662
|
+
if (!vParent || !Array.isArray(vParent._children)) return null
|
|
1663
|
+
let children = vParent._children
|
|
1664
|
+
let idx = children.indexOf(curr)
|
|
1665
|
+
if (idx === -1) return null
|
|
1666
|
+
for (let i = idx + 1; i < children.length; i++) {
|
|
1667
|
+
let dom = findFirstDomAnchor(children[i])
|
|
1668
|
+
if (dom) return dom
|
|
1669
|
+
}
|
|
1670
|
+
return null
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function reclaimPersistedMixinNode(
|
|
1674
|
+
persistedNode: CommittedHostNode,
|
|
1675
|
+
newNode: HostNode,
|
|
1676
|
+
frame: FrameHandle,
|
|
1677
|
+
scheduler: Scheduler,
|
|
1678
|
+
styles: StyleManager,
|
|
1679
|
+
vParent: VNode,
|
|
1680
|
+
rootTarget: EventTarget,
|
|
1681
|
+
): void {
|
|
1682
|
+
cancelPendingMixinRemoval(persistedNode._mixState as MixinRuntimeState | undefined)
|
|
1683
|
+
unmarkNodePersistedByMixins(persistedNode)
|
|
1684
|
+
|
|
1685
|
+
newNode._dom = persistedNode._dom
|
|
1686
|
+
newNode._parent = vParent
|
|
1687
|
+
newNode._controller = persistedNode._controller
|
|
1688
|
+
newNode._mixState = persistedNode._mixState
|
|
1689
|
+
newNode._controlledState = persistedNode._controlledState
|
|
1690
|
+
|
|
1691
|
+
let prevProps = getHostProps(persistedNode)
|
|
1692
|
+
let nextProps = resolveNodeMixProps(
|
|
1693
|
+
newNode,
|
|
1694
|
+
frame,
|
|
1695
|
+
scheduler,
|
|
1696
|
+
newNode._mixState as MixinRuntimeState | undefined,
|
|
1697
|
+
)
|
|
1698
|
+
if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
|
|
1699
|
+
dispatchMixinBeforeUpdate(newNode._mixState as MixinRuntimeState | undefined)
|
|
1700
|
+
}
|
|
1701
|
+
diffHostProps(prevProps, nextProps, persistedNode._dom)
|
|
1702
|
+
ensureControlledReflection(newNode as CommittedHostNode, scheduler)
|
|
1703
|
+
syncControlledReflection(newNode as CommittedHostNode, nextProps)
|
|
1704
|
+
|
|
1705
|
+
diffChildren(
|
|
1706
|
+
persistedNode._children,
|
|
1707
|
+
newNode._children,
|
|
1708
|
+
persistedNode._dom,
|
|
1709
|
+
frame,
|
|
1710
|
+
scheduler,
|
|
1711
|
+
styles,
|
|
1712
|
+
newNode,
|
|
1713
|
+
rootTarget,
|
|
1714
|
+
)
|
|
1715
|
+
|
|
1716
|
+
bindNodeMixRuntime(newNode as CommittedHostNode, frame, scheduler, styles, true)
|
|
1717
|
+
if (shouldDispatchInlineMixinLifecycle(persistedNode._dom)) {
|
|
1718
|
+
scheduler.enqueueCommitPhase([
|
|
1719
|
+
() => dispatchMixinCommit(newNode._mixState as MixinRuntimeState | undefined),
|
|
1720
|
+
])
|
|
1721
|
+
}
|
|
1722
|
+
}
|