zero-query 0.6.3 → 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 +39 -29
- package/cli/commands/build.js +113 -4
- package/cli/commands/bundle.js +392 -29
- 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 +29 -4
- package/cli/commands/dev/logger.js +6 -1
- package/cli/commands/dev/overlay.js +428 -2
- package/cli/commands/dev/server.js +42 -5
- package/cli/commands/dev/watcher.js +59 -1
- package/cli/help.js +8 -5
- package/cli/scaffold/{scripts → app}/app.js +16 -23
- package/cli/scaffold/{scripts → app}/components/about.js +4 -4
- package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
- package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
- 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} +4 -2
- package/cli/scaffold/index.html +12 -11
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1122 -158
- package/dist/zquery.min.js +3 -16
- package/index.d.ts +129 -1290
- package/index.js +15 -10
- package/package.json +7 -6
- package/src/component.js +172 -49
- package/src/core.js +359 -18
- package/src/diff.js +256 -58
- package/src/expression.js +33 -3
- package/src/reactive.js +37 -5
- package/src/router.js +243 -7
- package/tests/component.test.js +886 -0
- package/tests/core.test.js +977 -0
- package/tests/diff.test.js +525 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +482 -0
- package/tests/http.test.js +289 -0
- package/tests/reactive.test.js +339 -0
- package/tests/router.test.js +649 -0
- package/tests/store.test.js +379 -0
- package/tests/utils.test.js +512 -0
- package/types/collection.d.ts +383 -0
- package/types/component.d.ts +217 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +179 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +161 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- 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.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/index.js
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import { query, queryAll, ZQueryCollection } from './src/core.js';
|
|
13
13
|
import { reactive, Signal, signal, computed, effect } from './src/reactive.js';
|
|
14
|
-
import { component, mount, mountAll, getInstance, destroy, getRegistry, style } from './src/component.js';
|
|
14
|
+
import { component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style } from './src/component.js';
|
|
15
15
|
import { createRouter, getRouter } from './src/router.js';
|
|
16
16
|
import { createStore, getStore } from './src/store.js';
|
|
17
17
|
import { http } from './src/http.js';
|
|
18
|
-
import { morph } from './src/diff.js';
|
|
18
|
+
import { morph, morphElement } from './src/diff.js';
|
|
19
19
|
import { safeEval } from './src/expression.js';
|
|
20
20
|
import {
|
|
21
21
|
debounce, throttle, pipe, once, sleep,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
24
24
|
storage, session, bus,
|
|
25
25
|
} from './src/utils.js';
|
|
26
|
-
import { ZQueryError, ErrorCode, onError, reportError } from './src/errors.js';
|
|
26
|
+
import { ZQueryError, ErrorCode, onError, reportError, guardCallback, validate } from './src/errors.js';
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
@@ -100,8 +100,10 @@ $.mountAll = mountAll;
|
|
|
100
100
|
$.getInstance = getInstance;
|
|
101
101
|
$.destroy = destroy;
|
|
102
102
|
$.components = getRegistry;
|
|
103
|
+
$.prefetch = prefetch;
|
|
103
104
|
$.style = style;
|
|
104
|
-
$.morph
|
|
105
|
+
$.morph = morph;
|
|
106
|
+
$.morphElement = morphElement;
|
|
105
107
|
$.safeEval = safeEval;
|
|
106
108
|
|
|
107
109
|
// --- Router ----------------------------------------------------------------
|
|
@@ -142,12 +144,15 @@ $.session = session;
|
|
|
142
144
|
$.bus = bus;
|
|
143
145
|
|
|
144
146
|
// --- Error handling --------------------------------------------------------
|
|
145
|
-
$.onError
|
|
146
|
-
$.ZQueryError
|
|
147
|
-
$.ErrorCode
|
|
147
|
+
$.onError = onError;
|
|
148
|
+
$.ZQueryError = ZQueryError;
|
|
149
|
+
$.ErrorCode = ErrorCode;
|
|
150
|
+
$.guardCallback = guardCallback;
|
|
151
|
+
$.validate = validate;
|
|
148
152
|
|
|
149
153
|
// --- Meta ------------------------------------------------------------------
|
|
150
154
|
$.version = '__VERSION__';
|
|
155
|
+
$.libSize = '__LIB_SIZE__';
|
|
151
156
|
$.meta = {}; // populated at build time by CLI bundler
|
|
152
157
|
|
|
153
158
|
$.noConflict = () => {
|
|
@@ -176,13 +181,13 @@ export {
|
|
|
176
181
|
ZQueryCollection,
|
|
177
182
|
queryAll,
|
|
178
183
|
reactive, Signal, signal, computed, effect,
|
|
179
|
-
component, mount, mountAll, getInstance, destroy, getRegistry, style,
|
|
180
|
-
morph,
|
|
184
|
+
component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style,
|
|
185
|
+
morph, morphElement,
|
|
181
186
|
safeEval,
|
|
182
187
|
createRouter, getRouter,
|
|
183
188
|
createStore, getStore,
|
|
184
189
|
http,
|
|
185
|
-
ZQueryError, ErrorCode, onError, reportError,
|
|
190
|
+
ZQueryError, ErrorCode, onError, reportError, guardCallback, validate,
|
|
186
191
|
debounce, throttle, pipe, once, sleep,
|
|
187
192
|
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
188
193
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.6
|
|
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",
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
12
|
+
"tests",
|
|
12
13
|
"dist",
|
|
13
14
|
"cli",
|
|
15
|
+
"types",
|
|
14
16
|
"index.js",
|
|
15
17
|
"index.d.ts",
|
|
16
18
|
"LICENSE",
|
|
@@ -19,9 +21,10 @@
|
|
|
19
21
|
"scripts": {
|
|
20
22
|
"build": "node cli/index.js build",
|
|
21
23
|
"dev": "node cli/index.js dev zquery-website",
|
|
24
|
+
"dev:bundle": "node cli/index.js dev zquery-website --bundle",
|
|
22
25
|
"dev-lib": "node cli/index.js build --watch",
|
|
23
26
|
"bundle": "node cli/index.js bundle",
|
|
24
|
-
"bundle:app": "node cli/index.js bundle zquery-website
|
|
27
|
+
"bundle:app": "node cli/index.js bundle zquery-website",
|
|
25
28
|
"test": "vitest run",
|
|
26
29
|
"test:watch": "vitest"
|
|
27
30
|
},
|
|
@@ -52,11 +55,9 @@
|
|
|
52
55
|
"publishConfig": {
|
|
53
56
|
"access": "public"
|
|
54
57
|
},
|
|
55
|
-
"dependencies": {
|
|
56
|
-
"zero-http": "^0.2.3"
|
|
57
|
-
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"jsdom": "^28.1.0",
|
|
60
|
-
"vitest": "^4.0.18"
|
|
60
|
+
"vitest": "^4.0.18",
|
|
61
|
+
"zero-http": "^0.2.3"
|
|
61
62
|
}
|
|
62
63
|
}
|
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.
|
|
@@ -381,11 +381,14 @@ class Component {
|
|
|
381
381
|
if (def.styleUrl && !def._styleLoaded) {
|
|
382
382
|
const su = def.styleUrl;
|
|
383
383
|
if (typeof su === 'string') {
|
|
384
|
-
|
|
384
|
+
const resolved = _resolveUrl(su, base);
|
|
385
|
+
def._externalStyles = await _fetchResource(resolved);
|
|
386
|
+
def._resolvedStyleUrls = [resolved];
|
|
385
387
|
} else if (Array.isArray(su)) {
|
|
386
388
|
const urls = su.map(u => _resolveUrl(u, base));
|
|
387
389
|
const results = await Promise.all(urls.map(u => _fetchResource(u)));
|
|
388
390
|
def._externalStyles = results.join('\n');
|
|
391
|
+
def._resolvedStyleUrls = urls;
|
|
389
392
|
}
|
|
390
393
|
def._styleLoaded = true;
|
|
391
394
|
}
|
|
@@ -469,6 +472,11 @@ class Component {
|
|
|
469
472
|
html = '';
|
|
470
473
|
}
|
|
471
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
|
+
|
|
472
480
|
// -- Slot distribution ----------------------------------------
|
|
473
481
|
// Replace <slot> elements with captured slot content from parent.
|
|
474
482
|
// <slot> → default slot content
|
|
@@ -512,6 +520,13 @@ class Component {
|
|
|
512
520
|
const styleEl = document.createElement('style');
|
|
513
521
|
styleEl.textContent = scoped;
|
|
514
522
|
styleEl.setAttribute('data-zq-component', this._def._name || '');
|
|
523
|
+
styleEl.setAttribute('data-zq-scope', scopeAttr);
|
|
524
|
+
if (this._def._resolvedStyleUrls) {
|
|
525
|
+
styleEl.setAttribute('data-zq-style-urls', this._def._resolvedStyleUrls.join(' '));
|
|
526
|
+
if (this._def.styles) {
|
|
527
|
+
styleEl.setAttribute('data-zq-inline', this._def.styles);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
515
530
|
document.head.appendChild(styleEl);
|
|
516
531
|
this._styleEl = styleEl;
|
|
517
532
|
}
|
|
@@ -551,8 +566,10 @@ class Component {
|
|
|
551
566
|
|
|
552
567
|
// Update DOM via morphing (diffing) — preserves unchanged nodes
|
|
553
568
|
// First render uses innerHTML for speed; subsequent renders morph.
|
|
569
|
+
const _renderStart = typeof window !== 'undefined' && (window.__zqMorphHook || window.__zqRenderHook) ? performance.now() : 0;
|
|
554
570
|
if (!this._mounted) {
|
|
555
571
|
this._el.innerHTML = html;
|
|
572
|
+
if (_renderStart && window.__zqRenderHook) window.__zqRenderHook(this._el, performance.now() - _renderStart, 'mount', this._def._name);
|
|
556
573
|
} else {
|
|
557
574
|
morph(this._el, html);
|
|
558
575
|
}
|
|
@@ -595,31 +612,31 @@ class Component {
|
|
|
595
612
|
}
|
|
596
613
|
}
|
|
597
614
|
|
|
598
|
-
// 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.
|
|
599
622
|
_bindEvents() {
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
this._el.removeEventListener(event, handler);
|
|
603
|
-
});
|
|
604
|
-
this._listeners = [];
|
|
623
|
+
// Always rebuild the binding map from current DOM
|
|
624
|
+
const eventMap = new Map(); // event → [{ selector, methodExpr, modifiers, el }]
|
|
605
625
|
|
|
606
|
-
// Find all elements with @event or z-on:event attributes
|
|
607
626
|
const allEls = this._el.querySelectorAll('*');
|
|
608
|
-
const eventMap = new Map(); // event → [{ selector, method, modifiers }]
|
|
609
|
-
|
|
610
627
|
allEls.forEach(child => {
|
|
611
|
-
// Skip elements inside z-pre subtrees
|
|
612
628
|
if (child.closest('[z-pre]')) return;
|
|
613
629
|
|
|
614
|
-
|
|
615
|
-
|
|
630
|
+
const attrs = child.attributes;
|
|
631
|
+
for (let a = 0; a < attrs.length; a++) {
|
|
632
|
+
const attr = attrs[a];
|
|
616
633
|
let raw;
|
|
617
|
-
if (attr.name.
|
|
618
|
-
raw = attr.name.slice(1);
|
|
634
|
+
if (attr.name.charCodeAt(0) === 64) { // '@'
|
|
635
|
+
raw = attr.name.slice(1);
|
|
619
636
|
} else if (attr.name.startsWith('z-on:')) {
|
|
620
|
-
raw = attr.name.slice(5);
|
|
637
|
+
raw = attr.name.slice(5);
|
|
621
638
|
} else {
|
|
622
|
-
|
|
639
|
+
continue;
|
|
623
640
|
}
|
|
624
641
|
|
|
625
642
|
const parts = raw.split('.');
|
|
@@ -635,12 +652,45 @@ class Component {
|
|
|
635
652
|
|
|
636
653
|
if (!eventMap.has(event)) eventMap.set(event, []);
|
|
637
654
|
eventMap.get(event).push({ selector, methodExpr, modifiers, el: child });
|
|
638
|
-
}
|
|
655
|
+
}
|
|
639
656
|
});
|
|
640
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
|
+
|
|
641
686
|
// Register delegated listeners on the component root
|
|
642
687
|
for (const [event, bindings] of eventMap) {
|
|
643
|
-
|
|
688
|
+
this._attachDelegatedEvent(event, bindings);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Attach a single delegated listener for an event type
|
|
693
|
+
_attachDelegatedEvent(event, bindings) {
|
|
644
694
|
const needsCapture = bindings.some(b => b.modifiers.includes('capture'));
|
|
645
695
|
const needsPassive = bindings.some(b => b.modifiers.includes('passive'));
|
|
646
696
|
const listenerOpts = (needsCapture || needsPassive)
|
|
@@ -648,7 +698,9 @@ class Component {
|
|
|
648
698
|
: false;
|
|
649
699
|
|
|
650
700
|
const handler = (e) => {
|
|
651
|
-
|
|
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) {
|
|
652
704
|
if (!e.target.closest(selector)) continue;
|
|
653
705
|
|
|
654
706
|
// .self — only fire if target is the element itself
|
|
@@ -718,7 +770,7 @@ class Component {
|
|
|
718
770
|
};
|
|
719
771
|
this._el.addEventListener(event, handler, listenerOpts);
|
|
720
772
|
this._listeners.push({ event, handler });
|
|
721
|
-
|
|
773
|
+
this._delegatedEvents.set(event, { handler, opts: listenerOpts });
|
|
722
774
|
}
|
|
723
775
|
|
|
724
776
|
// Bind z-ref="name" → this.refs.name
|
|
@@ -757,7 +809,7 @@ class Component {
|
|
|
757
809
|
// Read current state value (supports dot-path keys)
|
|
758
810
|
const currentVal = _getPath(this.state, key);
|
|
759
811
|
|
|
760
|
-
// -- Set initial DOM value from state
|
|
812
|
+
// -- Set initial DOM value from state (always sync) ----------
|
|
761
813
|
if (tag === 'input' && type === 'checkbox') {
|
|
762
814
|
el.checked = !!currentVal;
|
|
763
815
|
} else if (tag === 'input' && type === 'radio') {
|
|
@@ -779,6 +831,11 @@ class Component {
|
|
|
779
831
|
: isEditable ? 'input' : 'input';
|
|
780
832
|
|
|
781
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
|
+
|
|
782
839
|
const handler = () => {
|
|
783
840
|
let val;
|
|
784
841
|
if (type === 'checkbox') val = el.checked;
|
|
@@ -898,6 +955,41 @@ class Component {
|
|
|
898
955
|
return temp.innerHTML;
|
|
899
956
|
}
|
|
900
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
|
+
|
|
901
993
|
// ---------------------------------------------------------------------------
|
|
902
994
|
// _processDirectives — Post-innerHTML DOM-level directive processing
|
|
903
995
|
// ---------------------------------------------------------------------------
|
|
@@ -950,25 +1042,36 @@ class Component {
|
|
|
950
1042
|
});
|
|
951
1043
|
|
|
952
1044
|
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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];
|
|
956
1059
|
let attrName;
|
|
957
1060
|
if (attr.name.startsWith('z-bind:')) attrName = attr.name.slice(7);
|
|
958
|
-
else if (attr.name.
|
|
959
|
-
else
|
|
1061
|
+
else if (attr.name.charCodeAt(0) === 58 && attr.name.charCodeAt(1) !== 58) attrName = attr.name.slice(1); // ':' but not '::'
|
|
1062
|
+
else continue;
|
|
960
1063
|
|
|
961
1064
|
const val = this._evalExpr(attr.value);
|
|
962
|
-
|
|
1065
|
+
node.removeAttribute(attr.name);
|
|
963
1066
|
if (val === false || val === null || val === undefined) {
|
|
964
|
-
|
|
1067
|
+
node.removeAttribute(attrName);
|
|
965
1068
|
} else if (val === true) {
|
|
966
|
-
|
|
1069
|
+
node.setAttribute(attrName, '');
|
|
967
1070
|
} else {
|
|
968
|
-
|
|
1071
|
+
node.setAttribute(attrName, String(val));
|
|
969
1072
|
}
|
|
970
|
-
}
|
|
971
|
-
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
972
1075
|
|
|
973
1076
|
// -- z-class (dynamic class binding) ---------------------------
|
|
974
1077
|
this._el.querySelectorAll('[z-class]').forEach(el => {
|
|
@@ -1000,21 +1103,9 @@ class Component {
|
|
|
1000
1103
|
el.removeAttribute('z-style');
|
|
1001
1104
|
});
|
|
1002
1105
|
|
|
1003
|
-
//
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const val = this._evalExpr(el.getAttribute('z-html'));
|
|
1007
|
-
el.innerHTML = val != null ? String(val) : '';
|
|
1008
|
-
el.removeAttribute('z-html');
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
// -- z-text (safe textContent binding) -------------------------
|
|
1012
|
-
this._el.querySelectorAll('[z-text]').forEach(el => {
|
|
1013
|
-
if (el.closest('[z-pre]')) return;
|
|
1014
|
-
const val = this._evalExpr(el.getAttribute('z-text'));
|
|
1015
|
-
el.textContent = val != null ? String(val) : '';
|
|
1016
|
-
el.removeAttribute('z-text');
|
|
1017
|
-
});
|
|
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.
|
|
1018
1109
|
|
|
1019
1110
|
// -- z-cloak (remove after render) -----------------------------
|
|
1020
1111
|
this._el.querySelectorAll('[z-cloak]').forEach(el => {
|
|
@@ -1047,6 +1138,8 @@ class Component {
|
|
|
1047
1138
|
}
|
|
1048
1139
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
1049
1140
|
this._listeners = [];
|
|
1141
|
+
this._delegatedEvents = null;
|
|
1142
|
+
this._eventBindings = null;
|
|
1050
1143
|
if (this._styleEl) this._styleEl.remove();
|
|
1051
1144
|
_instances.delete(this._el);
|
|
1052
1145
|
this._el.innerHTML = '';
|
|
@@ -1197,6 +1290,36 @@ export function getRegistry() {
|
|
|
1197
1290
|
return Object.fromEntries(_registry);
|
|
1198
1291
|
}
|
|
1199
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
|
+
|
|
1200
1323
|
|
|
1201
1324
|
// ---------------------------------------------------------------------------
|
|
1202
1325
|
// Global stylesheet loader
|