sprae 11.5.7 → 12.0.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 +229 -90
- package/directive/class.js +9 -13
- package/directive/default.js +2 -154
- package/directive/each.js +79 -75
- package/directive/else.js +22 -0
- package/directive/fx.js +2 -2
- package/directive/if.js +40 -34
- package/directive/ref.js +8 -7
- package/directive/scope.js +17 -0
- package/directive/spread.js +3 -0
- package/directive/style.js +9 -7
- package/directive/text.js +4 -4
- package/directive/value.js +39 -37
- package/dist/sprae.js +3 -495
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +3 -640
- package/dist/sprae.umd.js.map +4 -4
- package/package.json +16 -14
- package/readme.md +432 -205
- package/signal.js +41 -40
- package/sprae.js +127 -18
- package/store.js +109 -96
- package/directive/aria.js +0 -6
- package/directive/data.js +0 -3
- package/directive/with.js +0 -12
- package/dist/sprae.auto.js +0 -662
- package/dist/sprae.auto.js.map +0 -7
- package/dist/sprae.auto.min.js +0 -5
- package/dist/sprae.auto.min.js.map +0 -7
- package/dist/sprae.min.js +0 -5
- package/dist/sprae.min.js.map +0 -7
- package/dist/sprae.umd.min.js +0 -5
- package/dist/sprae.umd.min.js.map +0 -7
package/core.js
CHANGED
|
@@ -1,138 +1,247 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { store } from './store.js';
|
|
1
|
+
import store, { _change, _signals } from "./store.js";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export const _dispose = (Symbol.dispose ||= Symbol("dispose")),
|
|
4
|
+
_state = Symbol("state"),
|
|
5
|
+
_on = Symbol('on'),
|
|
6
|
+
_off = Symbol('off'),
|
|
7
|
+
_add = Symbol('add');
|
|
6
8
|
|
|
7
|
-
export const _state = Symbol("state"), _on = Symbol('on'), _off = Symbol('off')
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
export const directive = {}
|
|
10
|
+
export let prefix = ':', signal, effect, computed, batch = (fn) => fn(), untracked = batch;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* @param {(el: Element, state: Object, expr: string, name: string) => (value: any) => void} create - A function to create the directive.
|
|
16
|
-
* @param {(expr: string) => (state: Object) => any} [p=parse] - Create evaluator from expression string.
|
|
17
|
-
*/
|
|
18
|
-
export const dir = (name, create, p = parse) => directive[name] = (el, expr, state, name, update, evaluate) => (
|
|
19
|
-
update = create(el, state, expr, name),
|
|
20
|
-
evaluate = p(expr, ':'+name),
|
|
21
|
-
() => update(evaluate(state))
|
|
22
|
-
)
|
|
12
|
+
export let directive = {}, modifier = {}
|
|
13
|
+
|
|
14
|
+
let currentDir = null;
|
|
23
15
|
|
|
24
16
|
/**
|
|
25
17
|
* Applies directives to an HTML element and manages its reactive state.
|
|
26
18
|
*
|
|
27
19
|
* @param {Element} [el=document.body] - The target HTML element to apply directives to.
|
|
28
|
-
* @param {Object} [
|
|
20
|
+
* @param {Object|store} [state] - Initial state values to populate the element's reactive state.
|
|
29
21
|
* @returns {Object} The reactive state object associated with the element.
|
|
30
22
|
*/
|
|
31
|
-
|
|
23
|
+
const sprae = (el = document.body, state) => {
|
|
32
24
|
// repeated call can be caused by eg. :each with new objects with old keys
|
|
33
|
-
if (el[_state]) return Object.assign(el[_state],
|
|
25
|
+
if (el[_state]) return Object.assign(el[_state], state)
|
|
26
|
+
|
|
27
|
+
// console.group('sprae', el.outerHTML)
|
|
34
28
|
|
|
35
29
|
// take over existing state instead of creating a clone
|
|
36
|
-
|
|
30
|
+
state = store(state || {})
|
|
37
31
|
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let { name, value } = attrs[i], update, dir
|
|
32
|
+
let fx = [], offs = [], fn,
|
|
33
|
+
on = () => (!offs && (offs = fx.map(fn => fn()))),
|
|
34
|
+
off = () => (offs?.map(off => off()), offs = null)
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
// on/off all effects
|
|
37
|
+
// we don't call prevOn as convention: everything defined before :else :if won't be disabled by :if
|
|
38
|
+
// imagine <x :onx="..." :if="..."/> - when :if is false, it disables directives after :if (calls _off) but ignores :onx
|
|
39
|
+
el[_on] = on
|
|
40
|
+
el[_off] = off
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
update = (directive[dir] || directive.default)(el, value, state, dir)
|
|
42
|
+
// destroy
|
|
43
|
+
el[_dispose] ||= () => (el[_off](), el[_off] = el[_on] = el[_dispose] = el[_state] = el[_add] = null)
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const add = (el, _attrs = el.attributes) => {
|
|
46
|
+
// we iterate live collection (subsprae can init args)
|
|
47
|
+
if (_attrs) for (let i = 0; i < _attrs.length;) {
|
|
48
|
+
let { name, value } = _attrs[i]
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
} else i++
|
|
60
|
-
}
|
|
50
|
+
if (name.startsWith(prefix)) {
|
|
51
|
+
el.removeAttribute(name)
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
// directive initializer can be redefined
|
|
54
|
+
fx.push(fn = initDirective(el, name, value, state))
|
|
55
|
+
offs.push(fn())
|
|
65
56
|
|
|
66
|
-
|
|
57
|
+
// stop after subsprae like :each, :if, :scope etc.
|
|
58
|
+
if (_state in el) return
|
|
59
|
+
} else i++
|
|
60
|
+
}
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
el
|
|
62
|
+
// :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
|
|
63
|
+
// for (let i = 0, child; i < (el.childNodes.length); i++) child = el.childNodes[i], child.nodeType == 1 && add(child)
|
|
64
|
+
for (let child of [...el.childNodes]) child.nodeType == 1 && add(child)
|
|
65
|
+
};
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
el[_off] = () => (offs.map(off => off()), offs = [])
|
|
74
|
-
el[_on] = () => offs = fx.map(f => effect(f))
|
|
67
|
+
el[_add] = add;
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
add(el);
|
|
70
|
+
|
|
71
|
+
// if element was spraed by inline :with/:if/:each/etc instruction (meaning it has state placeholder) - skip, otherwise save _state
|
|
72
|
+
if (el[_state] === undefined) el[_state] = state
|
|
73
|
+
|
|
74
|
+
// console.groupEnd()
|
|
79
75
|
|
|
80
76
|
return state;
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
// configure signals/compile
|
|
84
|
-
// it's more compact than using sprae.signal = signal etc.
|
|
85
|
-
sprae.use = s => (
|
|
86
|
-
s.signal && use(s),
|
|
87
|
-
s.compile && (compile = s.compile),
|
|
88
|
-
s.prefix && (prefix = s.prefix)
|
|
89
|
-
)
|
|
90
79
|
|
|
91
80
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* @
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (fn = memo[expr = expr.trim()]) return fn
|
|
81
|
+
* Initializes directive (defined by sprae build), returns "on" function that enables it
|
|
82
|
+
* Multiprop sequences initializer, eg. :a:b..c:d
|
|
83
|
+
* @type {(el: HTMLElement, name:string, value:string, state:Object) => Function}
|
|
84
|
+
* */
|
|
85
|
+
const initDirective = (el, attrName, expr, state) => {
|
|
86
|
+
let cur, // current step callback
|
|
87
|
+
off // current step disposal
|
|
100
88
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
let steps = attrName.slice(prefix.length).split('..').map((step, i, { length }) => (
|
|
90
|
+
// multiple attributes like :id:for=""
|
|
91
|
+
step.split(prefix).reduce((prev, str) => {
|
|
92
|
+
let [name, ...mods] = str.split('.');
|
|
93
|
+
let evaluate = parse(expr, directive[currentDir = name]?.parse)
|
|
104
94
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
// events have no effects and can be sequenced
|
|
96
|
+
if (name.startsWith('on')) {
|
|
97
|
+
let type = name.slice(2),
|
|
98
|
+
first = e => (call(evaluate(state), e)),
|
|
99
|
+
fn = applyMods(
|
|
100
|
+
Object.assign(
|
|
101
|
+
// single event vs chain
|
|
102
|
+
length == 1 ? first :
|
|
103
|
+
e => (cur = (!i ? first : cur)(e), off(), off = steps[(i + 1) % length]()),
|
|
104
|
+
{ target: el, type }
|
|
105
|
+
),
|
|
106
|
+
mods);
|
|
107
|
+
|
|
108
|
+
return (_poff) => (_poff = prev?.(), fn.target.addEventListener(type, fn, fn), () => (_poff?.(), fn.target.removeEventListener(type, fn)))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// props have no sequences and can be sync
|
|
112
|
+
let update = (directive[name] || directive['*'])(el, state, expr, name)
|
|
113
|
+
|
|
114
|
+
// no-modifiers shortcut
|
|
115
|
+
if (!mods.length && !prev) return () => update && effect(() => evaluate(state, update))
|
|
116
|
+
|
|
117
|
+
let dispose,
|
|
118
|
+
change = signal(-1), // signal authorized to trigger effect: 0 = init; >0 = trigger
|
|
119
|
+
count = -1, // called effect count
|
|
120
|
+
|
|
121
|
+
// effect applier - first time it applies the effect, next times effect is triggered by change signal
|
|
122
|
+
fn = throttle(applyMods(() => {
|
|
123
|
+
if (++change.value) return // all calls except for the first one are handled by effect
|
|
124
|
+
dispose = effect(() => update && (
|
|
125
|
+
change.value == count ? fn() : // separate tick makes sure planner effect call is finished before real eval call
|
|
126
|
+
(count = change.value, evaluate(state, update)) // if changed more than effect called - call it
|
|
127
|
+
));
|
|
128
|
+
}, mods))
|
|
129
|
+
|
|
130
|
+
return (_poff) => (
|
|
131
|
+
_poff = prev?.(),
|
|
132
|
+
// console.log('ON', name),
|
|
133
|
+
fn(),
|
|
134
|
+
({
|
|
135
|
+
[name]: () => (
|
|
136
|
+
// console.log('OFF', name, el),
|
|
137
|
+
_poff?.(), dispose(), change.value = -1, count = dispose = null
|
|
138
|
+
)
|
|
139
|
+
})[name]
|
|
140
|
+
)
|
|
141
|
+
}, null)
|
|
142
|
+
));
|
|
143
|
+
|
|
144
|
+
// off can be changed on the go
|
|
145
|
+
return () => (off = steps[0]())
|
|
110
146
|
}
|
|
111
|
-
|
|
147
|
+
|
|
112
148
|
|
|
113
149
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* @param {Error} e - The original error object to enhance.
|
|
117
|
-
* @param {string} dir - The directive where the error occurred.
|
|
118
|
-
* @param {string} [expr=''] - The expression associated with the error, if any.
|
|
119
|
-
* @throws {Error} The enhanced error object with a formatted message.
|
|
150
|
+
* Configure sprae
|
|
120
151
|
*/
|
|
121
|
-
export const
|
|
122
|
-
|
|
152
|
+
export const use = (s) => (
|
|
153
|
+
s.compile && (compile = s.compile),
|
|
154
|
+
s.prefix && (prefix = s.prefix),
|
|
155
|
+
s.signal && (signal = s.signal),
|
|
156
|
+
s.effect && (effect = s.effect),
|
|
157
|
+
s.computed && (computed = s.computed),
|
|
158
|
+
s.batch && (batch = s.batch),
|
|
159
|
+
s.untracked && (untracked = s.untracked)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Lifecycle hanger: makes DOM slightly slower but spraes automatically
|
|
165
|
+
*/
|
|
166
|
+
export const start = (root = document.body, values) => {
|
|
167
|
+
const state = store(values);
|
|
168
|
+
sprae(root, state);
|
|
169
|
+
const mo = new MutationObserver(mutations => {
|
|
170
|
+
for (const m of mutations) {
|
|
171
|
+
for (const el of m.addedNodes) {
|
|
172
|
+
if (el.nodeType === 1 && el[_state] === undefined) {
|
|
173
|
+
for (const attr of el.attributes) {
|
|
174
|
+
if (attr.name.startsWith(prefix)) {
|
|
175
|
+
root[_add](el); break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// for (const el of m.removedNodes) el[Symbol.dispose]?.()
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
184
|
+
return state
|
|
123
185
|
}
|
|
124
186
|
|
|
187
|
+
|
|
125
188
|
/**
|
|
126
189
|
* Compiles an expression into an evaluator function.
|
|
127
|
-
*
|
|
128
|
-
* @type {(expr: string) => Function}
|
|
190
|
+
* @type {(dir:string, expr: string, clean?: string => string) => Function}
|
|
129
191
|
*/
|
|
130
192
|
export let compile
|
|
131
193
|
|
|
132
194
|
/**
|
|
133
|
-
*
|
|
195
|
+
* Parses an expression into an evaluator function, caching the result for reuse.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} expr The expression to parse and compile into a function.
|
|
198
|
+
* @returns {Function} The compiled evaluator function for the expression.
|
|
134
199
|
*/
|
|
135
|
-
export
|
|
200
|
+
export const parse = (expr, prepare, _fn) => {
|
|
201
|
+
if (_fn = parse.cache[expr]) return _fn
|
|
202
|
+
|
|
203
|
+
let _expr = expr.trim() || 'undefined'
|
|
204
|
+
if (prepare) _expr = prepare(_expr)
|
|
205
|
+
|
|
206
|
+
// if, const, let - no return
|
|
207
|
+
if (/^(if|let|const)\b/.test(_expr) || /;/.test(_expr)) ;
|
|
208
|
+
else _expr = `return ${_expr}`
|
|
209
|
+
|
|
210
|
+
// async expression
|
|
211
|
+
if (/\bawait\s/.test(_expr)) _expr = `return (async()=>{ ${_expr} })()`
|
|
212
|
+
|
|
213
|
+
// static time errors
|
|
214
|
+
try {
|
|
215
|
+
_fn = compile(_expr)
|
|
216
|
+
Object.defineProperty(_fn, "name", {value: `∴ ${expr}`})
|
|
217
|
+
} catch (e) { console.error(`∴ ${e}\n\n${prefix + currentDir}="${expr}"`) }
|
|
218
|
+
|
|
219
|
+
// run time errors
|
|
220
|
+
return parse.cache[expr] = (state, cb, _out) => {
|
|
221
|
+
try {
|
|
222
|
+
let result = _fn?.(state)
|
|
223
|
+
// if cb is given - call it with result and return function that returns last cb result - needed for effect cleanup
|
|
224
|
+
if (cb) return result?.then ? result.then(v => _out = cb(v)) : _out = cb(result), () => call(_out)
|
|
225
|
+
else return result
|
|
226
|
+
} catch (e) {
|
|
227
|
+
console.error(`∴ ${e}\n\n${prefix + currentDir}="${expr}"`)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
parse.cache = {};
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
// apply modifiers to context (from the end due to nature of wrapping ctx.call)
|
|
235
|
+
const applyMods = (fn, mods) => {
|
|
236
|
+
while (mods.length) {
|
|
237
|
+
let [name, ...params] = mods.pop().split('-')
|
|
238
|
+
fn = sx(modifier[name]?.(fn, ...params) ?? fn, fn)
|
|
239
|
+
}
|
|
240
|
+
return fn
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// soft-extend missing props and ignoring signals
|
|
244
|
+
const sx = (a, b) => { if (a != b) for (let k in b) (a[k] ??= b[k]); return a }
|
|
136
245
|
|
|
137
246
|
// instantiated <template> fragment holder, like persisting fragment but with minimal API surface
|
|
138
247
|
export const frag = (tpl) => {
|
|
@@ -160,4 +269,34 @@ export const frag = (tpl) => {
|
|
|
160
269
|
}
|
|
161
270
|
}
|
|
162
271
|
|
|
272
|
+
// if value is function - return result of its call
|
|
273
|
+
export const call = (v, arg) => typeof v === 'function' ? v(arg) : v
|
|
274
|
+
|
|
275
|
+
// camel to kebab
|
|
276
|
+
export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
|
|
277
|
+
|
|
278
|
+
// set attr
|
|
279
|
+
export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) : el.setAttribute(name, v === true ? "" : v);
|
|
280
|
+
|
|
281
|
+
// convert any-arg to className string
|
|
282
|
+
export const clsx = (c, _out = []) => !c ? '' : typeof c === 'string' ? c : (
|
|
283
|
+
Array.isArray(c) ? c.map(clsx) :
|
|
284
|
+
Object.entries(c).reduce((s, [k, v]) => !v ? s : [...s, k], [])
|
|
285
|
+
).join(' ')
|
|
286
|
+
|
|
287
|
+
// throttle function to (once per tick or other custom scheduler)
|
|
288
|
+
export const throttle = (fn, schedule = queueMicrotask) => {
|
|
289
|
+
let _planned = 0;
|
|
290
|
+
const throttled = (e) => {
|
|
291
|
+
if (!_planned++) fn(e), schedule((_dirty = _planned > 1) => (
|
|
292
|
+
_planned = 0, _dirty && throttled(e)
|
|
293
|
+
));
|
|
294
|
+
}
|
|
295
|
+
return throttled;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const debounce = (fn, schedule = queueMicrotask, _count = 0) => (arg, _planned=++_count) => schedule(() => (_planned == _count && fn(arg)))
|
|
299
|
+
|
|
300
|
+
export * from './store.js';
|
|
301
|
+
|
|
163
302
|
export default sprae
|
package/directive/class.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { clsx, call } from "../core.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
v => {
|
|
6
|
-
|
|
7
|
-
if (v)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
for (let cls of cur) if (clsx.has(cls)) clsx.delete(cls); else el.classList.remove(cls);
|
|
13
|
-
for (let cls of cur = clsx) el.classList.add(cls)
|
|
14
|
-
})
|
|
3
|
+
export default (el, _cur, _new) => (
|
|
4
|
+
_cur = new Set,
|
|
5
|
+
(v) => {
|
|
6
|
+
_new = new Set
|
|
7
|
+
if (v) clsx(call(v, el.className)).split(' ').map(c => c && _new.add(c))
|
|
8
|
+
for (let c of _cur) if (_new.has(c)) _new.delete(c); else el.classList.remove(c);
|
|
9
|
+
for (let c of _cur = _new) el.classList.add(c)
|
|
10
|
+
}
|
|
15
11
|
)
|
package/directive/default.js
CHANGED
|
@@ -1,155 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import { dir, err } from "../core.js";
|
|
1
|
+
import { attr, call } from "../core.js";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
// simple prop
|
|
6
|
-
if (!name.startsWith('on'))
|
|
7
|
-
return name ?
|
|
8
|
-
value => attr(target, name, value) :
|
|
9
|
-
value => { for (let key in value) attr(target, dashcase(key), value[key]) };
|
|
10
|
-
|
|
11
|
-
// bind event to a target
|
|
12
|
-
// NOTE: if you decide to remove chain of events, thing again - that's unique feature of sprae, don't diminish your own value.
|
|
13
|
-
// ona..onb
|
|
14
|
-
let ctxs = name.split('..').map(e => {
|
|
15
|
-
let ctx = { evt: '', target, test: () => true };
|
|
16
|
-
ctx.evt = (e.startsWith('on') ? e.slice(2) : e).replace(/\.(\w+)?-?([-\w]+)?/g,
|
|
17
|
-
(_, mod, param = '') => (ctx.test = mods[mod]?.(ctx, ...param.split('-')) || ctx.test, '')
|
|
18
|
-
);
|
|
19
|
-
return ctx;
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// add listener with the context
|
|
23
|
-
let addListener = (fn, { evt, target, test, defer, stop, prevent, immediate, ...opts }, cb) => {
|
|
24
|
-
if (defer) fn = defer(fn)
|
|
25
|
-
|
|
26
|
-
cb = (e) => {
|
|
27
|
-
try {
|
|
28
|
-
test(e) && (stop && (immediate ? e.stopImmediatePropagation() : e.stopPropagation()), prevent && e.preventDefault(), fn?.call(state, e))
|
|
29
|
-
} catch (error) { err(error, `:on${evt}`, fn) }
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
target.addEventListener(evt, cb, opts)
|
|
33
|
-
return () => target.removeEventListener(evt, cb, opts)
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// single event
|
|
37
|
-
if (ctxs.length == 1) return v => addListener(v, ctxs[0])
|
|
38
|
-
|
|
39
|
-
// events cycler
|
|
40
|
-
let startFn, nextFn, off, idx = 0
|
|
41
|
-
let nextListener = (fn) => {
|
|
42
|
-
off = addListener((e) => (
|
|
43
|
-
off(), nextFn = fn?.(e), (idx = ++idx % ctxs.length) ? nextListener(nextFn) : (startFn && nextListener(startFn))
|
|
44
|
-
), ctxs[idx]);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return value => (
|
|
48
|
-
startFn = value,
|
|
49
|
-
!off && nextListener(startFn),
|
|
50
|
-
() => startFn = null // nil startFn to autodispose chain
|
|
51
|
-
)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
// event modifiers
|
|
55
|
-
const mods = {
|
|
56
|
-
// actions
|
|
57
|
-
prevent(ctx) { ctx.prevent = true; },
|
|
58
|
-
stop(ctx) { ctx.stop = true; },
|
|
59
|
-
immediate(ctx) { ctx.immediate = true; },
|
|
60
|
-
|
|
61
|
-
// options
|
|
62
|
-
once(ctx) { ctx.once = true; },
|
|
63
|
-
passive(ctx) { ctx.passive = true; },
|
|
64
|
-
capture(ctx) { ctx.capture = true; },
|
|
65
|
-
|
|
66
|
-
// target
|
|
67
|
-
window(ctx) { ctx.target = window; },
|
|
68
|
-
document(ctx) { ctx.target = document; },
|
|
69
|
-
parent(ctx) { ctx.target = ctx.target.parentNode; },
|
|
70
|
-
|
|
71
|
-
throttle(ctx, limit=108) { ctx.defer = (fn) => throttle(fn, limit)},
|
|
72
|
-
debounce(ctx, wait=108) { ctx.defer = (fn) => debounce(fn, wait) },
|
|
73
|
-
|
|
74
|
-
// test
|
|
75
|
-
outside: (ctx) => (e) => {
|
|
76
|
-
let target = ctx.target;
|
|
77
|
-
if (target.contains(e.target)) return false;
|
|
78
|
-
if (e.target.isConnected === false) return false;
|
|
79
|
-
if (target.offsetWidth < 1 && target.offsetHeight < 1) return false;
|
|
80
|
-
return true;
|
|
81
|
-
},
|
|
82
|
-
self: (ctx) => (e) => e.target === ctx.target,
|
|
83
|
-
|
|
84
|
-
// keyboard
|
|
85
|
-
ctrl: (_, ...param) => (e) => keys.ctrl(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
86
|
-
shift: (_, ...param) => (e) => keys.shift(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
87
|
-
alt: (_, ...param) => (e) => keys.alt(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
88
|
-
meta: (_, ...param) => (e) => keys.meta(e) && param.every((p) => (keys[p] ? keys[p](e) : e.key === p)),
|
|
89
|
-
// NOTE: we don't expose up/left/right/down as too verbose: can and better be handled/differentiated at once
|
|
90
|
-
arrow: () => keys.arrow,
|
|
91
|
-
enter: () => keys.enter,
|
|
92
|
-
esc: () => keys.esc,
|
|
93
|
-
tab: () => keys.tab,
|
|
94
|
-
space: () => keys.space,
|
|
95
|
-
delete: () => keys.delete,
|
|
96
|
-
digit: () => keys.digit,
|
|
97
|
-
letter: () => keys.letter,
|
|
98
|
-
char: () => keys.char,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// key testers
|
|
102
|
-
const keys = {
|
|
103
|
-
ctrl: (e) => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
|
|
104
|
-
shift: (e) => e.shiftKey || e.key === "Shift",
|
|
105
|
-
alt: (e) => e.altKey || e.key === "Alt",
|
|
106
|
-
meta: (e) => e.metaKey || e.key === "Meta" || e.key === "Command",
|
|
107
|
-
arrow: (e) => e.key.startsWith("Arrow"),
|
|
108
|
-
enter: (e) => e.key === "Enter",
|
|
109
|
-
esc: (e) => e.key.startsWith("Esc"),
|
|
110
|
-
tab: (e) => e.key === "Tab",
|
|
111
|
-
space: (e) => e.key === " " || e.key === "Space" || e.key === " ",
|
|
112
|
-
delete: (e) => e.key === "Delete" || e.key === "Backspace",
|
|
113
|
-
digit: (e) => /^\d$/.test(e.key),
|
|
114
|
-
letter: (e) => /^\p{L}$/gu.test(e.key),
|
|
115
|
-
char: (e) => /^\S$/.test(e.key),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// create delayed fns
|
|
119
|
-
const throttle = (fn, limit) => {
|
|
120
|
-
let pause, planned,
|
|
121
|
-
block = (e) => {
|
|
122
|
-
pause = true;
|
|
123
|
-
setTimeout(() => {
|
|
124
|
-
pause = false;
|
|
125
|
-
// if event happened during blocked time, it schedules call by the end
|
|
126
|
-
if (planned) return (planned = false), block(e), fn(e);
|
|
127
|
-
}, limit);
|
|
128
|
-
};
|
|
129
|
-
return (e) => {
|
|
130
|
-
if (pause) return (planned = true);
|
|
131
|
-
block(e);
|
|
132
|
-
return fn(e);
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const debounce = (fn, wait) => {
|
|
137
|
-
let timeout;
|
|
138
|
-
return (e) => {
|
|
139
|
-
clearTimeout(timeout);
|
|
140
|
-
timeout = setTimeout(() => {
|
|
141
|
-
timeout = null;
|
|
142
|
-
fn(e);
|
|
143
|
-
}, wait);
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// set attr
|
|
148
|
-
export const attr = (el, name, v) => {
|
|
149
|
-
if (v == null || v === false) el.removeAttribute(name);
|
|
150
|
-
else el.setAttribute(name, v === true ? "" : typeof v === "number" || typeof v === "string" ? v : "");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export const dashcase = (str) => {
|
|
154
|
-
return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
|
|
155
|
-
}
|
|
3
|
+
export default (el, st, ex, name) => v => attr(el, name, call(v, el.getAttribute(name)))
|