zero-query 0.5.2 → 0.7.5
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 +12 -10
- package/cli/commands/build.js +7 -5
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +366 -0
- package/cli/commands/dev/server.js +158 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +147 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/cli/scaffold/styles/styles.css +1 -0
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2005 -216
- package/dist/zquery.min.js +3 -13
- package/index.d.ts +149 -1080
- package/index.js +18 -7
- package/package.json +9 -3
- package/src/component.js +186 -45
- package/src/core.js +327 -35
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +59 -6
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- package/tests/component.test.js +304 -0
- package/tests/core.test.js +726 -0
- package/tests/diff.test.js +194 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +334 -0
- package/tests/http.test.js +181 -0
- package/tests/reactive.test.js +191 -0
- package/tests/router.test.js +332 -0
- package/tests/store.test.js +253 -0
- package/tests/utils.test.js +353 -0
- package/types/collection.d.ts +368 -0
- package/types/component.d.ts +210 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +166 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +132 -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.js → dev.old.js} +0 -0
package/dist/zquery.js
CHANGED
|
@@ -1,13 +1,170 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.5
|
|
2
|
+
* zQuery (zeroQuery) v0.7.5
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
|
-
* (c) 2026 Anthony Wiedman
|
|
5
|
+
* (c) 2026 Anthony Wiedman - MIT License
|
|
6
6
|
*/
|
|
7
7
|
(function(global) {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
-
// --- src/
|
|
10
|
+
// --- src/errors.js -----------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* zQuery Errors — Structured error handling system
|
|
13
|
+
*
|
|
14
|
+
* Provides typed error classes and a configurable error handler so that
|
|
15
|
+
* errors surface consistently across all modules (reactive, component,
|
|
16
|
+
* router, store, expression parser, HTTP, etc.).
|
|
17
|
+
*
|
|
18
|
+
* Default behaviour: errors are logged via console.warn/error.
|
|
19
|
+
* Users can override with $.onError(handler) to integrate with their
|
|
20
|
+
* own logging, crash-reporting, or UI notification system.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Error codes — every zQuery error has a unique code for programmatic use
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const ErrorCode = Object.freeze({
|
|
27
|
+
// Reactive
|
|
28
|
+
REACTIVE_CALLBACK: 'ZQ_REACTIVE_CALLBACK',
|
|
29
|
+
SIGNAL_CALLBACK: 'ZQ_SIGNAL_CALLBACK',
|
|
30
|
+
EFFECT_EXEC: 'ZQ_EFFECT_EXEC',
|
|
31
|
+
|
|
32
|
+
// Expression parser
|
|
33
|
+
EXPR_PARSE: 'ZQ_EXPR_PARSE',
|
|
34
|
+
EXPR_EVAL: 'ZQ_EXPR_EVAL',
|
|
35
|
+
EXPR_UNSAFE_ACCESS: 'ZQ_EXPR_UNSAFE_ACCESS',
|
|
36
|
+
|
|
37
|
+
// Component
|
|
38
|
+
COMP_INVALID_NAME: 'ZQ_COMP_INVALID_NAME',
|
|
39
|
+
COMP_NOT_FOUND: 'ZQ_COMP_NOT_FOUND',
|
|
40
|
+
COMP_MOUNT_TARGET: 'ZQ_COMP_MOUNT_TARGET',
|
|
41
|
+
COMP_RENDER: 'ZQ_COMP_RENDER',
|
|
42
|
+
COMP_LIFECYCLE: 'ZQ_COMP_LIFECYCLE',
|
|
43
|
+
COMP_RESOURCE: 'ZQ_COMP_RESOURCE',
|
|
44
|
+
COMP_DIRECTIVE: 'ZQ_COMP_DIRECTIVE',
|
|
45
|
+
|
|
46
|
+
// Router
|
|
47
|
+
ROUTER_LOAD: 'ZQ_ROUTER_LOAD',
|
|
48
|
+
ROUTER_GUARD: 'ZQ_ROUTER_GUARD',
|
|
49
|
+
ROUTER_RESOLVE: 'ZQ_ROUTER_RESOLVE',
|
|
50
|
+
|
|
51
|
+
// Store
|
|
52
|
+
STORE_ACTION: 'ZQ_STORE_ACTION',
|
|
53
|
+
STORE_MIDDLEWARE: 'ZQ_STORE_MIDDLEWARE',
|
|
54
|
+
STORE_SUBSCRIBE: 'ZQ_STORE_SUBSCRIBE',
|
|
55
|
+
|
|
56
|
+
// HTTP
|
|
57
|
+
HTTP_REQUEST: 'ZQ_HTTP_REQUEST',
|
|
58
|
+
HTTP_TIMEOUT: 'ZQ_HTTP_TIMEOUT',
|
|
59
|
+
HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
|
|
60
|
+
HTTP_PARSE: 'ZQ_HTTP_PARSE',
|
|
61
|
+
|
|
62
|
+
// General
|
|
63
|
+
INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// ZQueryError — custom error class
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
class ZQueryError extends Error {
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} code — one of ErrorCode values
|
|
73
|
+
* @param {string} message — human-readable description
|
|
74
|
+
* @param {object} [context] — extra data (component name, expression, etc.)
|
|
75
|
+
* @param {Error} [cause] — original error
|
|
76
|
+
*/
|
|
77
|
+
constructor(code, message, context = {}, cause) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = 'ZQueryError';
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.context = context;
|
|
82
|
+
if (cause) this.cause = cause;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Global error handler
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
let _errorHandler = null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register a global error handler.
|
|
94
|
+
* Called whenever zQuery catches an error internally.
|
|
95
|
+
*
|
|
96
|
+
* @param {Function|null} handler — (error: ZQueryError) => void
|
|
97
|
+
*/
|
|
98
|
+
function onError(handler) {
|
|
99
|
+
_errorHandler = typeof handler === 'function' ? handler : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Report an error through the global handler and console.
|
|
104
|
+
* Non-throwing — used for recoverable errors in callbacks, lifecycle hooks, etc.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} code — ErrorCode
|
|
107
|
+
* @param {string} message
|
|
108
|
+
* @param {object} [context]
|
|
109
|
+
* @param {Error} [cause]
|
|
110
|
+
*/
|
|
111
|
+
function reportError(code, message, context = {}, cause) {
|
|
112
|
+
const err = cause instanceof ZQueryError
|
|
113
|
+
? cause
|
|
114
|
+
: new ZQueryError(code, message, context, cause);
|
|
115
|
+
|
|
116
|
+
// User handler gets first crack
|
|
117
|
+
if (_errorHandler) {
|
|
118
|
+
try { _errorHandler(err); } catch { /* prevent handler from crashing framework */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Always log for developer visibility
|
|
122
|
+
console.error(`[zQuery ${code}] ${message}`, context, cause || '');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wrap a callback so that thrown errors are caught, reported, and don't crash
|
|
127
|
+
* the current execution context.
|
|
128
|
+
*
|
|
129
|
+
* @param {Function} fn
|
|
130
|
+
* @param {string} code — ErrorCode to use if the callback throws
|
|
131
|
+
* @param {object} [context]
|
|
132
|
+
* @returns {Function}
|
|
133
|
+
*/
|
|
134
|
+
function guardCallback(fn, code, context = {}) {
|
|
135
|
+
return (...args) => {
|
|
136
|
+
try {
|
|
137
|
+
return fn(...args);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
reportError(code, err.message || 'Callback error', context, err);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate a required value is defined and of the expected type.
|
|
146
|
+
* Throws ZQueryError on failure (for fast-fail at API boundaries).
|
|
147
|
+
*
|
|
148
|
+
* @param {*} value
|
|
149
|
+
* @param {string} name — parameter name for error message
|
|
150
|
+
* @param {string} expectedType — 'string', 'function', 'object', etc.
|
|
151
|
+
*/
|
|
152
|
+
function validate(value, name, expectedType) {
|
|
153
|
+
if (value === undefined || value === null) {
|
|
154
|
+
throw new ZQueryError(
|
|
155
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
156
|
+
`"${name}" is required but got ${value}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (expectedType && typeof value !== expectedType) {
|
|
160
|
+
throw new ZQueryError(
|
|
161
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
162
|
+
`"${name}" must be a ${expectedType}, got ${typeof value}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- src/reactive.js ---------------------------------------------
|
|
11
168
|
/**
|
|
12
169
|
* zQuery Reactive — Proxy-based deep reactivity system
|
|
13
170
|
*
|
|
@@ -15,11 +172,16 @@
|
|
|
15
172
|
* Used internally by components and store for auto-updates.
|
|
16
173
|
*/
|
|
17
174
|
|
|
175
|
+
|
|
18
176
|
// ---------------------------------------------------------------------------
|
|
19
177
|
// Deep reactive proxy
|
|
20
178
|
// ---------------------------------------------------------------------------
|
|
21
179
|
function reactive(target, onChange, _path = '') {
|
|
22
180
|
if (typeof target !== 'object' || target === null) return target;
|
|
181
|
+
if (typeof onChange !== 'function') {
|
|
182
|
+
reportError(ErrorCode.REACTIVE_CALLBACK, 'reactive() onChange must be a function', { received: typeof onChange });
|
|
183
|
+
onChange = () => {};
|
|
184
|
+
}
|
|
23
185
|
|
|
24
186
|
const proxyCache = new WeakMap();
|
|
25
187
|
|
|
@@ -43,14 +205,22 @@ function reactive(target, onChange, _path = '') {
|
|
|
43
205
|
const old = obj[key];
|
|
44
206
|
if (old === value) return true;
|
|
45
207
|
obj[key] = value;
|
|
46
|
-
|
|
208
|
+
try {
|
|
209
|
+
onChange(key, value, old);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
reportError(ErrorCode.REACTIVE_CALLBACK, `Reactive onChange threw for key "${String(key)}"`, { key, value, old }, err);
|
|
212
|
+
}
|
|
47
213
|
return true;
|
|
48
214
|
},
|
|
49
215
|
|
|
50
216
|
deleteProperty(obj, key) {
|
|
51
217
|
const old = obj[key];
|
|
52
218
|
delete obj[key];
|
|
53
|
-
|
|
219
|
+
try {
|
|
220
|
+
onChange(key, undefined, old);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
reportError(ErrorCode.REACTIVE_CALLBACK, `Reactive onChange threw for key "${String(key)}"`, { key, old }, err);
|
|
223
|
+
}
|
|
54
224
|
return true;
|
|
55
225
|
}
|
|
56
226
|
};
|
|
@@ -85,7 +255,12 @@ class Signal {
|
|
|
85
255
|
peek() { return this._value; }
|
|
86
256
|
|
|
87
257
|
_notify() {
|
|
88
|
-
this._subscribers.forEach(fn =>
|
|
258
|
+
this._subscribers.forEach(fn => {
|
|
259
|
+
try { fn(); }
|
|
260
|
+
catch (err) {
|
|
261
|
+
reportError(ErrorCode.SIGNAL_CALLBACK, 'Signal subscriber threw', { signal: this }, err);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
89
264
|
}
|
|
90
265
|
|
|
91
266
|
subscribe(fn) {
|
|
@@ -128,13 +303,19 @@ function effect(fn) {
|
|
|
128
303
|
const execute = () => {
|
|
129
304
|
Signal._activeEffect = execute;
|
|
130
305
|
try { fn(); }
|
|
306
|
+
catch (err) {
|
|
307
|
+
reportError(ErrorCode.EFFECT_EXEC, 'Effect function threw', {}, err);
|
|
308
|
+
}
|
|
131
309
|
finally { Signal._activeEffect = null; }
|
|
132
310
|
};
|
|
133
311
|
execute();
|
|
134
|
-
return () => {
|
|
312
|
+
return () => {
|
|
313
|
+
// Remove this effect from all signals that track it
|
|
314
|
+
Signal._activeEffect = null;
|
|
315
|
+
};
|
|
135
316
|
}
|
|
136
317
|
|
|
137
|
-
// --- src/core.js
|
|
318
|
+
// --- src/core.js -------------------------------------------------
|
|
138
319
|
/**
|
|
139
320
|
* zQuery Core — Selector engine & chainable DOM collection
|
|
140
321
|
*
|
|
@@ -163,6 +344,11 @@ class ZQueryCollection {
|
|
|
163
344
|
return this.elements.map((el, i) => fn.call(el, i, el));
|
|
164
345
|
}
|
|
165
346
|
|
|
347
|
+
forEach(fn) {
|
|
348
|
+
this.elements.forEach((el, i) => fn(el, i, this.elements));
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
|
|
166
352
|
first() { return this.elements[0] || null; }
|
|
167
353
|
last() { return this.elements[this.length - 1] || null; }
|
|
168
354
|
eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
|
|
@@ -207,8 +393,96 @@ class ZQueryCollection {
|
|
|
207
393
|
return new ZQueryCollection(sibs);
|
|
208
394
|
}
|
|
209
395
|
|
|
210
|
-
next() {
|
|
211
|
-
|
|
396
|
+
next(selector) {
|
|
397
|
+
const els = this.elements.map(el => el.nextElementSibling).filter(Boolean);
|
|
398
|
+
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
prev(selector) {
|
|
402
|
+
const els = this.elements.map(el => el.previousElementSibling).filter(Boolean);
|
|
403
|
+
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
nextAll(selector) {
|
|
407
|
+
const result = [];
|
|
408
|
+
this.elements.forEach(el => {
|
|
409
|
+
let sib = el.nextElementSibling;
|
|
410
|
+
while (sib) {
|
|
411
|
+
if (!selector || sib.matches(selector)) result.push(sib);
|
|
412
|
+
sib = sib.nextElementSibling;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return new ZQueryCollection(result);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
nextUntil(selector, filter) {
|
|
419
|
+
const result = [];
|
|
420
|
+
this.elements.forEach(el => {
|
|
421
|
+
let sib = el.nextElementSibling;
|
|
422
|
+
while (sib) {
|
|
423
|
+
if (selector && sib.matches(selector)) break;
|
|
424
|
+
if (!filter || sib.matches(filter)) result.push(sib);
|
|
425
|
+
sib = sib.nextElementSibling;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return new ZQueryCollection(result);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
prevAll(selector) {
|
|
432
|
+
const result = [];
|
|
433
|
+
this.elements.forEach(el => {
|
|
434
|
+
let sib = el.previousElementSibling;
|
|
435
|
+
while (sib) {
|
|
436
|
+
if (!selector || sib.matches(selector)) result.push(sib);
|
|
437
|
+
sib = sib.previousElementSibling;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return new ZQueryCollection(result);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
prevUntil(selector, filter) {
|
|
444
|
+
const result = [];
|
|
445
|
+
this.elements.forEach(el => {
|
|
446
|
+
let sib = el.previousElementSibling;
|
|
447
|
+
while (sib) {
|
|
448
|
+
if (selector && sib.matches(selector)) break;
|
|
449
|
+
if (!filter || sib.matches(filter)) result.push(sib);
|
|
450
|
+
sib = sib.previousElementSibling;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return new ZQueryCollection(result);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
parents(selector) {
|
|
457
|
+
const result = [];
|
|
458
|
+
this.elements.forEach(el => {
|
|
459
|
+
let parent = el.parentElement;
|
|
460
|
+
while (parent) {
|
|
461
|
+
if (!selector || parent.matches(selector)) result.push(parent);
|
|
462
|
+
parent = parent.parentElement;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return new ZQueryCollection([...new Set(result)]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
parentsUntil(selector, filter) {
|
|
469
|
+
const result = [];
|
|
470
|
+
this.elements.forEach(el => {
|
|
471
|
+
let parent = el.parentElement;
|
|
472
|
+
while (parent) {
|
|
473
|
+
if (selector && parent.matches(selector)) break;
|
|
474
|
+
if (!filter || parent.matches(filter)) result.push(parent);
|
|
475
|
+
parent = parent.parentElement;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return new ZQueryCollection([...new Set(result)]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
contents() {
|
|
482
|
+
const result = [];
|
|
483
|
+
this.elements.forEach(el => result.push(...el.childNodes));
|
|
484
|
+
return new ZQueryCollection(result);
|
|
485
|
+
}
|
|
212
486
|
|
|
213
487
|
filter(selector) {
|
|
214
488
|
if (typeof selector === 'function') {
|
|
@@ -228,6 +502,42 @@ class ZQueryCollection {
|
|
|
228
502
|
return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
|
|
229
503
|
}
|
|
230
504
|
|
|
505
|
+
is(selector) {
|
|
506
|
+
if (typeof selector === 'function') {
|
|
507
|
+
return this.elements.some((el, i) => selector.call(el, i, el));
|
|
508
|
+
}
|
|
509
|
+
return this.elements.some(el => el.matches(selector));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
slice(start, end) {
|
|
513
|
+
return new ZQueryCollection(this.elements.slice(start, end));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
add(selector, context) {
|
|
517
|
+
const toAdd = (selector instanceof ZQueryCollection)
|
|
518
|
+
? selector.elements
|
|
519
|
+
: (selector instanceof Node)
|
|
520
|
+
? [selector]
|
|
521
|
+
: Array.from((context || document).querySelectorAll(selector));
|
|
522
|
+
return new ZQueryCollection([...this.elements, ...toAdd]);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
get(index) {
|
|
526
|
+
if (index === undefined) return [...this.elements];
|
|
527
|
+
return index < 0 ? this.elements[this.length + index] : this.elements[index];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
index(selector) {
|
|
531
|
+
if (selector === undefined) {
|
|
532
|
+
const el = this.first();
|
|
533
|
+
return el ? Array.from(el.parentElement.children).indexOf(el) : -1;
|
|
534
|
+
}
|
|
535
|
+
const target = (typeof selector === 'string')
|
|
536
|
+
? document.querySelector(selector)
|
|
537
|
+
: selector;
|
|
538
|
+
return this.elements.indexOf(target);
|
|
539
|
+
}
|
|
540
|
+
|
|
231
541
|
// --- Classes -------------------------------------------------------------
|
|
232
542
|
|
|
233
543
|
addClass(...names) {
|
|
@@ -240,8 +550,12 @@ class ZQueryCollection {
|
|
|
240
550
|
return this.each((_, el) => el.classList.remove(...classes));
|
|
241
551
|
}
|
|
242
552
|
|
|
243
|
-
toggleClass(
|
|
244
|
-
|
|
553
|
+
toggleClass(...args) {
|
|
554
|
+
const force = typeof args[args.length - 1] === 'boolean' ? args.pop() : undefined;
|
|
555
|
+
const classes = args.flatMap(n => n.split(/\s+/));
|
|
556
|
+
return this.each((_, el) => {
|
|
557
|
+
classes.forEach(c => force !== undefined ? el.classList.toggle(c, force) : el.classList.toggle(c));
|
|
558
|
+
});
|
|
245
559
|
}
|
|
246
560
|
|
|
247
561
|
hasClass(name) {
|
|
@@ -295,6 +609,60 @@ class ZQueryCollection {
|
|
|
295
609
|
return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
|
|
296
610
|
}
|
|
297
611
|
|
|
612
|
+
scrollTop(value) {
|
|
613
|
+
if (value === undefined) {
|
|
614
|
+
const el = this.first();
|
|
615
|
+
return el === window ? window.scrollY : el?.scrollTop;
|
|
616
|
+
}
|
|
617
|
+
return this.each((_, el) => {
|
|
618
|
+
if (el === window) window.scrollTo(window.scrollX, value);
|
|
619
|
+
else el.scrollTop = value;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
scrollLeft(value) {
|
|
624
|
+
if (value === undefined) {
|
|
625
|
+
const el = this.first();
|
|
626
|
+
return el === window ? window.scrollX : el?.scrollLeft;
|
|
627
|
+
}
|
|
628
|
+
return this.each((_, el) => {
|
|
629
|
+
if (el === window) window.scrollTo(value, window.scrollY);
|
|
630
|
+
else el.scrollLeft = value;
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
innerWidth() {
|
|
635
|
+
const el = this.first();
|
|
636
|
+
return el?.clientWidth;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
innerHeight() {
|
|
640
|
+
const el = this.first();
|
|
641
|
+
return el?.clientHeight;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
outerWidth(includeMargin = false) {
|
|
645
|
+
const el = this.first();
|
|
646
|
+
if (!el) return undefined;
|
|
647
|
+
let w = el.offsetWidth;
|
|
648
|
+
if (includeMargin) {
|
|
649
|
+
const style = getComputedStyle(el);
|
|
650
|
+
w += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
|
|
651
|
+
}
|
|
652
|
+
return w;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
outerHeight(includeMargin = false) {
|
|
656
|
+
const el = this.first();
|
|
657
|
+
if (!el) return undefined;
|
|
658
|
+
let h = el.offsetHeight;
|
|
659
|
+
if (includeMargin) {
|
|
660
|
+
const style = getComputedStyle(el);
|
|
661
|
+
h += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
|
|
662
|
+
}
|
|
663
|
+
return h;
|
|
664
|
+
}
|
|
665
|
+
|
|
298
666
|
// --- Content -------------------------------------------------------------
|
|
299
667
|
|
|
300
668
|
html(content) {
|
|
@@ -374,6 +742,73 @@ class ZQueryCollection {
|
|
|
374
742
|
});
|
|
375
743
|
}
|
|
376
744
|
|
|
745
|
+
appendTo(target) {
|
|
746
|
+
const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
747
|
+
if (dest) this.each((_, el) => dest.appendChild(el));
|
|
748
|
+
return this;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
prependTo(target) {
|
|
752
|
+
const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
753
|
+
if (dest) this.each((_, el) => dest.insertBefore(el, dest.firstChild));
|
|
754
|
+
return this;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
insertAfter(target) {
|
|
758
|
+
const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
759
|
+
if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref.nextSibling));
|
|
760
|
+
return this;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
insertBefore(target) {
|
|
764
|
+
const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
765
|
+
if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref));
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
replaceAll(target) {
|
|
770
|
+
const targets = typeof target === 'string'
|
|
771
|
+
? Array.from(document.querySelectorAll(target))
|
|
772
|
+
: target instanceof ZQueryCollection ? target.elements : [target];
|
|
773
|
+
targets.forEach((t, i) => {
|
|
774
|
+
const nodes = i === 0 ? this.elements : this.elements.map(el => el.cloneNode(true));
|
|
775
|
+
nodes.forEach(el => t.parentNode.insertBefore(el, t));
|
|
776
|
+
t.remove();
|
|
777
|
+
});
|
|
778
|
+
return this;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
unwrap(selector) {
|
|
782
|
+
this.elements.forEach(el => {
|
|
783
|
+
const parent = el.parentElement;
|
|
784
|
+
if (!parent || parent === document.body) return;
|
|
785
|
+
if (selector && !parent.matches(selector)) return;
|
|
786
|
+
parent.replaceWith(...parent.childNodes);
|
|
787
|
+
});
|
|
788
|
+
return this;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
wrapAll(wrapper) {
|
|
792
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
793
|
+
const first = this.first();
|
|
794
|
+
if (!first) return this;
|
|
795
|
+
first.parentNode.insertBefore(w, first);
|
|
796
|
+
this.each((_, el) => w.appendChild(el));
|
|
797
|
+
return this;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
wrapInner(wrapper) {
|
|
801
|
+
return this.each((_, el) => {
|
|
802
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
803
|
+
while (el.firstChild) w.appendChild(el.firstChild);
|
|
804
|
+
el.appendChild(w);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
detach() {
|
|
809
|
+
return this.each((_, el) => el.remove());
|
|
810
|
+
}
|
|
811
|
+
|
|
377
812
|
// --- Visibility ----------------------------------------------------------
|
|
378
813
|
|
|
379
814
|
show(display = '') {
|
|
@@ -399,9 +834,10 @@ class ZQueryCollection {
|
|
|
399
834
|
events.forEach(evt => {
|
|
400
835
|
if (typeof selectorOrHandler === 'function') {
|
|
401
836
|
el.addEventListener(evt, selectorOrHandler);
|
|
402
|
-
} else {
|
|
403
|
-
// Delegated event
|
|
837
|
+
} else if (typeof selectorOrHandler === 'string') {
|
|
838
|
+
// Delegated event — only works on elements that support closest()
|
|
404
839
|
el.addEventListener(evt, (e) => {
|
|
840
|
+
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
405
841
|
const target = e.target.closest(selectorOrHandler);
|
|
406
842
|
if (target && el.contains(target)) handler.call(target, e);
|
|
407
843
|
});
|
|
@@ -434,6 +870,10 @@ class ZQueryCollection {
|
|
|
434
870
|
submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
|
|
435
871
|
focus() { this.first()?.focus(); return this; }
|
|
436
872
|
blur() { this.first()?.blur(); return this; }
|
|
873
|
+
hover(enterFn, leaveFn) {
|
|
874
|
+
this.on('mouseenter', enterFn);
|
|
875
|
+
return this.on('mouseleave', leaveFn || enterFn);
|
|
876
|
+
}
|
|
437
877
|
|
|
438
878
|
// --- Animation -----------------------------------------------------------
|
|
439
879
|
|
|
@@ -465,6 +905,40 @@ class ZQueryCollection {
|
|
|
465
905
|
return this.animate({ opacity: '0' }, duration).then(col => col.hide());
|
|
466
906
|
}
|
|
467
907
|
|
|
908
|
+
fadeToggle(duration = 300) {
|
|
909
|
+
return Promise.all(this.elements.map(el => {
|
|
910
|
+
const visible = getComputedStyle(el).opacity !== '0' && getComputedStyle(el).display !== 'none';
|
|
911
|
+
const col = new ZQueryCollection([el]);
|
|
912
|
+
return visible ? col.fadeOut(duration) : col.fadeIn(duration);
|
|
913
|
+
})).then(() => this);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
fadeTo(duration, opacity) {
|
|
917
|
+
return this.animate({ opacity: String(opacity) }, duration);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
slideDown(duration = 300) {
|
|
921
|
+
return this.each((_, el) => {
|
|
922
|
+
el.style.display = '';
|
|
923
|
+
el.style.overflow = 'hidden';
|
|
924
|
+
const h = el.scrollHeight + 'px';
|
|
925
|
+
el.style.maxHeight = '0';
|
|
926
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
927
|
+
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
928
|
+
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
slideUp(duration = 300) {
|
|
933
|
+
return this.each((_, el) => {
|
|
934
|
+
el.style.overflow = 'hidden';
|
|
935
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
936
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
937
|
+
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
938
|
+
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
468
942
|
slideToggle(duration = 300) {
|
|
469
943
|
return this.each((_, el) => {
|
|
470
944
|
if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
|
|
@@ -511,163 +985,1252 @@ class ZQueryCollection {
|
|
|
511
985
|
|
|
512
986
|
|
|
513
987
|
// ---------------------------------------------------------------------------
|
|
514
|
-
// Helper — create document fragment from HTML string
|
|
988
|
+
// Helper — create document fragment from HTML string
|
|
989
|
+
// ---------------------------------------------------------------------------
|
|
990
|
+
function createFragment(html) {
|
|
991
|
+
const tpl = document.createElement('template');
|
|
992
|
+
tpl.innerHTML = html.trim();
|
|
993
|
+
return tpl.content;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
// ---------------------------------------------------------------------------
|
|
998
|
+
// $() — main selector / creator (returns ZQueryCollection, like jQuery)
|
|
999
|
+
// ---------------------------------------------------------------------------
|
|
1000
|
+
function query(selector, context) {
|
|
1001
|
+
// null / undefined
|
|
1002
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1003
|
+
|
|
1004
|
+
// Already a collection — return as-is
|
|
1005
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1006
|
+
|
|
1007
|
+
// DOM element or Window — wrap in collection
|
|
1008
|
+
if (selector instanceof Node || selector === window) {
|
|
1009
|
+
return new ZQueryCollection([selector]);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// NodeList / HTMLCollection / Array — wrap in collection
|
|
1013
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1014
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// HTML string → create elements, wrap in collection
|
|
1018
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
1019
|
+
const fragment = createFragment(selector);
|
|
1020
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// CSS selector string → querySelectorAll (collection)
|
|
1024
|
+
if (typeof selector === 'string') {
|
|
1025
|
+
const root = context
|
|
1026
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1027
|
+
: document;
|
|
1028
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return new ZQueryCollection([]);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
// ---------------------------------------------------------------------------
|
|
1036
|
+
// $.all() — collection selector (returns ZQueryCollection for CSS selectors)
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
function queryAll(selector, context) {
|
|
1039
|
+
// null / undefined
|
|
1040
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1041
|
+
|
|
1042
|
+
// Already a collection
|
|
1043
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1044
|
+
|
|
1045
|
+
// DOM element or Window
|
|
1046
|
+
if (selector instanceof Node || selector === window) {
|
|
1047
|
+
return new ZQueryCollection([selector]);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// NodeList / HTMLCollection / Array
|
|
1051
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1052
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// HTML string → create elements
|
|
1056
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
1057
|
+
const fragment = createFragment(selector);
|
|
1058
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// CSS selector string → querySelectorAll (collection)
|
|
1062
|
+
if (typeof selector === 'string') {
|
|
1063
|
+
const root = context
|
|
1064
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1065
|
+
: document;
|
|
1066
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return new ZQueryCollection([]);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
// ---------------------------------------------------------------------------
|
|
1074
|
+
// Quick-ref shortcuts, on $ namespace)
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
query.id = (id) => document.getElementById(id);
|
|
1077
|
+
query.class = (name) => document.querySelector(`.${name}`);
|
|
1078
|
+
query.classes = (name) => new ZQueryCollection(Array.from(document.getElementsByClassName(name)));
|
|
1079
|
+
query.tag = (name) => new ZQueryCollection(Array.from(document.getElementsByTagName(name)));
|
|
1080
|
+
Object.defineProperty(query, 'name', {
|
|
1081
|
+
value: (name) => new ZQueryCollection(Array.from(document.getElementsByName(name))),
|
|
1082
|
+
writable: true, configurable: true
|
|
1083
|
+
});
|
|
1084
|
+
query.children = (parentId) => {
|
|
1085
|
+
const p = document.getElementById(parentId);
|
|
1086
|
+
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
// Create element shorthand — returns ZQueryCollection for chaining
|
|
1090
|
+
query.create = (tag, attrs = {}, ...children) => {
|
|
1091
|
+
const el = document.createElement(tag);
|
|
1092
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1093
|
+
if (k === 'class') el.className = v;
|
|
1094
|
+
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1095
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
|
|
1096
|
+
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1097
|
+
else el.setAttribute(k, v);
|
|
1098
|
+
}
|
|
1099
|
+
children.flat().forEach(child => {
|
|
1100
|
+
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
1101
|
+
else if (child instanceof Node) el.appendChild(child);
|
|
1102
|
+
});
|
|
1103
|
+
return new ZQueryCollection(el);
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// DOM ready
|
|
1107
|
+
query.ready = (fn) => {
|
|
1108
|
+
if (document.readyState !== 'loading') fn();
|
|
1109
|
+
else document.addEventListener('DOMContentLoaded', fn);
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// Global event listeners — supports direct, delegated, and target-bound forms
|
|
1113
|
+
// $.on('keydown', handler) → direct listener on document
|
|
1114
|
+
// $.on('click', '.btn', handler) → delegated via closest()
|
|
1115
|
+
// $.on('scroll', window, handler) → direct listener on target
|
|
1116
|
+
query.on = (event, selectorOrHandler, handler) => {
|
|
1117
|
+
if (typeof selectorOrHandler === 'function') {
|
|
1118
|
+
// 2-arg: direct document listener (keydown, resize, etc.)
|
|
1119
|
+
document.addEventListener(event, selectorOrHandler);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
// EventTarget (window, element, etc.) — direct listener on target
|
|
1123
|
+
if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
|
|
1124
|
+
selectorOrHandler.addEventListener(event, handler);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
// 3-arg string: delegated
|
|
1128
|
+
document.addEventListener(event, (e) => {
|
|
1129
|
+
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1130
|
+
const target = e.target.closest(selectorOrHandler);
|
|
1131
|
+
if (target) handler.call(target, e);
|
|
1132
|
+
});
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
// Remove a direct global listener
|
|
1136
|
+
query.off = (event, handler) => {
|
|
1137
|
+
document.removeEventListener(event, handler);
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// Extend collection prototype (like $.fn in jQuery)
|
|
1141
|
+
query.fn = ZQueryCollection.prototype;
|
|
1142
|
+
|
|
1143
|
+
// --- src/expression.js -------------------------------------------
|
|
1144
|
+
/**
|
|
1145
|
+
* zQuery Expression Parser — CSP-safe expression evaluator
|
|
1146
|
+
*
|
|
1147
|
+
* Replaces `new Function()` / `eval()` with a hand-written parser that
|
|
1148
|
+
* evaluates expressions safely without violating Content Security Policy.
|
|
1149
|
+
*
|
|
1150
|
+
* Supports:
|
|
1151
|
+
* - Property access: user.name, items[0], items[i]
|
|
1152
|
+
* - Method calls: items.length, str.toUpperCase()
|
|
1153
|
+
* - Arithmetic: a + b, count * 2, i % 2
|
|
1154
|
+
* - Comparison: a === b, count > 0, x != null
|
|
1155
|
+
* - Logical: a && b, a || b, !a
|
|
1156
|
+
* - Ternary: a ? b : c
|
|
1157
|
+
* - Typeof: typeof x
|
|
1158
|
+
* - Unary: -a, +a, !a
|
|
1159
|
+
* - Literals: 42, 'hello', "world", true, false, null, undefined
|
|
1160
|
+
* - Template literals: `Hello ${name}`
|
|
1161
|
+
* - Array literals: [1, 2, 3]
|
|
1162
|
+
* - Object literals: { foo: 'bar', baz: 1 }
|
|
1163
|
+
* - Grouping: (a + b) * c
|
|
1164
|
+
* - Nullish coalescing: a ?? b
|
|
1165
|
+
* - Optional chaining: a?.b, a?.[b], a?.()
|
|
1166
|
+
* - Arrow functions: x => x.id, (a, b) => a + b
|
|
1167
|
+
*/
|
|
1168
|
+
|
|
1169
|
+
// Token types
|
|
1170
|
+
const T = {
|
|
1171
|
+
NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// Operator precedence (higher = binds tighter)
|
|
1175
|
+
const PREC = {
|
|
1176
|
+
'??': 2,
|
|
1177
|
+
'||': 3,
|
|
1178
|
+
'&&': 4,
|
|
1179
|
+
'==': 8, '!=': 8, '===': 8, '!==': 8,
|
|
1180
|
+
'<': 9, '>': 9, '<=': 9, '>=': 9, 'instanceof': 9, 'in': 9,
|
|
1181
|
+
'+': 11, '-': 11,
|
|
1182
|
+
'*': 12, '/': 12, '%': 12,
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const KEYWORDS = new Set([
|
|
1186
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
1187
|
+
'new', 'void'
|
|
1188
|
+
]);
|
|
1189
|
+
|
|
1190
|
+
// ---------------------------------------------------------------------------
|
|
1191
|
+
// Tokenizer
|
|
1192
|
+
// ---------------------------------------------------------------------------
|
|
1193
|
+
function tokenize(expr) {
|
|
1194
|
+
const tokens = [];
|
|
1195
|
+
let i = 0;
|
|
1196
|
+
const len = expr.length;
|
|
1197
|
+
|
|
1198
|
+
while (i < len) {
|
|
1199
|
+
const ch = expr[i];
|
|
1200
|
+
|
|
1201
|
+
// Whitespace
|
|
1202
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; }
|
|
1203
|
+
|
|
1204
|
+
// Numbers
|
|
1205
|
+
if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < len && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {
|
|
1206
|
+
let num = '';
|
|
1207
|
+
if (ch === '0' && i + 1 < len && (expr[i + 1] === 'x' || expr[i + 1] === 'X')) {
|
|
1208
|
+
num = '0x'; i += 2;
|
|
1209
|
+
while (i < len && /[0-9a-fA-F]/.test(expr[i])) num += expr[i++];
|
|
1210
|
+
} else {
|
|
1211
|
+
while (i < len && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) num += expr[i++];
|
|
1212
|
+
if (i < len && (expr[i] === 'e' || expr[i] === 'E')) {
|
|
1213
|
+
num += expr[i++];
|
|
1214
|
+
if (i < len && (expr[i] === '+' || expr[i] === '-')) num += expr[i++];
|
|
1215
|
+
while (i < len && expr[i] >= '0' && expr[i] <= '9') num += expr[i++];
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
tokens.push({ t: T.NUM, v: Number(num) });
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Strings
|
|
1223
|
+
if (ch === "'" || ch === '"') {
|
|
1224
|
+
const quote = ch;
|
|
1225
|
+
let str = '';
|
|
1226
|
+
i++;
|
|
1227
|
+
while (i < len && expr[i] !== quote) {
|
|
1228
|
+
if (expr[i] === '\\' && i + 1 < len) {
|
|
1229
|
+
const esc = expr[++i];
|
|
1230
|
+
if (esc === 'n') str += '\n';
|
|
1231
|
+
else if (esc === 't') str += '\t';
|
|
1232
|
+
else if (esc === 'r') str += '\r';
|
|
1233
|
+
else if (esc === '\\') str += '\\';
|
|
1234
|
+
else if (esc === quote) str += quote;
|
|
1235
|
+
else str += esc;
|
|
1236
|
+
} else {
|
|
1237
|
+
str += expr[i];
|
|
1238
|
+
}
|
|
1239
|
+
i++;
|
|
1240
|
+
}
|
|
1241
|
+
i++; // closing quote
|
|
1242
|
+
tokens.push({ t: T.STR, v: str });
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Template literals
|
|
1247
|
+
if (ch === '`') {
|
|
1248
|
+
const parts = []; // alternating: string, expr, string, expr, ...
|
|
1249
|
+
let str = '';
|
|
1250
|
+
i++;
|
|
1251
|
+
while (i < len && expr[i] !== '`') {
|
|
1252
|
+
if (expr[i] === '$' && i + 1 < len && expr[i + 1] === '{') {
|
|
1253
|
+
parts.push(str);
|
|
1254
|
+
str = '';
|
|
1255
|
+
i += 2;
|
|
1256
|
+
let depth = 1;
|
|
1257
|
+
let inner = '';
|
|
1258
|
+
while (i < len && depth > 0) {
|
|
1259
|
+
if (expr[i] === '{') depth++;
|
|
1260
|
+
else if (expr[i] === '}') { depth--; if (depth === 0) break; }
|
|
1261
|
+
inner += expr[i++];
|
|
1262
|
+
}
|
|
1263
|
+
i++; // closing }
|
|
1264
|
+
parts.push({ expr: inner });
|
|
1265
|
+
} else {
|
|
1266
|
+
if (expr[i] === '\\' && i + 1 < len) { str += expr[++i]; }
|
|
1267
|
+
else str += expr[i];
|
|
1268
|
+
i++;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
i++; // closing backtick
|
|
1272
|
+
parts.push(str);
|
|
1273
|
+
tokens.push({ t: T.TMPL, v: parts });
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Identifiers & keywords
|
|
1278
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
1279
|
+
let ident = '';
|
|
1280
|
+
while (i < len && /[\w$]/.test(expr[i])) ident += expr[i++];
|
|
1281
|
+
tokens.push({ t: T.IDENT, v: ident });
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Multi-char operators
|
|
1286
|
+
const two = expr.slice(i, i + 3);
|
|
1287
|
+
if (two === '===' || two === '!==' || two === '?.') {
|
|
1288
|
+
if (two === '?.') {
|
|
1289
|
+
tokens.push({ t: T.OP, v: '?.' });
|
|
1290
|
+
i += 2;
|
|
1291
|
+
} else {
|
|
1292
|
+
tokens.push({ t: T.OP, v: two });
|
|
1293
|
+
i += 3;
|
|
1294
|
+
}
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const pair = expr.slice(i, i + 2);
|
|
1298
|
+
if (pair === '==' || pair === '!=' || pair === '<=' || pair === '>=' ||
|
|
1299
|
+
pair === '&&' || pair === '||' || pair === '??' || pair === '?.' ||
|
|
1300
|
+
pair === '=>') {
|
|
1301
|
+
tokens.push({ t: T.OP, v: pair });
|
|
1302
|
+
i += 2;
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Single char operators and punctuation
|
|
1307
|
+
if ('+-*/%'.includes(ch)) {
|
|
1308
|
+
tokens.push({ t: T.OP, v: ch });
|
|
1309
|
+
i++; continue;
|
|
1310
|
+
}
|
|
1311
|
+
if ('<>=!'.includes(ch)) {
|
|
1312
|
+
tokens.push({ t: T.OP, v: ch });
|
|
1313
|
+
i++; continue;
|
|
1314
|
+
}
|
|
1315
|
+
if ('()[]{},.?:'.includes(ch)) {
|
|
1316
|
+
tokens.push({ t: T.PUNC, v: ch });
|
|
1317
|
+
i++; continue;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Unknown — skip
|
|
1321
|
+
i++;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
tokens.push({ t: T.EOF, v: null });
|
|
1325
|
+
return tokens;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ---------------------------------------------------------------------------
|
|
1329
|
+
// Parser — Pratt (precedence climbing)
|
|
1330
|
+
// ---------------------------------------------------------------------------
|
|
1331
|
+
class Parser {
|
|
1332
|
+
constructor(tokens, scope) {
|
|
1333
|
+
this.tokens = tokens;
|
|
1334
|
+
this.pos = 0;
|
|
1335
|
+
this.scope = scope;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
peek() { return this.tokens[this.pos]; }
|
|
1339
|
+
next() { return this.tokens[this.pos++]; }
|
|
1340
|
+
|
|
1341
|
+
expect(type, val) {
|
|
1342
|
+
const t = this.next();
|
|
1343
|
+
if (t.t !== type || (val !== undefined && t.v !== val)) {
|
|
1344
|
+
throw new Error(`Expected ${val || type} but got ${t.v}`);
|
|
1345
|
+
}
|
|
1346
|
+
return t;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
match(type, val) {
|
|
1350
|
+
const t = this.peek();
|
|
1351
|
+
if (t.t === type && (val === undefined || t.v === val)) {
|
|
1352
|
+
return this.next();
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Main entry
|
|
1358
|
+
parse() {
|
|
1359
|
+
const result = this.parseExpression(0);
|
|
1360
|
+
return result;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Precedence climbing
|
|
1364
|
+
parseExpression(minPrec) {
|
|
1365
|
+
let left = this.parseUnary();
|
|
1366
|
+
|
|
1367
|
+
while (true) {
|
|
1368
|
+
const tok = this.peek();
|
|
1369
|
+
|
|
1370
|
+
// Ternary
|
|
1371
|
+
if (tok.t === T.PUNC && tok.v === '?') {
|
|
1372
|
+
// Distinguish ternary ? from optional chaining ?.
|
|
1373
|
+
if (this.tokens[this.pos + 1]?.v !== '.') {
|
|
1374
|
+
if (1 <= minPrec) break; // ternary has very low precedence
|
|
1375
|
+
this.next(); // consume ?
|
|
1376
|
+
const truthy = this.parseExpression(0);
|
|
1377
|
+
this.expect(T.PUNC, ':');
|
|
1378
|
+
const falsy = this.parseExpression(1);
|
|
1379
|
+
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Binary operators
|
|
1385
|
+
if (tok.t === T.OP && tok.v in PREC) {
|
|
1386
|
+
const prec = PREC[tok.v];
|
|
1387
|
+
if (prec <= minPrec) break;
|
|
1388
|
+
this.next();
|
|
1389
|
+
const right = this.parseExpression(prec);
|
|
1390
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// instanceof and in as binary operators
|
|
1395
|
+
if (tok.t === T.IDENT && (tok.v === 'instanceof' || tok.v === 'in') && PREC[tok.v] > minPrec) {
|
|
1396
|
+
const prec = PREC[tok.v];
|
|
1397
|
+
this.next();
|
|
1398
|
+
const right = this.parseExpression(prec);
|
|
1399
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
break;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
return left;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
parseUnary() {
|
|
1410
|
+
const tok = this.peek();
|
|
1411
|
+
|
|
1412
|
+
// typeof
|
|
1413
|
+
if (tok.t === T.IDENT && tok.v === 'typeof') {
|
|
1414
|
+
this.next();
|
|
1415
|
+
const arg = this.parseUnary();
|
|
1416
|
+
return { type: 'typeof', arg };
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// void
|
|
1420
|
+
if (tok.t === T.IDENT && tok.v === 'void') {
|
|
1421
|
+
this.next();
|
|
1422
|
+
this.parseUnary(); // evaluate but discard
|
|
1423
|
+
return { type: 'literal', value: undefined };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// !expr
|
|
1427
|
+
if (tok.t === T.OP && tok.v === '!') {
|
|
1428
|
+
this.next();
|
|
1429
|
+
const arg = this.parseUnary();
|
|
1430
|
+
return { type: 'not', arg };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// -expr, +expr
|
|
1434
|
+
if (tok.t === T.OP && (tok.v === '-' || tok.v === '+')) {
|
|
1435
|
+
this.next();
|
|
1436
|
+
const arg = this.parseUnary();
|
|
1437
|
+
return { type: 'unary', op: tok.v, arg };
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
return this.parsePostfix();
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
parsePostfix() {
|
|
1444
|
+
let left = this.parsePrimary();
|
|
1445
|
+
|
|
1446
|
+
while (true) {
|
|
1447
|
+
const tok = this.peek();
|
|
1448
|
+
|
|
1449
|
+
// Property access: a.b
|
|
1450
|
+
if (tok.t === T.PUNC && tok.v === '.') {
|
|
1451
|
+
this.next();
|
|
1452
|
+
const prop = this.next();
|
|
1453
|
+
left = { type: 'member', obj: left, prop: prop.v, computed: false };
|
|
1454
|
+
// Check for method call: a.b()
|
|
1455
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1456
|
+
left = this._parseCall(left);
|
|
1457
|
+
}
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Optional chaining: a?.b, a?.[b], a?.()
|
|
1462
|
+
if (tok.t === T.OP && tok.v === '?.') {
|
|
1463
|
+
this.next();
|
|
1464
|
+
const next = this.peek();
|
|
1465
|
+
if (next.t === T.PUNC && next.v === '[') {
|
|
1466
|
+
// a?.[expr]
|
|
1467
|
+
this.next();
|
|
1468
|
+
const prop = this.parseExpression(0);
|
|
1469
|
+
this.expect(T.PUNC, ']');
|
|
1470
|
+
left = { type: 'optional_member', obj: left, prop, computed: true };
|
|
1471
|
+
} else if (next.t === T.PUNC && next.v === '(') {
|
|
1472
|
+
// a?.()
|
|
1473
|
+
left = { type: 'optional_call', callee: left, args: this._parseArgs() };
|
|
1474
|
+
} else {
|
|
1475
|
+
// a?.b
|
|
1476
|
+
const prop = this.next();
|
|
1477
|
+
left = { type: 'optional_member', obj: left, prop: prop.v, computed: false };
|
|
1478
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1479
|
+
left = this._parseCall(left);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Computed access: a[b]
|
|
1486
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
1487
|
+
this.next();
|
|
1488
|
+
const prop = this.parseExpression(0);
|
|
1489
|
+
this.expect(T.PUNC, ']');
|
|
1490
|
+
left = { type: 'member', obj: left, prop, computed: true };
|
|
1491
|
+
// Check for method call: a[b]()
|
|
1492
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1493
|
+
left = this._parseCall(left);
|
|
1494
|
+
}
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Function call: fn()
|
|
1499
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
1500
|
+
left = this._parseCall(left);
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
break;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return left;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
_parseCall(callee) {
|
|
1511
|
+
const args = this._parseArgs();
|
|
1512
|
+
return { type: 'call', callee, args };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
_parseArgs() {
|
|
1516
|
+
this.expect(T.PUNC, '(');
|
|
1517
|
+
const args = [];
|
|
1518
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
1519
|
+
args.push(this.parseExpression(0));
|
|
1520
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1521
|
+
}
|
|
1522
|
+
this.expect(T.PUNC, ')');
|
|
1523
|
+
return args;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
parsePrimary() {
|
|
1527
|
+
const tok = this.peek();
|
|
1528
|
+
|
|
1529
|
+
// Number literal
|
|
1530
|
+
if (tok.t === T.NUM) {
|
|
1531
|
+
this.next();
|
|
1532
|
+
return { type: 'literal', value: tok.v };
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// String literal
|
|
1536
|
+
if (tok.t === T.STR) {
|
|
1537
|
+
this.next();
|
|
1538
|
+
return { type: 'literal', value: tok.v };
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Template literal
|
|
1542
|
+
if (tok.t === T.TMPL) {
|
|
1543
|
+
this.next();
|
|
1544
|
+
return { type: 'template', parts: tok.v };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Arrow function with parens: () =>, (a) =>, (a, b) =>
|
|
1548
|
+
// or regular grouping: (expr)
|
|
1549
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
1550
|
+
const savedPos = this.pos;
|
|
1551
|
+
this.next(); // consume (
|
|
1552
|
+
const params = [];
|
|
1553
|
+
let couldBeArrow = true;
|
|
1554
|
+
|
|
1555
|
+
if (this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
1556
|
+
// () => ... — no params
|
|
1557
|
+
} else {
|
|
1558
|
+
while (couldBeArrow) {
|
|
1559
|
+
const p = this.peek();
|
|
1560
|
+
if (p.t === T.IDENT && !KEYWORDS.has(p.v)) {
|
|
1561
|
+
params.push(this.next().v);
|
|
1562
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') {
|
|
1563
|
+
this.next();
|
|
1564
|
+
} else {
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
} else {
|
|
1568
|
+
couldBeArrow = false;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (couldBeArrow && this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
1574
|
+
this.next(); // consume )
|
|
1575
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
1576
|
+
this.next(); // consume =>
|
|
1577
|
+
const body = this.parseExpression(0);
|
|
1578
|
+
return { type: 'arrow', params, body };
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Not an arrow — restore and parse as grouping
|
|
1583
|
+
this.pos = savedPos;
|
|
1584
|
+
this.next(); // consume (
|
|
1585
|
+
const expr = this.parseExpression(0);
|
|
1586
|
+
this.expect(T.PUNC, ')');
|
|
1587
|
+
return expr;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Array literal
|
|
1591
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
1592
|
+
this.next();
|
|
1593
|
+
const elements = [];
|
|
1594
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
1595
|
+
elements.push(this.parseExpression(0));
|
|
1596
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1597
|
+
}
|
|
1598
|
+
this.expect(T.PUNC, ']');
|
|
1599
|
+
return { type: 'array', elements };
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Object literal
|
|
1603
|
+
if (tok.t === T.PUNC && tok.v === '{') {
|
|
1604
|
+
this.next();
|
|
1605
|
+
const properties = [];
|
|
1606
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
1607
|
+
const keyTok = this.next();
|
|
1608
|
+
let key;
|
|
1609
|
+
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
1610
|
+
else if (keyTok.t === T.NUM) key = String(keyTok.v);
|
|
1611
|
+
else throw new Error('Invalid object key: ' + keyTok.v);
|
|
1612
|
+
|
|
1613
|
+
// Shorthand property: { foo } means { foo: foo }
|
|
1614
|
+
if (this.peek().t === T.PUNC && (this.peek().v === ',' || this.peek().v === '}')) {
|
|
1615
|
+
properties.push({ key, value: { type: 'ident', name: key } });
|
|
1616
|
+
} else {
|
|
1617
|
+
this.expect(T.PUNC, ':');
|
|
1618
|
+
properties.push({ key, value: this.parseExpression(0) });
|
|
1619
|
+
}
|
|
1620
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1621
|
+
}
|
|
1622
|
+
this.expect(T.PUNC, '}');
|
|
1623
|
+
return { type: 'object', properties };
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Identifiers & keywords
|
|
1627
|
+
if (tok.t === T.IDENT) {
|
|
1628
|
+
this.next();
|
|
1629
|
+
|
|
1630
|
+
// Keywords
|
|
1631
|
+
if (tok.v === 'true') return { type: 'literal', value: true };
|
|
1632
|
+
if (tok.v === 'false') return { type: 'literal', value: false };
|
|
1633
|
+
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
1634
|
+
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
1635
|
+
|
|
1636
|
+
// new keyword
|
|
1637
|
+
if (tok.v === 'new') {
|
|
1638
|
+
const classExpr = this.parsePostfix();
|
|
1639
|
+
let args = [];
|
|
1640
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1641
|
+
args = this._parseArgs();
|
|
1642
|
+
}
|
|
1643
|
+
return { type: 'new', callee: classExpr, args };
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Arrow function: x => expr
|
|
1647
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
1648
|
+
this.next(); // consume =>
|
|
1649
|
+
const body = this.parseExpression(0);
|
|
1650
|
+
return { type: 'arrow', params: [tok.v], body };
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
return { type: 'ident', name: tok.v };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Fallback — return undefined for unparseable
|
|
1657
|
+
this.next();
|
|
1658
|
+
return { type: 'literal', value: undefined };
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ---------------------------------------------------------------------------
|
|
1663
|
+
// Evaluator — walks the AST, resolves against scope
|
|
1664
|
+
// ---------------------------------------------------------------------------
|
|
1665
|
+
|
|
1666
|
+
/** Safe property access whitelist for built-in prototypes */
|
|
1667
|
+
const SAFE_ARRAY_METHODS = new Set([
|
|
1668
|
+
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
1669
|
+
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
1670
|
+
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
1671
|
+
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
1672
|
+
]);
|
|
1673
|
+
|
|
1674
|
+
const SAFE_STRING_METHODS = new Set([
|
|
1675
|
+
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
1676
|
+
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
1677
|
+
'toUpperCase', 'split', 'replace', 'replaceAll', 'match', 'search',
|
|
1678
|
+
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'at',
|
|
1679
|
+
'toString', 'valueOf',
|
|
1680
|
+
]);
|
|
1681
|
+
|
|
1682
|
+
const SAFE_NUMBER_METHODS = new Set([
|
|
1683
|
+
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
1684
|
+
]);
|
|
1685
|
+
|
|
1686
|
+
const SAFE_OBJECT_METHODS = new Set([
|
|
1687
|
+
'hasOwnProperty', 'toString', 'valueOf',
|
|
1688
|
+
]);
|
|
1689
|
+
|
|
1690
|
+
const SAFE_MATH_PROPS = new Set([
|
|
1691
|
+
'PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2',
|
|
1692
|
+
'abs', 'ceil', 'floor', 'round', 'trunc', 'max', 'min', 'pow',
|
|
1693
|
+
'sqrt', 'sign', 'random', 'log', 'log2', 'log10',
|
|
1694
|
+
]);
|
|
1695
|
+
|
|
1696
|
+
const SAFE_JSON_PROPS = new Set(['parse', 'stringify']);
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Check if property access is safe
|
|
1700
|
+
*/
|
|
1701
|
+
function _isSafeAccess(obj, prop) {
|
|
1702
|
+
// Never allow access to dangerous properties
|
|
1703
|
+
const BLOCKED = new Set([
|
|
1704
|
+
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
1705
|
+
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
1706
|
+
]);
|
|
1707
|
+
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
1708
|
+
|
|
1709
|
+
// Always allow plain object/function property access and array index access
|
|
1710
|
+
if (obj !== null && obj !== undefined && (typeof obj === 'object' || typeof obj === 'function')) return true;
|
|
1711
|
+
if (typeof obj === 'string') return SAFE_STRING_METHODS.has(prop);
|
|
1712
|
+
if (typeof obj === 'number') return SAFE_NUMBER_METHODS.has(prop);
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function evaluate(node, scope) {
|
|
1717
|
+
if (!node) return undefined;
|
|
1718
|
+
|
|
1719
|
+
switch (node.type) {
|
|
1720
|
+
case 'literal':
|
|
1721
|
+
return node.value;
|
|
1722
|
+
|
|
1723
|
+
case 'ident': {
|
|
1724
|
+
const name = node.name;
|
|
1725
|
+
// Check scope layers in order
|
|
1726
|
+
for (const layer of scope) {
|
|
1727
|
+
if (layer && typeof layer === 'object' && name in layer) {
|
|
1728
|
+
return layer[name];
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
// Built-in globals (safe ones only)
|
|
1732
|
+
if (name === 'Math') return Math;
|
|
1733
|
+
if (name === 'JSON') return JSON;
|
|
1734
|
+
if (name === 'Date') return Date;
|
|
1735
|
+
if (name === 'Array') return Array;
|
|
1736
|
+
if (name === 'Object') return Object;
|
|
1737
|
+
if (name === 'String') return String;
|
|
1738
|
+
if (name === 'Number') return Number;
|
|
1739
|
+
if (name === 'Boolean') return Boolean;
|
|
1740
|
+
if (name === 'parseInt') return parseInt;
|
|
1741
|
+
if (name === 'parseFloat') return parseFloat;
|
|
1742
|
+
if (name === 'isNaN') return isNaN;
|
|
1743
|
+
if (name === 'isFinite') return isFinite;
|
|
1744
|
+
if (name === 'Infinity') return Infinity;
|
|
1745
|
+
if (name === 'NaN') return NaN;
|
|
1746
|
+
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
1747
|
+
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
1748
|
+
if (name === 'console') return console;
|
|
1749
|
+
return undefined;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
case 'template': {
|
|
1753
|
+
// Template literal with interpolation
|
|
1754
|
+
let result = '';
|
|
1755
|
+
for (const part of node.parts) {
|
|
1756
|
+
if (typeof part === 'string') {
|
|
1757
|
+
result += part;
|
|
1758
|
+
} else if (part && part.expr) {
|
|
1759
|
+
result += String(safeEval(part.expr, scope) ?? '');
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return result;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
case 'member': {
|
|
1766
|
+
const obj = evaluate(node.obj, scope);
|
|
1767
|
+
if (obj == null) return undefined;
|
|
1768
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
1769
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
1770
|
+
return obj[prop];
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
case 'optional_member': {
|
|
1774
|
+
const obj = evaluate(node.obj, scope);
|
|
1775
|
+
if (obj == null) return undefined;
|
|
1776
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
1777
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
1778
|
+
return obj[prop];
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
case 'call': {
|
|
1782
|
+
const result = _resolveCall(node, scope, false);
|
|
1783
|
+
return result;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
case 'optional_call': {
|
|
1787
|
+
const callee = evaluate(node.callee, scope);
|
|
1788
|
+
if (callee == null) return undefined;
|
|
1789
|
+
if (typeof callee !== 'function') return undefined;
|
|
1790
|
+
const args = node.args.map(a => evaluate(a, scope));
|
|
1791
|
+
return callee(...args);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
case 'new': {
|
|
1795
|
+
const Ctor = evaluate(node.callee, scope);
|
|
1796
|
+
if (typeof Ctor !== 'function') return undefined;
|
|
1797
|
+
// Only allow safe constructors
|
|
1798
|
+
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
1799
|
+
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
1800
|
+
const args = node.args.map(a => evaluate(a, scope));
|
|
1801
|
+
return new Ctor(...args);
|
|
1802
|
+
}
|
|
1803
|
+
return undefined;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
case 'binary':
|
|
1807
|
+
return _evalBinary(node, scope);
|
|
1808
|
+
|
|
1809
|
+
case 'unary': {
|
|
1810
|
+
const val = evaluate(node.arg, scope);
|
|
1811
|
+
return node.op === '-' ? -val : +val;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
case 'not':
|
|
1815
|
+
return !evaluate(node.arg, scope);
|
|
1816
|
+
|
|
1817
|
+
case 'typeof': {
|
|
1818
|
+
try {
|
|
1819
|
+
return typeof evaluate(node.arg, scope);
|
|
1820
|
+
} catch {
|
|
1821
|
+
return 'undefined';
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
case 'ternary': {
|
|
1826
|
+
const cond = evaluate(node.cond, scope);
|
|
1827
|
+
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
case 'array':
|
|
1831
|
+
return node.elements.map(e => evaluate(e, scope));
|
|
1832
|
+
|
|
1833
|
+
case 'object': {
|
|
1834
|
+
const obj = {};
|
|
1835
|
+
for (const { key, value } of node.properties) {
|
|
1836
|
+
obj[key] = evaluate(value, scope);
|
|
1837
|
+
}
|
|
1838
|
+
return obj;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
case 'arrow': {
|
|
1842
|
+
const paramNames = node.params;
|
|
1843
|
+
const bodyNode = node.body;
|
|
1844
|
+
const closedScope = scope;
|
|
1845
|
+
return function(...args) {
|
|
1846
|
+
const arrowScope = {};
|
|
1847
|
+
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
1848
|
+
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
default:
|
|
1853
|
+
return undefined;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Resolve and execute a function call safely.
|
|
1859
|
+
*/
|
|
1860
|
+
function _resolveCall(node, scope) {
|
|
1861
|
+
const callee = node.callee;
|
|
1862
|
+
const args = node.args.map(a => evaluate(a, scope));
|
|
1863
|
+
|
|
1864
|
+
// Method call: obj.method() — bind `this` to obj
|
|
1865
|
+
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
1866
|
+
const obj = evaluate(callee.obj, scope);
|
|
1867
|
+
if (obj == null) return undefined;
|
|
1868
|
+
const prop = callee.computed ? evaluate(callee.prop, scope) : callee.prop;
|
|
1869
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
1870
|
+
const fn = obj[prop];
|
|
1871
|
+
if (typeof fn !== 'function') return undefined;
|
|
1872
|
+
return fn.apply(obj, args);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Direct call: fn(args)
|
|
1876
|
+
const fn = evaluate(callee, scope);
|
|
1877
|
+
if (typeof fn !== 'function') return undefined;
|
|
1878
|
+
return fn(...args);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Evaluate binary expression.
|
|
1883
|
+
*/
|
|
1884
|
+
function _evalBinary(node, scope) {
|
|
1885
|
+
// Short-circuit for logical ops
|
|
1886
|
+
if (node.op === '&&') {
|
|
1887
|
+
const left = evaluate(node.left, scope);
|
|
1888
|
+
return left ? evaluate(node.right, scope) : left;
|
|
1889
|
+
}
|
|
1890
|
+
if (node.op === '||') {
|
|
1891
|
+
const left = evaluate(node.left, scope);
|
|
1892
|
+
return left ? left : evaluate(node.right, scope);
|
|
1893
|
+
}
|
|
1894
|
+
if (node.op === '??') {
|
|
1895
|
+
const left = evaluate(node.left, scope);
|
|
1896
|
+
return left != null ? left : evaluate(node.right, scope);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const left = evaluate(node.left, scope);
|
|
1900
|
+
const right = evaluate(node.right, scope);
|
|
1901
|
+
|
|
1902
|
+
switch (node.op) {
|
|
1903
|
+
case '+': return left + right;
|
|
1904
|
+
case '-': return left - right;
|
|
1905
|
+
case '*': return left * right;
|
|
1906
|
+
case '/': return left / right;
|
|
1907
|
+
case '%': return left % right;
|
|
1908
|
+
case '==': return left == right;
|
|
1909
|
+
case '!=': return left != right;
|
|
1910
|
+
case '===': return left === right;
|
|
1911
|
+
case '!==': return left !== right;
|
|
1912
|
+
case '<': return left < right;
|
|
1913
|
+
case '>': return left > right;
|
|
1914
|
+
case '<=': return left <= right;
|
|
1915
|
+
case '>=': return left >= right;
|
|
1916
|
+
case 'instanceof': return left instanceof right;
|
|
1917
|
+
case 'in': return left in right;
|
|
1918
|
+
default: return undefined;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
// ---------------------------------------------------------------------------
|
|
1924
|
+
// Public API
|
|
515
1925
|
// ---------------------------------------------------------------------------
|
|
516
|
-
function createFragment(html) {
|
|
517
|
-
const tpl = document.createElement('template');
|
|
518
|
-
tpl.innerHTML = html.trim();
|
|
519
|
-
return tpl.content;
|
|
520
|
-
}
|
|
521
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* Safely evaluate a JS expression string against scope layers.
|
|
1929
|
+
*
|
|
1930
|
+
* @param {string} expr — expression string
|
|
1931
|
+
* @param {object[]} scope — array of scope objects, checked in order
|
|
1932
|
+
* Typical: [loopVars, state, { props, refs, $ }]
|
|
1933
|
+
* @returns {*} — evaluation result, or undefined on error
|
|
1934
|
+
*/
|
|
1935
|
+
function safeEval(expr, scope) {
|
|
1936
|
+
try {
|
|
1937
|
+
const trimmed = expr.trim();
|
|
1938
|
+
if (!trimmed) return undefined;
|
|
1939
|
+
const tokens = tokenize(trimmed);
|
|
1940
|
+
const parser = new Parser(tokens, scope);
|
|
1941
|
+
const ast = parser.parse();
|
|
1942
|
+
return evaluate(ast, scope);
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
1945
|
+
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
1946
|
+
}
|
|
1947
|
+
return undefined;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// --- src/diff.js -------------------------------------------------
|
|
1952
|
+
/**
|
|
1953
|
+
* zQuery Diff — Lightweight DOM morphing engine
|
|
1954
|
+
*
|
|
1955
|
+
* Patches an existing DOM tree to match new HTML without destroying nodes
|
|
1956
|
+
* that haven't changed. Preserves focus, scroll positions, third-party
|
|
1957
|
+
* widget state, video playback, and other live DOM state.
|
|
1958
|
+
*
|
|
1959
|
+
* Approach: walk old and new trees in parallel, reconcile node by node.
|
|
1960
|
+
* Keyed elements (via `z-key`) get matched across position changes.
|
|
1961
|
+
*/
|
|
522
1962
|
|
|
523
1963
|
// ---------------------------------------------------------------------------
|
|
524
|
-
//
|
|
1964
|
+
// morph(existingRoot, newHTML) — patch existing DOM to match newHTML
|
|
525
1965
|
// ---------------------------------------------------------------------------
|
|
526
|
-
function query(selector, context) {
|
|
527
|
-
// null / undefined
|
|
528
|
-
if (!selector) return null;
|
|
529
1966
|
|
|
530
|
-
|
|
531
|
-
|
|
1967
|
+
/**
|
|
1968
|
+
* Morph an existing DOM element's children to match new HTML.
|
|
1969
|
+
* Only touches nodes that actually differ.
|
|
1970
|
+
*
|
|
1971
|
+
* @param {Element} rootEl — The live DOM container to patch
|
|
1972
|
+
* @param {string} newHTML — The desired HTML string
|
|
1973
|
+
*/
|
|
1974
|
+
function morph(rootEl, newHTML) {
|
|
1975
|
+
const template = document.createElement('template');
|
|
1976
|
+
template.innerHTML = newHTML;
|
|
1977
|
+
const newRoot = template.content;
|
|
1978
|
+
|
|
1979
|
+
// Convert to element for consistent handling — wrap in a div if needed
|
|
1980
|
+
const tempDiv = document.createElement('div');
|
|
1981
|
+
while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
|
|
532
1982
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1983
|
+
_morphChildren(rootEl, tempDiv);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
/**
|
|
1987
|
+
* Reconcile children of `oldParent` to match `newParent`.
|
|
1988
|
+
*
|
|
1989
|
+
* @param {Element} oldParent — live DOM parent
|
|
1990
|
+
* @param {Element} newParent — desired state parent
|
|
1991
|
+
*/
|
|
1992
|
+
function _morphChildren(oldParent, newParent) {
|
|
1993
|
+
const oldChildren = [...oldParent.childNodes];
|
|
1994
|
+
const newChildren = [...newParent.childNodes];
|
|
1995
|
+
|
|
1996
|
+
// Build key maps for keyed element matching
|
|
1997
|
+
const oldKeyMap = new Map();
|
|
1998
|
+
const newKeyMap = new Map();
|
|
1999
|
+
|
|
2000
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
2001
|
+
const key = _getKey(oldChildren[i]);
|
|
2002
|
+
if (key != null) oldKeyMap.set(key, i);
|
|
2003
|
+
}
|
|
2004
|
+
for (let i = 0; i < newChildren.length; i++) {
|
|
2005
|
+
const key = _getKey(newChildren[i]);
|
|
2006
|
+
if (key != null) newKeyMap.set(key, i);
|
|
536
2007
|
}
|
|
537
2008
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
2009
|
+
const hasKeys = oldKeyMap.size > 0 || newKeyMap.size > 0;
|
|
2010
|
+
|
|
2011
|
+
if (hasKeys) {
|
|
2012
|
+
_morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap);
|
|
2013
|
+
} else {
|
|
2014
|
+
_morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
|
|
542
2015
|
}
|
|
2016
|
+
}
|
|
543
2017
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
2018
|
+
/**
|
|
2019
|
+
* Unkeyed reconciliation — positional matching.
|
|
2020
|
+
*/
|
|
2021
|
+
function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
2022
|
+
const maxLen = Math.max(oldChildren.length, newChildren.length);
|
|
2023
|
+
|
|
2024
|
+
for (let i = 0; i < maxLen; i++) {
|
|
2025
|
+
const oldNode = oldChildren[i];
|
|
2026
|
+
const newNode = newChildren[i];
|
|
2027
|
+
|
|
2028
|
+
if (!oldNode && newNode) {
|
|
2029
|
+
// New node — append
|
|
2030
|
+
oldParent.appendChild(newNode.cloneNode(true));
|
|
2031
|
+
} else if (oldNode && !newNode) {
|
|
2032
|
+
// Extra old node — remove
|
|
2033
|
+
oldParent.removeChild(oldNode);
|
|
2034
|
+
} else if (oldNode && newNode) {
|
|
2035
|
+
_morphNode(oldParent, oldNode, newNode);
|
|
2036
|
+
}
|
|
549
2037
|
}
|
|
2038
|
+
}
|
|
550
2039
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
2040
|
+
/**
|
|
2041
|
+
* Keyed reconciliation — match by z-key, reorder minimal moves.
|
|
2042
|
+
*/
|
|
2043
|
+
function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap) {
|
|
2044
|
+
// Track which old nodes are consumed
|
|
2045
|
+
const consumed = new Set();
|
|
2046
|
+
|
|
2047
|
+
// Step 1: Build ordered list of matched old nodes for new children
|
|
2048
|
+
const newLen = newChildren.length;
|
|
2049
|
+
const matched = new Array(newLen); // matched[newIdx] = oldNode | null
|
|
2050
|
+
|
|
2051
|
+
for (let i = 0; i < newLen; i++) {
|
|
2052
|
+
const key = _getKey(newChildren[i]);
|
|
2053
|
+
if (key != null && oldKeyMap.has(key)) {
|
|
2054
|
+
const oldIdx = oldKeyMap.get(key);
|
|
2055
|
+
matched[i] = oldChildren[oldIdx];
|
|
2056
|
+
consumed.add(oldIdx);
|
|
2057
|
+
} else {
|
|
2058
|
+
matched[i] = null;
|
|
2059
|
+
}
|
|
557
2060
|
}
|
|
558
2061
|
|
|
559
|
-
|
|
560
|
-
|
|
2062
|
+
// Step 2: Remove old nodes that are not in the new tree
|
|
2063
|
+
for (let i = oldChildren.length - 1; i >= 0; i--) {
|
|
2064
|
+
if (!consumed.has(i)) {
|
|
2065
|
+
const key = _getKey(oldChildren[i]);
|
|
2066
|
+
if (key != null && !newKeyMap.has(key)) {
|
|
2067
|
+
oldParent.removeChild(oldChildren[i]);
|
|
2068
|
+
} else if (key == null) {
|
|
2069
|
+
// Unkeyed old node — will be handled positionally below
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
561
2073
|
|
|
2074
|
+
// Step 3: Insert/reorder/morph
|
|
2075
|
+
let cursor = oldParent.firstChild;
|
|
2076
|
+
const unkeyedOld = oldChildren.filter((n, i) => !consumed.has(i) && _getKey(n) == null);
|
|
2077
|
+
let unkeyedIdx = 0;
|
|
562
2078
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// null / undefined
|
|
568
|
-
if (!selector) return new ZQueryCollection([]);
|
|
2079
|
+
for (let i = 0; i < newLen; i++) {
|
|
2080
|
+
const newNode = newChildren[i];
|
|
2081
|
+
const newKey = _getKey(newNode);
|
|
2082
|
+
let oldNode = matched[i];
|
|
569
2083
|
|
|
570
|
-
|
|
571
|
-
|
|
2084
|
+
if (!oldNode && newKey == null) {
|
|
2085
|
+
// Try to match an unkeyed old node positionally
|
|
2086
|
+
oldNode = unkeyedOld[unkeyedIdx++] || null;
|
|
2087
|
+
}
|
|
572
2088
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
2089
|
+
if (oldNode) {
|
|
2090
|
+
// Move into position if needed
|
|
2091
|
+
if (oldNode !== cursor) {
|
|
2092
|
+
oldParent.insertBefore(oldNode, cursor);
|
|
2093
|
+
}
|
|
2094
|
+
// Morph in place
|
|
2095
|
+
_morphNode(oldParent, oldNode, newNode);
|
|
2096
|
+
cursor = oldNode.nextSibling;
|
|
2097
|
+
} else {
|
|
2098
|
+
// Insert new node
|
|
2099
|
+
const clone = newNode.cloneNode(true);
|
|
2100
|
+
if (cursor) {
|
|
2101
|
+
oldParent.insertBefore(clone, cursor);
|
|
2102
|
+
} else {
|
|
2103
|
+
oldParent.appendChild(clone);
|
|
2104
|
+
}
|
|
2105
|
+
// cursor stays the same — new node is before it
|
|
2106
|
+
}
|
|
576
2107
|
}
|
|
577
2108
|
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
2109
|
+
// Remove any remaining unkeyed old nodes at the end
|
|
2110
|
+
while (unkeyedIdx < unkeyedOld.length) {
|
|
2111
|
+
const leftover = unkeyedOld[unkeyedIdx++];
|
|
2112
|
+
if (leftover.parentNode === oldParent) {
|
|
2113
|
+
oldParent.removeChild(leftover);
|
|
2114
|
+
}
|
|
581
2115
|
}
|
|
582
2116
|
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
2117
|
+
// Remove any remaining keyed old nodes that weren't consumed
|
|
2118
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
2119
|
+
if (!consumed.has(i)) {
|
|
2120
|
+
const node = oldChildren[i];
|
|
2121
|
+
if (node.parentNode === oldParent && _getKey(node) != null && !newKeyMap.has(_getKey(node))) {
|
|
2122
|
+
oldParent.removeChild(node);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
587
2125
|
}
|
|
2126
|
+
}
|
|
588
2127
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
2128
|
+
/**
|
|
2129
|
+
* Morph a single node in place.
|
|
2130
|
+
*/
|
|
2131
|
+
function _morphNode(parent, oldNode, newNode) {
|
|
2132
|
+
// Text / comment nodes — just update content
|
|
2133
|
+
if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
|
|
2134
|
+
if (newNode.nodeType === oldNode.nodeType) {
|
|
2135
|
+
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
2136
|
+
oldNode.nodeValue = newNode.nodeValue;
|
|
2137
|
+
}
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
// Different node types — replace
|
|
2141
|
+
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
2142
|
+
return;
|
|
595
2143
|
}
|
|
596
2144
|
|
|
597
|
-
|
|
598
|
-
|
|
2145
|
+
// Different node types or tag names — replace entirely
|
|
2146
|
+
if (oldNode.nodeType !== newNode.nodeType ||
|
|
2147
|
+
oldNode.nodeName !== newNode.nodeName) {
|
|
2148
|
+
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
599
2151
|
|
|
2152
|
+
// Both are elements — diff attributes then recurse children
|
|
2153
|
+
if (oldNode.nodeType === 1) {
|
|
2154
|
+
_morphAttributes(oldNode, newNode);
|
|
600
2155
|
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
2156
|
+
// Special elements: don't recurse into their children
|
|
2157
|
+
// (textarea value, input value, select, etc.)
|
|
2158
|
+
const tag = oldNode.nodeName;
|
|
2159
|
+
if (tag === 'INPUT') {
|
|
2160
|
+
_syncInputValue(oldNode, newNode);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
if (tag === 'TEXTAREA') {
|
|
2164
|
+
if (oldNode.value !== newNode.textContent) {
|
|
2165
|
+
oldNode.value = newNode.textContent || '';
|
|
2166
|
+
}
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (tag === 'SELECT') {
|
|
2170
|
+
// Recurse children (options) then sync value
|
|
2171
|
+
_morphChildren(oldNode, newNode);
|
|
2172
|
+
if (oldNode.value !== newNode.value) {
|
|
2173
|
+
oldNode.value = newNode.value;
|
|
2174
|
+
}
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
622
2177
|
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
const el = document.createElement(tag);
|
|
626
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
627
|
-
if (k === 'class') el.className = v;
|
|
628
|
-
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
629
|
-
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
|
|
630
|
-
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
631
|
-
else el.setAttribute(k, v);
|
|
2178
|
+
// Generic element — recurse children
|
|
2179
|
+
_morphChildren(oldNode, newNode);
|
|
632
2180
|
}
|
|
633
|
-
|
|
634
|
-
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
635
|
-
else if (child instanceof Node) el.appendChild(child);
|
|
636
|
-
});
|
|
637
|
-
return el;
|
|
638
|
-
};
|
|
2181
|
+
}
|
|
639
2182
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
2183
|
+
/**
|
|
2184
|
+
* Sync attributes from newEl onto oldEl.
|
|
2185
|
+
*/
|
|
2186
|
+
function _morphAttributes(oldEl, newEl) {
|
|
2187
|
+
// Add/update attributes
|
|
2188
|
+
const newAttrs = newEl.attributes;
|
|
2189
|
+
for (let i = 0; i < newAttrs.length; i++) {
|
|
2190
|
+
const attr = newAttrs[i];
|
|
2191
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
2192
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
645
2195
|
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return;
|
|
2196
|
+
// Remove stale attributes
|
|
2197
|
+
const oldAttrs = oldEl.attributes;
|
|
2198
|
+
for (let i = oldAttrs.length - 1; i >= 0; i--) {
|
|
2199
|
+
const attr = oldAttrs[i];
|
|
2200
|
+
if (!newEl.hasAttribute(attr.name)) {
|
|
2201
|
+
oldEl.removeAttribute(attr.name);
|
|
2202
|
+
}
|
|
654
2203
|
}
|
|
655
|
-
|
|
656
|
-
document.addEventListener(event, (e) => {
|
|
657
|
-
const target = e.target.closest(selectorOrHandler);
|
|
658
|
-
if (target) handler.call(target, e);
|
|
659
|
-
});
|
|
660
|
-
};
|
|
2204
|
+
}
|
|
661
2205
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
2206
|
+
/**
|
|
2207
|
+
* Sync input element value, checked, disabled states.
|
|
2208
|
+
*/
|
|
2209
|
+
function _syncInputValue(oldEl, newEl) {
|
|
2210
|
+
const type = (oldEl.type || '').toLowerCase();
|
|
666
2211
|
|
|
667
|
-
|
|
668
|
-
|
|
2212
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
2213
|
+
if (oldEl.checked !== newEl.checked) oldEl.checked = newEl.checked;
|
|
2214
|
+
} else {
|
|
2215
|
+
if (oldEl.value !== (newEl.getAttribute('value') || '')) {
|
|
2216
|
+
oldEl.value = newEl.getAttribute('value') || '';
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// Sync disabled
|
|
2221
|
+
if (oldEl.disabled !== newEl.disabled) oldEl.disabled = newEl.disabled;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* Get the reconciliation key from a node (z-key attribute).
|
|
2226
|
+
* @returns {string|null}
|
|
2227
|
+
*/
|
|
2228
|
+
function _getKey(node) {
|
|
2229
|
+
if (node.nodeType !== 1) return null;
|
|
2230
|
+
return node.getAttribute('z-key') || null;
|
|
2231
|
+
}
|
|
669
2232
|
|
|
670
|
-
// --- src/component.js
|
|
2233
|
+
// --- src/component.js --------------------------------------------
|
|
671
2234
|
/**
|
|
672
2235
|
* zQuery Component — Lightweight reactive component system
|
|
673
2236
|
*
|
|
@@ -690,6 +2253,9 @@ query.fn = ZQueryCollection.prototype;
|
|
|
690
2253
|
*/
|
|
691
2254
|
|
|
692
2255
|
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
|
|
693
2259
|
// ---------------------------------------------------------------------------
|
|
694
2260
|
// Component registry & external resource cache
|
|
695
2261
|
// ---------------------------------------------------------------------------
|
|
@@ -864,10 +2430,27 @@ class Component {
|
|
|
864
2430
|
this._destroyed = false;
|
|
865
2431
|
this._updateQueued = false;
|
|
866
2432
|
this._listeners = [];
|
|
2433
|
+
this._watchCleanups = [];
|
|
867
2434
|
|
|
868
2435
|
// Refs map
|
|
869
2436
|
this.refs = {};
|
|
870
2437
|
|
|
2438
|
+
// Capture slot content before first render replaces it
|
|
2439
|
+
this._slotContent = {};
|
|
2440
|
+
const defaultSlotNodes = [];
|
|
2441
|
+
[...el.childNodes].forEach(node => {
|
|
2442
|
+
if (node.nodeType === 1 && node.hasAttribute('slot')) {
|
|
2443
|
+
const slotName = node.getAttribute('slot');
|
|
2444
|
+
if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
|
|
2445
|
+
this._slotContent[slotName] += node.outerHTML;
|
|
2446
|
+
} else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
|
|
2447
|
+
defaultSlotNodes.push(node.nodeType === 1 ? node.outerHTML : node.textContent);
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
if (defaultSlotNodes.length) {
|
|
2451
|
+
this._slotContent['default'] = defaultSlotNodes.join('');
|
|
2452
|
+
}
|
|
2453
|
+
|
|
871
2454
|
// Props (read-only from parent)
|
|
872
2455
|
this.props = Object.freeze({ ...props });
|
|
873
2456
|
|
|
@@ -876,10 +2459,25 @@ class Component {
|
|
|
876
2459
|
? definition.state()
|
|
877
2460
|
: { ...(definition.state || {}) };
|
|
878
2461
|
|
|
879
|
-
this.state = reactive(initialState, () => {
|
|
880
|
-
if (!this._destroyed)
|
|
2462
|
+
this.state = reactive(initialState, (key, value, old) => {
|
|
2463
|
+
if (!this._destroyed) {
|
|
2464
|
+
// Run watchers for the changed key
|
|
2465
|
+
this._runWatchers(key, value, old);
|
|
2466
|
+
this._scheduleUpdate();
|
|
2467
|
+
}
|
|
881
2468
|
});
|
|
882
2469
|
|
|
2470
|
+
// Computed properties — lazy getters derived from state
|
|
2471
|
+
this.computed = {};
|
|
2472
|
+
if (definition.computed) {
|
|
2473
|
+
for (const [name, fn] of Object.entries(definition.computed)) {
|
|
2474
|
+
Object.defineProperty(this.computed, name, {
|
|
2475
|
+
get: () => fn.call(this, this.state.__raw || this.state),
|
|
2476
|
+
enumerable: true
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
883
2481
|
// Bind all user methods to this instance
|
|
884
2482
|
for (const [key, val] of Object.entries(definition)) {
|
|
885
2483
|
if (typeof val === 'function' && !_reservedKeys.has(key)) {
|
|
@@ -888,7 +2486,36 @@ class Component {
|
|
|
888
2486
|
}
|
|
889
2487
|
|
|
890
2488
|
// Init lifecycle
|
|
891
|
-
if (definition.init)
|
|
2489
|
+
if (definition.init) {
|
|
2490
|
+
try { definition.init.call(this); }
|
|
2491
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${definition._name}" init() threw`, { component: definition._name }, err); }
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Set up watchers after init so initial state is ready
|
|
2495
|
+
if (definition.watch) {
|
|
2496
|
+
this._prevWatchValues = {};
|
|
2497
|
+
for (const key of Object.keys(definition.watch)) {
|
|
2498
|
+
this._prevWatchValues[key] = _getPath(this.state.__raw || this.state, key);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// Run registered watchers for a changed key
|
|
2504
|
+
_runWatchers(changedKey, value, old) {
|
|
2505
|
+
const watchers = this._def.watch;
|
|
2506
|
+
if (!watchers) return;
|
|
2507
|
+
for (const [key, handler] of Object.entries(watchers)) {
|
|
2508
|
+
// Match exact key or parent key (e.g. watcher on 'user' fires when 'user.name' changes)
|
|
2509
|
+
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.') || changedKey === key) {
|
|
2510
|
+
const currentVal = _getPath(this.state.__raw || this.state, key);
|
|
2511
|
+
const prevVal = this._prevWatchValues?.[key];
|
|
2512
|
+
if (currentVal !== prevVal) {
|
|
2513
|
+
const fn = typeof handler === 'function' ? handler : handler.handler;
|
|
2514
|
+
if (typeof fn === 'function') fn.call(this, currentVal, prevVal);
|
|
2515
|
+
if (this._prevWatchValues) this._prevWatchValues[key] = currentVal;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
892
2519
|
}
|
|
893
2520
|
|
|
894
2521
|
// Schedule a batched DOM update (microtask)
|
|
@@ -986,11 +2613,14 @@ class Component {
|
|
|
986
2613
|
if (def.styleUrl && !def._styleLoaded) {
|
|
987
2614
|
const su = def.styleUrl;
|
|
988
2615
|
if (typeof su === 'string') {
|
|
989
|
-
|
|
2616
|
+
const resolved = _resolveUrl(su, base);
|
|
2617
|
+
def._externalStyles = await _fetchResource(resolved);
|
|
2618
|
+
def._resolvedStyleUrls = [resolved];
|
|
990
2619
|
} else if (Array.isArray(su)) {
|
|
991
2620
|
const urls = su.map(u => _resolveUrl(u, base));
|
|
992
2621
|
const results = await Promise.all(urls.map(u => _fetchResource(u)));
|
|
993
2622
|
def._externalStyles = results.join('\n');
|
|
2623
|
+
def._resolvedStyleUrls = urls;
|
|
994
2624
|
}
|
|
995
2625
|
def._styleLoaded = true;
|
|
996
2626
|
}
|
|
@@ -1063,17 +2693,29 @@ class Component {
|
|
|
1063
2693
|
// Then do global {{expression}} interpolation on the remaining content
|
|
1064
2694
|
html = html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
1065
2695
|
try {
|
|
1066
|
-
|
|
2696
|
+
const result = safeEval(expr.trim(), [
|
|
1067
2697
|
this.state.__raw || this.state,
|
|
1068
|
-
this.props,
|
|
1069
|
-
|
|
1070
|
-
|
|
2698
|
+
{ props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
2699
|
+
]);
|
|
2700
|
+
return result != null ? result : '';
|
|
1071
2701
|
} catch { return ''; }
|
|
1072
2702
|
});
|
|
1073
2703
|
} else {
|
|
1074
2704
|
html = '';
|
|
1075
2705
|
}
|
|
1076
2706
|
|
|
2707
|
+
// -- Slot distribution ----------------------------------------
|
|
2708
|
+
// Replace <slot> elements with captured slot content from parent.
|
|
2709
|
+
// <slot> → default slot content
|
|
2710
|
+
// <slot name="header"> → named slot content
|
|
2711
|
+
// Fallback content between <slot>...</slot> used when no content provided.
|
|
2712
|
+
if (html.includes('<slot')) {
|
|
2713
|
+
html = html.replace(/<slot(?:\s+name="([^"]*)")?\s*(?:\/>|>([\s\S]*?)<\/slot>)/g, (_, name, fallback) => {
|
|
2714
|
+
const slotName = name || 'default';
|
|
2715
|
+
return this._slotContent[slotName] || fallback || '';
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
|
|
1077
2719
|
// Combine inline styles + external styles
|
|
1078
2720
|
const combinedStyles = [
|
|
1079
2721
|
this._def.styles || '',
|
|
@@ -1084,33 +2726,52 @@ class Component {
|
|
|
1084
2726
|
if (!this._mounted && combinedStyles) {
|
|
1085
2727
|
const scopeAttr = `z-s${this._uid}`;
|
|
1086
2728
|
this._el.setAttribute(scopeAttr, '');
|
|
1087
|
-
|
|
2729
|
+
let inAtBlock = 0;
|
|
2730
|
+
const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
|
|
2731
|
+
if (match === '}') {
|
|
2732
|
+
if (inAtBlock > 0) inAtBlock--;
|
|
2733
|
+
return match;
|
|
2734
|
+
}
|
|
2735
|
+
const trimmed = selector.trim();
|
|
2736
|
+
// Don't scope @-rules (@media, @keyframes, @supports, @container, @layer, @font-face, etc.)
|
|
2737
|
+
if (trimmed.startsWith('@')) {
|
|
2738
|
+
inAtBlock++;
|
|
2739
|
+
return match;
|
|
2740
|
+
}
|
|
2741
|
+
// Don't scope keyframe stops (from, to, 0%, 50%, etc.)
|
|
2742
|
+
if (inAtBlock > 0 && /^[\d%\s,fromto]+$/.test(trimmed.replace(/\s/g, ''))) {
|
|
2743
|
+
return match;
|
|
2744
|
+
}
|
|
1088
2745
|
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
1089
2746
|
});
|
|
1090
2747
|
const styleEl = document.createElement('style');
|
|
1091
2748
|
styleEl.textContent = scoped;
|
|
1092
2749
|
styleEl.setAttribute('data-zq-component', this._def._name || '');
|
|
2750
|
+
styleEl.setAttribute('data-zq-scope', scopeAttr);
|
|
2751
|
+
if (this._def._resolvedStyleUrls) {
|
|
2752
|
+
styleEl.setAttribute('data-zq-style-urls', this._def._resolvedStyleUrls.join(' '));
|
|
2753
|
+
if (this._def.styles) {
|
|
2754
|
+
styleEl.setAttribute('data-zq-inline', this._def.styles);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
1093
2757
|
document.head.appendChild(styleEl);
|
|
1094
2758
|
this._styleEl = styleEl;
|
|
1095
2759
|
}
|
|
1096
2760
|
|
|
1097
2761
|
// -- Focus preservation ----------------------------------------
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
// input/textarea/select inside the component, not only z-model.
|
|
2762
|
+
// DOM morphing preserves unchanged nodes naturally, but we still
|
|
2763
|
+
// track focus for cases where the focused element's subtree changes.
|
|
1101
2764
|
let _focusInfo = null;
|
|
1102
2765
|
const _active = document.activeElement;
|
|
1103
2766
|
if (_active && this._el.contains(_active)) {
|
|
1104
2767
|
const modelKey = _active.getAttribute?.('z-model');
|
|
1105
2768
|
const refKey = _active.getAttribute?.('z-ref');
|
|
1106
|
-
// Build a selector that can locate the same element after re-render
|
|
1107
2769
|
let selector = null;
|
|
1108
2770
|
if (modelKey) {
|
|
1109
2771
|
selector = `[z-model="${modelKey}"]`;
|
|
1110
2772
|
} else if (refKey) {
|
|
1111
2773
|
selector = `[z-ref="${refKey}"]`;
|
|
1112
2774
|
} else {
|
|
1113
|
-
// Fallback: match by tag + type + name + placeholder combination
|
|
1114
2775
|
const tag = _active.tagName.toLowerCase();
|
|
1115
2776
|
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
1116
2777
|
let s = tag;
|
|
@@ -1130,8 +2791,13 @@ class Component {
|
|
|
1130
2791
|
}
|
|
1131
2792
|
}
|
|
1132
2793
|
|
|
1133
|
-
// Update DOM
|
|
1134
|
-
|
|
2794
|
+
// Update DOM via morphing (diffing) — preserves unchanged nodes
|
|
2795
|
+
// First render uses innerHTML for speed; subsequent renders morph.
|
|
2796
|
+
if (!this._mounted) {
|
|
2797
|
+
this._el.innerHTML = html;
|
|
2798
|
+
} else {
|
|
2799
|
+
morph(this._el, html);
|
|
2800
|
+
}
|
|
1135
2801
|
|
|
1136
2802
|
// Process structural & attribute directives
|
|
1137
2803
|
this._processDirectives();
|
|
@@ -1141,10 +2807,10 @@ class Component {
|
|
|
1141
2807
|
this._bindRefs();
|
|
1142
2808
|
this._bindModels();
|
|
1143
2809
|
|
|
1144
|
-
// Restore focus
|
|
2810
|
+
// Restore focus if the morph replaced the focused element
|
|
1145
2811
|
if (_focusInfo) {
|
|
1146
2812
|
const el = this._el.querySelector(_focusInfo.selector);
|
|
1147
|
-
if (el) {
|
|
2813
|
+
if (el && el !== document.activeElement) {
|
|
1148
2814
|
el.focus();
|
|
1149
2815
|
try {
|
|
1150
2816
|
if (_focusInfo.start !== null && _focusInfo.start !== undefined) {
|
|
@@ -1159,9 +2825,15 @@ class Component {
|
|
|
1159
2825
|
|
|
1160
2826
|
if (!this._mounted) {
|
|
1161
2827
|
this._mounted = true;
|
|
1162
|
-
if (this._def.mounted)
|
|
2828
|
+
if (this._def.mounted) {
|
|
2829
|
+
try { this._def.mounted.call(this); }
|
|
2830
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" mounted() threw`, { component: this._def._name }, err); }
|
|
2831
|
+
}
|
|
1163
2832
|
} else {
|
|
1164
|
-
if (this._def.updated)
|
|
2833
|
+
if (this._def.updated) {
|
|
2834
|
+
try { this._def.updated.call(this); }
|
|
2835
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" updated() threw`, { component: this._def._name }, err); }
|
|
2836
|
+
}
|
|
1165
2837
|
}
|
|
1166
2838
|
}
|
|
1167
2839
|
|
|
@@ -1370,18 +3042,13 @@ class Component {
|
|
|
1370
3042
|
}
|
|
1371
3043
|
|
|
1372
3044
|
// ---------------------------------------------------------------------------
|
|
1373
|
-
// Expression evaluator —
|
|
3045
|
+
// Expression evaluator — CSP-safe parser (no eval / new Function)
|
|
1374
3046
|
// ---------------------------------------------------------------------------
|
|
1375
3047
|
_evalExpr(expr) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
this.props,
|
|
1381
|
-
this.refs,
|
|
1382
|
-
typeof window !== 'undefined' ? window.$ : undefined
|
|
1383
|
-
);
|
|
1384
|
-
} catch { return undefined; }
|
|
3048
|
+
return safeEval(expr, [
|
|
3049
|
+
this.state.__raw || this.state,
|
|
3050
|
+
{ props: this.props, refs: this.refs, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
3051
|
+
]);
|
|
1385
3052
|
}
|
|
1386
3053
|
|
|
1387
3054
|
// ---------------------------------------------------------------------------
|
|
@@ -1443,13 +3110,15 @@ class Component {
|
|
|
1443
3110
|
const evalReplace = (str, item, index) =>
|
|
1444
3111
|
str.replace(/\{\{(.+?)\}\}/g, (_, inner) => {
|
|
1445
3112
|
try {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
3113
|
+
const loopScope = {};
|
|
3114
|
+
loopScope[itemVar] = item;
|
|
3115
|
+
loopScope[indexVar] = index;
|
|
3116
|
+
const result = safeEval(inner.trim(), [
|
|
3117
|
+
loopScope,
|
|
1449
3118
|
this.state.__raw || this.state,
|
|
1450
|
-
this.props,
|
|
1451
|
-
|
|
1452
|
-
|
|
3119
|
+
{ props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
3120
|
+
]);
|
|
3121
|
+
return result != null ? result : '';
|
|
1453
3122
|
} catch { return ''; }
|
|
1454
3123
|
});
|
|
1455
3124
|
|
|
@@ -1614,7 +3283,10 @@ class Component {
|
|
|
1614
3283
|
destroy() {
|
|
1615
3284
|
if (this._destroyed) return;
|
|
1616
3285
|
this._destroyed = true;
|
|
1617
|
-
if (this._def.destroyed)
|
|
3286
|
+
if (this._def.destroyed) {
|
|
3287
|
+
try { this._def.destroyed.call(this); }
|
|
3288
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" destroyed() threw`, { component: this._def._name }, err); }
|
|
3289
|
+
}
|
|
1618
3290
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
1619
3291
|
this._listeners = [];
|
|
1620
3292
|
if (this._styleEl) this._styleEl.remove();
|
|
@@ -1627,7 +3299,8 @@ class Component {
|
|
|
1627
3299
|
// Reserved definition keys (not user methods)
|
|
1628
3300
|
const _reservedKeys = new Set([
|
|
1629
3301
|
'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
|
|
1630
|
-
'templateUrl', 'styleUrl', 'templates', 'pages', 'activePage', 'base'
|
|
3302
|
+
'templateUrl', 'styleUrl', 'templates', 'pages', 'activePage', 'base',
|
|
3303
|
+
'computed', 'watch'
|
|
1631
3304
|
]);
|
|
1632
3305
|
|
|
1633
3306
|
|
|
@@ -1641,8 +3314,11 @@ const _reservedKeys = new Set([
|
|
|
1641
3314
|
* @param {object} definition — component definition
|
|
1642
3315
|
*/
|
|
1643
3316
|
function component(name, definition) {
|
|
3317
|
+
if (!name || typeof name !== 'string') {
|
|
3318
|
+
throw new ZQueryError(ErrorCode.COMP_INVALID_NAME, 'Component name must be a non-empty string');
|
|
3319
|
+
}
|
|
1644
3320
|
if (!name.includes('-')) {
|
|
1645
|
-
throw new
|
|
3321
|
+
throw new ZQueryError(ErrorCode.COMP_INVALID_NAME, `Component name "${name}" must contain a hyphen (Web Component convention)`);
|
|
1646
3322
|
}
|
|
1647
3323
|
definition._name = name;
|
|
1648
3324
|
|
|
@@ -1667,10 +3343,10 @@ function component(name, definition) {
|
|
|
1667
3343
|
*/
|
|
1668
3344
|
function mount(target, componentName, props = {}) {
|
|
1669
3345
|
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
1670
|
-
if (!el) throw new
|
|
3346
|
+
if (!el) throw new ZQueryError(ErrorCode.COMP_MOUNT_TARGET, `Mount target "${target}" not found`, { target });
|
|
1671
3347
|
|
|
1672
3348
|
const def = _registry.get(componentName);
|
|
1673
|
-
if (!def) throw new
|
|
3349
|
+
if (!def) throw new ZQueryError(ErrorCode.COMP_NOT_FOUND, `Component "${componentName}" not registered`, { component: componentName });
|
|
1674
3350
|
|
|
1675
3351
|
// Destroy existing instance
|
|
1676
3352
|
if (_instances.has(el)) _instances.get(el).destroy();
|
|
@@ -1693,12 +3369,40 @@ function mountAll(root = document.body) {
|
|
|
1693
3369
|
|
|
1694
3370
|
// Extract props from attributes
|
|
1695
3371
|
const props = {};
|
|
3372
|
+
|
|
3373
|
+
// Find parent component instance for evaluating dynamic prop expressions
|
|
3374
|
+
let parentInstance = null;
|
|
3375
|
+
let ancestor = tag.parentElement;
|
|
3376
|
+
while (ancestor) {
|
|
3377
|
+
if (_instances.has(ancestor)) {
|
|
3378
|
+
parentInstance = _instances.get(ancestor);
|
|
3379
|
+
break;
|
|
3380
|
+
}
|
|
3381
|
+
ancestor = ancestor.parentElement;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
1696
3384
|
[...tag.attributes].forEach(attr => {
|
|
1697
|
-
if (
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
3385
|
+
if (attr.name.startsWith('@') || attr.name.startsWith('z-')) return;
|
|
3386
|
+
|
|
3387
|
+
// Dynamic prop: :propName="expression" — evaluate in parent context
|
|
3388
|
+
if (attr.name.startsWith(':')) {
|
|
3389
|
+
const propName = attr.name.slice(1);
|
|
3390
|
+
if (parentInstance) {
|
|
3391
|
+
props[propName] = safeEval(attr.value, [
|
|
3392
|
+
parentInstance.state.__raw || parentInstance.state,
|
|
3393
|
+
{ props: parentInstance.props, refs: parentInstance.refs, computed: parentInstance.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
3394
|
+
]);
|
|
3395
|
+
} else {
|
|
3396
|
+
// No parent — try JSON parse
|
|
3397
|
+
try { props[propName] = JSON.parse(attr.value); }
|
|
3398
|
+
catch { props[propName] = attr.value; }
|
|
3399
|
+
}
|
|
3400
|
+
return;
|
|
1701
3401
|
}
|
|
3402
|
+
|
|
3403
|
+
// Static prop
|
|
3404
|
+
try { props[attr.name] = JSON.parse(attr.value); }
|
|
3405
|
+
catch { props[attr.name] = attr.value; }
|
|
1702
3406
|
});
|
|
1703
3407
|
|
|
1704
3408
|
const instance = new Component(tag, def, props);
|
|
@@ -1836,7 +3540,7 @@ function style(urls, opts = {}) {
|
|
|
1836
3540
|
};
|
|
1837
3541
|
}
|
|
1838
3542
|
|
|
1839
|
-
// --- src/router.js
|
|
3543
|
+
// --- src/router.js -----------------------------------------------
|
|
1840
3544
|
/**
|
|
1841
3545
|
* zQuery Router — Client-side SPA router
|
|
1842
3546
|
*
|
|
@@ -1857,6 +3561,7 @@ function style(urls, opts = {}) {
|
|
|
1857
3561
|
*/
|
|
1858
3562
|
|
|
1859
3563
|
|
|
3564
|
+
|
|
1860
3565
|
class Router {
|
|
1861
3566
|
constructor(config = {}) {
|
|
1862
3567
|
this._el = null;
|
|
@@ -1915,7 +3620,21 @@ class Router {
|
|
|
1915
3620
|
if (!link) return;
|
|
1916
3621
|
if (link.getAttribute('target') === '_blank') return;
|
|
1917
3622
|
e.preventDefault();
|
|
1918
|
-
|
|
3623
|
+
let href = link.getAttribute('z-link');
|
|
3624
|
+
// Support z-link-params for dynamic :param interpolation
|
|
3625
|
+
const paramsAttr = link.getAttribute('z-link-params');
|
|
3626
|
+
if (paramsAttr) {
|
|
3627
|
+
try {
|
|
3628
|
+
const params = JSON.parse(paramsAttr);
|
|
3629
|
+
href = this._interpolateParams(href, params);
|
|
3630
|
+
} catch { /* ignore malformed JSON */ }
|
|
3631
|
+
}
|
|
3632
|
+
this.navigate(href);
|
|
3633
|
+
// z-to-top modifier: scroll to top after navigation
|
|
3634
|
+
if (link.hasAttribute('z-to-top')) {
|
|
3635
|
+
const scrollBehavior = link.getAttribute('z-to-top') || 'instant';
|
|
3636
|
+
window.scrollTo({ top: 0, behavior: scrollBehavior });
|
|
3637
|
+
}
|
|
1919
3638
|
});
|
|
1920
3639
|
|
|
1921
3640
|
// Initial resolve
|
|
@@ -1959,7 +3678,23 @@ class Router {
|
|
|
1959
3678
|
|
|
1960
3679
|
// --- Navigation ----------------------------------------------------------
|
|
1961
3680
|
|
|
3681
|
+
/**
|
|
3682
|
+
* Interpolate :param placeholders in a path with the given values.
|
|
3683
|
+
* @param {string} path — e.g. '/user/:id/posts/:pid'
|
|
3684
|
+
* @param {Object} params — e.g. { id: 42, pid: 7 }
|
|
3685
|
+
* @returns {string}
|
|
3686
|
+
*/
|
|
3687
|
+
_interpolateParams(path, params) {
|
|
3688
|
+
if (!params || typeof params !== 'object') return path;
|
|
3689
|
+
return path.replace(/:([\w]+)/g, (_, key) => {
|
|
3690
|
+
const val = params[key];
|
|
3691
|
+
return val != null ? encodeURIComponent(String(val)) : ':' + key;
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
|
|
1962
3695
|
navigate(path, options = {}) {
|
|
3696
|
+
// Interpolate :param placeholders if options.params is provided
|
|
3697
|
+
if (options.params) path = this._interpolateParams(path, options.params);
|
|
1963
3698
|
// Separate hash fragment (e.g. /docs/getting-started#cli-bundler)
|
|
1964
3699
|
const [cleanPath, fragment] = (path || '').split('#');
|
|
1965
3700
|
let normalized = this._normalizePath(cleanPath);
|
|
@@ -1977,6 +3712,8 @@ class Router {
|
|
|
1977
3712
|
}
|
|
1978
3713
|
|
|
1979
3714
|
replace(path, options = {}) {
|
|
3715
|
+
// Interpolate :param placeholders if options.params is provided
|
|
3716
|
+
if (options.params) path = this._interpolateParams(path, options.params);
|
|
1980
3717
|
const [cleanPath, fragment] = (path || '').split('#');
|
|
1981
3718
|
let normalized = this._normalizePath(cleanPath);
|
|
1982
3719
|
const hash = fragment ? '#' + fragment : '';
|
|
@@ -2090,6 +3827,7 @@ class Router {
|
|
|
2090
3827
|
// Prevent re-entrant calls (e.g. listener triggering navigation)
|
|
2091
3828
|
if (this._resolving) return;
|
|
2092
3829
|
this._resolving = true;
|
|
3830
|
+
this._redirectCount = 0;
|
|
2093
3831
|
try {
|
|
2094
3832
|
await this.__resolve();
|
|
2095
3833
|
} finally {
|
|
@@ -2127,10 +3865,29 @@ class Router {
|
|
|
2127
3865
|
|
|
2128
3866
|
// Run before guards
|
|
2129
3867
|
for (const guard of this._guards.before) {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
3868
|
+
try {
|
|
3869
|
+
const result = await guard(to, from);
|
|
3870
|
+
if (result === false) return; // Cancel
|
|
3871
|
+
if (typeof result === 'string') { // Redirect
|
|
3872
|
+
if (++this._redirectCount > 10) {
|
|
3873
|
+
reportError(ErrorCode.ROUTER_GUARD, 'Too many guard redirects (possible loop)', { to }, null);
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
// Update URL directly and re-resolve (avoids re-entrancy block)
|
|
3877
|
+
const [rPath, rFrag] = result.split('#');
|
|
3878
|
+
const rNorm = this._normalizePath(rPath || '/');
|
|
3879
|
+
const rHash = rFrag ? '#' + rFrag : '';
|
|
3880
|
+
if (this._mode === 'hash') {
|
|
3881
|
+
if (rFrag) window.__zqScrollTarget = rFrag;
|
|
3882
|
+
window.location.replace('#' + rNorm);
|
|
3883
|
+
} else {
|
|
3884
|
+
window.history.replaceState({}, '', this._base + rNorm + rHash);
|
|
3885
|
+
}
|
|
3886
|
+
return this.__resolve();
|
|
3887
|
+
}
|
|
3888
|
+
} catch (err) {
|
|
3889
|
+
reportError(ErrorCode.ROUTER_GUARD, 'Before-guard threw', { to, from }, err);
|
|
3890
|
+
return;
|
|
2134
3891
|
}
|
|
2135
3892
|
}
|
|
2136
3893
|
|
|
@@ -2138,7 +3895,7 @@ class Router {
|
|
|
2138
3895
|
if (matched.load) {
|
|
2139
3896
|
try { await matched.load(); }
|
|
2140
3897
|
catch (err) {
|
|
2141
|
-
|
|
3898
|
+
reportError(ErrorCode.ROUTER_LOAD, `Failed to load module for route "${matched.path}"`, { path: matched.path }, err);
|
|
2142
3899
|
return;
|
|
2143
3900
|
}
|
|
2144
3901
|
}
|
|
@@ -2205,7 +3962,7 @@ function getRouter() {
|
|
|
2205
3962
|
return _activeRouter;
|
|
2206
3963
|
}
|
|
2207
3964
|
|
|
2208
|
-
// --- src/store.js
|
|
3965
|
+
// --- src/store.js ------------------------------------------------
|
|
2209
3966
|
/**
|
|
2210
3967
|
* zQuery Store — Global reactive state management
|
|
2211
3968
|
*
|
|
@@ -2234,6 +3991,7 @@ function getRouter() {
|
|
|
2234
3991
|
*/
|
|
2235
3992
|
|
|
2236
3993
|
|
|
3994
|
+
|
|
2237
3995
|
class Store {
|
|
2238
3996
|
constructor(config = {}) {
|
|
2239
3997
|
this._subscribers = new Map(); // key → Set<fn>
|
|
@@ -2250,9 +4008,15 @@ class Store {
|
|
|
2250
4008
|
this.state = reactive(initial, (key, value, old) => {
|
|
2251
4009
|
// Notify key-specific subscribers
|
|
2252
4010
|
const subs = this._subscribers.get(key);
|
|
2253
|
-
if (subs) subs.forEach(fn =>
|
|
4011
|
+
if (subs) subs.forEach(fn => {
|
|
4012
|
+
try { fn(value, old, key); }
|
|
4013
|
+
catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, `Subscriber for "${key}" threw`, { key }, err); }
|
|
4014
|
+
});
|
|
2254
4015
|
// Notify wildcard subscribers
|
|
2255
|
-
this._wildcards.forEach(fn =>
|
|
4016
|
+
this._wildcards.forEach(fn => {
|
|
4017
|
+
try { fn(key, value, old); }
|
|
4018
|
+
catch (err) { reportError(ErrorCode.STORE_SUBSCRIBE, 'Wildcard subscriber threw', { key }, err); }
|
|
4019
|
+
});
|
|
2256
4020
|
});
|
|
2257
4021
|
|
|
2258
4022
|
// Build getters as computed properties
|
|
@@ -2273,23 +4037,32 @@ class Store {
|
|
|
2273
4037
|
dispatch(name, ...args) {
|
|
2274
4038
|
const action = this._actions[name];
|
|
2275
4039
|
if (!action) {
|
|
2276
|
-
|
|
4040
|
+
reportError(ErrorCode.STORE_ACTION, `Unknown action "${name}"`, { action: name, args });
|
|
2277
4041
|
return;
|
|
2278
4042
|
}
|
|
2279
4043
|
|
|
2280
4044
|
// Run middleware
|
|
2281
4045
|
for (const mw of this._middleware) {
|
|
2282
|
-
|
|
2283
|
-
|
|
4046
|
+
try {
|
|
4047
|
+
const result = mw(name, args, this.state);
|
|
4048
|
+
if (result === false) return; // blocked by middleware
|
|
4049
|
+
} catch (err) {
|
|
4050
|
+
reportError(ErrorCode.STORE_MIDDLEWARE, `Middleware threw during "${name}"`, { action: name }, err);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
2284
4053
|
}
|
|
2285
4054
|
|
|
2286
4055
|
if (this._debug) {
|
|
2287
4056
|
console.log(`%c[Store] ${name}`, 'color: #4CAF50; font-weight: bold;', ...args);
|
|
2288
4057
|
}
|
|
2289
4058
|
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
4059
|
+
try {
|
|
4060
|
+
const result = action(this.state, ...args);
|
|
4061
|
+
this._history.push({ action: name, args, timestamp: Date.now() });
|
|
4062
|
+
return result;
|
|
4063
|
+
} catch (err) {
|
|
4064
|
+
reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
|
|
4065
|
+
}
|
|
2293
4066
|
}
|
|
2294
4067
|
|
|
2295
4068
|
/**
|
|
@@ -2375,7 +4148,7 @@ function getStore(name = 'default') {
|
|
|
2375
4148
|
return _stores.get(name) || null;
|
|
2376
4149
|
}
|
|
2377
4150
|
|
|
2378
|
-
// --- src/http.js
|
|
4151
|
+
// --- src/http.js -------------------------------------------------
|
|
2379
4152
|
/**
|
|
2380
4153
|
* zQuery HTTP — Lightweight fetch wrapper
|
|
2381
4154
|
*
|
|
@@ -2408,6 +4181,9 @@ const _interceptors = {
|
|
|
2408
4181
|
* Core request function
|
|
2409
4182
|
*/
|
|
2410
4183
|
async function request(method, url, data, options = {}) {
|
|
4184
|
+
if (!url || typeof url !== 'string') {
|
|
4185
|
+
throw new Error(`HTTP request requires a URL string, got ${typeof url}`);
|
|
4186
|
+
}
|
|
2411
4187
|
let fullURL = url.startsWith('http') ? url : _config.baseURL + url;
|
|
2412
4188
|
let headers = { ..._config.headers, ...options.headers };
|
|
2413
4189
|
let body = undefined;
|
|
@@ -2463,16 +4239,21 @@ async function request(method, url, data, options = {}) {
|
|
|
2463
4239
|
const contentType = response.headers.get('Content-Type') || '';
|
|
2464
4240
|
let responseData;
|
|
2465
4241
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
4242
|
+
try {
|
|
4243
|
+
if (contentType.includes('application/json')) {
|
|
4244
|
+
responseData = await response.json();
|
|
4245
|
+
} else if (contentType.includes('text/')) {
|
|
4246
|
+
responseData = await response.text();
|
|
4247
|
+
} else if (contentType.includes('application/octet-stream') || contentType.includes('image/')) {
|
|
4248
|
+
responseData = await response.blob();
|
|
4249
|
+
} else {
|
|
4250
|
+
// Try JSON first, fall back to text
|
|
4251
|
+
const text = await response.text();
|
|
4252
|
+
try { responseData = JSON.parse(text); } catch { responseData = text; }
|
|
4253
|
+
}
|
|
4254
|
+
} catch (parseErr) {
|
|
4255
|
+
responseData = null;
|
|
4256
|
+
console.warn(`[zQuery HTTP] Failed to parse response body from ${method} ${fullURL}:`, parseErr.message);
|
|
2476
4257
|
}
|
|
2477
4258
|
|
|
2478
4259
|
const result = {
|
|
@@ -2554,7 +4335,7 @@ const http = {
|
|
|
2554
4335
|
raw: (url, opts) => fetch(url, opts),
|
|
2555
4336
|
};
|
|
2556
4337
|
|
|
2557
|
-
// --- src/utils.js
|
|
4338
|
+
// --- src/utils.js ------------------------------------------------
|
|
2558
4339
|
/**
|
|
2559
4340
|
* zQuery Utils — Common utility functions
|
|
2560
4341
|
*
|
|
@@ -2827,7 +4608,7 @@ class EventBus {
|
|
|
2827
4608
|
|
|
2828
4609
|
const bus = new EventBus();
|
|
2829
4610
|
|
|
2830
|
-
// --- index.js (assembly)
|
|
4611
|
+
// --- index.js (assembly) ------------------------------------------
|
|
2831
4612
|
/**
|
|
2832
4613
|
* ┌---------------------------------------------------------┐
|
|
2833
4614
|
* │ zQuery (zeroQuery) — Lightweight Frontend Library │
|
|
@@ -2846,21 +4627,24 @@ const bus = new EventBus();
|
|
|
2846
4627
|
|
|
2847
4628
|
|
|
2848
4629
|
|
|
4630
|
+
|
|
4631
|
+
|
|
4632
|
+
|
|
2849
4633
|
// ---------------------------------------------------------------------------
|
|
2850
4634
|
// $ — The main function & namespace
|
|
2851
4635
|
// ---------------------------------------------------------------------------
|
|
2852
4636
|
|
|
2853
4637
|
/**
|
|
2854
|
-
* Main selector function
|
|
4638
|
+
* Main selector function — always returns a ZQueryCollection (like jQuery).
|
|
2855
4639
|
*
|
|
2856
|
-
* $('selector') →
|
|
2857
|
-
* $('<div>hello</div>') →
|
|
2858
|
-
* $(element) →
|
|
4640
|
+
* $('selector') → ZQueryCollection (querySelectorAll)
|
|
4641
|
+
* $('<div>hello</div>') → ZQueryCollection from created elements
|
|
4642
|
+
* $(element) → ZQueryCollection wrapping the element
|
|
2859
4643
|
* $(fn) → DOMContentLoaded shorthand
|
|
2860
4644
|
*
|
|
2861
4645
|
* @param {string|Element|NodeList|Function} selector
|
|
2862
4646
|
* @param {string|Element} [context]
|
|
2863
|
-
* @returns {
|
|
4647
|
+
* @returns {ZQueryCollection}
|
|
2864
4648
|
*/
|
|
2865
4649
|
function $(selector, context) {
|
|
2866
4650
|
// $(fn) → DOM ready shorthand
|
|
@@ -2880,8 +4664,6 @@ $.tag = query.tag;
|
|
|
2880
4664
|
Object.defineProperty($, 'name', {
|
|
2881
4665
|
value: query.name, writable: true, configurable: true
|
|
2882
4666
|
});
|
|
2883
|
-
$.attr = query.attr;
|
|
2884
|
-
$.data = query.data;
|
|
2885
4667
|
$.children = query.children;
|
|
2886
4668
|
|
|
2887
4669
|
// --- Collection selector ---------------------------------------------------
|
|
@@ -2923,6 +4705,8 @@ $.getInstance = getInstance;
|
|
|
2923
4705
|
$.destroy = destroy;
|
|
2924
4706
|
$.components = getRegistry;
|
|
2925
4707
|
$.style = style;
|
|
4708
|
+
$.morph = morph;
|
|
4709
|
+
$.safeEval = safeEval;
|
|
2926
4710
|
|
|
2927
4711
|
// --- Router ----------------------------------------------------------------
|
|
2928
4712
|
$.router = createRouter;
|
|
@@ -2961,8 +4745,13 @@ $.storage = storage;
|
|
|
2961
4745
|
$.session = session;
|
|
2962
4746
|
$.bus = bus;
|
|
2963
4747
|
|
|
4748
|
+
// --- Error handling --------------------------------------------------------
|
|
4749
|
+
$.onError = onError;
|
|
4750
|
+
$.ZQueryError = ZQueryError;
|
|
4751
|
+
$.ErrorCode = ErrorCode;
|
|
4752
|
+
|
|
2964
4753
|
// --- Meta ------------------------------------------------------------------
|
|
2965
|
-
$.version = '0.5
|
|
4754
|
+
$.version = '0.7.5';
|
|
2966
4755
|
$.meta = {}; // populated at build time by CLI bundler
|
|
2967
4756
|
|
|
2968
4757
|
$.noConflict = () => {
|