sprae 12.3.9 → 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 -6
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +3 -6
- 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 -6
- package/dist/sprae.micro.js.map +0 -7
- package/micro.js +0 -56
package/core.js
CHANGED
|
@@ -1,23 +1,142 @@
|
|
|
1
1
|
import store, { _change, _signals } from "./store.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
_on = Symbol('on'),
|
|
6
|
-
_off = Symbol('off'),
|
|
7
|
-
_add = Symbol('init');
|
|
3
|
+
/** Symbol for disposal (using standard Symbol.dispose if available) */
|
|
4
|
+
export const _dispose = (Symbol.dispose ||= Symbol("dispose"))
|
|
8
5
|
|
|
9
|
-
|
|
6
|
+
/** Symbol for accessing element's reactive state */
|
|
7
|
+
export const _state = Symbol("state")
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
/** Symbol for enabling element effects */
|
|
10
|
+
export const _on = Symbol('on')
|
|
11
|
+
|
|
12
|
+
/** Symbol for disabling element effects */
|
|
13
|
+
export const _off = Symbol('off')
|
|
14
|
+
|
|
15
|
+
/** Symbol for adding child to element */
|
|
16
|
+
export const _add = Symbol('init')
|
|
17
|
+
|
|
18
|
+
/** Directive prefix (default: ':') */
|
|
19
|
+
export let prefix = ':';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A reactive signal containing a value.
|
|
23
|
+
* @template T
|
|
24
|
+
* @typedef {Object} Signal
|
|
25
|
+
* @property {T} value - Current value (reading subscribes, writing notifies)
|
|
26
|
+
* @property {() => T} peek - Read without subscribing
|
|
27
|
+
* @property {() => T} valueOf - Get value for coercion
|
|
28
|
+
* @property {() => T} toJSON - Get value for JSON serialization
|
|
29
|
+
* @property {() => string} toString - Get value as string
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Internal effect function type.
|
|
34
|
+
* @typedef {Object} EffectFn
|
|
35
|
+
* @property {Set<Set<EffectFn>>} deps - Dependency sets
|
|
36
|
+
* @property {() => void} fn - Original function
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a reactive signal.
|
|
41
|
+
* @template T
|
|
42
|
+
* @type {<T>(value: T) => Signal<T>}
|
|
43
|
+
*/
|
|
44
|
+
export let signal;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a reactive effect that re-runs when dependencies change.
|
|
48
|
+
* @type {(fn: () => void | (() => void)) => () => void}
|
|
49
|
+
*/
|
|
50
|
+
export let effect;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a computed signal derived from other signals.
|
|
54
|
+
* @template T
|
|
55
|
+
* @type {<T>(fn: () => T) => Signal<T>}
|
|
56
|
+
*/
|
|
57
|
+
export let computed;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Batches multiple signal updates into a single notification.
|
|
61
|
+
* @template T
|
|
62
|
+
* @type {<T>(fn: () => T) => T}
|
|
63
|
+
*/
|
|
64
|
+
export let batch = (fn) => fn();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runs a function without tracking signal dependencies.
|
|
68
|
+
* @template T
|
|
69
|
+
* @type {<T>(fn: () => T) => T}
|
|
70
|
+
*/
|
|
71
|
+
export let untracked = batch;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Registry of directive handlers.
|
|
75
|
+
* @type {Record<string, DirectiveHandler>}
|
|
76
|
+
*/
|
|
77
|
+
export let directive = {};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Registry of modifier functions.
|
|
81
|
+
* @type {Record<string, ModifierHandler>}
|
|
82
|
+
*/
|
|
83
|
+
export let modifier = {}
|
|
12
84
|
|
|
13
85
|
let currentDir = null;
|
|
86
|
+
let currentEl = null;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Formats element for error message (minimal context).
|
|
90
|
+
* @param {Element} [el] - Element to format
|
|
91
|
+
* @returns {string} Element hint like "<div#id.class>"
|
|
92
|
+
*/
|
|
93
|
+
const elHint = (el) => {
|
|
94
|
+
if (!el?.tagName) return ''
|
|
95
|
+
let hint = el.tagName.toLowerCase()
|
|
96
|
+
if (el.id) hint += '#' + el.id
|
|
97
|
+
else if (el.className) hint += '.' + el.className.split(' ')[0]
|
|
98
|
+
return `<${hint}>`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reports an error with context.
|
|
103
|
+
* @param {Error|string} e - Error to report
|
|
104
|
+
* @param {string} [expr] - Expression that caused error
|
|
105
|
+
*/
|
|
106
|
+
const err = (e, expr) => {
|
|
107
|
+
let msg = `∴ ${e}`
|
|
108
|
+
if (currentEl) msg += `\n in ${elHint(currentEl)}`
|
|
109
|
+
if (currentDir && expr) msg += `\n ${currentDir}="${expr}"`
|
|
110
|
+
console.error(msg)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @callback DirectiveHandler
|
|
115
|
+
* @param {Element} el - Target element
|
|
116
|
+
* @param {Object} state - Reactive state object
|
|
117
|
+
* @param {string} expr - Expression string
|
|
118
|
+
* @param {string} [name] - Directive name with modifiers
|
|
119
|
+
* @returns {((value: any) => void | (() => void)) | { [Symbol.dispose]: () => void } | void}
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @callback ModifierHandler
|
|
124
|
+
* @param {Function} fn - Function to modify
|
|
125
|
+
* @param {...string} args - Modifier arguments (from dash-separated values)
|
|
126
|
+
* @returns {Function}
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @typedef {Object} SpraeState
|
|
131
|
+
* @property {Record<string, Signal>} [_signals] - Internal signals map
|
|
132
|
+
*/
|
|
14
133
|
|
|
15
134
|
/**
|
|
16
135
|
* Applies directives to an HTML element and manages its reactive state.
|
|
17
136
|
*
|
|
18
137
|
* @param {Element} [el=document.body] - The target HTML element to apply directives to.
|
|
19
|
-
* @param {Object
|
|
20
|
-
* @returns {Object} The reactive state object associated with the element.
|
|
138
|
+
* @param {Object} [state] - Initial state values to populate the element's reactive state.
|
|
139
|
+
* @returns {SpraeState & Object} The reactive state object associated with the element.
|
|
21
140
|
*/
|
|
22
141
|
const sprae = (el = document.body, state) => {
|
|
23
142
|
// repeated call can be caused by eg. :each with new objects with old keys
|
|
@@ -50,6 +169,7 @@ const sprae = (el = document.body, state) => {
|
|
|
50
169
|
el.removeAttribute(name)
|
|
51
170
|
|
|
52
171
|
currentDir = name;
|
|
172
|
+
currentEl = el;
|
|
53
173
|
|
|
54
174
|
// directive initializer can be redefined
|
|
55
175
|
fx.push(start = dir(el, name.slice(prefix.length), value, state)), offs.push(start())
|
|
@@ -60,8 +180,12 @@ const sprae = (el = document.body, state) => {
|
|
|
60
180
|
}
|
|
61
181
|
|
|
62
182
|
// :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
|
|
63
|
-
//
|
|
64
|
-
|
|
183
|
+
// real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
|
|
184
|
+
if (el.firstChild !== undefined) {
|
|
185
|
+
let child = el.firstChild, next
|
|
186
|
+
while (child) (next = child.nextSibling, child.nodeType == 1 && add(child), child = next)
|
|
187
|
+
}
|
|
188
|
+
else for (let child of el.childNodes) child.nodeType == 1 && add(child)
|
|
65
189
|
};
|
|
66
190
|
|
|
67
191
|
add(el);
|
|
@@ -75,80 +199,82 @@ const sprae = (el = document.body, state) => {
|
|
|
75
199
|
}
|
|
76
200
|
|
|
77
201
|
// directive initializer
|
|
202
|
+
/** @type {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} */
|
|
78
203
|
export let dir
|
|
79
204
|
|
|
80
205
|
/**
|
|
81
|
-
* Compiles an expression into an evaluator function.
|
|
82
|
-
* @type {(
|
|
206
|
+
* Compiles an expression string into an evaluator function.
|
|
207
|
+
* @type {(expr: string) => (state: Object) => any}
|
|
83
208
|
*/
|
|
84
209
|
export let compile
|
|
85
210
|
|
|
86
211
|
/**
|
|
87
212
|
* Parses an expression into an evaluator function, caching the result for reuse.
|
|
88
213
|
*
|
|
89
|
-
* @param {string} expr The expression to parse and compile into a function.
|
|
90
|
-
* @returns {
|
|
214
|
+
* @param {string} expr - The expression to parse and compile into a function.
|
|
215
|
+
* @returns {(state: Object, cb?: (value: any) => any) => any} The compiled evaluator function for the expression.
|
|
91
216
|
*/
|
|
92
217
|
export const parse = (expr) => {
|
|
93
218
|
let fn = cache[expr=expr.trim()]
|
|
94
219
|
if (fn) return fn
|
|
95
220
|
|
|
96
|
-
let _expr = (expr || 'undefined') + '\n'
|
|
97
|
-
|
|
98
|
-
// if, const, let - no return
|
|
99
|
-
if (/^(if|let|const)\b/.test(_expr));
|
|
100
|
-
// first-level semicolons - no return
|
|
101
|
-
else if (hasSemi(_expr));
|
|
102
|
-
else _expr = `return ${_expr}`
|
|
103
|
-
|
|
104
|
-
// async expression
|
|
105
|
-
if (/\bawait\s/.test(_expr)) _expr = `return (async()=>{${_expr}})()`
|
|
106
|
-
|
|
107
221
|
// static time errors
|
|
108
222
|
try {
|
|
109
|
-
fn = compile(
|
|
223
|
+
fn = compile(expr || 'undefined')
|
|
110
224
|
// Object.defineProperty(fn, "name", { value: `∴ ${expr}` })
|
|
111
|
-
} catch (e) {
|
|
225
|
+
} catch (e) { err(e, expr) }
|
|
112
226
|
|
|
113
227
|
// run time errors
|
|
114
228
|
return cache[expr] = function (state, cb, _out) {
|
|
115
229
|
try {
|
|
116
230
|
let result = fn?.call(this, state)
|
|
117
231
|
// if cb is given (to handle async/await exprs, usually directive update) - call it with result and return a cleanup function
|
|
118
|
-
if (cb) return result?.then
|
|
232
|
+
if (cb) return result?.then
|
|
233
|
+
? (result.then(v => _out = cb(v)).catch(e => err(e, expr)), () => typeof _out === 'function' && _out())
|
|
234
|
+
: cb(result)
|
|
119
235
|
else return result
|
|
120
236
|
} catch (e) {
|
|
121
|
-
|
|
237
|
+
err(e, expr)
|
|
122
238
|
}
|
|
123
239
|
}
|
|
124
240
|
}
|
|
125
241
|
const cache = {};
|
|
126
242
|
|
|
127
|
-
const hasSemi = s => {
|
|
128
|
-
for (let d=0,i=0;i<s.length;i++) {
|
|
129
|
-
if (s[i]=='{') d++
|
|
130
|
-
else if (s[i]=='}') d--
|
|
131
|
-
else if (s[i]==';' && !d) return true
|
|
132
|
-
}
|
|
133
|
-
return false
|
|
134
|
-
}
|
|
135
243
|
|
|
244
|
+
/**
|
|
245
|
+
* @typedef {Object} SpraeConfig
|
|
246
|
+
* @property {(expr: string) => (state: Object) => any} [compile] - Custom expression compiler
|
|
247
|
+
* @property {string} [prefix] - Directive prefix (default: ':')
|
|
248
|
+
* @property {<T>(value: T) => Signal<T>} [signal] - Signal factory
|
|
249
|
+
* @property {(fn: () => void | (() => void)) => () => void} [effect] - Effect factory
|
|
250
|
+
* @property {<T>(fn: () => T) => Signal<T>} [computed] - Computed factory
|
|
251
|
+
* @property {<T>(fn: () => T) => T} [batch] - Batch function
|
|
252
|
+
* @property {<T>(fn: () => T) => T} [untracked] - Untracked function
|
|
253
|
+
* @property {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} [dir] - Directive initializer
|
|
254
|
+
*/
|
|
136
255
|
|
|
137
256
|
/**
|
|
138
|
-
* Configure sprae
|
|
257
|
+
* Configure sprae with custom signals, compiler, or prefix.
|
|
258
|
+
* @param {SpraeConfig} config - Configuration options
|
|
259
|
+
* @returns {void}
|
|
139
260
|
*/
|
|
140
|
-
export const use = (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
261
|
+
export const use = (config) => (
|
|
262
|
+
config.compile && (compile = config.compile),
|
|
263
|
+
config.prefix && (prefix = config.prefix),
|
|
264
|
+
config.signal && (signal = config.signal),
|
|
265
|
+
config.effect && (effect = config.effect),
|
|
266
|
+
config.computed && (computed = config.computed),
|
|
267
|
+
config.batch && (batch = config.batch),
|
|
268
|
+
config.untracked && (untracked = config.untracked),
|
|
269
|
+
config.dir && (dir = config.dir)
|
|
149
270
|
)
|
|
150
271
|
|
|
151
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Applies modifiers to a function.
|
|
274
|
+
* @param {Function & { target?: Element }} fn - Function to decorate
|
|
275
|
+
* @param {string[]} mods - Modifier names with arguments (e.g., ['throttle-500', 'prevent'])
|
|
276
|
+
* @returns {Function} Decorated function
|
|
277
|
+
*/
|
|
152
278
|
export const decorate = (fn, mods) => {
|
|
153
279
|
while (mods.length) {
|
|
154
280
|
let [name, ...params] = mods.pop().split('-'), mod = modifier[name], wrapFn
|
|
@@ -162,7 +288,21 @@ export const decorate = (fn, mods) => {
|
|
|
162
288
|
return fn
|
|
163
289
|
}
|
|
164
290
|
|
|
165
|
-
|
|
291
|
+
/**
|
|
292
|
+
* @typedef {Object} FragmentLike
|
|
293
|
+
* @property {Node[]} childNodes - Child nodes of the fragment
|
|
294
|
+
* @property {DocumentFragment} content - The document fragment content
|
|
295
|
+
* @property {() => void} remove - Remove the fragment from DOM
|
|
296
|
+
* @property {(el: Node) => void} replaceWith - Replace the fragment with an element
|
|
297
|
+
* @property {Attr[]} attributes - Attributes from the original template
|
|
298
|
+
* @property {(name: string) => void} removeAttribute - Remove an attribute
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Creates a fragment holder from a template element with minimal API surface.
|
|
303
|
+
* @param {HTMLTemplateElement | FragmentLike} tpl - Template element or existing fragment
|
|
304
|
+
* @returns {FragmentLike} Fragment-like object
|
|
305
|
+
*/
|
|
166
306
|
export const frag = (tpl) => {
|
|
167
307
|
if (!tpl.nodeType) return tpl // existing tpl
|
|
168
308
|
|
|
@@ -173,7 +313,6 @@ export const frag = (tpl) => {
|
|
|
173
313
|
childNodes = (content.append(ref), [...content.childNodes])
|
|
174
314
|
|
|
175
315
|
return {
|
|
176
|
-
// get parentNode() { return childNodes[0].parentNode },
|
|
177
316
|
childNodes,
|
|
178
317
|
content,
|
|
179
318
|
remove: () => content.append(...childNodes),
|
|
@@ -184,25 +323,45 @@ export const frag = (tpl) => {
|
|
|
184
323
|
},
|
|
185
324
|
attributes,
|
|
186
325
|
removeAttribute(name) { attributes.splice(attributes.findIndex(a => a.name === name), 1) },
|
|
187
|
-
// setAttributeNode() { }
|
|
188
326
|
}
|
|
189
327
|
}
|
|
190
328
|
|
|
191
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Converts camelCase to kebab-case.
|
|
331
|
+
* @param {string} str - String to convert
|
|
332
|
+
* @returns {string} Kebab-case string
|
|
333
|
+
*/
|
|
192
334
|
export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
|
|
193
335
|
|
|
194
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Sets or removes an attribute on an element.
|
|
338
|
+
* @param {Element} el - Target element
|
|
339
|
+
* @param {string} name - Attribute name
|
|
340
|
+
* @param {string | boolean | null | undefined} v - Attribute value (null/false removes, true sets empty)
|
|
341
|
+
* @returns {void}
|
|
342
|
+
*/
|
|
195
343
|
export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) : el.setAttribute(name, v === true ? "" : v);
|
|
196
344
|
|
|
197
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Converts class input to className string (like clsx/classnames).
|
|
347
|
+
* @param {string | string[] | Record<string, boolean> | null | undefined} c - Class input
|
|
348
|
+
* @returns {string} Space-separated class string
|
|
349
|
+
*/
|
|
198
350
|
export const clsx = (c, _out = []) => !c ? '' : typeof c === 'string' ? c : (
|
|
199
351
|
Array.isArray(c) ? c.map(clsx) :
|
|
200
352
|
Object.entries(c).reduce((s, [k, v]) => !v ? s : [...s, k], [])
|
|
201
353
|
).join(' ')
|
|
202
354
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Throttles a function to run at most once per tick (or custom scheduler).
|
|
357
|
+
* Fires on leading edge, then on trailing edge if called during throttle.
|
|
358
|
+
* @template {Function} T
|
|
359
|
+
* @param {T} fn - Function to throttle
|
|
360
|
+
* @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
|
|
361
|
+
* @returns {T} Throttled function
|
|
362
|
+
*/
|
|
363
|
+
export const throttle = (fn, ms) => {
|
|
364
|
+
let _planned = 0, arg, schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
|
|
206
365
|
const throttled = (e) => {
|
|
207
366
|
arg = e
|
|
208
367
|
if (!_planned++) fn(arg), schedule((_dirty = _planned > 1) => (
|
|
@@ -212,8 +371,26 @@ export const throttle = (fn, schedule = queueMicrotask) => {
|
|
|
212
371
|
return throttled;
|
|
213
372
|
}
|
|
214
373
|
|
|
215
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Debounces a function to run after a delay since the last call.
|
|
376
|
+
* @template {Function} T
|
|
377
|
+
* @param {T} fn - Function to debounce
|
|
378
|
+
* @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
|
|
379
|
+
* @param {boolean} [immediate=false] - Fire on leading edge instead of trailing
|
|
380
|
+
* @returns {T} Debounced function
|
|
381
|
+
*/
|
|
382
|
+
export const debounce = (fn, ms, immediate) => {
|
|
383
|
+
let schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
|
|
384
|
+
return immediate
|
|
385
|
+
? ((_blocked) => (arg) => !_blocked && (fn(arg), _blocked = 1, schedule(() => _blocked = 0)))()
|
|
386
|
+
: ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
|
|
387
|
+
}
|
|
216
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Parses time string to milliseconds. Supports: 100, 100ms, 1s, 1m
|
|
391
|
+
* @param {string|number} t - Time value
|
|
392
|
+
* @returns {number} Milliseconds
|
|
393
|
+
*/
|
|
217
394
|
export * from './store.js';
|
|
218
395
|
|
|
219
396
|
export default sprae
|
package/directive/_.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { attr } from "../core.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Default attribute directive - sets any attribute value.
|
|
5
|
+
* @param {Element} el - Target element
|
|
6
|
+
* @param {Object} st - State object
|
|
7
|
+
* @param {string} ex - Expression
|
|
8
|
+
* @param {string} name - Attribute name
|
|
9
|
+
* @returns {(v: any) => void} Update function
|
|
10
|
+
*/
|
|
3
11
|
export default (el, st, ex, name) => v => attr(el, name, typeof v === 'function' ? v(el.getAttribute(name)) : v)
|
package/directive/class.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { clsx, decorate } from "../core.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Class directive - manages CSS classes reactively.
|
|
5
|
+
* Supports strings, arrays, and objects (like clsx/classnames).
|
|
6
|
+
* @param {Element} el - Target element
|
|
7
|
+
* @param {Object} st - State object
|
|
8
|
+
* @param {string} ex - Expression
|
|
9
|
+
* @param {string} name - Directive name with modifiers
|
|
10
|
+
* @returns {(v: string | string[] | Record<string, boolean>) => void} Update function
|
|
11
|
+
*/
|
|
3
12
|
export default (el, st, ex, name) => {
|
|
4
13
|
let _cur = new Set, _new
|
|
5
14
|
|
package/directive/each.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import sprae, { store, parse, _state, effect, _change, _signals, frag, throttle, debounce } from "../core.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Each directive - renders list items from array/object/number.
|
|
5
|
+
* Syntax: `:each="item in items"` or `:each="(item, idx) of items"`
|
|
6
|
+
* @param {HTMLTemplateElement | Element} tpl - Template element
|
|
7
|
+
* @param {Object} state - State object
|
|
8
|
+
* @param {string} expr - Iterator expression
|
|
9
|
+
* @returns {{ eval: Function, [Symbol.dispose]: () => void }} Directive result
|
|
10
|
+
*/
|
|
3
11
|
export default (tpl, state, expr) => {
|
|
4
12
|
const [lhs, rhs] = expr.split(/\bin|of\b/)
|
|
5
13
|
|
|
@@ -11,7 +19,6 @@ export default (tpl, state, expr) => {
|
|
|
11
19
|
// we re-create items any time new items are produced
|
|
12
20
|
let cur, keys, items, prevl = 0
|
|
13
21
|
|
|
14
|
-
// FIXME: pass items to update instead of global
|
|
15
22
|
let update = throttle(() => {
|
|
16
23
|
let i = 0, newItems = items, newl = newItems.length
|
|
17
24
|
|
|
@@ -32,27 +39,32 @@ export default (tpl, state, expr) => {
|
|
|
32
39
|
// update
|
|
33
40
|
else while (i < prevl) cur[i] = newItems[i++]
|
|
34
41
|
|
|
42
|
+
// batch append using DocumentFragment for efficiency
|
|
43
|
+
let batchSize = newl - i
|
|
44
|
+
let batch = batchSize > 1 ? document.createDocumentFragment() : null
|
|
45
|
+
let pending = batch ? [] : null
|
|
46
|
+
|
|
35
47
|
// append
|
|
36
48
|
for (; i < newl; i++) {
|
|
37
49
|
cur[i] = newItems[i]
|
|
38
50
|
|
|
39
|
-
let idx = i
|
|
40
|
-
// inherited state must be cheaper in terms of memory and faster in terms of performance, compared to creating a proxy store
|
|
41
|
-
// subscope = store({
|
|
42
|
-
// // NOTE: since we simulate signal, we have to make sure it's actual signal, not fake one
|
|
43
|
-
// // FIXME: try to avoid this, we also have issue with wrongly calling dispose in store on delete
|
|
44
|
-
// [itemVar]: cur[_signals]?.[idx]?.peek ? cur[_signals]?.[idx] : cur[idx],
|
|
45
|
-
// [idxVar]: keys ? keys[idx] : idx
|
|
46
|
-
// }, state)
|
|
47
|
-
subscope = Object.create(state, {
|
|
48
|
-
[itemVar]: { get: () => cur[idx] },
|
|
49
|
-
[idxVar]: { value: keys ? keys[idx] : idx }
|
|
50
|
-
})
|
|
51
|
-
|
|
51
|
+
let idx = i
|
|
52
52
|
let el = tpl.content ? frag(tpl) : tpl.cloneNode(true);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// el.content is DocumentFragment for frag() output, el itself for cloneNode
|
|
54
|
+
let insertNode = el.content || el
|
|
55
|
+
|
|
56
|
+
// collect for batch insert
|
|
57
|
+
if (batch) {
|
|
58
|
+
batch.appendChild(insertNode)
|
|
59
|
+
pending.push([ el, idx ])
|
|
60
|
+
} else {
|
|
61
|
+
holder.before(insertNode)
|
|
62
|
+
let subscope = Object.create(state, {
|
|
63
|
+
[itemVar]: { get: () => cur[idx] },
|
|
64
|
+
[idxVar]: { value: keys ? keys[idx] : idx }
|
|
65
|
+
})
|
|
66
|
+
sprae(el, subscope)
|
|
67
|
+
}
|
|
56
68
|
|
|
57
69
|
// signal/holder disposal removes element
|
|
58
70
|
let _prev = ((cur[_signals] ||= [])[i] ||= {})[Symbol.dispose]
|
|
@@ -60,6 +72,18 @@ export default (tpl, state, expr) => {
|
|
|
60
72
|
_prev?.(), el[Symbol.dispose]?.(), el.remove()
|
|
61
73
|
};
|
|
62
74
|
}
|
|
75
|
+
|
|
76
|
+
// batch insert all at once, then sprae
|
|
77
|
+
if (batch) {
|
|
78
|
+
holder.before(batch)
|
|
79
|
+
for (let [el, idx] of pending) {
|
|
80
|
+
let subscope = Object.create(state, {
|
|
81
|
+
[itemVar]: { get: () => cur[idx] },
|
|
82
|
+
[idxVar]: { value: keys ? keys[idx] : idx }
|
|
83
|
+
})
|
|
84
|
+
sprae(el, subscope)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
prevl = newl
|
package/directive/else.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { _on, _off, _state, frag } from '../core.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Else directive - conditional branch following :if.
|
|
5
|
+
* Can be used as `:else` or `:else :if="condition"`.
|
|
6
|
+
* @param {Element | HTMLTemplateElement} el - Element with directive
|
|
7
|
+
* @returns {() => void} Update function
|
|
8
|
+
*/
|
|
5
9
|
export default (el) => {
|
|
6
10
|
let _el, _prev = el
|
|
7
11
|
|
package/directive/event.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { parse, decorate } from "../core.js"
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Event directive - attaches event listeners with modifiers.
|
|
5
|
+
* Syntax: `:onclick="handler"` or `:onclick.prevent.stop="handler"`
|
|
6
|
+
* @param {Element} el - Target element
|
|
7
|
+
* @param {Object} state - State object
|
|
8
|
+
* @param {string} expr - Handler expression
|
|
9
|
+
* @param {string} name - Event name with modifiers (e.g., 'onclick.prevent')
|
|
10
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
11
|
+
*/
|
|
3
12
|
export default (el, state, expr, name) => {
|
|
4
13
|
// wrap inline cb into function
|
|
5
14
|
// if (!/^(?:[\w$]+|\([^()]*\))\s*=>/.test(expr) && !/^function\b/.test(expr)) expr = `()=>{${expr}}`;
|
package/directive/fx.js
CHANGED
package/directive/html.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
-
import sprae, { _dispose
|
|
1
|
+
import sprae, { _dispose } from "../core.js"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
/**
|
|
4
|
+
* HTML directive - sets innerHTML and initializes nested directives.
|
|
5
|
+
* Supports templates for fragment insertion.
|
|
6
|
+
* @param {Element | HTMLTemplateElement} el - Target element
|
|
7
|
+
* @param {Object} state - State object
|
|
8
|
+
* @returns {(v: string | ((html: string) => string)) => void | (() => void)} Update function
|
|
9
|
+
*/
|
|
10
|
+
export default (el, state) => {
|
|
11
|
+
// <template :html="a"/> - fragment case: use placeholder + range
|
|
12
|
+
if (el.content) {
|
|
13
|
+
let start = document.createTextNode(''),
|
|
14
|
+
end = document.createTextNode(''),
|
|
15
|
+
range = document.createRange()
|
|
16
|
+
el.replaceWith(start, end)
|
|
17
|
+
return v => {
|
|
18
|
+
v = typeof v === 'function' ? v('') : v
|
|
19
|
+
range.setStartAfter(start)
|
|
20
|
+
range.setEndBefore(end)
|
|
21
|
+
range.deleteContents()
|
|
22
|
+
if (v != null && v !== '') {
|
|
23
|
+
let frag = range.createContextualFragment(v)
|
|
24
|
+
sprae(frag, state)
|
|
25
|
+
range.insertNode(frag)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return v => (v = typeof v === 'function' ? v(el.innerHTML) : v, el.innerHTML = v == null ? "" : v, sprae(el, state), el[_dispose])
|
|
30
|
+
}
|
package/directive/if.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
// "centralized" version of :if
|
|
2
1
|
import sprae, { throttle, _on, _off, _state, frag } from '../core.js';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Conditional directive - shows/hides element based on condition.
|
|
5
|
+
* Works with :else and :else :if for branching.
|
|
6
|
+
* @param {Element | HTMLTemplateElement} el - Target element
|
|
7
|
+
* @param {Object} state - State object
|
|
8
|
+
* @returns {(value: any) => void} Update function
|
|
9
|
+
*/
|
|
5
10
|
export default (el, state) => {
|
|
6
11
|
let _holder, _el, _match
|
|
7
12
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal directive - teleports element to another container.
|
|
3
|
+
* Value can be selector string, element, or falsy to return home.
|
|
4
|
+
* @param {Element} el - Element to teleport
|
|
5
|
+
* @param {Object} state - State object
|
|
6
|
+
* @param {string} expr - Expression
|
|
7
|
+
* @returns {(value: string | Element | null | false) => void} Update function
|
|
8
|
+
*/
|
|
9
|
+
export default (el, state, expr) => {
|
|
10
|
+
const comment = document.createComment(':portal')
|
|
11
|
+
let currentTarget = null
|
|
12
|
+
|
|
13
|
+
// Insert placeholder before element
|
|
14
|
+
el.before(comment)
|
|
15
|
+
|
|
16
|
+
return (value) => {
|
|
17
|
+
// Resolve target: selector string, element, or null/false to return home
|
|
18
|
+
// For selectors, first try within the same root, then document
|
|
19
|
+
const root = el.getRootNode()
|
|
20
|
+
const target = typeof value === 'string'
|
|
21
|
+
? (root.querySelector?.(value) || document.querySelector(value))
|
|
22
|
+
: value instanceof Element ? value
|
|
23
|
+
: value ? document.body : null
|
|
24
|
+
|
|
25
|
+
// No change needed
|
|
26
|
+
if (target === currentTarget) return
|
|
27
|
+
|
|
28
|
+
if (target) {
|
|
29
|
+
// Move to target
|
|
30
|
+
target.appendChild(el)
|
|
31
|
+
} else {
|
|
32
|
+
// Return home (after placeholder)
|
|
33
|
+
comment.after(el)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
currentTarget = target
|
|
37
|
+
}
|
|
38
|
+
}
|
package/directive/ref.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { parse } from "../core.js"
|
|
2
2
|
import { setter } from "./value.js"
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Ref directive - stores element reference in state.
|
|
6
|
+
* If expression is a function, calls it with element (returns dispose).
|
|
7
|
+
* @param {Element} el - Target element
|
|
8
|
+
* @param {Object} state - State object
|
|
9
|
+
* @param {string} expr - Variable name or function expression
|
|
10
|
+
* @returns {{ [Symbol.dispose]: () => void } | void} Disposal object or void
|
|
11
|
+
*/
|
|
4
12
|
export default (el, state, expr) => {
|
|
5
13
|
let fn = parse(expr)(state)
|
|
6
14
|
|