zero-query 1.2.0 → 1.2.3
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/cli/commands/build-api.js +0 -1
- package/cli/commands/build.js +0 -1
- package/cli/commands/bundle.js +0 -2
- package/cli/commands/create.js +46 -1
- package/cli/scaffold/webrtc/app/components/video-room.js +276 -130
- package/cli/scaffold/webrtc/package.json +16 -0
- package/cli/scaffold/webrtc/server/index.js +198 -0
- package/dist/API.md +1 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +296 -155
- package/dist/zquery.min.js +7 -7
- package/package.json +3 -2
- package/src/component.js +161 -112
- package/src/core.js +11 -3
- package/src/expression.js +26 -19
- package/src/reactive.js +18 -2
- package/src/router.js +38 -3
- package/src/ssr.js +14 -11
- package/src/store.js +16 -9
- package/src/utils.js +24 -5
- package/tests/audit.test.js +9 -9
- package/tests/bench/render.bench.js +77 -0
- package/tests/cli.test.js +7 -7
- package/tests/component.test.js +107 -0
- package/tests/core.test.js +16 -0
- package/tests/expression.test.js +31 -0
- package/tests/http.test.js +45 -0
- package/tests/reactive.test.js +25 -0
- package/tests/router.test.js +96 -0
- package/tests/ssr.test.js +46 -0
- package/tests/store.test.js +10 -0
- package/tests/utils.test.js +34 -0
- package/cli/scaffold/webrtc/app/lib/room.js +0 -252
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v1.2.
|
|
2
|
+
* zQuery (zeroQuery) v1.2.3
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -3865,11 +3865,27 @@ const webrtc = {
|
|
|
3865
3865
|
*/
|
|
3866
3866
|
|
|
3867
3867
|
|
|
3868
|
+
// ---------------------------------------------------------------------------
|
|
3869
|
+
// Host-object passthrough
|
|
3870
|
+
// ---------------------------------------------------------------------------
|
|
3871
|
+
// The reactive proxy is meant for plain data: state bags, arrays, nested
|
|
3872
|
+
// records. Wrapping host/native objects (MediaStream, RTCPeerConnection,
|
|
3873
|
+
// Blob, Map, Date, etc.) breaks them - the proxy receiver is not the real
|
|
3874
|
+
// underlying object, so any internal-slot method call throws
|
|
3875
|
+
// "Illegal invocation" in the browser. Only proxy values whose prototype is
|
|
3876
|
+
// Object.prototype/null (plain objects) or that are Arrays.
|
|
3877
|
+
function _isProxyable(value) {
|
|
3878
|
+
if (value === null || typeof value !== 'object') return false;
|
|
3879
|
+
if (Array.isArray(value)) return true;
|
|
3880
|
+
const proto = Object.getPrototypeOf(value);
|
|
3881
|
+
return proto === Object.prototype || proto === null;
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3868
3884
|
// ---------------------------------------------------------------------------
|
|
3869
3885
|
// Deep reactive proxy
|
|
3870
3886
|
// ---------------------------------------------------------------------------
|
|
3871
3887
|
function reactive(target, onChange, _path = '') {
|
|
3872
|
-
if (
|
|
3888
|
+
if (!_isProxyable(target)) return target;
|
|
3873
3889
|
if (typeof onChange !== 'function') {
|
|
3874
3890
|
reportError(ErrorCode.REACTIVE_CALLBACK, 'reactive() onChange must be a function', { received: typeof onChange });
|
|
3875
3891
|
onChange = () => {};
|
|
@@ -3883,7 +3899,7 @@ function reactive(target, onChange, _path = '') {
|
|
|
3883
3899
|
if (key === '__raw') return obj;
|
|
3884
3900
|
|
|
3885
3901
|
const value = obj[key];
|
|
3886
|
-
if (
|
|
3902
|
+
if (_isProxyable(value)) {
|
|
3887
3903
|
// Return cached proxy or create new one
|
|
3888
3904
|
if (proxyCache.has(value)) return proxyCache.get(value);
|
|
3889
3905
|
const childProxy = new Proxy(value, handler);
|
|
@@ -4896,11 +4912,17 @@ class ZQueryCollection {
|
|
|
4896
4912
|
});
|
|
4897
4913
|
}
|
|
4898
4914
|
if (value === undefined) return this.first()?.getAttribute(name);
|
|
4899
|
-
|
|
4915
|
+
// Fast path: tight loop, no closure allocation, no `each` overhead.
|
|
4916
|
+
// Mirrors the addClass/removeClass single-name fast path.
|
|
4917
|
+
const els = this.elements;
|
|
4918
|
+
for (let i = 0; i < els.length; i++) els[i].setAttribute(name, value);
|
|
4919
|
+
return this;
|
|
4900
4920
|
}
|
|
4901
4921
|
|
|
4902
4922
|
removeAttr(name) {
|
|
4903
|
-
|
|
4923
|
+
const els = this.elements;
|
|
4924
|
+
for (let i = 0; i < els.length; i++) els[i].removeAttribute(name);
|
|
4925
|
+
return this;
|
|
4904
4926
|
}
|
|
4905
4927
|
|
|
4906
4928
|
prop(name, value) {
|
|
@@ -5465,7 +5487,9 @@ function queryAll(selector, context) {
|
|
|
5465
5487
|
// Quick-ref shortcuts, on $ namespace)
|
|
5466
5488
|
// ---------------------------------------------------------------------------
|
|
5467
5489
|
query.id = (id) => document.getElementById(id);
|
|
5468
|
-
|
|
5490
|
+
// Escape the class name so callers can safely pass identifiers containing
|
|
5491
|
+
// dots, colons, leading digits, etc. without breaking the selector.
|
|
5492
|
+
query.class = (name) => document.querySelector(`.${typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(name) : name}`);
|
|
5469
5493
|
query.classes = (name) => new ZQueryCollection(Array.from(document.getElementsByClassName(name)));
|
|
5470
5494
|
query.tag = (name) => new ZQueryCollection(Array.from(document.getElementsByTagName(name)));
|
|
5471
5495
|
Object.defineProperty(query, 'name', {
|
|
@@ -5754,12 +5778,28 @@ class Parser {
|
|
|
5754
5778
|
|
|
5755
5779
|
// Main entry
|
|
5756
5780
|
parse() {
|
|
5781
|
+
this.depth = 0;
|
|
5757
5782
|
const result = this.parseExpression(0);
|
|
5758
5783
|
return result;
|
|
5759
5784
|
}
|
|
5760
5785
|
|
|
5761
5786
|
// Precedence climbing
|
|
5762
5787
|
parseExpression(minPrec) {
|
|
5788
|
+
// Cap nesting depth so a deeply parenthesised or recursive expression
|
|
5789
|
+
// can’t stack-overflow the host. Threshold (96) is generous - real
|
|
5790
|
+
// template expressions are flat - but stops pathological inputs cold.
|
|
5791
|
+
if (++this.depth > 96) {
|
|
5792
|
+
this.depth--;
|
|
5793
|
+
throw new Error('Expression nesting depth exceeded (max 96)');
|
|
5794
|
+
}
|
|
5795
|
+
try {
|
|
5796
|
+
return this._parseExpressionImpl(minPrec);
|
|
5797
|
+
} finally {
|
|
5798
|
+
this.depth--;
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5802
|
+
_parseExpressionImpl(minPrec) {
|
|
5763
5803
|
let left = this.parseUnary();
|
|
5764
5804
|
|
|
5765
5805
|
while (true) {
|
|
@@ -6086,13 +6126,6 @@ class Parser {
|
|
|
6086
6126
|
// ---------------------------------------------------------------------------
|
|
6087
6127
|
|
|
6088
6128
|
/** Safe property access whitelist for built-in prototypes */
|
|
6089
|
-
const SAFE_ARRAY_METHODS = new Set([
|
|
6090
|
-
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
6091
|
-
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
6092
|
-
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
6093
|
-
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
6094
|
-
]);
|
|
6095
|
-
|
|
6096
6129
|
const SAFE_STRING_METHODS = new Set([
|
|
6097
6130
|
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
6098
6131
|
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
@@ -6105,18 +6138,6 @@ const SAFE_NUMBER_METHODS = new Set([
|
|
|
6105
6138
|
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
6106
6139
|
]);
|
|
6107
6140
|
|
|
6108
|
-
const SAFE_OBJECT_METHODS = new Set([
|
|
6109
|
-
'hasOwnProperty', 'toString', 'valueOf',
|
|
6110
|
-
]);
|
|
6111
|
-
|
|
6112
|
-
const SAFE_MATH_PROPS = new Set([
|
|
6113
|
-
'PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2',
|
|
6114
|
-
'abs', 'ceil', 'floor', 'round', 'trunc', 'max', 'min', 'pow',
|
|
6115
|
-
'sqrt', 'sign', 'random', 'log', 'log2', 'log10',
|
|
6116
|
-
]);
|
|
6117
|
-
|
|
6118
|
-
const SAFE_JSON_PROPS = new Set(['parse', 'stringify']);
|
|
6119
|
-
|
|
6120
6141
|
/**
|
|
6121
6142
|
* Check if property access is safe
|
|
6122
6143
|
*/
|
|
@@ -6414,8 +6435,18 @@ function _evalBinary(node, scope) {
|
|
|
6414
6435
|
const _astCache = new Map();
|
|
6415
6436
|
const _AST_CACHE_MAX = 512;
|
|
6416
6437
|
|
|
6438
|
+
// Maximum source length accepted by safeEval. Real template expressions
|
|
6439
|
+
// are tens of bytes; anything beyond 8 KB is almost certainly an attack
|
|
6440
|
+
// payload or accidental dump. Reject up front to avoid feeding the
|
|
6441
|
+
// tokenizer and parser unbounded input.
|
|
6442
|
+
const _EXPR_MAX_LEN = 8192;
|
|
6443
|
+
|
|
6417
6444
|
function safeEval(expr, scope) {
|
|
6418
6445
|
try {
|
|
6446
|
+
if (typeof expr !== 'string') return undefined;
|
|
6447
|
+
if (expr.length > _EXPR_MAX_LEN) {
|
|
6448
|
+
throw new Error(`Expression exceeds max length (${_EXPR_MAX_LEN} bytes)`);
|
|
6449
|
+
}
|
|
6419
6450
|
const trimmed = expr.trim();
|
|
6420
6451
|
if (!trimmed) return undefined;
|
|
6421
6452
|
|
|
@@ -6649,6 +6680,10 @@ class Component {
|
|
|
6649
6680
|
this._updateQueued = false;
|
|
6650
6681
|
this._listeners = [];
|
|
6651
6682
|
this._watchCleanups = [];
|
|
6683
|
+
// Elements that have scheduled debounce/throttle timers under this
|
|
6684
|
+
// component instance. Tracked so destroy() can clear them even if the
|
|
6685
|
+
// element is detached from the subtree before teardown.
|
|
6686
|
+
this._timerEls = new Set();
|
|
6652
6687
|
|
|
6653
6688
|
// Refs map
|
|
6654
6689
|
this.refs = {};
|
|
@@ -6673,15 +6708,21 @@ class Component {
|
|
|
6673
6708
|
if (definition.props && typeof definition.props === 'object' && !Array.isArray(definition.props)) {
|
|
6674
6709
|
// Reactive props with type coercion and defaults
|
|
6675
6710
|
this.props = this._resolveReactiveProps(definition.props, props);
|
|
6676
|
-
// MutationObserver to re-read props when parent re-renders and changes attributes
|
|
6711
|
+
// MutationObserver to re-read props when parent re-renders and changes attributes.
|
|
6712
|
+
// attributeFilter limits callbacks to the declared prop names (plus their
|
|
6713
|
+
// dynamic `:name` aliases) — unrelated attribute churn no longer fires.
|
|
6714
|
+
const propNames = Object.keys(definition.props);
|
|
6715
|
+
const filter = [];
|
|
6716
|
+
for (const n of propNames) {
|
|
6717
|
+
filter.push(n.toLowerCase());
|
|
6718
|
+
filter.push(':' + n.toLowerCase());
|
|
6719
|
+
}
|
|
6677
6720
|
this._propObserver = new MutationObserver((mutations) => {
|
|
6678
6721
|
if (this._destroyed) return;
|
|
6679
6722
|
let changed = false;
|
|
6680
6723
|
for (const mut of mutations) {
|
|
6681
6724
|
if (mut.type === 'attributes') {
|
|
6682
6725
|
const attrName = mut.attributeName;
|
|
6683
|
-
// Skip internal attributes
|
|
6684
|
-
if (attrName.startsWith('z-') || attrName.startsWith('@') || attrName.startsWith(':') || attrName.startsWith('data-zq')) continue;
|
|
6685
6726
|
// Check if this is a defined prop (attribute names are lowercase)
|
|
6686
6727
|
const propName = attrName.startsWith(':') ? attrName.slice(1) : attrName;
|
|
6687
6728
|
if (propName in definition.props) {
|
|
@@ -6694,7 +6735,7 @@ class Component {
|
|
|
6694
6735
|
this._scheduleUpdate();
|
|
6695
6736
|
}
|
|
6696
6737
|
});
|
|
6697
|
-
this._propObserver.observe(el, { attributes: true });
|
|
6738
|
+
this._propObserver.observe(el, { attributes: true, attributeFilter: filter });
|
|
6698
6739
|
} else {
|
|
6699
6740
|
// Legacy: frozen props from parent
|
|
6700
6741
|
this.props = Object.freeze({ ...props });
|
|
@@ -6976,44 +7017,60 @@ class Component {
|
|
|
6976
7017
|
|
|
6977
7018
|
// Apply scoped styles on first render
|
|
6978
7019
|
if (!this._mounted && combinedStyles) {
|
|
6979
|
-
const
|
|
7020
|
+
const def = this._def;
|
|
7021
|
+
// Use a stable per-definition scope attr so all instances share one
|
|
7022
|
+
// <style> tag (ref-counted). Anonymous defs fall back to per-instance
|
|
7023
|
+
// scoping for safety.
|
|
7024
|
+
let scopeAttr = def._scopeAttr;
|
|
7025
|
+
if (!scopeAttr) {
|
|
7026
|
+
scopeAttr = def._name ? `z-s-${def._name}` : `z-s${this._uid}`;
|
|
7027
|
+
def._scopeAttr = scopeAttr;
|
|
7028
|
+
}
|
|
6980
7029
|
this._el.setAttribute(scopeAttr, '');
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
7030
|
+
this._scopeAttr = scopeAttr;
|
|
7031
|
+
|
|
7032
|
+
if (def._styleEl && def._styleEl.isConnected) {
|
|
7033
|
+
// Already injected by a previous instance — just bump refcount.
|
|
7034
|
+
def._styleRefCount = (def._styleRefCount || 0) + 1;
|
|
7035
|
+
} else {
|
|
7036
|
+
let noScopeDepth = 0; // brace depth at which a no-scope @-rule started (0 = none active)
|
|
7037
|
+
let braceDepth = 0; // overall brace depth
|
|
7038
|
+
const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
|
|
7039
|
+
if (match === '}') {
|
|
7040
|
+
if (noScopeDepth > 0 && braceDepth <= noScopeDepth) noScopeDepth = 0;
|
|
7041
|
+
braceDepth--;
|
|
7042
|
+
return match;
|
|
7043
|
+
}
|
|
7044
|
+
braceDepth++;
|
|
7045
|
+
const trimmed = selector.trim();
|
|
7046
|
+
// Don't scope @-rules themselves
|
|
7047
|
+
if (trimmed.startsWith('@')) {
|
|
7048
|
+
// @keyframes and @font-face contain non-selector content - skip scoping inside them
|
|
7049
|
+
if (/^@(keyframes|font-face)\b/.test(trimmed)) {
|
|
7050
|
+
noScopeDepth = braceDepth;
|
|
7051
|
+
}
|
|
7052
|
+
return match;
|
|
7053
|
+
}
|
|
7054
|
+
// Inside @keyframes or @font-face - don't scope inner rules
|
|
7055
|
+
if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
|
|
7056
|
+
return match;
|
|
7057
|
+
}
|
|
7058
|
+
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
7059
|
+
});
|
|
7060
|
+
const styleEl = document.createElement('style');
|
|
7061
|
+
styleEl.textContent = scoped;
|
|
7062
|
+
styleEl.setAttribute('data-zq-component', def._name || '');
|
|
7063
|
+
styleEl.setAttribute('data-zq-scope', scopeAttr);
|
|
7064
|
+
if (def._resolvedStyleUrls) {
|
|
7065
|
+
styleEl.setAttribute('data-zq-style-urls', def._resolvedStyleUrls.join(' '));
|
|
7066
|
+
if (def.styles) {
|
|
7067
|
+
styleEl.setAttribute('data-zq-inline', def.styles);
|
|
6996
7068
|
}
|
|
6997
|
-
return match;
|
|
6998
|
-
}
|
|
6999
|
-
// Inside @keyframes or @font-face - don't scope inner rules
|
|
7000
|
-
if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
|
|
7001
|
-
return match;
|
|
7002
|
-
}
|
|
7003
|
-
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
7004
|
-
});
|
|
7005
|
-
const styleEl = document.createElement('style');
|
|
7006
|
-
styleEl.textContent = scoped;
|
|
7007
|
-
styleEl.setAttribute('data-zq-component', this._def._name || '');
|
|
7008
|
-
styleEl.setAttribute('data-zq-scope', scopeAttr);
|
|
7009
|
-
if (this._def._resolvedStyleUrls) {
|
|
7010
|
-
styleEl.setAttribute('data-zq-style-urls', this._def._resolvedStyleUrls.join(' '));
|
|
7011
|
-
if (this._def.styles) {
|
|
7012
|
-
styleEl.setAttribute('data-zq-inline', this._def.styles);
|
|
7013
7069
|
}
|
|
7070
|
+
document.head.appendChild(styleEl);
|
|
7071
|
+
def._styleEl = styleEl;
|
|
7072
|
+
def._styleRefCount = 1;
|
|
7014
7073
|
}
|
|
7015
|
-
document.head.appendChild(styleEl);
|
|
7016
|
-
this._styleEl = styleEl;
|
|
7017
7074
|
}
|
|
7018
7075
|
|
|
7019
7076
|
// -- Focus preservation ----------------------------------------
|
|
@@ -7222,7 +7279,7 @@ class Component {
|
|
|
7222
7279
|
});
|
|
7223
7280
|
|
|
7224
7281
|
let stoppedAt = null; // Track elements that called .stop
|
|
7225
|
-
for (const {
|
|
7282
|
+
for (const { methodExpr, modifiers, el, matched } of hits) {
|
|
7226
7283
|
|
|
7227
7284
|
// In delegated events, .stop should prevent ancestor bindings from
|
|
7228
7285
|
// firing - stopPropagation alone only stops real DOM bubbling.
|
|
@@ -7316,6 +7373,7 @@ class Component {
|
|
|
7316
7373
|
clearTimeout(timers[event]);
|
|
7317
7374
|
timers[event] = setTimeout(() => invoke(e), ms);
|
|
7318
7375
|
_debounceTimers.set(el, timers);
|
|
7376
|
+
this._timerEls.add(el);
|
|
7319
7377
|
continue;
|
|
7320
7378
|
}
|
|
7321
7379
|
|
|
@@ -7328,6 +7386,7 @@ class Component {
|
|
|
7328
7386
|
invoke(e);
|
|
7329
7387
|
timers[event] = setTimeout(() => { timers[event] = null; }, ms);
|
|
7330
7388
|
_throttleTimers.set(el, timers);
|
|
7389
|
+
this._timerEls.add(el);
|
|
7331
7390
|
continue;
|
|
7332
7391
|
}
|
|
7333
7392
|
|
|
@@ -7410,10 +7469,15 @@ class Component {
|
|
|
7410
7469
|
: isEditable ? 'input' : 'input';
|
|
7411
7470
|
|
|
7412
7471
|
// -- Handler: read DOM → write to reactive state -------------
|
|
7413
|
-
//
|
|
7414
|
-
//
|
|
7415
|
-
|
|
7416
|
-
|
|
7472
|
+
// Remove any previously-bound z-model listener on this element so
|
|
7473
|
+
// re-runs (re-render, keyed reconciliation, modifier change) replace
|
|
7474
|
+
// instead of stacking. A boolean "already bound" flag is not enough:
|
|
7475
|
+
// if the bound state key / modifiers ever change, the stale handler
|
|
7476
|
+
// would keep writing to the old key.
|
|
7477
|
+
if (el._zqModelUnbind) {
|
|
7478
|
+
try { el._zqModelUnbind(); } catch { /* ignore */ }
|
|
7479
|
+
el._zqModelUnbind = null;
|
|
7480
|
+
}
|
|
7417
7481
|
|
|
7418
7482
|
const handler = () => {
|
|
7419
7483
|
let val;
|
|
@@ -7435,12 +7499,18 @@ class Component {
|
|
|
7435
7499
|
|
|
7436
7500
|
if (hasDebounce) {
|
|
7437
7501
|
let timer = null;
|
|
7438
|
-
|
|
7502
|
+
const debounced = () => {
|
|
7439
7503
|
clearTimeout(timer);
|
|
7440
7504
|
timer = setTimeout(handler, debounceMs);
|
|
7441
|
-
}
|
|
7505
|
+
};
|
|
7506
|
+
el.addEventListener(event, debounced);
|
|
7507
|
+
el._zqModelUnbind = () => {
|
|
7508
|
+
el.removeEventListener(event, debounced);
|
|
7509
|
+
clearTimeout(timer);
|
|
7510
|
+
};
|
|
7442
7511
|
} else {
|
|
7443
7512
|
el.addEventListener(event, handler);
|
|
7513
|
+
el._zqModelUnbind = () => el.removeEventListener(event, handler);
|
|
7444
7514
|
}
|
|
7445
7515
|
});
|
|
7446
7516
|
}
|
|
@@ -7665,19 +7735,20 @@ class Component {
|
|
|
7665
7735
|
}
|
|
7666
7736
|
});
|
|
7667
7737
|
|
|
7668
|
-
// -- z-bind
|
|
7669
|
-
//
|
|
7670
|
-
//
|
|
7671
|
-
//
|
|
7672
|
-
// at the walker level (faster than per-node closest('[z-pre]') checks).
|
|
7738
|
+
// -- Single TreeWalker pass for z-bind/:attr, z-class, z-style,
|
|
7739
|
+
// z-stream, z-cloak. One walk amortises the cost across five
|
|
7740
|
+
// directives that used to issue separate querySelectorAll calls.
|
|
7741
|
+
// FILTER_REJECT on z-pre subtrees skips per-node closest() checks.
|
|
7673
7742
|
const walker = document.createTreeWalker(this._el, NodeFilter.SHOW_ELEMENT, {
|
|
7674
7743
|
acceptNode(n) {
|
|
7675
7744
|
return n.hasAttribute('z-pre') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
|
7676
7745
|
}
|
|
7677
7746
|
});
|
|
7747
|
+
const hasMediaStream = typeof MediaStream !== 'undefined';
|
|
7678
7748
|
let node;
|
|
7679
7749
|
while ((node = walker.nextNode())) {
|
|
7680
7750
|
const attrs = node.attributes;
|
|
7751
|
+
// z-bind:* / :* — iterate attrs once (reverse to allow remove)
|
|
7681
7752
|
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
7682
7753
|
const attr = attrs[i];
|
|
7683
7754
|
let attrName;
|
|
@@ -7688,71 +7759,67 @@ class Component {
|
|
|
7688
7759
|
const val = this._evalExpr(attr.value);
|
|
7689
7760
|
node.removeAttribute(attr.name);
|
|
7690
7761
|
if (val === false || val === null || val === undefined) {
|
|
7691
|
-
node.
|
|
7762
|
+
node.toggleAttribute(attrName, false);
|
|
7692
7763
|
} else if (val === true) {
|
|
7693
|
-
node.
|
|
7764
|
+
node.toggleAttribute(attrName, true);
|
|
7694
7765
|
} else {
|
|
7695
7766
|
node.setAttribute(attrName, String(val));
|
|
7696
7767
|
}
|
|
7697
7768
|
}
|
|
7698
|
-
}
|
|
7699
7769
|
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
val
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7770
|
+
// z-class
|
|
7771
|
+
if (node.hasAttribute('z-class')) {
|
|
7772
|
+
const val = this._evalExpr(node.getAttribute('z-class'));
|
|
7773
|
+
if (typeof val === 'string') {
|
|
7774
|
+
val.split(/\s+/).filter(Boolean).forEach(c => node.classList.add(c));
|
|
7775
|
+
} else if (Array.isArray(val)) {
|
|
7776
|
+
val.filter(Boolean).forEach(c => node.classList.add(String(c)));
|
|
7777
|
+
} else if (val && typeof val === 'object') {
|
|
7778
|
+
for (const [cls, active] of Object.entries(val)) {
|
|
7779
|
+
node.classList.toggle(cls, !!active);
|
|
7780
|
+
}
|
|
7711
7781
|
}
|
|
7782
|
+
node.removeAttribute('z-class');
|
|
7712
7783
|
}
|
|
7713
|
-
el.removeAttribute('z-class');
|
|
7714
|
-
});
|
|
7715
7784
|
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7785
|
+
// z-style
|
|
7786
|
+
if (node.hasAttribute('z-style')) {
|
|
7787
|
+
const val = this._evalExpr(node.getAttribute('z-style'));
|
|
7788
|
+
if (typeof val === 'string') {
|
|
7789
|
+
node.style.cssText += ';' + val;
|
|
7790
|
+
} else if (val && typeof val === 'object') {
|
|
7791
|
+
for (const [prop, v] of Object.entries(val)) {
|
|
7792
|
+
node.style[prop] = v;
|
|
7793
|
+
}
|
|
7725
7794
|
}
|
|
7795
|
+
node.removeAttribute('z-style');
|
|
7726
7796
|
}
|
|
7727
|
-
el.removeAttribute('z-style');
|
|
7728
|
-
});
|
|
7729
7797
|
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7798
|
+
// z-stream → MediaStream → <video>/<audio>.srcObject
|
|
7799
|
+
if (node.hasAttribute('z-stream')) {
|
|
7800
|
+
const val = this._evalExpr(node.getAttribute('z-stream'));
|
|
7801
|
+
if (val == null) {
|
|
7802
|
+
node.srcObject = null;
|
|
7803
|
+
} else if (hasMediaStream && val instanceof MediaStream) {
|
|
7804
|
+
node.srcObject = val;
|
|
7805
|
+
} else if (val && typeof val.getTracks === 'function') {
|
|
7806
|
+
// Accept duck-typed stream objects (test fakes, polyfills).
|
|
7807
|
+
node.srcObject = val;
|
|
7808
|
+
} else {
|
|
7809
|
+
node.srcObject = null;
|
|
7810
|
+
}
|
|
7811
|
+
node.removeAttribute('z-stream');
|
|
7744
7812
|
}
|
|
7745
|
-
|
|
7746
|
-
|
|
7813
|
+
|
|
7814
|
+
// z-cloak (just strip)
|
|
7815
|
+
if (node.hasAttribute('z-cloak')) {
|
|
7816
|
+
node.removeAttribute('z-cloak');
|
|
7817
|
+
}
|
|
7818
|
+
}
|
|
7747
7819
|
|
|
7748
7820
|
// z-html and z-text are now pre-expanded at string level (before
|
|
7749
7821
|
// morph) via _expandContentDirectives(), so the diff engine can
|
|
7750
7822
|
// properly diff their content instead of clearing + re-injecting.
|
|
7751
|
-
|
|
7752
|
-
// -- z-cloak (remove after render) -----------------------------
|
|
7753
|
-
this._el.querySelectorAll('[z-cloak]').forEach(el => {
|
|
7754
|
-
el.removeAttribute('z-cloak');
|
|
7755
|
-
});
|
|
7756
7823
|
}
|
|
7757
7824
|
|
|
7758
7825
|
// ---------------------------------------------------------------------------
|
|
@@ -7884,21 +7951,34 @@ class Component {
|
|
|
7884
7951
|
this._delegatedEvents = null;
|
|
7885
7952
|
this._eventBindings = null;
|
|
7886
7953
|
// Clear any pending debounce/throttle timers to prevent stale closures.
|
|
7887
|
-
//
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7954
|
+
// Use the per-instance _timerEls set so detached elements (removed by
|
|
7955
|
+
// z-if / z-for / keyed reconciliation before destroy) are still cleaned.
|
|
7956
|
+
if (this._timerEls) {
|
|
7957
|
+
this._timerEls.forEach(child => {
|
|
7958
|
+
const dTimers = _debounceTimers.get(child);
|
|
7959
|
+
if (dTimers) {
|
|
7960
|
+
for (const key in dTimers) clearTimeout(dTimers[key]);
|
|
7961
|
+
_debounceTimers.delete(child);
|
|
7962
|
+
}
|
|
7963
|
+
const tTimers = _throttleTimers.get(child);
|
|
7964
|
+
if (tTimers) {
|
|
7965
|
+
for (const key in tTimers) clearTimeout(tTimers[key]);
|
|
7966
|
+
_throttleTimers.delete(child);
|
|
7967
|
+
}
|
|
7968
|
+
});
|
|
7969
|
+
this._timerEls.clear();
|
|
7970
|
+
}
|
|
7971
|
+
if (this._scopeAttr) {
|
|
7972
|
+
const def = this._def;
|
|
7973
|
+
if (def && def._styleEl && def._scopeAttr === this._scopeAttr) {
|
|
7974
|
+
def._styleRefCount = (def._styleRefCount || 1) - 1;
|
|
7975
|
+
if (def._styleRefCount <= 0) {
|
|
7976
|
+
def._styleEl.remove();
|
|
7977
|
+
def._styleEl = null;
|
|
7978
|
+
def._styleRefCount = 0;
|
|
7979
|
+
}
|
|
7899
7980
|
}
|
|
7900
|
-
}
|
|
7901
|
-
if (this._styleEl) this._styleEl.remove();
|
|
7981
|
+
}
|
|
7902
7982
|
_instances.delete(this._el);
|
|
7903
7983
|
this._el.innerHTML = '';
|
|
7904
7984
|
}
|
|
@@ -8217,7 +8297,12 @@ class Router {
|
|
|
8217
8297
|
this._mode = isFile ? 'hash' : (config.mode || 'history');
|
|
8218
8298
|
|
|
8219
8299
|
// Keep-alive cache: component name → { container, instance }
|
|
8300
|
+
// Map iteration order = insertion order, used for LRU eviction.
|
|
8220
8301
|
this._keepAliveCache = new Map();
|
|
8302
|
+
// Optional cap on cached keep-alive instances. null = unbounded (default).
|
|
8303
|
+
this._keepAliveMax = (typeof config.keepAliveMax === 'number' && config.keepAliveMax > 0)
|
|
8304
|
+
? config.keepAliveMax
|
|
8305
|
+
: null;
|
|
8221
8306
|
|
|
8222
8307
|
// Base path for sub-path deployments
|
|
8223
8308
|
// Priority: explicit config.base → window.__ZQ_BASE → <base href> tag
|
|
@@ -8342,7 +8427,13 @@ class Router {
|
|
|
8342
8427
|
if (paramsAttr) {
|
|
8343
8428
|
try {
|
|
8344
8429
|
const params = JSON.parse(paramsAttr);
|
|
8345
|
-
|
|
8430
|
+
// Reject arrays, null, primitives - z-link-params must be a plain
|
|
8431
|
+
// key/value object since we use it to interpolate :param placeholders.
|
|
8432
|
+
if (typeof params !== 'object' || params === null || Array.isArray(params)) {
|
|
8433
|
+
reportError(ErrorCode.ROUTER_RESOLVE, 'z-link-params must be a JSON object', { href, paramsAttr });
|
|
8434
|
+
} else {
|
|
8435
|
+
href = this._interpolateParams(href, params);
|
|
8436
|
+
}
|
|
8346
8437
|
} catch (err) {
|
|
8347
8438
|
reportError(ErrorCode.ROUTER_RESOLVE, 'Malformed JSON in z-link-params', { href, paramsAttr }, err);
|
|
8348
8439
|
}
|
|
@@ -8564,7 +8655,6 @@ class Router {
|
|
|
8564
8655
|
if (this._mode === 'hash') {
|
|
8565
8656
|
// Hash mode: stash the substate in a global - hashchange will check.
|
|
8566
8657
|
// We still push a history entry via a sentinel hash suffix.
|
|
8567
|
-
const current = window.location.hash || '#/';
|
|
8568
8658
|
window.history.pushState(
|
|
8569
8659
|
{ [_ZQ_STATE_KEY]: 'substate', key, data },
|
|
8570
8660
|
'',
|
|
@@ -8805,6 +8895,9 @@ class Router {
|
|
|
8805
8895
|
// Keep-alive: reuse cached instance
|
|
8806
8896
|
if (isKeepAlive && componentName && this._keepAliveCache.has(componentName)) {
|
|
8807
8897
|
const cached = this._keepAliveCache.get(componentName);
|
|
8898
|
+
// Refresh LRU order: move to most-recently-used position
|
|
8899
|
+
this._keepAliveCache.delete(componentName);
|
|
8900
|
+
this._keepAliveCache.set(componentName, cached);
|
|
8808
8901
|
// Hide all children, show the cached one
|
|
8809
8902
|
[...this._el.children].forEach(c => { c.style.display = 'none'; });
|
|
8810
8903
|
cached.container.style.display = '';
|
|
@@ -8843,6 +8936,7 @@ class Router {
|
|
|
8843
8936
|
|
|
8844
8937
|
if (isKeepAlive) {
|
|
8845
8938
|
this._keepAliveCache.set(componentName, { container, instance: this._instance });
|
|
8939
|
+
this._evictKeepAliveLRU();
|
|
8846
8940
|
// Call activated() on first mount
|
|
8847
8941
|
if (this._instance._def.activated) {
|
|
8848
8942
|
try { this._instance._def.activated.call(this._instance); }
|
|
@@ -8910,6 +9004,27 @@ class Router {
|
|
|
8910
9004
|
|
|
8911
9005
|
// --- Destroy -------------------------------------------------------------
|
|
8912
9006
|
|
|
9007
|
+
/**
|
|
9008
|
+
* @private Evict least-recently-used keep-alive entries beyond _keepAliveMax.
|
|
9009
|
+
* The current route's component is preserved even if it would otherwise be
|
|
9010
|
+
* the oldest entry, to avoid destroying the active view.
|
|
9011
|
+
*/
|
|
9012
|
+
_evictKeepAliveLRU() {
|
|
9013
|
+
if (this._keepAliveMax == null) return;
|
|
9014
|
+
while (this._keepAliveCache.size > this._keepAliveMax) {
|
|
9015
|
+
// Find oldest entry that isn't the currently-active component
|
|
9016
|
+
let evictKey = null;
|
|
9017
|
+
for (const key of this._keepAliveCache.keys()) {
|
|
9018
|
+
if (key !== this._currentComponentName) { evictKey = key; break; }
|
|
9019
|
+
}
|
|
9020
|
+
if (evictKey == null) break;
|
|
9021
|
+
const cached = this._keepAliveCache.get(evictKey);
|
|
9022
|
+
this._keepAliveCache.delete(evictKey);
|
|
9023
|
+
try { cached.instance.destroy(); } catch { /* swallow */ }
|
|
9024
|
+
if (cached.container && cached.container.parentNode) cached.container.remove();
|
|
9025
|
+
}
|
|
9026
|
+
}
|
|
9027
|
+
|
|
8913
9028
|
destroy() {
|
|
8914
9029
|
// Remove window/document event listeners to prevent memory leaks
|
|
8915
9030
|
if (this._onNavEvent) {
|
|
@@ -9041,6 +9156,7 @@ function getRouter() {
|
|
|
9041
9156
|
|
|
9042
9157
|
|
|
9043
9158
|
|
|
9159
|
+
|
|
9044
9160
|
class Store {
|
|
9045
9161
|
constructor(config = {}) {
|
|
9046
9162
|
this._subscribers = new Map(); // key → Set<fn>
|
|
@@ -9059,7 +9175,7 @@ class Store {
|
|
|
9059
9175
|
|
|
9060
9176
|
// Store initial state for reset
|
|
9061
9177
|
const initial = typeof config.state === 'function' ? config.state() : { ...(config.state || {}) };
|
|
9062
|
-
this._initialState =
|
|
9178
|
+
this._initialState = deepClone(initial);
|
|
9063
9179
|
|
|
9064
9180
|
this.state = reactive(initial, (key, value, old) => {
|
|
9065
9181
|
if (this._batching) {
|
|
@@ -9121,7 +9237,7 @@ class Store {
|
|
|
9121
9237
|
* Save a snapshot for undo. Call before making changes you want to be undoable.
|
|
9122
9238
|
*/
|
|
9123
9239
|
checkpoint() {
|
|
9124
|
-
const snap =
|
|
9240
|
+
const snap = deepClone(this.state.__raw || this.state);
|
|
9125
9241
|
this._undoStack.push(snap);
|
|
9126
9242
|
if (this._undoStack.length > this._maxUndo) {
|
|
9127
9243
|
this._undoStack.splice(0, this._undoStack.length - this._maxUndo);
|
|
@@ -9135,7 +9251,7 @@ class Store {
|
|
|
9135
9251
|
*/
|
|
9136
9252
|
undo() {
|
|
9137
9253
|
if (this._undoStack.length === 0) return false;
|
|
9138
|
-
const current =
|
|
9254
|
+
const current = deepClone(this.state.__raw || this.state);
|
|
9139
9255
|
this._redoStack.push(current);
|
|
9140
9256
|
const prev = this._undoStack.pop();
|
|
9141
9257
|
this.replaceState(prev);
|
|
@@ -9148,7 +9264,7 @@ class Store {
|
|
|
9148
9264
|
*/
|
|
9149
9265
|
redo() {
|
|
9150
9266
|
if (this._redoStack.length === 0) return false;
|
|
9151
|
-
const current =
|
|
9267
|
+
const current = deepClone(this.state.__raw || this.state);
|
|
9152
9268
|
this._undoStack.push(current);
|
|
9153
9269
|
const next = this._redoStack.pop();
|
|
9154
9270
|
this.replaceState(next);
|
|
@@ -9238,10 +9354,16 @@ class Store {
|
|
|
9238
9354
|
}
|
|
9239
9355
|
|
|
9240
9356
|
/**
|
|
9241
|
-
* Get current state snapshot (plain object)
|
|
9357
|
+
* Get current state snapshot (plain object).
|
|
9358
|
+
*
|
|
9359
|
+
* Pass `{ clone: false }` to skip the structuredClone pass when the caller
|
|
9360
|
+
* treats state as read-only. Big perf win for serialise/inspect paths that
|
|
9361
|
+
* never mutate the returned value. Default remains a defensive deep copy.
|
|
9242
9362
|
*/
|
|
9243
|
-
snapshot() {
|
|
9244
|
-
|
|
9363
|
+
snapshot(opts) {
|
|
9364
|
+
const raw = this.state.__raw || this.state;
|
|
9365
|
+
if (opts && opts.clone === false) return raw;
|
|
9366
|
+
return deepClone(raw);
|
|
9245
9367
|
}
|
|
9246
9368
|
|
|
9247
9369
|
/**
|
|
@@ -9274,7 +9396,7 @@ class Store {
|
|
|
9274
9396
|
* Reset state to initial values. If no argument, resets to the original state.
|
|
9275
9397
|
*/
|
|
9276
9398
|
reset(initialState) {
|
|
9277
|
-
this.replaceState(initialState ||
|
|
9399
|
+
this.replaceState(initialState || deepClone(this._initialState));
|
|
9278
9400
|
this._history = [];
|
|
9279
9401
|
this._undoStack = [];
|
|
9280
9402
|
this._redoStack = [];
|
|
@@ -9587,25 +9709,35 @@ const http = {
|
|
|
9587
9709
|
// ---------------------------------------------------------------------------
|
|
9588
9710
|
|
|
9589
9711
|
/**
|
|
9590
|
-
* Debounce - delays execution until after `ms` of inactivity
|
|
9712
|
+
* Debounce - delays execution until after `ms` of inactivity.
|
|
9713
|
+
* Optional `{ signal }` aborts any pending invocation and prevents future ones.
|
|
9591
9714
|
*/
|
|
9592
|
-
function debounce(fn, ms = 250) {
|
|
9715
|
+
function debounce(fn, ms = 250, opts = {}) {
|
|
9593
9716
|
let timer;
|
|
9717
|
+
const signal = opts.signal;
|
|
9594
9718
|
const debounced = (...args) => {
|
|
9719
|
+
if (signal && signal.aborted) return;
|
|
9595
9720
|
clearTimeout(timer);
|
|
9596
9721
|
timer = setTimeout(() => fn(...args), ms);
|
|
9597
9722
|
};
|
|
9598
9723
|
debounced.cancel = () => clearTimeout(timer);
|
|
9724
|
+
if (signal) {
|
|
9725
|
+
if (signal.aborted) debounced.cancel();
|
|
9726
|
+
else signal.addEventListener('abort', debounced.cancel, { once: true });
|
|
9727
|
+
}
|
|
9599
9728
|
return debounced;
|
|
9600
9729
|
}
|
|
9601
9730
|
|
|
9602
9731
|
/**
|
|
9603
|
-
* Throttle - limits execution to once per `ms
|
|
9732
|
+
* Throttle - limits execution to once per `ms`.
|
|
9733
|
+
* Optional `{ signal }` aborts any pending trailing call and prevents future ones.
|
|
9604
9734
|
*/
|
|
9605
|
-
function throttle(fn, ms = 250) {
|
|
9735
|
+
function throttle(fn, ms = 250, opts = {}) {
|
|
9606
9736
|
let last = 0;
|
|
9607
9737
|
let timer;
|
|
9608
|
-
|
|
9738
|
+
const signal = opts.signal;
|
|
9739
|
+
const throttled = (...args) => {
|
|
9740
|
+
if (signal && signal.aborted) return;
|
|
9609
9741
|
const now = Date.now();
|
|
9610
9742
|
const remaining = ms - (now - last);
|
|
9611
9743
|
clearTimeout(timer);
|
|
@@ -9616,6 +9748,12 @@ function throttle(fn, ms = 250) {
|
|
|
9616
9748
|
timer = setTimeout(() => { last = Date.now(); fn(...args); }, remaining);
|
|
9617
9749
|
}
|
|
9618
9750
|
};
|
|
9751
|
+
throttled.cancel = () => clearTimeout(timer);
|
|
9752
|
+
if (signal) {
|
|
9753
|
+
if (signal.aborted) throttled.cancel();
|
|
9754
|
+
else signal.addEventListener('abort', throttled.cancel, { once: true });
|
|
9755
|
+
}
|
|
9756
|
+
return throttled;
|
|
9619
9757
|
}
|
|
9620
9758
|
|
|
9621
9759
|
/**
|
|
@@ -9940,6 +10078,9 @@ function chunk(arr, size) {
|
|
|
9940
10078
|
}
|
|
9941
10079
|
|
|
9942
10080
|
function groupBy(arr, keyFn) {
|
|
10081
|
+
// Prefer the native Object.groupBy (Node 21+, all evergreens) when available -
|
|
10082
|
+
// it returns a null-prototype object which is safer for use as a lookup map.
|
|
10083
|
+
if (typeof Object.groupBy === 'function') return Object.groupBy(arr, keyFn);
|
|
9943
10084
|
const result = {};
|
|
9944
10085
|
for (const item of arr) {
|
|
9945
10086
|
const k = keyFn(item);
|
|
@@ -10308,9 +10449,9 @@ $.TurnError = TurnError;
|
|
|
10308
10449
|
$.E2eeError = E2eeError;
|
|
10309
10450
|
|
|
10310
10451
|
// --- Meta ------------------------------------------------------------------
|
|
10311
|
-
$.version = '1.2.
|
|
10312
|
-
$.libSize = '~
|
|
10313
|
-
$.unitTests = {"passed":
|
|
10452
|
+
$.version = '1.2.3';
|
|
10453
|
+
$.libSize = '~130 KB';
|
|
10454
|
+
$.unitTests = {"passed":2348,"failed":0,"total":2534,"suites":620,"duration":8205,"ok":true};
|
|
10314
10455
|
$.meta = {}; // populated at build time by CLI bundler
|
|
10315
10456
|
|
|
10316
10457
|
// --- Environment detection -------------------------------------------------
|