zero-query 0.9.1 → 0.9.6
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/README.md +11 -8
- package/cli/commands/dev/devtools/js/elements.js +5 -0
- package/cli/scaffold/app/app.js +1 -1
- package/cli/scaffold/global.css +1 -2
- package/cli/scaffold/index.html +2 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +429 -35
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +46 -2
- package/index.js +32 -4
- package/package.json +1 -1
- package/src/component.js +98 -8
- package/src/core.js +3 -1
- package/src/expression.js +103 -16
- package/src/http.js +6 -2
- package/src/router.js +6 -1
- package/src/utils.js +191 -5
- package/tests/audit.test.js +4030 -0
- package/tests/component.test.js +1185 -0
- package/tests/core.test.js +41 -0
- package/tests/expression.test.js +10 -10
- package/tests/utils.test.js +543 -1
- package/types/utils.d.ts +103 -0
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.9.
|
|
2
|
+
* zQuery (zeroQuery) v0.9.6
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -1218,6 +1218,8 @@ query.children = (parentId) => {
|
|
|
1218
1218
|
const p = document.getElementById(parentId);
|
|
1219
1219
|
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1220
1220
|
};
|
|
1221
|
+
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
1222
|
+
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
1221
1223
|
|
|
1222
1224
|
// Create element shorthand — returns ZQueryCollection for chaining
|
|
1223
1225
|
query.create = (tag, attrs = {}, ...children) => {
|
|
@@ -1225,7 +1227,7 @@ query.create = (tag, attrs = {}, ...children) => {
|
|
|
1225
1227
|
for (const [k, v] of Object.entries(attrs)) {
|
|
1226
1228
|
if (k === 'class') el.className = v;
|
|
1227
1229
|
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1228
|
-
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
|
|
1230
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
1229
1231
|
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1230
1232
|
else el.setAttribute(k, v);
|
|
1231
1233
|
}
|
|
@@ -1445,6 +1447,11 @@ function tokenize(expr) {
|
|
|
1445
1447
|
tokens.push({ t: T.OP, v: ch });
|
|
1446
1448
|
i++; continue;
|
|
1447
1449
|
}
|
|
1450
|
+
// Spread operator: ...
|
|
1451
|
+
if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
|
|
1452
|
+
tokens.push({ t: T.OP, v: '...' });
|
|
1453
|
+
i += 3; continue;
|
|
1454
|
+
}
|
|
1448
1455
|
if ('()[]{},.?:'.includes(ch)) {
|
|
1449
1456
|
tokens.push({ t: T.PUNC, v: ch });
|
|
1450
1457
|
i++; continue;
|
|
@@ -1508,7 +1515,7 @@ class Parser {
|
|
|
1508
1515
|
this.next(); // consume ?
|
|
1509
1516
|
const truthy = this.parseExpression(0);
|
|
1510
1517
|
this.expect(T.PUNC, ':');
|
|
1511
|
-
const falsy = this.parseExpression(
|
|
1518
|
+
const falsy = this.parseExpression(0);
|
|
1512
1519
|
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
1513
1520
|
continue;
|
|
1514
1521
|
}
|
|
@@ -1649,7 +1656,12 @@ class Parser {
|
|
|
1649
1656
|
this.expect(T.PUNC, '(');
|
|
1650
1657
|
const args = [];
|
|
1651
1658
|
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
1652
|
-
|
|
1659
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
1660
|
+
this.next();
|
|
1661
|
+
args.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
1662
|
+
} else {
|
|
1663
|
+
args.push(this.parseExpression(0));
|
|
1664
|
+
}
|
|
1653
1665
|
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1654
1666
|
}
|
|
1655
1667
|
this.expect(T.PUNC, ')');
|
|
@@ -1725,7 +1737,12 @@ class Parser {
|
|
|
1725
1737
|
this.next();
|
|
1726
1738
|
const elements = [];
|
|
1727
1739
|
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
1728
|
-
|
|
1740
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
1741
|
+
this.next();
|
|
1742
|
+
elements.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
1743
|
+
} else {
|
|
1744
|
+
elements.push(this.parseExpression(0));
|
|
1745
|
+
}
|
|
1729
1746
|
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1730
1747
|
}
|
|
1731
1748
|
this.expect(T.PUNC, ']');
|
|
@@ -1737,6 +1754,14 @@ class Parser {
|
|
|
1737
1754
|
this.next();
|
|
1738
1755
|
const properties = [];
|
|
1739
1756
|
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
1757
|
+
// Spread in object: { ...obj }
|
|
1758
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
1759
|
+
this.next();
|
|
1760
|
+
properties.push({ spread: true, value: this.parseExpression(0) });
|
|
1761
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1740
1765
|
const keyTok = this.next();
|
|
1741
1766
|
let key;
|
|
1742
1767
|
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
@@ -1768,7 +1793,13 @@ class Parser {
|
|
|
1768
1793
|
|
|
1769
1794
|
// new keyword
|
|
1770
1795
|
if (tok.v === 'new') {
|
|
1771
|
-
|
|
1796
|
+
let classExpr = this.parsePrimary();
|
|
1797
|
+
// Handle member access (e.g. ns.MyClass) without consuming call args
|
|
1798
|
+
while (this.peek().t === T.PUNC && this.peek().v === '.') {
|
|
1799
|
+
this.next();
|
|
1800
|
+
const prop = this.next();
|
|
1801
|
+
classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
|
|
1802
|
+
}
|
|
1772
1803
|
let args = [];
|
|
1773
1804
|
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1774
1805
|
args = this._parseArgs();
|
|
@@ -1880,6 +1911,12 @@ function evaluate(node, scope) {
|
|
|
1880
1911
|
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
1881
1912
|
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
1882
1913
|
if (name === 'console') return console;
|
|
1914
|
+
if (name === 'Map') return Map;
|
|
1915
|
+
if (name === 'Set') return Set;
|
|
1916
|
+
if (name === 'RegExp') return RegExp;
|
|
1917
|
+
if (name === 'Error') return Error;
|
|
1918
|
+
if (name === 'URL') return URL;
|
|
1919
|
+
if (name === 'URLSearchParams') return URLSearchParams;
|
|
1883
1920
|
return undefined;
|
|
1884
1921
|
}
|
|
1885
1922
|
|
|
@@ -1918,10 +1955,21 @@ function evaluate(node, scope) {
|
|
|
1918
1955
|
}
|
|
1919
1956
|
|
|
1920
1957
|
case 'optional_call': {
|
|
1921
|
-
const
|
|
1958
|
+
const calleeNode = node.callee;
|
|
1959
|
+
const args = _evalArgs(node.args, scope);
|
|
1960
|
+
// Method call: obj?.method() — bind `this` to obj
|
|
1961
|
+
if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
|
|
1962
|
+
const obj = evaluate(calleeNode.obj, scope);
|
|
1963
|
+
if (obj == null) return undefined;
|
|
1964
|
+
const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
|
|
1965
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
1966
|
+
const fn = obj[prop];
|
|
1967
|
+
if (typeof fn !== 'function') return undefined;
|
|
1968
|
+
return fn.apply(obj, args);
|
|
1969
|
+
}
|
|
1970
|
+
const callee = evaluate(calleeNode, scope);
|
|
1922
1971
|
if (callee == null) return undefined;
|
|
1923
1972
|
if (typeof callee !== 'function') return undefined;
|
|
1924
|
-
const args = node.args.map(a => evaluate(a, scope));
|
|
1925
1973
|
return callee(...args);
|
|
1926
1974
|
}
|
|
1927
1975
|
|
|
@@ -1931,7 +1979,7 @@ function evaluate(node, scope) {
|
|
|
1931
1979
|
// Only allow safe constructors
|
|
1932
1980
|
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
1933
1981
|
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
1934
|
-
const args = node.args
|
|
1982
|
+
const args = _evalArgs(node.args, scope);
|
|
1935
1983
|
return new Ctor(...args);
|
|
1936
1984
|
}
|
|
1937
1985
|
return undefined;
|
|
@@ -1961,13 +2009,32 @@ function evaluate(node, scope) {
|
|
|
1961
2009
|
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
1962
2010
|
}
|
|
1963
2011
|
|
|
1964
|
-
case 'array':
|
|
1965
|
-
|
|
2012
|
+
case 'array': {
|
|
2013
|
+
const arr = [];
|
|
2014
|
+
for (const e of node.elements) {
|
|
2015
|
+
if (e.type === 'spread') {
|
|
2016
|
+
const iterable = evaluate(e.arg, scope);
|
|
2017
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2018
|
+
for (const v of iterable) arr.push(v);
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
arr.push(evaluate(e, scope));
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
return arr;
|
|
2025
|
+
}
|
|
1966
2026
|
|
|
1967
2027
|
case 'object': {
|
|
1968
2028
|
const obj = {};
|
|
1969
|
-
for (const
|
|
1970
|
-
|
|
2029
|
+
for (const prop of node.properties) {
|
|
2030
|
+
if (prop.spread) {
|
|
2031
|
+
const source = evaluate(prop.value, scope);
|
|
2032
|
+
if (source != null && typeof source === 'object') {
|
|
2033
|
+
Object.assign(obj, source);
|
|
2034
|
+
}
|
|
2035
|
+
} else {
|
|
2036
|
+
obj[prop.key] = evaluate(prop.value, scope);
|
|
2037
|
+
}
|
|
1971
2038
|
}
|
|
1972
2039
|
return obj;
|
|
1973
2040
|
}
|
|
@@ -1988,12 +2055,30 @@ function evaluate(node, scope) {
|
|
|
1988
2055
|
}
|
|
1989
2056
|
}
|
|
1990
2057
|
|
|
2058
|
+
/**
|
|
2059
|
+
* Evaluate a list of argument AST nodes, flattening any spread elements.
|
|
2060
|
+
*/
|
|
2061
|
+
function _evalArgs(argNodes, scope) {
|
|
2062
|
+
const result = [];
|
|
2063
|
+
for (const a of argNodes) {
|
|
2064
|
+
if (a.type === 'spread') {
|
|
2065
|
+
const iterable = evaluate(a.arg, scope);
|
|
2066
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2067
|
+
for (const v of iterable) result.push(v);
|
|
2068
|
+
}
|
|
2069
|
+
} else {
|
|
2070
|
+
result.push(evaluate(a, scope));
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return result;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1991
2076
|
/**
|
|
1992
2077
|
* Resolve and execute a function call safely.
|
|
1993
2078
|
*/
|
|
1994
2079
|
function _resolveCall(node, scope) {
|
|
1995
2080
|
const callee = node.callee;
|
|
1996
|
-
const args = node.args
|
|
2081
|
+
const args = _evalArgs(node.args, scope);
|
|
1997
2082
|
|
|
1998
2083
|
// Method call: obj.method() — bind `this` to obj
|
|
1999
2084
|
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
@@ -2067,8 +2152,9 @@ function _evalBinary(node, scope) {
|
|
|
2067
2152
|
* @returns {*} — evaluation result, or undefined on error
|
|
2068
2153
|
*/
|
|
2069
2154
|
|
|
2070
|
-
// AST cache — avoids re-tokenizing and re-parsing the same expression
|
|
2071
|
-
//
|
|
2155
|
+
// AST cache (LRU) — avoids re-tokenizing and re-parsing the same expression.
|
|
2156
|
+
// Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
|
|
2157
|
+
// Eviction removes the least-recently-used (first) entry when at capacity.
|
|
2072
2158
|
const _astCache = new Map();
|
|
2073
2159
|
const _AST_CACHE_MAX = 512;
|
|
2074
2160
|
|
|
@@ -2088,9 +2174,12 @@ function safeEval(expr, scope) {
|
|
|
2088
2174
|
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2089
2175
|
}
|
|
2090
2176
|
|
|
2091
|
-
// Check AST cache
|
|
2177
|
+
// Check AST cache (LRU: move to end on hit)
|
|
2092
2178
|
let ast = _astCache.get(trimmed);
|
|
2093
|
-
if (
|
|
2179
|
+
if (ast) {
|
|
2180
|
+
_astCache.delete(trimmed);
|
|
2181
|
+
_astCache.set(trimmed, ast);
|
|
2182
|
+
} else {
|
|
2094
2183
|
const tokens = tokenize(trimmed);
|
|
2095
2184
|
const parser = new Parser(tokens, scope);
|
|
2096
2185
|
ast = parser.parse();
|
|
@@ -2798,7 +2887,7 @@ class Component {
|
|
|
2798
2887
|
const defaultSlotNodes = [];
|
|
2799
2888
|
[...el.childNodes].forEach(node => {
|
|
2800
2889
|
if (node.nodeType === 1 && node.hasAttribute('slot')) {
|
|
2801
|
-
const slotName = node.getAttribute('slot');
|
|
2890
|
+
const slotName = node.getAttribute('slot') || 'default';
|
|
2802
2891
|
if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
|
|
2803
2892
|
this._slotContent[slotName] += node.outerHTML;
|
|
2804
2893
|
} else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
|
|
@@ -3207,6 +3296,24 @@ class Component {
|
|
|
3207
3296
|
for (const [event, bindings] of eventMap) {
|
|
3208
3297
|
this._attachDelegatedEvent(event, bindings);
|
|
3209
3298
|
}
|
|
3299
|
+
|
|
3300
|
+
// .outside — attach a document-level listener for bindings that need
|
|
3301
|
+
// to detect clicks/events outside their element.
|
|
3302
|
+
this._outsideListeners = this._outsideListeners || [];
|
|
3303
|
+
for (const [event, bindings] of eventMap) {
|
|
3304
|
+
for (const binding of bindings) {
|
|
3305
|
+
if (!binding.modifiers.includes('outside')) continue;
|
|
3306
|
+
const outsideHandler = (e) => {
|
|
3307
|
+
if (binding.el.contains(e.target)) return;
|
|
3308
|
+
const match = binding.methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
|
|
3309
|
+
if (!match) return;
|
|
3310
|
+
const fn = this[match[1]];
|
|
3311
|
+
if (typeof fn === 'function') fn.call(this, e);
|
|
3312
|
+
};
|
|
3313
|
+
document.addEventListener(event, outsideHandler, true);
|
|
3314
|
+
this._outsideListeners.push({ event, handler: outsideHandler });
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3210
3317
|
}
|
|
3211
3318
|
|
|
3212
3319
|
// Attach a single delegated listener for an event type
|
|
@@ -3220,15 +3327,66 @@ class Component {
|
|
|
3220
3327
|
const handler = (e) => {
|
|
3221
3328
|
// Read bindings from live map — always up to date after re-renders
|
|
3222
3329
|
const currentBindings = this._eventBindings?.get(event) || [];
|
|
3223
|
-
|
|
3224
|
-
|
|
3330
|
+
|
|
3331
|
+
// Collect matching bindings with their matched elements, then sort
|
|
3332
|
+
// deepest-first so .stop correctly prevents ancestor handlers
|
|
3333
|
+
// (mimics real DOM bubbling order within delegated events).
|
|
3334
|
+
const hits = [];
|
|
3335
|
+
for (const binding of currentBindings) {
|
|
3336
|
+
const matched = e.target.closest(binding.selector);
|
|
3337
|
+
if (!matched) continue;
|
|
3338
|
+
hits.push({ ...binding, matched });
|
|
3339
|
+
}
|
|
3340
|
+
hits.sort((a, b) => {
|
|
3341
|
+
if (a.matched === b.matched) return 0;
|
|
3342
|
+
return a.matched.contains(b.matched) ? 1 : -1;
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
let stoppedAt = null; // Track elements that called .stop
|
|
3346
|
+
for (const { selector, methodExpr, modifiers, el, matched } of hits) {
|
|
3347
|
+
|
|
3348
|
+
// In delegated events, .stop should prevent ancestor bindings from
|
|
3349
|
+
// firing — stopPropagation alone only stops real DOM bubbling.
|
|
3350
|
+
if (stoppedAt) {
|
|
3351
|
+
let blocked = false;
|
|
3352
|
+
for (const stopped of stoppedAt) {
|
|
3353
|
+
if (matched.contains(stopped) && matched !== stopped) { blocked = true; break; }
|
|
3354
|
+
}
|
|
3355
|
+
if (blocked) continue;
|
|
3356
|
+
}
|
|
3225
3357
|
|
|
3226
3358
|
// .self — only fire if target is the element itself
|
|
3227
3359
|
if (modifiers.includes('self') && e.target !== el) continue;
|
|
3228
3360
|
|
|
3361
|
+
// .outside — only fire if event target is OUTSIDE the element
|
|
3362
|
+
if (modifiers.includes('outside')) {
|
|
3363
|
+
if (el.contains(e.target)) continue;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
// Key modifiers — filter keyboard events by key
|
|
3367
|
+
const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
|
|
3368
|
+
let keyFiltered = false;
|
|
3369
|
+
for (const mod of modifiers) {
|
|
3370
|
+
if (_keyMap[mod]) {
|
|
3371
|
+
const keys = _keyMap[mod].split('|');
|
|
3372
|
+
if (!e.key || !keys.includes(e.key)) { keyFiltered = true; break; }
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
if (keyFiltered) continue;
|
|
3376
|
+
|
|
3377
|
+
// System key modifiers — require modifier keys to be held
|
|
3378
|
+
if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
|
|
3379
|
+
if (modifiers.includes('shift') && !e.shiftKey) continue;
|
|
3380
|
+
if (modifiers.includes('alt') && !e.altKey) continue;
|
|
3381
|
+
if (modifiers.includes('meta') && !e.metaKey) continue;
|
|
3382
|
+
|
|
3229
3383
|
// Handle modifiers
|
|
3230
3384
|
if (modifiers.includes('prevent')) e.preventDefault();
|
|
3231
|
-
if (modifiers.includes('stop'))
|
|
3385
|
+
if (modifiers.includes('stop')) {
|
|
3386
|
+
e.stopPropagation();
|
|
3387
|
+
if (!stoppedAt) stoppedAt = [];
|
|
3388
|
+
stoppedAt.push(matched);
|
|
3389
|
+
}
|
|
3232
3390
|
|
|
3233
3391
|
// Build the invocation function
|
|
3234
3392
|
const invoke = (evt) => {
|
|
@@ -3307,9 +3465,12 @@ class Component {
|
|
|
3307
3465
|
// textarea, select (single & multiple), contenteditable
|
|
3308
3466
|
// Nested state keys: z-model="user.name" → this.state.user.name
|
|
3309
3467
|
// Modifiers (boolean attributes on the same element):
|
|
3310
|
-
// z-lazy
|
|
3311
|
-
// z-trim
|
|
3312
|
-
// z-number
|
|
3468
|
+
// z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
|
|
3469
|
+
// z-trim — trim whitespace before writing to state
|
|
3470
|
+
// z-number — force Number() conversion regardless of input type
|
|
3471
|
+
// z-debounce — debounce state writes (default 250ms, or z-debounce="300")
|
|
3472
|
+
// z-uppercase — convert string to uppercase before writing to state
|
|
3473
|
+
// z-lowercase — convert string to lowercase before writing to state
|
|
3313
3474
|
//
|
|
3314
3475
|
// Writes to reactive state so the rest of the UI stays in sync.
|
|
3315
3476
|
// Focus and cursor position are preserved in _render() via focusInfo.
|
|
@@ -3325,6 +3486,10 @@ class Component {
|
|
|
3325
3486
|
const isLazy = el.hasAttribute('z-lazy');
|
|
3326
3487
|
const isTrim = el.hasAttribute('z-trim');
|
|
3327
3488
|
const isNum = el.hasAttribute('z-number');
|
|
3489
|
+
const isUpper = el.hasAttribute('z-uppercase');
|
|
3490
|
+
const isLower = el.hasAttribute('z-lowercase');
|
|
3491
|
+
const hasDebounce = el.hasAttribute('z-debounce');
|
|
3492
|
+
const debounceMs = hasDebounce ? (parseInt(el.getAttribute('z-debounce'), 10) || 250) : 0;
|
|
3328
3493
|
|
|
3329
3494
|
// Read current state value (supports dot-path keys)
|
|
3330
3495
|
const currentVal = _getPath(this.state, key);
|
|
@@ -3365,6 +3530,8 @@ class Component {
|
|
|
3365
3530
|
|
|
3366
3531
|
// Apply modifiers
|
|
3367
3532
|
if (isTrim && typeof val === 'string') val = val.trim();
|
|
3533
|
+
if (isUpper && typeof val === 'string') val = val.toUpperCase();
|
|
3534
|
+
if (isLower && typeof val === 'string') val = val.toLowerCase();
|
|
3368
3535
|
if (isNum || type === 'number' || type === 'range') val = Number(val);
|
|
3369
3536
|
|
|
3370
3537
|
// Write through the reactive proxy (triggers re-render).
|
|
@@ -3372,7 +3539,15 @@ class Component {
|
|
|
3372
3539
|
_setPath(this.state, key, val);
|
|
3373
3540
|
};
|
|
3374
3541
|
|
|
3375
|
-
|
|
3542
|
+
if (hasDebounce) {
|
|
3543
|
+
let timer = null;
|
|
3544
|
+
el.addEventListener(event, () => {
|
|
3545
|
+
clearTimeout(timer);
|
|
3546
|
+
timer = setTimeout(handler, debounceMs);
|
|
3547
|
+
});
|
|
3548
|
+
} else {
|
|
3549
|
+
el.addEventListener(event, handler);
|
|
3550
|
+
}
|
|
3376
3551
|
});
|
|
3377
3552
|
}
|
|
3378
3553
|
|
|
@@ -3658,6 +3833,10 @@ class Component {
|
|
|
3658
3833
|
}
|
|
3659
3834
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
3660
3835
|
this._listeners = [];
|
|
3836
|
+
if (this._outsideListeners) {
|
|
3837
|
+
this._outsideListeners.forEach(({ event, handler }) => document.removeEventListener(event, handler, true));
|
|
3838
|
+
this._outsideListeners = [];
|
|
3839
|
+
}
|
|
3661
3840
|
this._delegatedEvents = null;
|
|
3662
3841
|
this._eventBindings = null;
|
|
3663
3842
|
// Clear any pending debounce/throttle timers to prevent stale closures.
|
|
@@ -4529,7 +4708,12 @@ class Router {
|
|
|
4529
4708
|
if (typeof matched.component === 'string') {
|
|
4530
4709
|
const container = document.createElement(matched.component);
|
|
4531
4710
|
this._el.appendChild(container);
|
|
4532
|
-
|
|
4711
|
+
try {
|
|
4712
|
+
this._instance = mount(container, matched.component, props);
|
|
4713
|
+
} catch (err) {
|
|
4714
|
+
reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
|
|
4715
|
+
return;
|
|
4716
|
+
}
|
|
4533
4717
|
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', matched.component);
|
|
4534
4718
|
}
|
|
4535
4719
|
// If component is a render function
|
|
@@ -4862,8 +5046,9 @@ async function request(method, url, data, options = {}) {
|
|
|
4862
5046
|
} else {
|
|
4863
5047
|
fetchOpts.signal = controller.signal;
|
|
4864
5048
|
}
|
|
5049
|
+
let _timedOut = false;
|
|
4865
5050
|
if (timeout > 0) {
|
|
4866
|
-
timer = setTimeout(() => controller.abort(), timeout);
|
|
5051
|
+
timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
|
|
4867
5052
|
}
|
|
4868
5053
|
|
|
4869
5054
|
// Run request interceptors
|
|
@@ -4923,7 +5108,10 @@ async function request(method, url, data, options = {}) {
|
|
|
4923
5108
|
} catch (err) {
|
|
4924
5109
|
if (timer) clearTimeout(timer);
|
|
4925
5110
|
if (err.name === 'AbortError') {
|
|
4926
|
-
|
|
5111
|
+
if (_timedOut) {
|
|
5112
|
+
throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
|
|
5113
|
+
}
|
|
5114
|
+
throw new Error(`Request aborted: ${method} ${fullURL}`);
|
|
4927
5115
|
}
|
|
4928
5116
|
throw err;
|
|
4929
5117
|
}
|
|
@@ -5060,6 +5248,10 @@ function escapeHtml(str) {
|
|
|
5060
5248
|
return String(str).replace(/[&<>"']/g, c => map[c]);
|
|
5061
5249
|
}
|
|
5062
5250
|
|
|
5251
|
+
function stripHtml(str) {
|
|
5252
|
+
return String(str).replace(/<[^>]*>/g, '');
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5063
5255
|
/**
|
|
5064
5256
|
* Template tag for auto-escaping interpolated values
|
|
5065
5257
|
* Usage: $.html`<div>${userInput}</div>`
|
|
@@ -5105,7 +5297,10 @@ function camelCase(str) {
|
|
|
5105
5297
|
* CamelCase to kebab-case
|
|
5106
5298
|
*/
|
|
5107
5299
|
function kebabCase(str) {
|
|
5108
|
-
return str
|
|
5300
|
+
return str
|
|
5301
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
5302
|
+
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
|
|
5303
|
+
.toLowerCase();
|
|
5109
5304
|
}
|
|
5110
5305
|
|
|
5111
5306
|
|
|
@@ -5146,15 +5341,19 @@ function deepMerge(target, ...sources) {
|
|
|
5146
5341
|
/**
|
|
5147
5342
|
* Simple object equality check
|
|
5148
5343
|
*/
|
|
5149
|
-
function isEqual(a, b) {
|
|
5344
|
+
function isEqual(a, b, _seen) {
|
|
5150
5345
|
if (a === b) return true;
|
|
5151
5346
|
if (typeof a !== typeof b) return false;
|
|
5152
5347
|
if (typeof a !== 'object' || a === null || b === null) return false;
|
|
5153
5348
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
5349
|
+
// Guard against circular references
|
|
5350
|
+
if (!_seen) _seen = new Set();
|
|
5351
|
+
if (_seen.has(a)) return true;
|
|
5352
|
+
_seen.add(a);
|
|
5154
5353
|
const keysA = Object.keys(a);
|
|
5155
5354
|
const keysB = Object.keys(b);
|
|
5156
5355
|
if (keysA.length !== keysB.length) return false;
|
|
5157
|
-
return keysA.every(k => isEqual(a[k], b[k]));
|
|
5356
|
+
return keysA.every(k => isEqual(a[k], b[k], _seen));
|
|
5158
5357
|
}
|
|
5159
5358
|
|
|
5160
5359
|
|
|
@@ -5255,7 +5454,182 @@ class EventBus {
|
|
|
5255
5454
|
clear() { this._handlers.clear(); }
|
|
5256
5455
|
}
|
|
5257
5456
|
|
|
5258
|
-
const bus = new EventBus();
|
|
5457
|
+
const bus = new EventBus();
|
|
5458
|
+
|
|
5459
|
+
|
|
5460
|
+
// ---------------------------------------------------------------------------
|
|
5461
|
+
// Array utilities
|
|
5462
|
+
// ---------------------------------------------------------------------------
|
|
5463
|
+
|
|
5464
|
+
function range(startOrEnd, end, step) {
|
|
5465
|
+
let s, e, st;
|
|
5466
|
+
if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
|
|
5467
|
+
else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
|
|
5468
|
+
if (st === 0) return [];
|
|
5469
|
+
const result = [];
|
|
5470
|
+
if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
|
|
5471
|
+
else { for (let i = s; i > e; i += st) result.push(i); }
|
|
5472
|
+
return result;
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
function unique(arr, keyFn) {
|
|
5476
|
+
if (!keyFn) return [...new Set(arr)];
|
|
5477
|
+
const seen = new Set();
|
|
5478
|
+
return arr.filter(item => {
|
|
5479
|
+
const k = keyFn(item);
|
|
5480
|
+
if (seen.has(k)) return false;
|
|
5481
|
+
seen.add(k);
|
|
5482
|
+
return true;
|
|
5483
|
+
});
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
function chunk(arr, size) {
|
|
5487
|
+
const result = [];
|
|
5488
|
+
for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
|
|
5489
|
+
return result;
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
function groupBy(arr, keyFn) {
|
|
5493
|
+
const result = {};
|
|
5494
|
+
for (const item of arr) {
|
|
5495
|
+
const k = keyFn(item);
|
|
5496
|
+
(result[k] ??= []).push(item);
|
|
5497
|
+
}
|
|
5498
|
+
return result;
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5501
|
+
|
|
5502
|
+
// ---------------------------------------------------------------------------
|
|
5503
|
+
// Object utilities
|
|
5504
|
+
// ---------------------------------------------------------------------------
|
|
5505
|
+
|
|
5506
|
+
function pick(obj, keys) {
|
|
5507
|
+
const result = {};
|
|
5508
|
+
for (const k of keys) { if (k in obj) result[k] = obj[k]; }
|
|
5509
|
+
return result;
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5512
|
+
function omit(obj, keys) {
|
|
5513
|
+
const exclude = new Set(keys);
|
|
5514
|
+
const result = {};
|
|
5515
|
+
for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
|
|
5516
|
+
return result;
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5519
|
+
function getPath(obj, path, fallback) {
|
|
5520
|
+
const keys = path.split('.');
|
|
5521
|
+
let cur = obj;
|
|
5522
|
+
for (const k of keys) {
|
|
5523
|
+
if (cur == null || typeof cur !== 'object') return fallback;
|
|
5524
|
+
cur = cur[k];
|
|
5525
|
+
}
|
|
5526
|
+
return cur === undefined ? fallback : cur;
|
|
5527
|
+
}
|
|
5528
|
+
|
|
5529
|
+
function setPath(obj, path, value) {
|
|
5530
|
+
const keys = path.split('.');
|
|
5531
|
+
let cur = obj;
|
|
5532
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
5533
|
+
const k = keys[i];
|
|
5534
|
+
if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
|
|
5535
|
+
cur = cur[k];
|
|
5536
|
+
}
|
|
5537
|
+
cur[keys[keys.length - 1]] = value;
|
|
5538
|
+
return obj;
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5541
|
+
function isEmpty(val) {
|
|
5542
|
+
if (val == null) return true;
|
|
5543
|
+
if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
|
|
5544
|
+
if (val instanceof Map || val instanceof Set) return val.size === 0;
|
|
5545
|
+
if (typeof val === 'object') return Object.keys(val).length === 0;
|
|
5546
|
+
return false;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
|
|
5550
|
+
// ---------------------------------------------------------------------------
|
|
5551
|
+
// String utilities
|
|
5552
|
+
// ---------------------------------------------------------------------------
|
|
5553
|
+
|
|
5554
|
+
function capitalize(str) {
|
|
5555
|
+
if (!str) return '';
|
|
5556
|
+
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
function truncate(str, maxLen, suffix = '…') {
|
|
5560
|
+
if (str.length <= maxLen) return str;
|
|
5561
|
+
const end = Math.max(0, maxLen - suffix.length);
|
|
5562
|
+
return str.slice(0, end) + suffix;
|
|
5563
|
+
}
|
|
5564
|
+
|
|
5565
|
+
|
|
5566
|
+
// ---------------------------------------------------------------------------
|
|
5567
|
+
// Number utilities
|
|
5568
|
+
// ---------------------------------------------------------------------------
|
|
5569
|
+
|
|
5570
|
+
function clamp(val, min, max) {
|
|
5571
|
+
return val < min ? min : val > max ? max : val;
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5574
|
+
|
|
5575
|
+
// ---------------------------------------------------------------------------
|
|
5576
|
+
// Function utilities
|
|
5577
|
+
// ---------------------------------------------------------------------------
|
|
5578
|
+
|
|
5579
|
+
function memoize(fn, keyFnOrOpts) {
|
|
5580
|
+
let keyFn, maxSize = 0;
|
|
5581
|
+
if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
|
|
5582
|
+
else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
|
|
5583
|
+
|
|
5584
|
+
const cache = new Map();
|
|
5585
|
+
|
|
5586
|
+
const memoized = (...args) => {
|
|
5587
|
+
const key = keyFn ? keyFn(...args) : args[0];
|
|
5588
|
+
if (cache.has(key)) return cache.get(key);
|
|
5589
|
+
const result = fn(...args);
|
|
5590
|
+
cache.set(key, result);
|
|
5591
|
+
if (maxSize > 0 && cache.size > maxSize) {
|
|
5592
|
+
cache.delete(cache.keys().next().value);
|
|
5593
|
+
}
|
|
5594
|
+
return result;
|
|
5595
|
+
};
|
|
5596
|
+
|
|
5597
|
+
memoized.clear = () => cache.clear();
|
|
5598
|
+
return memoized;
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
|
|
5602
|
+
// ---------------------------------------------------------------------------
|
|
5603
|
+
// Async utilities
|
|
5604
|
+
// ---------------------------------------------------------------------------
|
|
5605
|
+
|
|
5606
|
+
function retry(fn, opts = {}) {
|
|
5607
|
+
const { attempts = 3, delay = 1000, backoff = 1 } = opts;
|
|
5608
|
+
return new Promise((resolve, reject) => {
|
|
5609
|
+
let attempt = 0, currentDelay = delay;
|
|
5610
|
+
const tryOnce = () => {
|
|
5611
|
+
attempt++;
|
|
5612
|
+
fn(attempt).then(resolve, (err) => {
|
|
5613
|
+
if (attempt >= attempts) return reject(err);
|
|
5614
|
+
const d = currentDelay;
|
|
5615
|
+
currentDelay *= backoff;
|
|
5616
|
+
setTimeout(tryOnce, d);
|
|
5617
|
+
});
|
|
5618
|
+
};
|
|
5619
|
+
tryOnce();
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
function timeout(promise, ms, message) {
|
|
5624
|
+
let timer;
|
|
5625
|
+
const race = Promise.race([
|
|
5626
|
+
promise,
|
|
5627
|
+
new Promise((_, reject) => {
|
|
5628
|
+
timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
|
|
5629
|
+
})
|
|
5630
|
+
]);
|
|
5631
|
+
return race.finally(() => clearTimeout(timer));
|
|
5632
|
+
}
|
|
5259
5633
|
|
|
5260
5634
|
// --- index.js (assembly) ------------------------------------------
|
|
5261
5635
|
/**
|
|
@@ -5314,6 +5688,8 @@ Object.defineProperty($, 'name', {
|
|
|
5314
5688
|
value: query.name, writable: true, configurable: true
|
|
5315
5689
|
});
|
|
5316
5690
|
$.children = query.children;
|
|
5691
|
+
$.qs = query.qs;
|
|
5692
|
+
$.qsa = query.qsa;
|
|
5317
5693
|
|
|
5318
5694
|
// --- Collection selector ---------------------------------------------------
|
|
5319
5695
|
/**
|
|
@@ -5382,8 +5758,10 @@ $.pipe = pipe;
|
|
|
5382
5758
|
$.once = once;
|
|
5383
5759
|
$.sleep = sleep;
|
|
5384
5760
|
$.escapeHtml = escapeHtml;
|
|
5761
|
+
$.stripHtml = stripHtml;
|
|
5385
5762
|
$.html = html;
|
|
5386
5763
|
$.trust = trust;
|
|
5764
|
+
$.TrustedHTML = TrustedHTML;
|
|
5387
5765
|
$.uuid = uuid;
|
|
5388
5766
|
$.camelCase = camelCase;
|
|
5389
5767
|
$.kebabCase = kebabCase;
|
|
@@ -5394,7 +5772,23 @@ $.param = param;
|
|
|
5394
5772
|
$.parseQuery = parseQuery;
|
|
5395
5773
|
$.storage = storage;
|
|
5396
5774
|
$.session = session;
|
|
5775
|
+
$.EventBus = EventBus;
|
|
5397
5776
|
$.bus = bus;
|
|
5777
|
+
$.range = range;
|
|
5778
|
+
$.unique = unique;
|
|
5779
|
+
$.chunk = chunk;
|
|
5780
|
+
$.groupBy = groupBy;
|
|
5781
|
+
$.pick = pick;
|
|
5782
|
+
$.omit = omit;
|
|
5783
|
+
$.getPath = getPath;
|
|
5784
|
+
$.setPath = setPath;
|
|
5785
|
+
$.isEmpty = isEmpty;
|
|
5786
|
+
$.capitalize = capitalize;
|
|
5787
|
+
$.truncate = truncate;
|
|
5788
|
+
$.clamp = clamp;
|
|
5789
|
+
$.memoize = memoize;
|
|
5790
|
+
$.retry = retry;
|
|
5791
|
+
$.timeout = timeout;
|
|
5398
5792
|
|
|
5399
5793
|
// --- Error handling --------------------------------------------------------
|
|
5400
5794
|
$.onError = onError;
|
|
@@ -5404,8 +5798,8 @@ $.guardCallback = guardCallback;
|
|
|
5404
5798
|
$.validate = validate;
|
|
5405
5799
|
|
|
5406
5800
|
// --- Meta ------------------------------------------------------------------
|
|
5407
|
-
$.version = '0.9.
|
|
5408
|
-
$.libSize = '~
|
|
5801
|
+
$.version = '0.9.6';
|
|
5802
|
+
$.libSize = '~100 KB';
|
|
5409
5803
|
$.meta = {}; // populated at build time by CLI bundler
|
|
5410
5804
|
|
|
5411
5805
|
$.noConflict = () => {
|