zero-query 0.9.0 → 0.9.5
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 -9
- package/cli/commands/bundle.js +15 -2
- 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 +544 -71
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +51 -3
- package/index.js +32 -4
- package/package.json +1 -1
- package/src/component.js +60 -10
- package/src/core.js +65 -13
- package/src/diff.js +11 -5
- package/src/expression.js +104 -16
- package/src/http.js +23 -3
- package/src/reactive.js +8 -2
- package/src/router.js +43 -9
- package/src/ssr.js +1 -1
- package/src/store.js +5 -0
- package/src/utils.js +203 -11
- package/tests/audit.test.js +4030 -0
- package/tests/cli.test.js +456 -0
- package/tests/component.test.js +1387 -0
- package/tests/core.test.js +934 -1
- package/tests/diff.test.js +891 -0
- package/tests/errors.test.js +179 -0
- package/tests/expression.test.js +569 -0
- package/tests/http.test.js +160 -1
- package/tests/reactive.test.js +320 -0
- package/tests/router.test.js +1187 -0
- package/tests/ssr.test.js +261 -0
- package/tests/store.test.js +210 -0
- package/tests/utils.test.js +729 -1
- package/types/store.d.ts +3 -0
- 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.5
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -300,7 +300,13 @@ function signal(initial) {
|
|
|
300
300
|
*/
|
|
301
301
|
function computed(fn) {
|
|
302
302
|
const s = new Signal(undefined);
|
|
303
|
-
effect(() => {
|
|
303
|
+
effect(() => {
|
|
304
|
+
const v = fn();
|
|
305
|
+
if (v !== s._value) {
|
|
306
|
+
s._value = v;
|
|
307
|
+
s._notify();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
304
310
|
return s;
|
|
305
311
|
}
|
|
306
312
|
|
|
@@ -343,7 +349,7 @@ function effect(fn) {
|
|
|
343
349
|
}
|
|
344
350
|
execute._deps.clear();
|
|
345
351
|
}
|
|
346
|
-
|
|
352
|
+
// Don't clobber _activeEffect — another effect may be running
|
|
347
353
|
};
|
|
348
354
|
}
|
|
349
355
|
|
|
@@ -361,7 +367,7 @@ function effect(fn) {
|
|
|
361
367
|
// ---------------------------------------------------------------------------
|
|
362
368
|
class ZQueryCollection {
|
|
363
369
|
constructor(elements) {
|
|
364
|
-
this.elements = Array.isArray(elements) ? elements : [elements];
|
|
370
|
+
this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
|
|
365
371
|
this.length = this.elements.length;
|
|
366
372
|
this.elements.forEach((el, i) => { this[i] = el; });
|
|
367
373
|
}
|
|
@@ -418,10 +424,12 @@ class ZQueryCollection {
|
|
|
418
424
|
return new ZQueryCollection([...kids]);
|
|
419
425
|
}
|
|
420
426
|
|
|
421
|
-
siblings() {
|
|
427
|
+
siblings(selector) {
|
|
422
428
|
const sibs = [];
|
|
423
429
|
this.elements.forEach(el => {
|
|
424
|
-
|
|
430
|
+
if (!el.parentElement) return;
|
|
431
|
+
const all = [...el.parentElement.children].filter(c => c !== el);
|
|
432
|
+
sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
|
|
425
433
|
});
|
|
426
434
|
return new ZQueryCollection(sibs);
|
|
427
435
|
}
|
|
@@ -563,7 +571,8 @@ class ZQueryCollection {
|
|
|
563
571
|
index(selector) {
|
|
564
572
|
if (selector === undefined) {
|
|
565
573
|
const el = this.first();
|
|
566
|
-
|
|
574
|
+
if (!el || !el.parentElement) return -1;
|
|
575
|
+
return Array.from(el.parentElement.children).indexOf(el);
|
|
567
576
|
}
|
|
568
577
|
const target = (typeof selector === 'string')
|
|
569
578
|
? document.querySelector(selector)
|
|
@@ -623,6 +632,11 @@ class ZQueryCollection {
|
|
|
623
632
|
// --- Attributes ----------------------------------------------------------
|
|
624
633
|
|
|
625
634
|
attr(name, value) {
|
|
635
|
+
if (typeof name === 'object' && name !== null) {
|
|
636
|
+
return this.each((_, el) => {
|
|
637
|
+
for (const [k, v] of Object.entries(name)) el.setAttribute(k, v);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
626
640
|
if (value === undefined) return this.first()?.getAttribute(name);
|
|
627
641
|
return this.each((_, el) => el.setAttribute(name, value));
|
|
628
642
|
}
|
|
@@ -647,7 +661,10 @@ class ZQueryCollection {
|
|
|
647
661
|
|
|
648
662
|
// --- CSS / Dimensions ----------------------------------------------------
|
|
649
663
|
|
|
650
|
-
css(props) {
|
|
664
|
+
css(props, value) {
|
|
665
|
+
if (typeof props === 'string' && value !== undefined) {
|
|
666
|
+
return this.each((_, el) => { el.style[props] = value; });
|
|
667
|
+
}
|
|
651
668
|
if (typeof props === 'string') {
|
|
652
669
|
const el = this.first();
|
|
653
670
|
return el ? getComputedStyle(el)[props] : undefined;
|
|
@@ -787,6 +804,7 @@ class ZQueryCollection {
|
|
|
787
804
|
wrap(wrapper) {
|
|
788
805
|
return this.each((_, el) => {
|
|
789
806
|
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
807
|
+
if (!w || !el.parentNode) return;
|
|
790
808
|
el.parentNode.insertBefore(w, el);
|
|
791
809
|
w.appendChild(el);
|
|
792
810
|
});
|
|
@@ -912,12 +930,18 @@ class ZQueryCollection {
|
|
|
912
930
|
if (typeof selectorOrHandler === 'function') {
|
|
913
931
|
el.addEventListener(evt, selectorOrHandler);
|
|
914
932
|
} else if (typeof selectorOrHandler === 'string') {
|
|
915
|
-
// Delegated event —
|
|
916
|
-
|
|
933
|
+
// Delegated event — store wrapper so off() can remove it
|
|
934
|
+
const wrapper = (e) => {
|
|
917
935
|
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
918
936
|
const target = e.target.closest(selectorOrHandler);
|
|
919
937
|
if (target && el.contains(target)) handler.call(target, e);
|
|
920
|
-
}
|
|
938
|
+
};
|
|
939
|
+
wrapper._zqOriginal = handler;
|
|
940
|
+
wrapper._zqSelector = selectorOrHandler;
|
|
941
|
+
el.addEventListener(evt, wrapper);
|
|
942
|
+
// Track delegated handlers for removal
|
|
943
|
+
if (!el._zqDelegated) el._zqDelegated = [];
|
|
944
|
+
el._zqDelegated.push({ evt, wrapper });
|
|
921
945
|
}
|
|
922
946
|
});
|
|
923
947
|
});
|
|
@@ -926,7 +950,20 @@ class ZQueryCollection {
|
|
|
926
950
|
off(event, handler) {
|
|
927
951
|
const events = event.split(/\s+/);
|
|
928
952
|
return this.each((_, el) => {
|
|
929
|
-
events.forEach(evt =>
|
|
953
|
+
events.forEach(evt => {
|
|
954
|
+
// Try direct removal first
|
|
955
|
+
el.removeEventListener(evt, handler);
|
|
956
|
+
// Also check delegated handlers
|
|
957
|
+
if (el._zqDelegated) {
|
|
958
|
+
el._zqDelegated = el._zqDelegated.filter(d => {
|
|
959
|
+
if (d.evt === evt && d.wrapper._zqOriginal === handler) {
|
|
960
|
+
el.removeEventListener(evt, d.wrapper);
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
return true;
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
});
|
|
930
967
|
});
|
|
931
968
|
}
|
|
932
969
|
|
|
@@ -955,8 +992,12 @@ class ZQueryCollection {
|
|
|
955
992
|
// --- Animation -----------------------------------------------------------
|
|
956
993
|
|
|
957
994
|
animate(props, duration = 300, easing = 'ease') {
|
|
995
|
+
// Empty collection — resolve immediately
|
|
996
|
+
if (this.length === 0) return Promise.resolve(this);
|
|
958
997
|
return new Promise(resolve => {
|
|
998
|
+
let resolved = false;
|
|
959
999
|
const count = { done: 0 };
|
|
1000
|
+
const listeners = [];
|
|
960
1001
|
this.each((_, el) => {
|
|
961
1002
|
el.style.transition = `all ${duration}ms ${easing}`;
|
|
962
1003
|
requestAnimationFrame(() => {
|
|
@@ -964,13 +1005,27 @@ class ZQueryCollection {
|
|
|
964
1005
|
const onEnd = () => {
|
|
965
1006
|
el.removeEventListener('transitionend', onEnd);
|
|
966
1007
|
el.style.transition = '';
|
|
967
|
-
if (++count.done >= this.length)
|
|
1008
|
+
if (!resolved && ++count.done >= this.length) {
|
|
1009
|
+
resolved = true;
|
|
1010
|
+
resolve(this);
|
|
1011
|
+
}
|
|
968
1012
|
};
|
|
969
1013
|
el.addEventListener('transitionend', onEnd);
|
|
1014
|
+
listeners.push({ el, onEnd });
|
|
970
1015
|
});
|
|
971
1016
|
});
|
|
972
1017
|
// Fallback in case transitionend doesn't fire
|
|
973
|
-
setTimeout(() =>
|
|
1018
|
+
setTimeout(() => {
|
|
1019
|
+
if (!resolved) {
|
|
1020
|
+
resolved = true;
|
|
1021
|
+
// Clean up any remaining transitionend listeners
|
|
1022
|
+
for (const { el, onEnd } of listeners) {
|
|
1023
|
+
el.removeEventListener('transitionend', onEnd);
|
|
1024
|
+
el.style.transition = '';
|
|
1025
|
+
}
|
|
1026
|
+
resolve(this);
|
|
1027
|
+
}
|
|
1028
|
+
}, duration + 50);
|
|
974
1029
|
});
|
|
975
1030
|
}
|
|
976
1031
|
|
|
@@ -984,7 +1039,8 @@ class ZQueryCollection {
|
|
|
984
1039
|
|
|
985
1040
|
fadeToggle(duration = 300) {
|
|
986
1041
|
return Promise.all(this.elements.map(el => {
|
|
987
|
-
const
|
|
1042
|
+
const cs = getComputedStyle(el);
|
|
1043
|
+
const visible = cs.opacity !== '0' && cs.display !== 'none';
|
|
988
1044
|
const col = new ZQueryCollection([el]);
|
|
989
1045
|
return visible ? col.fadeOut(duration) : col.fadeIn(duration);
|
|
990
1046
|
})).then(() => this);
|
|
@@ -1162,6 +1218,8 @@ query.children = (parentId) => {
|
|
|
1162
1218
|
const p = document.getElementById(parentId);
|
|
1163
1219
|
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1164
1220
|
};
|
|
1221
|
+
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
1222
|
+
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
1165
1223
|
|
|
1166
1224
|
// Create element shorthand — returns ZQueryCollection for chaining
|
|
1167
1225
|
query.create = (tag, attrs = {}, ...children) => {
|
|
@@ -1169,7 +1227,7 @@ query.create = (tag, attrs = {}, ...children) => {
|
|
|
1169
1227
|
for (const [k, v] of Object.entries(attrs)) {
|
|
1170
1228
|
if (k === 'class') el.className = v;
|
|
1171
1229
|
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1172
|
-
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);
|
|
1173
1231
|
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1174
1232
|
else el.setAttribute(k, v);
|
|
1175
1233
|
}
|
|
@@ -1389,6 +1447,11 @@ function tokenize(expr) {
|
|
|
1389
1447
|
tokens.push({ t: T.OP, v: ch });
|
|
1390
1448
|
i++; continue;
|
|
1391
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
|
+
}
|
|
1392
1455
|
if ('()[]{},.?:'.includes(ch)) {
|
|
1393
1456
|
tokens.push({ t: T.PUNC, v: ch });
|
|
1394
1457
|
i++; continue;
|
|
@@ -1452,7 +1515,7 @@ class Parser {
|
|
|
1452
1515
|
this.next(); // consume ?
|
|
1453
1516
|
const truthy = this.parseExpression(0);
|
|
1454
1517
|
this.expect(T.PUNC, ':');
|
|
1455
|
-
const falsy = this.parseExpression(
|
|
1518
|
+
const falsy = this.parseExpression(0);
|
|
1456
1519
|
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
1457
1520
|
continue;
|
|
1458
1521
|
}
|
|
@@ -1593,7 +1656,12 @@ class Parser {
|
|
|
1593
1656
|
this.expect(T.PUNC, '(');
|
|
1594
1657
|
const args = [];
|
|
1595
1658
|
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
1596
|
-
|
|
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
|
+
}
|
|
1597
1665
|
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1598
1666
|
}
|
|
1599
1667
|
this.expect(T.PUNC, ')');
|
|
@@ -1669,7 +1737,12 @@ class Parser {
|
|
|
1669
1737
|
this.next();
|
|
1670
1738
|
const elements = [];
|
|
1671
1739
|
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
1672
|
-
|
|
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
|
+
}
|
|
1673
1746
|
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1674
1747
|
}
|
|
1675
1748
|
this.expect(T.PUNC, ']');
|
|
@@ -1681,6 +1754,14 @@ class Parser {
|
|
|
1681
1754
|
this.next();
|
|
1682
1755
|
const properties = [];
|
|
1683
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
|
+
|
|
1684
1765
|
const keyTok = this.next();
|
|
1685
1766
|
let key;
|
|
1686
1767
|
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
@@ -1712,7 +1793,13 @@ class Parser {
|
|
|
1712
1793
|
|
|
1713
1794
|
// new keyword
|
|
1714
1795
|
if (tok.v === 'new') {
|
|
1715
|
-
|
|
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
|
+
}
|
|
1716
1803
|
let args = [];
|
|
1717
1804
|
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1718
1805
|
args = this._parseArgs();
|
|
@@ -1780,6 +1867,7 @@ function _isSafeAccess(obj, prop) {
|
|
|
1780
1867
|
const BLOCKED = new Set([
|
|
1781
1868
|
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
1782
1869
|
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
1870
|
+
'call', 'apply', 'bind',
|
|
1783
1871
|
]);
|
|
1784
1872
|
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
1785
1873
|
|
|
@@ -1823,6 +1911,12 @@ function evaluate(node, scope) {
|
|
|
1823
1911
|
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
1824
1912
|
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
1825
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;
|
|
1826
1920
|
return undefined;
|
|
1827
1921
|
}
|
|
1828
1922
|
|
|
@@ -1861,10 +1955,21 @@ function evaluate(node, scope) {
|
|
|
1861
1955
|
}
|
|
1862
1956
|
|
|
1863
1957
|
case 'optional_call': {
|
|
1864
|
-
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);
|
|
1865
1971
|
if (callee == null) return undefined;
|
|
1866
1972
|
if (typeof callee !== 'function') return undefined;
|
|
1867
|
-
const args = node.args.map(a => evaluate(a, scope));
|
|
1868
1973
|
return callee(...args);
|
|
1869
1974
|
}
|
|
1870
1975
|
|
|
@@ -1874,7 +1979,7 @@ function evaluate(node, scope) {
|
|
|
1874
1979
|
// Only allow safe constructors
|
|
1875
1980
|
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
1876
1981
|
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
1877
|
-
const args = node.args
|
|
1982
|
+
const args = _evalArgs(node.args, scope);
|
|
1878
1983
|
return new Ctor(...args);
|
|
1879
1984
|
}
|
|
1880
1985
|
return undefined;
|
|
@@ -1904,13 +2009,32 @@ function evaluate(node, scope) {
|
|
|
1904
2009
|
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
1905
2010
|
}
|
|
1906
2011
|
|
|
1907
|
-
case 'array':
|
|
1908
|
-
|
|
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
|
+
}
|
|
1909
2026
|
|
|
1910
2027
|
case 'object': {
|
|
1911
2028
|
const obj = {};
|
|
1912
|
-
for (const
|
|
1913
|
-
|
|
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
|
+
}
|
|
1914
2038
|
}
|
|
1915
2039
|
return obj;
|
|
1916
2040
|
}
|
|
@@ -1931,12 +2055,30 @@ function evaluate(node, scope) {
|
|
|
1931
2055
|
}
|
|
1932
2056
|
}
|
|
1933
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
|
+
|
|
1934
2076
|
/**
|
|
1935
2077
|
* Resolve and execute a function call safely.
|
|
1936
2078
|
*/
|
|
1937
2079
|
function _resolveCall(node, scope) {
|
|
1938
2080
|
const callee = node.callee;
|
|
1939
|
-
const args = node.args
|
|
2081
|
+
const args = _evalArgs(node.args, scope);
|
|
1940
2082
|
|
|
1941
2083
|
// Method call: obj.method() — bind `this` to obj
|
|
1942
2084
|
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
@@ -2010,8 +2152,9 @@ function _evalBinary(node, scope) {
|
|
|
2010
2152
|
* @returns {*} — evaluation result, or undefined on error
|
|
2011
2153
|
*/
|
|
2012
2154
|
|
|
2013
|
-
// AST cache — avoids re-tokenizing and re-parsing the same expression
|
|
2014
|
-
//
|
|
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.
|
|
2015
2158
|
const _astCache = new Map();
|
|
2016
2159
|
const _AST_CACHE_MAX = 512;
|
|
2017
2160
|
|
|
@@ -2031,9 +2174,12 @@ function safeEval(expr, scope) {
|
|
|
2031
2174
|
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2032
2175
|
}
|
|
2033
2176
|
|
|
2034
|
-
// Check AST cache
|
|
2177
|
+
// Check AST cache (LRU: move to end on hit)
|
|
2035
2178
|
let ast = _astCache.get(trimmed);
|
|
2036
|
-
if (
|
|
2179
|
+
if (ast) {
|
|
2180
|
+
_astCache.delete(trimmed);
|
|
2181
|
+
_astCache.set(trimmed, ast);
|
|
2182
|
+
} else {
|
|
2037
2183
|
const tokens = tokenize(trimmed);
|
|
2038
2184
|
const parser = new Parser(tokens, scope);
|
|
2039
2185
|
ast = parser.parse();
|
|
@@ -2297,8 +2443,11 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
2297
2443
|
if (!lisSet.has(i)) {
|
|
2298
2444
|
oldParent.insertBefore(oldNode, cursor);
|
|
2299
2445
|
}
|
|
2446
|
+
// Capture next sibling BEFORE _morphNode — if _morphNode calls
|
|
2447
|
+
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
2448
|
+
const nextSib = oldNode.nextSibling;
|
|
2300
2449
|
_morphNode(oldParent, oldNode, newNode);
|
|
2301
|
-
cursor =
|
|
2450
|
+
cursor = nextSib;
|
|
2302
2451
|
} else {
|
|
2303
2452
|
// Insert new node
|
|
2304
2453
|
const clone = newNode.cloneNode(true);
|
|
@@ -2476,10 +2625,13 @@ function _morphAttributes(oldEl, newEl) {
|
|
|
2476
2625
|
}
|
|
2477
2626
|
}
|
|
2478
2627
|
|
|
2479
|
-
// Remove stale attributes
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2628
|
+
// Remove stale attributes — snapshot names first because oldAttrs
|
|
2629
|
+
// is a live NamedNodeMap that mutates on removeAttribute().
|
|
2630
|
+
const oldNames = new Array(oldLen);
|
|
2631
|
+
for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
|
|
2632
|
+
for (let i = oldNames.length - 1; i >= 0; i--) {
|
|
2633
|
+
if (!newNames.has(oldNames[i])) {
|
|
2634
|
+
oldEl.removeAttribute(oldNames[i]);
|
|
2483
2635
|
}
|
|
2484
2636
|
}
|
|
2485
2637
|
}
|
|
@@ -2801,7 +2953,7 @@ class Component {
|
|
|
2801
2953
|
if (!watchers) return;
|
|
2802
2954
|
for (const [key, handler] of Object.entries(watchers)) {
|
|
2803
2955
|
// Match exact key or parent key (e.g. watcher on 'user' fires when 'user.name' changes)
|
|
2804
|
-
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.')
|
|
2956
|
+
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.')) {
|
|
2805
2957
|
const currentVal = _getPath(this.state.__raw || this.state, key);
|
|
2806
2958
|
const prevVal = this._prevWatchValues?.[key];
|
|
2807
2959
|
if (currentVal !== prevVal) {
|
|
@@ -2950,20 +3102,26 @@ class Component {
|
|
|
2950
3102
|
if (!this._mounted && combinedStyles) {
|
|
2951
3103
|
const scopeAttr = `z-s${this._uid}`;
|
|
2952
3104
|
this._el.setAttribute(scopeAttr, '');
|
|
2953
|
-
let
|
|
3105
|
+
let noScopeDepth = 0; // brace depth at which a no-scope @-rule started (0 = none active)
|
|
3106
|
+
let braceDepth = 0; // overall brace depth
|
|
2954
3107
|
const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
|
|
2955
3108
|
if (match === '}') {
|
|
2956
|
-
if (
|
|
3109
|
+
if (noScopeDepth > 0 && braceDepth <= noScopeDepth) noScopeDepth = 0;
|
|
3110
|
+
braceDepth--;
|
|
2957
3111
|
return match;
|
|
2958
3112
|
}
|
|
3113
|
+
braceDepth++;
|
|
2959
3114
|
const trimmed = selector.trim();
|
|
2960
|
-
// Don't scope @-rules
|
|
3115
|
+
// Don't scope @-rules themselves
|
|
2961
3116
|
if (trimmed.startsWith('@')) {
|
|
2962
|
-
|
|
3117
|
+
// @keyframes and @font-face contain non-selector content — skip scoping inside them
|
|
3118
|
+
if (/^@(keyframes|font-face)\b/.test(trimmed)) {
|
|
3119
|
+
noScopeDepth = braceDepth;
|
|
3120
|
+
}
|
|
2963
3121
|
return match;
|
|
2964
3122
|
}
|
|
2965
|
-
//
|
|
2966
|
-
if (
|
|
3123
|
+
// Inside @keyframes or @font-face — don't scope inner rules
|
|
3124
|
+
if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
|
|
2967
3125
|
return match;
|
|
2968
3126
|
}
|
|
2969
3127
|
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
@@ -3151,15 +3309,44 @@ class Component {
|
|
|
3151
3309
|
const handler = (e) => {
|
|
3152
3310
|
// Read bindings from live map — always up to date after re-renders
|
|
3153
3311
|
const currentBindings = this._eventBindings?.get(event) || [];
|
|
3154
|
-
|
|
3155
|
-
|
|
3312
|
+
|
|
3313
|
+
// Collect matching bindings with their matched elements, then sort
|
|
3314
|
+
// deepest-first so .stop correctly prevents ancestor handlers
|
|
3315
|
+
// (mimics real DOM bubbling order within delegated events).
|
|
3316
|
+
const hits = [];
|
|
3317
|
+
for (const binding of currentBindings) {
|
|
3318
|
+
const matched = e.target.closest(binding.selector);
|
|
3319
|
+
if (!matched) continue;
|
|
3320
|
+
hits.push({ ...binding, matched });
|
|
3321
|
+
}
|
|
3322
|
+
hits.sort((a, b) => {
|
|
3323
|
+
if (a.matched === b.matched) return 0;
|
|
3324
|
+
return a.matched.contains(b.matched) ? 1 : -1;
|
|
3325
|
+
});
|
|
3326
|
+
|
|
3327
|
+
let stoppedAt = null; // Track elements that called .stop
|
|
3328
|
+
for (const { selector, methodExpr, modifiers, el, matched } of hits) {
|
|
3329
|
+
|
|
3330
|
+
// In delegated events, .stop should prevent ancestor bindings from
|
|
3331
|
+
// firing — stopPropagation alone only stops real DOM bubbling.
|
|
3332
|
+
if (stoppedAt) {
|
|
3333
|
+
let blocked = false;
|
|
3334
|
+
for (const stopped of stoppedAt) {
|
|
3335
|
+
if (matched.contains(stopped) && matched !== stopped) { blocked = true; break; }
|
|
3336
|
+
}
|
|
3337
|
+
if (blocked) continue;
|
|
3338
|
+
}
|
|
3156
3339
|
|
|
3157
3340
|
// .self — only fire if target is the element itself
|
|
3158
3341
|
if (modifiers.includes('self') && e.target !== el) continue;
|
|
3159
3342
|
|
|
3160
3343
|
// Handle modifiers
|
|
3161
3344
|
if (modifiers.includes('prevent')) e.preventDefault();
|
|
3162
|
-
if (modifiers.includes('stop'))
|
|
3345
|
+
if (modifiers.includes('stop')) {
|
|
3346
|
+
e.stopPropagation();
|
|
3347
|
+
if (!stoppedAt) stoppedAt = [];
|
|
3348
|
+
stoppedAt.push(matched);
|
|
3349
|
+
}
|
|
3163
3350
|
|
|
3164
3351
|
// Build the invocation function
|
|
3165
3352
|
const invoke = (evt) => {
|
|
@@ -3591,6 +3778,21 @@ class Component {
|
|
|
3591
3778
|
this._listeners = [];
|
|
3592
3779
|
this._delegatedEvents = null;
|
|
3593
3780
|
this._eventBindings = null;
|
|
3781
|
+
// Clear any pending debounce/throttle timers to prevent stale closures.
|
|
3782
|
+
// Timers are keyed by individual child elements, so iterate all descendants.
|
|
3783
|
+
const allEls = this._el.querySelectorAll('*');
|
|
3784
|
+
allEls.forEach(child => {
|
|
3785
|
+
const dTimers = _debounceTimers.get(child);
|
|
3786
|
+
if (dTimers) {
|
|
3787
|
+
for (const key in dTimers) clearTimeout(dTimers[key]);
|
|
3788
|
+
_debounceTimers.delete(child);
|
|
3789
|
+
}
|
|
3790
|
+
const tTimers = _throttleTimers.get(child);
|
|
3791
|
+
if (tTimers) {
|
|
3792
|
+
for (const key in tTimers) clearTimeout(tTimers[key]);
|
|
3793
|
+
_throttleTimers.delete(child);
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3594
3796
|
if (this._styleEl) this._styleEl.remove();
|
|
3595
3797
|
_instances.delete(this._el);
|
|
3596
3798
|
this._el.innerHTML = '';
|
|
@@ -3886,6 +4088,23 @@ function style(urls, opts = {}) {
|
|
|
3886
4088
|
// Unique marker on history.state to identify zQuery-managed entries
|
|
3887
4089
|
const _ZQ_STATE_KEY = '__zq';
|
|
3888
4090
|
|
|
4091
|
+
/**
|
|
4092
|
+
* Shallow-compare two flat objects (for params / query comparison).
|
|
4093
|
+
* Avoids JSON.stringify overhead on every navigation.
|
|
4094
|
+
*/
|
|
4095
|
+
function _shallowEqual(a, b) {
|
|
4096
|
+
if (a === b) return true;
|
|
4097
|
+
if (!a || !b) return false;
|
|
4098
|
+
const keysA = Object.keys(a);
|
|
4099
|
+
const keysB = Object.keys(b);
|
|
4100
|
+
if (keysA.length !== keysB.length) return false;
|
|
4101
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
4102
|
+
const k = keysA[i];
|
|
4103
|
+
if (a[k] !== b[k]) return false;
|
|
4104
|
+
}
|
|
4105
|
+
return true;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
3889
4108
|
class Router {
|
|
3890
4109
|
constructor(config = {}) {
|
|
3891
4110
|
this._el = null;
|
|
@@ -3933,11 +4152,12 @@ class Router {
|
|
|
3933
4152
|
config.routes.forEach(r => this.add(r));
|
|
3934
4153
|
}
|
|
3935
4154
|
|
|
3936
|
-
// Listen for navigation
|
|
4155
|
+
// Listen for navigation — store handler references for cleanup in destroy()
|
|
3937
4156
|
if (this._mode === 'hash') {
|
|
3938
|
-
|
|
4157
|
+
this._onNavEvent = () => this._resolve();
|
|
4158
|
+
window.addEventListener('hashchange', this._onNavEvent);
|
|
3939
4159
|
} else {
|
|
3940
|
-
|
|
4160
|
+
this._onNavEvent = (e) => {
|
|
3941
4161
|
// Check for substate pop first — if a listener handles it, don't route
|
|
3942
4162
|
const st = e.state;
|
|
3943
4163
|
if (st && st[_ZQ_STATE_KEY] === 'substate') {
|
|
@@ -3951,11 +4171,12 @@ class Router {
|
|
|
3951
4171
|
this._fireSubstate(null, null, 'reset');
|
|
3952
4172
|
}
|
|
3953
4173
|
this._resolve();
|
|
3954
|
-
}
|
|
4174
|
+
};
|
|
4175
|
+
window.addEventListener('popstate', this._onNavEvent);
|
|
3955
4176
|
}
|
|
3956
4177
|
|
|
3957
4178
|
// Intercept link clicks for SPA navigation
|
|
3958
|
-
|
|
4179
|
+
this._onLinkClick = (e) => {
|
|
3959
4180
|
// Don't intercept modified clicks (Ctrl/Cmd+click = new tab)
|
|
3960
4181
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
3961
4182
|
const link = e.target.closest('[z-link]');
|
|
@@ -3977,7 +4198,8 @@ class Router {
|
|
|
3977
4198
|
const scrollBehavior = link.getAttribute('z-to-top') || 'instant';
|
|
3978
4199
|
window.scrollTo({ top: 0, behavior: scrollBehavior });
|
|
3979
4200
|
}
|
|
3980
|
-
}
|
|
4201
|
+
};
|
|
4202
|
+
document.addEventListener('click', this._onLinkClick);
|
|
3981
4203
|
|
|
3982
4204
|
// Initial resolve
|
|
3983
4205
|
if (this._el) {
|
|
@@ -4349,8 +4571,8 @@ class Router {
|
|
|
4349
4571
|
// with the same params, skip the full destroy/mount cycle and just
|
|
4350
4572
|
// update props. This prevents flashing and unnecessary DOM churn.
|
|
4351
4573
|
if (from && this._instance && matched.component === from.route.component) {
|
|
4352
|
-
const sameParams =
|
|
4353
|
-
const sameQuery =
|
|
4574
|
+
const sameParams = _shallowEqual(params, from.params);
|
|
4575
|
+
const sameQuery = _shallowEqual(query, from.query);
|
|
4354
4576
|
if (sameParams && sameQuery) {
|
|
4355
4577
|
// Identical navigation — nothing to do
|
|
4356
4578
|
return;
|
|
@@ -4425,7 +4647,12 @@ class Router {
|
|
|
4425
4647
|
if (typeof matched.component === 'string') {
|
|
4426
4648
|
const container = document.createElement(matched.component);
|
|
4427
4649
|
this._el.appendChild(container);
|
|
4428
|
-
|
|
4650
|
+
try {
|
|
4651
|
+
this._instance = mount(container, matched.component, props);
|
|
4652
|
+
} catch (err) {
|
|
4653
|
+
reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4429
4656
|
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', matched.component);
|
|
4430
4657
|
}
|
|
4431
4658
|
// If component is a render function
|
|
@@ -4447,6 +4674,15 @@ class Router {
|
|
|
4447
4674
|
// --- Destroy -------------------------------------------------------------
|
|
4448
4675
|
|
|
4449
4676
|
destroy() {
|
|
4677
|
+
// Remove window/document event listeners to prevent memory leaks
|
|
4678
|
+
if (this._onNavEvent) {
|
|
4679
|
+
window.removeEventListener(this._mode === 'hash' ? 'hashchange' : 'popstate', this._onNavEvent);
|
|
4680
|
+
this._onNavEvent = null;
|
|
4681
|
+
}
|
|
4682
|
+
if (this._onLinkClick) {
|
|
4683
|
+
document.removeEventListener('click', this._onLinkClick);
|
|
4684
|
+
this._onLinkClick = null;
|
|
4685
|
+
}
|
|
4450
4686
|
if (this._instance) this._instance.destroy();
|
|
4451
4687
|
this._listeners.clear();
|
|
4452
4688
|
this._substateListeners = [];
|
|
@@ -4509,6 +4745,7 @@ class Store {
|
|
|
4509
4745
|
this._getters = config.getters || {};
|
|
4510
4746
|
this._middleware = [];
|
|
4511
4747
|
this._history = []; // action log
|
|
4748
|
+
this._maxHistory = config.maxHistory || 1000;
|
|
4512
4749
|
this._debug = config.debug || false;
|
|
4513
4750
|
|
|
4514
4751
|
// Create reactive state
|
|
@@ -4568,6 +4805,10 @@ class Store {
|
|
|
4568
4805
|
try {
|
|
4569
4806
|
const result = action(this.state, ...args);
|
|
4570
4807
|
this._history.push({ action: name, args, timestamp: Date.now() });
|
|
4808
|
+
// Cap history to prevent unbounded memory growth
|
|
4809
|
+
if (this._history.length > this._maxHistory) {
|
|
4810
|
+
this._history.splice(0, this._history.length - this._maxHistory);
|
|
4811
|
+
}
|
|
4571
4812
|
return result;
|
|
4572
4813
|
} catch (err) {
|
|
4573
4814
|
reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
|
|
@@ -4725,11 +4966,28 @@ async function request(method, url, data, options = {}) {
|
|
|
4725
4966
|
|
|
4726
4967
|
// Timeout via AbortController
|
|
4727
4968
|
const controller = new AbortController();
|
|
4728
|
-
fetchOpts.signal = options.signal || controller.signal;
|
|
4729
4969
|
const timeout = options.timeout ?? _config.timeout;
|
|
4730
4970
|
let timer;
|
|
4971
|
+
// Combine user signal with internal controller for proper timeout support
|
|
4972
|
+
if (options.signal) {
|
|
4973
|
+
// If AbortSignal.any is available, combine both signals
|
|
4974
|
+
if (typeof AbortSignal.any === 'function') {
|
|
4975
|
+
fetchOpts.signal = AbortSignal.any([options.signal, controller.signal]);
|
|
4976
|
+
} else {
|
|
4977
|
+
// Fallback: forward user signal's abort to our controller
|
|
4978
|
+
fetchOpts.signal = controller.signal;
|
|
4979
|
+
if (options.signal.aborted) {
|
|
4980
|
+
controller.abort(options.signal.reason);
|
|
4981
|
+
} else {
|
|
4982
|
+
options.signal.addEventListener('abort', () => controller.abort(options.signal.reason), { once: true });
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
} else {
|
|
4986
|
+
fetchOpts.signal = controller.signal;
|
|
4987
|
+
}
|
|
4988
|
+
let _timedOut = false;
|
|
4731
4989
|
if (timeout > 0) {
|
|
4732
|
-
timer = setTimeout(() => controller.abort(), timeout);
|
|
4990
|
+
timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
|
|
4733
4991
|
}
|
|
4734
4992
|
|
|
4735
4993
|
// Run request interceptors
|
|
@@ -4789,7 +5047,10 @@ async function request(method, url, data, options = {}) {
|
|
|
4789
5047
|
} catch (err) {
|
|
4790
5048
|
if (timer) clearTimeout(timer);
|
|
4791
5049
|
if (err.name === 'AbortError') {
|
|
4792
|
-
|
|
5050
|
+
if (_timedOut) {
|
|
5051
|
+
throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
|
|
5052
|
+
}
|
|
5053
|
+
throw new Error(`Request aborted: ${method} ${fullURL}`);
|
|
4793
5054
|
}
|
|
4794
5055
|
throw err;
|
|
4795
5056
|
}
|
|
@@ -4926,6 +5187,10 @@ function escapeHtml(str) {
|
|
|
4926
5187
|
return String(str).replace(/[&<>"']/g, c => map[c]);
|
|
4927
5188
|
}
|
|
4928
5189
|
|
|
5190
|
+
function stripHtml(str) {
|
|
5191
|
+
return String(str).replace(/<[^>]*>/g, '');
|
|
5192
|
+
}
|
|
5193
|
+
|
|
4929
5194
|
/**
|
|
4930
5195
|
* Template tag for auto-escaping interpolated values
|
|
4931
5196
|
* Usage: $.html`<div>${userInput}</div>`
|
|
@@ -4971,7 +5236,10 @@ function camelCase(str) {
|
|
|
4971
5236
|
* CamelCase to kebab-case
|
|
4972
5237
|
*/
|
|
4973
5238
|
function kebabCase(str) {
|
|
4974
|
-
return str
|
|
5239
|
+
return str
|
|
5240
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
5241
|
+
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
|
|
5242
|
+
.toLowerCase();
|
|
4975
5243
|
}
|
|
4976
5244
|
|
|
4977
5245
|
|
|
@@ -4991,30 +5259,40 @@ function deepClone(obj) {
|
|
|
4991
5259
|
* Deep merge objects
|
|
4992
5260
|
*/
|
|
4993
5261
|
function deepMerge(target, ...sources) {
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5262
|
+
const seen = new WeakSet();
|
|
5263
|
+
function merge(tgt, src) {
|
|
5264
|
+
if (seen.has(src)) return tgt;
|
|
5265
|
+
seen.add(src);
|
|
5266
|
+
for (const key of Object.keys(src)) {
|
|
5267
|
+
if (src[key] && typeof src[key] === 'object' && !Array.isArray(src[key])) {
|
|
5268
|
+
if (!tgt[key] || typeof tgt[key] !== 'object') tgt[key] = {};
|
|
5269
|
+
merge(tgt[key], src[key]);
|
|
4999
5270
|
} else {
|
|
5000
|
-
|
|
5271
|
+
tgt[key] = src[key];
|
|
5001
5272
|
}
|
|
5002
5273
|
}
|
|
5274
|
+
return tgt;
|
|
5003
5275
|
}
|
|
5276
|
+
for (const source of sources) merge(target, source);
|
|
5004
5277
|
return target;
|
|
5005
5278
|
}
|
|
5006
5279
|
|
|
5007
5280
|
/**
|
|
5008
5281
|
* Simple object equality check
|
|
5009
5282
|
*/
|
|
5010
|
-
function isEqual(a, b) {
|
|
5283
|
+
function isEqual(a, b, _seen) {
|
|
5011
5284
|
if (a === b) return true;
|
|
5012
5285
|
if (typeof a !== typeof b) return false;
|
|
5013
5286
|
if (typeof a !== 'object' || a === null || b === null) return false;
|
|
5287
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
5288
|
+
// Guard against circular references
|
|
5289
|
+
if (!_seen) _seen = new Set();
|
|
5290
|
+
if (_seen.has(a)) return true;
|
|
5291
|
+
_seen.add(a);
|
|
5014
5292
|
const keysA = Object.keys(a);
|
|
5015
5293
|
const keysB = Object.keys(b);
|
|
5016
5294
|
if (keysA.length !== keysB.length) return false;
|
|
5017
|
-
return keysA.every(k => isEqual(a[k], b[k]));
|
|
5295
|
+
return keysA.every(k => isEqual(a[k], b[k], _seen));
|
|
5018
5296
|
}
|
|
5019
5297
|
|
|
5020
5298
|
|
|
@@ -5115,7 +5393,182 @@ class EventBus {
|
|
|
5115
5393
|
clear() { this._handlers.clear(); }
|
|
5116
5394
|
}
|
|
5117
5395
|
|
|
5118
|
-
const bus = new EventBus();
|
|
5396
|
+
const bus = new EventBus();
|
|
5397
|
+
|
|
5398
|
+
|
|
5399
|
+
// ---------------------------------------------------------------------------
|
|
5400
|
+
// Array utilities
|
|
5401
|
+
// ---------------------------------------------------------------------------
|
|
5402
|
+
|
|
5403
|
+
function range(startOrEnd, end, step) {
|
|
5404
|
+
let s, e, st;
|
|
5405
|
+
if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
|
|
5406
|
+
else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
|
|
5407
|
+
if (st === 0) return [];
|
|
5408
|
+
const result = [];
|
|
5409
|
+
if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
|
|
5410
|
+
else { for (let i = s; i > e; i += st) result.push(i); }
|
|
5411
|
+
return result;
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
function unique(arr, keyFn) {
|
|
5415
|
+
if (!keyFn) return [...new Set(arr)];
|
|
5416
|
+
const seen = new Set();
|
|
5417
|
+
return arr.filter(item => {
|
|
5418
|
+
const k = keyFn(item);
|
|
5419
|
+
if (seen.has(k)) return false;
|
|
5420
|
+
seen.add(k);
|
|
5421
|
+
return true;
|
|
5422
|
+
});
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
function chunk(arr, size) {
|
|
5426
|
+
const result = [];
|
|
5427
|
+
for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
|
|
5428
|
+
return result;
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
function groupBy(arr, keyFn) {
|
|
5432
|
+
const result = {};
|
|
5433
|
+
for (const item of arr) {
|
|
5434
|
+
const k = keyFn(item);
|
|
5435
|
+
(result[k] ??= []).push(item);
|
|
5436
|
+
}
|
|
5437
|
+
return result;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
|
|
5441
|
+
// ---------------------------------------------------------------------------
|
|
5442
|
+
// Object utilities
|
|
5443
|
+
// ---------------------------------------------------------------------------
|
|
5444
|
+
|
|
5445
|
+
function pick(obj, keys) {
|
|
5446
|
+
const result = {};
|
|
5447
|
+
for (const k of keys) { if (k in obj) result[k] = obj[k]; }
|
|
5448
|
+
return result;
|
|
5449
|
+
}
|
|
5450
|
+
|
|
5451
|
+
function omit(obj, keys) {
|
|
5452
|
+
const exclude = new Set(keys);
|
|
5453
|
+
const result = {};
|
|
5454
|
+
for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
|
|
5455
|
+
return result;
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5458
|
+
function getPath(obj, path, fallback) {
|
|
5459
|
+
const keys = path.split('.');
|
|
5460
|
+
let cur = obj;
|
|
5461
|
+
for (const k of keys) {
|
|
5462
|
+
if (cur == null || typeof cur !== 'object') return fallback;
|
|
5463
|
+
cur = cur[k];
|
|
5464
|
+
}
|
|
5465
|
+
return cur === undefined ? fallback : cur;
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5468
|
+
function setPath(obj, path, value) {
|
|
5469
|
+
const keys = path.split('.');
|
|
5470
|
+
let cur = obj;
|
|
5471
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
5472
|
+
const k = keys[i];
|
|
5473
|
+
if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
|
|
5474
|
+
cur = cur[k];
|
|
5475
|
+
}
|
|
5476
|
+
cur[keys[keys.length - 1]] = value;
|
|
5477
|
+
return obj;
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5480
|
+
function isEmpty(val) {
|
|
5481
|
+
if (val == null) return true;
|
|
5482
|
+
if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
|
|
5483
|
+
if (val instanceof Map || val instanceof Set) return val.size === 0;
|
|
5484
|
+
if (typeof val === 'object') return Object.keys(val).length === 0;
|
|
5485
|
+
return false;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
|
|
5489
|
+
// ---------------------------------------------------------------------------
|
|
5490
|
+
// String utilities
|
|
5491
|
+
// ---------------------------------------------------------------------------
|
|
5492
|
+
|
|
5493
|
+
function capitalize(str) {
|
|
5494
|
+
if (!str) return '';
|
|
5495
|
+
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
function truncate(str, maxLen, suffix = '…') {
|
|
5499
|
+
if (str.length <= maxLen) return str;
|
|
5500
|
+
const end = Math.max(0, maxLen - suffix.length);
|
|
5501
|
+
return str.slice(0, end) + suffix;
|
|
5502
|
+
}
|
|
5503
|
+
|
|
5504
|
+
|
|
5505
|
+
// ---------------------------------------------------------------------------
|
|
5506
|
+
// Number utilities
|
|
5507
|
+
// ---------------------------------------------------------------------------
|
|
5508
|
+
|
|
5509
|
+
function clamp(val, min, max) {
|
|
5510
|
+
return val < min ? min : val > max ? max : val;
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
|
|
5514
|
+
// ---------------------------------------------------------------------------
|
|
5515
|
+
// Function utilities
|
|
5516
|
+
// ---------------------------------------------------------------------------
|
|
5517
|
+
|
|
5518
|
+
function memoize(fn, keyFnOrOpts) {
|
|
5519
|
+
let keyFn, maxSize = 0;
|
|
5520
|
+
if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
|
|
5521
|
+
else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
|
|
5522
|
+
|
|
5523
|
+
const cache = new Map();
|
|
5524
|
+
|
|
5525
|
+
const memoized = (...args) => {
|
|
5526
|
+
const key = keyFn ? keyFn(...args) : args[0];
|
|
5527
|
+
if (cache.has(key)) return cache.get(key);
|
|
5528
|
+
const result = fn(...args);
|
|
5529
|
+
cache.set(key, result);
|
|
5530
|
+
if (maxSize > 0 && cache.size > maxSize) {
|
|
5531
|
+
cache.delete(cache.keys().next().value);
|
|
5532
|
+
}
|
|
5533
|
+
return result;
|
|
5534
|
+
};
|
|
5535
|
+
|
|
5536
|
+
memoized.clear = () => cache.clear();
|
|
5537
|
+
return memoized;
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
|
|
5541
|
+
// ---------------------------------------------------------------------------
|
|
5542
|
+
// Async utilities
|
|
5543
|
+
// ---------------------------------------------------------------------------
|
|
5544
|
+
|
|
5545
|
+
function retry(fn, opts = {}) {
|
|
5546
|
+
const { attempts = 3, delay = 1000, backoff = 1 } = opts;
|
|
5547
|
+
return new Promise((resolve, reject) => {
|
|
5548
|
+
let attempt = 0, currentDelay = delay;
|
|
5549
|
+
const tryOnce = () => {
|
|
5550
|
+
attempt++;
|
|
5551
|
+
fn(attempt).then(resolve, (err) => {
|
|
5552
|
+
if (attempt >= attempts) return reject(err);
|
|
5553
|
+
const d = currentDelay;
|
|
5554
|
+
currentDelay *= backoff;
|
|
5555
|
+
setTimeout(tryOnce, d);
|
|
5556
|
+
});
|
|
5557
|
+
};
|
|
5558
|
+
tryOnce();
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
5561
|
+
|
|
5562
|
+
function timeout(promise, ms, message) {
|
|
5563
|
+
let timer;
|
|
5564
|
+
const race = Promise.race([
|
|
5565
|
+
promise,
|
|
5566
|
+
new Promise((_, reject) => {
|
|
5567
|
+
timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
|
|
5568
|
+
})
|
|
5569
|
+
]);
|
|
5570
|
+
return race.finally(() => clearTimeout(timer));
|
|
5571
|
+
}
|
|
5119
5572
|
|
|
5120
5573
|
// --- index.js (assembly) ------------------------------------------
|
|
5121
5574
|
/**
|
|
@@ -5174,6 +5627,8 @@ Object.defineProperty($, 'name', {
|
|
|
5174
5627
|
value: query.name, writable: true, configurable: true
|
|
5175
5628
|
});
|
|
5176
5629
|
$.children = query.children;
|
|
5630
|
+
$.qs = query.qs;
|
|
5631
|
+
$.qsa = query.qsa;
|
|
5177
5632
|
|
|
5178
5633
|
// --- Collection selector ---------------------------------------------------
|
|
5179
5634
|
/**
|
|
@@ -5242,8 +5697,10 @@ $.pipe = pipe;
|
|
|
5242
5697
|
$.once = once;
|
|
5243
5698
|
$.sleep = sleep;
|
|
5244
5699
|
$.escapeHtml = escapeHtml;
|
|
5700
|
+
$.stripHtml = stripHtml;
|
|
5245
5701
|
$.html = html;
|
|
5246
5702
|
$.trust = trust;
|
|
5703
|
+
$.TrustedHTML = TrustedHTML;
|
|
5247
5704
|
$.uuid = uuid;
|
|
5248
5705
|
$.camelCase = camelCase;
|
|
5249
5706
|
$.kebabCase = kebabCase;
|
|
@@ -5254,7 +5711,23 @@ $.param = param;
|
|
|
5254
5711
|
$.parseQuery = parseQuery;
|
|
5255
5712
|
$.storage = storage;
|
|
5256
5713
|
$.session = session;
|
|
5714
|
+
$.EventBus = EventBus;
|
|
5257
5715
|
$.bus = bus;
|
|
5716
|
+
$.range = range;
|
|
5717
|
+
$.unique = unique;
|
|
5718
|
+
$.chunk = chunk;
|
|
5719
|
+
$.groupBy = groupBy;
|
|
5720
|
+
$.pick = pick;
|
|
5721
|
+
$.omit = omit;
|
|
5722
|
+
$.getPath = getPath;
|
|
5723
|
+
$.setPath = setPath;
|
|
5724
|
+
$.isEmpty = isEmpty;
|
|
5725
|
+
$.capitalize = capitalize;
|
|
5726
|
+
$.truncate = truncate;
|
|
5727
|
+
$.clamp = clamp;
|
|
5728
|
+
$.memoize = memoize;
|
|
5729
|
+
$.retry = retry;
|
|
5730
|
+
$.timeout = timeout;
|
|
5258
5731
|
|
|
5259
5732
|
// --- Error handling --------------------------------------------------------
|
|
5260
5733
|
$.onError = onError;
|
|
@@ -5264,8 +5737,8 @@ $.guardCallback = guardCallback;
|
|
|
5264
5737
|
$.validate = validate;
|
|
5265
5738
|
|
|
5266
5739
|
// --- Meta ------------------------------------------------------------------
|
|
5267
|
-
$.version = '0.9.
|
|
5268
|
-
$.libSize = '~
|
|
5740
|
+
$.version = '0.9.5';
|
|
5741
|
+
$.libSize = '~98 KB';
|
|
5269
5742
|
$.meta = {}; // populated at build time by CLI bundler
|
|
5270
5743
|
|
|
5271
5744
|
$.noConflict = () => {
|