zero-query 0.9.9 → 1.0.0
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 +33 -32
- package/cli/args.js +1 -1
- package/cli/commands/build.js +2 -2
- package/cli/commands/bundle.js +15 -15
- package/cli/commands/create.js +2 -2
- package/cli/commands/dev/devtools/index.js +1 -1
- package/cli/commands/dev/devtools/js/core.js +14 -14
- package/cli/commands/dev/devtools/js/elements.js +4 -4
- package/cli/commands/dev/devtools/js/stats.js +1 -1
- package/cli/commands/dev/devtools/styles.css +2 -2
- package/cli/commands/dev/index.js +2 -2
- package/cli/commands/dev/logger.js +1 -1
- package/cli/commands/dev/overlay.js +21 -14
- package/cli/commands/dev/server.js +5 -5
- package/cli/commands/dev/validator.js +7 -7
- package/cli/commands/dev/watcher.js +6 -6
- package/cli/help.js +3 -3
- package/cli/index.js +2 -2
- package/cli/scaffold/default/app/app.js +17 -18
- package/cli/scaffold/default/app/components/about.js +9 -9
- package/cli/scaffold/default/app/components/api-demo.js +6 -6
- package/cli/scaffold/default/app/components/contact-card.js +4 -4
- package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
- package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
- package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
- package/cli/scaffold/default/app/components/counter.js +8 -8
- package/cli/scaffold/default/app/components/home.js +13 -13
- package/cli/scaffold/default/app/components/not-found.js +1 -1
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +11 -11
- package/cli/scaffold/default/app/components/playground/playground.js +11 -11
- package/cli/scaffold/default/app/components/todos.js +8 -8
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
- package/cli/scaffold/default/app/routes.js +1 -1
- package/cli/scaffold/default/app/store.js +1 -1
- package/cli/scaffold/default/global.css +2 -2
- package/cli/scaffold/default/index.html +2 -2
- package/cli/scaffold/minimal/app/app.js +6 -7
- package/cli/scaffold/minimal/app/components/about.js +5 -5
- package/cli/scaffold/minimal/app/components/counter.js +6 -6
- package/cli/scaffold/minimal/app/components/home.js +8 -8
- package/cli/scaffold/minimal/app/components/not-found.js +1 -1
- package/cli/scaffold/minimal/app/routes.js +1 -1
- package/cli/scaffold/minimal/app/store.js +1 -1
- package/cli/scaffold/minimal/global.css +2 -2
- package/cli/scaffold/minimal/index.html +1 -1
- package/cli/scaffold/ssr/app/app.js +1 -2
- package/cli/scaffold/ssr/app/components/about.js +5 -5
- package/cli/scaffold/ssr/app/components/home.js +2 -2
- package/cli/scaffold/ssr/app/components/not-found.js +1 -1
- package/cli/scaffold/ssr/app/routes.js +1 -1
- package/cli/scaffold/ssr/global.css +2 -2
- package/cli/scaffold/ssr/index.html +2 -2
- package/cli/scaffold/ssr/server/index.js +4 -4
- package/cli/utils.js +6 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +508 -227
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +16 -13
- package/index.js +7 -5
- package/package.json +2 -2
- package/src/component.js +64 -63
- package/src/core.js +15 -15
- package/src/diff.js +38 -38
- package/src/errors.js +17 -17
- package/src/expression.js +15 -17
- package/src/http.js +4 -4
- package/src/reactive.js +75 -9
- package/src/router.js +104 -24
- package/src/ssr.js +28 -28
- package/src/store.js +103 -21
- package/src/utils.js +64 -12
- package/tests/audit.test.js +143 -15
- package/tests/cli.test.js +20 -20
- package/tests/component.test.js +121 -121
- package/tests/core.test.js +56 -56
- package/tests/diff.test.js +42 -42
- package/tests/errors.test.js +5 -5
- package/tests/expression.test.js +58 -53
- package/tests/http.test.js +20 -20
- package/tests/reactive.test.js +185 -24
- package/tests/router.test.js +501 -74
- package/tests/ssr.test.js +15 -13
- package/tests/store.test.js +264 -23
- package/tests/utils.test.js +163 -26
- package/types/collection.d.ts +2 -2
- package/types/component.d.ts +5 -5
- package/types/errors.d.ts +3 -3
- package/types/http.d.ts +3 -3
- package/types/misc.d.ts +9 -9
- package/types/reactive.d.ts +25 -3
- package/types/router.d.ts +10 -6
- package/types/ssr.d.ts +2 -2
- package/types/store.d.ts +40 -5
- package/types/utils.d.ts +1 -1
package/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery)
|
|
2
|
+
* zQuery (zeroQuery) - TypeScript Declarations
|
|
3
3
|
*
|
|
4
|
-
* Lightweight modern frontend library
|
|
4
|
+
* Lightweight modern frontend library - jQuery-like selectors, reactive
|
|
5
5
|
* components, SPA router, state management, HTTP client & utilities.
|
|
6
6
|
*
|
|
7
|
-
* @version 0.
|
|
7
|
+
* @version 1.0.0
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @see https://z-query.com/docs
|
|
10
10
|
*/
|
|
@@ -22,6 +22,8 @@ export {
|
|
|
22
22
|
signal,
|
|
23
23
|
computed,
|
|
24
24
|
effect,
|
|
25
|
+
batch,
|
|
26
|
+
untracked,
|
|
25
27
|
} from './types/reactive';
|
|
26
28
|
|
|
27
29
|
export {
|
|
@@ -135,15 +137,14 @@ export {
|
|
|
135
137
|
SSRApp,
|
|
136
138
|
createSSRApp,
|
|
137
139
|
renderToString,
|
|
138
|
-
escapeHtml as escapeHtmlSSR,
|
|
139
140
|
} from './types/ssr';
|
|
140
141
|
|
|
141
142
|
// ---------------------------------------------------------------------------
|
|
142
|
-
// $
|
|
143
|
+
// $ - Main function & namespace
|
|
143
144
|
// ---------------------------------------------------------------------------
|
|
144
145
|
|
|
145
146
|
import type { ZQueryCollection } from './types/collection';
|
|
146
|
-
import type { reactive, Signal, signal, computed, effect } from './types/reactive';
|
|
147
|
+
import type { reactive, Signal, signal, computed, effect, batch, untracked } from './types/reactive';
|
|
147
148
|
import type { component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style } from './types/component';
|
|
148
149
|
import type { createRouter, getRouter } from './types/router';
|
|
149
150
|
import type { createStore, getStore } from './types/store';
|
|
@@ -162,7 +163,7 @@ import type { onError, ZQueryError, ErrorCode, guardCallback, validate } from '.
|
|
|
162
163
|
import type { morph, morphElement, safeEval } from './types/misc';
|
|
163
164
|
|
|
164
165
|
/**
|
|
165
|
-
* Main selector / DOM-ready function
|
|
166
|
+
* Main selector / DOM-ready function - always returns a `ZQueryCollection` (like jQuery).
|
|
166
167
|
*
|
|
167
168
|
* - `$('selector')` → ZQueryCollection via `querySelectorAll`
|
|
168
169
|
* - `$('<div>…</div>')` → ZQueryCollection from created elements
|
|
@@ -177,7 +178,7 @@ interface ZQueryStatic {
|
|
|
177
178
|
|
|
178
179
|
// -- Collection selector -------------------------------------------------
|
|
179
180
|
/**
|
|
180
|
-
* Collection selector
|
|
181
|
+
* Collection selector - returns a `ZQueryCollection`.
|
|
181
182
|
*
|
|
182
183
|
* - `$.all('.card')` → all matching elements
|
|
183
184
|
* - `$.all('<div>…</div>')` → create elements as collection
|
|
@@ -201,9 +202,9 @@ interface ZQueryStatic {
|
|
|
201
202
|
name(name: string): ZQueryCollection;
|
|
202
203
|
/** Children of `#parentId` as `ZQueryCollection`. */
|
|
203
204
|
children(parentId: string): ZQueryCollection;
|
|
204
|
-
/** `document.querySelector(selector)`
|
|
205
|
+
/** `document.querySelector(selector)` - raw Element or null. */
|
|
205
206
|
qs(selector: string, context?: Element | Document): Element | null;
|
|
206
|
-
/** `document.querySelectorAll(selector)`
|
|
207
|
+
/** `document.querySelectorAll(selector)` - as a real `Array<Element>`. */
|
|
207
208
|
qsa(selector: string, context?: Element | Document): Element[];
|
|
208
209
|
|
|
209
210
|
// -- Static helpers ------------------------------------------------------
|
|
@@ -232,7 +233,7 @@ interface ZQueryStatic {
|
|
|
232
233
|
/** Remove a direct global event listener previously attached with `$.on(event, handler)`. */
|
|
233
234
|
off(event: string, handler: (e: Event) => void): void;
|
|
234
235
|
|
|
235
|
-
/** Alias for `ZQueryCollection.prototype`
|
|
236
|
+
/** Alias for `ZQueryCollection.prototype` - extend to add custom collection methods. */
|
|
236
237
|
fn: typeof ZQueryCollection.prototype;
|
|
237
238
|
|
|
238
239
|
// -- Reactive ------------------------------------------------------------
|
|
@@ -241,6 +242,8 @@ interface ZQueryStatic {
|
|
|
241
242
|
signal: typeof signal;
|
|
242
243
|
computed: typeof computed;
|
|
243
244
|
effect: typeof effect;
|
|
245
|
+
batch: typeof batch;
|
|
246
|
+
untracked: typeof untracked;
|
|
244
247
|
|
|
245
248
|
// -- Components ----------------------------------------------------------
|
|
246
249
|
component: typeof component;
|
|
@@ -254,7 +257,7 @@ interface ZQueryStatic {
|
|
|
254
257
|
prefetch: typeof prefetch;
|
|
255
258
|
style: typeof style;
|
|
256
259
|
morph: typeof morph;
|
|
257
|
-
/** Morph a single element in place
|
|
260
|
+
/** Morph a single element in place - preserves identity when tag name matches. */
|
|
258
261
|
morphElement: typeof morphElement;
|
|
259
262
|
safeEval: typeof safeEval;
|
|
260
263
|
|
|
@@ -353,7 +356,7 @@ interface ZQueryStatic {
|
|
|
353
356
|
export const $: ZQueryStatic;
|
|
354
357
|
export { $ as zQuery };
|
|
355
358
|
|
|
356
|
-
/** Collection selector function
|
|
359
|
+
/** Collection selector function - same as `$.all()`. */
|
|
357
360
|
export function queryAll(selector: string, context?: string | Element): ZQueryCollection;
|
|
358
361
|
export function queryAll(element: Element): ZQueryCollection;
|
|
359
362
|
export function queryAll(nodeList: NodeList | HTMLCollection | Element[]): ZQueryCollection;
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ┌---------------------------------------------------------┐
|
|
3
|
-
* │ zQuery (zeroQuery)
|
|
3
|
+
* │ zQuery (zeroQuery) - Lightweight Frontend Library │
|
|
4
4
|
* │ │
|
|
5
5
|
* │ jQuery-like selectors · Reactive components │
|
|
6
6
|
* │ SPA router · State management · Zero dependencies │
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { query, queryAll, ZQueryCollection } from './src/core.js';
|
|
13
|
-
import { reactive, Signal, signal, computed, effect } from './src/reactive.js';
|
|
13
|
+
import { reactive, Signal, signal, computed, effect, batch, untracked } from './src/reactive.js';
|
|
14
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';
|
|
@@ -31,11 +31,11 @@ import { ZQueryError, ErrorCode, onError, reportError, guardCallback, guardAsync
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
|
-
// $
|
|
34
|
+
// $ - The main function & namespace
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Main selector function
|
|
38
|
+
* Main selector function - always returns a ZQueryCollection (like jQuery).
|
|
39
39
|
*
|
|
40
40
|
* $('selector') → ZQueryCollection (querySelectorAll)
|
|
41
41
|
* $('<div>hello</div>') → ZQueryCollection from created elements
|
|
@@ -98,6 +98,8 @@ $.Signal = Signal;
|
|
|
98
98
|
$.signal = signal;
|
|
99
99
|
$.computed = computed;
|
|
100
100
|
$.effect = effect;
|
|
101
|
+
$.batch = batch;
|
|
102
|
+
$.untracked = untracked;
|
|
101
103
|
|
|
102
104
|
// --- Components ------------------------------------------------------------
|
|
103
105
|
$.component = component;
|
|
@@ -208,7 +210,7 @@ export {
|
|
|
208
210
|
$ as zQuery,
|
|
209
211
|
ZQueryCollection,
|
|
210
212
|
queryAll,
|
|
211
|
-
reactive, Signal, signal, computed, effect,
|
|
213
|
+
reactive, Signal, signal, computed, effect, batch, untracked,
|
|
212
214
|
component, mount, mountAll, getInstance, destroy, getRegistry, prefetch, style,
|
|
213
215
|
morph, morphElement,
|
|
214
216
|
safeEval,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lightweight modern frontend library
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
7
7
|
"exports": {
|
package/src/component.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery Component
|
|
2
|
+
* zQuery Component - Lightweight reactive component system
|
|
3
3
|
*
|
|
4
4
|
* Declarative components using template literals with directive support.
|
|
5
5
|
* Proxy-based state triggers targeted re-renders via event delegation.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Scoped styles (inline or via styleUrl)
|
|
16
16
|
* - External templates via templateUrl (with {{expression}} interpolation)
|
|
17
17
|
* - External styles via styleUrl (fetched & scoped automatically)
|
|
18
|
-
* - Relative path resolution
|
|
18
|
+
* - Relative path resolution - templateUrl and styleUrl
|
|
19
19
|
* resolve relative to the component file automatically
|
|
20
20
|
*/
|
|
21
21
|
|
|
@@ -23,6 +23,7 @@ import { reactive } from './reactive.js';
|
|
|
23
23
|
import { morph } from './diff.js';
|
|
24
24
|
import { safeEval } from './expression.js';
|
|
25
25
|
import { reportError, ErrorCode, ZQueryError } from './errors.js';
|
|
26
|
+
import { escapeHtml } from './utils.js';
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Component registry & external resource cache
|
|
@@ -48,7 +49,7 @@ const _throttleTimers = new WeakMap();
|
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Fetch and cache a text resource (HTML template or CSS file).
|
|
51
|
-
* @param {string} url
|
|
52
|
+
* @param {string} url - URL to fetch
|
|
52
53
|
* @returns {Promise<string>}
|
|
53
54
|
*/
|
|
54
55
|
function _fetchResource(url) {
|
|
@@ -92,23 +93,23 @@ function _fetchResource(url) {
|
|
|
92
93
|
* - If `base` is an absolute URL (http/https/file), resolve directly.
|
|
93
94
|
* - If `base` is a relative path string, resolve it against the page root
|
|
94
95
|
* (or <base href>) first, then resolve `url` against that.
|
|
95
|
-
* - If `base` is falsy, return `url` unchanged
|
|
96
|
+
* - If `base` is falsy, return `url` unchanged - _fetchResource's own
|
|
96
97
|
* fallback (page root / <base href>) handles it.
|
|
97
98
|
*
|
|
98
|
-
* @param {string} url
|
|
99
|
-
* @param {string} [base]
|
|
99
|
+
* @param {string} url - URL or relative path to resolve
|
|
100
|
+
* @param {string} [base] - auto-detected caller URL or explicit base path
|
|
100
101
|
* @returns {string}
|
|
101
102
|
*/
|
|
102
103
|
function _resolveUrl(url, base) {
|
|
103
104
|
if (!base || !url || typeof url !== 'string') return url;
|
|
104
|
-
// Already absolute
|
|
105
|
+
// Already absolute - nothing to do
|
|
105
106
|
if (url.startsWith('/') || url.includes('://') || url.startsWith('//')) return url;
|
|
106
107
|
try {
|
|
107
108
|
if (base.includes('://')) {
|
|
108
109
|
// Absolute base (auto-detected module URL)
|
|
109
110
|
return new URL(url, base).href;
|
|
110
111
|
}
|
|
111
|
-
// Relative base string
|
|
112
|
+
// Relative base string - resolve against page root first
|
|
112
113
|
const baseEl = document.querySelector('base');
|
|
113
114
|
const root = baseEl ? baseEl.href : (window.location.origin + '/');
|
|
114
115
|
const absBase = new URL(base.endsWith('/') ? base : base + '/', root).href;
|
|
@@ -140,13 +141,13 @@ function _detectCallerBase() {
|
|
|
140
141
|
for (const raw of urls) {
|
|
141
142
|
// Strip line:col suffixes e.g. ":3:5" or ":12:1"
|
|
142
143
|
const url = raw.replace(/:\d+:\d+$/, '').replace(/:\d+$/, '');
|
|
143
|
-
// Skip the zQuery library itself
|
|
144
|
+
// Skip the zQuery library itself - by filename pattern and captured URL
|
|
144
145
|
if (/zquery(\.min)?\.js$/i.test(url)) continue;
|
|
145
146
|
if (_ownScriptUrl && url.replace(/[?#].*$/, '') === _ownScriptUrl) continue;
|
|
146
147
|
// Return directory (strip filename, keep trailing slash)
|
|
147
148
|
return url.replace(/\/[^/]*$/, '/');
|
|
148
149
|
}
|
|
149
|
-
} catch { /* stack parsing unsupported
|
|
150
|
+
} catch { /* stack parsing unsupported - fall back silently */ }
|
|
150
151
|
return undefined;
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -225,7 +226,7 @@ class Component {
|
|
|
225
226
|
}
|
|
226
227
|
});
|
|
227
228
|
|
|
228
|
-
// Computed properties
|
|
229
|
+
// Computed properties - lazy getters derived from state
|
|
229
230
|
this.computed = {};
|
|
230
231
|
if (definition.computed) {
|
|
231
232
|
for (const [name, fn] of Object.entries(definition.computed)) {
|
|
@@ -354,7 +355,7 @@ class Component {
|
|
|
354
355
|
this._loadExternals().then(() => {
|
|
355
356
|
if (!this._destroyed) this._render();
|
|
356
357
|
});
|
|
357
|
-
return; // Skip this render
|
|
358
|
+
return; // Skip this render - will re-render after load
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
// Expose multi-template map on instance (if available)
|
|
@@ -379,7 +380,7 @@ class Component {
|
|
|
379
380
|
this.state.__raw || this.state,
|
|
380
381
|
{ props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
381
382
|
]);
|
|
382
|
-
return result != null ? result : '';
|
|
383
|
+
return result != null ? escapeHtml(String(result)) : '';
|
|
383
384
|
} catch { return ''; }
|
|
384
385
|
});
|
|
385
386
|
} else {
|
|
@@ -425,13 +426,13 @@ class Component {
|
|
|
425
426
|
const trimmed = selector.trim();
|
|
426
427
|
// Don't scope @-rules themselves
|
|
427
428
|
if (trimmed.startsWith('@')) {
|
|
428
|
-
// @keyframes and @font-face contain non-selector content
|
|
429
|
+
// @keyframes and @font-face contain non-selector content - skip scoping inside them
|
|
429
430
|
if (/^@(keyframes|font-face)\b/.test(trimmed)) {
|
|
430
431
|
noScopeDepth = braceDepth;
|
|
431
432
|
}
|
|
432
433
|
return match;
|
|
433
434
|
}
|
|
434
|
-
// Inside @keyframes or @font-face
|
|
435
|
+
// Inside @keyframes or @font-face - don't scope inner rules
|
|
435
436
|
if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
|
|
436
437
|
return match;
|
|
437
438
|
}
|
|
@@ -484,7 +485,7 @@ class Component {
|
|
|
484
485
|
}
|
|
485
486
|
}
|
|
486
487
|
|
|
487
|
-
// Update DOM via morphing (diffing)
|
|
488
|
+
// Update DOM via morphing (diffing) - preserves unchanged nodes
|
|
488
489
|
// First render uses innerHTML for speed; subsequent renders morph.
|
|
489
490
|
const _renderStart = typeof window !== 'undefined' && (window.__zqMorphHook || window.__zqRenderHook) ? performance.now() : 0;
|
|
490
491
|
if (!this._mounted) {
|
|
@@ -503,8 +504,8 @@ class Component {
|
|
|
503
504
|
this._bindModels();
|
|
504
505
|
|
|
505
506
|
// Restore focus if the morph replaced the focused element.
|
|
506
|
-
// Always restore selectionRange
|
|
507
|
-
// the activeElement
|
|
507
|
+
// Always restore selectionRange - even when the element is still
|
|
508
|
+
// the activeElement - because _bindModels or morph attribute syncing
|
|
508
509
|
// can alter the value and move the cursor.
|
|
509
510
|
if (_focusInfo) {
|
|
510
511
|
const el = this._el.querySelector(_focusInfo.selector);
|
|
@@ -540,7 +541,7 @@ class Component {
|
|
|
540
541
|
// Optimization: on the FIRST render, we scan for event attributes, build
|
|
541
542
|
// a delegated handler map, and attach one listener per event type to the
|
|
542
543
|
// component root. On subsequent renders (re-bind), we only rebuild the
|
|
543
|
-
// internal binding map
|
|
544
|
+
// internal binding map - existing DOM listeners are reused since they
|
|
544
545
|
// delegate to event.target.closest(selector) at fire time.
|
|
545
546
|
_bindEvents() {
|
|
546
547
|
// Always rebuild the binding map from current DOM
|
|
@@ -581,11 +582,11 @@ class Component {
|
|
|
581
582
|
// Store binding map for the delegated handlers to reference
|
|
582
583
|
this._eventBindings = eventMap;
|
|
583
584
|
|
|
584
|
-
// Only attach DOM listeners once
|
|
585
|
+
// Only attach DOM listeners once - reuse on subsequent renders.
|
|
585
586
|
// The handlers close over `this` and read `this._eventBindings`
|
|
586
587
|
// at fire time, so they always use the latest binding map.
|
|
587
588
|
if (this._delegatedEvents) {
|
|
588
|
-
// Already attached
|
|
589
|
+
// Already attached - just make sure new event types are covered
|
|
589
590
|
for (const event of eventMap.keys()) {
|
|
590
591
|
if (!this._delegatedEvents.has(event)) {
|
|
591
592
|
this._attachDelegatedEvent(event, eventMap.get(event));
|
|
@@ -611,7 +612,7 @@ class Component {
|
|
|
611
612
|
this._attachDelegatedEvent(event, bindings);
|
|
612
613
|
}
|
|
613
614
|
|
|
614
|
-
// .outside
|
|
615
|
+
// .outside - attach a document-level listener for bindings that need
|
|
615
616
|
// to detect clicks/events outside their element.
|
|
616
617
|
this._outsideListeners = this._outsideListeners || [];
|
|
617
618
|
for (const [event, bindings] of eventMap) {
|
|
@@ -639,7 +640,7 @@ class Component {
|
|
|
639
640
|
: false;
|
|
640
641
|
|
|
641
642
|
const handler = (e) => {
|
|
642
|
-
// Read bindings from live map
|
|
643
|
+
// Read bindings from live map - always up to date after re-renders
|
|
643
644
|
const currentBindings = this._eventBindings?.get(event) || [];
|
|
644
645
|
|
|
645
646
|
// Collect matching bindings with their matched elements, then sort
|
|
@@ -660,7 +661,7 @@ class Component {
|
|
|
660
661
|
for (const { selector, methodExpr, modifiers, el, matched } of hits) {
|
|
661
662
|
|
|
662
663
|
// In delegated events, .stop should prevent ancestor bindings from
|
|
663
|
-
// firing
|
|
664
|
+
// firing - stopPropagation alone only stops real DOM bubbling.
|
|
664
665
|
if (stoppedAt) {
|
|
665
666
|
let blocked = false;
|
|
666
667
|
for (const stopped of stoppedAt) {
|
|
@@ -669,15 +670,15 @@ class Component {
|
|
|
669
670
|
if (blocked) continue;
|
|
670
671
|
}
|
|
671
672
|
|
|
672
|
-
// .self
|
|
673
|
+
// .self - only fire if target is the element itself
|
|
673
674
|
if (modifiers.includes('self') && e.target !== el) continue;
|
|
674
675
|
|
|
675
|
-
// .outside
|
|
676
|
+
// .outside - only fire if event target is OUTSIDE the element
|
|
676
677
|
if (modifiers.includes('outside')) {
|
|
677
678
|
if (el.contains(e.target)) continue;
|
|
678
679
|
}
|
|
679
680
|
|
|
680
|
-
// Key modifiers
|
|
681
|
+
// Key modifiers - filter keyboard events by key
|
|
681
682
|
const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
|
|
682
683
|
let keyFiltered = false;
|
|
683
684
|
for (const mod of modifiers) {
|
|
@@ -688,7 +689,7 @@ class Component {
|
|
|
688
689
|
}
|
|
689
690
|
if (keyFiltered) continue;
|
|
690
691
|
|
|
691
|
-
// System key modifiers
|
|
692
|
+
// System key modifiers - require modifier keys to be held
|
|
692
693
|
if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
|
|
693
694
|
if (modifiers.includes('shift') && !e.shiftKey) continue;
|
|
694
695
|
if (modifiers.includes('alt') && !e.altKey) continue;
|
|
@@ -728,7 +729,7 @@ class Component {
|
|
|
728
729
|
}
|
|
729
730
|
};
|
|
730
731
|
|
|
731
|
-
// .debounce.{ms}
|
|
732
|
+
// .debounce.{ms} - delay invocation until idle
|
|
732
733
|
const debounceIdx = modifiers.indexOf('debounce');
|
|
733
734
|
if (debounceIdx !== -1) {
|
|
734
735
|
const ms = parseInt(modifiers[debounceIdx + 1], 10) || 250;
|
|
@@ -739,7 +740,7 @@ class Component {
|
|
|
739
740
|
continue;
|
|
740
741
|
}
|
|
741
742
|
|
|
742
|
-
// .throttle.{ms}
|
|
743
|
+
// .throttle.{ms} - fire at most once per interval
|
|
743
744
|
const throttleIdx = modifiers.indexOf('throttle');
|
|
744
745
|
if (throttleIdx !== -1) {
|
|
745
746
|
const ms = parseInt(modifiers[throttleIdx + 1], 10) || 250;
|
|
@@ -751,7 +752,7 @@ class Component {
|
|
|
751
752
|
continue;
|
|
752
753
|
}
|
|
753
754
|
|
|
754
|
-
// .once
|
|
755
|
+
// .once - fire once then ignore
|
|
755
756
|
if (modifiers.includes('once')) {
|
|
756
757
|
if (el.dataset.zqOnce === event) continue;
|
|
757
758
|
el.dataset.zqOnce = event;
|
|
@@ -779,12 +780,12 @@ class Component {
|
|
|
779
780
|
// textarea, select (single & multiple), contenteditable
|
|
780
781
|
// Nested state keys: z-model="user.name" → this.state.user.name
|
|
781
782
|
// Modifiers (boolean attributes on the same element):
|
|
782
|
-
// z-lazy
|
|
783
|
-
// z-trim
|
|
784
|
-
// z-number
|
|
785
|
-
// z-debounce
|
|
786
|
-
// z-uppercase
|
|
787
|
-
// z-lowercase
|
|
783
|
+
// z-lazy - listen on 'change' instead of 'input' (update on blur / commit)
|
|
784
|
+
// z-trim - trim whitespace before writing to state
|
|
785
|
+
// z-number - force Number() conversion regardless of input type
|
|
786
|
+
// z-debounce - debounce state writes (default 250ms, or z-debounce="300")
|
|
787
|
+
// z-uppercase - convert string to uppercase before writing to state
|
|
788
|
+
// z-lowercase - convert string to lowercase before writing to state
|
|
788
789
|
//
|
|
789
790
|
// Writes to reactive state so the rest of the UI stays in sync.
|
|
790
791
|
// Focus and cursor position are preserved in _render() via focusInfo.
|
|
@@ -866,7 +867,7 @@ class Component {
|
|
|
866
867
|
}
|
|
867
868
|
|
|
868
869
|
// ---------------------------------------------------------------------------
|
|
869
|
-
// Expression evaluator
|
|
870
|
+
// Expression evaluator - CSP-safe parser (no eval / new Function)
|
|
870
871
|
// ---------------------------------------------------------------------------
|
|
871
872
|
_evalExpr(expr) {
|
|
872
873
|
return safeEval(expr, [
|
|
@@ -876,7 +877,7 @@ class Component {
|
|
|
876
877
|
}
|
|
877
878
|
|
|
878
879
|
// ---------------------------------------------------------------------------
|
|
879
|
-
// z-for
|
|
880
|
+
// z-for - Expand list-rendering directives (pre-innerHTML, string level)
|
|
880
881
|
//
|
|
881
882
|
// <li z-for="item in items">{{item.name}}</li>
|
|
882
883
|
// <li z-for="(item, i) in items">{{i}}: {{item.name}}</li>
|
|
@@ -942,7 +943,7 @@ class Component {
|
|
|
942
943
|
this.state.__raw || this.state,
|
|
943
944
|
{ props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
944
945
|
]);
|
|
945
|
-
return result != null ? result : '';
|
|
946
|
+
return result != null ? escapeHtml(String(result)) : '';
|
|
946
947
|
} catch { return ''; }
|
|
947
948
|
});
|
|
948
949
|
|
|
@@ -965,7 +966,7 @@ class Component {
|
|
|
965
966
|
}
|
|
966
967
|
|
|
967
968
|
// ---------------------------------------------------------------------------
|
|
968
|
-
// _expandContentDirectives
|
|
969
|
+
// _expandContentDirectives - Pre-morph z-html & z-text expansion
|
|
969
970
|
//
|
|
970
971
|
// Evaluates z-html and z-text directives at the string level so the morph
|
|
971
972
|
// engine receives HTML with the actual content inline. This lets the diff
|
|
@@ -1000,7 +1001,7 @@ class Component {
|
|
|
1000
1001
|
}
|
|
1001
1002
|
|
|
1002
1003
|
// ---------------------------------------------------------------------------
|
|
1003
|
-
// _processDirectives
|
|
1004
|
+
// _processDirectives - Post-innerHTML DOM-level directive processing
|
|
1004
1005
|
// ---------------------------------------------------------------------------
|
|
1005
1006
|
_processDirectives() {
|
|
1006
1007
|
// z-pre: skip all directive processing on subtrees
|
|
@@ -1051,7 +1052,7 @@ class Component {
|
|
|
1051
1052
|
});
|
|
1052
1053
|
|
|
1053
1054
|
// -- z-bind:attr / :attr (dynamic attribute binding) -----------
|
|
1054
|
-
// Use TreeWalker instead of querySelectorAll('*')
|
|
1055
|
+
// Use TreeWalker instead of querySelectorAll('*') - avoids
|
|
1055
1056
|
// creating a flat array of every single descendant element.
|
|
1056
1057
|
// TreeWalker visits nodes lazily; FILTER_REJECT skips z-pre subtrees
|
|
1057
1058
|
// at the walker level (faster than per-node closest('[z-pre]') checks).
|
|
@@ -1189,8 +1190,8 @@ const _reservedKeys = new Set([
|
|
|
1189
1190
|
|
|
1190
1191
|
/**
|
|
1191
1192
|
* Register a component
|
|
1192
|
-
* @param {string} name
|
|
1193
|
-
* @param {object} definition
|
|
1193
|
+
* @param {string} name - tag name (must contain a hyphen, e.g. 'app-counter')
|
|
1194
|
+
* @param {object} definition - component definition
|
|
1194
1195
|
*/
|
|
1195
1196
|
export function component(name, definition) {
|
|
1196
1197
|
if (!name || typeof name !== 'string') {
|
|
@@ -1215,9 +1216,9 @@ export function component(name, definition) {
|
|
|
1215
1216
|
|
|
1216
1217
|
/**
|
|
1217
1218
|
* Mount a component into a target element
|
|
1218
|
-
* @param {string|Element} target
|
|
1219
|
-
* @param {string} componentName
|
|
1220
|
-
* @param {object} props
|
|
1219
|
+
* @param {string|Element} target - selector or element to mount into
|
|
1220
|
+
* @param {string} componentName - registered component name
|
|
1221
|
+
* @param {object} props - props to pass
|
|
1221
1222
|
* @returns {Component}
|
|
1222
1223
|
*/
|
|
1223
1224
|
export function mount(target, componentName, props = {}) {
|
|
@@ -1238,7 +1239,7 @@ export function mount(target, componentName, props = {}) {
|
|
|
1238
1239
|
|
|
1239
1240
|
/**
|
|
1240
1241
|
* Scan a container for custom component tags and auto-mount them
|
|
1241
|
-
* @param {Element} root
|
|
1242
|
+
* @param {Element} root - root element to scan (default: document.body)
|
|
1242
1243
|
*/
|
|
1243
1244
|
export function mountAll(root = document.body) {
|
|
1244
1245
|
for (const [name, def] of _registry) {
|
|
@@ -1263,7 +1264,7 @@ export function mountAll(root = document.body) {
|
|
|
1263
1264
|
[...tag.attributes].forEach(attr => {
|
|
1264
1265
|
if (attr.name.startsWith('@') || attr.name.startsWith('z-')) return;
|
|
1265
1266
|
|
|
1266
|
-
// Dynamic prop: :propName="expression"
|
|
1267
|
+
// Dynamic prop: :propName="expression" - evaluate in parent context
|
|
1267
1268
|
if (attr.name.startsWith(':')) {
|
|
1268
1269
|
const propName = attr.name.slice(1);
|
|
1269
1270
|
if (parentInstance) {
|
|
@@ -1272,7 +1273,7 @@ export function mountAll(root = document.body) {
|
|
|
1272
1273
|
{ props: parentInstance.props, refs: parentInstance.refs, computed: parentInstance.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
1273
1274
|
]);
|
|
1274
1275
|
} else {
|
|
1275
|
-
// No parent
|
|
1276
|
+
// No parent - try JSON parse
|
|
1276
1277
|
try { props[propName] = JSON.parse(attr.value); }
|
|
1277
1278
|
catch { props[propName] = attr.value; }
|
|
1278
1279
|
}
|
|
@@ -1321,8 +1322,8 @@ export function getRegistry() {
|
|
|
1321
1322
|
/**
|
|
1322
1323
|
* Pre-load a component's external templates and styles so the next mount
|
|
1323
1324
|
* renders synchronously (no blank flash while fetching).
|
|
1324
|
-
* Safe to call multiple times
|
|
1325
|
-
* @param {string} name
|
|
1325
|
+
* Safe to call multiple times - skips if already loaded.
|
|
1326
|
+
* @param {string} name - registered component name
|
|
1326
1327
|
* @returns {Promise<void>}
|
|
1327
1328
|
*/
|
|
1328
1329
|
export async function prefetch(name) {
|
|
@@ -1350,27 +1351,27 @@ const _globalStyles = new Map(); // url → <link> element
|
|
|
1350
1351
|
*
|
|
1351
1352
|
* $.style('app.css') // critical by default
|
|
1352
1353
|
* $.style(['app.css', 'theme.css']) // multiple files
|
|
1353
|
-
* $.style('/assets/global.css') // absolute
|
|
1354
|
+
* $.style('/assets/global.css') // absolute - used as-is
|
|
1354
1355
|
* $.style('app.css', { critical: false }) // opt out of FOUC prevention
|
|
1355
1356
|
*
|
|
1356
1357
|
* Options:
|
|
1357
|
-
* critical
|
|
1358
|
+
* critical - (boolean, default true) When true, zQuery injects a tiny
|
|
1358
1359
|
* inline style that hides the page (`visibility: hidden`) and
|
|
1359
1360
|
* removes it once the stylesheet has loaded. This prevents
|
|
1360
|
-
* FOUC (Flash of Unstyled Content) entirely
|
|
1361
|
+
* FOUC (Flash of Unstyled Content) entirely - no special
|
|
1361
1362
|
* markup needed in the HTML file. Set to false to load
|
|
1362
1363
|
* the stylesheet without blocking paint.
|
|
1363
|
-
* bg
|
|
1364
|
+
* bg - (string, default '#0d1117') Background color applied while
|
|
1364
1365
|
* the page is hidden during critical load. Prevents a white
|
|
1365
1366
|
* flash on dark-themed apps. Only used when critical is true.
|
|
1366
1367
|
*
|
|
1367
1368
|
* Duplicate URLs are ignored (idempotent).
|
|
1368
1369
|
*
|
|
1369
|
-
* @param {string|string[]} urls
|
|
1370
|
-
* @param {object} [opts]
|
|
1371
|
-
* @param {boolean} [opts.critical=true]
|
|
1372
|
-
* @param {string} [opts.bg]
|
|
1373
|
-
* @returns {{ remove: Function, ready: Promise }}
|
|
1370
|
+
* @param {string|string[]} urls - stylesheet URL(s) to load
|
|
1371
|
+
* @param {object} [opts] - options
|
|
1372
|
+
* @param {boolean} [opts.critical=true] - hide page until loaded (prevents FOUC)
|
|
1373
|
+
* @param {string} [opts.bg] - background color while hidden (default '#0d1117')
|
|
1374
|
+
* @returns {{ remove: Function, ready: Promise }} - .remove() to unload, .ready resolves when loaded
|
|
1374
1375
|
*/
|
|
1375
1376
|
export function style(urls, opts = {}) {
|
|
1376
1377
|
const callerBase = _detectCallerBase();
|
|
@@ -1379,7 +1380,7 @@ export function style(urls, opts = {}) {
|
|
|
1379
1380
|
const loadPromises = [];
|
|
1380
1381
|
|
|
1381
1382
|
// Critical mode (default: true): inject a tiny inline <style> that hides the
|
|
1382
|
-
// page and sets a background color. Fully self-contained
|
|
1383
|
+
// page and sets a background color. Fully self-contained - no markup needed
|
|
1383
1384
|
// in the HTML file. The style is removed once the sheet loads.
|
|
1384
1385
|
let _criticalStyle = null;
|
|
1385
1386
|
if (opts.critical !== false) {
|