@kudzujs/core 0.4.3 → 0.4.6
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/README.md +34 -18
- package/framework/build.mjs +12 -34
- package/framework/core.mjs +48 -12
- package/framework/list-runtime.js +183 -35
- package/framework/native-runtime.js +32 -40
- package/framework/shared-runtime.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ const [items, setItems] = useState([
|
|
|
166
166
|
)}</ul>
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
Kudzu emits initial items as static HTML, then adds, removes, updates, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Item-local handlers use
|
|
169
|
+
Kudzu emits initial items as static HTML, then adds, removes, updates, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
170
170
|
|
|
171
171
|
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a direct local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested conditions or lists, item spreads, component tags, fragments, and reactive `style`, `ref`, or `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
172
172
|
|
|
@@ -190,7 +190,7 @@ async function load() {
|
|
|
190
190
|
|
|
191
191
|
Primitive values, arrays, plain objects, and destructured props can be captured by client handlers. Functions, symbols, bigints, cycles, class instances, and imported helper functions are not yet supported as captures.
|
|
192
192
|
|
|
193
|
-
Native handlers
|
|
193
|
+
Native handlers use direct DOM listeners with normal `currentTarget`, bubbling, default-action, and propagation semantics. Handler modules load before listener registration, so `preventDefault`, `stopPropagation`, and `stopImmediatePropagation` work synchronously as expected.
|
|
194
194
|
|
|
195
195
|
## Rendering
|
|
196
196
|
|
|
@@ -249,13 +249,13 @@ Same counter with initial value `7` and increment/decrement buttons:
|
|
|
249
249
|
|
|
250
250
|
| Framework | Initial content | Initial JS gzip | Total output | Clean build |
|
|
251
251
|
|---|---:|---:|---:|---:|
|
|
252
|
-
| Kudzu | Yes | 487 B | 1.4 KB | **
|
|
253
|
-
| Astro | Yes | **158 B** | **365 B** |
|
|
254
|
-
| Svelte CSR | No | 10.5 KB | 26.9 KB |
|
|
255
|
-
| Qwik CSR | No | 20.6 KB | 57.8 KB |
|
|
256
|
-
| Vue CSR | No | 24.0 KB | 60.3 KB |
|
|
257
|
-
| React CSR | No | 59.2 KB | 189.0 KB |
|
|
258
|
-
| Next.js | Yes | 182.1 KB | 652.2 KB |
|
|
252
|
+
| Kudzu | Yes | 487 B | 1.4 KB | **363 ms** |
|
|
253
|
+
| Astro | Yes | **158 B** | **365 B** | 851 ms |
|
|
254
|
+
| Svelte CSR | No | 10.5 KB | 26.9 KB | 827 ms |
|
|
255
|
+
| Qwik CSR | No | 20.6 KB | 57.8 KB | 598 ms |
|
|
256
|
+
| Vue CSR | No | 24.0 KB | 60.3 KB | 762 ms |
|
|
257
|
+
| React CSR | No | 59.2 KB | 189.0 KB | 1018 ms |
|
|
258
|
+
| Next.js | Yes | 182.1 KB | 652.2 KB | 2989 ms |
|
|
259
259
|
|
|
260
260
|
Astro produces the smallest hand-authored counter. Kudzu's advantage in this fixture is React-shaped state code with a sub-1 KB runtime, not the smallest possible JavaScript.
|
|
261
261
|
|
|
@@ -265,15 +265,31 @@ Same content and CSS across every fixture:
|
|
|
265
265
|
|
|
266
266
|
| Framework | Initial content | Initial JS gzip | Total output | Clean build |
|
|
267
267
|
|---|---:|---:|---:|---:|
|
|
268
|
-
| Kudzu | Yes | **0 B** | 3.2 KB | **
|
|
269
|
-
| Astro | Yes | **0 B** | **3.0 KB** |
|
|
270
|
-
| Svelte CSR | No | 10.2 KB | 27.2 KB |
|
|
271
|
-
| Qwik CSR | No | 20.2 KB | 59.6 KB |
|
|
272
|
-
| Vue CSR | No | 24.2 KB | 62.3 KB |
|
|
273
|
-
| React CSR | No | 59.8 KB | 192.3 KB |
|
|
274
|
-
| Next.js | Yes | 182.6 KB | 663.6 KB |
|
|
275
|
-
|
|
276
|
-
|
|
268
|
+
| Kudzu | Yes | **0 B** | 3.2 KB | **380 ms** |
|
|
269
|
+
| Astro | Yes | **0 B** | **3.0 KB** | 965 ms |
|
|
270
|
+
| Svelte CSR | No | 10.2 KB | 27.2 KB | 824 ms |
|
|
271
|
+
| Qwik CSR | No | 20.2 KB | 59.6 KB | 594 ms |
|
|
272
|
+
| Vue CSR | No | 24.2 KB | 62.3 KB | 755 ms |
|
|
273
|
+
| React CSR | No | 59.8 KB | 192.3 KB | 1041 ms |
|
|
274
|
+
| Next.js | Yes | 182.6 KB | 663.6 KB | 2975 ms |
|
|
275
|
+
|
|
276
|
+
### 1,000-item Keyed List
|
|
277
|
+
|
|
278
|
+
The list starts with 1,000 keyed items, then updates every label, reverses the order, removes odd IDs, and adds 500 items. Browser timings are medians from seven fresh headless Chrome runs.
|
|
279
|
+
|
|
280
|
+
| Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
|
|
281
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
282
|
+
| Astro | Yes | **324 B** | **43.6 KB** | 852 ms | **3.5 ms** | 25.8 ms | 15.8 ms | **17.9 ms** | **63.0 ms** |
|
|
283
|
+
| Kudzu | Yes | 6.3 KB | 69.3 KB | **406 ms** | 6.0 ms | 28.5 ms | 14.7 ms | 19.1 ms | **68.3 ms** |
|
|
284
|
+
| Next.js | Yes | 182.2 KB | 695.2 KB | 2950 ms | 6.8 ms | 34.1 ms | **9.0 ms** | 19.3 ms | 69.2 ms |
|
|
285
|
+
| React CSR | No | 59.3 KB | 189.4 KB | 1028 ms | 9.5 ms | 35.3 ms | 10.1 ms | **17.8 ms** | 72.7 ms |
|
|
286
|
+
| Vue CSR | No | 24.3 KB | 61.3 KB | 759 ms | 10.2 ms | 33.9 ms | 9.3 ms | 19.7 ms | 73.1 ms |
|
|
287
|
+
| Qwik CSR | No | 22.2 KB | 64.1 KB | 604 ms | 15.6 ms | **20.0 ms** | 30.4 ms | 18.1 ms | 84.1 ms |
|
|
288
|
+
| Svelte CSR | No | 12.9 KB | 33.1 KB | 845 ms | 5.4 ms | 64.4 ms | 11.2 ms | 19.1 ms | 100.1 ms |
|
|
289
|
+
|
|
290
|
+
Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 68.3 ms, ahead of the React fixture's 72.7 ms and within 5.3 ms of Astro across all four operations.
|
|
291
|
+
|
|
292
|
+
Benchmark snapshot collected on July 22, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
|
|
277
293
|
|
|
278
294
|
## Development
|
|
279
295
|
|
package/framework/build.mjs
CHANGED
|
@@ -64,12 +64,13 @@ export async function build({ quiet = false } = {}) {
|
|
|
64
64
|
await mkdir(assetsDirectory, { recursive: true })
|
|
65
65
|
const commandEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.commands).map(event => event.event)))].sort()
|
|
66
66
|
const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
|
|
67
|
+
const nativeModules = handlerModules.filter(module => module.hasNativeHandlers).map(module => `/assets/${module.path}`)
|
|
68
|
+
const hasNativeHandlers = nativeModules.length > 0
|
|
67
69
|
if (behaviorCount) {
|
|
68
|
-
const runtimeFile = bindingCount || listCount ? "./shared-runtime.js" : "./runtime.js"
|
|
70
|
+
const runtimeFile = bindingCount || listCount || hasNativeHandlers ? "./shared-runtime.js" : "./runtime.js"
|
|
69
71
|
const runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, stateSeedCount > 0)
|
|
70
72
|
await writeFile(join(assetsDirectory, "kudzu.js"), runtime)
|
|
71
73
|
}
|
|
72
|
-
const hasNativeHandlers = handlerModules.some(module => module.hasNativeHandlers)
|
|
73
74
|
if (bindingCount || hasNativeHandlers) await cp(new URL("./serialization.js", import.meta.url), join(assetsDirectory, "kudzu-serialization.js"))
|
|
74
75
|
if (bindingCount) {
|
|
75
76
|
const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
|
|
@@ -84,9 +85,9 @@ export async function build({ quiet = false } = {}) {
|
|
|
84
85
|
}
|
|
85
86
|
if (hasNativeHandlers) {
|
|
86
87
|
const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
|
|
87
|
-
.replace('"./runtime.js"', '"./kudzu.js"')
|
|
88
|
+
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
88
89
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
89
|
-
await writeFile(join(assetsDirectory, "kudzu-native.js"),
|
|
90
|
+
await writeFile(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules))
|
|
90
91
|
}
|
|
91
92
|
for (const handlerModule of handlerModules) {
|
|
92
93
|
const output = join(assetsDirectory, handlerModule.path)
|
|
@@ -104,12 +105,18 @@ function specializeEvents(source, events) {
|
|
|
104
105
|
return source.replace(/const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`)
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function specializeNativeRuntime(source, events, modules) {
|
|
109
|
+
const imports = modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
|
|
110
|
+
const entries = modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
|
|
111
|
+
return `${imports}\n${specializeEvents(source, events).replace(/const modules = new Map\(\[[^\n]*\]\)/, `const modules = new Map([${entries}])`)}`
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
function specializeRuntime(source, events, hasStateSeed) {
|
|
108
115
|
const specialized = specializeEvents(source, events)
|
|
109
116
|
if (hasStateSeed) return specialized
|
|
110
117
|
return specialized
|
|
111
118
|
.replace(" const initialState = document.body.dataset.kState\n", "")
|
|
112
|
-
.replace(" if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)\n", "")
|
|
119
|
+
.replace(" if (initialState) for (const [id, value, compact] of JSON.parse(initialState)) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)\n", "")
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
export function parseDevPort(value) {
|
|
@@ -661,7 +668,6 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
|
|
|
661
668
|
if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
|
|
662
669
|
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
|
|
663
670
|
|
|
664
|
-
rejectNativeEventControls(expression)
|
|
665
671
|
const optimized = compileOptimizedEvent(expression, setters, factory)
|
|
666
672
|
if (optimized) return optimized
|
|
667
673
|
|
|
@@ -684,34 +690,6 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
|
|
|
684
690
|
])
|
|
685
691
|
}
|
|
686
692
|
|
|
687
|
-
function rejectNativeEventControls(expression) {
|
|
688
|
-
const controls = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"])
|
|
689
|
-
const found = new Set()
|
|
690
|
-
const eventAliases = new Set()
|
|
691
|
-
const parameter = expression.parameters[0]?.name
|
|
692
|
-
if (parameter && ts.isIdentifier(parameter)) eventAliases.add(parameter.text)
|
|
693
|
-
const visit = node => {
|
|
694
|
-
if (ts.isIdentifier(node) && controls.has(node.text)) found.add(node.text)
|
|
695
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isIdentifier(unwrapEventAlias(node.initializer)) && eventAliases.has(unwrapEventAlias(node.initializer).text)) {
|
|
696
|
-
eventAliases.add(node.name.text)
|
|
697
|
-
}
|
|
698
|
-
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left) && ts.isIdentifier(unwrapEventAlias(node.right)) && eventAliases.has(unwrapEventAlias(node.right).text)) eventAliases.add(node.left.text)
|
|
699
|
-
if (ts.isElementAccessExpression(node) && ts.isIdentifier(unwrapEventAlias(node.expression)) && eventAliases.has(unwrapEventAlias(node.expression).text)) {
|
|
700
|
-
if (ts.isStringLiteral(node.argumentExpression) && controls.has(node.argumentExpression.text)) found.add(node.argumentExpression.text)
|
|
701
|
-
else if (!ts.isStringLiteral(node.argumentExpression)) for (const control of controls) found.add(control)
|
|
702
|
-
}
|
|
703
|
-
ts.forEachChild(node, visit)
|
|
704
|
-
}
|
|
705
|
-
for (const parameter of expression.parameters) visit(parameter)
|
|
706
|
-
visit(expression.body)
|
|
707
|
-
if (found.size) throw new Error(`Delegated native handlers do not support event control methods: ${[...found].sort().join(", ")}`)
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function unwrapEventAlias(node) {
|
|
711
|
-
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) return unwrapEventAlias(node.expression)
|
|
712
|
-
return node
|
|
713
|
-
}
|
|
714
|
-
|
|
715
693
|
function nativeStateNames(expression, setters) {
|
|
716
694
|
return referencedStateNames(expression.body, setters, expression)
|
|
717
695
|
}
|
package/framework/core.mjs
CHANGED
|
@@ -187,7 +187,7 @@ function serializeCapture(name, value, seen) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
export async function renderPage(component, metadata = {}) {
|
|
190
|
-
renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
|
|
190
|
+
renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listFields: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
|
|
191
191
|
|
|
192
192
|
try {
|
|
193
193
|
const body = await renderNode({ type: component, props: {} })
|
|
@@ -208,8 +208,13 @@ export async function renderPage(component, metadata = {}) {
|
|
|
208
208
|
const listRuntime = renderContext.hasLists
|
|
209
209
|
? '<script type="module" src="/assets/kudzu-list.js"></script>'
|
|
210
210
|
: ""
|
|
211
|
+
const listStates = new Set(renderContext.lists.map(list => list.state))
|
|
212
|
+
const seededListStates = new Set(renderContext.lists.filter(list => list.seed && !renderContext.textStates.has(list.state) && !renderContext.conditionStates.has(list.state)).map(list => list.state))
|
|
211
213
|
const initialState = renderContext.hasBehaviors
|
|
212
|
-
? Object.entries(renderContext.states).filter(([id]) => !renderContext.textStates.has(id) || renderContext.conditionStates.has(id)).map(([id, entry]) =>
|
|
214
|
+
? Object.entries(renderContext.states).filter(([id]) => (!renderContext.textStates.has(id) || renderContext.conditionStates.has(id)) && !seededListStates.has(id)).map(([id, entry]) => {
|
|
215
|
+
const compact = listStates.has(id) && compactListState(entry.initialValue)
|
|
216
|
+
return compact ? [id, compact, 1] : [id, entry.initialValue]
|
|
217
|
+
})
|
|
213
218
|
: []
|
|
214
219
|
const state = initialState.length
|
|
215
220
|
? ` data-k-state='${escapeJsonAttribute(initialState)}'`
|
|
@@ -305,11 +310,14 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
305
310
|
}
|
|
306
311
|
if (node?.[listMarker]) return renderList(node, namespace, selectValue)
|
|
307
312
|
if (node?.[listFieldMarker]) {
|
|
308
|
-
|
|
313
|
+
if (renderContext.listTemplate) renderContext.listFields?.add(node.field)
|
|
314
|
+
const marker = renderContext.listTemplate ? ` data-k-list-text="${escapeAttribute(node.field)}"` : ""
|
|
315
|
+
return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
|
|
309
316
|
}
|
|
310
317
|
if (node?.[listExpressionMarker]) {
|
|
311
318
|
const descriptor = { module: node.module, handler: node.handler }
|
|
312
|
-
|
|
319
|
+
const marker = renderContext.listTemplate ? ` data-k-list-expression='${escapeJsonAttribute(descriptor)}'` : ""
|
|
320
|
+
return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
|
|
313
321
|
}
|
|
314
322
|
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
315
323
|
throw new Error(`Cannot render ${String(node)}`)
|
|
@@ -320,6 +328,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
320
328
|
|
|
321
329
|
const tag = node.type
|
|
322
330
|
const props = node.props ?? {}
|
|
331
|
+
const directListText = props.children?.[listFieldMarker] ? props.children : undefined
|
|
323
332
|
const childSelectValue = tag === "select"
|
|
324
333
|
? Object.hasOwn(props, "value") ? bindingValue(props.value) : noSelectValue
|
|
325
334
|
: selectValue
|
|
@@ -335,9 +344,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
335
344
|
if (renderContext.listRoot) {
|
|
336
345
|
const root = renderContext.listRoot
|
|
337
346
|
renderContext.listRoot = undefined
|
|
338
|
-
attributes += root.
|
|
339
|
-
? ` data-k-list-root="${root.id}"`
|
|
340
|
-
: ` data-k-list-item='${escapeJsonAttribute([root.id, root.key])}'`
|
|
347
|
+
if (root.template) attributes += ` data-k-list-root="${root.id}"`
|
|
341
348
|
}
|
|
342
349
|
|
|
343
350
|
for (const [rawName, value] of Object.entries(props)) {
|
|
@@ -411,26 +418,36 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
411
418
|
}
|
|
412
419
|
|
|
413
420
|
if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
|
|
414
|
-
if (listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
|
|
415
|
-
if (listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
|
|
416
|
-
if (listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
|
|
421
|
+
if (renderContext.listTemplate && listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
|
|
422
|
+
if (renderContext.listTemplate && listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
|
|
423
|
+
if (renderContext.listTemplate && listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
|
|
424
|
+
if (renderContext.listTemplate && directListText) {
|
|
425
|
+
renderContext.listFields?.add(directListText.field)
|
|
426
|
+
attributes += ` data-k-list-text="${escapeAttribute(directListText.field)}"`
|
|
427
|
+
}
|
|
417
428
|
|
|
418
429
|
if (tag === "option" && selectValue !== noSelectValue && String(optionValue(props)) === (selectValue == null ? "" : String(selectValue))) attributes += " selected"
|
|
419
430
|
|
|
420
431
|
const voidElements = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"])
|
|
421
432
|
if (voidElements.has(tag)) return `<${tag}${attributes}>`
|
|
422
|
-
|
|
433
|
+
const children = directListText ? escapeHtml(directListText.value ?? "") : await renderNode(props.children, childNamespace, childSelectValue)
|
|
434
|
+
return `<${tag}${attributes}>${children}</${tag}>`
|
|
423
435
|
}
|
|
424
436
|
|
|
425
437
|
async function renderList(node, namespace, selectValue) {
|
|
426
438
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
427
439
|
const id = `l${renderContext.nextList++}`
|
|
428
|
-
const descriptor = { id, state: node.items.id, key: node.keyField }
|
|
440
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]) }
|
|
429
441
|
renderContext.listDepth++
|
|
442
|
+
const previousListFields = renderContext.listFields
|
|
430
443
|
try {
|
|
431
444
|
renderContext.listTemplate = true
|
|
445
|
+
renderContext.listFields = new Set([node.keyField])
|
|
432
446
|
renderContext.listRoot = { id, template: true }
|
|
433
447
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
448
|
+
if (template.includes("data-k-native-")) descriptor.mount = true
|
|
449
|
+
const seed = listSeed(node.items.value, renderContext.listFields)
|
|
450
|
+
if (seed) descriptor.seed = seed
|
|
434
451
|
let current = ""
|
|
435
452
|
renderContext.listTemplate = false
|
|
436
453
|
for (const item of node.items.value) {
|
|
@@ -444,6 +461,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
444
461
|
} finally {
|
|
445
462
|
renderContext.listRoot = undefined
|
|
446
463
|
renderContext.listTemplate = false
|
|
464
|
+
renderContext.listFields = previousListFields
|
|
447
465
|
renderContext.listDepth--
|
|
448
466
|
}
|
|
449
467
|
}
|
|
@@ -490,6 +508,24 @@ function escapeJsonAttribute(value) {
|
|
|
490
508
|
return JSON.stringify(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll("'", "'")
|
|
491
509
|
}
|
|
492
510
|
|
|
511
|
+
function compactListState(value) {
|
|
512
|
+
if (!Array.isArray(value) || !value.length) return undefined
|
|
513
|
+
const fields = Object.keys(value[0])
|
|
514
|
+
if (!fields.length || !value.every(item => Object.keys(item).length === fields.length && fields.every(field => Object.hasOwn(item, field)))) return undefined
|
|
515
|
+
return [fields, value.map(item => fields.map(field => item[field]))]
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function listSeed(items, fields) {
|
|
519
|
+
if (!items.length || !items.every(item => Object.keys(item).length === fields.size && Object.keys(item).every(field => fields.has(field)))) return undefined
|
|
520
|
+
const seed = {}
|
|
521
|
+
for (const field of fields) {
|
|
522
|
+
const types = new Set(items.map(item => item[field] === null ? "null" : typeof item[field]))
|
|
523
|
+
if (types.size !== 1 || !["string", "number", "boolean", "null"].includes([...types][0])) return undefined
|
|
524
|
+
seed[field] = [...types][0]
|
|
525
|
+
}
|
|
526
|
+
return seed
|
|
527
|
+
}
|
|
528
|
+
|
|
493
529
|
function toKebabCase(value) {
|
|
494
530
|
return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
|
|
495
531
|
}
|
|
@@ -5,6 +5,8 @@ const listRegistrations = new WeakMap()
|
|
|
5
5
|
const mountedLists = new WeakSet()
|
|
6
6
|
const imports = new Map()
|
|
7
7
|
const revisions = new WeakMap()
|
|
8
|
+
const itemParts = new WeakMap()
|
|
9
|
+
const itemPartsSelector = "[data-k-list-text],[data-k-list-attrs],[data-k-list-events],[data-k-list-expression],[data-k-list-expression-attrs]"
|
|
8
10
|
|
|
9
11
|
function commitLists(id) {
|
|
10
12
|
const lists = listTargets.get(id)
|
|
@@ -26,13 +28,22 @@ function mountLists(root) {
|
|
|
26
28
|
if (mountedLists.has(start)) continue
|
|
27
29
|
mountedLists.add(start)
|
|
28
30
|
const descriptor = JSON.parse(start.dataset.kList)
|
|
29
|
-
const
|
|
31
|
+
const end = findEnd(start, descriptor.id)
|
|
32
|
+
const roots = listRoots(start, end)
|
|
33
|
+
const templateRoot = start.content.firstElementChild
|
|
34
|
+
const parts = listItemPartPlan(templateRoot)
|
|
35
|
+
for (const root of roots) mapListItemParts(parts, root)
|
|
36
|
+
if (descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
37
|
+
const items = browserState.get(descriptor.state)
|
|
30
38
|
const list = {
|
|
31
39
|
start,
|
|
32
40
|
descriptor,
|
|
33
|
-
|
|
41
|
+
parts,
|
|
42
|
+
seedFields: descriptor.seed && Object.keys(descriptor.seed),
|
|
43
|
+
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
44
|
+
values: new Map(),
|
|
34
45
|
container: roots[0]?.parentNode,
|
|
35
|
-
boundary:
|
|
46
|
+
boundary: end
|
|
36
47
|
}
|
|
37
48
|
register(listTargets, descriptor.state, list)
|
|
38
49
|
listRegistrations.set(start, { state: descriptor.state, list })
|
|
@@ -60,66 +71,111 @@ function updateList(list) {
|
|
|
60
71
|
if (!Array.isArray(items)) throw new Error("Keyed list state must remain an array")
|
|
61
72
|
const entries = []
|
|
62
73
|
const keys = new Set()
|
|
74
|
+
const seen = new Set()
|
|
63
75
|
for (const item of items) {
|
|
64
76
|
const key = item?.[list.descriptor.key]
|
|
65
77
|
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
66
78
|
assertListItem(item)
|
|
67
|
-
|
|
79
|
+
const seededValue = list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed)
|
|
80
|
+
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
68
81
|
const token = keyToken(key)
|
|
69
82
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
70
83
|
keys.add(token)
|
|
71
|
-
entries.push({ item, key, token })
|
|
84
|
+
entries.push({ item, key, token, value: seededValue ?? JSON.stringify(item) })
|
|
72
85
|
}
|
|
73
86
|
const next = []
|
|
74
|
-
|
|
87
|
+
const values = new Map()
|
|
88
|
+
const parent = list.container ?? list.start.parentNode
|
|
89
|
+
const additions = parent.ownerDocument.createDocumentFragment()
|
|
90
|
+
let added = false
|
|
91
|
+
for (const { item, key, token, value } of entries) {
|
|
75
92
|
let node = list.roots.get(token)
|
|
76
93
|
if (!node) {
|
|
77
|
-
|
|
78
|
-
node
|
|
94
|
+
node = list.start.content.firstElementChild?.cloneNode(true)
|
|
95
|
+
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
79
96
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
80
97
|
node.removeAttribute("data-k-list-root")
|
|
81
|
-
|
|
98
|
+
mapListItemParts(list.parts, node)
|
|
82
99
|
fillListItem(node, item)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} else {
|
|
100
|
+
additions.append(node)
|
|
101
|
+
added = true
|
|
102
|
+
} else if (list.values.get(token) !== value) {
|
|
87
103
|
fillListItem(node, item)
|
|
88
104
|
}
|
|
89
105
|
next.push([token, node])
|
|
106
|
+
values.set(token, value)
|
|
90
107
|
}
|
|
108
|
+
const removals = parent.ownerDocument.createDocumentFragment()
|
|
91
109
|
for (const [token, node] of list.roots) {
|
|
92
110
|
if (keys.has(token)) continue
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
if (list.descriptor.mount) {
|
|
112
|
+
unmountDom(node)
|
|
113
|
+
node.remove()
|
|
114
|
+
} else {
|
|
115
|
+
removals.append(node)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (added) {
|
|
119
|
+
if (list.descriptor.mount) mountDom(additions)
|
|
120
|
+
parent.insertBefore(additions, list.boundary)
|
|
121
|
+
list.container ??= parent
|
|
122
|
+
}
|
|
123
|
+
let anchor = list.boundary
|
|
124
|
+
let misplaced = 0
|
|
125
|
+
for (let index = next.length - 1; index >= 0; index--) {
|
|
126
|
+
const node = next[index][1]
|
|
127
|
+
if (node.nextSibling !== anchor) misplaced++
|
|
128
|
+
anchor = node
|
|
129
|
+
}
|
|
130
|
+
if (misplaced > next.length / 2) {
|
|
131
|
+
const reordered = parent.ownerDocument.createDocumentFragment()
|
|
132
|
+
reordered.append(...next.map(([, node]) => node))
|
|
133
|
+
parent.insertBefore(reordered, list.boundary)
|
|
134
|
+
} else if (misplaced) {
|
|
135
|
+
anchor = list.boundary
|
|
136
|
+
for (let index = next.length - 1; index >= 0; index--) {
|
|
137
|
+
const node = next[index][1]
|
|
138
|
+
if (node.nextSibling !== anchor) parent.insertBefore(node, anchor)
|
|
139
|
+
anchor = node
|
|
140
|
+
}
|
|
95
141
|
}
|
|
96
|
-
const parent = list.container ?? list.start.parentNode
|
|
97
|
-
for (const [, node] of next) parent.insertBefore(node, list.boundary)
|
|
98
142
|
list.roots = new Map(next)
|
|
143
|
+
list.values = values
|
|
99
144
|
}
|
|
100
145
|
|
|
101
146
|
function fillListItem(root, item) {
|
|
102
147
|
const revision = (revisions.get(root) ?? 0) + 1
|
|
103
148
|
revisions.set(root, revision)
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
const parts = listItemParts(root)
|
|
150
|
+
for (const [node, field] of parts.directTexts) {
|
|
151
|
+
const text = item?.[field]
|
|
152
|
+
const value = text == null ? "" : String(text)
|
|
153
|
+
const current = node.firstChild
|
|
154
|
+
if (current?.nodeType === Node.TEXT_NODE && !current.nextSibling) {
|
|
155
|
+
if (current.data !== value) current.data = value
|
|
156
|
+
} else {
|
|
157
|
+
node.textContent = value
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [marker, field] of parts.texts) {
|
|
161
|
+
patchListText(marker, "template[data-k-list-text-end]", item?.[field])
|
|
106
162
|
}
|
|
107
|
-
for (const node of
|
|
108
|
-
for (const [target, field] of
|
|
163
|
+
for (const [node, attributes] of parts.attributes) {
|
|
164
|
+
for (const [target, field] of attributes) patchBinding(node, target, item?.[field])
|
|
109
165
|
}
|
|
110
|
-
for (const node of
|
|
111
|
-
for (const [event, native] of JSON.parse(
|
|
166
|
+
for (const [node, events] of parts.events) {
|
|
167
|
+
for (const [event, native] of JSON.parse(events)) {
|
|
112
168
|
native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
|
|
113
169
|
node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
|
|
114
170
|
}
|
|
115
171
|
}
|
|
116
|
-
for (const marker of
|
|
117
|
-
evaluate(
|
|
172
|
+
for (const [marker, descriptor] of parts.expressions) {
|
|
173
|
+
evaluate(descriptor, item).then(value => {
|
|
118
174
|
if (revisions.get(root) === revision && root.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
|
|
119
175
|
}).catch(error => console.error(error))
|
|
120
176
|
}
|
|
121
|
-
for (const node of
|
|
122
|
-
for (const [target, module, handler] of
|
|
177
|
+
for (const [node, attributes] of parts.expressionAttributes) {
|
|
178
|
+
for (const [target, module, handler] of attributes) {
|
|
123
179
|
evaluate({ module, handler }, item).then(value => {
|
|
124
180
|
if (revisions.get(root) === revision && root.isConnected) patchBinding(node, target, value)
|
|
125
181
|
}).catch(error => console.error(error))
|
|
@@ -127,15 +183,91 @@ function fillListItem(root, item) {
|
|
|
127
183
|
}
|
|
128
184
|
}
|
|
129
185
|
|
|
186
|
+
function listItemParts(root) {
|
|
187
|
+
let parts = itemParts.get(root)
|
|
188
|
+
if (parts) return parts
|
|
189
|
+
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [] }
|
|
190
|
+
for (const node of matching(root, itemPartsSelector)) {
|
|
191
|
+
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
|
|
192
|
+
if (node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
193
|
+
if (node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
|
|
194
|
+
if (node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
|
|
195
|
+
if (node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
|
|
196
|
+
}
|
|
197
|
+
itemParts.set(root, parts)
|
|
198
|
+
return parts
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function listItemPartPlan(template) {
|
|
202
|
+
const source = [template, ...template.querySelectorAll("*")]
|
|
203
|
+
const indexes = new Map(source.map((node, index) => [node, index]))
|
|
204
|
+
const parts = listItemParts(template)
|
|
205
|
+
return {
|
|
206
|
+
directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
|
|
207
|
+
texts: parts.texts.map(([node, field]) => [indexes.get(node), field]),
|
|
208
|
+
attributes: parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]),
|
|
209
|
+
events: parts.events.map(([node, events]) => [indexes.get(node), events]),
|
|
210
|
+
expressions: parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]),
|
|
211
|
+
expressionAttributes: parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes])
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mapListItemParts(parts, root) {
|
|
216
|
+
const target = [root, ...root.querySelectorAll("*")]
|
|
217
|
+
itemParts.set(root, {
|
|
218
|
+
directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
|
|
219
|
+
texts: parts.texts.map(([index, field]) => [target[index], field]),
|
|
220
|
+
attributes: parts.attributes.map(([index, attributes]) => [target[index], attributes]),
|
|
221
|
+
events: parts.events.map(([index, events]) => [target[index], events]),
|
|
222
|
+
expressions: parts.expressions.map(([index, descriptor]) => [target[index], descriptor]),
|
|
223
|
+
expressionAttributes: parts.expressionAttributes.map(([index, attributes]) => [target[index], attributes])
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function listRoots(start, end) {
|
|
228
|
+
const roots = []
|
|
229
|
+
for (let node = start.nextSibling; node && node !== end; node = node.nextSibling) {
|
|
230
|
+
if (node.nodeType === Node.ELEMENT_NODE) roots.push(node)
|
|
231
|
+
}
|
|
232
|
+
return roots
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function seedListItem(root, descriptor, index) {
|
|
236
|
+
const item = { [descriptor.key]: descriptor.keys[index] }
|
|
237
|
+
const parts = itemParts.get(root)
|
|
238
|
+
for (const [node, field] of parts.directTexts) item[field] = seedValue(node.textContent, descriptor.seed[field])
|
|
239
|
+
for (const [marker, field] of parts.texts) item[field] = seedValue(rangeText(marker, "template[data-k-list-text-end]"), descriptor.seed[field])
|
|
240
|
+
return item
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function seedValue(value, type) {
|
|
244
|
+
if (type === "number") return Number(value)
|
|
245
|
+
if (type === "boolean") return value === "true"
|
|
246
|
+
if (type === "null") return null
|
|
247
|
+
return value
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function rangeText(marker, endSelector) {
|
|
251
|
+
let value = ""
|
|
252
|
+
for (let node = marker.nextSibling; node && !(node.nodeType === Node.ELEMENT_NODE && node.matches(endSelector)); node = node.nextSibling) value += node.textContent
|
|
253
|
+
return value
|
|
254
|
+
}
|
|
255
|
+
|
|
130
256
|
function patchListText(marker, endSelector, value) {
|
|
131
257
|
let end = marker.nextSibling
|
|
132
258
|
while (end && !(end.nodeType === Node.ELEMENT_NODE && end.matches(endSelector))) end = end.nextSibling
|
|
133
259
|
if (!end) throw new Error("Keyed list text marker has no end")
|
|
260
|
+
const text = value == null ? "" : String(value)
|
|
261
|
+
const current = marker.nextSibling
|
|
262
|
+
if (current?.nodeType === Node.TEXT_NODE && current.nextSibling === end) {
|
|
263
|
+
if (current.data !== text) current.data = text
|
|
264
|
+
return
|
|
265
|
+
}
|
|
134
266
|
const range = marker.ownerDocument.createRange()
|
|
135
267
|
range.setStartAfter(marker)
|
|
136
268
|
range.setEndBefore(end)
|
|
137
269
|
range.deleteContents()
|
|
138
|
-
end.before(marker.ownerDocument.createTextNode(
|
|
270
|
+
end.before(marker.ownerDocument.createTextNode(text))
|
|
139
271
|
}
|
|
140
272
|
|
|
141
273
|
function evaluate(descriptor, item) {
|
|
@@ -189,19 +321,35 @@ function assertListItem(item) {
|
|
|
189
321
|
if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
|
|
190
322
|
}
|
|
191
323
|
|
|
192
|
-
function
|
|
324
|
+
function seededListValue(item, fields, seed) {
|
|
325
|
+
const keys = Reflect.ownKeys(item)
|
|
326
|
+
if (keys.length !== fields.length || keys.some(key => typeof key !== "string" || !fields.includes(key))) return undefined
|
|
327
|
+
const values = []
|
|
328
|
+
for (const field of fields) {
|
|
329
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, field)
|
|
330
|
+
if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
|
|
331
|
+
if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
|
|
332
|
+
const value = descriptor.value
|
|
333
|
+
const type = value === null ? "null" : typeof value
|
|
334
|
+
if (type !== seed[field] || type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) return undefined
|
|
335
|
+
values.push(value)
|
|
336
|
+
}
|
|
337
|
+
return JSON.stringify(values)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function assertListValue(value, seen, root = false) {
|
|
193
341
|
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return
|
|
194
342
|
if (!value || typeof value !== "object") throw new Error("Keyed list items must contain only JSON-safe values")
|
|
195
343
|
if (seen.has(value)) throw new Error("Keyed list items must not contain cycles")
|
|
196
|
-
|
|
197
|
-
if (!Array.isArray(value) && prototype !== Object.prototype) throw new Error("Keyed list items must contain only arrays and ordinary plain objects")
|
|
198
|
-
if (Object.getOwnPropertySymbols(value).length) throw new Error("Keyed list items must not contain symbols")
|
|
344
|
+
if (!root && !Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype) throw new Error("Keyed list items must contain only arrays and ordinary plain objects")
|
|
199
345
|
seen.add(value)
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
346
|
+
const keys = Reflect.ownKeys(value)
|
|
347
|
+
if (keys.some(key => typeof key === "symbol")) throw new Error("Keyed list items must not contain symbols")
|
|
348
|
+
if (Array.isArray(value) && keys.some(key => key !== "length" && !/^(0|[1-9]\d*)$/.test(key))) throw new Error("Keyed list arrays must not contain custom properties")
|
|
202
349
|
if (Array.isArray(value) && Object.keys(value).length !== value.length) throw new Error("Keyed list arrays must not contain holes")
|
|
203
|
-
for (const
|
|
350
|
+
for (const key of keys) {
|
|
204
351
|
if (Array.isArray(value) && key === "length") continue
|
|
352
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
205
353
|
if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
|
|
206
354
|
if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
|
|
207
355
|
assertListValue(descriptor.value, seen)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { browserState, commitDom } from "./runtime.js"
|
|
1
|
+
import { browserState, commitDom, registerMountHook, registerUnmountHook } from "./shared-runtime.js"
|
|
2
2
|
import { deserialize } from "./serialization.js"
|
|
3
3
|
|
|
4
|
+
const registrations = new WeakMap()
|
|
5
|
+
|
|
4
6
|
export function createNativeContext(state, stateIds, commit, serializedScope = {}) {
|
|
5
7
|
const changed = new Set()
|
|
6
8
|
let scheduled = false
|
|
@@ -34,54 +36,44 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
if (typeof document !== "undefined") {
|
|
37
|
-
const modules = new Map()
|
|
38
|
-
|
|
39
39
|
const eventNames = ["click", "input", "change", "submit", "keydown", "keyup"]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.error(error)
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
}
|
|
40
|
+
const modules = new Map([])
|
|
41
|
+
const mount = root => mountNative(root, eventNames, modules)
|
|
42
|
+
registerMountHook(mount)
|
|
43
|
+
registerUnmountHook(unmountNative)
|
|
44
|
+
mount(document)
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
function mountNative(root, eventNames, modules) {
|
|
48
|
+
for (const eventName of eventNames) {
|
|
49
|
+
for (const node of matching(root, `[data-k-native-${eventName}]`)) {
|
|
50
|
+
const listeners = registrations.get(node) ?? new Map()
|
|
51
|
+
if (listeners.has(eventName)) continue
|
|
52
|
+
const listener = event => {
|
|
53
|
+
try {
|
|
54
|
+
const native = JSON.parse(node.dataset[`kNative${capitalize(eventName)}`])
|
|
55
|
+
const result = modules.get(native.module)[native.handler](createNativeContext(browserState, native.states, commitDom, native.scope), event)
|
|
56
|
+
if (result && typeof result.then === "function") result.catch(error => console.error(error))
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(error)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
node.addEventListener(eventName, listener)
|
|
62
|
+
listeners.set(eventName, listener)
|
|
63
|
+
registrations.set(node, listeners)
|
|
64
|
+
}
|
|
56
65
|
}
|
|
57
|
-
return targets
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
for (const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
modulePromise = import(native.module)
|
|
65
|
-
modules.set(native.module, modulePromise)
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
const module = await modulePromise
|
|
69
|
-
const result = module[native.handler](createNativeContext(browserState, native.states, commitDom, native.scope), delegatedEvent(event, target))
|
|
70
|
-
if (result && typeof result.then === "function") result.catch(error => console.error(error))
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error(error)
|
|
73
|
-
}
|
|
68
|
+
function unmountNative(root) {
|
|
69
|
+
for (const node of matching(root, "*")) {
|
|
70
|
+
for (const [eventName, listener] of registrations.get(node) ?? []) node.removeEventListener(eventName, listener)
|
|
71
|
+
registrations.delete(node)
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
function
|
|
78
|
-
return
|
|
79
|
-
get(source, property) {
|
|
80
|
-
if (property === "currentTarget") return currentTarget
|
|
81
|
-
const value = Reflect.get(source, property, source)
|
|
82
|
-
return typeof value === "function" ? value.bind(source) : value
|
|
83
|
-
}
|
|
84
|
-
})
|
|
75
|
+
function matching(root, selector) {
|
|
76
|
+
return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
function capitalize(value) {
|
|
@@ -55,7 +55,7 @@ export function unmountDom(root) {
|
|
|
55
55
|
|
|
56
56
|
if (typeof document !== "undefined") {
|
|
57
57
|
const initialState = document.body.dataset.kState
|
|
58
|
-
if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)
|
|
58
|
+
if (initialState) for (const [id, value, compact] of JSON.parse(initialState)) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)
|
|
59
59
|
mountText(document)
|
|
60
60
|
|
|
61
61
|
const eventNames = ["click", "input", "change"]
|