lume-js 2.2.1 → 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 +70 -10
- package/dist/addons.min.mjs +1 -1
- package/dist/addons.mjs +149 -4
- package/dist/addons.mjs.map +1 -1
- package/dist/handlers.min.mjs +1 -1
- package/dist/handlers.mjs +26 -0
- 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/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/core/batch.js +139 -0
- package/src/core/bindDom.js +7 -3
- package/src/core/effect.js +34 -5
- package/src/core/state.js +110 -73
- package/src/handlers/index.d.ts +24 -0
- package/src/handlers/index.js +1 -0
- package/src/handlers/on.js +60 -0
- 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-x2HJmEyO.mjs +0 -260
- package/dist/shared-x2HJmEyO.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -4,18 +4,22 @@
|
|
|
4
4
|
<p><strong>Reactivity that follows web standards.</strong></p>
|
|
5
5
|
<p>
|
|
6
6
|
Minimal reactive state management using only standard JavaScript and HTML.<br>
|
|
7
|
-
No custom syntax · No build step · No framework lock-in
|
|
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>
|
|
8
9
|
</p>
|
|
9
10
|
<p>
|
|
10
11
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
11
12
|
|
|
12
|
-
<a href="package.json"><img src="https://img.shields.io/badge/version-2.
|
|
13
|
+
<a href="package.json"><img src="https://img.shields.io/badge/version-2.3.0-orange.svg" alt="v2.3.0"></a>
|
|
13
14
|
|
|
14
|
-
<a href="tests/"><img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<a href="tests/"><img src="https://img.shields.io/badge/tests-425%20passing-brightgreen.svg" alt="425 tests"></a>
|
|
15
16
|
|
|
16
|
-
<a href="scripts/check-size.js"><img src="https://img.shields.io/badge/
|
|
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>
|
|
17
20
|
</p>
|
|
18
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>
|
|
19
23
|
</div>
|
|
20
24
|
|
|
21
25
|
---
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
|---------|---------|-----------|-----|-------|
|
|
27
31
|
| Custom Syntax | ❌ No | ✅ `x-data` | ✅ `v-bind` | ✅ JSX |
|
|
28
32
|
| Build Step | ❌ Optional | ❌ Optional | ⚠️ Recommended | ✅ Required |
|
|
29
|
-
| Bundle Size |
|
|
33
|
+
| Bundle Size | 1.45–2.66KB | ~15KB | ~35KB | ~45KB |
|
|
30
34
|
| HTML Validation | ✅ Pass | ⚠️ Warnings | ⚠️ Warnings | ❌ JSX |
|
|
31
35
|
| Extensible Handlers | ✅ | ❌ Built-in only | ❌ Built-in only | N/A |
|
|
32
36
|
|
|
@@ -36,6 +40,15 @@
|
|
|
36
40
|
|
|
37
41
|
## Installation
|
|
38
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
|
+
|
|
39
52
|
### Via CDN (Recommended for simple projects)
|
|
40
53
|
|
|
41
54
|
```html
|
|
@@ -44,6 +57,14 @@
|
|
|
44
57
|
</script>
|
|
45
58
|
```
|
|
46
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
|
+
|
|
47
68
|
### Via NPM (Recommended for bundlers)
|
|
48
69
|
|
|
49
70
|
```bash
|
|
@@ -51,9 +72,13 @@ npm install lume-js
|
|
|
51
72
|
```
|
|
52
73
|
|
|
53
74
|
```javascript
|
|
54
|
-
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
|
|
55
77
|
```
|
|
56
78
|
|
|
79
|
+
> **→ Using Lume without a DOM?** See the [Universal core guide](docs/guides/universal-core.md).
|
|
80
|
+
|
|
81
|
+
|
|
57
82
|
### Browser Support
|
|
58
83
|
|
|
59
84
|
| Browser | Minimum version |
|
|
@@ -88,6 +113,37 @@ bindDom(document.body, store);
|
|
|
88
113
|
|
|
89
114
|
That's it — two-way binding, no build step, valid HTML.
|
|
90
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
|
+
|
|
91
147
|
---
|
|
92
148
|
|
|
93
149
|
## Built-in Reactive Attributes
|
|
@@ -149,6 +205,7 @@ bindDom(document.body, store, {
|
|
|
149
205
|
| `ariaAttr(name)` | `data-aria-pressed="key"` | Sets ARIA attribute to "true"/"false" |
|
|
150
206
|
| `classToggle(...names)` | `data-class-active="key"` | Toggles individual CSS classes |
|
|
151
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 |
|
|
152
209
|
| `htmlAttrs()` | *(all of the above)* | One-import preset — all standard HTML + ARIA attrs |
|
|
153
210
|
|
|
154
211
|
### Presets
|
|
@@ -188,7 +245,8 @@ import { computed, watch, repeat } from 'lume-js/addons';
|
|
|
188
245
|
| `effect(fn)` *(core)* | Write derived values back into the store, or trigger side effects on state change |
|
|
189
246
|
| `computed(fn)` | Derive a read-only value from state to consume *outside* the store (templates, display logic) |
|
|
190
247
|
| `watch(store, key, fn)` | React to a *specific* key changing — DOM updates, analytics, syncing external state |
|
|
191
|
-
| `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 |
|
|
192
250
|
| `createCleanupGroup()` | Collect multiple cleanup/unsubscribe functions and dispose them all at once |
|
|
193
251
|
| `hydrateState(selector?)` | Read initial state from a `<script type="application/json">` tag (SSR hydration) |
|
|
194
252
|
|
|
@@ -230,7 +288,7 @@ watch(store, 'count', (val) => {
|
|
|
230
288
|
|
|
231
289
|
## Documentation
|
|
232
290
|
|
|
233
|
-
|
|
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:
|
|
234
292
|
|
|
235
293
|
- **Tutorials**
|
|
236
294
|
- [Build a Todo App](docs/tutorials/build-todo-app.md)
|
|
@@ -240,10 +298,12 @@ Full documentation is available in the [docs/](docs/) directory:
|
|
|
240
298
|
- [state()](docs/api/core/state.md) — Reactive state
|
|
241
299
|
- [bindDom()](docs/api/core/bindDom.md) — DOM binding
|
|
242
300
|
- [effect()](docs/api/core/effect.md) — Reactive effects
|
|
301
|
+
- [batch()](docs/api/core/batch.md) — Cross-store write batching
|
|
243
302
|
- [Handlers](docs/api/core/handlers.md) — Extensible attribute handlers
|
|
244
|
-
- [Plugins](docs/api/
|
|
245
|
-
- [Addons](docs/api/addons/computed.md) — computed, watch, repeat, createCleanupGroup, hydrateState
|
|
303
|
+
- [Plugins](docs/api/addons/withPlugins.md) — State extension system
|
|
304
|
+
- [Addons](docs/api/addons/computed.md) — computed, watch, repeat, persist, createCleanupGroup, hydrateState
|
|
246
305
|
- **Guides**
|
|
306
|
+
- [Universal core (Node, CLI, workers)](docs/guides/universal-core.md) — using `lume-js/state` without a DOM
|
|
247
307
|
- [Choosing reactive primitives](docs/guides/choosing-reactive-primitives.md) — when to use effect vs computed vs watch
|
|
248
308
|
- [Cleanup & Disposal](docs/guides/cleanup-and-dispose.md) — tearing down effects, bindings, and subscriptions
|
|
249
309
|
- [SSR & Hydration](docs/guides/ssr-hydration.md) — server-rendered HTML with reactive hydration
|
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 i=!1;const s=()=>{if(!i){i=!0;try{e()}catch(e){throw t("[Lume.js effect] Error in effect:",e),e}finally{i=!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:s()});c.push(n)}}s()}else{const o=()=>{if(i)return;const s=c.splice(0),l={fn:e,cleanups:c,execute:o,tracking:{}},f=r;r=l,i=!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(...s),t("[Lume.js effect] Error in effect:",e),e}finally{r=f,i=!1}if(c.length>0)for(const e of s)e();else c.push(...s)};o()}return()=>{for(;c.length;)c.pop()()}}function i(e){if("function"!=typeof e)throw Error("computed() requires a function");let o,n=!1,r=!1,i=!1;const s=[],l=c(()=>{if(!r&&!i){r=!0;try{const t=e();n&&Object.is(t,o)||(o=t,n=!0,s.forEach(e=>e(o)))}catch(e){t("[Lume.js computed] Error in computation:",e),n&&void 0===o||(o=void 0,n=!0,s.forEach(e=>e(o)))}finally{queueMicrotask(()=>{i||(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 s.push(e),n&&e(o),()=>{const t=s.indexOf(e);t>-1&&s.splice(t,1)}},dispose(){i=!0,l(),s.length=0,n=!1,r=!1}}}function s(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:i,render:s,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 i)throw Error("[Lume.js] repeat(): options.key must be a function");if("function"!=typeof s&&"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(i(o[e]))){c=!1;break}}E.clear();const l=[];for(let n=0;n<o.length;n++){const r=o[n],c=i(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}):s&&s(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,i=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(),i&&i()}(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)}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 i=e[c];for(const e of o)try{const t=e.onGet?.(c,i);void 0!==t&&(i=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onGet:`,o)}return i},set(e,r,c){const i=e[r];let s=c;for(const e of o)try{const t=e.onSet?.(r,s,i);void 0!==t&&(s=t)}catch(o){t(`[Lume.js] Plugin "${e.name}" error in onSet:`,o)}return Object.is(s,i)||n.set(r,s),e[r]=s,!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__",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 k(e){return!(!e||"object"!=typeof e||"function"!=typeof e.$subscribe)}export{i 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,s 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
|
}
|
|
@@ -682,8 +743,91 @@ function hydrateState(selector = "#__LUME_DATA__", validate) {
|
|
|
682
743
|
}
|
|
683
744
|
return data;
|
|
684
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
|
+
};
|
|
828
|
+
}
|
|
685
829
|
function isReactive(obj) {
|
|
686
|
-
return !!(obj && typeof obj === "object" && typeof obj.$subscribe === "function");
|
|
830
|
+
return !!(obj && typeof obj === "object" && (REACTIVE_BRAND in obj || typeof obj.$subscribe === "function"));
|
|
687
831
|
}
|
|
688
832
|
export {
|
|
689
833
|
computed,
|
|
@@ -694,6 +838,7 @@ export {
|
|
|
694
838
|
defaultScrollPreservation,
|
|
695
839
|
hydrateState,
|
|
696
840
|
isReactive,
|
|
841
|
+
persist,
|
|
697
842
|
repeat,
|
|
698
843
|
watch,
|
|
699
844
|
withPlugins
|