@vidact/runtime 0.2.0-beta.0 → 0.2.0-beta.11
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/CHANGELOG.md +73 -0
- package/README.md +1 -1
- package/dist/actions.d.ts +32 -30
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +251 -266
- package/dist/actions.js.map +1 -1
- package/dist/async.d.ts +20 -17
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +18 -17
- package/dist/async.js.map +1 -1
- package/dist/compiled/async-resource.d.ts +9 -6
- package/dist/compiled/async-resource.d.ts.map +1 -1
- package/dist/compiled/async-resource.js +69 -77
- package/dist/compiled/async-resource.js.map +1 -1
- package/dist/compiled/core.d.ts +148 -147
- package/dist/compiled/core.d.ts.map +1 -1
- package/dist/compiled/core.js +3064 -2987
- package/dist/compiled/core.js.map +1 -1
- package/dist/compiled/types.d.ts +44 -56
- package/dist/compiled/types.d.ts.map +1 -1
- package/dist/compiled/types.js +10 -6
- package/dist/compiled/types.js.map +1 -1
- package/dist/compiled/validation.d.ts +4 -1
- package/dist/compiled/validation.d.ts.map +1 -1
- package/dist/compiled/validation.js +17 -20
- package/dist/compiled/validation.js.map +1 -1
- package/dist/component-spread.d.ts +5 -3
- package/dist/component-spread.d.ts.map +1 -1
- package/dist/component-spread.js +49 -67
- package/dist/component-spread.js.map +1 -1
- package/dist/concurrent.d.ts +20 -18
- package/dist/concurrent.d.ts.map +1 -1
- package/dist/concurrent.js +191 -210
- package/dist/concurrent.js.map +1 -1
- package/dist/direct-dom.d.ts +17 -12
- package/dist/direct-dom.d.ts.map +1 -1
- package/dist/direct-dom.js +279 -222
- package/dist/direct-dom.js.map +1 -1
- package/dist/dom/attributes.d.ts +9 -6
- package/dist/dom/attributes.d.ts.map +1 -1
- package/dist/dom/attributes.js +65 -47
- package/dist/dom/attributes.js.map +1 -1
- package/dist/dom/events.d.ts +8 -2
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +118 -65
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/forms.d.ts +9 -6
- package/dist/dom/forms.d.ts.map +1 -1
- package/dist/dom/forms.js +152 -198
- package/dist/dom/forms.js.map +1 -1
- package/dist/dom/intrinsic.d.ts +17 -15
- package/dist/dom/intrinsic.d.ts.map +1 -1
- package/dist/dom/intrinsic.js +48 -50
- package/dist/dom/intrinsic.js.map +1 -1
- package/dist/dom/namespace.d.ts +4 -1
- package/dist/dom/namespace.d.ts.map +1 -1
- package/dist/dom/namespace.js +25 -28
- package/dist/dom/namespace.js.map +1 -1
- package/dist/dom/namespaces.d.ts +11 -8
- package/dist/dom/namespaces.d.ts.map +1 -1
- package/dist/dom/namespaces.js +14 -12
- package/dist/dom/namespaces.js.map +1 -1
- package/dist/dom/properties.d.ts +18 -16
- package/dist/dom/properties.d.ts.map +1 -1
- package/dist/dom/properties.js +273 -296
- package/dist/dom/properties.js.map +1 -1
- package/dist/dom/style-properties.d.ts +4 -1
- package/dist/dom/style-properties.d.ts.map +1 -1
- package/dist/dom/style-properties.js +49 -45
- package/dist/dom/style-properties.js.map +1 -1
- package/dist/dom/styles.d.ts +5 -2
- package/dist/dom/styles.d.ts.map +1 -1
- package/dist/dom/styles.js +47 -67
- package/dist/dom/styles.js.map +1 -1
- package/dist/dom-forms.d.ts +4 -1
- package/dist/dom-forms.d.ts.map +1 -1
- package/dist/dom-forms.js +16 -13
- package/dist/dom-forms.js.map +1 -1
- package/dist/dom-styles.d.ts +4 -1
- package/dist/dom-styles.d.ts.map +1 -1
- package/dist/dom-styles.js +10 -7
- package/dist/dom-styles.js.map +1 -1
- package/dist/framework-hydrate.d.ts +20 -17
- package/dist/framework-hydrate.d.ts.map +1 -1
- package/dist/framework-hydrate.js +128 -146
- package/dist/framework-hydrate.js.map +1 -1
- package/dist/framework-protocol.d.ts +27 -25
- package/dist/framework-protocol.d.ts.map +1 -1
- package/dist/framework-protocol.js +173 -211
- package/dist/framework-protocol.js.map +1 -1
- package/dist/framework.d.ts +34 -32
- package/dist/framework.d.ts.map +1 -1
- package/dist/framework.js +74 -93
- package/dist/framework.js.map +1 -1
- package/dist/hydrate.d.ts +11 -8
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +13 -9
- package/dist/hydrate.js.map +1 -1
- package/dist/hydration-bridge.d.ts +70 -58
- package/dist/hydration-bridge.d.ts.map +1 -1
- package/dist/hydration-bridge.js +77 -66
- package/dist/hydration-bridge.js.map +1 -1
- package/dist/hydration.d.ts +54 -28
- package/dist/hydration.d.ts.map +1 -1
- package/dist/hydration.js +450 -463
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +11 -13
- package/dist/indexed-list.d.ts +8 -5
- package/dist/indexed-list.d.ts.map +1 -1
- package/dist/indexed-list.js +14 -7
- package/dist/indexed-list.js.map +1 -1
- package/dist/jsx-runtime.d.ts +10 -8
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +28 -23
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/keyed-list.d.ts +17 -14
- package/dist/keyed-list.d.ts.map +1 -1
- package/dist/keyed-list.js +244 -161
- package/dist/keyed-list.js.map +1 -1
- package/dist/metadata.d.ts +6 -3
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +131 -164
- package/dist/metadata.js.map +1 -1
- package/dist/nested-prop.d.ts +4 -2
- package/dist/nested-prop.d.ts.map +1 -1
- package/dist/nested-prop.js +17 -16
- package/dist/nested-prop.js.map +1 -1
- package/dist/profiling.d.ts +11 -8
- package/dist/profiling.d.ts.map +1 -1
- package/dist/profiling.js +17 -17
- package/dist/profiling.js.map +1 -1
- package/dist/protocol.d.ts +5 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +8 -5
- package/dist/protocol.js.map +1 -1
- package/dist/raw-html.d.ts +5 -2
- package/dist/raw-html.d.ts.map +1 -1
- package/dist/raw-html.js +86 -146
- package/dist/raw-html.js.map +1 -1
- package/dist/ref.d.ts +8 -3
- package/dist/ref.d.ts.map +1 -1
- package/dist/ref.js +10 -2
- package/dist/ref.js.map +1 -1
- package/dist/renderable-protocol.d.ts +26 -8
- package/dist/renderable-protocol.d.ts.map +1 -1
- package/dist/renderable-protocol.js +30 -9
- package/dist/renderable-protocol.js.map +1 -1
- package/dist/renderable.d.ts +28 -22
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +152 -133
- package/dist/renderable.js.map +1 -1
- package/dist/retained-ui.d.ts +9 -6
- package/dist/retained-ui.d.ts.map +1 -1
- package/dist/retained-ui.js +12 -10
- package/dist/retained-ui.js.map +1 -1
- package/dist/root.d.ts +18 -15
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +73 -85
- package/dist/root.js.map +1 -1
- package/dist/scheduler.d.ts +10 -8
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +97 -105
- package/dist/scheduler.js.map +1 -1
- package/dist/server-framework.d.ts +45 -42
- package/dist/server-framework.d.ts.map +1 -1
- package/dist/server-framework.js +209 -217
- package/dist/server-framework.js.map +1 -1
- package/dist/server.d.ts +130 -128
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +603 -710
- package/dist/server.js.map +1 -1
- package/dist/shared/promise.d.ts +6 -3
- package/dist/shared/promise.d.ts.map +1 -1
- package/dist/shared/promise.js +21 -19
- package/dist/shared/promise.js.map +1 -1
- package/dist/source-mask.d.ts +10 -6
- package/dist/source-mask.d.ts.map +1 -1
- package/dist/source-mask.js +60 -63
- package/dist/source-mask.js.map +1 -1
- package/dist/spread.d.ts +5 -2
- package/dist/spread.d.ts.map +1 -1
- package/dist/spread.js +79 -99
- package/dist/spread.js.map +1 -1
- package/dist/state-slot.d.ts +29 -15
- package/dist/state-slot.d.ts.map +1 -1
- package/dist/state-slot.js +73 -42
- package/dist/state-slot.js.map +1 -1
- package/dist/testing.d.ts +9 -6
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +16 -12
- package/dist/testing.js.map +1 -1
- package/package.json +8 -7
- package/src/actions.ts +415 -0
- package/src/async.ts +78 -0
- package/src/compiled/async-resource.ts +113 -0
- package/src/compiled/core.ts +4647 -0
- package/src/compiled/types.ts +96 -0
- package/src/compiled/validation.ts +23 -0
- package/src/component-spread.ts +86 -0
- package/src/concurrent.ts +310 -0
- package/src/direct-dom.ts +491 -0
- package/src/dom/attributes.ts +55 -0
- package/src/dom/events.ts +179 -0
- package/src/dom/forms.ts +220 -0
- package/src/dom/intrinsic.ts +94 -0
- package/src/dom/namespace.ts +30 -0
- package/src/dom/namespaces.ts +14 -0
- package/src/dom/properties.ts +355 -0
- package/src/dom/style-properties.ts +46 -0
- package/src/dom/styles.ts +86 -0
- package/src/dom-forms.ts +23 -0
- package/src/dom-styles.ts +11 -0
- package/src/env.d.ts +6 -0
- package/src/framework-hydrate.ts +224 -0
- package/src/framework-protocol.ts +335 -0
- package/src/framework.ts +161 -0
- package/src/hydrate.ts +38 -0
- package/src/hydration-bridge.ts +202 -0
- package/src/hydration.ts +644 -0
- package/src/index.ts +94 -0
- package/src/indexed-list.ts +20 -0
- package/src/jsx-runtime.ts +55 -0
- package/src/keyed-list.ts +379 -0
- package/src/metadata.ts +212 -0
- package/src/nested-prop.ts +27 -0
- package/src/profiling.ts +43 -0
- package/src/protocol.ts +8 -0
- package/src/raw-html.ts +206 -0
- package/src/ref.ts +13 -0
- package/src/renderable-protocol.ts +63 -0
- package/src/renderable.ts +313 -0
- package/src/retained-ui.ts +19 -0
- package/src/root.ts +122 -0
- package/src/scheduler.ts +136 -0
- package/src/server-framework.ts +397 -0
- package/src/server.ts +1160 -0
- package/src/shared/promise.ts +28 -0
- package/src/source-mask.ts +103 -0
- package/src/spread.ts +131 -0
- package/src/state-slot.ts +132 -0
- package/src/testing.ts +27 -0
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { isReactFormChangeEvent, restoreControlledFormState } from './properties.ts'
|
|
2
|
+
|
|
3
|
+
const DEV = typeof __VIDACT_DEV__ === 'undefined' || __VIDACT_DEV__
|
|
4
|
+
const delegatedClick = Symbol('vidact.click')
|
|
5
|
+
const delegatedClickCursor = Symbol('vidact.click.cursor')
|
|
6
|
+
const delegatedClickRoots = new WeakMap<EventTarget, number>()
|
|
7
|
+
export const COMPILED_DELEGATED_EVENT_INVOKE = Symbol('vidact.compiled-event.invoke')
|
|
8
|
+
|
|
9
|
+
type DelegatedEventListener = EventListener & {
|
|
10
|
+
[COMPILED_DELEGATED_EVENT_INVOKE]?: EventListener
|
|
11
|
+
}
|
|
12
|
+
type DelegatedClickElement = Element & { [delegatedClick]?: DelegatedEventListener }
|
|
13
|
+
type DelegatedClickEvent = Event & { [delegatedClickCursor]?: EventTarget }
|
|
14
|
+
|
|
15
|
+
function isSupportedReactEventName(name: string): boolean {
|
|
16
|
+
return '|Abort|AnimationEnd|AnimationIteration|AnimationStart|AuxClick|BeforeInput|BeforeToggle|Blur|CanPlay|CanPlayThrough|Cancel|Change|Click|Close|CompositionEnd|CompositionStart|CompositionUpdate|ContextMenu|Copy|Cut|DoubleClick|Drag|DragEnd|DragEnter|DragExit|DragLeave|DragOver|DragStart|Drop|DurationChange|Emptied|Encrypted|Ended|Error|Focus|GotPointerCapture|Input|Invalid|KeyDown|KeyPress|KeyUp|Load|LoadedData|LoadedMetadata|LoadStart|LostPointerCapture|MouseDown|MouseEnter|MouseLeave|MouseMove|MouseOut|MouseOver|MouseUp|Paste|Pause|Play|Playing|PointerCancel|PointerDown|PointerEnter|PointerLeave|PointerMove|PointerOut|PointerOver|PointerUp|Progress|RateChange|Reset|Resize|Scroll|ScrollEnd|Seeked|Seeking|Select|Stalled|Submit|Suspend|TimeUpdate|Toggle|TouchCancel|TouchEnd|TouchMove|TouchStart|TransitionCancel|TransitionEnd|TransitionRun|TransitionStart|VolumeChange|Waiting|Wheel|'.includes(
|
|
17
|
+
`|${name}|`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function attachEventProp(element: Element, name: string, value: unknown): () => void {
|
|
22
|
+
if (value === null || value === undefined) return () => {}
|
|
23
|
+
if (typeof value !== 'function') {
|
|
24
|
+
throw new TypeError(DEV ? `event prop ${name} must be a function, null, or undefined` : 'V201')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const reactEventName = name.slice(2)
|
|
28
|
+
const capture = reactEventName.endsWith('Capture') && !reactEventName.endsWith('PointerCapture')
|
|
29
|
+
const eventNameWithoutPhase = capture
|
|
30
|
+
? reactEventName.slice(0, -'Capture'.length)
|
|
31
|
+
: reactEventName
|
|
32
|
+
if (DEV && !isSupportedReactEventName(eventNameWithoutPhase)) {
|
|
33
|
+
throw new TypeError(DEV ? `unsupported event prop ${name}` : 'V202')
|
|
34
|
+
}
|
|
35
|
+
if (eventNameWithoutPhase === 'Change') {
|
|
36
|
+
return attachReactChangeEvent(element, value as EventListener, capture)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const eventName = nativeEventName(eventNameWithoutPhase)
|
|
40
|
+
if (eventName === 'input') {
|
|
41
|
+
const dispatch = (event: Event): void =>
|
|
42
|
+
invokeFormListener(element, value as EventListener, event)
|
|
43
|
+
element.addEventListener(eventName, dispatch, capture)
|
|
44
|
+
return () => element.removeEventListener(eventName, dispatch, capture)
|
|
45
|
+
}
|
|
46
|
+
const dispatch = (event: Event): void => (value as EventListener)(reactShapedEvent(event))
|
|
47
|
+
element.addEventListener(eventName, dispatch, capture)
|
|
48
|
+
return () => element.removeEventListener(eventName, dispatch, capture)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function attachCompiledEventProp(
|
|
52
|
+
element: Element,
|
|
53
|
+
name: string,
|
|
54
|
+
value: unknown,
|
|
55
|
+
): () => void {
|
|
56
|
+
if (name !== 'onClick') return attachEventProp(element, name, value)
|
|
57
|
+
if (value === null || value === undefined) return () => {}
|
|
58
|
+
if (typeof value !== 'function') {
|
|
59
|
+
throw new TypeError(DEV ? `event prop ${name} must be a function, null, or undefined` : 'V201')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const target = element as DelegatedClickElement
|
|
63
|
+
const listener = value as DelegatedEventListener
|
|
64
|
+
target[delegatedClick] = listener
|
|
65
|
+
return () => {
|
|
66
|
+
if (target[delegatedClick] === listener) delete target[delegatedClick]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function retainCompiledEventRoot(root: EventTarget): () => void {
|
|
71
|
+
const references = delegatedClickRoots.get(root) ?? 0
|
|
72
|
+
if (references === 0) root.addEventListener('click', dispatchDelegatedClick)
|
|
73
|
+
delegatedClickRoots.set(root, references + 1)
|
|
74
|
+
let retained = true
|
|
75
|
+
return () => {
|
|
76
|
+
if (!retained) return
|
|
77
|
+
retained = false
|
|
78
|
+
const nextReferences = (delegatedClickRoots.get(root) ?? 1) - 1
|
|
79
|
+
if (nextReferences > 0) {
|
|
80
|
+
delegatedClickRoots.set(root, nextReferences)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
delegatedClickRoots.delete(root)
|
|
84
|
+
root.removeEventListener('click', dispatchDelegatedClick)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isEventProp(name: string): boolean {
|
|
89
|
+
return /^on[A-Z]/.test(name)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function attachReactChangeEvent(
|
|
93
|
+
element: Element,
|
|
94
|
+
listener: EventListener,
|
|
95
|
+
capture: boolean,
|
|
96
|
+
): () => void {
|
|
97
|
+
const dispatch = (event: Event): void => {
|
|
98
|
+
const target = event.target
|
|
99
|
+
if (!(target instanceof Element) || !isReactFormChangeEvent(target, event.type)) return
|
|
100
|
+
invokeFormListener(element, listener, event)
|
|
101
|
+
}
|
|
102
|
+
element.addEventListener('input', dispatch, capture)
|
|
103
|
+
element.addEventListener('change', dispatch, capture)
|
|
104
|
+
return () => {
|
|
105
|
+
element.removeEventListener('input', dispatch, capture)
|
|
106
|
+
element.removeEventListener('change', dispatch, capture)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function invokeFormListener(element: Element, listener: EventListener, event: Event): void {
|
|
111
|
+
try {
|
|
112
|
+
listener(reactShapedEvent(event))
|
|
113
|
+
} finally {
|
|
114
|
+
const target = event.target as Element
|
|
115
|
+
if (isReactFormChangeEvent(target, event.type) && (target === element || event.cancelBubble)) {
|
|
116
|
+
restoreControlledFormState(target)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function reactShapedEvent(event: Event): Event {
|
|
122
|
+
const shaped = event as Event & {
|
|
123
|
+
nativeEvent?: Event
|
|
124
|
+
isDefaultPrevented?: () => boolean
|
|
125
|
+
isPropagationStopped?: () => boolean
|
|
126
|
+
persist?: () => void
|
|
127
|
+
}
|
|
128
|
+
shaped.nativeEvent ??= event
|
|
129
|
+
shaped.isDefaultPrevented ??= () => event.defaultPrevented
|
|
130
|
+
shaped.isPropagationStopped ??= () => event.cancelBubble
|
|
131
|
+
shaped.persist ??= () => {}
|
|
132
|
+
return shaped
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function dispatchDelegatedClick(event: Event): void {
|
|
136
|
+
const root = event.currentTarget
|
|
137
|
+
if (root === null) return
|
|
138
|
+
const path = event.composedPath()
|
|
139
|
+
const previousRoot = (event as DelegatedClickEvent)[delegatedClickCursor]
|
|
140
|
+
const start = previousRoot === undefined ? 0 : path.indexOf(previousRoot) + 1
|
|
141
|
+
for (let index = start; index < path.length; index += 1) {
|
|
142
|
+
const target = path[index]
|
|
143
|
+
if (target instanceof Element) {
|
|
144
|
+
const listener = (target as DelegatedClickElement)[delegatedClick]
|
|
145
|
+
if (listener !== undefined) invokeDelegatedClick(target, listener, event)
|
|
146
|
+
}
|
|
147
|
+
if (event.cancelBubble) break
|
|
148
|
+
if (target === root) break
|
|
149
|
+
}
|
|
150
|
+
;(event as DelegatedClickEvent)[delegatedClickCursor] = root
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function invokeDelegatedClick(
|
|
154
|
+
element: Element,
|
|
155
|
+
listener: DelegatedEventListener,
|
|
156
|
+
event: Event,
|
|
157
|
+
): void {
|
|
158
|
+
const currentTarget = Object.getOwnPropertyDescriptor(event, 'currentTarget')
|
|
159
|
+
Object.defineProperty(event, 'currentTarget', { configurable: true, value: element })
|
|
160
|
+
try {
|
|
161
|
+
const shapedEvent = reactShapedEvent(event)
|
|
162
|
+
const compiledInvoke = listener[COMPILED_DELEGATED_EVENT_INVOKE]
|
|
163
|
+
if (compiledInvoke === undefined) listener(shapedEvent)
|
|
164
|
+
else compiledInvoke.call(listener, shapedEvent)
|
|
165
|
+
} finally {
|
|
166
|
+
if (currentTarget === undefined) {
|
|
167
|
+
Reflect.deleteProperty(event, 'currentTarget')
|
|
168
|
+
} else {
|
|
169
|
+
Object.defineProperty(event, 'currentTarget', currentTarget)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function nativeEventName(reactEventName: string): string {
|
|
175
|
+
if (reactEventName === 'DoubleClick') return 'dblclick'
|
|
176
|
+
if (reactEventName === 'Focus') return 'focusin'
|
|
177
|
+
if (reactEventName === 'Blur') return 'focusout'
|
|
178
|
+
return reactEventName.toLowerCase()
|
|
179
|
+
}
|
package/src/dom/forms.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
type ControlledFormState = [value?: unknown, checked?: boolean]
|
|
2
|
+
|
|
3
|
+
const controlledFormStates = new WeakMap<Element, ControlledFormState>()
|
|
4
|
+
const controlledRestorationCleanups = new WeakMap<Element, () => void>()
|
|
5
|
+
const DEV = typeof __VIDACT_DEV__ === 'undefined' || __VIDACT_DEV__
|
|
6
|
+
|
|
7
|
+
export function isControlledFormProp(element: Element, name: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
(name === 'value' &&
|
|
10
|
+
(element instanceof HTMLInputElement ||
|
|
11
|
+
element instanceof HTMLTextAreaElement ||
|
|
12
|
+
element instanceof HTMLSelectElement)) ||
|
|
13
|
+
(name === 'checked' && element instanceof HTMLInputElement)
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ensureControlledFormRestoration(element: Element): () => void {
|
|
18
|
+
const existing = controlledRestorationCleanups.get(element)
|
|
19
|
+
if (existing !== undefined) return () => {}
|
|
20
|
+
|
|
21
|
+
const schedule = (event: Event): void => {
|
|
22
|
+
if (!isReactFormChangeEvent(element, event.type)) return
|
|
23
|
+
const root = element.getRootNode()
|
|
24
|
+
let restored = false
|
|
25
|
+
const restore = (): void => {
|
|
26
|
+
if (restored) return
|
|
27
|
+
restored = true
|
|
28
|
+
root.removeEventListener(event.type, restore)
|
|
29
|
+
restoreControlledFormState(element)
|
|
30
|
+
}
|
|
31
|
+
root.addEventListener(event.type, restore, { once: true })
|
|
32
|
+
setTimeout(restore, 0)
|
|
33
|
+
}
|
|
34
|
+
element.addEventListener('input', schedule)
|
|
35
|
+
element.addEventListener('change', schedule)
|
|
36
|
+
const cleanup = (): void => {
|
|
37
|
+
element.removeEventListener('input', schedule)
|
|
38
|
+
element.removeEventListener('change', schedule)
|
|
39
|
+
controlledRestorationCleanups.delete(element)
|
|
40
|
+
}
|
|
41
|
+
controlledRestorationCleanups.set(element, cleanup)
|
|
42
|
+
return cleanup
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isReactFormChangeEvent(element: Element, eventName: string): boolean {
|
|
46
|
+
if (element instanceof HTMLTextAreaElement) return eventName === 'input'
|
|
47
|
+
if (element instanceof HTMLSelectElement) return eventName === 'change'
|
|
48
|
+
if (!(element instanceof HTMLInputElement)) return false
|
|
49
|
+
return element.type === 'checkbox' || element.type === 'radio' || element.type === 'file'
|
|
50
|
+
? eventName === 'change'
|
|
51
|
+
: eventName === 'input'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function applyFormProp(element: Element, name: string, value: unknown): boolean {
|
|
55
|
+
if (name === 'value') {
|
|
56
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
delete remember(element)[0]
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
const next = value === null || value === undefined ? '' : String(value)
|
|
62
|
+
remember(element)[0] = next
|
|
63
|
+
if (element.value !== next) element.value = next
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
if (element instanceof HTMLSelectElement) {
|
|
67
|
+
if (value === undefined) {
|
|
68
|
+
delete remember(element)[0]
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
const next = normalizeSelectValue(element, value)
|
|
72
|
+
remember(element)[0] = value
|
|
73
|
+
applySelectValue(element, next)
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (name === 'checked' && element instanceof HTMLInputElement) {
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
delete remember(element)[1]
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
const next = Boolean(value)
|
|
84
|
+
remember(element)[1] = next
|
|
85
|
+
if (element.checked !== next) element.checked = next
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
if (name === 'defaultValue' && 'defaultValue' in element) {
|
|
89
|
+
const next = value === null || value === undefined ? '' : String(value)
|
|
90
|
+
if (Reflect.get(element, 'defaultValue') !== next) Reflect.set(element, 'defaultValue', next)
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
if (name === 'defaultChecked' && element instanceof HTMLInputElement) {
|
|
94
|
+
element.defaultChecked = Boolean(value)
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
if (name === 'selected' && element instanceof HTMLOptionElement) {
|
|
98
|
+
element.selected = Boolean(value)
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
if (name === 'multiple' && element instanceof HTMLSelectElement) {
|
|
102
|
+
element.multiple = Boolean(value)
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
if (name === 'muted' && element instanceof HTMLMediaElement) {
|
|
106
|
+
element.muted = Boolean(value)
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function restoreControlledForm(form: HTMLFormElement): void {
|
|
113
|
+
for (const element of form.elements) {
|
|
114
|
+
if (element instanceof Element) restoreControlledFormState(element)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resetActionForm(form: HTMLFormElement): void {
|
|
119
|
+
for (const element of form.elements) {
|
|
120
|
+
const controlled = controlledFormStates.get(element)
|
|
121
|
+
if (controlled?.[0] !== undefined || controlled?.[1] !== undefined) continue
|
|
122
|
+
if (element instanceof HTMLInputElement) {
|
|
123
|
+
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
124
|
+
element.checked = element.defaultChecked
|
|
125
|
+
} else {
|
|
126
|
+
element.value = element.type === 'file' ? '' : element.defaultValue
|
|
127
|
+
}
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
131
|
+
element.value = element.defaultValue
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (element instanceof HTMLSelectElement) resetSelect(element)
|
|
135
|
+
}
|
|
136
|
+
restoreControlledForm(form)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function restoreControlledFormState(element: Element): void {
|
|
140
|
+
restoreElement(element)
|
|
141
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'radio' || element.name === '') {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const root = element.getRootNode()
|
|
146
|
+
if (!(root instanceof Document || root instanceof ShadowRoot)) return
|
|
147
|
+
const escapedName = CSS.escape(element.name)
|
|
148
|
+
for (const candidate of root.querySelectorAll<HTMLInputElement>(
|
|
149
|
+
`input[type="radio"][name="${escapedName}"]`,
|
|
150
|
+
)) {
|
|
151
|
+
if (candidate !== element && candidate.form === element.form) restoreElement(candidate)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function restoreElement(element: Element): void {
|
|
156
|
+
const state = controlledFormStates.get(element)
|
|
157
|
+
if (state === undefined) return
|
|
158
|
+
if (state[0] !== undefined) {
|
|
159
|
+
if (element instanceof HTMLSelectElement) {
|
|
160
|
+
applySelectValue(element, normalizeSelectValue(element, state[0]))
|
|
161
|
+
} else if (
|
|
162
|
+
(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) &&
|
|
163
|
+
element.value !== state[0]
|
|
164
|
+
) {
|
|
165
|
+
element.value = String(state[0])
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
state[1] !== undefined &&
|
|
170
|
+
element instanceof HTMLInputElement &&
|
|
171
|
+
element.checked !== state[1]
|
|
172
|
+
) {
|
|
173
|
+
element.checked = state[1]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function remember(element: Element): ControlledFormState {
|
|
178
|
+
const existing = controlledFormStates.get(element)
|
|
179
|
+
if (existing !== undefined) return existing
|
|
180
|
+
const state: ControlledFormState = []
|
|
181
|
+
controlledFormStates.set(element, state)
|
|
182
|
+
return state
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeSelectValue(element: HTMLSelectElement, value: unknown): unknown {
|
|
186
|
+
if (!element.multiple) return value === null || value === undefined ? '' : String(value)
|
|
187
|
+
if (value === null || value === undefined) return []
|
|
188
|
+
if (!Array.isArray(value)) {
|
|
189
|
+
throw new TypeError(DEV ? 'a controlled <select multiple> value must be an array' : 'V301')
|
|
190
|
+
}
|
|
191
|
+
return value.map(String)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function applySelectValue(element: HTMLSelectElement, value: unknown): void {
|
|
195
|
+
if (!element.multiple) {
|
|
196
|
+
const selected = String(value)
|
|
197
|
+
let matched = false
|
|
198
|
+
for (const option of element.options) {
|
|
199
|
+
const next = option.value === selected
|
|
200
|
+
option.selected = next
|
|
201
|
+
matched ||= next
|
|
202
|
+
}
|
|
203
|
+
if (!matched) element.selectedIndex = -1
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const selected = new Set(value as readonly string[])
|
|
208
|
+
for (const option of element.options) option.selected = selected.has(option.value)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resetSelect(element: HTMLSelectElement): void {
|
|
212
|
+
let selected = false
|
|
213
|
+
for (const option of element.options) {
|
|
214
|
+
option.selected = option.defaultSelected
|
|
215
|
+
selected ||= option.selected
|
|
216
|
+
}
|
|
217
|
+
if (!element.multiple && !selected && element.options.length > 0) {
|
|
218
|
+
element.options[0]!.selected = true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const DEV = typeof __VIDACT_DEV__ === 'undefined' || __VIDACT_DEV__
|
|
2
|
+
|
|
3
|
+
export const INTERNAL_NAMESPACE_PROP = '__vidactNamespace'
|
|
4
|
+
export const INTERNAL_COMPONENT_SPREAD_PROP = Symbol(DEV ? 'Vidact.ComponentSpread' : undefined)
|
|
5
|
+
|
|
6
|
+
export type IntrinsicNamespace = 'html' | 'svg' | 'mathml'
|
|
7
|
+
|
|
8
|
+
interface NamespaceDomCapability {
|
|
9
|
+
readonly childrenNamespace: (type: string, namespace: IntrinsicNamespace) => IntrinsicNamespace
|
|
10
|
+
readonly createElement: (
|
|
11
|
+
document: Document,
|
|
12
|
+
type: string,
|
|
13
|
+
namespace: IntrinsicNamespace,
|
|
14
|
+
) => Element
|
|
15
|
+
readonly resolveNamespace: (
|
|
16
|
+
type: string,
|
|
17
|
+
explicit: IntrinsicNamespace | undefined,
|
|
18
|
+
inherited: IntrinsicNamespace,
|
|
19
|
+
) => IntrinsicNamespace
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let activeNamespace: IntrinsicNamespace = 'html'
|
|
23
|
+
let namespaceCapability: NamespaceDomCapability | undefined
|
|
24
|
+
|
|
25
|
+
/** @internal */
|
|
26
|
+
export function installNamespaceDomCapability(capability: NamespaceDomCapability): void {
|
|
27
|
+
namespaceCapability = capability
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function currentIntrinsicNamespace(): IntrinsicNamespace {
|
|
31
|
+
return activeNamespace
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readIntrinsicNamespace(
|
|
35
|
+
props: Record<string, unknown> | null,
|
|
36
|
+
): IntrinsicNamespace | undefined {
|
|
37
|
+
const value = props?.[INTERNAL_NAMESPACE_PROP]
|
|
38
|
+
return value === 'html' || value === 'svg' || value === 'mathml' ? value : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createComponentProps(
|
|
42
|
+
props: Record<string, unknown> | null,
|
|
43
|
+
children: readonly unknown[],
|
|
44
|
+
): Record<string, unknown> {
|
|
45
|
+
const publicProps: Record<string, unknown> = { ...props }
|
|
46
|
+
if (children.length === 1) publicProps.children = children[0]
|
|
47
|
+
else if (children.length > 1) publicProps.children = children
|
|
48
|
+
delete publicProps[INTERNAL_NAMESPACE_PROP]
|
|
49
|
+
delete publicProps.key
|
|
50
|
+
if (publicProps.ref === undefined) delete publicProps.ref
|
|
51
|
+
const propsWithDirective = publicProps as Record<PropertyKey, unknown>
|
|
52
|
+
const directive = propsWithDirective[INTERNAL_COMPONENT_SPREAD_PROP]
|
|
53
|
+
delete propsWithDirective[INTERNAL_COMPONENT_SPREAD_PROP]
|
|
54
|
+
return typeof directive === 'function'
|
|
55
|
+
? (directive(publicProps) as Record<string, unknown>)
|
|
56
|
+
: publicProps
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function withIntrinsicNamespace<Result>(
|
|
60
|
+
namespace: IntrinsicNamespace | undefined,
|
|
61
|
+
render: () => Result,
|
|
62
|
+
): Result {
|
|
63
|
+
const previous = activeNamespace
|
|
64
|
+
activeNamespace = namespace ?? previous
|
|
65
|
+
try {
|
|
66
|
+
return render()
|
|
67
|
+
} finally {
|
|
68
|
+
activeNamespace = previous
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createIntrinsicElement(
|
|
73
|
+
document: Document,
|
|
74
|
+
type: string,
|
|
75
|
+
namespace: IntrinsicNamespace,
|
|
76
|
+
): Element {
|
|
77
|
+
return (
|
|
78
|
+
namespaceCapability?.createElement(document, type, namespace) ?? document.createElement(type)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveIntrinsicNamespace(
|
|
83
|
+
type: string,
|
|
84
|
+
namespace: IntrinsicNamespace | undefined,
|
|
85
|
+
): IntrinsicNamespace {
|
|
86
|
+
return namespaceCapability?.resolveNamespace(type, namespace, activeNamespace) ?? activeNamespace
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function intrinsicChildrenNamespace(
|
|
90
|
+
type: string,
|
|
91
|
+
namespace: IntrinsicNamespace,
|
|
92
|
+
): IntrinsicNamespace {
|
|
93
|
+
return namespaceCapability?.childrenNamespace(type, namespace) ?? namespace
|
|
94
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { installNamespaceDomCapability, type IntrinsicNamespace } from './intrinsic.ts'
|
|
2
|
+
import { MATHML_NAMESPACE, SVG_NAMESPACE } from './namespaces.ts'
|
|
3
|
+
|
|
4
|
+
let enabled = false
|
|
5
|
+
|
|
6
|
+
/** @internal Compiler-injected activation for SVG and MathML namespace behavior. */
|
|
7
|
+
export function enableDomNamespace(): void {
|
|
8
|
+
if (enabled) return
|
|
9
|
+
enabled = true
|
|
10
|
+
installNamespaceDomCapability({
|
|
11
|
+
childrenNamespace: (type, namespace) =>
|
|
12
|
+
namespace === 'svg' && type === 'foreignObject' ? 'html' : namespace,
|
|
13
|
+
createElement(document, type, namespace) {
|
|
14
|
+
if (namespace === 'svg') return document.createElementNS(SVG_NAMESPACE, type)
|
|
15
|
+
if (namespace === 'mathml') return document.createElementNS(MATHML_NAMESPACE, type)
|
|
16
|
+
return document.createElement(type)
|
|
17
|
+
},
|
|
18
|
+
resolveNamespace(type, explicit, inherited) {
|
|
19
|
+
if (explicit !== undefined) return explicit
|
|
20
|
+
return rootNamespace(type, inherited)
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rootNamespace(type: string, inherited: IntrinsicNamespace): IntrinsicNamespace {
|
|
26
|
+
if (inherited !== 'html') return inherited
|
|
27
|
+
if (type === 'svg') return 'svg'
|
|
28
|
+
if (type === 'math') return 'mathml'
|
|
29
|
+
return inherited
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IntrinsicNamespace } from './intrinsic.ts'
|
|
2
|
+
|
|
3
|
+
export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
|
|
4
|
+
export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
|
|
5
|
+
export const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'
|
|
6
|
+
export const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'
|
|
7
|
+
export const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'
|
|
8
|
+
export const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'
|
|
9
|
+
|
|
10
|
+
export function intrinsicNamespaceUrl(namespace: IntrinsicNamespace): string {
|
|
11
|
+
if (namespace === 'svg') return SVG_NAMESPACE
|
|
12
|
+
if (namespace === 'mathml') return MATHML_NAMESPACE
|
|
13
|
+
return HTML_NAMESPACE
|
|
14
|
+
}
|