lume-js 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -19
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +160 -6
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +38 -2
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.min.mjs +1 -1
- package/dist/index.mjs +4 -141
- package/dist/index.mjs.map +1 -1
- package/dist/lume.global.js +1 -1
- package/dist/lume.global.js.map +1 -1
- package/dist/shared-Bk_gndPJ.mjs +232 -0
- package/dist/shared-Bk_gndPJ.mjs.map +1 -0
- package/dist/shared-DNe4ez8V.mjs +249 -0
- package/dist/shared-DNe4ez8V.mjs.map +1 -0
- package/dist/shared-DmpHYKx7.mjs +15 -0
- package/dist/shared-DmpHYKx7.mjs.map +1 -0
- package/dist/state.min.mjs +1 -0
- package/dist/state.mjs +7 -0
- package/dist/state.mjs.map +1 -0
- package/package.json +5 -1
- package/src/addons/computed.js +5 -0
- package/src/addons/hydrateState.js +32 -4
- package/src/addons/index.d.ts +70 -2
- package/src/addons/index.js +13 -2
- package/src/addons/persist.js +152 -0
- package/src/addons/repeat.js +120 -5
- package/src/addons/withPlugins.js +7 -1
- package/src/core/batch.js +139 -0
- package/src/core/bindDom.js +12 -3
- package/src/core/effect.js +34 -5
- package/src/core/state.js +124 -68
- package/src/handlers/index.d.ts +24 -0
- package/src/handlers/index.js +1 -0
- package/src/handlers/on.js +60 -0
- package/src/handlers/stringAttr.js +14 -2
- package/src/index.d.ts +40 -0
- package/src/index.js +3 -1
- package/src/state.d.ts +17 -0
- package/src/state.js +25 -0
- package/dist/shared-Dcokqj5a.mjs +0 -249
- package/dist/shared-Dcokqj5a.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
>
|
|
9
|
-
>
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="lume-logo.png" alt="Lume.js" width="128"><br><br>
|
|
3
|
+
<h3>Lume.js</h3>
|
|
4
|
+
<p><strong>Reactivity that follows web standards.</strong></p>
|
|
5
|
+
<p>
|
|
6
|
+
Minimal reactive state management using only standard JavaScript and HTML.<br>
|
|
7
|
+
No custom syntax · No build step · No framework lock-in.<br>
|
|
8
|
+
<strong>1.45 KB universal core</strong> · <strong>2.66 KB with DOM</strong>
|
|
9
|
+
</p>
|
|
10
|
+
<p>
|
|
11
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
12
|
+
|
|
13
|
+
<a href="package.json"><img src="https://img.shields.io/badge/version-2.3.0-orange.svg" alt="v2.3.0"></a>
|
|
14
|
+
|
|
15
|
+
<a href="tests/"><img src="https://img.shields.io/badge/tests-425%20passing-brightgreen.svg" alt="425 tests"></a>
|
|
16
|
+
|
|
17
|
+
<a href="scripts/check-size.js"><img src="https://img.shields.io/badge/universal%20core-1.45KB-blue.svg" alt="universal core 1.45KB"></a>
|
|
18
|
+
|
|
19
|
+
<a href="scripts/check-size.js"><img src="https://img.shields.io/badge/core%20%2B%20DOM-2.66KB-blue.svg" alt="core + DOM 2.66KB"></a>
|
|
20
|
+
</p>
|
|
21
|
+
<p><code>npm install lume-js</code></p>
|
|
22
|
+
<p><a href="https://sathvikc.github.io/lume-js/"><strong>Docs & live examples →</strong></a></p>
|
|
23
|
+
</div>
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
[](package.json)
|
|
13
|
-
[](tests/)
|
|
14
|
-
[](scripts/check-size.js)
|
|
25
|
+
---
|
|
15
26
|
|
|
16
27
|
## Why Lume.js?
|
|
17
28
|
|
|
@@ -19,7 +30,7 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
|
|
|
19
30
|
|---------|---------|-----------|-----|-------|
|
|
20
31
|
| Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
|
|
21
32
|
| Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
|
|
22
|
-
| Bundle Size |
|
|
33
|
+
| Bundle Size | 1.45–2.66KB | ~15KB | ~35KB | ~45KB |
|
|
23
34
|
| HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
|
|
24
35
|
| Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
|
|
25
36
|
|
|
@@ -29,6 +40,15 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
|
|
|
29
40
|
|
|
30
41
|
## Installation
|
|
31
42
|
|
|
43
|
+
### Pick your entry
|
|
44
|
+
|
|
45
|
+
| Entry | Size (gz) | Contents | For |
|
|
46
|
+
|-------|-----------|----------|-----|
|
|
47
|
+
| `lume-js/state` | **1.45 KB** | `state`, `batch`, `withReadObserver` | Node, Deno, Bun, workers, CLI — anywhere without a DOM |
|
|
48
|
+
| `lume-js` | **2.66 KB** | + `bindDom`, `effect` | Browsers |
|
|
49
|
+
| `lume-js/addons` | pay per import | `computed`, `watch`, `repeat`, `persist`, … | Optional patterns |
|
|
50
|
+
| `lume-js/handlers` | pay per import | `show`, `classToggle`, `on`, … | Extra reactive attributes |
|
|
51
|
+
|
|
32
52
|
### Via CDN (Recommended for simple projects)
|
|
33
53
|
|
|
34
54
|
```html
|
|
@@ -37,6 +57,14 @@ Minimal reactive state management using only standard JavaScript and HTML. No cu
|
|
|
37
57
|
</script>
|
|
38
58
|
```
|
|
39
59
|
|
|
60
|
+
DOM-free kernel only (servers, workers, embedded):
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<script type="module">
|
|
64
|
+
import { state, batch } from 'https://cdn.jsdelivr.net/npm/lume-js/dist/state.min.mjs';
|
|
65
|
+
</script>
|
|
66
|
+
```
|
|
67
|
+
|
|
40
68
|
### Via NPM (Recommended for bundlers)
|
|
41
69
|
|
|
42
70
|
```bash
|
|
@@ -44,9 +72,13 @@ npm install lume-js
|
|
|
44
72
|
```
|
|
45
73
|
|
|
46
74
|
```javascript
|
|
47
|
-
import { state, bindDom } from 'lume-js';
|
|
75
|
+
import { state, bindDom } from 'lume-js'; // browser: full core
|
|
76
|
+
import { state, batch } from 'lume-js/state'; // Node/CLI/workers: 1.45 KB kernel
|
|
48
77
|
```
|
|
49
78
|
|
|
79
|
+
> **→ Using Lume without a DOM?** See the [Universal core guide](docs/guides/universal-core.md).
|
|
80
|
+
|
|
81
|
+
|
|
50
82
|
### Browser Support
|
|
51
83
|
|
|
52
84
|
| Browser | Minimum version |
|
|
@@ -81,6 +113,37 @@ bindDom(document.body, store);
|
|
|
81
113
|
|
|
82
114
|
That's it — two-way binding, no build step, valid HTML.
|
|
83
115
|
|
|
116
|
+
### Lists? Also just HTML.
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<ul id="todos">
|
|
120
|
+
<template>
|
|
121
|
+
<li><span data-bind="title"></span> <em data-bind="$index"></em></li>
|
|
122
|
+
</template>
|
|
123
|
+
</ul>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
import { repeat } from 'lume-js/addons';
|
|
128
|
+
|
|
129
|
+
repeat('#todos', store, 'todos', { key: t => t.id, template: true });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The row structure is a standard `<template>` element — no `createElement`, no JSX, no custom syntax.
|
|
133
|
+
|
|
134
|
+
### Multiple stores? Batch them.
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
import { batch } from 'lume-js';
|
|
138
|
+
|
|
139
|
+
batch(() => {
|
|
140
|
+
cart.items = [...cart.items, item];
|
|
141
|
+
totals.sum += item.price;
|
|
142
|
+
ui.message = 'Added!';
|
|
143
|
+
});
|
|
144
|
+
// effects depending on several stores ran exactly ONCE, synchronously
|
|
145
|
+
```
|
|
146
|
+
|
|
84
147
|
---
|
|
85
148
|
|
|
86
149
|
## Built-in Reactive Attributes
|
|
@@ -142,6 +205,7 @@ bindDom(document.body, store, {
|
|
|
142
205
|
| `ariaAttr(name)` | `data-aria-pressed="key"` | Sets ARIA attribute to "true"/"false" |
|
|
143
206
|
| `classToggle(...names)` | `data-class-active="key"` | Toggles individual CSS classes |
|
|
144
207
|
| `stringAttr(name)` | `data-href="key"` | Sets string attributes (removes on null) |
|
|
208
|
+
| `on(...types)` | `data-onclick="key"` | Wires the function at that state key as an event listener |
|
|
145
209
|
| `htmlAttrs()` | *(all of the above)* | One-import preset — all standard HTML + ARIA attrs |
|
|
146
210
|
|
|
147
211
|
### Presets
|
|
@@ -181,7 +245,10 @@ import { computed, watch, repeat } from 'lume-js/addons';
|
|
|
181
245
|
| `effect(fn)` *(core)* | Write derived values back into the store, or trigger side effects on state change |
|
|
182
246
|
| `computed(fn)` | Derive a read-only value from state to consume *outside* the store (templates, display logic) |
|
|
183
247
|
| `watch(store, key, fn)` | React to a *specific* key changing — DOM updates, analytics, syncing external state |
|
|
184
|
-
| `repeat(container, store, key, opts)` | Render a keyed list with element reuse
|
|
248
|
+
| `repeat(container, store, key, opts)` | Render a keyed list with element reuse — incl. declarative `template:` mode bound straight from a `<template>` element |
|
|
249
|
+
| `persist(store, key, opts)` | Sync selected keys with localStorage/sessionStorage — hydrate on call, auto-save on change |
|
|
250
|
+
| `createCleanupGroup()` | Collect multiple cleanup/unsubscribe functions and dispose them all at once |
|
|
251
|
+
| `hydrateState(selector?)` | Read initial state from a `<script type="application/json">` tag (SSR hydration) |
|
|
185
252
|
|
|
186
253
|
**Quick rule:** `effect` for writing back into state → `computed` for reading outside state → `watch` for observing a single key → `repeat` for arrays in the DOM.
|
|
187
254
|
|
|
@@ -221,7 +288,7 @@ watch(store, 'count', (val) => {
|
|
|
221
288
|
|
|
222
289
|
## Documentation
|
|
223
290
|
|
|
224
|
-
|
|
291
|
+
Browse the full docs at **[sathvikc.github.io/lume-js](https://sathvikc.github.io/lume-js/)**, or read the source markdown in the [docs/](docs/) directory:
|
|
225
292
|
|
|
226
293
|
- **Tutorials**
|
|
227
294
|
- [Build a Todo App](docs/tutorials/build-todo-app.md)
|
|
@@ -231,11 +298,15 @@ Full documentation is available in the [docs/](docs/) directory:
|
|
|
231
298
|
- [state()](docs/api/core/state.md) — Reactive state
|
|
232
299
|
- [bindDom()](docs/api/core/bindDom.md) — DOM binding
|
|
233
300
|
- [effect()](docs/api/core/effect.md) — Reactive effects
|
|
301
|
+
- [batch()](docs/api/core/batch.md) — Cross-store write batching
|
|
234
302
|
- [Handlers](docs/api/core/handlers.md) — Extensible attribute handlers
|
|
235
|
-
- [Plugins](docs/api/
|
|
236
|
-
- [Addons](docs/api/addons/computed.md) — computed, watch, repeat,
|
|
303
|
+
- [Plugins](docs/api/addons/withPlugins.md) — State extension system
|
|
304
|
+
- [Addons](docs/api/addons/computed.md) — computed, watch, repeat, persist, createCleanupGroup, hydrateState
|
|
237
305
|
- **Guides**
|
|
306
|
+
- [Universal core (Node, CLI, workers)](docs/guides/universal-core.md) — using `lume-js/state` without a DOM
|
|
238
307
|
- [Choosing reactive primitives](docs/guides/choosing-reactive-primitives.md) — when to use effect vs computed vs watch
|
|
308
|
+
- [Cleanup & Disposal](docs/guides/cleanup-and-dispose.md) — tearing down effects, bindings, and subscriptions
|
|
309
|
+
- [SSR & Hydration](docs/guides/ssr-hydration.md) — server-rendered HTML with reactive hydration
|
|
239
310
|
- **Design**
|
|
240
311
|
- [Design Decisions](docs/design/design-decisions.md)
|
|
241
312
|
|
package/dist/addons.min.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const o=new Set;function n(e,t){o.add(e);try{return t()}finally{o.delete(e)}}let r=null;function c(e,o){if("function"!=typeof e)throw Error("effect() requires a function");const c=[];let s=!1;const i=()=>{if(!s){s=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{s=!1}}};if(Array.isArray(o)){for(const e of o)if(Array.isArray(e)&&e.length>=2){const[t,...o]=e;if(t&&"function"==typeof t.$subscribe)for(const e of o){let o=!0;const n=t.$subscribe(e,()=>{o?o=!1:i()});c.push(n)}}i()}else{const o=()=>{if(s)return;const i=c.splice(0),l={fn:e,cleanups:c,execute:o,tracking:{}},f=r;r=l,s=!0;try{n((e,t,o)=>{r===l&&(l.tracking[t]||(l.tracking[t]=!0,l.cleanups.push(o(t,l.execute))))},e)}catch(e){throw c.length=0,c.push(...i),t("[Lume.js effect] Error in effect:",e),e}finally{r=f,s=!1}if(c.length>0)for(const e of i)e();else c.push(...i)};o()}return()=>{for(;c.length;)c.pop()()}}function s(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,s=!1;const i=[],l=c(()=>{if(!r&&!s){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,i.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,i.forEach(e=>e(o)))}finally{queueMicrotask(()=>{s||(r=!1)})}}});return{get value(){if(!n)throw Error("Computed value accessed before initialization");return o},subscribe(e){if("function"!=typeof e)throw Error("subscribe() requires a function");return i.push(e),n&&e(o),()=>{const t=i.indexOf(e);t>-1&&i.splice(t,1)}},dispose(){s=!0,l(),i.length=0,n=!1,r=!1}}}function i(e,t,o,{immediate:n=!0}={}){if(!e.$subscribe)throw Error("store must be created with state()");if(!n){let n=!1;return e.$subscribe(t,e=>{n?o(e):n=!0})}return e.$subscribe(t,o)}function l(e){const t=document.activeElement;if(!e.contains(t))return null;let o=null,n=null;return"INPUT"!==t.tagName&&"TEXTAREA"!==t.tagName||(o=t.selectionStart,n=t.selectionEnd),()=>{document.body.contains(t)&&(t.focus(),null!==o&&null!==n&&t.setSelectionRange(o,n))}}function f(e,t={}){const{isReorder:o=!1}=t,n=e.scrollTop;if(0===n)return()=>{e.scrollTop=0};let r=null,c=0;if(!o){const t=e.getBoundingClientRect();for(let o=e.firstElementChild;o;o=o.nextElementSibling){const e=o.getBoundingClientRect();if(e.bottom>t.top){r=o,c=e.top-t.top;break}}}return()=>{if(r&&document.body.contains(r)){const t=r.getBoundingClientRect(),o=e.getBoundingClientRect(),n=t.top-o.top-c;e.scrollTop=e.scrollTop+n}else e.scrollTop=n}}function u(o,n,r,c){const{key:s,render:i,create:u,update:a,remove:g,element:p="div",preserveFocus:d=l,preserveScroll:b=f}=c,h="string"==typeof o?document.querySelector(o):o;if(!h)return e(`[Lume.js] repeat(): container "${o}" not found`),()=>{};if("function"!=typeof s)throw Error("[Lume.js] repeat(): options.key must be a function");if("function"!=typeof i&&"function"!=typeof u)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const y=new Map,m=new Map,$=new Map,w=new Map,E=new Set;function j(){return"function"==typeof p?p():document.createElement(p)}function S(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if(b&&y.size===o.length){c=!0;for(let e=0;e<o.length;e++)if(!y.has(s(o[e]))){c=!1;break}}E.clear();const l=[];for(let n=0;n<o.length;n++){const r=o[n],c=s(r);if(E.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}E.add(c);let f=y.get(c);const g=!f;g&&(f=j(),y.set(c,f));try{if(g&&u){const e=u(r,f,n);"function"==typeof e&&w.set(c,e)}const e=m.get(c),t=$.get(c);a?e===r&&t===n||a(r,f,n,{isFirstRender:g}):i&&i(r,f,n),m.set(c,r),$.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}l.push(f)}!function(e,o,n){const r=document.body.contains(e),c=r&&d?d(e):null,s=r&&b?b(e,{isReorder:n}):null;(()=>{if(function(e,t){let o=e.firstChild;for(let n=0;n<t.length;n++){const r=t[n];o!==r?e.insertBefore(r,o):o=o.nextSibling}for(;o;){const t=o.nextSibling;e.removeChild(o),o=t}}(h,l),y.size!==E.size)for(const e of y.keys())if(!E.has(e)){const o=y.get(e),n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&o&&g(n,o),y.delete(e),m.delete(e),$.delete(e),w.delete(e)}})(),c&&c(),s&&s()}(h,0,c)}let L;if("function"==typeof n.$subscribe)L=n.$subscribe(r,S);else{if("function"!=typeof n.subscribe)return S(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{for(const[e,o]of y){const n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&g(n,o)}h.replaceChildren(),y.clear(),m.clear(),$.clear(),w.clear(),E.clear()};{const e=n.subscribe(()=>S());S(),L="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof L&&L();for(const[e,o]of y){const n=m.get(e),r=w.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof g&&g(n,o)}h.replaceChildren(),y.clear(),m.clear(),$.clear(),w.clear(),E.clear()}}let a=!0,g=null;const p=new Map;function d(e){return null===g||("string"==typeof g?e.includes(g):!(g instanceof RegExp)||g.test(e))}function b(e){return p.has(e)||p.set(e,{gets:new Map,sets:new Map,notifies:new Map}),p.get(e)}function h(e,t,o){const n=b(e)[t];n.set(o,(n.get(o)||0)+1)}const y=100,m=97;function $(e){try{const t=JSON.stringify(e);return t.length>y?t.slice(0,m)+"...":t}catch{return e+""}}function w(e={}){const t=e.label??"store",o=(t,o)=>{const n=e[t];return void 0!==n?n:o};return{name:"debug:"+t,onInit:()=>{a&&console.log(`%c[${t}]%c initialized`,"color: #888; font-weight: bold","color: inherit")},onGet:(e,n)=>("string"==typeof e&&e.startsWith("$")||(h(t,"gets",e),a&&o("logGet",!1)&&d(e)&&console.log(`%c[${t}]%c GET %c${e}%c = ${$(n)}`,"color: #888; font-weight: bold","color: #4CAF50","color: #2196F3; font-weight: bold","color: inherit")),n),onSet:(e,n,r)=>("string"==typeof e&&e.startsWith("$")||(h(t,"sets",e),a&&o("logSet",!0)&&d(e)&&(console.log(`%c[${t}]%c SET %c${e}%c: ${$(r)} → ${$(n)}`,"color: #888; font-weight: bold","color: #FF9800","color: #2196F3; font-weight: bold","color: inherit"),o("trace",!1)&&console.trace(`%c[${t}] Stack trace for ${e}`,"color: #888"))),n),onSubscribe:e=>{a&&d(e)&&console.log(`%c[${t}]%c SUBSCRIBE %c${e}`,"color: #888; font-weight: bold","color: #9C27B0","color: #2196F3; font-weight: bold")},onNotify:(e,n)=>{"string"==typeof e&&e.startsWith("$")||(h(t,"notifies",e),a&&o("logNotify",!0)&&d(e)&&console.log(`%c[${t}]%c NOTIFY %c${e}%c = ${$(n)}`,"color: #888; font-weight: bold","color: #E91E63","color: #2196F3; font-weight: bold","color: inherit"))}}}const E={enable(){a=!0,console.log("%c[lume-debug]%c Logging enabled","color: #888; font-weight: bold","color: #4CAF50")},disable(){a=!1,console.log("%c[lume-debug]%c Logging disabled","color: #888; font-weight: bold","color: #F44336")},isEnabled:()=>a,filter(e){g=e,console.log(null===e?"%c[lume-debug]%c Filter cleared":"%c[lume-debug]%c Filter set: "+e,"color: #888; font-weight: bold","color: inherit")},getFilter:()=>g,stats(){const e={};for(const[t,o]of p)e[t]={gets:Object.fromEntries(o.gets),sets:Object.fromEntries(o.sets),notifies:Object.fromEntries(o.notifies)};return e},logStats(){const e=this.stats();if(0===Object.keys(e).length)return console.log("%c[lume-debug]%c No stats collected yet","color: #888; font-weight: bold","color: inherit"),e;console.group("%c[lume-debug] Statistics","color: #888; font-weight: bold");for(const[t,o]of Object.entries(e)){console.group("%c"+t,"color: #2196F3; font-weight: bold");const e=[],n=new Set([...Object.keys(o.gets),...Object.keys(o.sets),...Object.keys(o.notifies)]);for(const t of n)e.push({key:t,gets:o.gets[t]||0,sets:o.sets[t]||0,notifies:o.notifies[t]||0});e.length>0&&console.table(e),console.groupEnd()}return console.groupEnd(),e},resetStats(){p.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function j(e,o=[]){if(!o.length)return e;for(const e of o)try{e.onInit?.()}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onInit:`,o)}const n=new Map;let r;return"function"==typeof e.$beforeFlush&&(r=e.$beforeFlush(function(){for(const[e,r]of n)for(const n of o)try{n.onNotify?.(e,r)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onNotify:`,e)}n.clear()})),new Proxy(e,{get(e,c){if("$dispose"===c)return()=>{r&&r(),n.clear()};if("string"==typeof c&&c.startsWith("$")){const n=e[c];return"$subscribe"===c&&"function"==typeof n?(e,r)=>{for(const n of o)try{n.onSubscribe?.(e)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onSubscribe:`,e)}return n(e,r)}:n}let s=e[c];for(const e of o)try{const t=e.onGet?.(c,s);void 0!==t&&(s=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return s},set(e,r,c){const s=e[r];let i=c;for(const e of o)try{const t=e.onSet?.(r,i,s);void 0!==t&&(i=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onSet:`,o)}return Object.is(i,s)||n.set(r,i),e[r]=i,!0}})}function S(){const e=[];return{add(t){"function"==typeof t&&e.push(t)},dispose(){for(;e.length;){const t=e.pop();try{t()}catch(e){}}}}}function L(e="#__LUME_DATA__"){const t="undefined"!=typeof document?document.querySelector(e):null;if(!t)return{};try{return JSON.parse(t.textContent)}catch{return{}}}function k(e){return!(!e||"object"!=typeof e||"function"!=typeof e.$subscribe)}export{s as computed,S as createCleanupGroup,w as createDebugPlugin,E as debug,l as defaultFocusPreservation,f as defaultScrollPreservation,L as hydrateState,k as isReactive,u as repeat,i as watch,j as withPlugins};
|
|
1
|
+
function e(e,...t){void 0!==console&&"function"==typeof console.warn&&console.warn(e,...t)}function t(e,...t){void 0!==console&&"function"==typeof console.error&&console.error(e,...t)}const o=new Set,n=Symbol.for("lume.reactive");function r(e,t){o.add(e);try{return t()}finally{o.delete(e)}}let c=null;function s(e,o){if("function"!=typeof e)throw Error("effect() requires a function");const n=[];let s=!1;const i=()=>{if(!s){s=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{s=!1}}};if(Array.isArray(o)){let e=!1,t=!1;n.push(()=>{t=!0});const r=()=>{e||(e=!0,queueMicrotask(()=>{if(e=!1,!t)try{i()}catch{}}))};for(const e of o)if(Array.isArray(e)&&e.length>=2){const[t,...o]=e;if(t&&"function"==typeof t.$subscribe)for(const e of o){let o=!0;const c=t.$subscribe(e,()=>{o?o=!1:r()});n.push(c)}}i()}else{const o=()=>{if(s)return;const i=n.splice(0),l={fn:e,cleanups:n,execute:o,tracking:new WeakMap},u=c;c=l,s=!0;try{r((e,t,o)=>{if(c!==l)return;let n=l.tracking.get(e);n||(n=new Set,l.tracking.set(e,n)),n.has(t)||(n.add(t),l.cleanups.push(o(t,l.execute)))},e)}catch(e){throw n.length=0,n.push(...i),t("[Lume.js effect] Error in effect:",e),e}finally{c=u,s=!1}if(n.length>0)for(const e of i)e();else n.push(...i)};o()}return()=>{for(;n.length;)n.pop()()}}function i(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,c=!1;const i=[],l=s(()=>{if(!r&&!c){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,i.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,i.forEach(e=>e(o)))}finally{queueMicrotask(()=>{c||(r=!1)})}}});return{get value(){if(!n)throw Error("Computed value accessed before initialization");return o},subscribe(e){if("function"!=typeof e)throw Error("subscribe() requires a function");return i.push(e),n&&e(o),()=>{const t=i.indexOf(e);t>-1&&i.splice(t,1)}},dispose(){c=!0,l(),i.length=0,n=!1,r=!1}}}function l(e,t,o,{immediate:n=!0}={}){if(!e.$subscribe)throw Error("store must be created with state()");if(!n){let n=!1;return e.$subscribe(t,e=>{n?o(e):n=!0})}return e.$subscribe(t,o)}function u(e,t){"INPUT"===e.tagName?"checkbox"===e.type?e.checked=!!t:"radio"===e.type?e.checked=e.value===t+"":e.value=t??"":"TEXTAREA"===e.tagName||"SELECT"===e.tagName?e.value=t??"":e.textContent=t??""}function f(e,t){let o=e;if(!0===e?o=t.querySelector("template"):"string"==typeof e&&(o=document.querySelector(e)),!o||"TEMPLATE"!==o.tagName)throw Error("[Lume.js] repeat(): template not found or not a <template> element");if(1!==o.content.children.length)throw Error("[Lume.js] repeat(): template must contain exactly one root element");return o.content.firstElementChild}function a(e){const t=[],o=e=>{const o=e.getAttribute("data-bind");t.push({node:e,path:o,keys:"$item"===o||"$index"===o?null:o.split(".")})};e.hasAttribute("data-bind")&&o(e);for(const t of e.querySelectorAll("[data-bind]"))o(t);return t}function p(e,t,o){for(const n of e){let e;if("$index"===n.path)e=o;else if("$item"===n.path)e=t;else{e=t;for(let t=0;t<n.keys.length&&null!=e;t++)e=e[n.keys[t]]}u(n.node,e)}}function g(e){const t=document.activeElement;if(!e.contains(t))return null;let o=null,n=null;return"INPUT"!==t.tagName&&"TEXTAREA"!==t.tagName||(o=t.selectionStart,n=t.selectionEnd),()=>{document.body.contains(t)&&(t.focus(),null!==o&&null!==n&&t.setSelectionRange(o,n))}}function d(e,t={}){const{isReorder:o=!1}=t,n=e.scrollTop;if(0===n)return()=>{e.scrollTop=0};let r=null,c=0;if(!o){const t=e.getBoundingClientRect();for(let o=e.firstElementChild;o;o=o.nextElementSibling){const e=o.getBoundingClientRect();if(e.bottom>t.top){r=o,c=e.top-t.top;break}}}return()=>{if(r&&document.body.contains(r)){const t=r.getBoundingClientRect(),o=e.getBoundingClientRect(),n=t.top-o.top-c;e.scrollTop=e.scrollTop+n}else e.scrollTop=n}}function y(o,n,r,c){const{key:s,render:i,create:l,update:u,remove:y,template:h=null,element:b="div",preserveFocus:m=g,preserveScroll:$=d}=c,w="string"==typeof o?document.querySelector(o):o;if(!w)return e(`[Lume.js] repeat(): container "${o}" not found`),()=>{};if("function"!=typeof s)throw Error("[Lume.js] repeat(): options.key must be a function");const j=h?f(h,w):null;if(j&&"function"==typeof i&&e("[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead"),!j&&"function"!=typeof i&&"function"!=typeof l)throw Error("[Lume.js] repeat(): options.render or options.create must be a function");const E=new Map,S=new Map,k=new Map,L=new Map,v=new Map,A=new Set;function N(){return j?j.cloneNode(!0):"function"==typeof b?b():document.createElement(b)}function O(){const o=n[r];if(!Array.isArray(o))return void e(`[Lume.js] repeat(): store.${r} is not an array`);let c=!1;if($&&E.size===o.length){c=!0;for(let e=0;e<o.length;e++)if(!E.has(s(o[e]))){c=!1;break}}A.clear();const f=[];for(let n=0;n<o.length;n++){const r=o[n],c=s(r);if(A.has(c)){e(`[Lume.js] repeat(): duplicate key "${c}"`);continue}A.add(c);let g=E.get(c);const d=!g;d&&(g=N(),E.set(c,g),j&&v.set(c,a(g)));try{if(d&&l){const e=l(r,g,n);"function"==typeof e&&L.set(c,e)}const e=S.get(c),t=k.get(c);j?e===r&&t===n||(p(v.get(c),r,n),u&&u(r,g,n,{isFirstRender:d})):u?e===r&&t===n||u(r,g,n,{isFirstRender:d}):i&&i(r,g,n),S.set(c,r),k.set(c,n)}catch(e){t(`[Lume.js] repeat(): error rendering key "${c}":`,e)}f.push(g)}!function(e,o,n){const r=document.body.contains(e),c=r&&m?m(e):null,s=r&&$?$(e,{isReorder:n}):null;(()=>{if(function(e,t){let o=e.firstChild;for(let n=0;n<t.length;n++){const r=t[n];o!==r?e.insertBefore(r,o):o=o.nextSibling}for(;o;){const t=o.nextSibling;e.removeChild(o),o=t}}(w,f),E.size!==A.size)for(const e of E.keys())if(!A.has(e)){const o=E.get(e),n=S.get(e),r=L.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof y&&o&&y(n,o),E.delete(e),S.delete(e),k.delete(e),L.delete(e),v.delete(e)}})(),c&&c(),s&&s()}(w,0,c)}let T;if("function"==typeof n.$subscribe)T=n.$subscribe(r,O);else{if("function"!=typeof n.subscribe)return O(),e("[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)"),()=>{for(const[e,o]of E){const n=S.get(e),r=L.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof y&&y(n,o)}w.replaceChildren(),E.clear(),S.clear(),k.clear(),L.clear(),v.clear(),A.clear()};{const e=n.subscribe(()=>O());O(),T="function"==typeof e?e:()=>{e?.unsubscribe?.()}}}return()=>{"function"==typeof T&&T();for(const[e,o]of E){const n=S.get(e),r=L.get(e);if("function"==typeof r)try{r()}catch(o){t(`[Lume.js] repeat(): cleanup error for key "${e}":`,o)}"function"==typeof y&&y(n,o)}w.replaceChildren(),E.clear(),S.clear(),k.clear(),L.clear(),v.clear(),A.clear()}}let h=!0,b=null;const m=new Map;function $(e){return null===b||("string"==typeof b?e.includes(b):!(b instanceof RegExp)||b.test(e))}function w(e){return m.has(e)||m.set(e,{gets:new Map,sets:new Map,notifies:new Map}),m.get(e)}function j(e,t,o){const n=w(e)[t];n.set(o,(n.get(o)||0)+1)}const E=100,S=97;function k(e){try{const t=JSON.stringify(e);return t.length>E?t.slice(0,S)+"...":t}catch{return e+""}}function L(e={}){const t=e.label??"store",o=(t,o)=>{const n=e[t];return void 0!==n?n:o};return{name:"debug:"+t,onInit:()=>{h&&console.log(`%c[${t}]%c initialized`,"color: #888; font-weight: bold","color: inherit")},onGet:(e,n)=>("string"==typeof e&&e.startsWith("$")||(j(t,"gets",e),h&&o("logGet",!1)&&$(e)&&console.log(`%c[${t}]%c GET %c${e}%c = ${k(n)}`,"color: #888; font-weight: bold","color: #4CAF50","color: #2196F3; font-weight: bold","color: inherit")),n),onSet:(e,n,r)=>("string"==typeof e&&e.startsWith("$")||(j(t,"sets",e),h&&o("logSet",!0)&&$(e)&&(console.log(`%c[${t}]%c SET %c${e}%c: ${k(r)} → ${k(n)}`,"color: #888; font-weight: bold","color: #FF9800","color: #2196F3; font-weight: bold","color: inherit"),o("trace",!1)&&console.trace(`%c[${t}] Stack trace for ${e}`,"color: #888"))),n),onSubscribe:e=>{h&&$(e)&&console.log(`%c[${t}]%c SUBSCRIBE %c${e}`,"color: #888; font-weight: bold","color: #9C27B0","color: #2196F3; font-weight: bold")},onNotify:(e,n)=>{"string"==typeof e&&e.startsWith("$")||(j(t,"notifies",e),h&&o("logNotify",!0)&&$(e)&&console.log(`%c[${t}]%c NOTIFY %c${e}%c = ${k(n)}`,"color: #888; font-weight: bold","color: #E91E63","color: #2196F3; font-weight: bold","color: inherit"))}}}const v={enable(){h=!0,console.log("%c[lume-debug]%c Logging enabled","color: #888; font-weight: bold","color: #4CAF50")},disable(){h=!1,console.log("%c[lume-debug]%c Logging disabled","color: #888; font-weight: bold","color: #F44336")},isEnabled:()=>h,filter(e){b=e,console.log(null===e?"%c[lume-debug]%c Filter cleared":"%c[lume-debug]%c Filter set: "+e,"color: #888; font-weight: bold","color: inherit")},getFilter:()=>b,stats(){const e={};for(const[t,o]of m)e[t]={gets:Object.fromEntries(o.gets),sets:Object.fromEntries(o.sets),notifies:Object.fromEntries(o.notifies)};return e},logStats(){const e=this.stats();if(0===Object.keys(e).length)return console.log("%c[lume-debug]%c No stats collected yet","color: #888; font-weight: bold","color: inherit"),e;console.group("%c[lume-debug] Statistics","color: #888; font-weight: bold");for(const[t,o]of Object.entries(e)){console.group("%c"+t,"color: #2196F3; font-weight: bold");const e=[],n=new Set([...Object.keys(o.gets),...Object.keys(o.sets),...Object.keys(o.notifies)]);for(const t of n)e.push({key:t,gets:o.gets[t]||0,sets:o.sets[t]||0,notifies:o.notifies[t]||0});e.length>0&&console.table(e),console.groupEnd()}return console.groupEnd(),e},resetStats(){m.clear(),console.log("%c[lume-debug]%c Stats reset","color: #888; font-weight: bold","color: inherit")}};function A(e,o=[]){if(!o.length)return e;for(const e of o){try{e.onInit?.()}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onInit:`,o)}Object.freeze(e)}const n=new Map;let r;return"function"==typeof e.$beforeFlush&&(r=e.$beforeFlush(function(){for(const[e,r]of n)for(const n of o)try{n.onNotify?.(e,r)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onNotify:`,e)}n.clear()})),new Proxy(e,{get(e,c){if("$dispose"===c)return()=>{r&&r(),n.clear()};if("string"==typeof c&&c.startsWith("$")){const n=e[c];return"$subscribe"===c&&"function"==typeof n?(e,r)=>{for(const n of o)try{n.onSubscribe?.(e)}catch(e){t(`[Lume.js] Plugin "${n.name}" error in onSubscribe:`,e)}return n(e,r)}:n}let s=e[c];for(const e of o)try{const t=e.onGet?.(c,s);void 0!==t&&(s=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return s},set(e,r,c){const s=e[r];let i=c;for(const e of o)try{const t=e.onSet?.(r,i,s);void 0!==t&&(i=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onSet:`,o)}return Object.is(i,s)||n.set(r,i),e[r]=i,!0}})}function N(){const e=[];return{add(t){"function"==typeof t&&e.push(t)},dispose(){for(;e.length;){const t=e.pop();try{t()}catch(e){}}}}}function O(e="#__LUME_DATA__",t){const o="undefined"!=typeof document?document.querySelector(e):null;if(!o)return{};if("SCRIPT"!==o.tagName||"application/json"!==o.type)return{};let n;try{n=JSON.parse(o.textContent)}catch{return{}}return"function"!=typeof t||t(n)?n:{}}function T(t,o){try{const e=t.getItem(o);if(!e)return null;const n=JSON.parse(e);return n&&"object"==typeof n&&!Array.isArray(n)?n:null}catch{return e(`[Lume.js] persist(): could not read "${o}" — starting fresh`),null}}function C(e,t){const o={};for(const n of t)o[n]=e[n];return JSON.stringify(o)}function F(t,o,n={}){if(!t||"function"!=typeof t.$subscribe)throw Error("[Lume.js] persist() requires a reactive store from state()");if("string"!=typeof o||0===o.length)throw Error("[Lume.js] persist() requires a non-empty storage key");const r=void 0!==n.storage?n.storage:globalThis.localStorage;if(!r||"function"!=typeof r.getItem)return e("[Lume.js] persist(): no storage available — persistence disabled"),()=>{};const c=Array.isArray(n.keys)&&n.keys.length>0?n.keys.slice():Object.keys(t).filter(e=>!e.startsWith("$")),s=T(r,o);if(s)for(const e of c)Object.prototype.hasOwnProperty.call(s,e)&&(t[e]=s[e]);let i=null;try{i=C(t,c)}catch{}let l=!1,u=!1;const f=()=>{if(l=!1,u)return;let n;try{n=C(t,c)}catch(t){return void e("[Lume.js] persist(): state not serializable — skipping save",t)}if(n!==i)try{r.setItem(o,n),i=n}catch(t){e("[Lume.js] persist(): could not write — storage full or unavailable?",t)}},a=c.map(e=>{let o=!0;return t.$subscribe(e,()=>{o?o=!1:l||(l=!0,queueMicrotask(f))})});return()=>{for(u=!0;a.length;)a.pop()()}}function M(e){return!(!e||"object"!=typeof e||!(n in e)&&"function"!=typeof e.$subscribe)}export{i as computed,N as createCleanupGroup,L as createDebugPlugin,v as debug,g as defaultFocusPreservation,d as defaultScrollPreservation,O as hydrateState,M as isReactive,F as persist,y as repeat,l as watch,A as withPlugins};
|
package/dist/addons.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { R as REACTIVE_BRAND } from "./shared-Bk_gndPJ.mjs";
|
|
2
|
+
import { e as effect, a as applyBindValue } from "./shared-DNe4ez8V.mjs";
|
|
3
|
+
import { l as logError, a as logWarn } from "./shared-DmpHYKx7.mjs";
|
|
2
4
|
function computed(fn) {
|
|
3
5
|
if (typeof fn !== "function") {
|
|
4
6
|
throw new Error("computed() requires a function");
|
|
@@ -92,6 +94,47 @@ function watch(store, key, callback, { immediate = true } = {}) {
|
|
|
92
94
|
}
|
|
93
95
|
return store.$subscribe(key, callback);
|
|
94
96
|
}
|
|
97
|
+
function resolveTemplateRoot(template, containerEl) {
|
|
98
|
+
let templateEl = template;
|
|
99
|
+
if (template === true) {
|
|
100
|
+
templateEl = containerEl.querySelector("template");
|
|
101
|
+
} else if (typeof template === "string") {
|
|
102
|
+
templateEl = document.querySelector(template);
|
|
103
|
+
}
|
|
104
|
+
if (!templateEl || templateEl.tagName !== "TEMPLATE") {
|
|
105
|
+
throw new Error("[Lume.js] repeat(): template not found or not a <template> element");
|
|
106
|
+
}
|
|
107
|
+
if (templateEl.content.children.length !== 1) {
|
|
108
|
+
throw new Error("[Lume.js] repeat(): template must contain exactly one root element");
|
|
109
|
+
}
|
|
110
|
+
return templateEl.content.firstElementChild;
|
|
111
|
+
}
|
|
112
|
+
function collectItemBindings(el) {
|
|
113
|
+
const bindings = [];
|
|
114
|
+
const add = (node) => {
|
|
115
|
+
const path = node.getAttribute("data-bind");
|
|
116
|
+
bindings.push({ node, path, keys: path === "$item" || path === "$index" ? null : path.split(".") });
|
|
117
|
+
};
|
|
118
|
+
if (el.hasAttribute("data-bind")) add(el);
|
|
119
|
+
for (const node of el.querySelectorAll("[data-bind]")) add(node);
|
|
120
|
+
return bindings;
|
|
121
|
+
}
|
|
122
|
+
function applyItemBindings(bindings, item, index) {
|
|
123
|
+
for (const b of bindings) {
|
|
124
|
+
let val;
|
|
125
|
+
if (b.path === "$index") {
|
|
126
|
+
val = index;
|
|
127
|
+
} else if (b.path === "$item") {
|
|
128
|
+
val = item;
|
|
129
|
+
} else {
|
|
130
|
+
val = item;
|
|
131
|
+
for (let i = 0; i < b.keys.length && val != null; i++) {
|
|
132
|
+
val = val[b.keys[i]];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
applyBindValue(b.node, val);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
95
138
|
function defaultFocusPreservation(container) {
|
|
96
139
|
const activeEl = document.activeElement;
|
|
97
140
|
const shouldRestore = container.contains(activeEl);
|
|
@@ -151,6 +194,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
151
194
|
create,
|
|
152
195
|
update,
|
|
153
196
|
remove,
|
|
197
|
+
template = null,
|
|
154
198
|
element = "div",
|
|
155
199
|
preserveFocus = defaultFocusPreservation,
|
|
156
200
|
preserveScroll = defaultScrollPreservation
|
|
@@ -164,15 +208,21 @@ function repeat(container, store, arrayKey, options) {
|
|
|
164
208
|
if (typeof key !== "function") {
|
|
165
209
|
throw new Error("[Lume.js] repeat(): options.key must be a function");
|
|
166
210
|
}
|
|
167
|
-
|
|
211
|
+
const templateRoot = template ? resolveTemplateRoot(template, containerEl) : null;
|
|
212
|
+
if (templateRoot && typeof render === "function") {
|
|
213
|
+
logWarn("[Lume.js] repeat(): options.render is ignored when options.template is set — use create/update instead");
|
|
214
|
+
}
|
|
215
|
+
if (!templateRoot && typeof render !== "function" && typeof create !== "function") {
|
|
168
216
|
throw new Error("[Lume.js] repeat(): options.render or options.create must be a function");
|
|
169
217
|
}
|
|
170
218
|
const elementsByKey = /* @__PURE__ */ new Map();
|
|
171
219
|
const prevItemsByKey = /* @__PURE__ */ new Map();
|
|
172
220
|
const prevIndexByKey = /* @__PURE__ */ new Map();
|
|
173
221
|
const cleanupByKey = /* @__PURE__ */ new Map();
|
|
222
|
+
const bindingsByKey = /* @__PURE__ */ new Map();
|
|
174
223
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
175
224
|
function createElement() {
|
|
225
|
+
if (templateRoot) return templateRoot.cloneNode(true);
|
|
176
226
|
return typeof element === "function" ? element() : document.createElement(element);
|
|
177
227
|
}
|
|
178
228
|
function reconcileDOM(container2, nextEls) {
|
|
@@ -230,6 +280,9 @@ function repeat(container, store, arrayKey, options) {
|
|
|
230
280
|
if (isFirstRender) {
|
|
231
281
|
el = createElement();
|
|
232
282
|
elementsByKey.set(k, el);
|
|
283
|
+
if (templateRoot) {
|
|
284
|
+
bindingsByKey.set(k, collectItemBindings(el));
|
|
285
|
+
}
|
|
233
286
|
}
|
|
234
287
|
try {
|
|
235
288
|
if (isFirstRender && create) {
|
|
@@ -240,7 +293,12 @@ function repeat(container, store, arrayKey, options) {
|
|
|
240
293
|
}
|
|
241
294
|
const prevItem = prevItemsByKey.get(k);
|
|
242
295
|
const prevIndex = prevIndexByKey.get(k);
|
|
243
|
-
if (
|
|
296
|
+
if (templateRoot) {
|
|
297
|
+
if (prevItem !== item || prevIndex !== i) {
|
|
298
|
+
applyItemBindings(bindingsByKey.get(k), item, i);
|
|
299
|
+
if (update) update(item, el, i, { isFirstRender });
|
|
300
|
+
}
|
|
301
|
+
} else if (update) {
|
|
244
302
|
if (prevItem !== item || prevIndex !== i) {
|
|
245
303
|
update(item, el, i, { isFirstRender });
|
|
246
304
|
}
|
|
@@ -276,6 +334,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
276
334
|
prevItemsByKey.delete(k);
|
|
277
335
|
prevIndexByKey.delete(k);
|
|
278
336
|
cleanupByKey.delete(k);
|
|
337
|
+
bindingsByKey.delete(k);
|
|
279
338
|
}
|
|
280
339
|
}
|
|
281
340
|
}
|
|
@@ -313,6 +372,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
313
372
|
prevItemsByKey.clear();
|
|
314
373
|
prevIndexByKey.clear();
|
|
315
374
|
cleanupByKey.clear();
|
|
375
|
+
bindingsByKey.clear();
|
|
316
376
|
seenKeys.clear();
|
|
317
377
|
};
|
|
318
378
|
}
|
|
@@ -339,6 +399,7 @@ function repeat(container, store, arrayKey, options) {
|
|
|
339
399
|
prevItemsByKey.clear();
|
|
340
400
|
prevIndexByKey.clear();
|
|
341
401
|
cleanupByKey.clear();
|
|
402
|
+
bindingsByKey.clear();
|
|
342
403
|
seenKeys.clear();
|
|
343
404
|
};
|
|
344
405
|
}
|
|
@@ -566,6 +627,7 @@ function withPlugins(store, plugins = []) {
|
|
|
566
627
|
} catch (e) {
|
|
567
628
|
logError(`[Lume.js] Plugin "${p.name}" error in onInit:`, e);
|
|
568
629
|
}
|
|
630
|
+
Object.freeze(p);
|
|
569
631
|
}
|
|
570
632
|
const pendingNotifications = /* @__PURE__ */ new Map();
|
|
571
633
|
function runNotifyHooks() {
|
|
@@ -664,17 +726,108 @@ function createCleanupGroup() {
|
|
|
664
726
|
}
|
|
665
727
|
};
|
|
666
728
|
}
|
|
667
|
-
function hydrateState(selector = "#__LUME_DATA__") {
|
|
729
|
+
function hydrateState(selector = "#__LUME_DATA__", validate) {
|
|
668
730
|
const el = typeof document !== "undefined" ? document.querySelector(selector) : null;
|
|
669
731
|
if (!el) return {};
|
|
732
|
+
if (el.tagName !== "SCRIPT" || el.type !== "application/json") {
|
|
733
|
+
return {};
|
|
734
|
+
}
|
|
735
|
+
let data;
|
|
670
736
|
try {
|
|
671
|
-
|
|
737
|
+
data = JSON.parse(el.textContent);
|
|
672
738
|
} catch {
|
|
673
739
|
return {};
|
|
674
740
|
}
|
|
741
|
+
if (typeof validate === "function" && !validate(data)) {
|
|
742
|
+
return {};
|
|
743
|
+
}
|
|
744
|
+
return data;
|
|
745
|
+
}
|
|
746
|
+
function readStored(storage, storageKey) {
|
|
747
|
+
try {
|
|
748
|
+
const raw = storage.getItem(storageKey);
|
|
749
|
+
if (!raw) return null;
|
|
750
|
+
const data = JSON.parse(raw);
|
|
751
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : null;
|
|
752
|
+
} catch {
|
|
753
|
+
logWarn(`[Lume.js] persist(): could not read "${storageKey}" — starting fresh`);
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function serializeKeys(store, watched) {
|
|
758
|
+
const out = {};
|
|
759
|
+
for (const k of watched) out[k] = store[k];
|
|
760
|
+
return JSON.stringify(out);
|
|
761
|
+
}
|
|
762
|
+
function persist(store, storageKey, options = {}) {
|
|
763
|
+
if (!store || typeof store.$subscribe !== "function") {
|
|
764
|
+
throw new Error("[Lume.js] persist() requires a reactive store from state()");
|
|
765
|
+
}
|
|
766
|
+
if (typeof storageKey !== "string" || storageKey.length === 0) {
|
|
767
|
+
throw new Error("[Lume.js] persist() requires a non-empty storage key");
|
|
768
|
+
}
|
|
769
|
+
const storage = options.storage !== void 0 ? options.storage : globalThis.localStorage;
|
|
770
|
+
if (!storage || typeof storage.getItem !== "function") {
|
|
771
|
+
logWarn("[Lume.js] persist(): no storage available — persistence disabled");
|
|
772
|
+
return () => {
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const watched = Array.isArray(options.keys) && options.keys.length > 0 ? options.keys.slice() : Object.keys(store).filter((k) => !k.startsWith("$"));
|
|
776
|
+
const stored = readStored(storage, storageKey);
|
|
777
|
+
if (stored) {
|
|
778
|
+
for (const k of watched) {
|
|
779
|
+
if (Object.prototype.hasOwnProperty.call(stored, k)) {
|
|
780
|
+
store[k] = stored[k];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
let lastWritten = null;
|
|
785
|
+
try {
|
|
786
|
+
lastWritten = serializeKeys(store, watched);
|
|
787
|
+
} catch {
|
|
788
|
+
}
|
|
789
|
+
let scheduled = false;
|
|
790
|
+
let disposed = false;
|
|
791
|
+
const flushSave = () => {
|
|
792
|
+
scheduled = false;
|
|
793
|
+
if (disposed) return;
|
|
794
|
+
let json;
|
|
795
|
+
try {
|
|
796
|
+
json = serializeKeys(store, watched);
|
|
797
|
+
} catch (err) {
|
|
798
|
+
logWarn("[Lume.js] persist(): state not serializable — skipping save", err);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (json === lastWritten) return;
|
|
802
|
+
try {
|
|
803
|
+
storage.setItem(storageKey, json);
|
|
804
|
+
lastWritten = json;
|
|
805
|
+
} catch (err) {
|
|
806
|
+
logWarn("[Lume.js] persist(): could not write — storage full or unavailable?", err);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
const save = () => {
|
|
810
|
+
if (scheduled) return;
|
|
811
|
+
scheduled = true;
|
|
812
|
+
queueMicrotask(flushSave);
|
|
813
|
+
};
|
|
814
|
+
const unsubs = watched.map((k) => {
|
|
815
|
+
let first = true;
|
|
816
|
+
return store.$subscribe(k, () => {
|
|
817
|
+
if (first) {
|
|
818
|
+
first = false;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
save();
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
return () => {
|
|
825
|
+
disposed = true;
|
|
826
|
+
while (unsubs.length) unsubs.pop()();
|
|
827
|
+
};
|
|
675
828
|
}
|
|
676
829
|
function isReactive(obj) {
|
|
677
|
-
return !!(obj && typeof obj === "object" && typeof obj.$subscribe === "function");
|
|
830
|
+
return !!(obj && typeof obj === "object" && (REACTIVE_BRAND in obj || typeof obj.$subscribe === "function"));
|
|
678
831
|
}
|
|
679
832
|
export {
|
|
680
833
|
computed,
|
|
@@ -685,6 +838,7 @@ export {
|
|
|
685
838
|
defaultScrollPreservation,
|
|
686
839
|
hydrateState,
|
|
687
840
|
isReactive,
|
|
841
|
+
persist,
|
|
688
842
|
repeat,
|
|
689
843
|
watch,
|
|
690
844
|
withPlugins
|