what-core 0.10.0 → 0.11.1
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/dist/chunk-D5YDPQ57.min.js +1 -0
- package/dist/chunk-O3SKPRTY.min.js +0 -1
- package/dist/chunk-W33M3HL5.min.js +1 -0
- package/dist/index.min.js +6 -7
- package/dist/jsx-dev-runtime.min.js +0 -1
- package/dist/jsx-runtime.min.js +0 -1
- package/dist/render.min.js +1 -2
- package/dist/testing.min.js +1 -2
- package/package.json +2 -2
- package/render.d.ts +18 -0
- package/src/agent-context.js +3 -2
- package/src/dom.js +16 -0
- package/src/guardrails.js +17 -46
- package/src/reactive.js +42 -4
- package/src/render.js +159 -55
- package/dist/a11y.js +0 -440
- package/dist/animation.js +0 -548
- package/dist/chunk-AW3BAPIK.js +0 -1685
- package/dist/chunk-AW3BAPIK.js.map +0 -7
- package/dist/chunk-AZP2EOGX.js +0 -188
- package/dist/chunk-AZP2EOGX.js.map +0 -7
- package/dist/chunk-F2HUXI22.js +0 -1675
- package/dist/chunk-F2HUXI22.js.map +0 -7
- package/dist/chunk-KBM6CWG4.min.js +0 -2
- package/dist/chunk-KBM6CWG4.min.js.map +0 -7
- package/dist/chunk-KL7TNUIU.min.js +0 -2
- package/dist/chunk-KL7TNUIU.min.js.map +0 -7
- package/dist/chunk-L6XOF7P4.min.js +0 -2
- package/dist/chunk-L6XOF7P4.min.js.map +0 -7
- package/dist/chunk-M7UEET5O.js +0 -1323
- package/dist/chunk-M7UEET5O.js.map +0 -7
- package/dist/chunk-O3SKPRTY.min.js.map +0 -7
- package/dist/chunk-RN6QIBWL.min.js +0 -2
- package/dist/chunk-RN6QIBWL.min.js.map +0 -7
- package/dist/chunk-VMTTYB4L.min.js +0 -2
- package/dist/chunk-VMTTYB4L.min.js.map +0 -7
- package/dist/chunk-VP4WLF5A.js +0 -1323
- package/dist/chunk-VP4WLF5A.js.map +0 -7
- package/dist/chunk-YA3W4XKH.js +0 -1323
- package/dist/chunk-YA3W4XKH.js.map +0 -7
- package/dist/compiler.js +0 -1799
- package/dist/compiler.js.map +0 -7
- package/dist/compiler.min.js +0 -2
- package/dist/compiler.min.js.map +0 -7
- package/dist/components.js +0 -229
- package/dist/data.js +0 -638
- package/dist/devtools.js +0 -10
- package/dist/devtools.js.map +0 -7
- package/dist/devtools.min.js +0 -2
- package/dist/devtools.min.js.map +0 -7
- package/dist/dom.js +0 -439
- package/dist/form.js +0 -509
- package/dist/h.js +0 -152
- package/dist/head.js +0 -51
- package/dist/helpers.js +0 -140
- package/dist/hooks.js +0 -210
- package/dist/index.js +0 -3578
- package/dist/index.js.map +0 -7
- package/dist/index.min.js.map +0 -7
- package/dist/jsx-dev-runtime.js +0 -23
- package/dist/jsx-dev-runtime.js.map +0 -7
- package/dist/jsx-dev-runtime.min.js.map +0 -7
- package/dist/jsx-runtime.js +0 -21
- package/dist/jsx-runtime.js.map +0 -7
- package/dist/jsx-runtime.min.js.map +0 -7
- package/dist/reactive.js +0 -432
- package/dist/render.js +0 -41
- package/dist/render.js.map +0 -7
- package/dist/render.min.js.map +0 -7
- package/dist/scheduler.js +0 -246
- package/dist/skeleton.js +0 -363
- package/dist/store.js +0 -83
- package/dist/testing.js +0 -439
- package/dist/testing.js.map +0 -7
- package/dist/testing.min.js.map +0 -7
- package/dist/what.js +0 -117
package/src/guardrails.js
CHANGED
|
@@ -10,7 +10,6 @@ import { createWhatError, collectError } from './errors.js';
|
|
|
10
10
|
|
|
11
11
|
const guardrails = {
|
|
12
12
|
signalReadDetection: true,
|
|
13
|
-
effectCycleDetection: true,
|
|
14
13
|
componentNaming: true,
|
|
15
14
|
importValidation: true,
|
|
16
15
|
};
|
|
@@ -25,16 +24,19 @@ export function getGuardrailConfig() {
|
|
|
25
24
|
|
|
26
25
|
// --- Guardrail 1: Signal Read Detection ---
|
|
27
26
|
// Detect when a signal function reference is used where its value was intended.
|
|
28
|
-
// This catches
|
|
27
|
+
// This catches patterns like `Total: ${count}` or `count > 5` (should be count()).
|
|
29
28
|
//
|
|
30
|
-
// At runtime, we
|
|
31
|
-
// and warn that it should be called.
|
|
29
|
+
// At runtime, we detect this when a signal is coerced to a string or number
|
|
30
|
+
// (via toString/valueOf) and warn that it should be called.
|
|
31
|
+
//
|
|
32
|
+
// Wiring: what-devtools calls this for every signal it registers (dev only),
|
|
33
|
+
// so any app with devtools installed gets the guardrail automatically.
|
|
34
|
+
// It can also be called manually: installSignalReadGuardrail(sig, 'name').
|
|
32
35
|
|
|
33
36
|
export function installSignalReadGuardrail(signalFn, debugName) {
|
|
34
37
|
if (!__DEV__ || !guardrails.signalReadDetection) return signalFn;
|
|
35
38
|
|
|
36
39
|
// Override toString to catch template literal coercion
|
|
37
|
-
const originalToString = signalFn.toString;
|
|
38
40
|
signalFn.toString = function () {
|
|
39
41
|
const err = createWhatError('MISSING_SIGNAL_READ', {
|
|
40
42
|
signalName: debugName || '(unnamed)',
|
|
@@ -58,47 +60,16 @@ export function installSignalReadGuardrail(signalFn, debugName) {
|
|
|
58
60
|
return signalFn;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!effectWriteTracking.has(effectRef)) {
|
|
71
|
-
effectWriteTracking.set(effectRef, new Set());
|
|
72
|
-
}
|
|
73
|
-
effectWriteTracking.get(effectRef).add(signalDebugName);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function checkEffectCycle(effectRef, readSignals) {
|
|
77
|
-
if (!__DEV__ || !guardrails.effectCycleDetection) return null;
|
|
78
|
-
|
|
79
|
-
const writes = effectWriteTracking.get(effectRef);
|
|
80
|
-
if (!writes || writes.size === 0) return null;
|
|
81
|
-
|
|
82
|
-
const overlapping = [];
|
|
83
|
-
for (const sigName of readSignals) {
|
|
84
|
-
if (writes.has(sigName)) {
|
|
85
|
-
overlapping.push(sigName);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (overlapping.length > 0) {
|
|
90
|
-
const err = createWhatError('INFINITE_EFFECT', {
|
|
91
|
-
effectName: effectRef.fn?.name || '(anonymous)',
|
|
92
|
-
signalName: overlapping.join(', '),
|
|
93
|
-
});
|
|
94
|
-
collectError(err);
|
|
95
|
-
return err;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
63
|
+
// NOTE: an "effect cycle detection" guardrail (trackEffectSignalWrite /
|
|
64
|
+
// checkEffectCycle) used to live here, exported but never called by anything
|
|
65
|
+
// — it was removed in v0.11 rather than shipped as a dead API. Reviving it
|
|
66
|
+
// requires hooks inside reactive.js: (1) a call on every signal WRITE while
|
|
67
|
+
// an effect is the active listener (to attribute writes to effects), and
|
|
68
|
+
// (2) a post-run callback with the effect's tracked read-set (to intersect
|
|
69
|
+
// reads with writes). reactive.js already detects runaway cascades at flush
|
|
70
|
+
// time (iteration cap), which covers the practical failure mode.
|
|
100
71
|
|
|
101
|
-
// --- Guardrail
|
|
72
|
+
// --- Guardrail 2: Component Naming ---
|
|
102
73
|
// Warn if a component function is not PascalCase.
|
|
103
74
|
|
|
104
75
|
export function checkComponentName(name) {
|
|
@@ -119,7 +90,7 @@ function capitalize(str) {
|
|
|
119
90
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
120
91
|
}
|
|
121
92
|
|
|
122
|
-
// --- Guardrail
|
|
93
|
+
// --- Guardrail 3: Import Validation ---
|
|
123
94
|
// Verify that all named imports from 'what-framework' are valid exports.
|
|
124
95
|
|
|
125
96
|
const VALID_EXPORTS = new Set([
|
package/src/reactive.js
CHANGED
|
@@ -7,10 +7,26 @@
|
|
|
7
7
|
// - Ownership tree: createRoot children auto-dispose when parent disposes
|
|
8
8
|
// - Performance: cached levels, lazy sort, fast-path notify, minimal allocation
|
|
9
9
|
|
|
10
|
-
// Dev-mode flag —
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Dev-mode flag — `if (__DEV__)` branches dead-code-eliminate when this is false.
|
|
11
|
+
//
|
|
12
|
+
// Resolution order (first definitive signal wins; PRODUCTION-SAFE default):
|
|
13
|
+
// 1. globalThis.__WHAT_DEV__ — explicit boolean override for browser tooling
|
|
14
|
+
// (e.g. the playground can force dev mode without a bundler env).
|
|
15
|
+
// 2. import.meta.env.DEV — Vite & modern bundlers statically replace this,
|
|
16
|
+
// so production builds fully strip every dev branch.
|
|
17
|
+
// 3. process.env.NODE_ENV — Node / webpack / esbuild `define`.
|
|
18
|
+
// 4. false — a raw browser bundle with NO build signal must
|
|
19
|
+
// default to PRODUCTION. (Previously defaulted to `true`, so EVERY browser
|
|
20
|
+
// bundle ran in dev mode: perf/size tax + the spurious internal
|
|
21
|
+
// `template()` XSS warning surfacing on production sites. AUDIT 2026-06-10.)
|
|
22
|
+
export const __DEV__ =
|
|
23
|
+
(typeof globalThis !== 'undefined' && typeof globalThis.__WHAT_DEV__ === 'boolean')
|
|
24
|
+
? globalThis.__WHAT_DEV__
|
|
25
|
+
: (import.meta && import.meta.env)
|
|
26
|
+
? !!import.meta.env.DEV
|
|
27
|
+
: (typeof process !== 'undefined' && process.env)
|
|
28
|
+
? process.env.NODE_ENV !== 'production'
|
|
29
|
+
: false;
|
|
14
30
|
|
|
15
31
|
// DevTools hooks — set by what-devtools when installed.
|
|
16
32
|
// These are no-ops in production (dead-code eliminated with __DEV__).
|
|
@@ -287,6 +303,8 @@ function _updateLevel(e) {
|
|
|
287
303
|
// Runs a function, auto-tracking signal reads. Re-runs when deps change.
|
|
288
304
|
// Returns a dispose function.
|
|
289
305
|
|
|
306
|
+
const _noopDispose = () => {};
|
|
307
|
+
|
|
290
308
|
export function effect(fn, opts) {
|
|
291
309
|
const e = _createEffect(fn);
|
|
292
310
|
e._level = 1;
|
|
@@ -303,6 +321,26 @@ export function effect(fn, opts) {
|
|
|
303
321
|
_updateLevel(e);
|
|
304
322
|
// Mark as stable after first run — subsequent re-runs skip cleanup/re-subscribe
|
|
305
323
|
if (opts?.stable) e._stable = true;
|
|
324
|
+
|
|
325
|
+
// Zero-dependency release (SPRINT v0.11 C4): an effect that tracked zero
|
|
326
|
+
// signals on its first run can never be notified again — re-tracking only
|
|
327
|
+
// happens during a re-run, and a re-run requires a notification from a
|
|
328
|
+
// subscribed signal. The compiler conservatively wraps destructured props /
|
|
329
|
+
// imported accessors in effects; when those turn out to be plain values the
|
|
330
|
+
// effect is one-shot. If it also registered no cleanup, release it now:
|
|
331
|
+
// no dispose closure, no owner registration, nothing retained.
|
|
332
|
+
// - Effects that returned a cleanup keep full registration so the cleanup
|
|
333
|
+
// still runs on owner disposal.
|
|
334
|
+
// - onCleanup() callbacks register with currentRoot directly (not with the
|
|
335
|
+
// effect), so they are unaffected by this release.
|
|
336
|
+
// - untrack()/peek() reads inside the fn produce zero deps by design — the
|
|
337
|
+
// effect could never re-fire anyway, so releasing is safe.
|
|
338
|
+
if (e.deps.length === 0 && e._cleanup === null) {
|
|
339
|
+
e.disposed = true;
|
|
340
|
+
if (__DEV__ && __devtools) __devtools.onEffectDispose(e);
|
|
341
|
+
return _noopDispose;
|
|
342
|
+
}
|
|
343
|
+
|
|
306
344
|
const dispose = () => _disposeEffect(e);
|
|
307
345
|
// Register with current root for automatic cleanup
|
|
308
346
|
if (currentRoot) {
|
package/src/render.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
// Solid-style rendering: components run once, signals create individual DOM effects.
|
|
3
3
|
// No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
|
|
4
4
|
|
|
5
|
-
import { effect, untrack, createRoot, _createItemScope, signal, __DEV__ } from './reactive.js';
|
|
5
|
+
import { effect, untrack, createRoot, _createItemScope, signal, memo, __DEV__ } from './reactive.js';
|
|
6
6
|
import { createDOM, disposeTree, getCurrentComponent, getComponentStack, _setSelectValue } from './dom.js';
|
|
7
7
|
export { effect, untrack };
|
|
8
|
+
// Re-export memo for compiled output (branch memoization: the compiler emits
|
|
9
|
+
// _$memo(() => cond) so conditional branches only re-create DOM when the
|
|
10
|
+
// condition value actually changes, not on every dependency write).
|
|
11
|
+
export { memo };
|
|
8
12
|
|
|
9
13
|
// --- Generic text insertion hook ---
|
|
10
14
|
// External text engines (e.g., what-text) register a callback here via
|
|
@@ -173,41 +177,44 @@ export function insert(parent, child, marker) {
|
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
if (typeof child === 'function') {
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (m) parent.insertBefore(textNode, m);
|
|
185
|
-
else parent.appendChild(textNode);
|
|
186
|
-
if (_onTextInsert) _onTextInsert(parent, String(first));
|
|
187
|
-
let current = textNode;
|
|
188
|
-
let isTextFastPath = true;
|
|
189
|
-
effect(() => {
|
|
190
|
-
const val = child();
|
|
191
|
-
const vt = typeof val;
|
|
192
|
-
if (isTextFastPath && (vt === 'string' || vt === 'number')) {
|
|
193
|
-
// Fast path: still text — update data directly (no allocations)
|
|
194
|
-
const str = String(val);
|
|
195
|
-
if (textNode.data !== str) textNode.data = str;
|
|
196
|
-
if (_onTextInsert) _onTextInsert(parent, str);
|
|
197
|
-
} else {
|
|
198
|
-
// Type changed — fall back to full reconcile
|
|
199
|
-
isTextFastPath = false;
|
|
200
|
-
current = reconcileInsert(parent, val, current, m);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
return textNode;
|
|
204
|
-
}
|
|
205
|
-
// General path for non-text reactive children (first value was null/vnode/array).
|
|
206
|
-
// Let the effect handle both the initial insert and subsequent updates to avoid
|
|
207
|
-
// double-evaluating child() (which would create components twice on mount).
|
|
180
|
+
// Single-evaluation mount: child() is evaluated exactly ONCE at mount,
|
|
181
|
+
// inside the effect (so signal reads are tracked). The first run decides
|
|
182
|
+
// between the text fast path (direct textNode.data updates, zero
|
|
183
|
+
// allocations) and the general reconcile path. Previously the first
|
|
184
|
+
// evaluation happened outside the effect to pick the path, then the
|
|
185
|
+
// effect's first run re-evaluated child() — creating components twice
|
|
186
|
+
// on mount for non-text children. (SPRINT v0.11 C3)
|
|
187
|
+
const m = marker || null;
|
|
208
188
|
let current = null;
|
|
189
|
+
let textNode = null; // non-null while on the text fast path
|
|
190
|
+
let mounted = false;
|
|
209
191
|
effect(() => {
|
|
210
|
-
|
|
192
|
+
const val = child();
|
|
193
|
+
const vt = typeof val;
|
|
194
|
+
if (!mounted) {
|
|
195
|
+
// First run — mount
|
|
196
|
+
mounted = true;
|
|
197
|
+
if (vt === 'string' || vt === 'number') {
|
|
198
|
+
textNode = document.createTextNode(String(val));
|
|
199
|
+
if (m) parent.insertBefore(textNode, m);
|
|
200
|
+
else parent.appendChild(textNode);
|
|
201
|
+
if (_onTextInsert) _onTextInsert(parent, String(val));
|
|
202
|
+
current = textNode;
|
|
203
|
+
} else {
|
|
204
|
+
current = reconcileInsert(parent, val, null, m);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (textNode !== null && (vt === 'string' || vt === 'number')) {
|
|
209
|
+
// Fast path: still text — update data directly (no allocations)
|
|
210
|
+
const str = String(val);
|
|
211
|
+
if (textNode.data !== str) textNode.data = str;
|
|
212
|
+
if (_onTextInsert) _onTextInsert(parent, str);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Type changed (or never was text) — full reconcile
|
|
216
|
+
textNode = null;
|
|
217
|
+
current = reconcileInsert(parent, val, current, m);
|
|
211
218
|
});
|
|
212
219
|
return current;
|
|
213
220
|
}
|
|
@@ -417,10 +424,17 @@ export function mapArray(source, mapFn, options) {
|
|
|
417
424
|
|
|
418
425
|
effect(() => {
|
|
419
426
|
const newItems = source() || [];
|
|
427
|
+
// Resolve the LIVE parent from the end marker each run. When this inserter
|
|
428
|
+
// is mounted at a fragment-as-root (`<>{items().map(...)}</>`), createDOM
|
|
429
|
+
// calls it against a throwaway DocumentFragment which is then appended to
|
|
430
|
+
// the real container — the marker (and existing rows) move with it, so the
|
|
431
|
+
// captured `parent` goes stale. endMarker.parentNode always reflects where
|
|
432
|
+
// the list currently lives. Falls back to the captured parent pre-mount.
|
|
433
|
+
const liveParent = endMarker.parentNode || parent;
|
|
420
434
|
if (keyFn) {
|
|
421
|
-
reconcileKeyed(
|
|
435
|
+
reconcileKeyed(liveParent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState);
|
|
422
436
|
} else {
|
|
423
|
-
reconcileList(
|
|
437
|
+
reconcileList(liveParent, endMarker, items, newItems, mappedNodes, disposeFns, mapFn);
|
|
424
438
|
}
|
|
425
439
|
// Save a snapshot of items for next diff. Use slice() to defend against
|
|
426
440
|
// in-place mutation, but skip for empty arrays (common clear case).
|
|
@@ -1244,12 +1258,9 @@ export function spread(el, props) {
|
|
|
1244
1258
|
else el.className = cls;
|
|
1245
1259
|
});
|
|
1246
1260
|
} else if (key === 'style' && typeof value() === 'object') {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
el.style[prop] = styles[prop] ?? '';
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1261
|
+
// Route through setStyle so stale object keys are cleared between
|
|
1262
|
+
// re-evaluations (el._lastStyleObj diffing).
|
|
1263
|
+
el._propEffects[key] = effect(() => { setStyle(el, value()); });
|
|
1253
1264
|
} else {
|
|
1254
1265
|
el._propEffects[key] = effect(() => { setProp(el, key, value()); });
|
|
1255
1266
|
}
|
|
@@ -1332,13 +1343,8 @@ export function setProp(el, key, value) {
|
|
|
1332
1343
|
}
|
|
1333
1344
|
}
|
|
1334
1345
|
} else if (key === 'style') {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
} else if (typeof value === 'object') {
|
|
1338
|
-
for (const prop in value) {
|
|
1339
|
-
el.style[prop] = value[prop] ?? '';
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1346
|
+
// Delegate to setStyle so the object form clears stale keys (el._lastStyleObj).
|
|
1347
|
+
setStyle(el, value);
|
|
1342
1348
|
} else if (key.startsWith('data-') || key.startsWith('aria-')) {
|
|
1343
1349
|
el.setAttribute(key, value);
|
|
1344
1350
|
} else if (typeof value === 'boolean') {
|
|
@@ -1355,6 +1361,99 @@ export function setProp(el, key, value) {
|
|
|
1355
1361
|
}
|
|
1356
1362
|
}
|
|
1357
1363
|
|
|
1364
|
+
// --- Specialized attribute setters (SPRINT v0.11 C2) ---
|
|
1365
|
+
// The compiler statically knows most attribute names, so it emits direct calls
|
|
1366
|
+
// to these monomorphic helpers instead of routing everything through the
|
|
1367
|
+
// generic setProp() dispatcher (which re-checks ref/key/url/class/style/...
|
|
1368
|
+
// string-compares on every reactive update). setProp() remains the target for
|
|
1369
|
+
// spreads, URL attributes (href/src/action — sanitization lives there) and any
|
|
1370
|
+
// name the compiler can't classify.
|
|
1371
|
+
//
|
|
1372
|
+
// Function values are reactive ACCESSORS (e.g. `value={() => user().name}`),
|
|
1373
|
+
// exactly like setProp treats them: wrap in an effect that re-applies the
|
|
1374
|
+
// resolved value, with the disposer registered on el._propEffects so
|
|
1375
|
+
// disposeTree() tears it down on unmount.
|
|
1376
|
+
|
|
1377
|
+
function _wrapPropAccessor(el, key, accessor, apply) {
|
|
1378
|
+
if (!el._propEffects) el._propEffects = {};
|
|
1379
|
+
if (el._propEffects[key]) {
|
|
1380
|
+
try { el._propEffects[key](); } catch (e) { /* already disposed */ }
|
|
1381
|
+
}
|
|
1382
|
+
el._propEffects[key] = effect(() => apply(el, accessor()));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// class / className — hottest dynamic attribute in real apps.
|
|
1386
|
+
export function setClass(el, value) {
|
|
1387
|
+
if (typeof value === 'function') return _wrapPropAccessor(el, 'class', value, setClass);
|
|
1388
|
+
if (_hasSVGElement && el instanceof SVGElement) {
|
|
1389
|
+
el.setAttribute('class', value || '');
|
|
1390
|
+
} else {
|
|
1391
|
+
el.className = value || '';
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// style — string (cssText) or object form.
|
|
1396
|
+
export function setStyle(el, value) {
|
|
1397
|
+
if (typeof value === 'function') return _wrapPropAccessor(el, 'style', value, setStyle);
|
|
1398
|
+
if (typeof value === 'string') {
|
|
1399
|
+
el.style.cssText = value;
|
|
1400
|
+
// cssText fully replaces inline styles — drop any tracked object so a later
|
|
1401
|
+
// object form starts clean rather than diffing against stale keys.
|
|
1402
|
+
el._lastStyleObj = null;
|
|
1403
|
+
} else if (value && typeof value === 'object') {
|
|
1404
|
+
const style = el.style;
|
|
1405
|
+
// Clear properties present in the previously-applied object but absent from
|
|
1406
|
+
// the new one. Without this, `style={() => cond() ? {color, fontWeight} :
|
|
1407
|
+
// {color}}` would leave fontWeight set after flipping to the second object.
|
|
1408
|
+
const prev = el._lastStyleObj;
|
|
1409
|
+
if (prev) {
|
|
1410
|
+
for (const prop in prev) {
|
|
1411
|
+
if (!(prop in value)) style[prop] = '';
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
for (const prop in value) {
|
|
1415
|
+
style[prop] = value[prop] ?? '';
|
|
1416
|
+
}
|
|
1417
|
+
el._lastStyleObj = value;
|
|
1418
|
+
} else if (value == null) {
|
|
1419
|
+
el.style.cssText = '';
|
|
1420
|
+
el._lastStyleObj = null;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Plain attribute set — used for data-*/aria-* (statically recognizable).
|
|
1425
|
+
// null/undefined removes the attribute (previously setProp stringified them
|
|
1426
|
+
// to "null"/"undefined" — removal is the correct semantic). Booleans are
|
|
1427
|
+
// stringified ("true"/"false") because aria-* boolean strings are meaningful.
|
|
1428
|
+
export function setAttr(el, name, value) {
|
|
1429
|
+
if (typeof value === 'function') {
|
|
1430
|
+
return _wrapPropAccessor(el, name, value, (e2, v) => setAttr(e2, name, v));
|
|
1431
|
+
}
|
|
1432
|
+
if (value == null) el.removeAttribute(name);
|
|
1433
|
+
else el.setAttribute(name, value);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// value — controlled-input property set. <select> keeps multi/deferred-option
|
|
1437
|
+
// handling; other elements get a guarded property write (the !== guard avoids
|
|
1438
|
+
// resetting the caret position in focused inputs on unrelated re-runs).
|
|
1439
|
+
export function setValue(el, value) {
|
|
1440
|
+
if (typeof value === 'function') return _wrapPropAccessor(el, 'value', value, setValue);
|
|
1441
|
+
if (el.tagName === 'SELECT') {
|
|
1442
|
+
_setSelectValue(el, value);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const str = value == null ? '' : String(value);
|
|
1446
|
+
if (el.value !== str) el.value = str;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// checked — live property write (matches bind:checked). The old generic path
|
|
1450
|
+
// used setAttribute('checked'), which only sets the DEFAULT-checked state and
|
|
1451
|
+
// stops reflecting once the user has toggled the input.
|
|
1452
|
+
export function setChecked(el, value) {
|
|
1453
|
+
if (typeof value === 'function') return _wrapPropAccessor(el, 'checked', value, setChecked);
|
|
1454
|
+
el.checked = !!value;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1358
1457
|
// --- delegateEvents(eventNames) ---
|
|
1359
1458
|
// Event delegation: common events handled at document level.
|
|
1360
1459
|
// Handlers stored as el.$$click, el.$$input, etc.
|
|
@@ -1371,6 +1470,15 @@ export function delegateEvents(eventNames) {
|
|
|
1371
1470
|
let node = e.target;
|
|
1372
1471
|
const key = '$$' + name;
|
|
1373
1472
|
|
|
1473
|
+
// Shim e.currentTarget so handlers see the element the (virtual) listener
|
|
1474
|
+
// is attached to — not `document` — during the ancestor walk. Mirrors
|
|
1475
|
+
// Solid's delegation shim. configurable so nested dispatch can redefine.
|
|
1476
|
+
// (SPRINT v0.11 C9)
|
|
1477
|
+
Object.defineProperty(e, 'currentTarget', {
|
|
1478
|
+
configurable: true,
|
|
1479
|
+
get() { return node || document; },
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1374
1482
|
// Walk up the DOM tree looking for handlers
|
|
1375
1483
|
while (node) {
|
|
1376
1484
|
const handler = node[key];
|
|
@@ -1659,12 +1767,8 @@ function hydrateElementProps(el, props) {
|
|
|
1659
1767
|
if (key === 'class' || key === 'className') {
|
|
1660
1768
|
effect(() => { el.className = value() || ''; });
|
|
1661
1769
|
} else if (key === 'style' && typeof value() === 'object') {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
for (const prop in styles) {
|
|
1665
|
-
el.style[prop] = styles[prop] ?? '';
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1770
|
+
// Route through setStyle so stale object keys are cleared (el._lastStyleObj).
|
|
1771
|
+
effect(() => { setStyle(el, value()); });
|
|
1668
1772
|
} else {
|
|
1669
1773
|
effect(() => { setProp(el, key, value()); });
|
|
1670
1774
|
}
|