@furystack/shades 11.0.35 → 12.0.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/CHANGELOG.md +337 -0
- package/README.md +99 -13
- package/esm/compile-route.spec.d.ts +2 -0
- package/esm/compile-route.spec.d.ts.map +1 -0
- package/esm/compile-route.spec.js +34 -0
- package/esm/compile-route.spec.js.map +1 -0
- package/esm/component-factory.spec.js +13 -5
- package/esm/component-factory.spec.js.map +1 -1
- package/esm/components/index.d.ts +4 -1
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +4 -1
- package/esm/components/index.js.map +1 -1
- package/esm/components/lazy-load.d.ts +2 -4
- package/esm/components/lazy-load.d.ts.map +1 -1
- package/esm/components/lazy-load.js +40 -24
- package/esm/components/lazy-load.js.map +1 -1
- package/esm/components/lazy-load.spec.js +57 -50
- package/esm/components/lazy-load.spec.js.map +1 -1
- package/esm/components/link-to-route.d.ts +2 -0
- package/esm/components/link-to-route.d.ts.map +1 -1
- package/esm/components/link-to-route.js +3 -2
- package/esm/components/link-to-route.js.map +1 -1
- package/esm/components/link-to-route.spec.js +13 -9
- package/esm/components/link-to-route.spec.js.map +1 -1
- package/esm/components/nested-route-link.d.ts +62 -0
- package/esm/components/nested-route-link.d.ts.map +1 -0
- package/esm/components/nested-route-link.js +66 -0
- package/esm/components/nested-route-link.js.map +1 -0
- package/esm/components/nested-route-link.spec.d.ts +2 -0
- package/esm/components/nested-route-link.spec.d.ts.map +1 -0
- package/esm/components/nested-route-link.spec.js +179 -0
- package/esm/components/nested-route-link.spec.js.map +1 -0
- package/esm/components/nested-route-types.d.ts +37 -0
- package/esm/components/nested-route-types.d.ts.map +1 -0
- package/esm/components/nested-route-types.js +2 -0
- package/esm/components/nested-route-types.js.map +1 -0
- package/esm/components/nested-router.d.ts +103 -0
- package/esm/components/nested-router.d.ts.map +1 -0
- package/esm/components/nested-router.js +178 -0
- package/esm/components/nested-router.js.map +1 -0
- package/esm/components/nested-router.spec.d.ts +2 -0
- package/esm/components/nested-router.spec.d.ts.map +1 -0
- package/esm/components/nested-router.spec.js +659 -0
- package/esm/components/nested-router.spec.js.map +1 -0
- package/esm/components/route-link.d.ts +4 -0
- package/esm/components/route-link.d.ts.map +1 -1
- package/esm/components/route-link.js +9 -10
- package/esm/components/route-link.js.map +1 -1
- package/esm/components/route-link.spec.js +16 -12
- package/esm/components/route-link.spec.js.map +1 -1
- package/esm/components/router.d.ts +20 -2
- package/esm/components/router.d.ts.map +1 -1
- package/esm/components/router.js +3 -0
- package/esm/components/router.js.map +1 -1
- package/esm/components/router.spec.js +75 -74
- package/esm/components/router.spec.js.map +1 -1
- package/esm/css-generator.d.ts +50 -0
- package/esm/css-generator.d.ts.map +1 -0
- package/esm/css-generator.js +107 -0
- package/esm/css-generator.js.map +1 -0
- package/esm/css-generator.spec.d.ts +2 -0
- package/esm/css-generator.spec.d.ts.map +1 -0
- package/esm/css-generator.spec.js +162 -0
- package/esm/css-generator.spec.js.map +1 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -1
- package/esm/initialize.d.ts +11 -0
- package/esm/initialize.d.ts.map +1 -1
- package/esm/initialize.js +5 -0
- package/esm/initialize.js.map +1 -1
- package/esm/jsx.d.ts +83 -2
- package/esm/jsx.d.ts.map +1 -1
- package/esm/models/children-list.d.ts +5 -1
- package/esm/models/children-list.d.ts.map +1 -1
- package/esm/models/css-object.d.ts +33 -0
- package/esm/models/css-object.d.ts.map +1 -0
- package/esm/models/css-object.js +2 -0
- package/esm/models/css-object.js.map +1 -0
- package/esm/models/index.d.ts +1 -0
- package/esm/models/index.d.ts.map +1 -1
- package/esm/models/index.js +1 -0
- package/esm/models/index.js.map +1 -1
- package/esm/models/partial-element.d.ts +12 -2
- package/esm/models/partial-element.d.ts.map +1 -1
- package/esm/models/render-options.d.ts +89 -3
- package/esm/models/render-options.d.ts.map +1 -1
- package/esm/models/selection-state.d.ts +4 -0
- package/esm/models/selection-state.d.ts.map +1 -1
- package/esm/services/location-service.d.ts +11 -0
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +11 -0
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/resource-manager.d.ts +24 -0
- package/esm/services/resource-manager.d.ts.map +1 -1
- package/esm/services/resource-manager.js +30 -0
- package/esm/services/resource-manager.js.map +1 -1
- package/esm/services/resource-manager.spec.js +93 -0
- package/esm/services/resource-manager.spec.js.map +1 -1
- package/esm/services/screen-service.d.ts +81 -4
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +75 -4
- package/esm/services/screen-service.js.map +1 -1
- package/esm/services/screen-service.spec.js +91 -7
- package/esm/services/screen-service.spec.js.map +1 -1
- package/esm/shade-component.d.ts +17 -4
- package/esm/shade-component.d.ts.map +1 -1
- package/esm/shade-component.js +67 -5
- package/esm/shade-component.js.map +1 -1
- package/esm/shade-host-props-ref.integration.spec.d.ts +2 -0
- package/esm/shade-host-props-ref.integration.spec.d.ts.map +1 -0
- package/esm/shade-host-props-ref.integration.spec.js +381 -0
- package/esm/shade-host-props-ref.integration.spec.js.map +1 -0
- package/esm/shade-resources.integration.spec.js +208 -39
- package/esm/shade-resources.integration.spec.js.map +1 -1
- package/esm/shade.d.ts +34 -15
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +180 -33
- package/esm/shade.js.map +1 -1
- package/esm/shade.spec.d.ts +2 -0
- package/esm/shade.spec.d.ts.map +1 -0
- package/esm/shade.spec.js +198 -0
- package/esm/shade.spec.js.map +1 -0
- package/esm/shades.integration.spec.js +135 -72
- package/esm/shades.integration.spec.js.map +1 -1
- package/esm/style-manager.d.ts +65 -0
- package/esm/style-manager.d.ts.map +1 -0
- package/esm/style-manager.js +95 -0
- package/esm/style-manager.js.map +1 -0
- package/esm/style-manager.spec.d.ts +2 -0
- package/esm/style-manager.spec.d.ts.map +1 -0
- package/esm/style-manager.spec.js +179 -0
- package/esm/style-manager.spec.js.map +1 -0
- package/esm/styled-element.spec.d.ts +2 -0
- package/esm/styled-element.spec.d.ts.map +1 -0
- package/esm/styled-element.spec.js +86 -0
- package/esm/styled-element.spec.js.map +1 -0
- package/esm/styled-shade.spec.d.ts +2 -0
- package/esm/styled-shade.spec.d.ts.map +1 -0
- package/esm/styled-shade.spec.js +66 -0
- package/esm/styled-shade.spec.js.map +1 -0
- package/esm/svg-types.d.ts +389 -0
- package/esm/svg-types.d.ts.map +1 -0
- package/esm/svg-types.js +9 -0
- package/esm/svg-types.js.map +1 -0
- package/esm/svg.d.ts +15 -0
- package/esm/svg.d.ts.map +1 -0
- package/esm/svg.js +76 -0
- package/esm/svg.js.map +1 -0
- package/esm/svg.spec.d.ts +2 -0
- package/esm/svg.spec.d.ts.map +1 -0
- package/esm/svg.spec.js +80 -0
- package/esm/svg.spec.js.map +1 -0
- package/esm/vnode.d.ts +103 -0
- package/esm/vnode.d.ts.map +1 -0
- package/esm/vnode.integration.spec.d.ts +2 -0
- package/esm/vnode.integration.spec.d.ts.map +1 -0
- package/esm/vnode.integration.spec.js +494 -0
- package/esm/vnode.integration.spec.js.map +1 -0
- package/esm/vnode.js +453 -0
- package/esm/vnode.js.map +1 -0
- package/esm/vnode.spec.d.ts +2 -0
- package/esm/vnode.spec.d.ts.map +1 -0
- package/esm/vnode.spec.js +473 -0
- package/esm/vnode.spec.js.map +1 -0
- package/package.json +3 -3
- package/src/compile-route.spec.ts +39 -0
- package/src/component-factory.spec.tsx +18 -5
- package/src/components/index.ts +4 -1
- package/src/components/lazy-load.spec.tsx +82 -75
- package/src/components/lazy-load.tsx +49 -27
- package/src/components/link-to-route.spec.tsx +25 -21
- package/src/components/link-to-route.tsx +4 -2
- package/src/components/nested-route-link.spec.tsx +303 -0
- package/src/components/nested-route-link.tsx +100 -0
- package/src/components/nested-route-types.ts +42 -0
- package/src/components/nested-router.spec.tsx +817 -0
- package/src/components/nested-router.tsx +256 -0
- package/src/components/route-link.spec.tsx +22 -18
- package/src/components/route-link.tsx +10 -10
- package/src/components/router.spec.tsx +109 -108
- package/src/components/router.tsx +15 -2
- package/src/css-generator.spec.ts +183 -0
- package/src/css-generator.ts +117 -0
- package/src/index.ts +2 -0
- package/src/initialize.ts +12 -0
- package/src/jsx.ts +129 -2
- package/src/models/children-list.ts +7 -1
- package/src/models/css-object.ts +34 -0
- package/src/models/index.ts +1 -0
- package/src/models/partial-element.ts +13 -2
- package/src/models/render-options.ts +90 -3
- package/src/models/selection-state.ts +4 -0
- package/src/services/location-service.tsx +11 -0
- package/src/services/resource-manager.spec.ts +116 -0
- package/src/services/resource-manager.ts +30 -0
- package/src/services/screen-service.spec.ts +109 -7
- package/src/services/screen-service.ts +81 -4
- package/src/shade-component.ts +72 -6
- package/src/shade-host-props-ref.integration.spec.tsx +460 -0
- package/src/shade-resources.integration.spec.tsx +276 -52
- package/src/shade.spec.tsx +239 -0
- package/src/shade.ts +211 -56
- package/src/shades.integration.spec.tsx +154 -80
- package/src/style-manager.spec.ts +229 -0
- package/src/style-manager.ts +104 -0
- package/src/styled-element.spec.tsx +117 -0
- package/src/styled-shade.spec.ts +86 -0
- package/src/svg-types.ts +437 -0
- package/src/svg.spec.ts +89 -0
- package/src/svg.ts +78 -0
- package/src/vnode.integration.spec.tsx +657 -0
- package/src/vnode.spec.ts +579 -0
- package/src/vnode.ts +508 -0
package/src/vnode.ts
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VNode-based reconciliation for Shades.
|
|
3
|
+
*
|
|
4
|
+
* Instead of creating real DOM elements during each render and then diffing them,
|
|
5
|
+
* the JSX factory produces lightweight VNode descriptors. A reconciler diffs the
|
|
6
|
+
* previous VNode tree against the new one and applies surgical DOM updates using
|
|
7
|
+
* tracked `_el` references.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChildrenList } from './models/children-list.js'
|
|
11
|
+
import type { RefObject } from './models/render-options.js'
|
|
12
|
+
import { SVG_NS, isSvgTag } from './svg.js'
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Brands & sentinels
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const VNODE_BRAND = 'vnode' as const
|
|
19
|
+
const VTEXT_BRAND = 'vtext' as const
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel type used as VNode.type for JSX fragments (`<>...</>`).
|
|
23
|
+
*/
|
|
24
|
+
export const FRAGMENT: unique symbol = Symbol('fragment')
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sentinel type for VNodes that wrap a pre-existing real DOM node.
|
|
28
|
+
* Used when shadeChildren (created outside render mode) flow into a VNode render.
|
|
29
|
+
*/
|
|
30
|
+
export const EXISTING_NODE: unique symbol = Symbol('existing-node')
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A lightweight descriptor for a DOM element or Shade component.
|
|
38
|
+
*/
|
|
39
|
+
export type VNode = {
|
|
40
|
+
_brand: typeof VNODE_BRAND
|
|
41
|
+
type: string | ((...args: unknown[]) => unknown) | typeof FRAGMENT | typeof EXISTING_NODE
|
|
42
|
+
props: Record<string, unknown> | null
|
|
43
|
+
children: VChild[]
|
|
44
|
+
_el?: Node
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A lightweight descriptor for a DOM text node.
|
|
49
|
+
*/
|
|
50
|
+
export type VTextNode = {
|
|
51
|
+
_brand: typeof VTEXT_BRAND
|
|
52
|
+
text: string
|
|
53
|
+
_el?: Text
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A single child in a VNode tree -- either an element/component or a text node.
|
|
58
|
+
*/
|
|
59
|
+
export type VChild = VNode | VTextNode
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Type guards
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export const isVNode = (v: unknown): v is VNode =>
|
|
66
|
+
typeof v === 'object' && v !== null && (v as VNode)._brand === VNODE_BRAND
|
|
67
|
+
|
|
68
|
+
export const isVTextNode = (v: unknown): v is VTextNode =>
|
|
69
|
+
typeof v === 'object' && v !== null && (v as VTextNode)._brand === VTEXT_BRAND
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// VNode creation
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursively flattens raw JSX children into a flat VChild array.
|
|
77
|
+
* - Strings and numbers become VTextNodes.
|
|
78
|
+
* - Fragment VNodes are inlined (their children are spliced in).
|
|
79
|
+
* - Nullish / boolean values are skipped.
|
|
80
|
+
*/
|
|
81
|
+
export const flattenVChildren = (raw: unknown[]): VChild[] => {
|
|
82
|
+
const result: VChild[] = []
|
|
83
|
+
for (const child of raw) {
|
|
84
|
+
if (child === null || child === undefined || child === false || child === true) continue
|
|
85
|
+
if (typeof child === 'string') {
|
|
86
|
+
result.push({ _brand: VTEXT_BRAND, text: child })
|
|
87
|
+
} else if (typeof child === 'number') {
|
|
88
|
+
result.push({ _brand: VTEXT_BRAND, text: String(child) })
|
|
89
|
+
} else if (Array.isArray(child)) {
|
|
90
|
+
result.push(...flattenVChildren(child))
|
|
91
|
+
} else if (isVNode(child)) {
|
|
92
|
+
if (child.type === FRAGMENT) {
|
|
93
|
+
result.push(...child.children)
|
|
94
|
+
} else {
|
|
95
|
+
result.push(child)
|
|
96
|
+
}
|
|
97
|
+
} else if (isVTextNode(child)) {
|
|
98
|
+
result.push(child)
|
|
99
|
+
} else if (child instanceof Node) {
|
|
100
|
+
// Real DOM node from shadeChildren (created outside render mode).
|
|
101
|
+
// Wrap it so the reconciler can track it.
|
|
102
|
+
result.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props: null, children: [], _el: child })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a VNode descriptor. Used as the JSX factory during renders.
|
|
110
|
+
*
|
|
111
|
+
* For intrinsic elements (string type), the returned VNode includes DOM-shim
|
|
112
|
+
* methods (`setAttribute`, `appendChild`, etc.) so that component code which
|
|
113
|
+
* creates intermediate JSX and calls DOM methods on it continues to work.
|
|
114
|
+
*
|
|
115
|
+
* @param type Tag name, Shade factory function, or null (fragment)
|
|
116
|
+
* @param props Element props / component props
|
|
117
|
+
* @param rawChildren Varargs children (strings, VNodes, arrays, etc.)
|
|
118
|
+
*/
|
|
119
|
+
export const createVNode = (
|
|
120
|
+
type: string | ((...args: unknown[]) => unknown) | null,
|
|
121
|
+
props: Record<string, unknown> | null,
|
|
122
|
+
...rawChildren: unknown[]
|
|
123
|
+
): VNode => {
|
|
124
|
+
const children = flattenVChildren(rawChildren)
|
|
125
|
+
const vnode: VNode = {
|
|
126
|
+
_brand: VNODE_BRAND,
|
|
127
|
+
type: type === null ? FRAGMENT : type,
|
|
128
|
+
props: props ? { ...props } : null,
|
|
129
|
+
children,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For intrinsic elements, add DOM-shim methods so that component render code
|
|
133
|
+
// which does `const el = <div/>; el.setAttribute(...)` still works in VNode mode.
|
|
134
|
+
if (typeof type === 'string') {
|
|
135
|
+
const v = vnode as unknown as Record<string, unknown>
|
|
136
|
+
v.setAttribute = (name: string, value: string) => {
|
|
137
|
+
if (!vnode.props) vnode.props = {}
|
|
138
|
+
vnode.props[name] = value
|
|
139
|
+
}
|
|
140
|
+
v.removeAttribute = (name: string) => {
|
|
141
|
+
if (vnode.props) delete vnode.props[name]
|
|
142
|
+
}
|
|
143
|
+
v.getAttribute = (name: string) => {
|
|
144
|
+
return (vnode.props?.[name] as string) ?? null
|
|
145
|
+
}
|
|
146
|
+
v.hasAttribute = (name: string) => {
|
|
147
|
+
return vnode.props ? name in vnode.props : false
|
|
148
|
+
}
|
|
149
|
+
v.appendChild = (child: unknown) => {
|
|
150
|
+
if (child instanceof Node) {
|
|
151
|
+
vnode.children.push({ _brand: VNODE_BRAND, type: EXISTING_NODE, props: null, children: [], _el: child })
|
|
152
|
+
} else if (isVNode(child) || isVTextNode(child)) {
|
|
153
|
+
vnode.children.push(child)
|
|
154
|
+
}
|
|
155
|
+
return child
|
|
156
|
+
}
|
|
157
|
+
v.tagName = type.toUpperCase()
|
|
158
|
+
v.nodeName = type.toUpperCase()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return vnode
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Helpers
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Shallow-compares two props objects. Returns true if all keys and values match.
|
|
170
|
+
*/
|
|
171
|
+
export const shallowEqual = (a: Record<string, unknown> | null, b: Record<string, unknown> | null): boolean => {
|
|
172
|
+
if (a === b) return true
|
|
173
|
+
if (!a || !b) return false
|
|
174
|
+
const keysA = Object.keys(a)
|
|
175
|
+
const keysB = Object.keys(b)
|
|
176
|
+
if (keysA.length !== keysB.length) return false
|
|
177
|
+
for (const key of keysA) {
|
|
178
|
+
if (a[key] !== b[key]) return false
|
|
179
|
+
}
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Converts a render result (VNode | HTMLElement | string | null) into a flat
|
|
185
|
+
* VChild array suitable for `patchChildren`.
|
|
186
|
+
*
|
|
187
|
+
* Real DOM elements can appear here when component state stores elements created
|
|
188
|
+
* outside renderMode (e.g. in async callbacks like Router's `updateUrl`).
|
|
189
|
+
*/
|
|
190
|
+
export const toVChildArray = (renderResult: unknown): VChild[] => {
|
|
191
|
+
if (renderResult === null || renderResult === undefined) return []
|
|
192
|
+
if (typeof renderResult === 'string' || typeof renderResult === 'number') {
|
|
193
|
+
return [{ _brand: VTEXT_BRAND, text: String(renderResult) }]
|
|
194
|
+
}
|
|
195
|
+
if (isVNode(renderResult)) {
|
|
196
|
+
if (renderResult.type === FRAGMENT) return renderResult.children
|
|
197
|
+
return [renderResult]
|
|
198
|
+
}
|
|
199
|
+
// Real DOM element (from async code that ran outside renderMode)
|
|
200
|
+
if (renderResult instanceof DocumentFragment) {
|
|
201
|
+
return Array.from(renderResult.childNodes).map((node) => ({
|
|
202
|
+
_brand: VNODE_BRAND as typeof VNODE_BRAND,
|
|
203
|
+
type: EXISTING_NODE,
|
|
204
|
+
props: null,
|
|
205
|
+
children: [] as VChild[],
|
|
206
|
+
_el: node,
|
|
207
|
+
}))
|
|
208
|
+
}
|
|
209
|
+
if (renderResult instanceof Node) {
|
|
210
|
+
return [{ _brand: VNODE_BRAND, type: EXISTING_NODE, props: null, children: [], _el: renderResult }]
|
|
211
|
+
}
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Props / style application
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
const setProp = (el: Element, key: string, value: unknown): void => {
|
|
220
|
+
if (key === 'ref') return
|
|
221
|
+
if (key === 'style' && typeof value === 'object' && value !== null) {
|
|
222
|
+
for (const [sk, sv] of Object.entries(value as Record<string, string>)) {
|
|
223
|
+
;((el as HTMLElement).style as unknown as Record<string, string>)[sk] = sv
|
|
224
|
+
}
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (el instanceof SVGElement) {
|
|
229
|
+
if (key === 'className') {
|
|
230
|
+
el.setAttribute('class', String(value))
|
|
231
|
+
} else if (key.startsWith('on') && typeof value === 'function') {
|
|
232
|
+
;(el as unknown as Record<string, unknown>)[key] = value
|
|
233
|
+
} else if (value === null || value === undefined || value === false) {
|
|
234
|
+
el.removeAttribute(key)
|
|
235
|
+
} else {
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
237
|
+
el.setAttribute(key, String(value))
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (key.startsWith('data-') || key.startsWith('aria-')) {
|
|
243
|
+
el.setAttribute(key, typeof value === 'string' ? value : '')
|
|
244
|
+
} else {
|
|
245
|
+
;(el as unknown as Record<string, unknown>)[key] = value
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const removeProp = (el: Element, key: string): void => {
|
|
250
|
+
if (key === 'ref') return
|
|
251
|
+
if (el instanceof SVGElement) {
|
|
252
|
+
el.removeAttribute(key === 'className' ? 'class' : key)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (key === 'style') {
|
|
257
|
+
el.removeAttribute('style')
|
|
258
|
+
} else if (key.startsWith('data-') || key.startsWith('aria-')) {
|
|
259
|
+
el.removeAttribute(key)
|
|
260
|
+
} else if (key.startsWith('on')) {
|
|
261
|
+
;(el as unknown as Record<string, unknown>)[key] = null
|
|
262
|
+
} else {
|
|
263
|
+
try {
|
|
264
|
+
;(el as unknown as Record<string, unknown>)[key] = ''
|
|
265
|
+
} catch {
|
|
266
|
+
// Some properties are read-only
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const patchStyle = (
|
|
272
|
+
el: Element,
|
|
273
|
+
oldStyle: Record<string, string> | undefined,
|
|
274
|
+
newStyle: Record<string, string> | undefined,
|
|
275
|
+
): void => {
|
|
276
|
+
const style = (el as HTMLElement).style as unknown as Record<string, string>
|
|
277
|
+
const oldS = oldStyle || {}
|
|
278
|
+
const newS = newStyle || {}
|
|
279
|
+
for (const key of Object.keys(oldS)) {
|
|
280
|
+
if (!(key in newS)) {
|
|
281
|
+
style[key] = ''
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const [key, value] of Object.entries(newS)) {
|
|
285
|
+
if (oldS[key] !== value) {
|
|
286
|
+
style[key] = value
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Applies all props to a freshly created element (initial mount).
|
|
293
|
+
*/
|
|
294
|
+
const applyProps = (el: Element, props: Record<string, unknown> | null): void => {
|
|
295
|
+
if (!props) return
|
|
296
|
+
for (const [key, value] of Object.entries(props)) {
|
|
297
|
+
setProp(el, key, value)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Diffs old and new props and applies minimal updates to a live DOM element.
|
|
303
|
+
*/
|
|
304
|
+
export const patchProps = (
|
|
305
|
+
el: Element,
|
|
306
|
+
oldProps: Record<string, unknown> | null,
|
|
307
|
+
newProps: Record<string, unknown> | null,
|
|
308
|
+
): void => {
|
|
309
|
+
const oldP = oldProps || {}
|
|
310
|
+
const newP = newProps || {}
|
|
311
|
+
|
|
312
|
+
// Remove props that no longer exist
|
|
313
|
+
for (const key of Object.keys(oldP)) {
|
|
314
|
+
if (!(key in newP)) {
|
|
315
|
+
removeProp(el, key)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Add / update props
|
|
320
|
+
for (const [key, value] of Object.entries(newP)) {
|
|
321
|
+
if (key === 'style') {
|
|
322
|
+
patchStyle(el, oldP.style as Record<string, string> | undefined, value as Record<string, string> | undefined)
|
|
323
|
+
} else if (oldP[key] !== value) {
|
|
324
|
+
setProp(el, key, value)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Mount (VNode tree → real DOM)
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Creates real DOM nodes from a VChild and optionally appends to a parent.
|
|
335
|
+
* Sets `_el` on the VChild so subsequent patches can find the DOM node.
|
|
336
|
+
* @returns The created DOM node.
|
|
337
|
+
*/
|
|
338
|
+
export const mountChild = (child: VChild, parent: Node | null): Node => {
|
|
339
|
+
if (child._brand === VTEXT_BRAND) {
|
|
340
|
+
const text = document.createTextNode(child.text)
|
|
341
|
+
child._el = text
|
|
342
|
+
if (parent) parent.appendChild(text)
|
|
343
|
+
return text
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Pre-existing real DOM node (from shadeChildren)
|
|
347
|
+
if (child.type === EXISTING_NODE) {
|
|
348
|
+
if (parent && child._el) parent.appendChild(child._el)
|
|
349
|
+
return child._el as Node
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Shade component
|
|
353
|
+
if (typeof child.type === 'function') {
|
|
354
|
+
const factory = child.type as (props: unknown, children?: ChildrenList) => JSX.Element
|
|
355
|
+
const el = factory(child.props || {}, child.children as unknown as ChildrenList)
|
|
356
|
+
child._el = el
|
|
357
|
+
if (parent) parent.appendChild(el)
|
|
358
|
+
return el
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Intrinsic element
|
|
362
|
+
const tag = child.type as string
|
|
363
|
+
const el = isSvgTag(tag) ? document.createElementNS(SVG_NS, tag) : document.createElement(tag)
|
|
364
|
+
applyProps(el, child.props)
|
|
365
|
+
child._el = el
|
|
366
|
+
|
|
367
|
+
for (const c of child.children) {
|
|
368
|
+
mountChild(c, el)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (parent) parent.appendChild(el)
|
|
372
|
+
|
|
373
|
+
// Set ref after the element is fully created and appended
|
|
374
|
+
const ref = child.props?.ref as RefObject<Element> | undefined
|
|
375
|
+
if (ref) {
|
|
376
|
+
;(ref as { current: Element | null }).current = el
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return el
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Unmount (remove real DOM)
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Removes the DOM node associated with a VChild from its parent.
|
|
388
|
+
*/
|
|
389
|
+
export const unmountChild = (child: VChild): void => {
|
|
390
|
+
// Clear ref before removing from DOM
|
|
391
|
+
if (child._brand === VNODE_BRAND && child.props?.ref) {
|
|
392
|
+
;(child.props.ref as { current: Element | null }).current = null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const node = child._el
|
|
396
|
+
if (node?.parentNode) {
|
|
397
|
+
node.parentNode.removeChild(node)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Patch (diff old VNode tree vs new VNode tree → DOM updates)
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Patches a single old/new VChild pair. Updates the real DOM in place when
|
|
407
|
+
* possible, or replaces the DOM node when types differ.
|
|
408
|
+
*/
|
|
409
|
+
const patchChild = (_parentEl: Node, oldChild: VChild, newChild: VChild): void => {
|
|
410
|
+
// Both text nodes
|
|
411
|
+
if (oldChild._brand === VTEXT_BRAND && newChild._brand === VTEXT_BRAND) {
|
|
412
|
+
if (oldChild.text !== newChild.text && oldChild._el) {
|
|
413
|
+
oldChild._el.textContent = newChild.text
|
|
414
|
+
}
|
|
415
|
+
newChild._el = oldChild._el
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Both element/component VNodes with the same type
|
|
420
|
+
if (oldChild._brand === VNODE_BRAND && newChild._brand === VNODE_BRAND && oldChild.type === newChild.type) {
|
|
421
|
+
if (oldChild.type === EXISTING_NODE) {
|
|
422
|
+
// --- Pre-existing DOM node ---
|
|
423
|
+
newChild._el = newChild._el || oldChild._el
|
|
424
|
+
if (oldChild._el !== newChild._el && oldChild._el?.parentNode) {
|
|
425
|
+
oldChild._el.parentNode.replaceChild(newChild._el!, oldChild._el)
|
|
426
|
+
}
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (typeof oldChild.type === 'function') {
|
|
431
|
+
// --- Shade component boundary ---
|
|
432
|
+
const el = oldChild._el as JSX.Element
|
|
433
|
+
newChild._el = el
|
|
434
|
+
|
|
435
|
+
const propsChanged = !shallowEqual(oldChild.props, newChild.props)
|
|
436
|
+
// For children, reference check is enough -- if the parent re-rendered,
|
|
437
|
+
// the children VNodes are always fresh objects, so we compare lengths
|
|
438
|
+
// and item identity as a fast heuristic.
|
|
439
|
+
const childrenChanged =
|
|
440
|
+
oldChild.children.length !== newChild.children.length ||
|
|
441
|
+
oldChild.children.some((c, i) => c !== newChild.children[i])
|
|
442
|
+
|
|
443
|
+
if (propsChanged || childrenChanged) {
|
|
444
|
+
if (propsChanged) {
|
|
445
|
+
el.props = newChild.props
|
|
446
|
+
patchProps(el, oldChild.props, newChild.props)
|
|
447
|
+
}
|
|
448
|
+
el.shadeChildren = newChild.children as unknown as ChildrenList
|
|
449
|
+
el.updateComponent()
|
|
450
|
+
}
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// --- Intrinsic element ---
|
|
455
|
+
const el = oldChild._el as Element
|
|
456
|
+
newChild._el = el
|
|
457
|
+
patchProps(el, oldChild.props, newChild.props)
|
|
458
|
+
patchChildren(el, oldChild.children, newChild.children)
|
|
459
|
+
|
|
460
|
+
// Update refs: clear old ref if different, set new ref
|
|
461
|
+
const oldRef = oldChild.props?.ref as RefObject<Element> | undefined
|
|
462
|
+
const newRef = newChild.props?.ref as RefObject<Element> | undefined
|
|
463
|
+
if (oldRef !== newRef) {
|
|
464
|
+
if (oldRef) (oldRef as { current: Element | null }).current = null
|
|
465
|
+
if (newRef) (newRef as { current: Element | null }).current = el
|
|
466
|
+
}
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Types differ → replace
|
|
471
|
+
const oldNode = oldChild._el
|
|
472
|
+
if (oldNode && oldNode.parentNode) {
|
|
473
|
+
const newNode = mountChild(newChild, null)
|
|
474
|
+
oldNode.parentNode.replaceChild(newNode, oldNode)
|
|
475
|
+
} else {
|
|
476
|
+
mountChild(newChild, _parentEl)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Reconciles an array of old VChildren against new VChildren inside a parent
|
|
482
|
+
* DOM element. Patches matching pairs, removes excess old children, and
|
|
483
|
+
* mounts excess new children.
|
|
484
|
+
*
|
|
485
|
+
* **Note:** This uses positional (index-based) matching, not key-based
|
|
486
|
+
* reconciliation. Reordering list items will cause all children from the
|
|
487
|
+
* reorder point onward to be patched/replaced rather than moved. For
|
|
488
|
+
* dynamic lists where order changes frequently, wrap each item in its own
|
|
489
|
+
* Shade component so that the component boundary prevents unnecessary
|
|
490
|
+
* inner-DOM churn.
|
|
491
|
+
*/
|
|
492
|
+
export const patchChildren = (parentEl: Node, oldChildren: VChild[], newChildren: VChild[]): void => {
|
|
493
|
+
const commonLen = Math.min(oldChildren.length, newChildren.length)
|
|
494
|
+
|
|
495
|
+
for (let i = 0; i < commonLen; i++) {
|
|
496
|
+
patchChild(parentEl, oldChildren[i], newChildren[i])
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Remove excess old children (iterate backwards to avoid index issues)
|
|
500
|
+
for (let i = oldChildren.length - 1; i >= commonLen; i--) {
|
|
501
|
+
unmountChild(oldChildren[i])
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Mount excess new children
|
|
505
|
+
for (let i = commonLen; i < newChildren.length; i++) {
|
|
506
|
+
mountChild(newChildren[i], parentEl)
|
|
507
|
+
}
|
|
508
|
+
}
|