lume-js 2.3.0 → 2.3.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/AGENT_GUIDE.md +224 -0
- package/README.md +34 -13
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +48 -10
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +3 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/{shared-DNe4ez8V.mjs → shared-BGg9PbiG.mjs} +2 -2
- package/dist/{shared-DNe4ez8V.mjs.map → shared-BGg9PbiG.mjs.map} +1 -1
- package/dist/{shared-Bk_gndPJ.mjs → shared-SUXdsYBx.mjs} +4 -3
- package/dist/shared-SUXdsYBx.mjs.map +1 -0
- package/dist/state.min.mjs +1 -1
- package/dist/state.mjs +1 -1
- package/llms-full.txt +6999 -0
- package/llms.txt +89 -0
- package/package.json +7 -2
- package/src/addons/index.d.ts +29 -5
- package/src/addons/persist.js +40 -2
- package/src/addons/repeat.js +43 -9
- package/src/core/state.js +10 -2
- package/src/handlers/stringAttr.js +9 -2
- package/src/index.d.ts +14 -240
- package/src/state.d.ts +246 -11
- package/dist/shared-Bk_gndPJ.mjs.map +0 -1
package/llms.txt
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Lume.js
|
|
2
|
+
|
|
3
|
+
> Minimal reactive state management using only standard JavaScript and HTML —
|
|
4
|
+
> no custom syntax, no build step, no framework lock-in. 1.46 KB universal
|
|
5
|
+
> core (state/batch), 2.66 KB with DOM binding (bindDom/effect). Reactivity
|
|
6
|
+
> that follows web standards: stores are Proxies over plain objects, DOM
|
|
7
|
+
> binding is declarative via valid data-* attributes, updates are
|
|
8
|
+
> microtask-batched per store with an explicit batch() escape hatch for
|
|
9
|
+
> cross-store writes.
|
|
10
|
+
|
|
11
|
+
Version: 2.3.1. Install: `npm install lume-js` or import from
|
|
12
|
+
`https://cdn.jsdelivr.net/npm/lume-js/dist/index.min.mjs`.
|
|
13
|
+
|
|
14
|
+
Key facts agents get wrong without reading the docs: arrays and nested
|
|
15
|
+
objects must be REPLACED, never mutated in place (reference equality);
|
|
16
|
+
nested objects need explicit state() wrapping; updates flush on the next
|
|
17
|
+
microtask, not synchronously; effects only track keys read synchronously;
|
|
18
|
+
data-* attribute values are state keys, never expressions.
|
|
19
|
+
|
|
20
|
+
## Start here
|
|
21
|
+
|
|
22
|
+
- [AGENT_GUIDE.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/AGENT_GUIDE.md): Distilled rules, pitfalls, and patterns for coding agents
|
|
23
|
+
- [README.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/README.md): Pitch, installation, quick start, API overview
|
|
24
|
+
|
|
25
|
+
## Guides
|
|
26
|
+
|
|
27
|
+
- [docs/guides/installation.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/installation.md): CDN, npm, and import options
|
|
28
|
+
- [docs/guides/quick-start.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/quick-start.md): First app in five minutes
|
|
29
|
+
- [docs/guides/core-concepts.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/core-concepts.md): Stores, effects, bindings
|
|
30
|
+
- [docs/guides/reactivity.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/reactivity.md): How the Proxy, tracking, and microtask flush work
|
|
31
|
+
- [docs/guides/choosing-reactive-primitives.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/choosing-reactive-primitives.md): effect vs computed vs watch
|
|
32
|
+
- [docs/guides/two-way-binding.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/two-way-binding.md): Inputs and data-bind
|
|
33
|
+
- [docs/guides/lists.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/lists.md): Rendering arrays
|
|
34
|
+
- [docs/guides/forms.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/forms.md): Form patterns
|
|
35
|
+
- [docs/guides/handlers.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/handlers.md): Extending bindDom with new data-* attributes
|
|
36
|
+
- [docs/guides/cleanup-and-dispose.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/cleanup-and-dispose.md): Tearing down effects and bindings
|
|
37
|
+
- [docs/guides/performance.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/performance.md): Batching and update costs
|
|
38
|
+
- [docs/guides/animations.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/animations.md): Animating on state change
|
|
39
|
+
- [docs/guides/routing.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/routing.md): Client-side routing patterns
|
|
40
|
+
- [docs/guides/ssr-hydration.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/ssr-hydration.md): Server-rendered HTML with reactive hydration
|
|
41
|
+
- [docs/guides/universal-core.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/universal-core.md): Using lume-js/state without a DOM
|
|
42
|
+
- [docs/guides/testing.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/testing.md): Testing stores and bindings
|
|
43
|
+
- [docs/guides/migration.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/migration.md): Migrating between versions
|
|
44
|
+
- [docs/guides/faq.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/guides/faq.md): Frequently asked questions
|
|
45
|
+
|
|
46
|
+
## Tutorials
|
|
47
|
+
|
|
48
|
+
- [docs/tutorials/build-todo-app.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/tutorials/build-todo-app.md): Todo app walkthrough
|
|
49
|
+
- [docs/tutorials/working-with-arrays.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/tutorials/working-with-arrays.md): Immutable array updates — the golden rule
|
|
50
|
+
- [docs/tutorials/build-tic-tac-toe.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/tutorials/build-tic-tac-toe.md): Game with history and computed winner
|
|
51
|
+
|
|
52
|
+
## API reference — core
|
|
53
|
+
|
|
54
|
+
- [docs/api/core/state.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/core/state.md): state() reactive stores
|
|
55
|
+
- [docs/api/core/effect.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/core/effect.md): effect() auto-tracking and explicit deps
|
|
56
|
+
- [docs/api/core/bindDom.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/core/bindDom.md): bindDom() DOM binding
|
|
57
|
+
- [docs/api/core/batch.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/core/batch.md): batch() cross-store write grouping
|
|
58
|
+
- [docs/api/core/handlers.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/core/handlers.md): Handler system — the { attr, apply } extension contract
|
|
59
|
+
|
|
60
|
+
## API reference — handlers
|
|
61
|
+
|
|
62
|
+
- [docs/api/handlers/show.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/show.md): Conditional visibility
|
|
63
|
+
- [docs/api/handlers/className.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/className.md): Full class attribute from state
|
|
64
|
+
- [docs/api/handlers/boolAttr.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/boolAttr.md): Boolean attributes (disabled, hidden, …)
|
|
65
|
+
- [docs/api/handlers/ariaAttr.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/ariaAttr.md): ARIA attributes from state
|
|
66
|
+
- [docs/api/handlers/classToggle.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/classToggle.md): Toggle a single class
|
|
67
|
+
- [docs/api/handlers/stringAttr.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/stringAttr.md): String attributes (href, src, title, …)
|
|
68
|
+
- [docs/api/handlers/on.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/on.md): Declarative event wiring via data-on*
|
|
69
|
+
- [docs/api/handlers/htmlAttrs.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/handlers/htmlAttrs.md): Preset bundle of common attributes
|
|
70
|
+
|
|
71
|
+
## API reference — addons
|
|
72
|
+
|
|
73
|
+
- [docs/api/addons/computed.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/computed.md): Derived read-only values
|
|
74
|
+
- [docs/api/addons/watch.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/watch.md): Single-key observation
|
|
75
|
+
- [docs/api/addons/repeat.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/repeat.md): Keyed list rendering
|
|
76
|
+
- [docs/api/addons/persist.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/persist.md): localStorage/sessionStorage sync
|
|
77
|
+
- [docs/api/addons/hydrateState.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/hydrateState.md): SSR hydration
|
|
78
|
+
- [docs/api/addons/createCleanupGroup.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/createCleanupGroup.md): Grouped disposal
|
|
79
|
+
- [docs/api/addons/debug.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/debug.md): Write/flush logging
|
|
80
|
+
- [docs/api/addons/withPlugins.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/withPlugins.md): State extension system
|
|
81
|
+
- [docs/api/addons/isReactive.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/api/addons/isReactive.md): Reactive brand detection
|
|
82
|
+
|
|
83
|
+
## Design
|
|
84
|
+
|
|
85
|
+
- [docs/design/design-decisions.md](https://raw.githubusercontent.com/sathvikc/lume-js/main/docs/design/design-decisions.md): Ratified decisions and their reasoning
|
|
86
|
+
|
|
87
|
+
## Optional
|
|
88
|
+
|
|
89
|
+
- [llms-full.txt](https://raw.githubusercontent.com/sathvikc/lume-js/main/llms-full.txt): every file above concatenated into one document
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -43,13 +43,18 @@
|
|
|
43
43
|
"test:watch": "vitest",
|
|
44
44
|
"coverage": "vitest run --coverage",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"
|
|
46
|
+
"llms": "node scripts/build-llms.js",
|
|
47
|
+
"llms:check": "node scripts/build-llms.js --check",
|
|
48
|
+
"validate": "npm run build && node scripts/check-size.js && node scripts/complexity.js && npm run lint && npm run typecheck && npm run coverage && node scripts/build-llms.js --check",
|
|
47
49
|
"prepublishOnly": "npm run validate"
|
|
48
50
|
},
|
|
49
51
|
"files": [
|
|
50
52
|
"dist",
|
|
51
53
|
"src",
|
|
52
54
|
"README.md",
|
|
55
|
+
"AGENT_GUIDE.md",
|
|
56
|
+
"llms.txt",
|
|
57
|
+
"llms-full.txt",
|
|
53
58
|
"LICENSE"
|
|
54
59
|
],
|
|
55
60
|
"keywords": [
|
package/src/addons/index.d.ts
CHANGED
|
@@ -153,9 +153,11 @@ export interface RepeatOptions<T> {
|
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
155
|
* Function called ONCE when element is created (for DOM structure setup)
|
|
156
|
-
* Recommended for event listeners and innerHTML setup
|
|
156
|
+
* Recommended for event listeners and innerHTML setup.
|
|
157
|
+
* May return a cleanup function — it is called automatically when the
|
|
158
|
+
* element is removed (by list update or full cleanup).
|
|
157
159
|
*/
|
|
158
|
-
create?: (item: T, element: HTMLElement, index: number) => void;
|
|
160
|
+
create?: (item: T, element: HTMLElement, index: number) => void | (() => void);
|
|
159
161
|
|
|
160
162
|
/**
|
|
161
163
|
* Function called on every update for data binding
|
|
@@ -163,6 +165,13 @@ export interface RepeatOptions<T> {
|
|
|
163
165
|
*/
|
|
164
166
|
update?: (item: T, element: HTMLElement, index: number, context: UpdateContext) => void;
|
|
165
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Additional cleanup when an element is removed. Called after any cleanup
|
|
170
|
+
* function returned by create(). Prefer returning a cleanup from create()
|
|
171
|
+
* for automatic lifecycle management.
|
|
172
|
+
*/
|
|
173
|
+
remove?: (item: T, element: HTMLElement) => void;
|
|
174
|
+
|
|
166
175
|
/**
|
|
167
176
|
* Declarative item structure from a standard <template> element.
|
|
168
177
|
* - true: use the first <template> inside the container
|
|
@@ -458,6 +467,9 @@ export interface Plugin {
|
|
|
458
467
|
* Wrap a reactive state proxy with plugin hooks.
|
|
459
468
|
* Plugins can intercept get/set/notify/subscribe operations.
|
|
460
469
|
*
|
|
470
|
+
* The returned proxy additionally exposes $dispose(), which removes the
|
|
471
|
+
* plugin layer's flush hook and clears its pending notifications.
|
|
472
|
+
*
|
|
461
473
|
* @param store - A reactive proxy from state()
|
|
462
474
|
* @param plugins - Array of plugin objects
|
|
463
475
|
* @returns A new proxy wrapping the store with plugin behavior
|
|
@@ -468,9 +480,13 @@ export interface Plugin {
|
|
|
468
480
|
* import { withPlugins, createDebugPlugin } from 'lume-js/addons';
|
|
469
481
|
*
|
|
470
482
|
* const store = withPlugins(state({ count: 0 }), [createDebugPlugin({ label: 'counter' })]);
|
|
483
|
+
* store.$dispose(); // detach the plugin layer when done
|
|
471
484
|
* ```
|
|
472
485
|
*/
|
|
473
|
-
export function withPlugins<T extends object>(
|
|
486
|
+
export function withPlugins<T extends object>(
|
|
487
|
+
store: ReactiveState<T>,
|
|
488
|
+
plugins: Plugin[]
|
|
489
|
+
): ReactiveState<T> & { $dispose(): void };
|
|
474
490
|
|
|
475
491
|
/**
|
|
476
492
|
* A group that collects cleanup/unsubscribe functions and can dispose them all at once.
|
|
@@ -508,7 +524,9 @@ export function createCleanupGroup(): CleanupGroup;
|
|
|
508
524
|
* embedded in the server-rendered HTML.
|
|
509
525
|
*
|
|
510
526
|
* @param selector - CSS selector for the script element (default: '#__LUME_DATA__')
|
|
511
|
-
* @
|
|
527
|
+
* @param validate - Optional validator: (data) => boolean. If it returns
|
|
528
|
+
* false, hydrateState returns {} instead of the parsed data.
|
|
529
|
+
* @returns Parsed JSON object, or empty object if not found, invalid, or rejected
|
|
512
530
|
*
|
|
513
531
|
* @example
|
|
514
532
|
* ```typescript
|
|
@@ -516,9 +534,14 @@ export function createCleanupGroup(): CleanupGroup;
|
|
|
516
534
|
* import { hydrateState } from 'lume-js/addons';
|
|
517
535
|
*
|
|
518
536
|
* const store = state(hydrateState());
|
|
537
|
+
*
|
|
538
|
+
* // With schema validation
|
|
539
|
+
* const data = hydrateState('#__LUME_DATA__', d =>
|
|
540
|
+
* typeof (d as any).title === 'string'
|
|
541
|
+
* );
|
|
519
542
|
* ```
|
|
520
543
|
*/
|
|
521
|
-
export function hydrateState(selector?: string): object;
|
|
544
|
+
export function hydrateState(selector?: string, validate?: (data: unknown) => boolean): object;
|
|
522
545
|
|
|
523
546
|
/**
|
|
524
547
|
* Options for persist()
|
|
@@ -526,6 +549,7 @@ export function hydrateState(selector?: string): object;
|
|
|
526
549
|
export interface PersistOptions {
|
|
527
550
|
/**
|
|
528
551
|
* Keys to persist. Default: all own non-$ keys of the store at call time.
|
|
552
|
+
* An explicit empty array is respected: persists and hydrates nothing.
|
|
529
553
|
*/
|
|
530
554
|
keys?: string[];
|
|
531
555
|
/**
|
package/src/addons/persist.js
CHANGED
|
@@ -53,6 +53,25 @@ function serializeKeys(store, watched) {
|
|
|
53
53
|
return JSON.stringify(out);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// storage object → Set of storage keys currently managed by a persist()
|
|
57
|
+
// instance. Two instances on one entry silently overwrite each other's
|
|
58
|
+
// subsets (each serializes only its own watched keys over the whole blob),
|
|
59
|
+
// so the second registration gets a loud warning.
|
|
60
|
+
const activeEntries = new WeakMap();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Default storage resolution. Wrapped because accessing localStorage can
|
|
64
|
+
* THROW (SecurityError) in cookie-blocked iframes and some privacy modes —
|
|
65
|
+
* persist() must degrade to a warning, never crash the app at setup.
|
|
66
|
+
*/
|
|
67
|
+
function defaultStorage() {
|
|
68
|
+
try {
|
|
69
|
+
return typeof localStorage !== 'undefined' ? localStorage : undefined;
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
/**
|
|
57
76
|
* Sync store keys with a Storage object.
|
|
58
77
|
*
|
|
@@ -75,17 +94,35 @@ export function persist(store, storageKey, options = {}) {
|
|
|
75
94
|
|
|
76
95
|
const storage = options.storage !== undefined
|
|
77
96
|
? options.storage
|
|
78
|
-
:
|
|
97
|
+
: defaultStorage();
|
|
79
98
|
|
|
80
99
|
if (!storage || typeof storage.getItem !== 'function') {
|
|
81
100
|
logWarn('[Lume.js] persist(): no storage available — persistence disabled');
|
|
82
101
|
return () => {};
|
|
83
102
|
}
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
// An explicit array is respected as-is — including [] (persist nothing).
|
|
105
|
+
// Only an absent/non-array option falls back to all own non-$ keys.
|
|
106
|
+
const watched = Array.isArray(options.keys)
|
|
86
107
|
? options.keys.slice()
|
|
87
108
|
: Object.keys(store).filter(k => !k.startsWith('$'));
|
|
88
109
|
|
|
110
|
+
// Warn when another live persist() already manages this storage entry
|
|
111
|
+
let entrySet = activeEntries.get(storage);
|
|
112
|
+
if (!entrySet) {
|
|
113
|
+
entrySet = new Set();
|
|
114
|
+
activeEntries.set(storage, entrySet);
|
|
115
|
+
}
|
|
116
|
+
const ownsEntry = !entrySet.has(storageKey);
|
|
117
|
+
if (ownsEntry) {
|
|
118
|
+
entrySet.add(storageKey);
|
|
119
|
+
} else {
|
|
120
|
+
logWarn(
|
|
121
|
+
`[Lume.js] persist(): "${storageKey}" is already managed by another persist() on this storage — ` +
|
|
122
|
+
'instances will overwrite each other\'s data. Use a distinct key per store.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
// ── Hydrate ────────────────────────────────────────────────────────────
|
|
90
127
|
// Only watched keys are assigned — stale storage can't inject others.
|
|
91
128
|
const stored = readStored(storage, storageKey);
|
|
@@ -147,6 +184,7 @@ export function persist(store, storageKey, options = {}) {
|
|
|
147
184
|
|
|
148
185
|
return () => {
|
|
149
186
|
disposed = true;
|
|
187
|
+
if (ownsEntry) entrySet.delete(storageKey);
|
|
150
188
|
while (unsubs.length) unsubs.pop()();
|
|
151
189
|
};
|
|
152
190
|
}
|
package/src/addons/repeat.js
CHANGED
|
@@ -114,11 +114,11 @@ import { logWarn, logError } from '../utils/log.js';
|
|
|
114
114
|
import { applyBindValue } from '../core/bindDom.js';
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Resolve the template option to
|
|
117
|
+
* Resolve the template option to the <template> element.
|
|
118
118
|
* Accepts true (first <template> inside the container), a CSS selector,
|
|
119
119
|
* or an HTMLTemplateElement directly.
|
|
120
120
|
*/
|
|
121
|
-
function
|
|
121
|
+
function resolveTemplateEl(template, containerEl) {
|
|
122
122
|
let templateEl = template;
|
|
123
123
|
if (template === true) {
|
|
124
124
|
templateEl = containerEl.querySelector('template');
|
|
@@ -131,7 +131,22 @@ function resolveTemplateRoot(template, containerEl) {
|
|
|
131
131
|
if (templateEl.content.children.length !== 1) {
|
|
132
132
|
throw new Error('[Lume.js] repeat(): template must contain exactly one root element');
|
|
133
133
|
}
|
|
134
|
-
return templateEl
|
|
134
|
+
return templateEl;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the template option to its clone root, plus the container child
|
|
139
|
+
* that is (or contains) the source <template>. Reconciliation and cleanup
|
|
140
|
+
* must leave that child alone — it's the user's markup, and removing it
|
|
141
|
+
* would break re-binding with `template: true` after cleanup. keepEl is
|
|
142
|
+
* null when there is no template or it lives outside the container.
|
|
143
|
+
*/
|
|
144
|
+
function resolveTemplate(template, containerEl) {
|
|
145
|
+
if (!template) return { templateRoot: null, keepEl: null };
|
|
146
|
+
const templateEl = resolveTemplateEl(template, containerEl);
|
|
147
|
+
let keepEl = templateEl;
|
|
148
|
+
while (keepEl && keepEl.parentNode !== containerEl) keepEl = keepEl.parentNode;
|
|
149
|
+
return { templateRoot: templateEl.content.firstElementChild, keepEl };
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
/**
|
|
@@ -300,7 +315,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
300
315
|
|
|
301
316
|
// Template mode: structure and data binding come from the <template>;
|
|
302
317
|
// render/create/update are all optional on top of it.
|
|
303
|
-
const templateRoot
|
|
318
|
+
const { templateRoot, keepEl } = resolveTemplate(template, containerEl);
|
|
304
319
|
|
|
305
320
|
if (templateRoot && typeof render === 'function') {
|
|
306
321
|
logWarn('[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead');
|
|
@@ -329,10 +344,29 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
329
344
|
: document.createElement(element);
|
|
330
345
|
}
|
|
331
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Empty the container, preserving the in-container source template.
|
|
349
|
+
* Manual removal (not replaceChildren) for two reasons: replaceChildren
|
|
350
|
+
* is Chrome 86+/Safari 14+ — above the documented ES2020-era browser
|
|
351
|
+
* floor — and re-inserting keepEl would re-parent a template the user
|
|
352
|
+
* has since moved (or resurrect one they deleted).
|
|
353
|
+
*/
|
|
354
|
+
function clearContainer() {
|
|
355
|
+
let node = containerEl.firstChild;
|
|
356
|
+
while (node) {
|
|
357
|
+
const next = node.nextSibling;
|
|
358
|
+
if (node !== keepEl) containerEl.removeChild(node);
|
|
359
|
+
node = next;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
332
363
|
function reconcileDOM(container, nextEls) {
|
|
333
364
|
let ptr = container.firstChild;
|
|
334
365
|
|
|
335
366
|
for (let i = 0; i < nextEls.length; i++) {
|
|
367
|
+
// Never treat the source template (or its wrapper) as a list row
|
|
368
|
+
if (keepEl && ptr === keepEl) ptr = ptr.nextSibling;
|
|
369
|
+
|
|
336
370
|
const desired = nextEls[i];
|
|
337
371
|
|
|
338
372
|
if (ptr === desired) {
|
|
@@ -343,10 +377,10 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
343
377
|
container.insertBefore(desired, ptr);
|
|
344
378
|
}
|
|
345
379
|
|
|
346
|
-
// Remove leftover children not in nextEls
|
|
380
|
+
// Remove leftover children not in nextEls (preserving the template)
|
|
347
381
|
while (ptr) {
|
|
348
382
|
const next = ptr.nextSibling;
|
|
349
|
-
container.removeChild(ptr);
|
|
383
|
+
if (ptr !== keepEl) container.removeChild(ptr);
|
|
350
384
|
ptr = next;
|
|
351
385
|
}
|
|
352
386
|
}
|
|
@@ -510,7 +544,7 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
510
544
|
remove(prevItem, el);
|
|
511
545
|
}
|
|
512
546
|
}
|
|
513
|
-
|
|
547
|
+
clearContainer();
|
|
514
548
|
elementsByKey.clear();
|
|
515
549
|
prevItemsByKey.clear();
|
|
516
550
|
prevIndexByKey.clear();
|
|
@@ -539,8 +573,8 @@ export function repeat(container, store, arrayKey, options) {
|
|
|
539
573
|
remove(prevItem, el);
|
|
540
574
|
}
|
|
541
575
|
}
|
|
542
|
-
// Clear DOM elements (
|
|
543
|
-
|
|
576
|
+
// Clear DOM elements (preserving the source template, if any)
|
|
577
|
+
clearContainer();
|
|
544
578
|
elementsByKey.clear();
|
|
545
579
|
prevItemsByKey.clear();
|
|
546
580
|
prevIndexByKey.clear();
|
package/src/core/state.js
CHANGED
|
@@ -121,7 +121,16 @@ export function state(obj) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function notifySubscribers() {
|
|
124
|
-
|
|
124
|
+
// Drain BEFORE delivering. Iterating the live Map is wrong twice over:
|
|
125
|
+
// a subscriber writing back to an already-delivered key would update
|
|
126
|
+
// the in-flight entry (never revisited) and the trailing clear() would
|
|
127
|
+
// destroy it — a silently lost update; and a subscriber opening a
|
|
128
|
+
// batch() would re-deliver every in-flight entry through the wave's
|
|
129
|
+
// re-entrant call. Draining first means write-backs land in the now-
|
|
130
|
+
// empty live Map and are delivered by the next flush iteration/wave.
|
|
131
|
+
const entries = Array.from(pendingNotifications);
|
|
132
|
+
pendingNotifications.clear();
|
|
133
|
+
for (const [key, value] of entries) {
|
|
125
134
|
if (listeners[key]) {
|
|
126
135
|
const subs = listeners[key];
|
|
127
136
|
let i = 0;
|
|
@@ -137,7 +146,6 @@ export function state(obj) {
|
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
|
-
pendingNotifications.clear();
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/** Drain queued effects (Set deduplicates) into an array. */
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* @returns {{ attr: string, apply: function }}
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Matched against a normalized copy of the value (C0 controls, space, and
|
|
10
|
+
// DEL stripped; lowercased) because the browser URL parser ignores those
|
|
11
|
+
// characters when determining the scheme — "java\tscript:alert(1)" and
|
|
12
|
+
// " javascript:alert(1)" both execute. The colon is required so legitimate
|
|
13
|
+
// relative URLs like "javascript-tutorial.html" are not blocked.
|
|
14
|
+
const DANGEROUS_SCHEME = /^(?:javascript:|vbscript:|data:text\/html)/;
|
|
15
|
+
const IGNORED_URL_CHARS = /[\u0000-\u0020\u007F]/g;
|
|
10
16
|
const URI_ATTRS = new Set(['href', 'src', 'action', 'srcset', 'poster', 'formaction']);
|
|
11
17
|
|
|
12
18
|
export function stringAttr(name) {
|
|
@@ -18,7 +24,8 @@ export function stringAttr(name) {
|
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
20
26
|
const strVal = String(val);
|
|
21
|
-
if (URI_ATTRS.has(name) &&
|
|
27
|
+
if (URI_ATTRS.has(name) &&
|
|
28
|
+
DANGEROUS_SCHEME.test(strVal.replace(IGNORED_URL_CHARS, '').toLowerCase())) {
|
|
22
29
|
el.removeAttribute(name);
|
|
23
30
|
return;
|
|
24
31
|
}
|