@fluixi/dom 1.0.0-alpha.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +657 -0
- package/dist/cdn/dom.cjs +1 -0
- package/dist/cdn/dom.d.ts +16 -0
- package/dist/cdn/dom.d.ts.map +1 -0
- package/dist/cdn/dom.global.js +1 -0
- package/dist/cdn/dom.js +15 -0
- package/dist/cdn/dom.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.mjs +1 -0
- package/dist/lib/component/index.cjs +1 -0
- package/dist/lib/component/index.d.ts +2 -0
- package/dist/lib/component/index.d.ts.map +1 -0
- package/dist/lib/component/index.js +1 -0
- package/dist/lib/component/index.mjs +1 -0
- package/dist/lib/component/modifiers.cjs +1 -0
- package/dist/lib/component/modifiers.d.ts +21 -0
- package/dist/lib/component/modifiers.d.ts.map +1 -0
- package/dist/lib/component/modifiers.js +171 -0
- package/dist/lib/component/modifiers.mjs +1 -0
- package/dist/lib/dom/delegate.cjs +1 -0
- package/dist/lib/dom/delegate.d.ts +17 -0
- package/dist/lib/dom/delegate.d.ts.map +1 -0
- package/dist/lib/dom/delegate.js +50 -0
- package/dist/lib/dom/delegate.mjs +1 -0
- package/dist/lib/dom/hydration-state.cjs +1 -0
- package/dist/lib/dom/hydration-state.d.ts +45 -0
- package/dist/lib/dom/hydration-state.d.ts.map +1 -0
- package/dist/lib/dom/hydration-state.js +94 -0
- package/dist/lib/dom/hydration-state.mjs +1 -0
- package/dist/lib/dom/hydration.cjs +1 -0
- package/dist/lib/dom/hydration.d.ts +10 -0
- package/dist/lib/dom/hydration.d.ts.map +1 -0
- package/dist/lib/dom/hydration.js +143 -0
- package/dist/lib/dom/hydration.mjs +1 -0
- package/dist/lib/dom/index.cjs +1 -0
- package/dist/lib/dom/index.d.ts +9 -0
- package/dist/lib/dom/index.d.ts.map +1 -0
- package/dist/lib/dom/index.js +12 -0
- package/dist/lib/dom/index.mjs +1 -0
- package/dist/lib/dom/integration.cjs +1 -0
- package/dist/lib/dom/integration.d.ts +16 -0
- package/dist/lib/dom/integration.d.ts.map +1 -0
- package/dist/lib/dom/integration.js +25 -0
- package/dist/lib/dom/integration.mjs +1 -0
- package/dist/lib/dom/island.cjs +1 -0
- package/dist/lib/dom/island.d.ts +23 -0
- package/dist/lib/dom/island.d.ts.map +1 -0
- package/dist/lib/dom/island.js +100 -0
- package/dist/lib/dom/island.mjs +1 -0
- package/dist/lib/dom/runtime.cjs +1 -0
- package/dist/lib/dom/runtime.d.ts +131 -0
- package/dist/lib/dom/runtime.d.ts.map +1 -0
- package/dist/lib/dom/runtime.js +1425 -0
- package/dist/lib/dom/runtime.mjs +1 -0
- package/dist/lib/dom/server/fx-data.cjs +1 -0
- package/dist/lib/dom/server/fx-data.d.ts +36 -0
- package/dist/lib/dom/server/fx-data.d.ts.map +1 -0
- package/dist/lib/dom/server/fx-data.js +60 -0
- package/dist/lib/dom/server/fx-data.mjs +1 -0
- package/dist/lib/dom/server/host.cjs +1 -0
- package/dist/lib/dom/server/host.d.ts +14 -0
- package/dist/lib/dom/server/host.d.ts.map +1 -0
- package/dist/lib/dom/server/host.js +38 -0
- package/dist/lib/dom/server/host.mjs +1 -0
- package/dist/lib/dom/server/index.cjs +1 -0
- package/dist/lib/dom/server/index.d.ts +13 -0
- package/dist/lib/dom/server/index.d.ts.map +1 -0
- package/dist/lib/dom/server/index.js +12 -0
- package/dist/lib/dom/server/index.mjs +1 -0
- package/dist/lib/dom/server/nodes.cjs +1 -0
- package/dist/lib/dom/server/nodes.d.ts +105 -0
- package/dist/lib/dom/server/nodes.d.ts.map +1 -0
- package/dist/lib/dom/server/nodes.js +304 -0
- package/dist/lib/dom/server/nodes.mjs +1 -0
- package/dist/lib/dom/server/render.cjs +1 -0
- package/dist/lib/dom/server/render.d.ts +45 -0
- package/dist/lib/dom/server/render.d.ts.map +1 -0
- package/dist/lib/dom/server/render.js +174 -0
- package/dist/lib/dom/server/render.mjs +1 -0
- package/dist/lib/dom/server/request-context.cjs +1 -0
- package/dist/lib/dom/server/request-context.d.ts +85 -0
- package/dist/lib/dom/server/request-context.d.ts.map +1 -0
- package/dist/lib/dom/server/request-context.js +122 -0
- package/dist/lib/dom/server/request-context.mjs +1 -0
- package/dist/lib/dom/server/serialize.cjs +1 -0
- package/dist/lib/dom/server/serialize.d.ts +2 -0
- package/dist/lib/dom/server/serialize.d.ts.map +1 -0
- package/dist/lib/dom/server/serialize.js +98 -0
- package/dist/lib/dom/server/serialize.mjs +1 -0
- package/dist/lib/dom/server-renderer.cjs +1 -0
- package/dist/lib/dom/server-renderer.d.ts +2 -0
- package/dist/lib/dom/server-renderer.d.ts.map +1 -0
- package/dist/lib/dom/server-renderer.js +5 -0
- package/dist/lib/dom/server-renderer.mjs +1 -0
- package/dist/lib/dom/types.cjs +1 -0
- package/dist/lib/dom/types.d.ts +119 -0
- package/dist/lib/dom/types.d.ts.map +1 -0
- package/dist/lib/dom/types.js +1 -0
- package/dist/lib/dom/types.mjs +0 -0
- package/dist/lib/dom/utils.cjs +1 -0
- package/dist/lib/dom/utils.d.ts +411 -0
- package/dist/lib/dom/utils.d.ts.map +1 -0
- package/dist/lib/dom/utils.js +746 -0
- package/dist/lib/dom/utils.mjs +1 -0
- package/dist/lib/element/index.cjs +1 -0
- package/dist/lib/element/index.d.ts +2 -0
- package/dist/lib/element/index.d.ts.map +1 -0
- package/dist/lib/element/index.js +1 -0
- package/dist/lib/element/index.mjs +0 -0
- package/dist/lib/element/types.cjs +1 -0
- package/dist/lib/element/types.d.ts +129 -0
- package/dist/lib/element/types.d.ts.map +1 -0
- package/dist/lib/element/types.js +5 -0
- package/dist/lib/element/types.mjs +0 -0
- package/dist/lib/flow/class-map.cjs +1 -0
- package/dist/lib/flow/class-map.d.ts +40 -0
- package/dist/lib/flow/class-map.d.ts.map +1 -0
- package/dist/lib/flow/class-map.js +44 -0
- package/dist/lib/flow/class-map.mjs +1 -0
- package/dist/lib/flow/dynamic.cjs +1 -0
- package/dist/lib/flow/dynamic.d.ts +27 -0
- package/dist/lib/flow/dynamic.d.ts.map +1 -0
- package/dist/lib/flow/dynamic.js +30 -0
- package/dist/lib/flow/dynamic.mjs +1 -0
- package/dist/lib/flow/error-boundary.cjs +1 -0
- package/dist/lib/flow/error-boundary.d.ts +34 -0
- package/dist/lib/flow/error-boundary.d.ts.map +1 -0
- package/dist/lib/flow/error-boundary.js +49 -0
- package/dist/lib/flow/error-boundary.mjs +1 -0
- package/dist/lib/flow/for.cjs +1 -0
- package/dist/lib/flow/for.d.ts +64 -0
- package/dist/lib/flow/for.d.ts.map +1 -0
- package/dist/lib/flow/for.js +28 -0
- package/dist/lib/flow/for.mjs +1 -0
- package/dist/lib/flow/index-flow.cjs +1 -0
- package/dist/lib/flow/index-flow.d.ts +34 -0
- package/dist/lib/flow/index-flow.d.ts.map +1 -0
- package/dist/lib/flow/index-flow.js +27 -0
- package/dist/lib/flow/index-flow.mjs +1 -0
- package/dist/lib/flow/index.cjs +1 -0
- package/dist/lib/flow/index.d.ts +20 -0
- package/dist/lib/flow/index.d.ts.map +1 -0
- package/dist/lib/flow/index.js +11 -0
- package/dist/lib/flow/index.mjs +1 -0
- package/dist/lib/flow/portal.cjs +1 -0
- package/dist/lib/flow/portal.d.ts +30 -0
- package/dist/lib/flow/portal.d.ts.map +1 -0
- package/dist/lib/flow/portal.js +44 -0
- package/dist/lib/flow/portal.mjs +1 -0
- package/dist/lib/flow/show.cjs +1 -0
- package/dist/lib/flow/show.d.ts +35 -0
- package/dist/lib/flow/show.d.ts.map +1 -0
- package/dist/lib/flow/show.js +162 -0
- package/dist/lib/flow/show.mjs +1 -0
- package/dist/lib/flow/style-map.cjs +1 -0
- package/dist/lib/flow/style-map.d.ts +49 -0
- package/dist/lib/flow/style-map.d.ts.map +1 -0
- package/dist/lib/flow/style-map.js +58 -0
- package/dist/lib/flow/style-map.mjs +1 -0
- package/dist/lib/flow/switch.cjs +1 -0
- package/dist/lib/flow/switch.d.ts +63 -0
- package/dist/lib/flow/switch.d.ts.map +1 -0
- package/dist/lib/flow/switch.js +69 -0
- package/dist/lib/flow/switch.mjs +1 -0
- package/dist/lib/flow/utils.cjs +1 -0
- package/dist/lib/flow/utils.d.ts +19 -0
- package/dist/lib/flow/utils.d.ts.map +1 -0
- package/dist/lib/flow/utils.js +34 -0
- package/dist/lib/flow/utils.mjs +1 -0
- package/dist/lib/index.cjs +1 -0
- package/dist/lib/index.d.ts +21 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +30 -0
- package/dist/lib/index.mjs +1 -0
- package/dist/lib/jsx/control-flow/flow.cjs +1 -0
- package/dist/lib/jsx/control-flow/flow.d.ts +2 -0
- package/dist/lib/jsx/control-flow/flow.d.ts.map +1 -0
- package/dist/lib/jsx/control-flow/flow.js +627 -0
- package/dist/lib/jsx/control-flow/flow.mjs +0 -0
- package/dist/lib/jsx/control-flow/for-flow.cjs +1 -0
- package/dist/lib/jsx/control-flow/for-flow.d.ts +2 -0
- package/dist/lib/jsx/control-flow/for-flow.d.ts.map +1 -0
- package/dist/lib/jsx/control-flow/for-flow.js +1 -0
- package/dist/lib/jsx/control-flow/for-flow.mjs +0 -0
- package/dist/lib/shared/types.cjs +1 -0
- package/dist/lib/shared/types.d.ts +47 -0
- package/dist/lib/shared/types.d.ts.map +1 -0
- package/dist/lib/shared/types.js +1 -0
- package/dist/lib/shared/types.mjs +0 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core Reactive DOM Runtime
|
|
3
|
+
* @module @fluixi/dom/runtime
|
|
4
|
+
*
|
|
5
|
+
* Fine-grained reactive DOM manipulation utilities inspired by SolidJS.
|
|
6
|
+
* Key design: Event delegation uses `$$eventname` properties on DOM nodes
|
|
7
|
+
* (the SolidJS pattern) — simpler and more reliable than Map-based lookup.
|
|
8
|
+
*
|
|
9
|
+
* Critical fixes applied:
|
|
10
|
+
* - insertContent array branch: nested signal content cleanup via onCleanup
|
|
11
|
+
* - flattenArray: uses recursive flattenArray for signal results (not normalizeValue)
|
|
12
|
+
* - DOM Runtime Guards: insertBefore checks marker.parentNode !== container
|
|
13
|
+
* - createRoot owner restoration: Owner = prevOwner (not Owner ??= prevOwner)
|
|
14
|
+
*/
|
|
15
|
+
import { isSignal, } from '@fluixi/reactive/signal';
|
|
16
|
+
import { JSX_PROPERTIES, setAttribute, applyElementProp, SVG_NAMESPACE } from './utils.js';
|
|
17
|
+
import { isComponent } from '../component/modifiers.js';
|
|
18
|
+
// Reactive primitives come straight from the signal core (the globalThis-singleton graph makes
|
|
19
|
+
// this duplication-safe). The integration bridge is only for framework-provided glue that is
|
|
20
|
+
// NOT in the reactive core (e.g. createComponent).
|
|
21
|
+
import { batch, createEffect, createRenderEffect, createRoot, onCleanup, readChildren } from '@fluixi/reactive/signal';
|
|
22
|
+
import { isServer, getServerNodes, isDomNode, isDomElement, isDomText, } from './server/host.js';
|
|
23
|
+
import { hydration, getHydrationWalker, isAdopted, ascendPast, } from './hydration-state.js';
|
|
24
|
+
function getElementFingerprint(el) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
while (el && el.nodeType === 1) {
|
|
27
|
+
let part = el.tagName.toLowerCase();
|
|
28
|
+
// Prefer stable identifiers
|
|
29
|
+
if (el.id) {
|
|
30
|
+
part += `#${el.id}`;
|
|
31
|
+
parts.unshift(part);
|
|
32
|
+
break; // ID is already unique
|
|
33
|
+
}
|
|
34
|
+
if (el.getAttribute('data-id')) {
|
|
35
|
+
part += `[data-id="${el.getAttribute('data-id')}"]`;
|
|
36
|
+
}
|
|
37
|
+
// Add index among siblings (important)
|
|
38
|
+
let index = 0;
|
|
39
|
+
let sibling = el;
|
|
40
|
+
while ((sibling = sibling.previousElementSibling)) {
|
|
41
|
+
if (sibling.tagName === el.tagName)
|
|
42
|
+
index++;
|
|
43
|
+
}
|
|
44
|
+
part += `:nth-of-type(${index + 1})`;
|
|
45
|
+
parts.unshift(part);
|
|
46
|
+
el = el.parentElement;
|
|
47
|
+
}
|
|
48
|
+
return parts.join('>');
|
|
49
|
+
}
|
|
50
|
+
function hashString(str) {
|
|
51
|
+
let hash = 0;
|
|
52
|
+
for (let i = 0; i < str.length; i++) {
|
|
53
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
54
|
+
hash |= 0; // convert to 32bit int
|
|
55
|
+
}
|
|
56
|
+
return `el_${Math.abs(hash)}`;
|
|
57
|
+
}
|
|
58
|
+
export function getStableElementId(el) {
|
|
59
|
+
return hashString(getElementFingerprint(el));
|
|
60
|
+
}
|
|
61
|
+
export const generateID = () => {
|
|
62
|
+
return 'xxyxxxxxxy-4xx8'.replace(/[xy]/g, function (c) {
|
|
63
|
+
let r = (Math.random() * 16) | 0;
|
|
64
|
+
let v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
65
|
+
return v.toString(16);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Constants
|
|
70
|
+
// ============================================================================
|
|
71
|
+
const EMPTY_OBJ = {};
|
|
72
|
+
const EMPTY_ARR = [];
|
|
73
|
+
// Template cache
|
|
74
|
+
const templateCache = new Map();
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Event Delegation — SolidJS $$eventname property pattern
|
|
77
|
+
//
|
|
78
|
+
// Handlers are stored directly on DOM nodes as `node.$$click = handler`.
|
|
79
|
+
// The capture listener reads `node.$$click` directly — no Map lookups needed.
|
|
80
|
+
// This approach correctly handles e.preventDefault() and stopPropagation.
|
|
81
|
+
// ============================================================================
|
|
82
|
+
const delegatedSet = new Set();
|
|
83
|
+
// Events that don't bubble — must use direct addEventListener
|
|
84
|
+
const NON_DELEGATED = new Set(['scroll', 'focus', 'blur', 'load', 'error', 'resize', 'mouseenter', 'mouseleave', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture']);
|
|
85
|
+
/**
|
|
86
|
+
* Register global delegated event listener for the given event names.
|
|
87
|
+
* Stores handlers as `element.$$eventname = handler` properties.
|
|
88
|
+
*/
|
|
89
|
+
export function delegateEvents(eventNames) {
|
|
90
|
+
if (isServer)
|
|
91
|
+
return; // no document to attach to; events are inert in SSR
|
|
92
|
+
for (const name of eventNames) {
|
|
93
|
+
if (delegatedSet.has(name) || NON_DELEGATED.has(name))
|
|
94
|
+
continue;
|
|
95
|
+
delegatedSet.add(name);
|
|
96
|
+
document.addEventListener(name, delegatedEventHandler);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function delegatedEventHandler(e) {
|
|
100
|
+
const key = `$$${e.type}`;
|
|
101
|
+
// Walk up from the actual target (handles shadow DOM via composedPath)
|
|
102
|
+
let node = (e.composedPath && e.composedPath()[0]) || e.target;
|
|
103
|
+
while (node) {
|
|
104
|
+
const handler = node[key];
|
|
105
|
+
if (handler && !node.disabled) {
|
|
106
|
+
handler(e);
|
|
107
|
+
if (e.cancelBubble)
|
|
108
|
+
return; // stopPropagation was called
|
|
109
|
+
}
|
|
110
|
+
// Shadow DOM: step through to host if needed
|
|
111
|
+
node = node.host && node.host !== node && node.host instanceof Element
|
|
112
|
+
? node.host
|
|
113
|
+
: node.parentNode;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Add a delegated event handler — stores as $$eventname property
|
|
118
|
+
*/
|
|
119
|
+
export function addDelegatedEventListener(element, name, handler) {
|
|
120
|
+
element[`$$${name}`] = handler;
|
|
121
|
+
if (NON_DELEGATED.has(name)) {
|
|
122
|
+
// Non-bubbling event (focus/blur/pointerenter/…): document delegation can't observe it, so
|
|
123
|
+
// attach a direct listener once. A stable forwarder reads $$name so updating the handler
|
|
124
|
+
// doesn't churn addEventListener/removeEventListener.
|
|
125
|
+
const key = `__fx_${name}`;
|
|
126
|
+
if (!element[key]) {
|
|
127
|
+
const forwarder = (e) => { element[`$$${name}`]?.(e); };
|
|
128
|
+
element.addEventListener(name, forwarder);
|
|
129
|
+
element[key] = forwarder;
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!delegatedSet.has(name)) {
|
|
134
|
+
delegateEvents([name]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Remove a delegated event handler
|
|
139
|
+
*/
|
|
140
|
+
export function removeDelegatedEventListener(element, name) {
|
|
141
|
+
delete element[`$$${name}`];
|
|
142
|
+
const key = `__fx_${name}`;
|
|
143
|
+
const forwarder = element[key];
|
|
144
|
+
if (forwarder) {
|
|
145
|
+
element.removeEventListener(name, forwarder);
|
|
146
|
+
delete element[key];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Check if an event should be delegated
|
|
151
|
+
*/
|
|
152
|
+
export function isDelegatedEvent(name) {
|
|
153
|
+
return !NON_DELEGATED.has(name);
|
|
154
|
+
}
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Utilities
|
|
157
|
+
// ============================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Check if value is a function
|
|
160
|
+
*/
|
|
161
|
+
export function isFunction(value) {
|
|
162
|
+
return typeof value === 'function';
|
|
163
|
+
}
|
|
164
|
+
function createTextNode(value) {
|
|
165
|
+
if (isServer)
|
|
166
|
+
return getServerNodes().createText(value);
|
|
167
|
+
if (hydration.active) {
|
|
168
|
+
const node = getHydrationWalker()?.hydrateText(value);
|
|
169
|
+
if (node)
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
return document.createTextNode(value);
|
|
173
|
+
}
|
|
174
|
+
function createMarker(value) {
|
|
175
|
+
if (isServer)
|
|
176
|
+
return getServerNodes().createComment(value);
|
|
177
|
+
if (hydration.active) {
|
|
178
|
+
const node = getHydrationWalker()?.hydrateMarker(value);
|
|
179
|
+
if (node)
|
|
180
|
+
return node;
|
|
181
|
+
}
|
|
182
|
+
return document.createComment(value);
|
|
183
|
+
}
|
|
184
|
+
export function createNativeElement(tag, isSVG = false) {
|
|
185
|
+
if (isServer)
|
|
186
|
+
return getServerNodes().createElement(tag, isSVG);
|
|
187
|
+
if (hydration.active) {
|
|
188
|
+
const node = getHydrationWalker()?.hydrateElement(tag);
|
|
189
|
+
if (node)
|
|
190
|
+
return node;
|
|
191
|
+
}
|
|
192
|
+
return isSVG
|
|
193
|
+
? document.createElementNS(SVG_NAMESPACE, tag)
|
|
194
|
+
: document.createElement(tag);
|
|
195
|
+
}
|
|
196
|
+
export function createTemplate(html, isSVG = false) {
|
|
197
|
+
const cached = templateCache.get(html);
|
|
198
|
+
if (cached)
|
|
199
|
+
return cached;
|
|
200
|
+
const template = document.createElement('template');
|
|
201
|
+
if (isSVG) {
|
|
202
|
+
template.innerHTML = `<svg>${html}</svg>`;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
template.innerHTML = html;
|
|
206
|
+
}
|
|
207
|
+
templateCache.set(html, template);
|
|
208
|
+
return template;
|
|
209
|
+
}
|
|
210
|
+
export function cloneTemplate(template, isSVG = false) {
|
|
211
|
+
const clone = template.content.cloneNode(true);
|
|
212
|
+
if (isSVG)
|
|
213
|
+
return clone.firstChild;
|
|
214
|
+
return clone;
|
|
215
|
+
}
|
|
216
|
+
export function makeArrayFlat(values) {
|
|
217
|
+
if (!Array.isArray(values))
|
|
218
|
+
return values;
|
|
219
|
+
if (values.length === 2 && values[1] === ' ') {
|
|
220
|
+
if (Array.isArray(values[0]))
|
|
221
|
+
return makeArrayFlat(values[0]);
|
|
222
|
+
if (typeof values[0] === 'function')
|
|
223
|
+
return values[0];
|
|
224
|
+
return values[0];
|
|
225
|
+
}
|
|
226
|
+
if (values.length === 1 && typeof values[0] === 'function')
|
|
227
|
+
return values[0];
|
|
228
|
+
return values;
|
|
229
|
+
}
|
|
230
|
+
export function fnName(fn) {
|
|
231
|
+
if (typeof fn !== 'function')
|
|
232
|
+
return null;
|
|
233
|
+
return fn._name || fn.name || 'anonymous';
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* iterateFn — resolves a value by calling through plain function wrappers.
|
|
237
|
+
*
|
|
238
|
+
* Must stop at signals/memos/resources ($SIGNAL marker) and
|
|
239
|
+
* component thunks (fluixi-component marker). Without this:
|
|
240
|
+
* - components returning memos get unwrapped to static values, losing reactivity
|
|
241
|
+
* - lazy component createMemo re-triggers C(props) → infinite recursion
|
|
242
|
+
*/
|
|
243
|
+
export function iterateFn(item) {
|
|
244
|
+
// Stop immediately if it's a signal, memo, or resource
|
|
245
|
+
if (isSignal(item))
|
|
246
|
+
return item;
|
|
247
|
+
// Stop if it's a component thunk
|
|
248
|
+
if (isComponent(item))
|
|
249
|
+
return item;
|
|
250
|
+
if (typeof item !== 'function')
|
|
251
|
+
return item;
|
|
252
|
+
// Unwrap plain function wrappers — stop at signals and component thunks
|
|
253
|
+
let content = item();
|
|
254
|
+
while (typeof content === 'function' &&
|
|
255
|
+
!isSignal(content) &&
|
|
256
|
+
!isComponent(content)) {
|
|
257
|
+
content = content();
|
|
258
|
+
}
|
|
259
|
+
return content;
|
|
260
|
+
}
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Insert depth guard
|
|
263
|
+
// ============================================================================
|
|
264
|
+
let _insertDepth = 0;
|
|
265
|
+
const MAX_INSERT_DEPTH = 200;
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// _createInsertEffect
|
|
268
|
+
//
|
|
269
|
+
// Creates a reactive render effect for DOM insertion.
|
|
270
|
+
// Uses a componentCache to ensure component thunks are called at most once —
|
|
271
|
+
// same thunk function reference always returns the same output across re-runs.
|
|
272
|
+
// ============================================================================
|
|
273
|
+
function _createInsertEffect(getAccessor, parent, marker, initialCurrent, setCurrent) {
|
|
274
|
+
let current = initialCurrent;
|
|
275
|
+
const componentCache = new Map();
|
|
276
|
+
return createRenderEffect(() => {
|
|
277
|
+
let value;
|
|
278
|
+
// ── 0. If accessor itself is a component thunk (not a signal/memo), call it once via cache ──
|
|
279
|
+
// must check !isSignal to avoid caching reactive values that are also
|
|
280
|
+
// stamped as components (e.g. memos returned from components via $component/asComponent).
|
|
281
|
+
if (isComponent(getAccessor) && !isSignal(getAccessor)) {
|
|
282
|
+
if (!componentCache.has(getAccessor)) {
|
|
283
|
+
_insertDepth++;
|
|
284
|
+
try {
|
|
285
|
+
const inst = getAccessor();
|
|
286
|
+
componentCache.set(getAccessor, inst);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
_insertDepth--;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
value = componentCache.get(getAccessor);
|
|
293
|
+
while (typeof value === 'function' && isSignal(value))
|
|
294
|
+
value = value();
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
value = getAccessor();
|
|
298
|
+
// ── 1. Follow nested signal chain ──────────────────────────────────
|
|
299
|
+
while (typeof value === 'function' && isSignal(value)) {
|
|
300
|
+
value = value();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// ── 2. Resolve component/signal chain via cache ───────────────────────
|
|
304
|
+
{
|
|
305
|
+
let chainDepth = 0;
|
|
306
|
+
while (typeof value === 'function' && chainDepth++ < 50) {
|
|
307
|
+
if (isComponent(value)) {
|
|
308
|
+
if (!componentCache.has(value)) {
|
|
309
|
+
const thunk = value;
|
|
310
|
+
_insertDepth++;
|
|
311
|
+
try {
|
|
312
|
+
const inst = thunk();
|
|
313
|
+
componentCache.set(thunk, inst);
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
_insertDepth--;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
value = componentCache.get(value);
|
|
320
|
+
}
|
|
321
|
+
else if (isSignal(value)) {
|
|
322
|
+
value = value();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// Plain non-component, non-signal function
|
|
326
|
+
value = value();
|
|
327
|
+
if (typeof value !== 'function' || (!isSignal(value) && !isComponent(value)))
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ── 3. Insert resolved value ──────────────────────────────────────────
|
|
333
|
+
_insertDepth++;
|
|
334
|
+
try {
|
|
335
|
+
current = insertExpression(parent, value, marker, current, componentCache);
|
|
336
|
+
setCurrent(current);
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
_insertDepth--;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// ============================================================================
|
|
344
|
+
// insert
|
|
345
|
+
// ============================================================================
|
|
346
|
+
export function insert(parent, accessor, marker, init) {
|
|
347
|
+
if (_insertDepth > MAX_INSERT_DEPTH) {
|
|
348
|
+
console.error('[insert] Exceeded max call depth — possible infinite loop.');
|
|
349
|
+
return () => null;
|
|
350
|
+
}
|
|
351
|
+
if (marker !== undefined && !marker) {
|
|
352
|
+
marker = createTextNode('');
|
|
353
|
+
parent.appendChild(marker);
|
|
354
|
+
}
|
|
355
|
+
let current = init ?? null;
|
|
356
|
+
if (typeof accessor !== 'function') {
|
|
357
|
+
_insertDepth++;
|
|
358
|
+
try {
|
|
359
|
+
current = insertExpression(parent, accessor, marker, null);
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
_insertDepth--;
|
|
363
|
+
}
|
|
364
|
+
return () => current;
|
|
365
|
+
}
|
|
366
|
+
const dispose = _createInsertEffect(accessor, parent, marker, current, (v) => { current = v; });
|
|
367
|
+
return () => { dispose?.(); return current; };
|
|
368
|
+
}
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// insertExpression
|
|
371
|
+
// ============================================================================
|
|
372
|
+
export function insertExpression(parent, value, marker, current, componentCache) {
|
|
373
|
+
if (value == null || typeof value === 'boolean') {
|
|
374
|
+
return cleanChildren(parent, current, marker);
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === 'function') {
|
|
377
|
+
while (typeof value === 'function') {
|
|
378
|
+
value = value();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (value == null || typeof value === 'boolean') {
|
|
382
|
+
return cleanChildren(parent, current, marker);
|
|
383
|
+
}
|
|
384
|
+
// Handle promises
|
|
385
|
+
if (isPromise(value)) {
|
|
386
|
+
const placeholder = createTextNode('');
|
|
387
|
+
if (Array.isArray(current)) {
|
|
388
|
+
if (current.length > 0) {
|
|
389
|
+
parent.replaceChild(placeholder, current[0]);
|
|
390
|
+
cleanChildren(parent, current.slice(1), marker);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
insertBefore(parent, placeholder, marker);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else if (current) {
|
|
397
|
+
parent.replaceChild(placeholder, current);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
insertBefore(parent, placeholder, marker);
|
|
401
|
+
}
|
|
402
|
+
value
|
|
403
|
+
.then((resolved) => {
|
|
404
|
+
if (placeholder.parentNode === parent) {
|
|
405
|
+
insertExpression(parent, resolved, placeholder.nextSibling, placeholder);
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
.catch((err) => {
|
|
409
|
+
console.error('Error resolving promise in insertExpression:', err);
|
|
410
|
+
});
|
|
411
|
+
return placeholder;
|
|
412
|
+
}
|
|
413
|
+
const t = typeof value;
|
|
414
|
+
if (t === 'string' || t === 'number') {
|
|
415
|
+
if (t === 'number')
|
|
416
|
+
value = String(value);
|
|
417
|
+
if (Array.isArray(current)) {
|
|
418
|
+
if (current.length === 0) {
|
|
419
|
+
const node = createTextNode(value);
|
|
420
|
+
insertBefore(parent, node, marker);
|
|
421
|
+
return node;
|
|
422
|
+
}
|
|
423
|
+
if (current.length === 1 && current[0].nodeType === 3) {
|
|
424
|
+
current[0].data = value;
|
|
425
|
+
return current;
|
|
426
|
+
}
|
|
427
|
+
current = cleanChildren(parent, current, marker);
|
|
428
|
+
}
|
|
429
|
+
if (current && isDomText(current)) {
|
|
430
|
+
current.data = value;
|
|
431
|
+
return current;
|
|
432
|
+
}
|
|
433
|
+
const node = createTextNode(value);
|
|
434
|
+
if (current && current.parentNode === parent) {
|
|
435
|
+
parent.replaceChild(node, current);
|
|
436
|
+
}
|
|
437
|
+
else if (!isAdopted(parent, node)) {
|
|
438
|
+
// during hydration createTextNode reuses the in-place server text node and
|
|
439
|
+
// already advanced the cursor — don't re-insert (that would reorder).
|
|
440
|
+
insertBefore(parent, node, marker);
|
|
441
|
+
}
|
|
442
|
+
return node;
|
|
443
|
+
}
|
|
444
|
+
if (isDomNode(value)) {
|
|
445
|
+
// Hydration: the node was already produced from the server DOM (reused by
|
|
446
|
+
// createNativeElement) and sits in `parent` in place — adopt it without
|
|
447
|
+
// re-inserting, and ascend the cursor past its (already-walked) subtree.
|
|
448
|
+
if (isAdopted(parent, value)) {
|
|
449
|
+
ascendPast(value);
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
if (Array.isArray(current)) {
|
|
453
|
+
if (current.length === 0) {
|
|
454
|
+
insertBefore(parent, value, marker);
|
|
455
|
+
}
|
|
456
|
+
else if (current[0].parentNode === parent) {
|
|
457
|
+
parent.replaceChild(value, current[0]);
|
|
458
|
+
cleanChildren(parent, current.slice(1), marker);
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
insertBefore(parent, value, marker);
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
if (!current) {
|
|
466
|
+
insertBefore(parent, value, marker);
|
|
467
|
+
}
|
|
468
|
+
else if (current !== value) {
|
|
469
|
+
if (current.parentNode === parent) {
|
|
470
|
+
parent.replaceChild(value, current);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
insertBefore(parent, value, marker);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
if (Array.isArray(value)) {
|
|
479
|
+
const array = [];
|
|
480
|
+
const currentArray = Array.isArray(current)
|
|
481
|
+
? current
|
|
482
|
+
: current
|
|
483
|
+
? [current]
|
|
484
|
+
: [];
|
|
485
|
+
// Collect reactive members so each gets its own fine-grained insert effect instead of being
|
|
486
|
+
// read inline in this (enclosing) render effect.
|
|
487
|
+
const reactiveSlots = [];
|
|
488
|
+
flattenArray(value, array, parent, marker, true, componentCache, reactiveSlots);
|
|
489
|
+
const reconciled = reconcileArrays(parent, currentArray, array, marker);
|
|
490
|
+
// Markers are now in the DOM — wire a per-slot reactive insert. insert() sets up a nested
|
|
491
|
+
// render effect that re-inserts the member's content before its marker on change, so a bare
|
|
492
|
+
// control-flow component return (<Show>/<Switch>/<For>) updates in place.
|
|
493
|
+
//
|
|
494
|
+
// The per-slot content lives OUTSIDE the reconcile array (only the marker is tracked), so if
|
|
495
|
+
// this enclosing effect re-runs with a shorter array — e.g. a <For> item dropped, or its child
|
|
496
|
+
// resolved to a memo (Provider -> component) — reconcile removes the marker but would orphan the
|
|
497
|
+
// content. Register a cleanup that removes the slot's nodes when its effect is torn down. This
|
|
498
|
+
// only fires when the enclosing effect re-runs (the array itself changed); an in-place update
|
|
499
|
+
// (e.g. <Show> toggling) never re-runs this effect, so it keeps updating in place.
|
|
500
|
+
for (const slot of reactiveSlots) {
|
|
501
|
+
const disposeSlot = insert(parent, slot.sig, slot.marker);
|
|
502
|
+
onCleanup(() => {
|
|
503
|
+
cleanChildren(parent, disposeSlot());
|
|
504
|
+
if (slot.marker.parentNode === parent)
|
|
505
|
+
parent.removeChild(slot.marker);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return reconciled;
|
|
509
|
+
}
|
|
510
|
+
if (t === 'object' && value !== null && typeof value === 'object') {
|
|
511
|
+
const objValue = value;
|
|
512
|
+
if ('_$litType$' in objValue || '_$litDirective$' in objValue) {
|
|
513
|
+
return insertExpression(parent, renderLitToNodes(value), marker, current);
|
|
514
|
+
}
|
|
515
|
+
if ('type' in objValue && 'props' in objValue) {
|
|
516
|
+
const jsxValue = objValue;
|
|
517
|
+
if (typeof jsxValue.type === 'function') {
|
|
518
|
+
const result = jsxValue.type(jsxValue.props || {});
|
|
519
|
+
return insertExpression(parent, result, marker, current);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return cleanChildren(parent, current, marker);
|
|
524
|
+
}
|
|
525
|
+
// flattenArray — flatten a children array to DOM nodes. A signal whose result is a
|
|
526
|
+
// component thunk (e.g. a router memo returning a SuspenseThunk) is recursed through
|
|
527
|
+
// flattenArray, not read inline, so the thunk goes through componentCache and stays
|
|
528
|
+
// stable across navigations.
|
|
529
|
+
function flattenArray(arr, result, parent, marker, track = true, componentCache,
|
|
530
|
+
// When provided, a reactive member is NOT read inline (which would subscribe the enclosing
|
|
531
|
+
// render effect and re-render the whole subtree on change). Instead it emits a placeholder
|
|
532
|
+
// marker and records itself here; the array caller wires a fine-grained insert() per slot
|
|
533
|
+
// after the markers are placed. This is what makes a bare control-flow component return
|
|
534
|
+
// (a memo, e.g. <Show>) update in place, like an element child does.
|
|
535
|
+
reactiveSlots) {
|
|
536
|
+
for (let i = 0; i < arr.length; i++) {
|
|
537
|
+
let item = arr[i];
|
|
538
|
+
if (item == null || typeof item === 'boolean')
|
|
539
|
+
continue;
|
|
540
|
+
// Resolve plain functions (not signals, not component thunks)
|
|
541
|
+
while (typeof item === 'function' && !isPromise(item) && !isSignal(item) && !isComponent(item)) {
|
|
542
|
+
item = item();
|
|
543
|
+
if (item == null || typeof item === 'boolean')
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
if (item == null || typeof item === 'boolean')
|
|
547
|
+
continue;
|
|
548
|
+
if (typeof item === 'function') {
|
|
549
|
+
let resolved;
|
|
550
|
+
if (isComponent(item) && !isSignal(item)) {
|
|
551
|
+
// Pure component thunk (e.g. () => <div>) — call exactly once and cache so
|
|
552
|
+
// re-runs of the enclosing render effect return the same DOM nodes (keeps
|
|
553
|
+
// focus etc. stable across navigations).
|
|
554
|
+
// must check !isSignal. A memo stamped as a component (e.g. an
|
|
555
|
+
// Outlet/Routes/Suspense output returned via $component/asComponent) is BOTH
|
|
556
|
+
// isComponent AND isSignal; caching it would freeze its first value (so a
|
|
557
|
+
// boundary that later switches from a single node to an array — fallback ->
|
|
558
|
+
// children — would never update). Read those reactively in the else branch.
|
|
559
|
+
if (!componentCache)
|
|
560
|
+
componentCache = new Map();
|
|
561
|
+
if (!componentCache.has(item))
|
|
562
|
+
componentCache.set(item, item());
|
|
563
|
+
resolved = componentCache.get(item);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
// Signal/memo (or memo-stamped component) — read to track reactivity.
|
|
567
|
+
resolved = item();
|
|
568
|
+
}
|
|
569
|
+
// Unwrap plain wrappers; stop at signals and component thunks.
|
|
570
|
+
// Cache calls by function reference — a plain function returned by a
|
|
571
|
+
// signal/memo (e.g. `createMemo(() => cond ? () => <JSX> : ...)`) is a
|
|
572
|
+
// stable reference as long as the memo doesn't re-evaluate. Calling it
|
|
573
|
+
// once and caching the result means re-runs of the outer render effect
|
|
574
|
+
// (triggered by unrelated signals like `activeLevel()`) hit the cache and
|
|
575
|
+
// return the same DOM nodes. reconcileArrays then sees identical references
|
|
576
|
+
// and skips the slot — the focused <input> is never removed.
|
|
577
|
+
while (typeof resolved === 'function' &&
|
|
578
|
+
!isPromise(resolved) &&
|
|
579
|
+
!isSignal(resolved) &&
|
|
580
|
+
!isComponent(resolved)) {
|
|
581
|
+
if (!componentCache)
|
|
582
|
+
componentCache = new Map();
|
|
583
|
+
if (!componentCache.has(resolved))
|
|
584
|
+
componentCache.set(resolved, resolved());
|
|
585
|
+
resolved = componentCache.get(resolved);
|
|
586
|
+
}
|
|
587
|
+
if (resolved == null || typeof resolved === 'boolean')
|
|
588
|
+
continue;
|
|
589
|
+
if (typeof resolved === 'function') {
|
|
590
|
+
if (isSignal(resolved)) {
|
|
591
|
+
// Defer to a per-slot insert effect (see reactiveSlots) so a reactive member updates
|
|
592
|
+
// in place instead of re-rendering the enclosing subtree. Only the synchronous array
|
|
593
|
+
// path collects slots; async/promise resolutions fall back to the inline read below.
|
|
594
|
+
if (reactiveSlots) {
|
|
595
|
+
const slot = createTextNode('');
|
|
596
|
+
result.push(slot);
|
|
597
|
+
reactiveSlots.push({ marker: slot, sig: resolved });
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
// Read the signal — tracked inside the enclosing render effect.
|
|
601
|
+
// Use flattenArray recursively (NOT normalizeValue) so
|
|
602
|
+
// component thunks inside the result go through componentCache.
|
|
603
|
+
let val = resolved();
|
|
604
|
+
while (typeof val === 'function' && isSignal(val))
|
|
605
|
+
val = val();
|
|
606
|
+
if (val == null || typeof val === 'boolean')
|
|
607
|
+
continue;
|
|
608
|
+
if (Array.isArray(val)) {
|
|
609
|
+
flattenArray(val, result, parent, marker, track, componentCache);
|
|
610
|
+
}
|
|
611
|
+
else
|
|
612
|
+
flattenArray([val], result, parent, marker, track, componentCache);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
// Other function type — skip
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
// resolved is not a function
|
|
619
|
+
if (Array.isArray(resolved))
|
|
620
|
+
flattenArray(resolved, result, parent, marker, track, componentCache, reactiveSlots);
|
|
621
|
+
else
|
|
622
|
+
flattenArray([resolved], result, parent, marker, track, componentCache, reactiveSlots);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (isPromise(item)) {
|
|
626
|
+
const placeholder = createTextNode('');
|
|
627
|
+
result.push(placeholder);
|
|
628
|
+
item
|
|
629
|
+
.then((resolved) => {
|
|
630
|
+
if (placeholder.parentNode) {
|
|
631
|
+
const tempNodes = [];
|
|
632
|
+
flattenArray([resolved], tempNodes, placeholder.parentNode, undefined, false, componentCache);
|
|
633
|
+
if (tempNodes.length > 0) {
|
|
634
|
+
placeholder.parentNode.replaceChild(tempNodes[0], placeholder);
|
|
635
|
+
for (let j = 1; j < tempNodes.length; j++) {
|
|
636
|
+
placeholder.parentNode?.insertBefore(tempNodes[j], tempNodes[j - 1].nextSibling);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
.catch((err) => console.error('Error resolving promise in array:', err));
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (Array.isArray(item)) {
|
|
645
|
+
flattenArray(item, result, parent, marker, track, componentCache, reactiveSlots);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (isDomNode(item)) {
|
|
649
|
+
result.push(item);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (typeof item === 'object' &&
|
|
653
|
+
item !== null &&
|
|
654
|
+
('_$litType$' in item || '_$litDirective$' in item)) {
|
|
655
|
+
const nodes = renderLitToNodes(item);
|
|
656
|
+
for (const node of nodes)
|
|
657
|
+
result.push(node);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (typeof item === 'object' &&
|
|
661
|
+
item !== null &&
|
|
662
|
+
'type' in item &&
|
|
663
|
+
'props' in item) {
|
|
664
|
+
const jsxItem = item;
|
|
665
|
+
if (typeof jsxItem.type === 'function') {
|
|
666
|
+
const resolved = jsxItem.type(jsxItem.props || {});
|
|
667
|
+
if (Array.isArray(resolved))
|
|
668
|
+
flattenArray(resolved, result, parent, marker, false, componentCache);
|
|
669
|
+
else
|
|
670
|
+
flattenArray([resolved], result, parent, marker, false, componentCache);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (typeof item === 'string' || typeof item === 'number') {
|
|
675
|
+
const str = String(item);
|
|
676
|
+
if (str.trim() === '' && str !== ' ')
|
|
677
|
+
continue;
|
|
678
|
+
result.push(createTextNode(str));
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
result.push(createTextNode(String(item)));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// reconcileArrays
|
|
686
|
+
// ============================================================================
|
|
687
|
+
function reconcileArrays(parent, a, b, marker) {
|
|
688
|
+
// Pre-process: clone any node already owned by a different parent.
|
|
689
|
+
// The Babel plugin hoists static elements to module-level — the same node
|
|
690
|
+
// can appear in `b` for multiple parents simultaneously. Clone to avoid
|
|
691
|
+
// stealing it from its current owner; future reconciles track the clone.
|
|
692
|
+
for (let i = 0; i < b.length; i++) {
|
|
693
|
+
if (b[i].parentNode !== null && b[i].parentNode !== parent) {
|
|
694
|
+
b = b.slice();
|
|
695
|
+
b[i] = b[i].cloneNode(true);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// SolidJS-style reconcile: skip common prefix/suffix by identity so unchanged
|
|
699
|
+
// nodes (e.g. a focused <input> inside a stable parent div) are never removed.
|
|
700
|
+
let bLength = b.length, aEnd = a.length, bEnd = bLength, aStart = 0, bStart = 0,
|
|
701
|
+
// When `a` is empty fall back to marker so insertions land in the right slot.
|
|
702
|
+
after = aEnd > 0
|
|
703
|
+
? a[aEnd - 1].nextSibling ?? undefined
|
|
704
|
+
: marker, map = null;
|
|
705
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
706
|
+
// 1. Common prefix — already in the right place, skip.
|
|
707
|
+
if (a[aStart] === b[bStart]) {
|
|
708
|
+
aStart++;
|
|
709
|
+
bStart++;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
// 2. Common suffix — already in the right place, shrink window.
|
|
713
|
+
while (aEnd > aStart && bEnd > bStart && a[aEnd - 1] === b[bEnd - 1]) {
|
|
714
|
+
aEnd--;
|
|
715
|
+
bEnd--;
|
|
716
|
+
}
|
|
717
|
+
// 3. Old list exhausted — insert remaining new nodes.
|
|
718
|
+
if (aEnd === aStart) {
|
|
719
|
+
const node = bEnd < bLength
|
|
720
|
+
? bStart
|
|
721
|
+
? b[bStart - 1].nextSibling ?? undefined
|
|
722
|
+
: b[bEnd - bStart]
|
|
723
|
+
: after;
|
|
724
|
+
while (bStart < bEnd)
|
|
725
|
+
insertBefore(parent, b[bStart++], node);
|
|
726
|
+
}
|
|
727
|
+
// 4. New list exhausted — remove remaining old nodes.
|
|
728
|
+
else if (bEnd === bStart) {
|
|
729
|
+
while (aStart < aEnd) {
|
|
730
|
+
if ((!map || !map.has(a[aStart])) && a[aStart].parentNode === parent)
|
|
731
|
+
parent.removeChild(a[aStart]);
|
|
732
|
+
aStart++;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// 5. Two-node swap (cross-endpoints match).
|
|
736
|
+
else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
|
|
737
|
+
const node = a[--aEnd].nextSibling;
|
|
738
|
+
insertBefore(parent, b[bStart++], a[aStart++].nextSibling);
|
|
739
|
+
insertBefore(parent, b[--bEnd], node);
|
|
740
|
+
a[aEnd] = a[aStart - 1];
|
|
741
|
+
}
|
|
742
|
+
// 6. Map-based fallback for complex reordering.
|
|
743
|
+
else {
|
|
744
|
+
if (!map) {
|
|
745
|
+
map = new Map();
|
|
746
|
+
let i = bStart;
|
|
747
|
+
while (i < bEnd)
|
|
748
|
+
map.set(b[i], i++);
|
|
749
|
+
}
|
|
750
|
+
const index = map.get(a[aStart]);
|
|
751
|
+
if (index != null) {
|
|
752
|
+
if (bStart < index && index < bEnd) {
|
|
753
|
+
let i = aStart, sequence = 1;
|
|
754
|
+
while (++i < aEnd && i < bEnd) {
|
|
755
|
+
if (!map.has(a[i]) || map.get(a[i]) !== index + sequence)
|
|
756
|
+
break;
|
|
757
|
+
sequence++;
|
|
758
|
+
}
|
|
759
|
+
if (sequence > index - bStart) {
|
|
760
|
+
const node = a[aStart];
|
|
761
|
+
while (bStart < index)
|
|
762
|
+
insertBefore(parent, b[bStart++], node);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
// Guard replaceChild: if a[aStart] was moved out by another effect, insert instead.
|
|
766
|
+
if (a[aStart].parentNode === parent) {
|
|
767
|
+
parent.replaceChild(b[bStart++], a[aStart++]);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
insertBefore(parent, b[bStart++], after);
|
|
771
|
+
aStart++;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
aStart++;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
if (a[aStart].parentNode === parent)
|
|
781
|
+
parent.removeChild(a[aStart]);
|
|
782
|
+
aStart++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return b;
|
|
787
|
+
}
|
|
788
|
+
// ============================================================================
|
|
789
|
+
// cleanChildren
|
|
790
|
+
// ============================================================================
|
|
791
|
+
export function cleanChildren(parent, current, marker) {
|
|
792
|
+
// console.log('cleanChildren', parent, current, marker);
|
|
793
|
+
if (current) {
|
|
794
|
+
if (Array.isArray(current)) {
|
|
795
|
+
for (let i = 0; i < current.length; i++) {
|
|
796
|
+
if (current[i].parentNode === parent)
|
|
797
|
+
parent.removeChild(current[i]);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else if (current.parentNode === parent) {
|
|
801
|
+
parent.removeChild(current);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// insertBefore
|
|
808
|
+
//
|
|
809
|
+
// DOM Runtime Guard: checks marker.parentNode === container before inserting.
|
|
810
|
+
// This prevents failures when markers are removed during route changes.
|
|
811
|
+
// ============================================================================
|
|
812
|
+
function insertBefore(parent, node, marker) {
|
|
813
|
+
if (hydration.active && node.parentNode === parent)
|
|
814
|
+
return;
|
|
815
|
+
if (marker) {
|
|
816
|
+
if (marker.parentNode === parent) {
|
|
817
|
+
parent.insertBefore(node, marker);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
parent.appendChild(node);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
parent.appendChild(node);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// ============================================================================
|
|
828
|
+
// Attributes & Properties
|
|
829
|
+
// ============================================================================
|
|
830
|
+
export function setProperty(element, name, value) {
|
|
831
|
+
if (value == null)
|
|
832
|
+
delete element[name];
|
|
833
|
+
else
|
|
834
|
+
element[name] = value;
|
|
835
|
+
}
|
|
836
|
+
export function setDynamicAttribute(element, name, accessor, isSVG = false) {
|
|
837
|
+
if (typeof accessor !== 'function') {
|
|
838
|
+
setAttribute(element, name, accessor, isSVG);
|
|
839
|
+
return () => { };
|
|
840
|
+
}
|
|
841
|
+
return createRenderEffect(() => {
|
|
842
|
+
const value = typeof accessor === 'function' ? accessor() : accessor;
|
|
843
|
+
setAttribute(element, name, value, isSVG);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
export function setDynamicProperty(element, name, accessor) {
|
|
847
|
+
if (typeof accessor !== 'function') {
|
|
848
|
+
setProperty(element, name, accessor);
|
|
849
|
+
return () => { };
|
|
850
|
+
}
|
|
851
|
+
return createRenderEffect(() => {
|
|
852
|
+
const value = typeof accessor === 'function' ? accessor() : accessor;
|
|
853
|
+
setProperty(element, name, value);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Set className. Accepts string | string[] | Record<string,boolean> | null.
|
|
858
|
+
*/
|
|
859
|
+
export function setClassName(element, value) {
|
|
860
|
+
if (value == null) {
|
|
861
|
+
element.removeAttribute('class');
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (typeof value === 'function') {
|
|
865
|
+
createEffect(() => {
|
|
866
|
+
// console.log('set class fn', value)
|
|
867
|
+
setClassName(element, value());
|
|
868
|
+
// element.setAttribute('class', value());
|
|
869
|
+
});
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// if (typeof value === 'function') {
|
|
873
|
+
// setClassName(element, value());
|
|
874
|
+
// return;
|
|
875
|
+
// }
|
|
876
|
+
if (typeof value === 'string') {
|
|
877
|
+
element.setAttribute('class', value);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (Array.isArray(value)) {
|
|
881
|
+
element.setAttribute('class', value.filter(Boolean).join(' '));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (typeof value === 'object') {
|
|
885
|
+
const classes = Object.keys(value)
|
|
886
|
+
.filter((key) => {
|
|
887
|
+
const v = value[key];
|
|
888
|
+
return typeof v === 'function' ? v() : Boolean(v);
|
|
889
|
+
})
|
|
890
|
+
.join(' ');
|
|
891
|
+
element.setAttribute('class', classes);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
element.setAttribute('class', String(value));
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Apply a `classList`-style map `{ 'a b': condition }` — toggling each class on
|
|
898
|
+
* the element. A condition may be a value or a reactive accessor; when
|
|
899
|
+
* `disposers` is provided, accessor entries are wired with a render effect so
|
|
900
|
+
* they update fine-grained (Solid-style `classList`).
|
|
901
|
+
*/
|
|
902
|
+
export function setClassList(element, value, disposers) {
|
|
903
|
+
if (!value || typeof value !== 'object')
|
|
904
|
+
return;
|
|
905
|
+
for (const name in value) {
|
|
906
|
+
const cond = value[name];
|
|
907
|
+
const names = name.split(/\s+/).filter(Boolean);
|
|
908
|
+
if (typeof cond === 'function' && disposers) {
|
|
909
|
+
disposers.push(createRenderEffect(() => {
|
|
910
|
+
const on = !!cond();
|
|
911
|
+
for (const n of names)
|
|
912
|
+
element.classList.toggle(n, on);
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
const on = !!(typeof cond === 'function' ? cond() : cond);
|
|
917
|
+
for (const n of names)
|
|
918
|
+
element.classList.toggle(n, on);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
export function setStyle(element, _value) {
|
|
923
|
+
let value = _value;
|
|
924
|
+
while (typeof value === 'function')
|
|
925
|
+
value = value();
|
|
926
|
+
if (value == null) {
|
|
927
|
+
element.removeAttribute('style');
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (typeof value === 'string') {
|
|
931
|
+
element.style.cssText = value;
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (typeof value === 'object') {
|
|
935
|
+
for (const key in value) {
|
|
936
|
+
const styleValue = value[key];
|
|
937
|
+
if (styleValue == null)
|
|
938
|
+
element.style[key] = '';
|
|
939
|
+
else
|
|
940
|
+
element.style[key] = styleValue;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// ============================================================================
|
|
945
|
+
// spread
|
|
946
|
+
// ============================================================================
|
|
947
|
+
export function spread(options) {
|
|
948
|
+
const { element, props, prevProps = EMPTY_OBJ, isSVG = false, skipChildren = false, } = options;
|
|
949
|
+
if (!element || !isDomElement(element)) {
|
|
950
|
+
console.error('[spread] Error: element is not a DOM Element!');
|
|
951
|
+
return () => { };
|
|
952
|
+
}
|
|
953
|
+
if (!props) {
|
|
954
|
+
console.warn('[spread] Warning: props is undefined!');
|
|
955
|
+
return () => { };
|
|
956
|
+
}
|
|
957
|
+
// Trust the element's real namespace over the compile-time isSVG flag: the codegen only marks
|
|
958
|
+
// some SVG elements (e.g. <svg>/<defs>) and misses nested ones (<linearGradient>, <stop>). For
|
|
959
|
+
// an actually-SVG element, a key like `x1` IS `in element` (read-only SVGAnimatedLength), so the
|
|
960
|
+
// property branch below would throw "Cannot set property x1". Detecting SVG by namespaceURI keeps
|
|
961
|
+
// those on the setAttribute path. (Matters under hydration, where we adopt the real, correctly
|
|
962
|
+
// namespaced server node.)
|
|
963
|
+
const elementIsSVG = isSVG || element.namespaceURI === SVG_NAMESPACE;
|
|
964
|
+
const disposers = [];
|
|
965
|
+
for (const key in props) {
|
|
966
|
+
if (skipChildren && key === 'children')
|
|
967
|
+
continue;
|
|
968
|
+
const value = props[key];
|
|
969
|
+
const prev = prevProps[key];
|
|
970
|
+
if (value === prev)
|
|
971
|
+
continue;
|
|
972
|
+
if (key === 'ref') {
|
|
973
|
+
handleRef(value, element);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (key === 'class' || key === 'className') {
|
|
977
|
+
if (typeof value === 'function') {
|
|
978
|
+
disposers.push(createRenderEffect(() => setClassName(element, value())));
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
setClassName(element, value);
|
|
982
|
+
}
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (key === 'classList') {
|
|
986
|
+
if (typeof value === 'function') {
|
|
987
|
+
disposers.push(createRenderEffect(() => setClassList(element, value())));
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
setClassList(element, value, disposers);
|
|
991
|
+
}
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (key === 'style') {
|
|
995
|
+
if (typeof value === 'function') {
|
|
996
|
+
disposers.push(createRenderEffect(() => setStyle(element, value())));
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
setStyle(element, value);
|
|
1000
|
+
}
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (key.startsWith('on')) {
|
|
1004
|
+
const eventName = key.slice(2).toLowerCase();
|
|
1005
|
+
if (prev)
|
|
1006
|
+
removeDelegatedEventListener(element, eventName);
|
|
1007
|
+
if (value)
|
|
1008
|
+
addDelegatedEventListener(element, eventName, value);
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (JSX_PROPERTIES.has(key) || (!elementIsSVG && key in element)) {
|
|
1012
|
+
disposers.push(setDynamicProperty(element, key, value));
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
disposers.push(setDynamicAttribute(element, key, value, elementIsSVG));
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
for (const key in prevProps) {
|
|
1019
|
+
if (!(key in props)) {
|
|
1020
|
+
if (key.startsWith('on')) {
|
|
1021
|
+
const eventName = key.slice(2).toLowerCase();
|
|
1022
|
+
removeDelegatedEventListener(element, eventName);
|
|
1023
|
+
}
|
|
1024
|
+
else if (JSX_PROPERTIES.has(key)) {
|
|
1025
|
+
setProperty(element, key, null);
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
element.removeAttribute(key);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return () => { disposers.forEach((d) => d()); };
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Apply props to an element.
|
|
1036
|
+
* For event handlers, stores as $$eventname property for delegation.
|
|
1037
|
+
*/
|
|
1038
|
+
export function applyProps(element, props, isSVG = false) {
|
|
1039
|
+
for (const key in props) {
|
|
1040
|
+
const val = props[key];
|
|
1041
|
+
if (key === 'ref') {
|
|
1042
|
+
handleRef(val, element);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (key === 'children')
|
|
1046
|
+
continue;
|
|
1047
|
+
if (key.startsWith('on') && key.length > 2) {
|
|
1048
|
+
const name = key.slice(2).toLowerCase();
|
|
1049
|
+
if (typeof val === 'function') {
|
|
1050
|
+
if (NON_DELEGATED.has(name)) {
|
|
1051
|
+
// Non-bubbling events (blur, focus, scroll…): store handler on the element
|
|
1052
|
+
// and attach a single stable forwarder listener. This avoids
|
|
1053
|
+
// addEventListener/removeEventListener churn when a reactive scope re-runs,
|
|
1054
|
+
// which would silently drop the listener via onCleanup before re-adding it.
|
|
1055
|
+
element[`$$${name}`] = val;
|
|
1056
|
+
const listenerKey = `__fx_${name}`;
|
|
1057
|
+
if (!element[listenerKey]) {
|
|
1058
|
+
const forwarder = (e) => { element[`$$${name}`]?.(e); };
|
|
1059
|
+
element.addEventListener(name, forwarder);
|
|
1060
|
+
element[listenerKey] = forwarder;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
element[`$$${name}`] = val;
|
|
1065
|
+
if (!delegatedSet.has(name))
|
|
1066
|
+
delegateEvents([name]);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (key === 'class' || key === 'className') {
|
|
1072
|
+
setClassName(element, val);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (key === 'classList') {
|
|
1076
|
+
if (typeof val === 'function')
|
|
1077
|
+
createRenderEffect(() => setClassList(element, val()));
|
|
1078
|
+
else
|
|
1079
|
+
setClassList(element, val);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (key === 'style') {
|
|
1083
|
+
setStyle(element, val);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (key === 'innerHTML') {
|
|
1087
|
+
// Accept a static string OR a reactive accessor. A function is wrapped in an effect so
|
|
1088
|
+
// updates apply; a plain value is set once. (The previous code always called val(), which
|
|
1089
|
+
// threw on a static string — wiping content during hydration.)
|
|
1090
|
+
if (typeof val === 'function') {
|
|
1091
|
+
const dispose = createEffect(() => {
|
|
1092
|
+
let v = val();
|
|
1093
|
+
while (typeof v === 'function')
|
|
1094
|
+
v = v();
|
|
1095
|
+
element.innerHTML = v;
|
|
1096
|
+
});
|
|
1097
|
+
onCleanup(() => dispose());
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
element.innerHTML = val;
|
|
1101
|
+
}
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
// Special case: <input>/<textarea> value — use DOM property assignment, not
|
|
1105
|
+
// setAttribute. Skip reactive updates while the element has focus so user
|
|
1106
|
+
// input is never interrupted and focus is never lost due to signal writes.
|
|
1107
|
+
if (key === 'value' && (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA')) {
|
|
1108
|
+
if (typeof val === 'function') {
|
|
1109
|
+
const dispose = createEffect(() => {
|
|
1110
|
+
let v = val();
|
|
1111
|
+
// console.log('apply inpit props', v, element);
|
|
1112
|
+
while (typeof v === 'function' && v.length === 0)
|
|
1113
|
+
v = v();
|
|
1114
|
+
// Only update if element is NOT currently focused — don't fight user input
|
|
1115
|
+
if (document.activeElement !== element) {
|
|
1116
|
+
element.value = v ?? '';
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
onCleanup(() => { dispose(); });
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
element.value = val ?? '';
|
|
1123
|
+
}
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
if (key === 'checked' && (element.tagName === 'INPUT')) {
|
|
1127
|
+
if (typeof val === 'function') {
|
|
1128
|
+
const dispose = createEffect(() => {
|
|
1129
|
+
let v = val();
|
|
1130
|
+
while (typeof v === 'function' && v.length === 0)
|
|
1131
|
+
v = v();
|
|
1132
|
+
// Only update if element is NOT currently focused — don't fight user input
|
|
1133
|
+
if (document.activeElement !== element) {
|
|
1134
|
+
element.checked = v ?? '';
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
onCleanup(() => { dispose(); });
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
element.checked = val ?? '';
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
// Special case: <select value=...> — must assign .value as DOM property
|
|
1145
|
+
// AFTER options are in the DOM. We defer the initial run via queueMicrotask
|
|
1146
|
+
// (children/options are inserted synchronously after applyProps returns).
|
|
1147
|
+
if (key === 'value' && element.tagName === 'SELECT') {
|
|
1148
|
+
if (typeof val === 'function') {
|
|
1149
|
+
let firstRun = true;
|
|
1150
|
+
const dispose = createEffect(() => {
|
|
1151
|
+
let v = val();
|
|
1152
|
+
while (typeof v === 'function' && v.length === 0)
|
|
1153
|
+
v = v();
|
|
1154
|
+
if (firstRun) {
|
|
1155
|
+
firstRun = false;
|
|
1156
|
+
queueMicrotask(() => { element.value = v ?? ''; });
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
element.value = v ?? '';
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
onCleanup(() => { dispose(); });
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
queueMicrotask(() => { element.value = val ?? ''; });
|
|
1166
|
+
}
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof val === 'function') {
|
|
1170
|
+
const dispose = createEffect(() => {
|
|
1171
|
+
let v = val();
|
|
1172
|
+
while ((typeof v === 'function' && v.length === 0))
|
|
1173
|
+
v = v();
|
|
1174
|
+
// if(key==='opened'){
|
|
1175
|
+
// console.log('opened is', val, element,v)
|
|
1176
|
+
// }
|
|
1177
|
+
applyElementProp(element, key, v, isSVG);
|
|
1178
|
+
});
|
|
1179
|
+
onCleanup(() => {
|
|
1180
|
+
dispose();
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
else {
|
|
1184
|
+
applyElementProp(element, key, val, isSVG);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Merge several (possibly reactive) prop sources into one, last-source-wins — but a later
|
|
1190
|
+
* source's `undefined` does NOT clobber an earlier defined value; it falls through to it. This
|
|
1191
|
+
* is what makes `mergeProps(defaults, props)` work, and why the compiler can emit
|
|
1192
|
+
* `mergeProps(...spreads, baseProps)` for JSX spreads (`<div {...a} {...b} />`) without a missing
|
|
1193
|
+
* `b.x` wiping `a.x`.
|
|
1194
|
+
*
|
|
1195
|
+
* Like Solid's mergeProps (and `splitProps` above), the result is a Proxy that resolves each key
|
|
1196
|
+
* LAZILY off the sources — getter/signal-backed props stay live (re-read on every access, not
|
|
1197
|
+
* snapshotted), and the key set is the union across all sources (so dynamic prop objects work).
|
|
1198
|
+
*/
|
|
1199
|
+
export function mergeProps(...sources) {
|
|
1200
|
+
// Last defined (non-undefined) value wins; scan sources from the end so a later `undefined`
|
|
1201
|
+
// falls through to an earlier source. Reads `source[key]` live each call — no snapshot.
|
|
1202
|
+
const resolve = (key) => {
|
|
1203
|
+
for (let i = sources.length - 1; i >= 0; i--) {
|
|
1204
|
+
const s = sources[i];
|
|
1205
|
+
if (s && key in s && s[key] !== undefined)
|
|
1206
|
+
return s[key];
|
|
1207
|
+
}
|
|
1208
|
+
return undefined;
|
|
1209
|
+
};
|
|
1210
|
+
const hasKey = (key) => {
|
|
1211
|
+
for (const s of sources)
|
|
1212
|
+
if (s && key in s)
|
|
1213
|
+
return true;
|
|
1214
|
+
return false;
|
|
1215
|
+
};
|
|
1216
|
+
return new Proxy({}, {
|
|
1217
|
+
get: (_, key) => resolve(key),
|
|
1218
|
+
has: (_, key) => hasKey(key),
|
|
1219
|
+
ownKeys: () => {
|
|
1220
|
+
const keys = new Set();
|
|
1221
|
+
for (const s of sources)
|
|
1222
|
+
if (s)
|
|
1223
|
+
for (const k of Reflect.ownKeys(s))
|
|
1224
|
+
keys.add(k);
|
|
1225
|
+
return [...keys];
|
|
1226
|
+
},
|
|
1227
|
+
getOwnPropertyDescriptor: (_, key) => hasKey(key)
|
|
1228
|
+
? { enumerable: true, configurable: true, get: () => resolve(key) }
|
|
1229
|
+
: undefined,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Split a (possibly reactive) props object into groups of keys plus a `rest`,
|
|
1234
|
+
* preserving reactivity — each returned object is a Proxy whose `get` forwards
|
|
1235
|
+
* to the original `props`, so a getter/signal-backed prop stays live.
|
|
1236
|
+
*
|
|
1237
|
+
* const [local, rest] = splitProps(props, ['class', 'id']);
|
|
1238
|
+
* // local.class tracks props.class; rest has everything else.
|
|
1239
|
+
*
|
|
1240
|
+
* Mirrors Solid's splitProps. Descriptors are exposed as accessors (not eager
|
|
1241
|
+
* values) so spreading a group stays reactive, and forced `configurable` avoids
|
|
1242
|
+
* Proxy invariant errors when `props` has non-configurable descriptors.
|
|
1243
|
+
*/
|
|
1244
|
+
export function splitProps(props, ...keys) {
|
|
1245
|
+
const taken = new Set();
|
|
1246
|
+
for (const group of keys)
|
|
1247
|
+
for (const k of group)
|
|
1248
|
+
taken.add(k);
|
|
1249
|
+
const descriptor = (p) => ({
|
|
1250
|
+
enumerable: true,
|
|
1251
|
+
configurable: true,
|
|
1252
|
+
get: () => props[p],
|
|
1253
|
+
});
|
|
1254
|
+
const groups = keys.map((group) => {
|
|
1255
|
+
const keySet = new Set(group);
|
|
1256
|
+
return new Proxy({}, {
|
|
1257
|
+
get: (_, p) => (keySet.has(p) ? props[p] : undefined),
|
|
1258
|
+
has: (_, p) => keySet.has(p) && p in props,
|
|
1259
|
+
ownKeys: () => Array.from(keySet).filter((k) => k in props),
|
|
1260
|
+
getOwnPropertyDescriptor: (_, p) => keySet.has(p) && p in props ? descriptor(p) : undefined,
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
const rest = new Proxy({}, {
|
|
1264
|
+
get: (_, p) => (taken.has(p) ? undefined : props[p]),
|
|
1265
|
+
has: (_, p) => !taken.has(p) && p in props,
|
|
1266
|
+
ownKeys: () => Reflect.ownKeys(props).filter((k) => !taken.has(k)),
|
|
1267
|
+
getOwnPropertyDescriptor: (_, p) => !taken.has(p) && p in props ? descriptor(p) : undefined,
|
|
1268
|
+
});
|
|
1269
|
+
return [...groups, rest];
|
|
1270
|
+
}
|
|
1271
|
+
// ============================================================================
|
|
1272
|
+
// Refs
|
|
1273
|
+
// ============================================================================
|
|
1274
|
+
export function handleRef(ref, element) {
|
|
1275
|
+
if (!ref)
|
|
1276
|
+
return;
|
|
1277
|
+
// Swapped-argument guard
|
|
1278
|
+
if (isDomNode(ref)) {
|
|
1279
|
+
const actualRef = element;
|
|
1280
|
+
const actualElement = ref;
|
|
1281
|
+
if (!actualRef)
|
|
1282
|
+
return;
|
|
1283
|
+
if (typeof actualRef === 'function')
|
|
1284
|
+
actualRef(actualElement);
|
|
1285
|
+
else if (typeof actualRef === 'object' && 'current' in actualRef)
|
|
1286
|
+
actualRef.current = actualElement;
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (typeof ref === 'function')
|
|
1290
|
+
ref(element);
|
|
1291
|
+
else if (typeof ref === 'object' && 'current' in ref)
|
|
1292
|
+
ref.current = element;
|
|
1293
|
+
}
|
|
1294
|
+
// ============================================================================
|
|
1295
|
+
// Lit integration
|
|
1296
|
+
// ============================================================================
|
|
1297
|
+
let litRenderFn = null;
|
|
1298
|
+
export function registerLitRender(render) {
|
|
1299
|
+
litRenderFn = render;
|
|
1300
|
+
}
|
|
1301
|
+
export function renderLitToNodes(template) {
|
|
1302
|
+
const nodes = [];
|
|
1303
|
+
if (!litRenderFn) {
|
|
1304
|
+
console.warn('Lit render not registered. Call registerLitRender first.');
|
|
1305
|
+
return nodes;
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
const tempContainer = document.createElement('div');
|
|
1309
|
+
litRenderFn(template, tempContainer);
|
|
1310
|
+
while (tempContainer.firstChild) {
|
|
1311
|
+
nodes.push(tempContainer.firstChild);
|
|
1312
|
+
tempContainer.removeChild(tempContainer.firstChild);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
catch (e) {
|
|
1316
|
+
console.warn('Could not render Lit template:', e);
|
|
1317
|
+
}
|
|
1318
|
+
return nodes;
|
|
1319
|
+
}
|
|
1320
|
+
// ============================================================================
|
|
1321
|
+
// Portal
|
|
1322
|
+
// ============================================================================
|
|
1323
|
+
export function createPortal(children, element, props) {
|
|
1324
|
+
// 1. Define the target (default to document.body)
|
|
1325
|
+
const container = element || document.body;
|
|
1326
|
+
// 2. Use the 'children' helper to resolve the reactive content
|
|
1327
|
+
// This ensures that if children are a function or signal, they are tracked.
|
|
1328
|
+
const resolved = readChildren(() => children);
|
|
1329
|
+
const cleanups = [];
|
|
1330
|
+
batch(() => {
|
|
1331
|
+
const dispose = createRenderEffect(() => {
|
|
1332
|
+
const nodes = resolved();
|
|
1333
|
+
// console.log('resolved', nodes)
|
|
1334
|
+
// 3. Handle the insertion
|
|
1335
|
+
if (Array.isArray(nodes)) {
|
|
1336
|
+
nodes.forEach((node) => container.appendChild(node));
|
|
1337
|
+
// 4. Cleanup: Remove nodes when the effect re-runs or component unmounts
|
|
1338
|
+
cleanups.push(() => {
|
|
1339
|
+
nodes.forEach((node) => {
|
|
1340
|
+
if (node.parentNode === container) {
|
|
1341
|
+
container.removeChild(node);
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
else if (isDomNode(nodes)) {
|
|
1347
|
+
container.appendChild(nodes);
|
|
1348
|
+
cleanups.push(() => {
|
|
1349
|
+
if (nodes.parentNode === container) {
|
|
1350
|
+
container.removeChild(nodes);
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
cleanups.push(dispose);
|
|
1356
|
+
// 5. Return the cleanup function
|
|
1357
|
+
onCleanup(() => {
|
|
1358
|
+
cleanups.forEach((fn) => fn());
|
|
1359
|
+
// dispose();
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
// Portal returns nothing to its original parent's DOM location
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
// ============================================================================
|
|
1366
|
+
// Misc utilities
|
|
1367
|
+
// ============================================================================
|
|
1368
|
+
export function isNativeElement(tag) {
|
|
1369
|
+
return typeof tag === 'string' && tag.toLowerCase() === tag;
|
|
1370
|
+
}
|
|
1371
|
+
export function isSVGElement(tag) {
|
|
1372
|
+
return (tag === 'svg' || tag === 'path' || tag === 'circle' || tag === 'rect' ||
|
|
1373
|
+
tag === 'line' || tag === 'polygon' || tag === 'polyline' || tag === 'ellipse' ||
|
|
1374
|
+
tag === 'g' || tag === 'defs' || tag === 'clipPath' || tag === 'text');
|
|
1375
|
+
}
|
|
1376
|
+
export function escapeHTML(str) {
|
|
1377
|
+
return str
|
|
1378
|
+
.replace(/&/g, '&')
|
|
1379
|
+
.replace(/</g, '<')
|
|
1380
|
+
.replace(/>/g, '>')
|
|
1381
|
+
.replace(/"/g, '"')
|
|
1382
|
+
.replace(/'/g, ''');
|
|
1383
|
+
}
|
|
1384
|
+
export function isPromise(v) {
|
|
1385
|
+
return ((v != null &&
|
|
1386
|
+
(typeof v === 'object' || typeof v === 'function') &&
|
|
1387
|
+
typeof v.then === 'function') ||
|
|
1388
|
+
v instanceof Promise);
|
|
1389
|
+
}
|
|
1390
|
+
// ============================================================================
|
|
1391
|
+
// isAccessor — deprecated, kept for backward compat
|
|
1392
|
+
// ============================================================================
|
|
1393
|
+
export function isAccessor(value) {
|
|
1394
|
+
return typeof value === 'function' && value.length === 0 && isSignal(value);
|
|
1395
|
+
}
|
|
1396
|
+
/** Render a JSX element into a DOM container (nested arrays, thunks, promises, and node types). */
|
|
1397
|
+
export function render(element, container, context) {
|
|
1398
|
+
if (!container) {
|
|
1399
|
+
throw new Error('Container element is required');
|
|
1400
|
+
}
|
|
1401
|
+
let rendererDispose;
|
|
1402
|
+
let disposeRoot;
|
|
1403
|
+
const disposers = [];
|
|
1404
|
+
// Wrap in createRoot to establish reactive ownership tree.
|
|
1405
|
+
// This is critical for context propagation (Provider/useContext).
|
|
1406
|
+
createRoot((dispose) => {
|
|
1407
|
+
batch(() => {
|
|
1408
|
+
// Clear container (but not when hydrating — we reuse the server DOM).
|
|
1409
|
+
if (!hydration.active) {
|
|
1410
|
+
container.textContent = '';
|
|
1411
|
+
}
|
|
1412
|
+
// console.log('inserting', element);
|
|
1413
|
+
insert(container, element);
|
|
1414
|
+
return;
|
|
1415
|
+
});
|
|
1416
|
+
disposeRoot = dispose;
|
|
1417
|
+
});
|
|
1418
|
+
// Return cleanup function
|
|
1419
|
+
return () => {
|
|
1420
|
+
disposeRoot?.();
|
|
1421
|
+
rendererDispose?.();
|
|
1422
|
+
disposers.forEach((d) => d());
|
|
1423
|
+
container.textContent = '';
|
|
1424
|
+
};
|
|
1425
|
+
}
|