sprae 12.3.9 → 12.4.1
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 +240 -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 +36 -8
- package/readme.md +39 -54
- package/signal.js +41 -3
- package/sprae.js +114 -25
- 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,146 @@
|
|
|
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) {
|
|
110
|
+
// Truncate long expressions
|
|
111
|
+
const display = expr.length > 100 ? expr.slice(0, 80) + `… (${expr.length} chars)` : expr
|
|
112
|
+
msg += `\n ${currentDir}="${display}"`
|
|
113
|
+
}
|
|
114
|
+
console.error(msg)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @callback DirectiveHandler
|
|
119
|
+
* @param {Element} el - Target element
|
|
120
|
+
* @param {Object} state - Reactive state object
|
|
121
|
+
* @param {string} expr - Expression string
|
|
122
|
+
* @param {string} [name] - Directive name with modifiers
|
|
123
|
+
* @returns {((value: any) => void | (() => void)) | { [Symbol.dispose]: () => void } | void}
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @callback ModifierHandler
|
|
128
|
+
* @param {Function} fn - Function to modify
|
|
129
|
+
* @param {...string} args - Modifier arguments (from dash-separated values)
|
|
130
|
+
* @returns {Function}
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @typedef {Object} SpraeState
|
|
135
|
+
* @property {Record<string, Signal>} [_signals] - Internal signals map
|
|
136
|
+
*/
|
|
14
137
|
|
|
15
138
|
/**
|
|
16
139
|
* Applies directives to an HTML element and manages its reactive state.
|
|
17
140
|
*
|
|
18
141
|
* @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.
|
|
142
|
+
* @param {Object} [state] - Initial state values to populate the element's reactive state.
|
|
143
|
+
* @returns {SpraeState & Object} The reactive state object associated with the element.
|
|
21
144
|
*/
|
|
22
145
|
const sprae = (el = document.body, state) => {
|
|
23
146
|
// repeated call can be caused by eg. :each with new objects with old keys
|
|
@@ -50,6 +173,7 @@ const sprae = (el = document.body, state) => {
|
|
|
50
173
|
el.removeAttribute(name)
|
|
51
174
|
|
|
52
175
|
currentDir = name;
|
|
176
|
+
currentEl = el;
|
|
53
177
|
|
|
54
178
|
// directive initializer can be redefined
|
|
55
179
|
fx.push(start = dir(el, name.slice(prefix.length), value, state)), offs.push(start())
|
|
@@ -60,8 +184,12 @@ const sprae = (el = document.body, state) => {
|
|
|
60
184
|
}
|
|
61
185
|
|
|
62
186
|
// :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
|
|
63
|
-
//
|
|
64
|
-
|
|
187
|
+
// real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
|
|
188
|
+
if (el.firstChild !== undefined) {
|
|
189
|
+
let child = el.firstChild, next
|
|
190
|
+
while (child) (next = child.nextSibling, child.nodeType == 1 && add(child), child = next)
|
|
191
|
+
}
|
|
192
|
+
else for (let child of el.childNodes) child.nodeType == 1 && add(child)
|
|
65
193
|
};
|
|
66
194
|
|
|
67
195
|
add(el);
|
|
@@ -75,80 +203,82 @@ const sprae = (el = document.body, state) => {
|
|
|
75
203
|
}
|
|
76
204
|
|
|
77
205
|
// directive initializer
|
|
206
|
+
/** @type {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} */
|
|
78
207
|
export let dir
|
|
79
208
|
|
|
80
209
|
/**
|
|
81
|
-
* Compiles an expression into an evaluator function.
|
|
82
|
-
* @type {(
|
|
210
|
+
* Compiles an expression string into an evaluator function.
|
|
211
|
+
* @type {(expr: string) => (state: Object) => any}
|
|
83
212
|
*/
|
|
84
213
|
export let compile
|
|
85
214
|
|
|
86
215
|
/**
|
|
87
216
|
* Parses an expression into an evaluator function, caching the result for reuse.
|
|
88
217
|
*
|
|
89
|
-
* @param {string} expr The expression to parse and compile into a function.
|
|
90
|
-
* @returns {
|
|
218
|
+
* @param {string} expr - The expression to parse and compile into a function.
|
|
219
|
+
* @returns {(state: Object, cb?: (value: any) => any) => any} The compiled evaluator function for the expression.
|
|
91
220
|
*/
|
|
92
221
|
export const parse = (expr) => {
|
|
93
222
|
let fn = cache[expr=expr.trim()]
|
|
94
223
|
if (fn) return fn
|
|
95
224
|
|
|
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
225
|
// static time errors
|
|
108
226
|
try {
|
|
109
|
-
fn = compile(
|
|
227
|
+
fn = compile(expr || 'undefined')
|
|
110
228
|
// Object.defineProperty(fn, "name", { value: `∴ ${expr}` })
|
|
111
|
-
} catch (e) {
|
|
229
|
+
} catch (e) { err(e, expr) }
|
|
112
230
|
|
|
113
231
|
// run time errors
|
|
114
232
|
return cache[expr] = function (state, cb, _out) {
|
|
115
233
|
try {
|
|
116
234
|
let result = fn?.call(this, state)
|
|
117
235
|
// 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
|
|
236
|
+
if (cb) return result?.then
|
|
237
|
+
? (result.then(v => _out = cb(v)).catch(e => err(e, expr)), () => typeof _out === 'function' && _out())
|
|
238
|
+
: cb(result)
|
|
119
239
|
else return result
|
|
120
240
|
} catch (e) {
|
|
121
|
-
|
|
241
|
+
err(e, expr)
|
|
122
242
|
}
|
|
123
243
|
}
|
|
124
244
|
}
|
|
125
245
|
const cache = {};
|
|
126
246
|
|
|
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
247
|
|
|
248
|
+
/**
|
|
249
|
+
* @typedef {Object} SpraeConfig
|
|
250
|
+
* @property {(expr: string) => (state: Object) => any} [compile] - Custom expression compiler
|
|
251
|
+
* @property {string} [prefix] - Directive prefix (default: ':')
|
|
252
|
+
* @property {<T>(value: T) => Signal<T>} [signal] - Signal factory
|
|
253
|
+
* @property {(fn: () => void | (() => void)) => () => void} [effect] - Effect factory
|
|
254
|
+
* @property {<T>(fn: () => T) => Signal<T>} [computed] - Computed factory
|
|
255
|
+
* @property {<T>(fn: () => T) => T} [batch] - Batch function
|
|
256
|
+
* @property {<T>(fn: () => T) => T} [untracked] - Untracked function
|
|
257
|
+
* @property {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} [dir] - Directive initializer
|
|
258
|
+
*/
|
|
136
259
|
|
|
137
260
|
/**
|
|
138
|
-
* Configure sprae
|
|
261
|
+
* Configure sprae with custom signals, compiler, or prefix.
|
|
262
|
+
* @param {SpraeConfig} config - Configuration options
|
|
263
|
+
* @returns {void}
|
|
139
264
|
*/
|
|
140
|
-
export const use = (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
265
|
+
export const use = (config) => (
|
|
266
|
+
config.compile && (compile = config.compile),
|
|
267
|
+
config.prefix && (prefix = config.prefix),
|
|
268
|
+
config.signal && (signal = config.signal),
|
|
269
|
+
config.effect && (effect = config.effect),
|
|
270
|
+
config.computed && (computed = config.computed),
|
|
271
|
+
config.batch && (batch = config.batch),
|
|
272
|
+
config.untracked && (untracked = config.untracked),
|
|
273
|
+
config.dir && (dir = config.dir)
|
|
149
274
|
)
|
|
150
275
|
|
|
151
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Applies modifiers to a function.
|
|
278
|
+
* @param {Function & { target?: Element }} fn - Function to decorate
|
|
279
|
+
* @param {string[]} mods - Modifier names with arguments (e.g., ['throttle-500', 'prevent'])
|
|
280
|
+
* @returns {Function} Decorated function
|
|
281
|
+
*/
|
|
152
282
|
export const decorate = (fn, mods) => {
|
|
153
283
|
while (mods.length) {
|
|
154
284
|
let [name, ...params] = mods.pop().split('-'), mod = modifier[name], wrapFn
|
|
@@ -162,7 +292,21 @@ export const decorate = (fn, mods) => {
|
|
|
162
292
|
return fn
|
|
163
293
|
}
|
|
164
294
|
|
|
165
|
-
|
|
295
|
+
/**
|
|
296
|
+
* @typedef {Object} FragmentLike
|
|
297
|
+
* @property {Node[]} childNodes - Child nodes of the fragment
|
|
298
|
+
* @property {DocumentFragment} content - The document fragment content
|
|
299
|
+
* @property {() => void} remove - Remove the fragment from DOM
|
|
300
|
+
* @property {(el: Node) => void} replaceWith - Replace the fragment with an element
|
|
301
|
+
* @property {Attr[]} attributes - Attributes from the original template
|
|
302
|
+
* @property {(name: string) => void} removeAttribute - Remove an attribute
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Creates a fragment holder from a template element with minimal API surface.
|
|
307
|
+
* @param {HTMLTemplateElement | FragmentLike} tpl - Template element or existing fragment
|
|
308
|
+
* @returns {FragmentLike} Fragment-like object
|
|
309
|
+
*/
|
|
166
310
|
export const frag = (tpl) => {
|
|
167
311
|
if (!tpl.nodeType) return tpl // existing tpl
|
|
168
312
|
|
|
@@ -173,7 +317,6 @@ export const frag = (tpl) => {
|
|
|
173
317
|
childNodes = (content.append(ref), [...content.childNodes])
|
|
174
318
|
|
|
175
319
|
return {
|
|
176
|
-
// get parentNode() { return childNodes[0].parentNode },
|
|
177
320
|
childNodes,
|
|
178
321
|
content,
|
|
179
322
|
remove: () => content.append(...childNodes),
|
|
@@ -184,25 +327,45 @@ export const frag = (tpl) => {
|
|
|
184
327
|
},
|
|
185
328
|
attributes,
|
|
186
329
|
removeAttribute(name) { attributes.splice(attributes.findIndex(a => a.name === name), 1) },
|
|
187
|
-
// setAttributeNode() { }
|
|
188
330
|
}
|
|
189
331
|
}
|
|
190
332
|
|
|
191
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Converts camelCase to kebab-case.
|
|
335
|
+
* @param {string} str - String to convert
|
|
336
|
+
* @returns {string} Kebab-case string
|
|
337
|
+
*/
|
|
192
338
|
export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
|
|
193
339
|
|
|
194
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Sets or removes an attribute on an element.
|
|
342
|
+
* @param {Element} el - Target element
|
|
343
|
+
* @param {string} name - Attribute name
|
|
344
|
+
* @param {string | boolean | null | undefined} v - Attribute value (null/false removes, true sets empty)
|
|
345
|
+
* @returns {void}
|
|
346
|
+
*/
|
|
195
347
|
export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) : el.setAttribute(name, v === true ? "" : v);
|
|
196
348
|
|
|
197
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Converts class input to className string (like clsx/classnames).
|
|
351
|
+
* @param {string | string[] | Record<string, boolean> | null | undefined} c - Class input
|
|
352
|
+
* @returns {string} Space-separated class string
|
|
353
|
+
*/
|
|
198
354
|
export const clsx = (c, _out = []) => !c ? '' : typeof c === 'string' ? c : (
|
|
199
355
|
Array.isArray(c) ? c.map(clsx) :
|
|
200
356
|
Object.entries(c).reduce((s, [k, v]) => !v ? s : [...s, k], [])
|
|
201
357
|
).join(' ')
|
|
202
358
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Throttles a function to run at most once per tick (or custom scheduler).
|
|
361
|
+
* Fires on leading edge, then on trailing edge if called during throttle.
|
|
362
|
+
* @template {Function} T
|
|
363
|
+
* @param {T} fn - Function to throttle
|
|
364
|
+
* @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
|
|
365
|
+
* @returns {T} Throttled function
|
|
366
|
+
*/
|
|
367
|
+
export const throttle = (fn, ms) => {
|
|
368
|
+
let _planned = 0, arg, schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
|
|
206
369
|
const throttled = (e) => {
|
|
207
370
|
arg = e
|
|
208
371
|
if (!_planned++) fn(arg), schedule((_dirty = _planned > 1) => (
|
|
@@ -212,8 +375,26 @@ export const throttle = (fn, schedule = queueMicrotask) => {
|
|
|
212
375
|
return throttled;
|
|
213
376
|
}
|
|
214
377
|
|
|
215
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Debounces a function to run after a delay since the last call.
|
|
380
|
+
* @template {Function} T
|
|
381
|
+
* @param {T} fn - Function to debounce
|
|
382
|
+
* @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
|
|
383
|
+
* @param {boolean} [immediate=false] - Fire on leading edge instead of trailing
|
|
384
|
+
* @returns {T} Debounced function
|
|
385
|
+
*/
|
|
386
|
+
export const debounce = (fn, ms, immediate) => {
|
|
387
|
+
let schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
|
|
388
|
+
return immediate
|
|
389
|
+
? ((_blocked) => (arg) => !_blocked && (fn(arg), _blocked = 1, schedule(() => _blocked = 0)))()
|
|
390
|
+
: ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
|
|
391
|
+
}
|
|
216
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Parses time string to milliseconds. Supports: 100, 100ms, 1s, 1m
|
|
395
|
+
* @param {string|number} t - Time value
|
|
396
|
+
* @returns {number} Milliseconds
|
|
397
|
+
*/
|
|
217
398
|
export * from './store.js';
|
|
218
399
|
|
|
219
400
|
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
|
+
}
|