zero-query 0.7.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -27
- package/cli/commands/build.js +110 -1
- package/cli/commands/bundle.js +107 -22
- package/cli/commands/create.js +1 -1
- package/cli/commands/dev/devtools/index.js +56 -0
- package/cli/commands/dev/devtools/js/components.js +49 -0
- package/cli/commands/dev/devtools/js/core.js +409 -0
- package/cli/commands/dev/devtools/js/elements.js +413 -0
- package/cli/commands/dev/devtools/js/network.js +166 -0
- package/cli/commands/dev/devtools/js/performance.js +73 -0
- package/cli/commands/dev/devtools/js/router.js +105 -0
- package/cli/commands/dev/devtools/js/source.js +132 -0
- package/cli/commands/dev/devtools/js/stats.js +35 -0
- package/cli/commands/dev/devtools/js/tabs.js +79 -0
- package/cli/commands/dev/devtools/panel.html +95 -0
- package/cli/commands/dev/devtools/styles.css +244 -0
- package/cli/commands/dev/index.js +28 -3
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +377 -0
- package/cli/commands/dev/server.js +8 -0
- package/cli/commands/dev/watcher.js +26 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +1 -1
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/app/components/home.js +137 -0
- package/cli/scaffold/{scripts → app}/routes.js +1 -1
- package/cli/scaffold/{scripts → app}/store.js +6 -6
- package/cli/scaffold/assets/.gitkeep +0 -0
- package/cli/scaffold/{styles/styles.css → global.css} +3 -2
- package/cli/scaffold/index.html +11 -11
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +746 -134
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +11 -9
- package/index.js +15 -10
- package/package.json +3 -2
- package/src/component.js +161 -48
- package/src/core.js +57 -11
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +195 -6
- package/tests/component.test.js +582 -0
- package/tests/core.test.js +251 -0
- package/tests/diff.test.js +333 -2
- package/tests/expression.test.js +148 -0
- package/tests/http.test.js +108 -0
- package/tests/reactive.test.js +148 -0
- package/tests/router.test.js +317 -0
- package/tests/store.test.js +126 -0
- package/tests/utils.test.js +161 -2
- package/types/collection.d.ts +17 -2
- package/types/component.d.ts +7 -0
- package/types/misc.d.ts +13 -0
- package/types/router.d.ts +30 -1
- package/cli/commands/dev.old.js +0 -520
- package/cli/scaffold/scripts/components/home.js +0 -137
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +0 -0
- /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
- /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "node cli/index.js build",
|
|
23
23
|
"dev": "node cli/index.js dev zquery-website",
|
|
24
|
+
"dev:bundle": "node cli/index.js dev zquery-website --bundle",
|
|
24
25
|
"dev-lib": "node cli/index.js build --watch",
|
|
25
26
|
"bundle": "node cli/index.js bundle",
|
|
26
|
-
"bundle:app": "node cli/index.js bundle zquery-website
|
|
27
|
+
"bundle:app": "node cli/index.js bundle zquery-website",
|
|
27
28
|
"test": "vitest run",
|
|
28
29
|
"test:watch": "vitest"
|
|
29
30
|
},
|
package/src/component.js
CHANGED
|
@@ -339,7 +339,7 @@ class Component {
|
|
|
339
339
|
// Normalize items → [{id, label}, …]
|
|
340
340
|
def._pages = (p.items || []).map(item => {
|
|
341
341
|
if (typeof item === 'string') return { id: item, label: _titleCase(item) };
|
|
342
|
-
return {
|
|
342
|
+
return { ...item, label: item.label || _titleCase(item.id) };
|
|
343
343
|
});
|
|
344
344
|
|
|
345
345
|
// Build URL map for lazy per-page loading.
|
|
@@ -472,6 +472,11 @@ class Component {
|
|
|
472
472
|
html = '';
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
// Pre-expand z-html and z-text at string level so the morph engine
|
|
476
|
+
// can diff their content properly (instead of clearing + re-injecting
|
|
477
|
+
// on every re-render). Same pattern as z-for: parse → evaluate → serialize.
|
|
478
|
+
html = this._expandContentDirectives(html);
|
|
479
|
+
|
|
475
480
|
// -- Slot distribution ----------------------------------------
|
|
476
481
|
// Replace <slot> elements with captured slot content from parent.
|
|
477
482
|
// <slot> → default slot content
|
|
@@ -561,8 +566,10 @@ class Component {
|
|
|
561
566
|
|
|
562
567
|
// Update DOM via morphing (diffing) — preserves unchanged nodes
|
|
563
568
|
// First render uses innerHTML for speed; subsequent renders morph.
|
|
569
|
+
const _renderStart = typeof window !== 'undefined' && (window.__zqMorphHook || window.__zqRenderHook) ? performance.now() : 0;
|
|
564
570
|
if (!this._mounted) {
|
|
565
571
|
this._el.innerHTML = html;
|
|
572
|
+
if (_renderStart && window.__zqRenderHook) window.__zqRenderHook(this._el, performance.now() - _renderStart, 'mount', this._def._name);
|
|
566
573
|
} else {
|
|
567
574
|
morph(this._el, html);
|
|
568
575
|
}
|
|
@@ -605,31 +612,31 @@ class Component {
|
|
|
605
612
|
}
|
|
606
613
|
}
|
|
607
614
|
|
|
608
|
-
// Bind @event="method" and z-on:event="method" handlers via delegation
|
|
615
|
+
// Bind @event="method" and z-on:event="method" handlers via delegation.
|
|
616
|
+
//
|
|
617
|
+
// Optimization: on the FIRST render, we scan for event attributes, build
|
|
618
|
+
// a delegated handler map, and attach one listener per event type to the
|
|
619
|
+
// component root. On subsequent renders (re-bind), we only rebuild the
|
|
620
|
+
// internal binding map — existing DOM listeners are reused since they
|
|
621
|
+
// delegate to event.target.closest(selector) at fire time.
|
|
609
622
|
_bindEvents() {
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
this._el.removeEventListener(event, handler);
|
|
613
|
-
});
|
|
614
|
-
this._listeners = [];
|
|
623
|
+
// Always rebuild the binding map from current DOM
|
|
624
|
+
const eventMap = new Map(); // event → [{ selector, methodExpr, modifiers, el }]
|
|
615
625
|
|
|
616
|
-
// Find all elements with @event or z-on:event attributes
|
|
617
626
|
const allEls = this._el.querySelectorAll('*');
|
|
618
|
-
const eventMap = new Map(); // event → [{ selector, method, modifiers }]
|
|
619
|
-
|
|
620
627
|
allEls.forEach(child => {
|
|
621
|
-
// Skip elements inside z-pre subtrees
|
|
622
628
|
if (child.closest('[z-pre]')) return;
|
|
623
629
|
|
|
624
|
-
|
|
625
|
-
|
|
630
|
+
const attrs = child.attributes;
|
|
631
|
+
for (let a = 0; a < attrs.length; a++) {
|
|
632
|
+
const attr = attrs[a];
|
|
626
633
|
let raw;
|
|
627
|
-
if (attr.name.
|
|
628
|
-
raw = attr.name.slice(1);
|
|
634
|
+
if (attr.name.charCodeAt(0) === 64) { // '@'
|
|
635
|
+
raw = attr.name.slice(1);
|
|
629
636
|
} else if (attr.name.startsWith('z-on:')) {
|
|
630
|
-
raw = attr.name.slice(5);
|
|
637
|
+
raw = attr.name.slice(5);
|
|
631
638
|
} else {
|
|
632
|
-
|
|
639
|
+
continue;
|
|
633
640
|
}
|
|
634
641
|
|
|
635
642
|
const parts = raw.split('.');
|
|
@@ -645,12 +652,45 @@ class Component {
|
|
|
645
652
|
|
|
646
653
|
if (!eventMap.has(event)) eventMap.set(event, []);
|
|
647
654
|
eventMap.get(event).push({ selector, methodExpr, modifiers, el: child });
|
|
648
|
-
}
|
|
655
|
+
}
|
|
649
656
|
});
|
|
650
657
|
|
|
658
|
+
// Store binding map for the delegated handlers to reference
|
|
659
|
+
this._eventBindings = eventMap;
|
|
660
|
+
|
|
661
|
+
// Only attach DOM listeners once — reuse on subsequent renders.
|
|
662
|
+
// The handlers close over `this` and read `this._eventBindings`
|
|
663
|
+
// at fire time, so they always use the latest binding map.
|
|
664
|
+
if (this._delegatedEvents) {
|
|
665
|
+
// Already attached — just make sure new event types are covered
|
|
666
|
+
for (const event of eventMap.keys()) {
|
|
667
|
+
if (!this._delegatedEvents.has(event)) {
|
|
668
|
+
this._attachDelegatedEvent(event, eventMap.get(event));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Remove listeners for event types no longer in the template
|
|
672
|
+
for (const event of this._delegatedEvents.keys()) {
|
|
673
|
+
if (!eventMap.has(event)) {
|
|
674
|
+
const { handler, opts } = this._delegatedEvents.get(event);
|
|
675
|
+
this._el.removeEventListener(event, handler, opts);
|
|
676
|
+
this._delegatedEvents.delete(event);
|
|
677
|
+
// Also remove from _listeners array
|
|
678
|
+
this._listeners = this._listeners.filter(l => l.event !== event);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
this._delegatedEvents = new Map();
|
|
685
|
+
|
|
651
686
|
// Register delegated listeners on the component root
|
|
652
687
|
for (const [event, bindings] of eventMap) {
|
|
653
|
-
|
|
688
|
+
this._attachDelegatedEvent(event, bindings);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Attach a single delegated listener for an event type
|
|
693
|
+
_attachDelegatedEvent(event, bindings) {
|
|
654
694
|
const needsCapture = bindings.some(b => b.modifiers.includes('capture'));
|
|
655
695
|
const needsPassive = bindings.some(b => b.modifiers.includes('passive'));
|
|
656
696
|
const listenerOpts = (needsCapture || needsPassive)
|
|
@@ -658,7 +698,9 @@ class Component {
|
|
|
658
698
|
: false;
|
|
659
699
|
|
|
660
700
|
const handler = (e) => {
|
|
661
|
-
|
|
701
|
+
// Read bindings from live map — always up to date after re-renders
|
|
702
|
+
const currentBindings = this._eventBindings?.get(event) || [];
|
|
703
|
+
for (const { selector, methodExpr, modifiers, el } of currentBindings) {
|
|
662
704
|
if (!e.target.closest(selector)) continue;
|
|
663
705
|
|
|
664
706
|
// .self — only fire if target is the element itself
|
|
@@ -728,7 +770,7 @@ class Component {
|
|
|
728
770
|
};
|
|
729
771
|
this._el.addEventListener(event, handler, listenerOpts);
|
|
730
772
|
this._listeners.push({ event, handler });
|
|
731
|
-
|
|
773
|
+
this._delegatedEvents.set(event, { handler, opts: listenerOpts });
|
|
732
774
|
}
|
|
733
775
|
|
|
734
776
|
// Bind z-ref="name" → this.refs.name
|
|
@@ -767,7 +809,7 @@ class Component {
|
|
|
767
809
|
// Read current state value (supports dot-path keys)
|
|
768
810
|
const currentVal = _getPath(this.state, key);
|
|
769
811
|
|
|
770
|
-
// -- Set initial DOM value from state
|
|
812
|
+
// -- Set initial DOM value from state (always sync) ----------
|
|
771
813
|
if (tag === 'input' && type === 'checkbox') {
|
|
772
814
|
el.checked = !!currentVal;
|
|
773
815
|
} else if (tag === 'input' && type === 'radio') {
|
|
@@ -789,6 +831,11 @@ class Component {
|
|
|
789
831
|
: isEditable ? 'input' : 'input';
|
|
790
832
|
|
|
791
833
|
// -- Handler: read DOM → write to reactive state -------------
|
|
834
|
+
// Skip if already bound (morph preserves existing elements,
|
|
835
|
+
// so re-binding would stack duplicate listeners)
|
|
836
|
+
if (el._zqModelBound) return;
|
|
837
|
+
el._zqModelBound = true;
|
|
838
|
+
|
|
792
839
|
const handler = () => {
|
|
793
840
|
let val;
|
|
794
841
|
if (type === 'checkbox') val = el.checked;
|
|
@@ -908,6 +955,41 @@ class Component {
|
|
|
908
955
|
return temp.innerHTML;
|
|
909
956
|
}
|
|
910
957
|
|
|
958
|
+
// ---------------------------------------------------------------------------
|
|
959
|
+
// _expandContentDirectives — Pre-morph z-html & z-text expansion
|
|
960
|
+
//
|
|
961
|
+
// Evaluates z-html and z-text directives at the string level so the morph
|
|
962
|
+
// engine receives HTML with the actual content inline. This lets the diff
|
|
963
|
+
// algorithm properly compare old vs new content (text nodes, child elements)
|
|
964
|
+
// instead of clearing + re-injecting on every re-render.
|
|
965
|
+
//
|
|
966
|
+
// Same parse → evaluate → serialize pattern as _expandZFor.
|
|
967
|
+
// ---------------------------------------------------------------------------
|
|
968
|
+
_expandContentDirectives(html) {
|
|
969
|
+
if (!html.includes('z-html') && !html.includes('z-text')) return html;
|
|
970
|
+
|
|
971
|
+
const temp = document.createElement('div');
|
|
972
|
+
temp.innerHTML = html;
|
|
973
|
+
|
|
974
|
+
// z-html: evaluate expression → inject as innerHTML
|
|
975
|
+
temp.querySelectorAll('[z-html]').forEach(el => {
|
|
976
|
+
if (el.closest('[z-pre]')) return;
|
|
977
|
+
const val = this._evalExpr(el.getAttribute('z-html'));
|
|
978
|
+
el.innerHTML = val != null ? String(val) : '';
|
|
979
|
+
el.removeAttribute('z-html');
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// z-text: evaluate expression → inject as textContent (HTML-safe)
|
|
983
|
+
temp.querySelectorAll('[z-text]').forEach(el => {
|
|
984
|
+
if (el.closest('[z-pre]')) return;
|
|
985
|
+
const val = this._evalExpr(el.getAttribute('z-text'));
|
|
986
|
+
el.textContent = val != null ? String(val) : '';
|
|
987
|
+
el.removeAttribute('z-text');
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
return temp.innerHTML;
|
|
991
|
+
}
|
|
992
|
+
|
|
911
993
|
// ---------------------------------------------------------------------------
|
|
912
994
|
// _processDirectives — Post-innerHTML DOM-level directive processing
|
|
913
995
|
// ---------------------------------------------------------------------------
|
|
@@ -960,25 +1042,36 @@ class Component {
|
|
|
960
1042
|
});
|
|
961
1043
|
|
|
962
1044
|
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1045
|
+
// Use TreeWalker instead of querySelectorAll('*') — avoids
|
|
1046
|
+
// creating a flat array of every single descendant element.
|
|
1047
|
+
// TreeWalker visits nodes lazily; FILTER_REJECT skips z-pre subtrees
|
|
1048
|
+
// at the walker level (faster than per-node closest('[z-pre]') checks).
|
|
1049
|
+
const walker = document.createTreeWalker(this._el, NodeFilter.SHOW_ELEMENT, {
|
|
1050
|
+
acceptNode(n) {
|
|
1051
|
+
return n.hasAttribute('z-pre') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
let node;
|
|
1055
|
+
while ((node = walker.nextNode())) {
|
|
1056
|
+
const attrs = node.attributes;
|
|
1057
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
1058
|
+
const attr = attrs[i];
|
|
966
1059
|
let attrName;
|
|
967
1060
|
if (attr.name.startsWith('z-bind:')) attrName = attr.name.slice(7);
|
|
968
|
-
else if (attr.name.
|
|
969
|
-
else
|
|
1061
|
+
else if (attr.name.charCodeAt(0) === 58 && attr.name.charCodeAt(1) !== 58) attrName = attr.name.slice(1); // ':' but not '::'
|
|
1062
|
+
else continue;
|
|
970
1063
|
|
|
971
1064
|
const val = this._evalExpr(attr.value);
|
|
972
|
-
|
|
1065
|
+
node.removeAttribute(attr.name);
|
|
973
1066
|
if (val === false || val === null || val === undefined) {
|
|
974
|
-
|
|
1067
|
+
node.removeAttribute(attrName);
|
|
975
1068
|
} else if (val === true) {
|
|
976
|
-
|
|
1069
|
+
node.setAttribute(attrName, '');
|
|
977
1070
|
} else {
|
|
978
|
-
|
|
1071
|
+
node.setAttribute(attrName, String(val));
|
|
979
1072
|
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
982
1075
|
|
|
983
1076
|
// -- z-class (dynamic class binding) ---------------------------
|
|
984
1077
|
this._el.querySelectorAll('[z-class]').forEach(el => {
|
|
@@ -1010,21 +1103,9 @@ class Component {
|
|
|
1010
1103
|
el.removeAttribute('z-style');
|
|
1011
1104
|
});
|
|
1012
1105
|
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const val = this._evalExpr(el.getAttribute('z-html'));
|
|
1017
|
-
el.innerHTML = val != null ? String(val) : '';
|
|
1018
|
-
el.removeAttribute('z-html');
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
// -- z-text (safe textContent binding) -------------------------
|
|
1022
|
-
this._el.querySelectorAll('[z-text]').forEach(el => {
|
|
1023
|
-
if (el.closest('[z-pre]')) return;
|
|
1024
|
-
const val = this._evalExpr(el.getAttribute('z-text'));
|
|
1025
|
-
el.textContent = val != null ? String(val) : '';
|
|
1026
|
-
el.removeAttribute('z-text');
|
|
1027
|
-
});
|
|
1106
|
+
// z-html and z-text are now pre-expanded at string level (before
|
|
1107
|
+
// morph) via _expandContentDirectives(), so the diff engine can
|
|
1108
|
+
// properly diff their content instead of clearing + re-injecting.
|
|
1028
1109
|
|
|
1029
1110
|
// -- z-cloak (remove after render) -----------------------------
|
|
1030
1111
|
this._el.querySelectorAll('[z-cloak]').forEach(el => {
|
|
@@ -1057,6 +1138,8 @@ class Component {
|
|
|
1057
1138
|
}
|
|
1058
1139
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
1059
1140
|
this._listeners = [];
|
|
1141
|
+
this._delegatedEvents = null;
|
|
1142
|
+
this._eventBindings = null;
|
|
1060
1143
|
if (this._styleEl) this._styleEl.remove();
|
|
1061
1144
|
_instances.delete(this._el);
|
|
1062
1145
|
this._el.innerHTML = '';
|
|
@@ -1207,6 +1290,36 @@ export function getRegistry() {
|
|
|
1207
1290
|
return Object.fromEntries(_registry);
|
|
1208
1291
|
}
|
|
1209
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* Pre-load a component's external templates and styles so the next mount
|
|
1295
|
+
* renders synchronously (no blank flash while fetching).
|
|
1296
|
+
* Safe to call multiple times — skips if already loaded.
|
|
1297
|
+
* @param {string} name — registered component name
|
|
1298
|
+
* @returns {Promise<void>}
|
|
1299
|
+
*/
|
|
1300
|
+
export async function prefetch(name) {
|
|
1301
|
+
const def = _registry.get(name);
|
|
1302
|
+
if (!def) return;
|
|
1303
|
+
|
|
1304
|
+
// Load templateUrl, styleUrl, and normalize pages config
|
|
1305
|
+
if ((def.templateUrl && !def._templateLoaded) ||
|
|
1306
|
+
(def.styleUrl && !def._styleLoaded) ||
|
|
1307
|
+
(def.pages && !def._pagesNormalized)) {
|
|
1308
|
+
await Component.prototype._loadExternals.call({ _def: def });
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// For pages-based components, prefetch ALL page templates so any
|
|
1312
|
+
// active page renders instantly on mount.
|
|
1313
|
+
if (def._pageUrls && def._externalTemplates) {
|
|
1314
|
+
const missing = Object.entries(def._pageUrls)
|
|
1315
|
+
.filter(([id]) => !(id in def._externalTemplates));
|
|
1316
|
+
if (missing.length) {
|
|
1317
|
+
const results = await Promise.all(missing.map(([, url]) => _fetchResource(url)));
|
|
1318
|
+
missing.forEach(([id], i) => { def._externalTemplates[id] = results[i]; });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1210
1323
|
|
|
1211
1324
|
// ---------------------------------------------------------------------------
|
|
1212
1325
|
// Global stylesheet loader
|
package/src/core.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* into a full jQuery-like chainable wrapper with modern APIs.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { morph as _morph, morphElement as _morphElement } from './diff.js';
|
|
9
|
+
|
|
8
10
|
// ---------------------------------------------------------------------------
|
|
9
11
|
// ZQueryCollection — wraps an array of elements with chainable methods
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
@@ -223,21 +225,46 @@ export class ZQueryCollection {
|
|
|
223
225
|
// --- Classes -------------------------------------------------------------
|
|
224
226
|
|
|
225
227
|
addClass(...names) {
|
|
228
|
+
// Fast path: single class, no spaces — avoids flatMap + regex split allocation
|
|
229
|
+
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
230
|
+
const c = names[0];
|
|
231
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(c);
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
226
234
|
const classes = names.flatMap(n => n.split(/\s+/));
|
|
227
|
-
|
|
235
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(...classes);
|
|
236
|
+
return this;
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
removeClass(...names) {
|
|
240
|
+
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
241
|
+
const c = names[0];
|
|
242
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(c);
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
231
245
|
const classes = names.flatMap(n => n.split(/\s+/));
|
|
232
|
-
|
|
246
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(...classes);
|
|
247
|
+
return this;
|
|
233
248
|
}
|
|
234
249
|
|
|
235
250
|
toggleClass(...args) {
|
|
236
251
|
const force = typeof args[args.length - 1] === 'boolean' ? args.pop() : undefined;
|
|
252
|
+
// Fast path: single class, no spaces
|
|
253
|
+
if (args.length === 1 && args[0].indexOf(' ') === -1) {
|
|
254
|
+
const c = args[0];
|
|
255
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
256
|
+
force !== undefined ? this.elements[i].classList.toggle(c, force) : this.elements[i].classList.toggle(c);
|
|
257
|
+
}
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
237
260
|
const classes = args.flatMap(n => n.split(/\s+/));
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
261
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
262
|
+
const el = this.elements[i];
|
|
263
|
+
for (let j = 0; j < classes.length; j++) {
|
|
264
|
+
force !== undefined ? el.classList.toggle(classes[j], force) : el.classList.toggle(classes[j]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return this;
|
|
241
268
|
}
|
|
242
269
|
|
|
243
270
|
hasClass(name) {
|
|
@@ -273,7 +300,8 @@ export class ZQueryCollection {
|
|
|
273
300
|
|
|
274
301
|
css(props) {
|
|
275
302
|
if (typeof props === 'string') {
|
|
276
|
-
|
|
303
|
+
const el = this.first();
|
|
304
|
+
return el ? getComputedStyle(el)[props] : undefined;
|
|
277
305
|
}
|
|
278
306
|
return this.each((_, el) => Object.assign(el.style, props));
|
|
279
307
|
}
|
|
@@ -349,7 +377,21 @@ export class ZQueryCollection {
|
|
|
349
377
|
|
|
350
378
|
html(content) {
|
|
351
379
|
if (content === undefined) return this.first()?.innerHTML;
|
|
352
|
-
|
|
380
|
+
// Auto-morph: if the element already has children, use the diff engine
|
|
381
|
+
// to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
|
|
382
|
+
// Empty elements get raw innerHTML for fast first-paint — same strategy
|
|
383
|
+
// the component system uses (first render = innerHTML, updates = morph).
|
|
384
|
+
return this.each((_, el) => {
|
|
385
|
+
if (el.childNodes.length > 0) {
|
|
386
|
+
_morph(el, content);
|
|
387
|
+
} else {
|
|
388
|
+
el.innerHTML = content;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
morph(content) {
|
|
394
|
+
return this.each((_, el) => { _morph(el, content); });
|
|
353
395
|
}
|
|
354
396
|
|
|
355
397
|
text(content) {
|
|
@@ -406,7 +448,8 @@ export class ZQueryCollection {
|
|
|
406
448
|
}
|
|
407
449
|
|
|
408
450
|
empty() {
|
|
409
|
-
|
|
451
|
+
// textContent = '' clears all children without invoking the HTML parser
|
|
452
|
+
return this.each((_, el) => { el.textContent = ''; });
|
|
410
453
|
}
|
|
411
454
|
|
|
412
455
|
clone(deep = true) {
|
|
@@ -416,8 +459,9 @@ export class ZQueryCollection {
|
|
|
416
459
|
replaceWith(content) {
|
|
417
460
|
return this.each((_, el) => {
|
|
418
461
|
if (typeof content === 'string') {
|
|
419
|
-
|
|
420
|
-
|
|
462
|
+
// Auto-morph: diff attributes + children when the tag name matches
|
|
463
|
+
// instead of destroying and re-creating the element.
|
|
464
|
+
_morphElement(el, content);
|
|
421
465
|
} else if (content instanceof Node) {
|
|
422
466
|
el.parentNode.replaceChild(content, el);
|
|
423
467
|
}
|
|
@@ -503,7 +547,9 @@ export class ZQueryCollection {
|
|
|
503
547
|
|
|
504
548
|
toggle(display = '') {
|
|
505
549
|
return this.each((_, el) => {
|
|
506
|
-
|
|
550
|
+
// Check inline style first (cheap) before forcing layout via getComputedStyle
|
|
551
|
+
const hidden = el.style.display === 'none' || (el.style.display !== '' ? false : getComputedStyle(el).display === 'none');
|
|
552
|
+
el.style.display = hidden ? display : 'none';
|
|
507
553
|
});
|
|
508
554
|
}
|
|
509
555
|
|