@pfern/elements 0.1.10 → 0.2.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 +11 -11
- package/README.md +54 -237
- package/elements.js +4 -1970
- package/env.d.ts +24 -0
- package/mathml.js +1 -0
- package/package.json +19 -23
- package/src/core/elements.js +513 -0
- package/src/core/events.js +143 -0
- package/src/core/props.js +177 -0
- package/src/core/tags.js +225 -0
- package/src/core/tick.js +116 -0
- package/src/core/types.js +696 -0
- package/src/helpers.js +73 -0
- package/src/html.js +996 -0
- package/src/mathml.js +340 -0
- package/src/router.js +51 -0
- package/src/ssr.js +175 -0
- package/src/svg.js +407 -0
- package/types/elements.d.ts +4 -1403
- package/types/mathml.d.ts +1 -0
- package/types/src/core/elements.d.ts +29 -0
- package/types/src/core/events.d.ts +15 -0
- package/types/src/core/props.d.ts +9 -0
- package/types/src/core/tags.d.ts +4 -0
- package/types/src/core/tick.d.ts +5 -0
- package/types/src/core/types.d.ts +507 -0
- package/types/src/helpers.d.ts +5 -0
- package/types/src/html.d.ts +802 -0
- package/types/src/mathml.d.ts +264 -0
- package/types/src/router.d.ts +4 -0
- package/types/src/ssr.d.ts +3 -0
- package/types/src/svg.d.ts +348 -0
package/env.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient type declarations for the build environment.
|
|
3
|
+
*
|
|
4
|
+
* This project is JS-first; these declarations exist solely to provide a
|
|
5
|
+
* type-rich editor experience and to keep `tsc --checkJs` strict.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare module '*?raw' {
|
|
9
|
+
const content: string
|
|
10
|
+
export default content
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ImportMetaEnv {
|
|
14
|
+
readonly DEV?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ImportMeta {
|
|
18
|
+
readonly env: ImportMetaEnv
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Window {
|
|
22
|
+
x3dom?: any
|
|
23
|
+
}
|
|
24
|
+
|
package/mathml.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src/mathml.js'
|
package/package.json
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pfern/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A minimalist, pure functional declarative UI toolkit.",
|
|
5
|
+
"author": "Paul Fernandez",
|
|
6
|
+
"license": "MIT",
|
|
5
7
|
"type": "module",
|
|
6
|
-
"main": "elements.js",
|
|
7
8
|
"types": "./types/elements.d.ts",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
11
|
"types": "./types/elements.d.ts",
|
|
11
12
|
"import": "./elements.js",
|
|
12
13
|
"default": "./elements.js"
|
|
14
|
+
},
|
|
15
|
+
"./mathml": {
|
|
16
|
+
"types": "./types/mathml.d.ts",
|
|
17
|
+
"import": "./mathml.js",
|
|
18
|
+
"default": "./mathml.js"
|
|
13
19
|
}
|
|
14
20
|
},
|
|
15
21
|
"files": [
|
|
16
22
|
"./elements.js",
|
|
23
|
+
"./mathml.js",
|
|
24
|
+
"./env.d.ts",
|
|
25
|
+
"./src",
|
|
17
26
|
"./types"
|
|
18
27
|
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"clean:types": "node scripts/clean-types.js",
|
|
30
|
+
"build:types": "node scripts/build-types.js",
|
|
31
|
+
"typecheck": "node scripts/typecheck.js",
|
|
32
|
+
"test": "node --test test/*.test.* --test-reporter spec",
|
|
33
|
+
"test:coverage": "node scripts/test-coverage.js"
|
|
34
|
+
},
|
|
19
35
|
"keywords": [
|
|
20
36
|
"ui",
|
|
21
37
|
"functional",
|
|
@@ -26,25 +42,5 @@
|
|
|
26
42
|
"stateless",
|
|
27
43
|
"recursive",
|
|
28
44
|
"html"
|
|
29
|
-
]
|
|
30
|
-
"scripts": {
|
|
31
|
-
"dev": "vite",
|
|
32
|
-
"build": "vite build",
|
|
33
|
-
"build:types": "tsc",
|
|
34
|
-
"preview": "vite preview",
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"test": "node --test test/*.test.* --test-reporter spec"
|
|
37
|
-
},
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/pfernandez/elements.git"
|
|
41
|
-
},
|
|
42
|
-
"author": "Paul Fernandez",
|
|
43
|
-
"license": "MIT",
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/node": "^25.0.10",
|
|
46
|
-
"eslint": "^7.32.0",
|
|
47
|
-
"typescript": "^5.9.3",
|
|
48
|
-
"vite": "^7.3.1"
|
|
49
|
-
}
|
|
45
|
+
]
|
|
50
46
|
}
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/** expressive/elements.js
|
|
2
|
+
*
|
|
3
|
+
* Minimalist declarative UI framework based on pure functional composition.
|
|
4
|
+
*
|
|
5
|
+
* Purpose:
|
|
6
|
+
* - All UI defined as pure functions that return declarative arrays.
|
|
7
|
+
* - Directly composable into a symbolic tree compatible with Lisp-like
|
|
8
|
+
* dialects.
|
|
9
|
+
* - No internal mutable state required: DOM itself is the substrate for state.
|
|
10
|
+
* - No JSX, no keys, no reconciler heuristics — just pure structure +
|
|
11
|
+
* replacement.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { assignProperties, removeMissingProps } from './props.js'
|
|
16
|
+
import { htmlTagNames, svgTagNames } from './tags.js'
|
|
17
|
+
|
|
18
|
+
export * from './types.js'
|
|
19
|
+
|
|
20
|
+
export const DEBUG =
|
|
21
|
+
typeof process !== 'undefined'
|
|
22
|
+
&& process.env
|
|
23
|
+
&& (process.env.ELEMENTSJS_DEBUG?.toLowerCase() === 'true'
|
|
24
|
+
|| process.env.NODE_ENV === 'development')
|
|
25
|
+
|
|
26
|
+
const svgNS = 'http://www.w3.org/2000/svg'
|
|
27
|
+
const mathNS = 'http://www.w3.org/1998/Math/MathML'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maps vnode instances to their current root DOM element,
|
|
31
|
+
* allowing accurate replacement when the same vnode is re-invoked.
|
|
32
|
+
*/
|
|
33
|
+
const rootMap = new WeakMap()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Maps component-returned vnode roots to their component instance object.
|
|
37
|
+
*
|
|
38
|
+
* This keeps the public vnode representation “pure” (no wrapper nodes) while
|
|
39
|
+
* still allowing:
|
|
40
|
+
* - event updates to replace the nearest component boundary, and
|
|
41
|
+
* - programmatic updates to patch the correct DOM subtree for an instance.
|
|
42
|
+
*/
|
|
43
|
+
const componentRoots = new WeakMap()
|
|
44
|
+
|
|
45
|
+
const getVNode = el => el?.__vnode
|
|
46
|
+
const setVNode = (el, vnode) => el.__vnode = vnode
|
|
47
|
+
const isRoot = el => !!el?.__root
|
|
48
|
+
const setRoot = el => el.__root = true
|
|
49
|
+
|
|
50
|
+
const isNodeEnv = () => typeof document === 'undefined'
|
|
51
|
+
|
|
52
|
+
let componentUpdateDepth = 0
|
|
53
|
+
let currentEventRoot = null
|
|
54
|
+
|
|
55
|
+
const getCurrentEventRoot = () => currentEventRoot
|
|
56
|
+
const setCurrentEventRoot = el => currentEventRoot = el
|
|
57
|
+
|
|
58
|
+
const isObject = x =>
|
|
59
|
+
typeof x === 'object'
|
|
60
|
+
&& x !== null
|
|
61
|
+
|
|
62
|
+
const shallowObjectEqual = (a, b) => {
|
|
63
|
+
if (a === b) return true
|
|
64
|
+
if (!isObject(a) || !isObject(b)) return false
|
|
65
|
+
const aKeys = Object.keys(a)
|
|
66
|
+
const bKeys = Object.keys(b)
|
|
67
|
+
if (aKeys.length !== bKeys.length) return false
|
|
68
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
69
|
+
const key = aKeys[i]
|
|
70
|
+
if (!(key in b) || a[key] !== b[key]) return false
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const propsEqual = (a, b) => {
|
|
76
|
+
if (a === b) return true
|
|
77
|
+
if (!isObject(a) || !isObject(b)) return false
|
|
78
|
+
const aKeys = Object.keys(a)
|
|
79
|
+
const bKeys = Object.keys(b)
|
|
80
|
+
if (aKeys.length !== bKeys.length) return false
|
|
81
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
82
|
+
const key = aKeys[i]
|
|
83
|
+
if (!(key in b)) return false
|
|
84
|
+
const av = a[key]
|
|
85
|
+
const bv = b[key]
|
|
86
|
+
if (key === 'style' && isObject(av) && isObject(bv)) {
|
|
87
|
+
if (!shallowObjectEqual(av, bv)) return false
|
|
88
|
+
} else if (av !== bv) return false
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Determines whether two nodes have changed enough to require replacement.
|
|
95
|
+
* Compares type, string value, or element tag.
|
|
96
|
+
*
|
|
97
|
+
* @param {*} a - Previous vnode
|
|
98
|
+
* @param {*} b - New vnode
|
|
99
|
+
* @returns {boolean} - True if nodes are meaningfully different
|
|
100
|
+
*/
|
|
101
|
+
const changed = (a, b) =>
|
|
102
|
+
typeof a !== typeof b
|
|
103
|
+
|| typeof a === 'string' && a !== b
|
|
104
|
+
|| typeof a === 'number' && a !== b
|
|
105
|
+
|| Array.isArray(a) && Array.isArray(b) && a[0] !== b[0]
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Computes a patch object describing how to transform tree `a` into tree `b`.
|
|
109
|
+
* Used by `render` to apply minimal updates to the DOM.
|
|
110
|
+
*
|
|
111
|
+
* @param {*} a - Previous vnode
|
|
112
|
+
* @param {*} b - New vnode
|
|
113
|
+
* @returns {Object} - Patch object with type and content
|
|
114
|
+
*/
|
|
115
|
+
const diffTree = (a, b) => {
|
|
116
|
+
if (a == null) return { type: 'CREATE', newNode: b }
|
|
117
|
+
if (b == null) return { type: 'REMOVE' }
|
|
118
|
+
if (changed(a, b)) return { type: 'REPLACE', newNode: b }
|
|
119
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return
|
|
120
|
+
|
|
121
|
+
const prevProps = a[1] || {}
|
|
122
|
+
const nextProps = b[1] || {}
|
|
123
|
+
const propsChanged = !propsEqual(prevProps, nextProps)
|
|
124
|
+
const children = diffChildren(a, b)
|
|
125
|
+
|
|
126
|
+
return !propsChanged && !children
|
|
127
|
+
? undefined
|
|
128
|
+
: {
|
|
129
|
+
type: 'UPDATE',
|
|
130
|
+
prevProps: propsChanged ? prevProps : null,
|
|
131
|
+
props: propsChanged ? nextProps : null,
|
|
132
|
+
children
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compares the children of two vnodes and returns patch list.
|
|
138
|
+
*
|
|
139
|
+
* @param {any[]} a - Previous vnode
|
|
140
|
+
* @param {any[]} b - New vnode
|
|
141
|
+
* @returns {Array<[number, Object]> | null} patches - Sparse patch list
|
|
142
|
+
*/
|
|
143
|
+
const diffChildren = (a, b) => {
|
|
144
|
+
const aLen = Math.max(0, a.length - 2)
|
|
145
|
+
const bLen = Math.max(0, b.length - 2)
|
|
146
|
+
const len = Math.max(aLen, bLen)
|
|
147
|
+
/** @type {Array<[number, Object]>} */
|
|
148
|
+
const patches = []
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < len; i++) {
|
|
151
|
+
const aIndex = i + 2
|
|
152
|
+
const bIndex = i + 2
|
|
153
|
+
const aHasChild = aIndex < a.length
|
|
154
|
+
const bHasChild = bIndex < b.length
|
|
155
|
+
|
|
156
|
+
if (!aHasChild && !bHasChild) continue
|
|
157
|
+
|
|
158
|
+
const prevChild = aHasChild ? a[aIndex] : undefined
|
|
159
|
+
const nextChild = bHasChild ? b[bIndex] : undefined
|
|
160
|
+
|
|
161
|
+
// If a child position is explicitly present (even as null/undefined),
|
|
162
|
+
// renderTree() will create a placeholder comment node. Treat transitions
|
|
163
|
+
// between empty and non-empty as REPLACE so indices stay aligned.
|
|
164
|
+
let patch
|
|
165
|
+
if (!aHasChild && bHasChild) {
|
|
166
|
+
patch = { type: 'CREATE', newNode: nextChild }
|
|
167
|
+
} else if (aHasChild && !bHasChild) {
|
|
168
|
+
patch = { type: 'REMOVE' }
|
|
169
|
+
} else if (prevChild == null && nextChild == null) {
|
|
170
|
+
patch = undefined
|
|
171
|
+
} else if (prevChild == null || nextChild == null) {
|
|
172
|
+
patch = { type: 'REPLACE', newNode: nextChild }
|
|
173
|
+
} else {
|
|
174
|
+
patch = diffTree(prevChild, nextChild)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
patch && patches.push([i, /** @type {Object} */ patch])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return patches.length ? patches : null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
const propsEnv = {
|
|
185
|
+
svgNS,
|
|
186
|
+
debug: DEBUG,
|
|
187
|
+
isRoot,
|
|
188
|
+
renderTree: null,
|
|
189
|
+
getCurrentEventRoot,
|
|
190
|
+
setCurrentEventRoot
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Recursively builds a real DOM tree from a declarative vnode.
|
|
195
|
+
* Marks root nodes and tracks state/element associations.
|
|
196
|
+
*
|
|
197
|
+
* @param {*} node - Vnode to render
|
|
198
|
+
* @param {boolean} isRoot - Whether this is a root component
|
|
199
|
+
* @returns {any} - Real DOM node
|
|
200
|
+
*/
|
|
201
|
+
const renderTree = (node, isRoot = true, namespaceURI = null) => {
|
|
202
|
+
const type = typeof node
|
|
203
|
+
|
|
204
|
+
if (type === 'string' || type === 'number')
|
|
205
|
+
return isNodeEnv() ? node : document.createTextNode(String(node))
|
|
206
|
+
|
|
207
|
+
if (!node || node.length === 0) return document.createComment('Empty vnode')
|
|
208
|
+
|
|
209
|
+
if (!Array.isArray(node)) {
|
|
210
|
+
console.error('Malformed vnode (not an array):', node)
|
|
211
|
+
return document.createComment('Invalid vnode')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const componentInstance = componentRoots.get(node)
|
|
215
|
+
componentInstance && (isRoot = true)
|
|
216
|
+
|
|
217
|
+
const tag = node[0]
|
|
218
|
+
|
|
219
|
+
if (typeof tag !== 'string') {
|
|
220
|
+
console.error('Malformed vnode (non-string tag):', node)
|
|
221
|
+
return document.createComment('Invalid vnode')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const props = node[1] || {}
|
|
225
|
+
const isSpecialRootTag = tag === 'html' || tag === 'head' || tag === 'body'
|
|
226
|
+
|
|
227
|
+
const elNamespaceURI =
|
|
228
|
+
tag === 'svg' || namespaceURI === svgNS
|
|
229
|
+
? svgNS
|
|
230
|
+
: tag === 'math' || namespaceURI === mathNS
|
|
231
|
+
? mathNS
|
|
232
|
+
: null
|
|
233
|
+
|
|
234
|
+
const isAnnotationXmlHtml =
|
|
235
|
+
tag === 'annotation-xml'
|
|
236
|
+
&& typeof props?.encoding === 'string'
|
|
237
|
+
&& /(^|\/)(xhtml\+xml|xhtml|html)(;|$)/i.test(props.encoding)
|
|
238
|
+
|
|
239
|
+
const childNamespaceURI =
|
|
240
|
+
tag === 'foreignObject'
|
|
241
|
+
? null
|
|
242
|
+
: tag === 'annotation-xml'
|
|
243
|
+
? isAnnotationXmlHtml ? null : namespaceURI
|
|
244
|
+
: tag === 'svg'
|
|
245
|
+
? svgNS
|
|
246
|
+
: tag === 'math'
|
|
247
|
+
? mathNS
|
|
248
|
+
: namespaceURI
|
|
249
|
+
|
|
250
|
+
let el = null
|
|
251
|
+
if (tag === 'html') el = document.documentElement
|
|
252
|
+
else if (tag === 'head') el = document.head
|
|
253
|
+
else if (tag === 'body') el = document.body
|
|
254
|
+
else el = elNamespaceURI
|
|
255
|
+
? document.createElementNS(elNamespaceURI, tag)
|
|
256
|
+
: document.createElement(tag)
|
|
257
|
+
|
|
258
|
+
if (!el && (tag === 'head' || tag === 'body')) {
|
|
259
|
+
el = document.createElement(tag)
|
|
260
|
+
document.documentElement.appendChild(el)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setVNode(el, node)
|
|
264
|
+
|
|
265
|
+
if (isRoot && !isSpecialRootTag) {
|
|
266
|
+
setRoot(el)
|
|
267
|
+
rootMap.set(node, el)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
componentInstance && rootMap.set(componentInstance, el)
|
|
271
|
+
|
|
272
|
+
assignProperties(el, props, propsEnv)
|
|
273
|
+
|
|
274
|
+
for (let i = 2; i < node.length; i++) {
|
|
275
|
+
el.appendChild(renderTree(node[i], false, childNamespaceURI))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return el
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
propsEnv.renderTree = renderTree
|
|
282
|
+
|
|
283
|
+
const applyPropsUpdate = (el, prevProps, nextProps) =>
|
|
284
|
+
nextProps == null
|
|
285
|
+
? undefined
|
|
286
|
+
: (removeMissingProps(el, prevProps || {}, nextProps),
|
|
287
|
+
assignProperties(el, nextProps, propsEnv))
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Applies a patch object to a DOM subtree.
|
|
291
|
+
* Handles creation, removal, replacement, and child updates.
|
|
292
|
+
*
|
|
293
|
+
* @param {any} parent - DOM node to mutate
|
|
294
|
+
* @param {Object} patch - Patch object from diffTree
|
|
295
|
+
* @param {number} [index=0] - Child index to apply update to
|
|
296
|
+
*/
|
|
297
|
+
const applyPatch = (parent, patch, index = 0, isRootBoundary = false) => {
|
|
298
|
+
if (!patch) return
|
|
299
|
+
const child = parent.childNodes[index]
|
|
300
|
+
|
|
301
|
+
switch (patch.type) {
|
|
302
|
+
case 'CREATE': {
|
|
303
|
+
const newEl = renderTree(patch.newNode, isRootBoundary)
|
|
304
|
+
child ? parent.insertBefore(newEl, child) : parent.appendChild(newEl)
|
|
305
|
+
break
|
|
306
|
+
}
|
|
307
|
+
case 'REMOVE':
|
|
308
|
+
if (child) parent.removeChild(child)
|
|
309
|
+
break
|
|
310
|
+
case 'REPLACE': {
|
|
311
|
+
const newEl = renderTree(patch.newNode, isRootBoundary)
|
|
312
|
+
parent.replaceChild(newEl, child)
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
case 'UPDATE':
|
|
316
|
+
if (child) {
|
|
317
|
+
applyPropsUpdate(child, patch.prevProps, patch.props)
|
|
318
|
+
if (patch.children) {
|
|
319
|
+
for (let i = patch.children.length - 1; i >= 0; i--) {
|
|
320
|
+
const [childIndex, childPatch] = patch.children[i]
|
|
321
|
+
applyPatch(child, childPatch, childIndex, false)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
break
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
const clearChildren = el => {
|
|
331
|
+
if (!el) return
|
|
332
|
+
|
|
333
|
+
// Prefer the standard DOM API.
|
|
334
|
+
if (typeof el.firstChild !== 'undefined') {
|
|
335
|
+
while (el.firstChild) el.removeChild(el.firstChild)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Fake DOM fallback.
|
|
340
|
+
while (el.childNodes?.length) el.removeChild(el.childNodes[0])
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Render a vnode into the DOM.
|
|
345
|
+
*
|
|
346
|
+
* If `vtree[0]` is `html`, `head`, or `body`, no container is required.
|
|
347
|
+
*
|
|
348
|
+
* @param {import('./types.js').ElementsVNode} vtree
|
|
349
|
+
* @param {HTMLElement | null} [container]
|
|
350
|
+
*/
|
|
351
|
+
export const render = (vtree, container = null, { replace = false } = {}) => {
|
|
352
|
+
const target =
|
|
353
|
+
!container && Array.isArray(vtree) && vtree[0] === 'html'
|
|
354
|
+
? document.documentElement
|
|
355
|
+
: container
|
|
356
|
+
|
|
357
|
+
if (!target) {
|
|
358
|
+
throw new Error('render() requires a container for non-html() root')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const prevVNode = replace ? null : getVNode(target)
|
|
362
|
+
|
|
363
|
+
if (!prevVNode) {
|
|
364
|
+
replace && clearChildren(target)
|
|
365
|
+
const dom = renderTree(vtree)
|
|
366
|
+
if (target === document.documentElement) {
|
|
367
|
+
if (dom !== document.documentElement) {
|
|
368
|
+
document.replaceChild(dom, document.documentElement)
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
target.appendChild(dom)
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const patch = diffTree(prevVNode, vtree)
|
|
375
|
+
applyPatch(target, patch, 0, true)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
setVNode(target, vtree)
|
|
379
|
+
rootMap.set(vtree, target)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
const getChildIndex = (parent, child) => {
|
|
384
|
+
const nodes = parent?.childNodes
|
|
385
|
+
if (!nodes) return -1
|
|
386
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
387
|
+
if (nodes[i] === child) return i
|
|
388
|
+
}
|
|
389
|
+
return -1
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Wrap a pure function component so it participates in reconciliation.
|
|
394
|
+
*
|
|
395
|
+
* @template {any[]} Args
|
|
396
|
+
* @param {(...args: Args) => import('./types.js').ElementsVNode} fn
|
|
397
|
+
* @returns {(...args: Args) => import('./types.js').ElementsVNode}
|
|
398
|
+
*/
|
|
399
|
+
export const component = fn => {
|
|
400
|
+
const instance = {}
|
|
401
|
+
return (...args) => {
|
|
402
|
+
try {
|
|
403
|
+
const prevEl = rootMap.get(instance)
|
|
404
|
+
const canUpdateInPlace =
|
|
405
|
+
!!prevEl?.parentNode
|
|
406
|
+
&& componentUpdateDepth === 0
|
|
407
|
+
&& !currentEventRoot
|
|
408
|
+
|
|
409
|
+
componentUpdateDepth++
|
|
410
|
+
let vnode
|
|
411
|
+
try {
|
|
412
|
+
vnode = fn(...args)
|
|
413
|
+
} finally {
|
|
414
|
+
componentUpdateDepth--
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Record the root vnode for this component instance so the renderer can
|
|
418
|
+
// treat it as a boundary without introducing wrapper nodes into the
|
|
419
|
+
// public AST.
|
|
420
|
+
Array.isArray(vnode) && componentRoots.set(vnode, instance)
|
|
421
|
+
|
|
422
|
+
if (canUpdateInPlace) {
|
|
423
|
+
const prevVNode = getVNode(prevEl)
|
|
424
|
+
const patch = diffTree(prevVNode, vnode)
|
|
425
|
+
|
|
426
|
+
if (!patch) {
|
|
427
|
+
setVNode(prevEl, vnode)
|
|
428
|
+
return vnode
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (patch.type === 'REPLACE') {
|
|
432
|
+
const replacement = renderTree(vnode, true)
|
|
433
|
+
prevEl.parentNode.replaceChild(replacement, prevEl)
|
|
434
|
+
return getVNode(replacement)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const parent = prevEl.parentNode
|
|
438
|
+
const index = getChildIndex(parent, prevEl)
|
|
439
|
+
if (index !== -1) applyPatch(parent, patch, index)
|
|
440
|
+
|
|
441
|
+
if (patch.type === 'REMOVE') {
|
|
442
|
+
rootMap.delete(instance)
|
|
443
|
+
return vnode
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const nextEl = index === -1 ? prevEl : parent.childNodes[index] || prevEl
|
|
447
|
+
setVNode(nextEl, vnode)
|
|
448
|
+
rootMap.set(instance, nextEl)
|
|
449
|
+
return vnode
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return vnode
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.error('Component error:', err)
|
|
455
|
+
return ['div', {}, `Error: ${err.message}`]
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const tagNames = [...htmlTagNames, ...svgTagNames]
|
|
461
|
+
const isPropsObject = x =>
|
|
462
|
+
typeof x === 'object'
|
|
463
|
+
&& x !== null
|
|
464
|
+
&& !Array.isArray(x)
|
|
465
|
+
&& !(typeof Node !== 'undefined' && x instanceof Node)
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* @param {string} tag
|
|
469
|
+
* @returns {import('./types.js').ElementsElementHelper<any>}
|
|
470
|
+
*/
|
|
471
|
+
const createElementHelper = tag => (...args) => {
|
|
472
|
+
const hasFirstArg = args.length > 0
|
|
473
|
+
const [propsOrChild, ...children] = args
|
|
474
|
+
const props = hasFirstArg && isPropsObject(propsOrChild) ? propsOrChild : {}
|
|
475
|
+
const actualChildren = !hasFirstArg
|
|
476
|
+
? []
|
|
477
|
+
: props === propsOrChild
|
|
478
|
+
? children
|
|
479
|
+
: [propsOrChild, ...children]
|
|
480
|
+
return /** @type {import('./types.js').ElementsVNode} */ (
|
|
481
|
+
[tag, props, ...actualChildren]
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* A map of supported HTML and SVG element helpers.
|
|
487
|
+
*
|
|
488
|
+
* Each helper is a function that accepts optional props as first argument
|
|
489
|
+
* and children as subsequent arguments.
|
|
490
|
+
*
|
|
491
|
+
* Example:
|
|
492
|
+
*
|
|
493
|
+
* ```js
|
|
494
|
+
* div({ id: 'foo' }, 'Hello World')
|
|
495
|
+
* ```
|
|
496
|
+
*
|
|
497
|
+
* Produces:
|
|
498
|
+
*
|
|
499
|
+
* ```js
|
|
500
|
+
* ['div', { id: 'foo' }, 'Hello World']
|
|
501
|
+
* ```
|
|
502
|
+
*
|
|
503
|
+
* The following helpers are included:
|
|
504
|
+
* `div`, `span`, `button`, `svg`, `circle`, etc.
|
|
505
|
+
*/
|
|
506
|
+
/** @type {import('./types.js').ElementsElementMap} */
|
|
507
|
+
export const elements = (() => {
|
|
508
|
+
/** @type {Record<string, import('./types.js').ElementsElementHelper<any>>} */
|
|
509
|
+
const acc = {}
|
|
510
|
+
acc.fragment = createElementHelper('fragment')
|
|
511
|
+
for (const tag of tagNames) acc[tag] = createElementHelper(tag)
|
|
512
|
+
return /** @type {import('./types.js').ElementsElementMap} */ (acc)
|
|
513
|
+
})()
|