sprae 13.3.7 → 13.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 +47 -22
- package/directive/each.js +58 -30
- package/directive/if.js +23 -0
- package/dist/sprae-csp.js +11 -11
- package/dist/sprae-csp.js.map +4 -4
- package/dist/sprae-csp.umd.js +11 -11
- package/dist/sprae-csp.umd.js.map +4 -4
- package/dist/sprae-preact.js +4 -4
- package/dist/sprae-preact.js.map +4 -4
- package/dist/sprae-preact.umd.js +4 -4
- package/dist/sprae-preact.umd.js.map +4 -4
- package/dist/sprae.js +4 -4
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +4 -4
- package/dist/sprae.umd.js.map +4 -4
- package/package.json +1 -1
- package/readme.md +7 -7
- package/signal.js +33 -26
- package/sprae.js +26 -19
- package/store.js +36 -10
- package/types/core.d.ts +4 -20
- package/types/core.d.ts.map +1 -1
- package/types/directive/each.d.ts.map +1 -1
- package/types/directive/if.d.ts +1 -0
- package/types/directive/if.d.ts.map +1 -1
- package/types/signal.d.ts +3 -3
- package/types/signal.d.ts.map +1 -1
- package/types/sprae.d.ts.map +1 -1
- package/types/store.d.ts +2 -0
- package/types/store.d.ts.map +1 -1
- package/directive/else.js +0 -24
- package/types/directive/else.d.ts +0 -3
- package/types/directive/else.d.ts.map +0 -1
package/core.js
CHANGED
|
@@ -140,14 +140,19 @@ const err = (e, expr, el = currentEl) => {
|
|
|
140
140
|
* @property {Record<string, Signal>} [_signals] - Internal signals map
|
|
141
141
|
*/
|
|
142
142
|
|
|
143
|
+
/** Symbol for cached directive scan on clone masters */
|
|
144
|
+
const _dirs = Symbol('dirs')
|
|
145
|
+
|
|
143
146
|
/**
|
|
144
147
|
* Applies directives to an HTML element and manages its reactive state.
|
|
145
148
|
*
|
|
146
149
|
* @param {Element} [el=document.body] - The target HTML element to apply directives to.
|
|
147
150
|
* @param {Object} [state] - Initial state values to populate the element's reactive state.
|
|
151
|
+
* @param {Element} [master] - Internal: clone source (eg. :each template) — first clone records
|
|
152
|
+
* its directive scan on the master, later clones replay it without touching attributes.
|
|
148
153
|
* @returns {SpraeState & Object} The reactive state object associated with the element.
|
|
149
154
|
*/
|
|
150
|
-
const sprae = (root = document.body, state) => {
|
|
155
|
+
const sprae = (root = document.body, state, master) => {
|
|
151
156
|
// repeated call can be caused by eg. :each with new objects with old keys
|
|
152
157
|
if (root[_state]) return Object.assign(root[_state], state)
|
|
153
158
|
|
|
@@ -181,25 +186,41 @@ const sprae = (root = document.body, state) => {
|
|
|
181
186
|
el[_off] = el[_on] = el[_dispose] = el[_add] = el[_state] = null
|
|
182
187
|
}
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
// apply one directive to el; true = stop (subsprae directives like :each/:if/:scope change state identity)
|
|
190
|
+
const apply = (el, name, short, value, prev = el[_state]) => (
|
|
191
|
+
currentDir = name, currentEl = el,
|
|
192
|
+
// directive initializer can be redefined
|
|
193
|
+
fx.push(start = dir(el, short, value, state)), offs.push(start()),
|
|
194
|
+
el[_state] !== prev
|
|
195
|
+
)
|
|
196
|
+
let start
|
|
197
|
+
|
|
198
|
+
const add = el[_add] = (el, mel) => {
|
|
199
|
+
let dirs = mel?.[_dirs]
|
|
200
|
+
|
|
201
|
+
// replay master's recorded scan — no attribute reads, no parsing
|
|
202
|
+
if (dirs !== undefined) {
|
|
203
|
+
for (let [name, short, value] of dirs) {
|
|
204
|
+
el.removeAttribute(name) // clones from the recording batch still carry attrs; no-op for later ones
|
|
205
|
+
if (apply(el, name, short, value)) return
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
let _attrs = el.attributes, rec = mel && (mel[_dirs] = [])
|
|
192
210
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
currentEl = el;
|
|
211
|
+
if (_attrs) for (let i = 0; i < _attrs.length;) {
|
|
212
|
+
let { name, value } = _attrs[i]
|
|
196
213
|
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
if (name.startsWith(prefix)) {
|
|
215
|
+
el.removeAttribute(name)
|
|
216
|
+
// strip master too: later clones come out clean; unprocessed tail (after a stop) stays for subsprae
|
|
217
|
+
mel?.removeAttribute(name)
|
|
218
|
+
let short = name.slice(prefix.length)
|
|
219
|
+
rec?.push([name, short, value])
|
|
199
220
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
221
|
+
if (apply(el, name, short, value)) return
|
|
222
|
+
} else i++
|
|
223
|
+
}
|
|
203
224
|
}
|
|
204
225
|
|
|
205
226
|
// custom elements own their children — don't descend
|
|
@@ -208,13 +229,14 @@ const sprae = (root = document.body, state) => {
|
|
|
208
229
|
// :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
|
|
209
230
|
// real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
|
|
210
231
|
if (el.firstChild !== undefined) {
|
|
211
|
-
|
|
212
|
-
|
|
232
|
+
// master is never mutated structurally, so its pointers stay aligned with pre-captured clone pointers
|
|
233
|
+
let child = el.firstChild, mchild = mel?.firstChild, next
|
|
234
|
+
while (child) (next = child.nextSibling, child.nodeType == 1 && add(child, mchild), mchild &&= mchild.nextSibling, child = next)
|
|
213
235
|
}
|
|
214
236
|
else for (let child of el.childNodes) child.nodeType == 1 && add(child)
|
|
215
237
|
};
|
|
216
238
|
|
|
217
|
-
add(el);
|
|
239
|
+
add(el, master);
|
|
218
240
|
|
|
219
241
|
currentDir = currentEl = null;
|
|
220
242
|
|
|
@@ -443,8 +465,11 @@ export const clsx = (c) => !c ? '' : typeof c === 'string' ? c : (
|
|
|
443
465
|
* @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
|
|
444
466
|
* @returns {T} Throttled function
|
|
445
467
|
*/
|
|
468
|
+
/** Makes a scheduler from ms delay, custom scheduler fn, or default microtask */
|
|
469
|
+
const sched = (ms) => typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask
|
|
470
|
+
|
|
446
471
|
export const throttle = (fn, ms) => {
|
|
447
|
-
let _planned = 0, _depth = 0, arg, schedule =
|
|
472
|
+
let _planned = 0, _depth = 0, arg, schedule = sched(ms);
|
|
448
473
|
const throttled = (e) => {
|
|
449
474
|
arg = e
|
|
450
475
|
if (!_planned++) fn(arg), schedule(() => {
|
|
@@ -468,7 +493,7 @@ export const throttle = (fn, ms) => {
|
|
|
468
493
|
* @returns {T} Debounced function
|
|
469
494
|
*/
|
|
470
495
|
export const debounce = (fn, ms, immediate) => {
|
|
471
|
-
let schedule =
|
|
496
|
+
let schedule = sched(ms);
|
|
472
497
|
return immediate
|
|
473
498
|
? ((_blocked) => (arg) => !_blocked && (fn(arg), _blocked = 1, schedule(() => _blocked = 0)))()
|
|
474
499
|
: ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
|
package/directive/each.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import sprae, { parse, _state, _off, effect, _change, _signals, frag, throttle, mutate } from "../core.js"
|
|
1
|
+
import sprae, { parse, _state, _off, effect, untracked, _change, _touch, _signals, frag, throttle, mutate } from "../core.js"
|
|
2
2
|
|
|
3
|
-
// Row scope
|
|
4
|
-
const
|
|
5
|
-
get: (s, k) => k === s.v ? s.c
|
|
6
|
-
set: (s, k, v) => (k === s.v ? (s.c
|
|
7
|
-
has: () => true
|
|
8
|
-
}
|
|
9
|
-
const keyHandler = {
|
|
10
|
-
get: (s, k) => k === s.v ? s.r : k === s.k ? s.i : k === _signals ? s : s.l?.[k] !== undefined ? s.l[k] : s.p?.[k],
|
|
11
|
-
set: (s, k, v) => (k === s.v ? (s.r = v) : k === s.k ? 0 : s.l?.[k] !== undefined ? ((s.l[k] = v), 0) : k in (s.p?.[_signals] || {}) ? (s.p[k] = v) : (s.l ||= {})[k] = v, 1),
|
|
3
|
+
// Row scope proxy — positional rows (s.c = source array) read item live via c[i], keyed rows hold direct ref s.r
|
|
4
|
+
const rowHandler = {
|
|
5
|
+
get: (s, k) => k === s.v ? (s.c ? s.c[s.i] : s.r) : k === s.k ? (s.o ? s.o[s.i] : s.i) : k === _signals ? s : s.l?.[k] !== undefined ? s.l[k] : s.p?.[k],
|
|
6
|
+
set: (s, k, v) => (k === s.v ? (s.c ? s.c[s.i] = v : s.r = v) : k === s.k ? 0 : s.l?.[k] !== undefined ? ((s.l[k] = v), 0) : k in (s.p?.[_signals] || {}) ? (s.p[k] = v) : (s.l ||= {})[k] = v, 1),
|
|
12
7
|
has: () => true
|
|
13
8
|
}
|
|
14
9
|
|
|
15
|
-
const rm = r => { r.el[Symbol.dispose]?.(); r.el.remove() }
|
|
16
10
|
|
|
17
11
|
/**
|
|
18
12
|
* Each directive - renders list items from array/object/number.
|
|
@@ -22,17 +16,23 @@ const rm = r => { r.el[Symbol.dispose]?.(); r.el.remove() }
|
|
|
22
16
|
* Primitives use positional (index-based) mode.
|
|
23
17
|
*/
|
|
24
18
|
export default (tpl, state, expr) => {
|
|
25
|
-
|
|
19
|
+
// first standalone `in`/`of` splits the expression — `\b` on both sides so `includes`, `index`, `typeof` etc. in rhs don't match
|
|
20
|
+
const [, lhs, rhs] = expr.match(/^(.*?)\b(?:in|of)\b(.*)$/s) || []
|
|
26
21
|
let [itemVar, idxVar = "$"] = lhs.trim().replace(/\(|\)/g, '').split(/\s*,\s*/)
|
|
27
22
|
|
|
28
23
|
let doc = tpl.ownerDocument
|
|
29
24
|
let holder = tpl._eachHolder || (tpl._eachHolder = doc.createTextNode(""))
|
|
30
25
|
let rowMap = new Map, rows = [], items, keys, cur, keyed = false
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
// removal always evicts from rowMap — every path (keyed diff, positional shrink, clear) must, or rows leak
|
|
28
|
+
let rm = r => { rowMap.delete(r.scope.r); r.el.remove(); r.el[Symbol.dispose]?.() }
|
|
29
|
+
|
|
30
|
+
// _di tracks current DOM index so swap/reorder only touches actually-moved rows
|
|
31
|
+
// scope shape is uniform (keyed: r, positional: c/o) — monomorphic for the proxy traps
|
|
32
|
+
let mkrow = (r, c, i) => {
|
|
33
|
+
let scope = { p: state, v: itemVar, k: idxVar, r, c, i, o: keys, l: null }
|
|
34
34
|
let el = tpl.content ? frag(tpl) : tpl.cloneNode(true)
|
|
35
|
-
return { el, scope, proxy, node: el.content || el }
|
|
35
|
+
return { el, scope, proxy: new Proxy(scope, rowHandler), node: el.content || el, _di: i }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
let insert = pending => {
|
|
@@ -40,11 +40,14 @@ export default (tpl, state, expr) => {
|
|
|
40
40
|
let f = pending.length > 1 ? doc.createDocumentFragment() : null
|
|
41
41
|
for (let r of pending) f ? f.appendChild(r.node) : holder.before(r.node)
|
|
42
42
|
if (f) holder.before(f)
|
|
43
|
-
|
|
43
|
+
// element rows pair with tpl as clone master: first row records the directive scan, rest replay it
|
|
44
|
+
for (let r of pending) sprae(r.el, r.proxy, tpl.content ? undefined : tpl)
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// untracked: update reads item signals (src[i]) but must not subscribe the :each effect to them —
|
|
48
|
+
// it re-runs via _change/_touch only, else every index write re-notifies it (O(N) per splice)
|
|
49
|
+
let update = throttle(() => untracked(() => mutate(() => {
|
|
50
|
+
let src = items, newl = src.length, prevl = rows.length, lenChanged = newl !== prevl
|
|
48
51
|
|
|
49
52
|
// detect keyed: array of objects (store items are shallow proxies — keyed by proxy identity)
|
|
50
53
|
keyed = false
|
|
@@ -53,7 +56,7 @@ export default (tpl, state, expr) => {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
if (keyed && prevl) {
|
|
56
|
-
let newRows = [], pending = [], seen = new Set(), moved = false
|
|
59
|
+
let newRows = [], pending = [], seen = new Set(), moved = false, misplaced = false
|
|
57
60
|
|
|
58
61
|
for (let i = 0; i < newl; i++) {
|
|
59
62
|
let id = src[i]
|
|
@@ -63,28 +66,55 @@ export default (tpl, state, expr) => {
|
|
|
63
66
|
}
|
|
64
67
|
let row = rowMap.get(id)
|
|
65
68
|
if (row) {
|
|
66
|
-
|
|
67
|
-
row.scope.i
|
|
69
|
+
// index-only shifts from remove/append keep DOM order — reorder only for same-length permutes (swap)
|
|
70
|
+
if (!lenChanged && row.scope.i !== i) moved = true
|
|
71
|
+
// insert() appends new rows at the tail — a reused row after a new one means wrong placement
|
|
72
|
+
if (pending.length) misplaced = true
|
|
73
|
+
row.scope.i = i; row.scope.r = id; row.scope.o = keys
|
|
68
74
|
} else {
|
|
69
|
-
row = mkrow(
|
|
75
|
+
row = mkrow(id, null, i)
|
|
70
76
|
rowMap.set(id, row)
|
|
71
77
|
pending.push(row)
|
|
72
78
|
}
|
|
73
79
|
newRows.push(row)
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
for (let [id, row] of rowMap) if (!seen.has(id))
|
|
82
|
+
for (let [id, row] of rowMap) if (!seen.has(id)) rm(row)
|
|
77
83
|
|
|
78
84
|
insert(pending)
|
|
79
85
|
|
|
80
|
-
if (
|
|
86
|
+
if (misplaced) {
|
|
87
|
+
// new rows sit at the tail — sweep everything into list order
|
|
81
88
|
let next = holder
|
|
82
89
|
for (let i = newRows.length - 1; i >= 0; i--) {
|
|
83
|
-
let n = newRows[i].
|
|
90
|
+
let n = newRows[i].node
|
|
84
91
|
if (n.nextSibling !== next) next.before(n)
|
|
85
92
|
next = n
|
|
93
|
+
newRows[i]._di = i
|
|
86
94
|
}
|
|
87
95
|
}
|
|
96
|
+
else if (moved) {
|
|
97
|
+
// collect rows whose DOM position no longer matches their list index
|
|
98
|
+
let fix = []
|
|
99
|
+
for (let i = 0; i < newRows.length; i++) {
|
|
100
|
+
let row = newRows[i]
|
|
101
|
+
if (row._di !== i) fix.push(row)
|
|
102
|
+
row._di = i
|
|
103
|
+
}
|
|
104
|
+
// 2-node swap fast path (common case: JFB swap rows)
|
|
105
|
+
if (fix.length === 2) {
|
|
106
|
+
let a = fix[0].node, b = fix[1].node, t = doc.createTextNode('')
|
|
107
|
+
a.replaceWith(t); b.replaceWith(a); t.replaceWith(b)
|
|
108
|
+
} else {
|
|
109
|
+
// general reorder: backward sweep, only move rows that aren't already in place
|
|
110
|
+
let next = holder
|
|
111
|
+
for (let i = newRows.length - 1; i >= 0; i--) {
|
|
112
|
+
let n = newRows[i].node
|
|
113
|
+
if (n.nextSibling !== next) next.before(n)
|
|
114
|
+
next = n
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else for (let i = 0; i < newRows.length; i++) newRows[i]._di = i
|
|
88
118
|
|
|
89
119
|
rows = newRows
|
|
90
120
|
} else {
|
|
@@ -108,9 +138,7 @@ export default (tpl, state, expr) => {
|
|
|
108
138
|
if (newl > prevl) {
|
|
109
139
|
let pending = []
|
|
110
140
|
for (let i = prevl; i < newl; i++) {
|
|
111
|
-
let row = keyed
|
|
112
|
-
? mkrow({ p: state, v: itemVar, k: idxVar, r: src[i], i, l: null }, keyHandler)
|
|
113
|
-
: mkrow({ p: state, v: itemVar, k: idxVar, c: src, i, o: keys, l: null }, posHandler)
|
|
141
|
+
let row = keyed ? mkrow(src[i], null, i) : mkrow(null, src, i)
|
|
114
142
|
rows.push(row)
|
|
115
143
|
if (keyed) rowMap.set(src[i], row)
|
|
116
144
|
pending.push(row)
|
|
@@ -118,7 +146,7 @@ export default (tpl, state, expr) => {
|
|
|
118
146
|
insert(pending)
|
|
119
147
|
}
|
|
120
148
|
}
|
|
121
|
-
}))
|
|
149
|
+
})))
|
|
122
150
|
|
|
123
151
|
if (tpl.parentNode) mutate(() => tpl.replaceWith(holder))
|
|
124
152
|
tpl[_state] = null
|
|
@@ -130,7 +158,7 @@ export default (tpl, state, expr) => {
|
|
|
130
158
|
else items = value || []
|
|
131
159
|
let off = effect(() => {
|
|
132
160
|
items[_change]?.value
|
|
133
|
-
|
|
161
|
+
items[_touch] // O(1) subscribe to index/content changes (swap, splice) on list stores
|
|
134
162
|
update()
|
|
135
163
|
})
|
|
136
164
|
return () => off()
|
package/directive/if.js
CHANGED
|
@@ -53,3 +53,26 @@ export default (el, state) => {
|
|
|
53
53
|
cb[_off] = () => { _el._holder._match?.[0][_off]?.(); _el._holder._match = null }
|
|
54
54
|
return cb
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Else directive - conditional branch following :if (`:else` or `:else :if="cond"`).
|
|
59
|
+
* Shares the holder clause machinery above.
|
|
60
|
+
* @param {Element | HTMLTemplateElement} el - Element with directive
|
|
61
|
+
* @returns {() => void} Update function
|
|
62
|
+
*/
|
|
63
|
+
export const _else = (el) => {
|
|
64
|
+
let _el, _prev = el
|
|
65
|
+
|
|
66
|
+
_el = el.content ? frag(el) : el
|
|
67
|
+
_el[_state] ??= null // mark _el (frag) as needing sprae
|
|
68
|
+
|
|
69
|
+
// find holder
|
|
70
|
+
while (_prev && !(_el._holder = _prev._holder)) _prev = _prev.previousSibling
|
|
71
|
+
|
|
72
|
+
el.remove()
|
|
73
|
+
el[_state] = null // mark as fake-spraed to stop further init, to lazy-sprae when branch matches
|
|
74
|
+
|
|
75
|
+
_el._holder._clauses.push(_el._clause = [_el, true])
|
|
76
|
+
|
|
77
|
+
return _el._holder.update
|
|
78
|
+
}
|