zero-query 0.8.9 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/cli/commands/bundle.js +15 -2
- package/cli/commands/dev/devtools/js/core.js +16 -2
- package/cli/commands/dev/devtools/js/elements.js +4 -1
- package/cli/commands/dev/overlay.js +20 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +184 -44
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +6 -2
- package/package.json +1 -1
- package/src/component.js +28 -7
- package/src/core.js +62 -12
- package/src/diff.js +11 -5
- package/src/expression.js +1 -0
- package/src/http.js +17 -1
- package/src/reactive.js +8 -2
- package/src/router.js +37 -8
- package/src/ssr.js +1 -1
- package/src/store.js +5 -0
- package/src/utils.js +12 -6
- package/tests/cli.test.js +456 -0
- package/tests/component.test.js +1387 -0
- package/tests/core.test.js +893 -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 +186 -0
- package/types/store.d.ts +3 -0
package/README.md
CHANGED
|
@@ -262,14 +262,13 @@ location / {
|
|
|
262
262
|
| `$.router` `$.getRouter` | SPA router |
|
|
263
263
|
| `$.store` `$.getStore` | State management |
|
|
264
264
|
| `$.http` `$.get` `$.post` `$.put` `$.patch` `$.delete` | HTTP client |
|
|
265
|
-
| `$.reactive` `$.signal` `$.computed` `$.effect` | Reactive primitives |
|
|
265
|
+
| `$.reactive` `$.Signal` `$.signal` `$.computed` `$.effect` | Reactive primitives |
|
|
266
266
|
| `$.debounce` `$.throttle` `$.pipe` `$.once` `$.sleep` | Function utils |
|
|
267
267
|
| `$.escapeHtml` `$.html` `$.trust` `$.uuid` `$.camelCase` `$.kebabCase` | String utils |
|
|
268
268
|
| `$.deepClone` `$.deepMerge` `$.isEqual` | Object utils |
|
|
269
269
|
| `$.param` `$.parseQuery` | URL utils |
|
|
270
270
|
| `$.storage` `$.session` | Storage wrappers |
|
|
271
|
-
| `$.bus` | Event bus |
|
|
272
|
-
| `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
|
|
271
|
+
| `$.bus` | Event bus || `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.validate` | Error handling || `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
|
|
273
272
|
| `$.meta` | Build metadata (populated by CLI bundler) |
|
|
274
273
|
| `$.noConflict` | Release `$` global |
|
|
275
274
|
|
package/cli/commands/bundle.js
CHANGED
|
@@ -35,12 +35,25 @@ function resolveImport(specifier, fromFile) {
|
|
|
35
35
|
|
|
36
36
|
/** Extract import specifiers from a source file. */
|
|
37
37
|
function extractImports(code) {
|
|
38
|
+
// Only scan the import preamble (before the first top-level `export`)
|
|
39
|
+
// so that code examples inside exported template strings are not
|
|
40
|
+
// mistaken for real imports.
|
|
41
|
+
const exportStart = code.search(/^export\b/m);
|
|
42
|
+
const preamble = exportStart > -1 ? code.slice(0, exportStart) : code;
|
|
43
|
+
|
|
38
44
|
const specifiers = [];
|
|
39
45
|
let m;
|
|
40
46
|
const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
41
|
-
while ((m = fromRe.exec(
|
|
47
|
+
while ((m = fromRe.exec(preamble)) !== null) specifiers.push(m[1]);
|
|
42
48
|
const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
|
|
43
|
-
while ((m = sideRe.exec(
|
|
49
|
+
while ((m = sideRe.exec(preamble)) !== null) {
|
|
50
|
+
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Also capture re-exports anywhere in the file:
|
|
54
|
+
// export { x } from '...' export * from '...'
|
|
55
|
+
const reExportRe = /^\s*export\s+(?:\{[^}]*\}|\*)\s*from\s+['"]([^'"]+)['"]/gm;
|
|
56
|
+
while ((m = reExportRe.exec(code)) !== null) {
|
|
44
57
|
if (!specifiers.includes(m[1])) specifiers.push(m[1]);
|
|
45
58
|
}
|
|
46
59
|
return specifiers;
|
|
@@ -372,8 +372,22 @@ function connectToTarget() {
|
|
|
372
372
|
// Periodic refresh for components + perf (fast when tab is visible)
|
|
373
373
|
setInterval(function() {
|
|
374
374
|
if (!isConnected()) {
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
// Retry connection — opener may be mid-mutation, not truly gone
|
|
376
|
+
try {
|
|
377
|
+
if (mode === 'popup' && window.opener && !window.opener.closed) {
|
|
378
|
+
targetWin = window.opener;
|
|
379
|
+
targetDoc = targetWin.document;
|
|
380
|
+
document.getElementById('disconnected').style.display = 'none';
|
|
381
|
+
} else if (iframe && iframe.contentWindow) {
|
|
382
|
+
targetWin = iframe.contentWindow;
|
|
383
|
+
targetDoc = targetWin.document;
|
|
384
|
+
document.getElementById('disconnected').style.display = 'none';
|
|
385
|
+
}
|
|
386
|
+
} catch(e) {}
|
|
387
|
+
if (!isConnected()) {
|
|
388
|
+
document.getElementById('disconnected').style.display = 'flex';
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
377
391
|
}
|
|
378
392
|
// Keep targetDoc fresh — the opener may have reloaded (live-reload)
|
|
379
393
|
try {
|
|
@@ -359,6 +359,9 @@ function startObserver() {
|
|
|
359
359
|
if (!target) continue;
|
|
360
360
|
// Skip mutations caused by the devtools highlight overlay
|
|
361
361
|
if (target.id === '__zq_highlight' || target.id === '__zq_error_overlay' || target.id === '__zq_devbar') continue;
|
|
362
|
+
// Skip virtual-scroll hydration/dehydration churn (docs lazy chunks)
|
|
363
|
+
if (target.classList && target.classList.contains('docs-lazy-chunk')) continue;
|
|
364
|
+
if (target.closest && target.closest('.docs-lazy-chunk:not(.hydrated)')) continue;
|
|
362
365
|
var isHighlightMutation = false;
|
|
363
366
|
if (m.addedNodes) { for (var k = 0; k < m.addedNodes.length; k++) { if (m.addedNodes[k].id === '__zq_highlight') { isHighlightMutation = true; break; } } }
|
|
364
367
|
if (!isHighlightMutation && m.removedNodes) { for (var k = 0; k < m.removedNodes.length; k++) { if (m.removedNodes[k].id === '__zq_highlight') { isHighlightMutation = true; break; } } }
|
|
@@ -402,7 +405,7 @@ function startObserver() {
|
|
|
402
405
|
// Clear changed paths after applying
|
|
403
406
|
changedPaths = {};
|
|
404
407
|
mutatedPaths = {};
|
|
405
|
-
},
|
|
408
|
+
}, 300);
|
|
406
409
|
});
|
|
407
410
|
observer.observe(targetDoc.documentElement, {
|
|
408
411
|
childList: true,
|
|
@@ -617,6 +617,7 @@ const OVERLAY_SCRIPT = `<script>
|
|
|
617
617
|
// =====================================================================
|
|
618
618
|
var devBar;
|
|
619
619
|
var __zqBarExpanded = false;
|
|
620
|
+
try { __zqBarExpanded = localStorage.getItem('__zq_bar_expanded') === '1'; } catch(e) {}
|
|
620
621
|
var __zqRouteColors = {
|
|
621
622
|
navigate: { bg: 'rgba(63,185,80,0.12)', fg: '#3fb950' },
|
|
622
623
|
replace: { bg: 'rgba(210,153,34,0.12)', fg: '#d29922' },
|
|
@@ -671,8 +672,26 @@ const OVERLAY_SCRIPT = `<script>
|
|
|
671
672
|
'">×</button>';
|
|
672
673
|
|
|
673
674
|
document.body.appendChild(devBar);
|
|
675
|
+
|
|
676
|
+
// If previously expanded, restore that state immediately
|
|
677
|
+
if (__zqBarExpanded) {
|
|
678
|
+
var items = devBar.querySelectorAll('.__zq_ex');
|
|
679
|
+
var btn = document.getElementById('__zq_bar_toggle');
|
|
680
|
+
if (btn) {
|
|
681
|
+
btn.innerHTML = '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
|
|
682
|
+
btn.title = 'Collapse toolbar';
|
|
683
|
+
}
|
|
684
|
+
for (var i = 0; i < items.length; i++) {
|
|
685
|
+
items[i].style.display = 'inline';
|
|
686
|
+
items[i].style.transform = 'scale(1)';
|
|
687
|
+
items[i].style.opacity = '1';
|
|
688
|
+
}
|
|
689
|
+
}
|
|
674
690
|
updateDevBar();
|
|
675
691
|
|
|
692
|
+
// Live-poll stats so numbers stay current without waiting for events
|
|
693
|
+
setInterval(updateDevBar, 1000);
|
|
694
|
+
|
|
676
695
|
// Check if we're inside a devtools split-view iframe
|
|
677
696
|
function isInSplitFrame() {
|
|
678
697
|
try { return window.parent !== window && window.parent.document.getElementById('app-frame'); }
|
|
@@ -717,6 +736,7 @@ const OVERLAY_SCRIPT = `<script>
|
|
|
717
736
|
// Expand / collapse toggle
|
|
718
737
|
document.getElementById('__zq_bar_toggle').addEventListener('click', function() {
|
|
719
738
|
__zqBarExpanded = !__zqBarExpanded;
|
|
739
|
+
try { localStorage.setItem('__zq_bar_expanded', __zqBarExpanded ? '1' : '0'); } catch(e) {}
|
|
720
740
|
var items = devBar.querySelectorAll('.__zq_ex');
|
|
721
741
|
var btn = this;
|
|
722
742
|
if (__zqBarExpanded) {
|
package/dist/zquery.dist.zip
CHANGED
|
Binary file
|
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.
|
|
2
|
+
* zQuery (zeroQuery) v0.9.1
|
|
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);
|
|
@@ -1780,6 +1836,7 @@ function _isSafeAccess(obj, prop) {
|
|
|
1780
1836
|
const BLOCKED = new Set([
|
|
1781
1837
|
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
1782
1838
|
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
1839
|
+
'call', 'apply', 'bind',
|
|
1783
1840
|
]);
|
|
1784
1841
|
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
1785
1842
|
|
|
@@ -2297,8 +2354,11 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
2297
2354
|
if (!lisSet.has(i)) {
|
|
2298
2355
|
oldParent.insertBefore(oldNode, cursor);
|
|
2299
2356
|
}
|
|
2357
|
+
// Capture next sibling BEFORE _morphNode — if _morphNode calls
|
|
2358
|
+
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
2359
|
+
const nextSib = oldNode.nextSibling;
|
|
2300
2360
|
_morphNode(oldParent, oldNode, newNode);
|
|
2301
|
-
cursor =
|
|
2361
|
+
cursor = nextSib;
|
|
2302
2362
|
} else {
|
|
2303
2363
|
// Insert new node
|
|
2304
2364
|
const clone = newNode.cloneNode(true);
|
|
@@ -2476,10 +2536,13 @@ function _morphAttributes(oldEl, newEl) {
|
|
|
2476
2536
|
}
|
|
2477
2537
|
}
|
|
2478
2538
|
|
|
2479
|
-
// Remove stale attributes
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2539
|
+
// Remove stale attributes — snapshot names first because oldAttrs
|
|
2540
|
+
// is a live NamedNodeMap that mutates on removeAttribute().
|
|
2541
|
+
const oldNames = new Array(oldLen);
|
|
2542
|
+
for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
|
|
2543
|
+
for (let i = oldNames.length - 1; i >= 0; i--) {
|
|
2544
|
+
if (!newNames.has(oldNames[i])) {
|
|
2545
|
+
oldEl.removeAttribute(oldNames[i]);
|
|
2483
2546
|
}
|
|
2484
2547
|
}
|
|
2485
2548
|
}
|
|
@@ -2801,7 +2864,7 @@ class Component {
|
|
|
2801
2864
|
if (!watchers) return;
|
|
2802
2865
|
for (const [key, handler] of Object.entries(watchers)) {
|
|
2803
2866
|
// 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 + '.')
|
|
2867
|
+
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.')) {
|
|
2805
2868
|
const currentVal = _getPath(this.state.__raw || this.state, key);
|
|
2806
2869
|
const prevVal = this._prevWatchValues?.[key];
|
|
2807
2870
|
if (currentVal !== prevVal) {
|
|
@@ -2950,20 +3013,26 @@ class Component {
|
|
|
2950
3013
|
if (!this._mounted && combinedStyles) {
|
|
2951
3014
|
const scopeAttr = `z-s${this._uid}`;
|
|
2952
3015
|
this._el.setAttribute(scopeAttr, '');
|
|
2953
|
-
let
|
|
3016
|
+
let noScopeDepth = 0; // brace depth at which a no-scope @-rule started (0 = none active)
|
|
3017
|
+
let braceDepth = 0; // overall brace depth
|
|
2954
3018
|
const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
|
|
2955
3019
|
if (match === '}') {
|
|
2956
|
-
if (
|
|
3020
|
+
if (noScopeDepth > 0 && braceDepth <= noScopeDepth) noScopeDepth = 0;
|
|
3021
|
+
braceDepth--;
|
|
2957
3022
|
return match;
|
|
2958
3023
|
}
|
|
3024
|
+
braceDepth++;
|
|
2959
3025
|
const trimmed = selector.trim();
|
|
2960
|
-
// Don't scope @-rules
|
|
3026
|
+
// Don't scope @-rules themselves
|
|
2961
3027
|
if (trimmed.startsWith('@')) {
|
|
2962
|
-
|
|
3028
|
+
// @keyframes and @font-face contain non-selector content — skip scoping inside them
|
|
3029
|
+
if (/^@(keyframes|font-face)\b/.test(trimmed)) {
|
|
3030
|
+
noScopeDepth = braceDepth;
|
|
3031
|
+
}
|
|
2963
3032
|
return match;
|
|
2964
3033
|
}
|
|
2965
|
-
//
|
|
2966
|
-
if (
|
|
3034
|
+
// Inside @keyframes or @font-face — don't scope inner rules
|
|
3035
|
+
if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
|
|
2967
3036
|
return match;
|
|
2968
3037
|
}
|
|
2969
3038
|
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
@@ -3591,6 +3660,21 @@ class Component {
|
|
|
3591
3660
|
this._listeners = [];
|
|
3592
3661
|
this._delegatedEvents = null;
|
|
3593
3662
|
this._eventBindings = null;
|
|
3663
|
+
// Clear any pending debounce/throttle timers to prevent stale closures.
|
|
3664
|
+
// Timers are keyed by individual child elements, so iterate all descendants.
|
|
3665
|
+
const allEls = this._el.querySelectorAll('*');
|
|
3666
|
+
allEls.forEach(child => {
|
|
3667
|
+
const dTimers = _debounceTimers.get(child);
|
|
3668
|
+
if (dTimers) {
|
|
3669
|
+
for (const key in dTimers) clearTimeout(dTimers[key]);
|
|
3670
|
+
_debounceTimers.delete(child);
|
|
3671
|
+
}
|
|
3672
|
+
const tTimers = _throttleTimers.get(child);
|
|
3673
|
+
if (tTimers) {
|
|
3674
|
+
for (const key in tTimers) clearTimeout(tTimers[key]);
|
|
3675
|
+
_throttleTimers.delete(child);
|
|
3676
|
+
}
|
|
3677
|
+
});
|
|
3594
3678
|
if (this._styleEl) this._styleEl.remove();
|
|
3595
3679
|
_instances.delete(this._el);
|
|
3596
3680
|
this._el.innerHTML = '';
|
|
@@ -3886,6 +3970,23 @@ function style(urls, opts = {}) {
|
|
|
3886
3970
|
// Unique marker on history.state to identify zQuery-managed entries
|
|
3887
3971
|
const _ZQ_STATE_KEY = '__zq';
|
|
3888
3972
|
|
|
3973
|
+
/**
|
|
3974
|
+
* Shallow-compare two flat objects (for params / query comparison).
|
|
3975
|
+
* Avoids JSON.stringify overhead on every navigation.
|
|
3976
|
+
*/
|
|
3977
|
+
function _shallowEqual(a, b) {
|
|
3978
|
+
if (a === b) return true;
|
|
3979
|
+
if (!a || !b) return false;
|
|
3980
|
+
const keysA = Object.keys(a);
|
|
3981
|
+
const keysB = Object.keys(b);
|
|
3982
|
+
if (keysA.length !== keysB.length) return false;
|
|
3983
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
3984
|
+
const k = keysA[i];
|
|
3985
|
+
if (a[k] !== b[k]) return false;
|
|
3986
|
+
}
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3889
3990
|
class Router {
|
|
3890
3991
|
constructor(config = {}) {
|
|
3891
3992
|
this._el = null;
|
|
@@ -3933,11 +4034,12 @@ class Router {
|
|
|
3933
4034
|
config.routes.forEach(r => this.add(r));
|
|
3934
4035
|
}
|
|
3935
4036
|
|
|
3936
|
-
// Listen for navigation
|
|
4037
|
+
// Listen for navigation — store handler references for cleanup in destroy()
|
|
3937
4038
|
if (this._mode === 'hash') {
|
|
3938
|
-
|
|
4039
|
+
this._onNavEvent = () => this._resolve();
|
|
4040
|
+
window.addEventListener('hashchange', this._onNavEvent);
|
|
3939
4041
|
} else {
|
|
3940
|
-
|
|
4042
|
+
this._onNavEvent = (e) => {
|
|
3941
4043
|
// Check for substate pop first — if a listener handles it, don't route
|
|
3942
4044
|
const st = e.state;
|
|
3943
4045
|
if (st && st[_ZQ_STATE_KEY] === 'substate') {
|
|
@@ -3951,11 +4053,12 @@ class Router {
|
|
|
3951
4053
|
this._fireSubstate(null, null, 'reset');
|
|
3952
4054
|
}
|
|
3953
4055
|
this._resolve();
|
|
3954
|
-
}
|
|
4056
|
+
};
|
|
4057
|
+
window.addEventListener('popstate', this._onNavEvent);
|
|
3955
4058
|
}
|
|
3956
4059
|
|
|
3957
4060
|
// Intercept link clicks for SPA navigation
|
|
3958
|
-
|
|
4061
|
+
this._onLinkClick = (e) => {
|
|
3959
4062
|
// Don't intercept modified clicks (Ctrl/Cmd+click = new tab)
|
|
3960
4063
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
3961
4064
|
const link = e.target.closest('[z-link]');
|
|
@@ -3977,7 +4080,8 @@ class Router {
|
|
|
3977
4080
|
const scrollBehavior = link.getAttribute('z-to-top') || 'instant';
|
|
3978
4081
|
window.scrollTo({ top: 0, behavior: scrollBehavior });
|
|
3979
4082
|
}
|
|
3980
|
-
}
|
|
4083
|
+
};
|
|
4084
|
+
document.addEventListener('click', this._onLinkClick);
|
|
3981
4085
|
|
|
3982
4086
|
// Initial resolve
|
|
3983
4087
|
if (this._el) {
|
|
@@ -4349,8 +4453,8 @@ class Router {
|
|
|
4349
4453
|
// with the same params, skip the full destroy/mount cycle and just
|
|
4350
4454
|
// update props. This prevents flashing and unnecessary DOM churn.
|
|
4351
4455
|
if (from && this._instance && matched.component === from.route.component) {
|
|
4352
|
-
const sameParams =
|
|
4353
|
-
const sameQuery =
|
|
4456
|
+
const sameParams = _shallowEqual(params, from.params);
|
|
4457
|
+
const sameQuery = _shallowEqual(query, from.query);
|
|
4354
4458
|
if (sameParams && sameQuery) {
|
|
4355
4459
|
// Identical navigation — nothing to do
|
|
4356
4460
|
return;
|
|
@@ -4447,6 +4551,15 @@ class Router {
|
|
|
4447
4551
|
// --- Destroy -------------------------------------------------------------
|
|
4448
4552
|
|
|
4449
4553
|
destroy() {
|
|
4554
|
+
// Remove window/document event listeners to prevent memory leaks
|
|
4555
|
+
if (this._onNavEvent) {
|
|
4556
|
+
window.removeEventListener(this._mode === 'hash' ? 'hashchange' : 'popstate', this._onNavEvent);
|
|
4557
|
+
this._onNavEvent = null;
|
|
4558
|
+
}
|
|
4559
|
+
if (this._onLinkClick) {
|
|
4560
|
+
document.removeEventListener('click', this._onLinkClick);
|
|
4561
|
+
this._onLinkClick = null;
|
|
4562
|
+
}
|
|
4450
4563
|
if (this._instance) this._instance.destroy();
|
|
4451
4564
|
this._listeners.clear();
|
|
4452
4565
|
this._substateListeners = [];
|
|
@@ -4509,6 +4622,7 @@ class Store {
|
|
|
4509
4622
|
this._getters = config.getters || {};
|
|
4510
4623
|
this._middleware = [];
|
|
4511
4624
|
this._history = []; // action log
|
|
4625
|
+
this._maxHistory = config.maxHistory || 1000;
|
|
4512
4626
|
this._debug = config.debug || false;
|
|
4513
4627
|
|
|
4514
4628
|
// Create reactive state
|
|
@@ -4568,6 +4682,10 @@ class Store {
|
|
|
4568
4682
|
try {
|
|
4569
4683
|
const result = action(this.state, ...args);
|
|
4570
4684
|
this._history.push({ action: name, args, timestamp: Date.now() });
|
|
4685
|
+
// Cap history to prevent unbounded memory growth
|
|
4686
|
+
if (this._history.length > this._maxHistory) {
|
|
4687
|
+
this._history.splice(0, this._history.length - this._maxHistory);
|
|
4688
|
+
}
|
|
4571
4689
|
return result;
|
|
4572
4690
|
} catch (err) {
|
|
4573
4691
|
reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
|
|
@@ -4725,9 +4843,25 @@ async function request(method, url, data, options = {}) {
|
|
|
4725
4843
|
|
|
4726
4844
|
// Timeout via AbortController
|
|
4727
4845
|
const controller = new AbortController();
|
|
4728
|
-
fetchOpts.signal = options.signal || controller.signal;
|
|
4729
4846
|
const timeout = options.timeout ?? _config.timeout;
|
|
4730
4847
|
let timer;
|
|
4848
|
+
// Combine user signal with internal controller for proper timeout support
|
|
4849
|
+
if (options.signal) {
|
|
4850
|
+
// If AbortSignal.any is available, combine both signals
|
|
4851
|
+
if (typeof AbortSignal.any === 'function') {
|
|
4852
|
+
fetchOpts.signal = AbortSignal.any([options.signal, controller.signal]);
|
|
4853
|
+
} else {
|
|
4854
|
+
// Fallback: forward user signal's abort to our controller
|
|
4855
|
+
fetchOpts.signal = controller.signal;
|
|
4856
|
+
if (options.signal.aborted) {
|
|
4857
|
+
controller.abort(options.signal.reason);
|
|
4858
|
+
} else {
|
|
4859
|
+
options.signal.addEventListener('abort', () => controller.abort(options.signal.reason), { once: true });
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
} else {
|
|
4863
|
+
fetchOpts.signal = controller.signal;
|
|
4864
|
+
}
|
|
4731
4865
|
if (timeout > 0) {
|
|
4732
4866
|
timer = setTimeout(() => controller.abort(), timeout);
|
|
4733
4867
|
}
|
|
@@ -4991,16 +5125,21 @@ function deepClone(obj) {
|
|
|
4991
5125
|
* Deep merge objects
|
|
4992
5126
|
*/
|
|
4993
5127
|
function deepMerge(target, ...sources) {
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5128
|
+
const seen = new WeakSet();
|
|
5129
|
+
function merge(tgt, src) {
|
|
5130
|
+
if (seen.has(src)) return tgt;
|
|
5131
|
+
seen.add(src);
|
|
5132
|
+
for (const key of Object.keys(src)) {
|
|
5133
|
+
if (src[key] && typeof src[key] === 'object' && !Array.isArray(src[key])) {
|
|
5134
|
+
if (!tgt[key] || typeof tgt[key] !== 'object') tgt[key] = {};
|
|
5135
|
+
merge(tgt[key], src[key]);
|
|
4999
5136
|
} else {
|
|
5000
|
-
|
|
5137
|
+
tgt[key] = src[key];
|
|
5001
5138
|
}
|
|
5002
5139
|
}
|
|
5140
|
+
return tgt;
|
|
5003
5141
|
}
|
|
5142
|
+
for (const source of sources) merge(target, source);
|
|
5004
5143
|
return target;
|
|
5005
5144
|
}
|
|
5006
5145
|
|
|
@@ -5011,6 +5150,7 @@ function isEqual(a, b) {
|
|
|
5011
5150
|
if (a === b) return true;
|
|
5012
5151
|
if (typeof a !== typeof b) return false;
|
|
5013
5152
|
if (typeof a !== 'object' || a === null || b === null) return false;
|
|
5153
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
5014
5154
|
const keysA = Object.keys(a);
|
|
5015
5155
|
const keysB = Object.keys(b);
|
|
5016
5156
|
if (keysA.length !== keysB.length) return false;
|
|
@@ -5264,8 +5404,8 @@ $.guardCallback = guardCallback;
|
|
|
5264
5404
|
$.validate = validate;
|
|
5265
5405
|
|
|
5266
5406
|
// --- Meta ------------------------------------------------------------------
|
|
5267
|
-
$.version = '0.
|
|
5268
|
-
$.libSize = '~
|
|
5407
|
+
$.version = '0.9.1';
|
|
5408
|
+
$.libSize = '~92 KB';
|
|
5269
5409
|
$.meta = {}; // populated at build time by CLI bundler
|
|
5270
5410
|
|
|
5271
5411
|
$.noConflict = () => {
|