sprae 12.3.8 → 12.4.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/core.js +236 -59
- package/directive/_.js +8 -0
- package/directive/class.js +9 -0
- package/directive/each.js +41 -17
- package/directive/else.js +6 -2
- package/directive/event.js +9 -0
- package/directive/fx.js +5 -0
- package/directive/hidden.js +6 -0
- package/directive/html.js +29 -6
- package/directive/if.js +7 -2
- package/directive/portal.js +38 -0
- package/directive/ref.js +8 -0
- package/directive/scope.js +7 -0
- package/directive/sequence.js +9 -1
- package/directive/spread.js +6 -0
- package/directive/style.js +9 -0
- package/directive/text.js +5 -0
- package/directive/value.js +13 -3
- package/dist/sprae.js +3 -5
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +3 -5
- package/dist/sprae.umd.js.map +4 -4
- package/package.json +35 -8
- package/readme.md +39 -54
- package/signal.js +41 -3
- package/sprae.js +117 -21
- package/store.js +54 -13
- package/types/core.d.ts +222 -0
- package/types/core.d.ts.map +1 -0
- package/types/directive/_.d.ts +3 -0
- package/types/directive/_.d.ts.map +1 -0
- package/types/directive/aria.d.ts.map +1 -0
- package/types/directive/class.d.ts +3 -0
- package/types/directive/class.d.ts.map +1 -0
- package/types/directive/data.d.ts.map +1 -0
- package/types/directive/each.d.ts +6 -0
- package/types/directive/each.d.ts.map +1 -0
- package/types/directive/else.d.ts +3 -0
- package/types/directive/else.d.ts.map +1 -0
- package/types/directive/event.d.ts +5 -0
- package/types/directive/event.d.ts.map +1 -0
- package/types/directive/fx.d.ts +3 -0
- package/types/directive/fx.d.ts.map +1 -0
- package/types/directive/hidden.d.ts +3 -0
- package/types/directive/hidden.d.ts.map +1 -0
- package/types/directive/html.d.ts +3 -0
- package/types/directive/html.d.ts.map +1 -0
- package/types/directive/if.d.ts +3 -0
- package/types/directive/if.d.ts.map +1 -0
- package/types/directive/item.d.ts.map +1 -0
- package/types/directive/portal.d.ts +3 -0
- package/types/directive/portal.d.ts.map +1 -0
- package/types/directive/ref.d.ts +5 -0
- package/types/directive/ref.d.ts.map +1 -0
- package/types/directive/resize.d.ts.map +1 -0
- package/types/directive/scope.d.ts +3 -0
- package/types/directive/scope.d.ts.map +1 -0
- package/types/directive/sequence.d.ts +5 -0
- package/types/directive/sequence.d.ts.map +1 -0
- package/types/directive/spread.d.ts +3 -0
- package/types/directive/spread.d.ts.map +1 -0
- package/types/directive/style.d.ts +3 -0
- package/types/directive/style.d.ts.map +1 -0
- package/types/directive/text.d.ts +3 -0
- package/types/directive/text.d.ts.map +1 -0
- package/types/directive/value.d.ts +4 -0
- package/types/directive/value.d.ts.map +1 -0
- package/types/signal.d.ts +6 -0
- package/types/signal.d.ts.map +1 -0
- package/types/sprae.d.ts +15 -0
- package/types/sprae.d.ts.map +1 -0
- package/types/store.d.ts +15 -0
- package/types/store.d.ts.map +1 -0
- package/directive/aria.js +0 -0
- package/directive/data.js +0 -0
- package/directive/item.js +0 -0
- package/dist/sprae.micro.js +0 -5
- package/dist/sprae.micro.js.map +0 -7
- package/micro.js +0 -56
package/readme.md
CHANGED
|
@@ -1,82 +1,63 @@
|
|
|
1
|
-
# [∴](https://dy.github.io/sprae) spræ [](https://github.com/dy/sprae/actions/workflows/node.js.yml)
|
|
1
|
+
# [∴](https://dy.github.io/sprae) spræ [](https://github.com/dy/sprae/actions/workflows/node.js.yml)  [](https://www.npmjs.com/package/sprae)
|
|
2
2
|
|
|
3
|
-
Ræctive sprinkles for HTML/JSX
|
|
3
|
+
Ræctive sprinkles for HTML/JSX
|
|
4
4
|
|
|
5
5
|
## usage
|
|
6
6
|
|
|
7
7
|
```html
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<button :onclick="
|
|
11
|
-
</
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
<!-- Tabs -->
|
|
9
|
+
<nav :scope="{tab: 'A'}">
|
|
10
|
+
<button :class="{active: tab=='A'}" :onclick="tab='A'">A</button>
|
|
11
|
+
<button :class="{active: tab=='B'}" :onclick="tab='B'">B</button>
|
|
12
|
+
<section :if="tab=='A'">Content A</section>
|
|
13
|
+
<section :if="tab=='B'">Content B</section>
|
|
14
|
+
</nav>
|
|
15
|
+
|
|
16
|
+
<!-- Filter -->
|
|
17
|
+
<input :scope="{q: ''}" :value="q" :oninput="q=e.target.value" placeholder="Search...">
|
|
18
|
+
<ul :each="item in items.filter(i => i.includes(q))">
|
|
19
|
+
<li :text="item"></li>
|
|
20
|
+
</ul>
|
|
21
|
+
|
|
22
|
+
<script type="module" src="//unpkg.com/sprae"></script>
|
|
19
23
|
```
|
|
20
24
|
|
|
21
25
|
## [docs](docs.md)
|
|
22
26
|
|
|
23
27
|
<!-- [start](docs.md#start) [store](docs.md#store) [signals](docs.md#signals) [evaluator](docs.md#evaluator) [jsx](docs.md#jsx) [build](docs.md#custom-build) [hints](docs.md#hints) -->
|
|
24
28
|
|
|
25
|
-
[`:text`](docs.md#text) [`:class`](docs.md#class) [`:style`](docs.md#style) [`:value`](docs.md#value) [`:<attr>`](docs.md#attr-) [`:if :else`](docs.md#if-else) [`:each`](docs.md#each) [`:scope`](docs.md#scope) [`:fx`](docs.md#fx) [`:ref`](docs.md#ref) [`:on<event>`](docs.md#onevent)
|
|
29
|
+
[`:text`](docs.md#text) [`:class`](docs.md#class) [`:style`](docs.md#style) [`:value`](docs.md#value) [`:<attr>`](docs.md#attr-) [`:if :else`](docs.md#if-else) [`:each`](docs.md#each) [`:scope`](docs.md#scope) [`:fx`](docs.md#fx) [`:ref`](docs.md#ref) [`:hidden`](docs.md#hidden) [`:portal`](docs.md#portal) [`:on<event>`](docs.md#onevent)
|
|
26
30
|
|
|
27
31
|
[`.debounce`](docs.md#debounce-ms) [`.throttle`](docs.md#throttle-ms) [`.delay`](docs.md#tick) [`.once`](docs.md#once)<br>
|
|
28
|
-
[`.window`](docs.md#window-document-body-root-parent-
|
|
32
|
+
[`.window`](docs.md#window-document-body-root-parent-away-self) [`.document`](docs.md#window-document-body-root-parent-away-self) [`.root`](docs.md#window-document-body-root-parent-away-self) [`.body`](docs.md#window-document-body-root-parent-away-self) [`.parent`](docs.md#window-document-body-root-parent-away-self) [`.self`](docs.md#window-document-body-root-parent-away-self) [`.away`](docs.md#window-document-body-root-parent-away-self)<br>
|
|
29
33
|
[`.passive`](docs.md#passive-captureevents-only) [`.capture`](docs.md#passive-captureevents-only) [`.prevent`](docs.md#prevent-stop-immediateevents-only) [`.stop`](docs.md#prevent-stop-immediateevents-only) [`.<key>`](docs.md#key-filters)
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
## Micro
|
|
34
|
-
|
|
35
|
-
Micro sprae version is 2.5kb bundle with essentials:
|
|
36
|
+
## vs alpine
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
| | [alpine](alpine.md) | sprae |
|
|
39
|
+
|------------------|--------|-------|
|
|
40
|
+
| _size_ | ~16kb | ~5kb |
|
|
41
|
+
| _performance_ | ~2× slower | 1.00× |
|
|
42
|
+
| _CSP_ | limited | full |
|
|
43
|
+
| _reactivity_ | custom | [signals](docs.md#signals) |
|
|
44
|
+
| _sandboxing_ | no | yes |
|
|
45
|
+
| _typescript_ | partial | full |
|
|
46
|
+
| _JSX/SSR_ | no | [yes](docs.md#jsx) |
|
|
47
|
+
| _prefix_ | `x-`, `:`, `@` | `:` or [custom](docs.md#custom-build) |
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
<sup>[benchmark](https://krausest.github.io/js-framework-benchmark/current.html). CSP via [jessie](docs.md#evaluator).</sup>
|
|
44
50
|
|
|
45
|
-
[wavearea](), [maetr](), [settings-panel]() -->
|
|
46
51
|
|
|
47
|
-
##
|
|
52
|
+
## used by
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
<!--
|
|
51
|
-
<!--Inspired by [preact-signals](https://github.com/preactjs/signals), [alpine](https://github.com/alpinejs/alpine), [lodash](https://lodash.com) and <span title="petite-vue, lucia, nuejs, hmpl, unpoly, dagger">others</span>. <!--[petite-vue](https://github.com/vuejs/petite-vue) and others. -->
|
|
52
|
-
<!-- [lucia](https://github.com/aidenybai/lucia), [nuejs](https://github.com/nuejs/nuejs), [hmpl](https://github.com/hmpl-language/hmpl), [unpoly](https://unpoly.com/up.link), [dagger](https://github.com/dagger8224/dagger.js) -->
|
|
53
|
-
|
|
54
|
-
<!-- Made with 🫰 for better DX. -->
|
|
55
|
-
<!-- – for those who tired of complexity. -->
|
|
54
|
+
[watr](https://dy.github.io/watr/play), [wavearea](https://dy.github.io/wavearea)
|
|
55
|
+
<!-- , [maetr](), [settings-panel]() -->
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
<!--
|
|
59
|
-
|
|
60
|
-
|-----------------------|-------------------|-------------------|------------------|
|
|
61
|
-
| _Size_ | ~10KB | ~6KB | ~5KB |
|
|
62
|
-
| _Memory_ | 5.05 | 3.16 | 2.78 |
|
|
63
|
-
| _Performance_ | 2.64 | 2.43 | 1.76 |
|
|
64
|
-
| _CSP_ | Limited | No | Yes |
|
|
65
|
-
| _SSR_ | No | No | No |
|
|
66
|
-
| _Evaluation_ | [`new AsyncFunction`](https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L81) | [`new Function`](https://github.com/vuejs/petite-vue/blob/main/src/eval.ts#L20) | [`new Function`]() / [justin](https://github.com/dy/subscript) |
|
|
67
|
-
| _Reactivity_ | `Alpine.store` | _@vue/reactivity_ | _signals_ |
|
|
68
|
-
| _Sandboxing_ | No | No | Yes |
|
|
69
|
-
| _Directives_ | `:`, `x-`, `{}` | `:`, `v-`, `@`, `{}` | `:` |
|
|
70
|
-
| _Magic_ | `$data` | `$app` | - |
|
|
71
|
-
| _Fragments_ | Yes | No | Yes |
|
|
72
|
-
| _Plugins_ | Yes | No | Yes |
|
|
73
|
-
| _Modifiers_ | Yes | No | Yes |
|
|
74
|
-
|
|
75
|
-
_Nested directives_ Yes
|
|
76
|
-
_Inline directives_ Yes
|
|
77
|
-
-->
|
|
59
|
+
[lucia](https://github.com/aidenybai/lucia), [nuejs](https://github.com/nuejs/nuejs), [hmpl](https://github.com/hmpl-language/hmpl), [unpoly](https://unpoly.com/up.link), [dagger](https://github.com/dagger8224/dagger.js), [petite-vue](https://github.com/vuejs/petite-vue)
|
|
78
60
|
|
|
79
|
-
<!--
|
|
80
61
|
### Drops
|
|
81
62
|
|
|
82
63
|
* ToDo MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
|
|
@@ -85,3 +66,7 @@ _Inline directives_ Yes
|
|
|
85
66
|
* Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
|
|
86
67
|
* Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)-->
|
|
87
68
|
<!-- * Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/) -->
|
|
69
|
+
|
|
70
|
+
<p align='center'>
|
|
71
|
+
<a href="https://krishnized.github.io/license">ॐ</a>
|
|
72
|
+
</p>
|
package/signal.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Minimal signals implementation (preact-signals compatible)
|
|
3
|
+
* @module sprae/signal
|
|
4
|
+
*/
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
/** @type {import('./core.js').EffectFn | null} */
|
|
7
|
+
let current
|
|
5
8
|
|
|
9
|
+
let depth = 0
|
|
10
|
+
|
|
11
|
+
/** @type {Set<import('./core.js').EffectFn> | null} */
|
|
12
|
+
let batched;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a reactive signal.
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {T} v - Initial value
|
|
18
|
+
* @returns {import('./core.js').Signal<T>}
|
|
19
|
+
*/
|
|
6
20
|
export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
|
|
7
21
|
_s = {
|
|
8
22
|
get value() {
|
|
@@ -19,6 +33,11 @@ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
|
|
|
19
33
|
}
|
|
20
34
|
)
|
|
21
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Creates a reactive effect that re-runs when dependencies change.
|
|
38
|
+
* @param {() => void | (() => void)} fn - Effect function, may return cleanup
|
|
39
|
+
* @returns {() => void} Dispose function
|
|
40
|
+
*/
|
|
22
41
|
export const effect = (fn, _teardown, _fx, _deps) => (
|
|
23
42
|
_fx = (prev) => {
|
|
24
43
|
let tmp = _teardown;
|
|
@@ -35,6 +54,12 @@ export const effect = (fn, _teardown, _fx, _deps) => (
|
|
|
35
54
|
(dep) => { _teardown?.call?.(); for (dep of _deps) dep.delete(_fx); _deps.clear() }
|
|
36
55
|
)
|
|
37
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Creates a computed signal derived from other signals.
|
|
59
|
+
* @template T
|
|
60
|
+
* @param {() => T} fn - Computation function
|
|
61
|
+
* @returns {import('./core.js').Signal<T>}
|
|
62
|
+
*/
|
|
38
63
|
export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
|
|
39
64
|
_c = {
|
|
40
65
|
get value() {
|
|
@@ -46,10 +71,23 @@ export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
|
|
|
46
71
|
}
|
|
47
72
|
)
|
|
48
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Batches multiple signal updates into a single notification.
|
|
76
|
+
* @template T
|
|
77
|
+
* @param {() => T} fn - Function containing updates
|
|
78
|
+
* @returns {T}
|
|
79
|
+
*/
|
|
49
80
|
export const batch = (fn, _first = !batched, _list) => {
|
|
50
81
|
batched ??= new Set;
|
|
51
82
|
try { fn(); }
|
|
52
83
|
finally { if (_first) { [batched, _list] = [null, batched]; for (const fx of _list) fx(); } }
|
|
53
84
|
}
|
|
54
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Runs a function without tracking dependencies.
|
|
88
|
+
* @template T
|
|
89
|
+
* @param {() => T} fn - Function to run untracked
|
|
90
|
+
* @returns {T}
|
|
91
|
+
*/
|
|
55
92
|
export const untracked = (fn, _prev, _v) => (_prev = current, current = null, _v = fn(), current = _prev, _v)
|
|
93
|
+
|
package/sprae.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sprae - lightweight reactive HTML templating library
|
|
3
|
+
* @module sprae
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
import store from "./store.js";
|
|
2
7
|
import { batch, computed, effect, signal, untracked } from './core.js';
|
|
3
8
|
import * as signals from './signal.js';
|
|
@@ -17,6 +22,9 @@ import _default from "./directive/_.js";
|
|
|
17
22
|
import _spread from "./directive/spread.js";
|
|
18
23
|
import _event from "./directive/event.js";
|
|
19
24
|
import _seq from "./directive/sequence.js";
|
|
25
|
+
import _html from "./directive/html.js";
|
|
26
|
+
import _portal from "./directive/portal.js";
|
|
27
|
+
import _hidden from "./directive/hidden.js";
|
|
20
28
|
|
|
21
29
|
|
|
22
30
|
Object.assign(directive, {
|
|
@@ -24,6 +32,7 @@ Object.assign(directive, {
|
|
|
24
32
|
'': _spread,
|
|
25
33
|
class: _class,
|
|
26
34
|
text: _text,
|
|
35
|
+
html: _html,
|
|
27
36
|
style: _style,
|
|
28
37
|
fx: _fx,
|
|
29
38
|
value: _value,
|
|
@@ -31,14 +40,20 @@ Object.assign(directive, {
|
|
|
31
40
|
scope: _scope,
|
|
32
41
|
if: _if,
|
|
33
42
|
else: _else,
|
|
34
|
-
each: _each
|
|
43
|
+
each: _each,
|
|
44
|
+
portal: _portal,
|
|
45
|
+
hidden: _hidden
|
|
35
46
|
})
|
|
36
47
|
|
|
37
48
|
|
|
38
49
|
/**
|
|
39
|
-
* Directive initializer
|
|
40
|
-
* @
|
|
41
|
-
*
|
|
50
|
+
* Directive initializer with modifiers support.
|
|
51
|
+
* @param {Element} target - Target element
|
|
52
|
+
* @param {string} name - Directive name with modifiers (e.g., 'onclick.throttle-500')
|
|
53
|
+
* @param {string} expr - Expression string
|
|
54
|
+
* @param {Object} state - Reactive state object
|
|
55
|
+
* @returns {() => (() => void) | void} Initializer function that returns a disposer
|
|
56
|
+
*/
|
|
42
57
|
const dir = (target, name, expr, state) => {
|
|
43
58
|
let [dirName, ...mods] = name.split('.'), create = directive[dirName] || directive._
|
|
44
59
|
|
|
@@ -64,37 +79,75 @@ const dir = (target, name, expr, state) => {
|
|
|
64
79
|
}
|
|
65
80
|
}
|
|
66
81
|
|
|
82
|
+
// Parses time string to ms: 100, 100ms, 1s, 1m
|
|
83
|
+
const parseTime = (t) => !t ? 0 : typeof t === 'number' ? t :
|
|
84
|
+
(([, n, u] = t.match(/^(\d+)(ms|s|m)?$/) || []) => (n = +n, u === 's' ? n * 1000 : u === 'm' ? n * 60000 : n))()
|
|
85
|
+
|
|
86
|
+
// Creates scheduler from time/keyword (idle, raf, tick, or ms)
|
|
87
|
+
const scheduler = (t) =>
|
|
88
|
+
t === 'idle' ? requestIdleCallback :
|
|
89
|
+
t === 'raf' ? requestAnimationFrame :
|
|
90
|
+
!t || t === 'tick' ? queueMicrotask :
|
|
91
|
+
(fn) => setTimeout(fn, parseTime(t))
|
|
92
|
+
|
|
93
|
+
// Built-in modifiers for timing, targeting, and event handling
|
|
67
94
|
Object.assign(modifier, {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Delays callback by interval since last call (trailing edge).
|
|
97
|
+
* Supports: tick (default), raf, idle, N, Nms, Ns, Nm. Add -immediate for leading edge.
|
|
98
|
+
* Examples: .debounce, .debounce-100, .debounce-1s, .debounce-raf, .debounce-idle, .debounce-100-immediate
|
|
99
|
+
*/
|
|
100
|
+
debounce: (fn, a, b) => debounce(fn, scheduler(a === 'immediate' ? b : a), a === 'immediate' || b === 'immediate'),
|
|
101
|
+
/**
|
|
102
|
+
* Limits callback rate to interval (leading + trailing edges).
|
|
103
|
+
* Supports: tick (default), raf, idle, N, Nms, Ns, Nm.
|
|
104
|
+
* Examples: .throttle, .throttle-100, .throttle-1s, .throttle-raf, .throttle-idle
|
|
105
|
+
*/
|
|
106
|
+
throttle: (fn, a) => throttle(fn, scheduler(a)),
|
|
107
|
+
/** Runs callback after delay. Supports: tick (default), raf, idle, N, Nms, Ns, Nm. */
|
|
108
|
+
delay: (fn, a) => ((sched = scheduler(a)) => (e) => sched(() => fn(e)))(),
|
|
73
109
|
|
|
110
|
+
/** @deprecated Use .delay instead */
|
|
74
111
|
tick: (fn) => (console.warn('Deprecated'), (e) => (queueMicrotask(() => fn(e)))),
|
|
112
|
+
/** @deprecated Use .throttle-raf instead */
|
|
75
113
|
raf: (fn) => (console.warn('Deprecated'), (e) => requestAnimationFrame(() => fn(e))),
|
|
76
114
|
|
|
115
|
+
/** Calls handler only once. */
|
|
77
116
|
once: (fn, _done, _fn) => (_fn = (e) => !_done && (_done = 1, fn(e)), _fn.once = true, _fn),
|
|
78
117
|
|
|
79
|
-
|
|
118
|
+
/** Attaches event listener to window. */
|
|
80
119
|
window: fn => (fn.target = fn.target.ownerDocument.defaultView, fn),
|
|
120
|
+
/** Attaches event listener to document. */
|
|
81
121
|
document: fn => (fn.target = fn.target.ownerDocument, fn),
|
|
122
|
+
/** Attaches event listener to document root element (<html>). */
|
|
82
123
|
root: fn => (fn.target = fn.target.ownerDocument.documentElement, fn),
|
|
124
|
+
/** Attaches event listener to body. */
|
|
83
125
|
body: fn => (fn.target = fn.target.ownerDocument.body, fn),
|
|
126
|
+
/** Attaches event listener to parent element. */
|
|
84
127
|
parent: fn => (fn.target = fn.target.parentNode, fn),
|
|
128
|
+
/** Triggers only when event target is the element itself. */
|
|
85
129
|
self: (fn) => (e) => (e.target === fn.target && fn(e)),
|
|
130
|
+
/** Triggers when click is outside the element. */
|
|
86
131
|
away: (fn) => Object.assign((e) => (!fn.target.contains(e.target) && e.target.isConnected && fn(e)), {target: fn.target.ownerDocument}),
|
|
87
132
|
|
|
88
|
-
|
|
133
|
+
/** Calls preventDefault() before handler. */
|
|
89
134
|
prevent: (fn) => (e) => (e?.preventDefault(), fn(e)),
|
|
135
|
+
/** Calls stopPropagation() or stopImmediatePropagation() (with -immediate). */
|
|
90
136
|
stop: (fn, _how) => (e) => (_how?.[0] === 'i' ? e?.stopImmediatePropagation() : e?.stopPropagation(), fn(e)),
|
|
137
|
+
/** @deprecated Use .stop-immediate instead */
|
|
91
138
|
immediate: (fn) => (console.warn('Deprecated'), (e) => (e?.stopImmediatePropagation(), fn(e))),
|
|
139
|
+
/** Sets passive option for event listener. */
|
|
92
140
|
passive: fn => (fn.passive = true, fn),
|
|
141
|
+
/** Sets capture option for event listener. */
|
|
93
142
|
capture: fn => (fn.capture = true, fn),
|
|
94
143
|
})
|
|
144
|
+
/** Alias for .away modifier */
|
|
95
145
|
modifier.outside = modifier.away
|
|
96
146
|
|
|
97
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Key testers for keyboard event modifiers.
|
|
149
|
+
* @type {Record<string, (e: KeyboardEvent) => boolean>}
|
|
150
|
+
*/
|
|
98
151
|
const keys = {
|
|
99
152
|
ctrl: e => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
|
|
100
153
|
shift: e => e.shiftKey || e.key === "Shift",
|
|
@@ -112,12 +165,37 @@ const keys = {
|
|
|
112
165
|
char: e => /^\S$/.test(e.key),
|
|
113
166
|
};
|
|
114
167
|
|
|
115
|
-
//
|
|
116
|
-
|
|
168
|
+
// match key by name, or by e.key (case-insensitive), or by keyCode (digits)
|
|
169
|
+
const keyMatch = (k, e) => keys[k]?.(e) || e.key.toLowerCase() === k || e.keyCode == k
|
|
170
|
+
|
|
171
|
+
// Augment modifiers with key testers (e.g., .enter, .ctrl, .ctrl-a, .ctrl-65)
|
|
172
|
+
for (let k in keys) modifier[k] = (fn, a, b) => (e) => keys[k](e) && (!a || keyMatch(a, e)) && (!b || keyMatch(b, e)) && fn(e)
|
|
117
173
|
|
|
118
174
|
|
|
175
|
+
// Checks for first-level semicolons (statement vs expression)
|
|
176
|
+
const hasSemi = s => {
|
|
177
|
+
for (let d=0,i=0;i<s.length;i++) {
|
|
178
|
+
if (s[i]=='{') d++
|
|
179
|
+
else if (s[i]=='}') d--
|
|
180
|
+
else if (s[i]==';' && !d) return true
|
|
181
|
+
}
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Configure sprae with default compiler and signals
|
|
119
186
|
use({
|
|
120
|
-
|
|
187
|
+
|
|
188
|
+
// Default compiler wraps expression for new Function
|
|
189
|
+
compile: expr => {
|
|
190
|
+
// if, const, let - no return
|
|
191
|
+
if (/^(if|let|const)\b/.test(expr));
|
|
192
|
+
// first-level semicolons - no return
|
|
193
|
+
else if (hasSemi(expr));
|
|
194
|
+
else expr = `return ${expr}`
|
|
195
|
+
// async expression
|
|
196
|
+
if (/\bawait\s/.test(expr)) expr = `return (async()=>{${expr}})()`
|
|
197
|
+
return sprae.constructor(`with(arguments[0]){${expr}}`)
|
|
198
|
+
},
|
|
121
199
|
dir: (el, name, expr, state) => {
|
|
122
200
|
// sequences shortcut
|
|
123
201
|
if (name.includes('..')) return () => _seq(el, state, expr, name)[_dispose]
|
|
@@ -130,15 +208,32 @@ use({
|
|
|
130
208
|
})
|
|
131
209
|
|
|
132
210
|
|
|
133
|
-
//
|
|
211
|
+
// Expose for runtime configuration
|
|
134
212
|
sprae.use = use
|
|
135
213
|
sprae.store = store
|
|
136
214
|
sprae.directive = directive
|
|
137
215
|
sprae.modifier = modifier
|
|
138
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Disposes a spraed element, cleaning up all effects and state.
|
|
219
|
+
* @param {Element} el - Element to dispose
|
|
220
|
+
*/
|
|
221
|
+
sprae.dispose = (el) => el[_dispose]?.()
|
|
222
|
+
|
|
139
223
|
|
|
140
224
|
/**
|
|
141
|
-
*
|
|
225
|
+
* Auto-initializes sprae on dynamically added elements.
|
|
226
|
+
* Uses MutationObserver to detect new DOM nodes and apply directives.
|
|
227
|
+
*
|
|
228
|
+
* @param {Element} [root=document.body] - Root element to observe
|
|
229
|
+
* @param {Object} [values] - Initial state values
|
|
230
|
+
* @returns {Object} The reactive state object
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```js
|
|
234
|
+
* // Auto-init on page load
|
|
235
|
+
* sprae.start(document.body, { count: 0 })
|
|
236
|
+
* ```
|
|
142
237
|
*/
|
|
143
238
|
const start = sprae.start = (root = document.body, values) => {
|
|
144
239
|
const state = store(values)
|
|
@@ -150,10 +245,9 @@ const start = sprae.start = (root = document.body, values) => {
|
|
|
150
245
|
if (el.nodeType === 1 && el[_state] === undefined && root.contains(el)) {
|
|
151
246
|
// even if element has no spraeable attrs, some of its children can have
|
|
152
247
|
root[_add](el)
|
|
153
|
-
// sprae(el, state, root);
|
|
154
248
|
}
|
|
155
249
|
}
|
|
156
|
-
|
|
250
|
+
for (const el of m.removedNodes) el.nodeType === 1 && el[_dispose]?.()
|
|
157
251
|
}
|
|
158
252
|
});
|
|
159
253
|
mo.observe(root, { childList: true, subtree: true });
|
|
@@ -161,8 +255,10 @@ const start = sprae.start = (root = document.body, values) => {
|
|
|
161
255
|
}
|
|
162
256
|
|
|
163
257
|
|
|
164
|
-
|
|
258
|
+
/** Package version (injected by bundler) */
|
|
165
259
|
sprae.version = "[VI]{{inject}}[/VI]"
|
|
166
260
|
|
|
261
|
+
const dispose = sprae.dispose
|
|
262
|
+
|
|
167
263
|
export default sprae
|
|
168
|
-
export { sprae, store, signal, effect, computed, batch, untracked, start, use }
|
|
264
|
+
export { sprae, store, signal, effect, computed, batch, untracked, start, use, throttle, debounce, dispose }
|
package/store.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Signals-powered reactive proxy store
|
|
3
|
+
* @module sprae/store
|
|
4
|
+
*/
|
|
5
|
+
|
|
2
6
|
import { signal, computed, batch, untracked } from './core.js'
|
|
3
7
|
|
|
8
|
+
/** Symbol for accessing the internal signals map */
|
|
9
|
+
export const _signals = Symbol('signals')
|
|
10
|
+
|
|
11
|
+
/** Symbol for the change signal that tracks object keys or array length */
|
|
12
|
+
export const _change = Symbol('change')
|
|
4
13
|
|
|
5
|
-
|
|
6
|
-
export const
|
|
7
|
-
// _change is a signal that tracks changes to the object keys or array length
|
|
8
|
-
_change = Symbol('change'),
|
|
9
|
-
// _set is stashed setter for computed values
|
|
10
|
-
_set = Symbol('set')
|
|
14
|
+
/** Symbol for stashed setter on computed values */
|
|
15
|
+
export const _set = Symbol('set')
|
|
11
16
|
|
|
12
17
|
// a hack to simulate sandbox for `with` in evaluator
|
|
13
18
|
let sandbox = true
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Reactive store with signals backing.
|
|
22
|
+
* @template T
|
|
23
|
+
* @typedef {T & { [_signals]: Record<string | symbol, import('./core.js').Signal<any>> }} ReactiveStore
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a reactive proxy store from an object or array.
|
|
28
|
+
* Properties become signals for fine-grained reactivity.
|
|
29
|
+
* Supports nested objects, arrays, computed getters, and methods.
|
|
30
|
+
*
|
|
31
|
+
* @template {Object} T
|
|
32
|
+
* @param {T} values - Initial values object
|
|
33
|
+
* @param {Object} [parent] - Parent scope for inheritance
|
|
34
|
+
* @returns {ReactiveStore<T>} Reactive proxy store
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const state = store({ count: 0, get doubled() { return this.count * 2 } })
|
|
38
|
+
* state.count = 5 // triggers updates
|
|
39
|
+
* state.doubled // 10 (computed)
|
|
40
|
+
*/
|
|
17
41
|
export const store = (values, parent) => {
|
|
18
42
|
if (!values) return values
|
|
19
43
|
|
|
@@ -41,7 +65,7 @@ export const store = (values, parent) => {
|
|
|
41
65
|
return parent ? parent[k] : (typeof globalThis[k] === 'function' && !globalThis[k].prototype ? globalThis[k].bind(globalThis) : globalThis[k])
|
|
42
66
|
},
|
|
43
67
|
|
|
44
|
-
set: (_, k, v
|
|
68
|
+
set: (_, k, v) => {
|
|
45
69
|
// console.group('SET', k, v)
|
|
46
70
|
if (k in signals) return set(signals, k, v), 1
|
|
47
71
|
|
|
@@ -99,7 +123,13 @@ export const store = (values, parent) => {
|
|
|
99
123
|
return state
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Creates a reactive array store with lazy signal initialization.
|
|
128
|
+
* Arrays can be large, so signals are created on-demand.
|
|
129
|
+
* @param {any[]} values - Initial array values
|
|
130
|
+
* @param {Object} [parent=globalThis] - Parent scope
|
|
131
|
+
* @returns {ReactiveStore<any[]>} Reactive array proxy
|
|
132
|
+
*/
|
|
103
133
|
const list = (values, parent = globalThis) => {
|
|
104
134
|
|
|
105
135
|
// gotta fill with null since proto methods like .reduce may fail
|
|
@@ -168,10 +198,21 @@ const list = (values, parent = globalThis) => {
|
|
|
168
198
|
return state
|
|
169
199
|
}
|
|
170
200
|
|
|
171
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Creates a signal for a property value.
|
|
203
|
+
* Skips wrapping for untracked props (underscore prefix) and existing signals.
|
|
204
|
+
* @param {Object} signals - Signals storage object
|
|
205
|
+
* @param {string} k - Property key
|
|
206
|
+
* @param {any} v - Property value
|
|
207
|
+
*/
|
|
172
208
|
const create = (signals, k, v) => (signals[k] = (k[0] == '_' || v?.peek) ? v : signal(store(v)))
|
|
173
209
|
|
|
174
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Updates a signal value, handling arrays specially for efficient patching.
|
|
212
|
+
* @param {Object} signals - Signals storage object
|
|
213
|
+
* @param {string} k - Property key
|
|
214
|
+
* @param {any} v - New value
|
|
215
|
+
*/
|
|
175
216
|
const set = (signals, k, v, _s, _v) => {
|
|
176
217
|
// skip unchanged (although can be handled by last condition - we skip a few checks this way)
|
|
177
218
|
return k[0] === '_' ? (signals[k] = v) :
|