sprae 13.2.1 → 13.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/core.js +13 -3
- package/directive/class.js +21 -6
- package/directive/each.js +160 -73
- package/dist/sprae-csp.js +10 -10
- package/dist/sprae-csp.js.map +3 -3
- package/dist/sprae-csp.umd.js +10 -10
- package/dist/sprae-csp.umd.js.map +3 -3
- package/dist/sprae-preact.js +4 -4
- package/dist/sprae-preact.js.map +3 -3
- package/dist/sprae-preact.umd.js +4 -4
- package/dist/sprae-preact.umd.js.map +3 -3
- package/dist/sprae.js +4 -4
- package/dist/sprae.js.map +3 -3
- package/dist/sprae.umd.js +4 -4
- package/dist/sprae.umd.js.map +3 -3
- package/package.json +6 -3
- package/readme.md +35 -55
- package/signal.js +1 -0
- package/sprae.js +12 -10
- package/store.js +7 -2
- package/types/core.d.ts.map +1 -1
- package/types/directive/class.d.ts.map +1 -1
- package/types/directive/each.d.ts +5 -3
- package/types/directive/each.d.ts.map +1 -1
- package/types/signal.d.ts.map +1 -1
- package/types/sprae.d.ts.map +1 -1
- package/types/store.d.ts.map +1 -1
package/core.js
CHANGED
|
@@ -161,12 +161,22 @@ const sprae = (root = document.body, state) => {
|
|
|
161
161
|
// on/off all effects
|
|
162
162
|
// we don't call prevOn as convention: everything defined before :else :if won't be disabled by :if
|
|
163
163
|
// imagine <x :onx="..." :if="..."/> - when :if is false, it disables directives after :if (calls _off) but ignores :onx
|
|
164
|
-
el[_on] = () =>
|
|
165
|
-
|
|
164
|
+
el[_on] = () => {
|
|
165
|
+
if (offs) return offs
|
|
166
|
+
offs = Array(fx.length)
|
|
167
|
+
for (let i = 0; i < fx.length; i++) offs[i] = fx[i]()
|
|
168
|
+
return offs
|
|
169
|
+
}
|
|
170
|
+
el[_off] = () => {
|
|
171
|
+
if (!offs) return
|
|
172
|
+
let current = offs
|
|
173
|
+
offs = null
|
|
174
|
+
for (let i = 0; i < current.length; i++) current[i]?.()
|
|
175
|
+
}
|
|
166
176
|
|
|
167
177
|
// destroy
|
|
168
178
|
el[_dispose] ||= () => {
|
|
169
|
-
el[_off]()
|
|
179
|
+
el[_off]?.()
|
|
170
180
|
if (mo?._root === el) { mo.disconnect(); mo = null }
|
|
171
181
|
el[_off] = el[_on] = el[_dispose] = el[_add] = el[_state] = null
|
|
172
182
|
}
|
package/directive/class.js
CHANGED
|
@@ -10,14 +10,29 @@ import { clsx } from "../core.js";
|
|
|
10
10
|
* @returns {(v: string | string[] | Record<string, boolean>) => void} Update function
|
|
11
11
|
*/
|
|
12
12
|
export default (el, st, ex, name) => {
|
|
13
|
-
let _cur =
|
|
13
|
+
let _cur = null, _new, _prev = null
|
|
14
14
|
|
|
15
15
|
return (v) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
v = typeof v === 'function' ? v(el.className) : v
|
|
17
|
+
|
|
18
|
+
if (v?.constructor === Object) {
|
|
19
|
+
_prev = null
|
|
20
|
+
if (_cur) for (let c of _cur) if (!v[c]) el.classList.remove(c), _cur.delete(c)
|
|
21
|
+
if (!_cur?.size) _cur = null
|
|
22
|
+
for (let c in v) if (v[c] && !_cur?.has(c)) el.classList.add(c), (_cur ||= new Set).add(c)
|
|
23
|
+
if (!_cur) el.removeAttribute('class')
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
v = clsx(v)
|
|
28
|
+
if (v === _prev) return
|
|
29
|
+
_prev = v
|
|
30
|
+
|
|
31
|
+
_new = null
|
|
32
|
+
if (v) for (let c of v.split(' ')) c && (_new ||= new Set).add(c)
|
|
33
|
+
if (_cur) for (let c of _cur) if (!_new?.has(c)) el.classList.remove(c)
|
|
34
|
+
if (_new) for (let c of _new) if (!_cur?.has(c)) el.classList.add(c)
|
|
35
|
+
if (!_new) el.removeAttribute('class')
|
|
21
36
|
_cur = _new
|
|
22
37
|
}
|
|
23
38
|
}
|
package/directive/each.js
CHANGED
|
@@ -1,117 +1,204 @@
|
|
|
1
|
-
import sprae, {
|
|
1
|
+
import sprae, { parse, _state, _off, effect, _change, _signals, frag, throttle, mutate } from "../core.js";
|
|
2
|
+
|
|
3
|
+
// Lightweight row scope — reads item via cur[idx] (positional) or direct ref (keyed).
|
|
4
|
+
// Local vars (e.g. :ref) stored in `l` on first write.
|
|
5
|
+
const posHandler = {
|
|
6
|
+
get: (s, k) => k === s.v ? s.c?.[s.i] : k === s.k ? (s.o ? s.o[s.i] : s.i) : k === _signals ? s : s.l?.[k] !== undefined ? s.l[k] : s.p?.[k],
|
|
7
|
+
set: (s, k, v) => (k === s.v ? (s.c && (s.c[s.i] = 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),
|
|
8
|
+
has: () => true
|
|
9
|
+
}
|
|
10
|
+
// Keyed: `r` holds direct item reference — immune to store index shifts
|
|
11
|
+
const keyHandler = {
|
|
12
|
+
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],
|
|
13
|
+
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),
|
|
14
|
+
has: () => true
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
/**
|
|
4
18
|
* Each directive - renders list items from array/object/number.
|
|
5
19
|
* Syntax: `:each="item in items"` or `:each="(item, idx) of items"`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Keyed by object identity: when items are objects, splice/remove only
|
|
22
|
+
* disposes the removed row — survivors keep their DOM and don't re-evaluate.
|
|
23
|
+
* Primitives fall back to index-based (positional) updates.
|
|
10
24
|
*/
|
|
11
25
|
export default (tpl, state, expr) => {
|
|
12
26
|
const [lhs, rhs] = expr.split(/\bin|of\b/)
|
|
13
|
-
|
|
14
27
|
let [itemVar, idxVar = "$"] = lhs.trim().replace(/\(|\)/g, '').split(/\s*,\s*/);
|
|
15
28
|
|
|
16
|
-
// we need :if to be able to replace holder instead of tpl for :if :each case
|
|
17
|
-
// Reuse holder from previous init (survives :if off/on cycle)
|
|
18
29
|
let doc = tpl.ownerDocument
|
|
19
30
|
let holder = tpl._eachHolder || (tpl._eachHolder = doc.createTextNode(""));
|
|
20
31
|
|
|
21
|
-
//
|
|
22
|
-
|
|
32
|
+
// Map<identity, {el, scope, proxy}> for keyed mode
|
|
33
|
+
// Array<{el, scope, proxy}> for positional mode
|
|
34
|
+
let rowMap = new Map, rows = [], items, keys, cur
|
|
35
|
+
|
|
36
|
+
// Can we use identity keying? Only for object items.
|
|
37
|
+
let keyed = false
|
|
23
38
|
|
|
24
39
|
let update = throttle(() => mutate(() => {
|
|
25
|
-
let
|
|
40
|
+
let newItems = items, newl = newItems.length, prevl = rows.length
|
|
26
41
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
// detect keyed mode: first non-null item is object → keyed
|
|
43
|
+
keyed = false
|
|
44
|
+
for (let i = 0; i < newl; i++) {
|
|
45
|
+
let item = newItems[i]
|
|
46
|
+
if (item != null) { keyed = typeof item === 'object'; break }
|
|
31
47
|
}
|
|
32
48
|
|
|
49
|
+
if (keyed && prevl) {
|
|
50
|
+
// --- KEYED DIFF: only when we have existing rows to diff against ---
|
|
51
|
+
|
|
52
|
+
// fast path: pure append (list only grew, no removals possible)
|
|
53
|
+
if (newl > prevl && rowMap.size === prevl) {
|
|
54
|
+
let batch = (newl - prevl) > 1 ? doc.createDocumentFragment() : null
|
|
55
|
+
let pending = batch ? [] : null
|
|
56
|
+
for (let i = prevl; i < newl; i++) {
|
|
57
|
+
let identity = newItems[i]
|
|
58
|
+
let scope = { p: state, v: itemVar, k: idxVar, r: identity, i, l: null }
|
|
59
|
+
let proxy = new Proxy(scope, keyHandler)
|
|
60
|
+
let el = tpl.content ? frag(tpl) : tpl.cloneNode(true)
|
|
61
|
+
let insertNode = el.content || el
|
|
62
|
+
let row = { el, scope, proxy }
|
|
63
|
+
rowMap.set(identity, row)
|
|
64
|
+
rows.push(row)
|
|
65
|
+
if (batch) { batch.appendChild(insertNode); pending.push([el, proxy]) }
|
|
66
|
+
else { holder.before(insertNode); sprae(el, proxy) }
|
|
67
|
+
}
|
|
68
|
+
if (batch && pending.length) {
|
|
69
|
+
holder.before(batch)
|
|
70
|
+
for (let [el, proxy] of pending) sprae(el, proxy)
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// full diff: removals, reorders, mixed insert/remove
|
|
74
|
+
let removed = 0
|
|
75
|
+
if (newl <= prevl) {
|
|
76
|
+
let newSet = new Set
|
|
77
|
+
for (let i = 0; i < newl; i++) newSet.add(newItems[i])
|
|
78
|
+
for (let [identity, row] of rowMap) {
|
|
79
|
+
if (!newSet.has(identity)) {
|
|
80
|
+
row.el[Symbol.dispose]?.(); row.el.remove()
|
|
81
|
+
rowMap.delete(identity); removed++
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
33
85
|
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
let newRows = [], moved = false
|
|
87
|
+
let batch = null, pending = null
|
|
88
|
+
if (newl - rowMap.size > 1) batch = doc.createDocumentFragment(), pending = []
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < newl; i++) {
|
|
91
|
+
let identity = newItems[i], row = rowMap.get(identity)
|
|
92
|
+
if (row) {
|
|
93
|
+
if (row.scope.i !== i) moved = true
|
|
94
|
+
row.scope.i = i; row.scope.r = identity
|
|
95
|
+
newRows.push(row)
|
|
96
|
+
} else {
|
|
97
|
+
let scope = { p: state, v: itemVar, k: idxVar, r: identity, i, l: null }
|
|
98
|
+
let proxy = new Proxy(scope, keyHandler)
|
|
99
|
+
let el = tpl.content ? frag(tpl) : tpl.cloneNode(true)
|
|
100
|
+
let insertNode = el.content || el
|
|
101
|
+
row = { el, scope, proxy }
|
|
102
|
+
rowMap.set(identity, row)
|
|
103
|
+
newRows.push(row)
|
|
104
|
+
if (batch) { batch.appendChild(insertNode); pending.push([el, proxy]) }
|
|
105
|
+
else { holder.before(insertNode); sprae(el, proxy) }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
36
108
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// update
|
|
42
|
-
else while (i < prevl) cur[i] = newItems[i++]
|
|
109
|
+
if (batch && pending.length) {
|
|
110
|
+
holder.before(batch)
|
|
111
|
+
for (let [el, proxy] of pending) sprae(el, proxy)
|
|
112
|
+
}
|
|
43
113
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
114
|
+
if (moved || removed) {
|
|
115
|
+
let next = holder
|
|
116
|
+
for (let i = newRows.length - 1; i >= 0; i--) {
|
|
117
|
+
let node = newRows[i].el._holder || newRows[i].el.content || newRows[i].el
|
|
118
|
+
if (node.nextSibling !== next) next.before(node)
|
|
119
|
+
next = node
|
|
120
|
+
}
|
|
121
|
+
}
|
|
48
122
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
123
|
+
rows = newRows
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// --- POSITIONAL PATH: index-based (primitives, numbers) ---
|
|
52
127
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
128
|
+
// plain array replaced — full reset
|
|
129
|
+
if (prevl && cur !== newItems && !newItems[_change]) {
|
|
130
|
+
for (let r of rows) { r.el[Symbol.dispose]?.(); r.el.remove() }
|
|
131
|
+
rows.length = 0; prevl = 0; rowMap.clear()
|
|
132
|
+
}
|
|
133
|
+
cur = newItems
|
|
57
134
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
holder.before(insertNode)
|
|
64
|
-
let subscope = store({
|
|
65
|
-
get [itemVar]() { return cur?.[idx] },
|
|
66
|
-
[idxVar]: keys ? keys[idx] : idx
|
|
67
|
-
}, state)
|
|
68
|
-
sprae(el, subscope)
|
|
69
|
-
}
|
|
135
|
+
// shrink
|
|
136
|
+
if (newl < prevl) {
|
|
137
|
+
for (let i = newl; i < prevl; i++) { rows[i].el[Symbol.dispose]?.(); rows[i].el.remove() }
|
|
138
|
+
rows.length = newl
|
|
139
|
+
}
|
|
70
140
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
141
|
+
// update surviving rows' item source
|
|
142
|
+
for (let i = 0; i < Math.min(prevl, newl); i++) {
|
|
143
|
+
rows[i].scope.c = newItems
|
|
144
|
+
if (keys) rows[i].scope.o = keys
|
|
76
145
|
}
|
|
77
146
|
|
|
78
|
-
//
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
// append
|
|
148
|
+
if (newl > prevl) {
|
|
149
|
+
let batch = (newl - prevl) > 1 ? doc.createDocumentFragment() : null
|
|
150
|
+
let pending = batch ? [] : null
|
|
151
|
+
|
|
152
|
+
for (let i = prevl; i < newl; i++) {
|
|
153
|
+
let handler = keyed ? keyHandler : posHandler
|
|
154
|
+
let scope = keyed
|
|
155
|
+
? { p: state, v: itemVar, k: idxVar, r: newItems[i], i, l: null }
|
|
156
|
+
: { p: state, v: itemVar, k: idxVar, c: newItems, i, o: keys, l: null }
|
|
157
|
+
let proxy = new Proxy(scope, handler)
|
|
158
|
+
let el = tpl.content ? frag(tpl) : tpl.cloneNode(true)
|
|
159
|
+
let insertNode = el.content || el
|
|
160
|
+
let row = { el, scope }
|
|
161
|
+
rows.push(row)
|
|
162
|
+
if (keyed) rowMap.set(newItems[i], row)
|
|
163
|
+
|
|
164
|
+
if (batch) {
|
|
165
|
+
batch.appendChild(insertNode)
|
|
166
|
+
pending.push([el, proxy])
|
|
167
|
+
} else {
|
|
168
|
+
holder.before(insertNode)
|
|
169
|
+
sprae(el, proxy)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (batch) {
|
|
174
|
+
holder.before(batch)
|
|
175
|
+
for (let [el, proxy] of pending) sprae(el, proxy)
|
|
87
176
|
}
|
|
88
177
|
}
|
|
89
178
|
}
|
|
90
|
-
|
|
91
|
-
prevl = newl
|
|
92
179
|
}))
|
|
93
180
|
|
|
94
|
-
// Replace template with holder (idempotent: skip if already replaced, e.g. :if re-activation)
|
|
95
181
|
if (tpl.parentNode) mutate(() => tpl.replaceWith(holder))
|
|
96
|
-
tpl[_state] = null
|
|
182
|
+
tpl[_state] = null
|
|
97
183
|
|
|
98
|
-
let
|
|
184
|
+
let disposeAll = () => {
|
|
185
|
+
for (let r of rows) { r.el[Symbol.dispose]?.(); r.el.remove() }
|
|
186
|
+
rows.length = 0; rowMap.clear()
|
|
187
|
+
}
|
|
99
188
|
|
|
100
|
-
|
|
101
|
-
// resolve new items
|
|
189
|
+
let cb = value => {
|
|
102
190
|
keys = null
|
|
103
191
|
if (typeof value === "number") items = Array.from({ length: value }, (_, i) => i + 1)
|
|
104
192
|
else if (value?.constructor === Object) keys = Object.keys(value), items = Object.values(value)
|
|
105
193
|
else items = value || []
|
|
106
194
|
|
|
107
|
-
// whenever list changes, we rebind internal change effect
|
|
108
195
|
let off = effect(() => {
|
|
109
|
-
// subscribe to items change (.length) - we do it every time (not just in update) since preact signals unsubscribes unused signals
|
|
110
196
|
items[_change]?.value
|
|
111
|
-
|
|
112
|
-
// make first render immediately, debounce subsequent renders
|
|
113
197
|
update()
|
|
114
198
|
})
|
|
115
|
-
return () =>
|
|
116
|
-
}
|
|
199
|
+
return () => off()
|
|
200
|
+
}
|
|
201
|
+
cb.eval = parse(rhs)
|
|
202
|
+
cb[_off] = disposeAll
|
|
203
|
+
return cb
|
|
117
204
|
}
|