zero-query 0.9.7 → 0.9.9
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 +55 -4
- package/cli/commands/build.js +50 -3
- package/cli/commands/create.js +58 -11
- package/cli/help.js +4 -0
- package/cli/scaffold/default/app/app.js +211 -0
- package/cli/scaffold/default/app/components/about.js +201 -0
- package/cli/scaffold/default/app/components/api-demo.js +143 -0
- package/cli/scaffold/default/app/components/contact-card.js +231 -0
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -0
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -0
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -0
- package/cli/scaffold/default/app/components/counter.js +127 -0
- package/cli/scaffold/default/app/components/home.js +249 -0
- package/cli/scaffold/{app → default/app}/components/not-found.js +2 -2
- package/cli/scaffold/default/app/components/playground/playground.css +116 -0
- package/cli/scaffold/default/app/components/playground/playground.html +162 -0
- package/cli/scaffold/default/app/components/playground/playground.js +117 -0
- package/cli/scaffold/default/app/components/todos.js +225 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -0
- package/cli/scaffold/default/app/routes.js +15 -0
- package/cli/scaffold/{app → default/app}/store.js +15 -10
- package/cli/scaffold/{global.css → default/global.css} +238 -252
- package/cli/scaffold/{index.html → default/index.html} +35 -0
- package/cli/scaffold/{app → minimal/app}/app.js +37 -39
- package/cli/scaffold/minimal/app/components/about.js +68 -0
- package/cli/scaffold/minimal/app/components/counter.js +122 -0
- package/cli/scaffold/minimal/app/components/home.js +68 -0
- package/cli/scaffold/minimal/app/components/not-found.js +16 -0
- package/cli/scaffold/minimal/app/routes.js +9 -0
- package/cli/scaffold/minimal/app/store.js +36 -0
- package/cli/scaffold/minimal/assets/.gitkeep +0 -0
- package/cli/scaffold/minimal/global.css +291 -0
- package/cli/scaffold/minimal/index.html +44 -0
- package/cli/scaffold/ssr/app/app.js +30 -0
- package/cli/scaffold/ssr/app/components/about.js +28 -0
- package/cli/scaffold/ssr/app/components/home.js +37 -0
- package/cli/scaffold/ssr/app/components/not-found.js +15 -0
- package/cli/scaffold/ssr/app/routes.js +6 -0
- package/cli/scaffold/ssr/global.css +113 -0
- package/cli/scaffold/ssr/index.html +31 -0
- package/cli/scaffold/ssr/package.json +8 -0
- package/cli/scaffold/ssr/server/index.js +118 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2006 -1933
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +20 -1
- package/index.js +8 -5
- package/package.json +8 -2
- package/src/component.js +6 -3
- package/src/diff.js +15 -2
- package/src/errors.js +59 -5
- package/src/package.json +1 -0
- package/src/ssr.js +116 -22
- package/tests/cli.test.js +304 -0
- package/tests/errors.test.js +423 -145
- package/tests/ssr.test.js +435 -3
- package/types/errors.d.ts +34 -2
- package/types/ssr.d.ts +21 -1
- package/cli/scaffold/app/components/about.js +0 -131
- package/cli/scaffold/app/components/api-demo.js +0 -103
- package/cli/scaffold/app/components/contacts/contacts.css +0 -246
- package/cli/scaffold/app/components/contacts/contacts.html +0 -140
- package/cli/scaffold/app/components/contacts/contacts.js +0 -153
- package/cli/scaffold/app/components/counter.js +0 -85
- package/cli/scaffold/app/components/home.js +0 -137
- package/cli/scaffold/app/components/todos.js +0 -131
- package/cli/scaffold/app/routes.js +0 -13
- /package/cli/scaffold/{LICENSE → default/LICENSE} +0 -0
- /package/cli/scaffold/{assets → default/assets}/.gitkeep +0 -0
- /package/cli/scaffold/{favicon.ico → default/favicon.ico} +0 -0
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.9.
|
|
2
|
+
* zQuery (zeroQuery) v0.9.9
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -59,6 +59,12 @@ const ErrorCode = Object.freeze({
|
|
|
59
59
|
HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
|
|
60
60
|
HTTP_PARSE: 'ZQ_HTTP_PARSE',
|
|
61
61
|
|
|
62
|
+
// SSR
|
|
63
|
+
SSR_RENDER: 'ZQ_SSR_RENDER',
|
|
64
|
+
SSR_COMPONENT: 'ZQ_SSR_COMPONENT',
|
|
65
|
+
SSR_HYDRATION: 'ZQ_SSR_HYDRATION',
|
|
66
|
+
SSR_PAGE: 'ZQ_SSR_PAGE',
|
|
67
|
+
|
|
62
68
|
// General
|
|
63
69
|
INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
|
|
64
70
|
});
|
|
@@ -87,16 +93,28 @@ class ZQueryError extends Error {
|
|
|
87
93
|
// ---------------------------------------------------------------------------
|
|
88
94
|
// Global error handler
|
|
89
95
|
// ---------------------------------------------------------------------------
|
|
90
|
-
let
|
|
96
|
+
let _errorHandlers = [];
|
|
91
97
|
|
|
92
98
|
/**
|
|
93
99
|
* Register a global error handler.
|
|
94
100
|
* Called whenever zQuery catches an error internally.
|
|
101
|
+
* Multiple handlers are supported — each receives the error.
|
|
102
|
+
* Pass `null` to clear all handlers.
|
|
95
103
|
*
|
|
96
104
|
* @param {Function|null} handler — (error: ZQueryError) => void
|
|
105
|
+
* @returns {Function} unsubscribe function to remove this handler
|
|
97
106
|
*/
|
|
98
107
|
function onError(handler) {
|
|
99
|
-
|
|
108
|
+
if (handler === null) {
|
|
109
|
+
_errorHandlers = [];
|
|
110
|
+
return () => {};
|
|
111
|
+
}
|
|
112
|
+
if (typeof handler !== 'function') return () => {};
|
|
113
|
+
_errorHandlers.push(handler);
|
|
114
|
+
return () => {
|
|
115
|
+
const idx = _errorHandlers.indexOf(handler);
|
|
116
|
+
if (idx !== -1) _errorHandlers.splice(idx, 1);
|
|
117
|
+
};
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
/**
|
|
@@ -113,9 +131,9 @@ function reportError(code, message, context = {}, cause) {
|
|
|
113
131
|
? cause
|
|
114
132
|
: new ZQueryError(code, message, context, cause);
|
|
115
133
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
try {
|
|
134
|
+
// Notify all registered handlers
|
|
135
|
+
for (const handler of _errorHandlers) {
|
|
136
|
+
try { handler(err); } catch { /* prevent handler from crashing framework */ }
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
// Always log for developer visibility
|
|
@@ -162,6 +180,42 @@ function validate(value, name, expectedType) {
|
|
|
162
180
|
`"${name}" must be a ${expectedType}, got ${typeof value}`
|
|
163
181
|
);
|
|
164
182
|
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format a ZQueryError into a structured object suitable for overlays/logging.
|
|
187
|
+
* @param {ZQueryError|Error} err
|
|
188
|
+
* @returns {{ code: string, type: string, message: string, context: object, stack: string }}
|
|
189
|
+
*/
|
|
190
|
+
function formatError(err) {
|
|
191
|
+
const isZQ = err instanceof ZQueryError;
|
|
192
|
+
return {
|
|
193
|
+
code: isZQ ? err.code : '',
|
|
194
|
+
type: isZQ ? 'ZQueryError' : (err.name || 'Error'),
|
|
195
|
+
message: err.message || 'Unknown error',
|
|
196
|
+
context: isZQ ? err.context : {},
|
|
197
|
+
stack: err.stack || '',
|
|
198
|
+
cause: err.cause ? formatError(err.cause) : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Async version of guardCallback — wraps an async function so that
|
|
204
|
+
* rejections are caught, reported, and don't crash execution.
|
|
205
|
+
*
|
|
206
|
+
* @param {Function} fn — async function
|
|
207
|
+
* @param {string} code — ErrorCode to use
|
|
208
|
+
* @param {object} [context]
|
|
209
|
+
* @returns {Function}
|
|
210
|
+
*/
|
|
211
|
+
function guardAsync(fn, code, context = {}) {
|
|
212
|
+
return async (...args) => {
|
|
213
|
+
try {
|
|
214
|
+
return await fn(...args);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
reportError(code, err.message || 'Async callback error', context, err);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
165
219
|
}
|
|
166
220
|
|
|
167
221
|
// --- src/reactive.js ---------------------------------------------
|
|
@@ -353,2338 +407,2351 @@ function effect(fn) {
|
|
|
353
407
|
};
|
|
354
408
|
}
|
|
355
409
|
|
|
356
|
-
// --- src/
|
|
410
|
+
// --- src/diff.js -------------------------------------------------
|
|
357
411
|
/**
|
|
358
|
-
* zQuery
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
412
|
+
* zQuery Diff — Lightweight DOM morphing engine
|
|
413
|
+
*
|
|
414
|
+
* Patches an existing DOM tree to match new HTML without destroying nodes
|
|
415
|
+
* that haven't changed. Preserves focus, scroll positions, third-party
|
|
416
|
+
* widget state, video playback, and other live DOM state.
|
|
417
|
+
*
|
|
418
|
+
* Approach: walk old and new trees in parallel, reconcile node by node.
|
|
419
|
+
* Keyed elements (via `z-key`) get matched across position changes.
|
|
420
|
+
*
|
|
421
|
+
* Performance advantages over virtual DOM (React/Angular):
|
|
422
|
+
* - No virtual tree allocation or diffing — works directly on real DOM
|
|
423
|
+
* - Skips unchanged subtrees via fast isEqualNode() check
|
|
424
|
+
* - z-skip attribute to opt out of diffing entire subtrees
|
|
425
|
+
* - Reuses a single template element for HTML parsing (zero GC pressure)
|
|
426
|
+
* - Keyed reconciliation uses LIS (Longest Increasing Subsequence) to
|
|
427
|
+
* minimize DOM moves — same algorithm as Vue 3 / ivi
|
|
428
|
+
* - Minimal attribute diffing with early bail-out
|
|
362
429
|
*/
|
|
363
430
|
|
|
364
|
-
|
|
365
431
|
// ---------------------------------------------------------------------------
|
|
366
|
-
//
|
|
432
|
+
// Reusable template element — avoids per-call allocation
|
|
367
433
|
// ---------------------------------------------------------------------------
|
|
368
|
-
|
|
369
|
-
constructor(elements) {
|
|
370
|
-
this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
|
|
371
|
-
this.length = this.elements.length;
|
|
372
|
-
this.elements.forEach((el, i) => { this[i] = el; });
|
|
373
|
-
}
|
|
434
|
+
let _tpl = null;
|
|
374
435
|
|
|
375
|
-
|
|
436
|
+
function _getTemplate() {
|
|
437
|
+
if (!_tpl) _tpl = document.createElement('template');
|
|
438
|
+
return _tpl;
|
|
439
|
+
}
|
|
376
440
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// morph(existingRoot, newHTML) — patch existing DOM to match newHTML
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
381
444
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Morph an existing DOM element's children to match new HTML.
|
|
447
|
+
* Only touches nodes that actually differ.
|
|
448
|
+
*
|
|
449
|
+
* @param {Element} rootEl — The live DOM container to patch
|
|
450
|
+
* @param {string} newHTML — The desired HTML string
|
|
451
|
+
*/
|
|
452
|
+
function morph(rootEl, newHTML) {
|
|
453
|
+
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
454
|
+
const tpl = _getTemplate();
|
|
455
|
+
tpl.innerHTML = newHTML;
|
|
456
|
+
const newRoot = tpl.content;
|
|
385
457
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
458
|
+
// Move children into a wrapper for consistent handling.
|
|
459
|
+
// We move (not clone) from the template — cheaper than cloning.
|
|
460
|
+
const tempDiv = document.createElement('div');
|
|
461
|
+
while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
|
|
390
462
|
|
|
391
|
-
|
|
392
|
-
last() { return this.elements[this.length - 1] || null; }
|
|
393
|
-
eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
|
|
394
|
-
toArray(){ return [...this.elements]; }
|
|
463
|
+
_morphChildren(rootEl, tempDiv);
|
|
395
464
|
|
|
396
|
-
|
|
465
|
+
if (start) window.__zqMorphHook(rootEl, performance.now() - start);
|
|
466
|
+
}
|
|
397
467
|
|
|
398
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Morph a single element in place — diffs attributes and children
|
|
470
|
+
* without replacing the node reference. Useful for replaceWith-style
|
|
471
|
+
* updates where you want to keep the element identity when the tag
|
|
472
|
+
* name matches.
|
|
473
|
+
*
|
|
474
|
+
* If the new HTML produces a different tag, falls back to native replace.
|
|
475
|
+
*
|
|
476
|
+
* @param {Element} oldEl — The live DOM element to patch
|
|
477
|
+
* @param {string} newHTML — HTML string for the replacement element
|
|
478
|
+
* @returns {Element} — The resulting element (same ref if morphed, new if replaced)
|
|
479
|
+
*/
|
|
480
|
+
function morphElement(oldEl, newHTML) {
|
|
481
|
+
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
482
|
+
const tpl = _getTemplate();
|
|
483
|
+
tpl.innerHTML = newHTML;
|
|
484
|
+
const newEl = tpl.content.firstElementChild;
|
|
485
|
+
if (!newEl) return oldEl;
|
|
399
486
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
487
|
+
// Same tag — morph in place (preserves identity, event listeners, refs)
|
|
488
|
+
if (oldEl.nodeName === newEl.nodeName) {
|
|
489
|
+
_morphAttributes(oldEl, newEl);
|
|
490
|
+
_morphChildren(oldEl, newEl);
|
|
491
|
+
if (start) window.__zqMorphHook(oldEl, performance.now() - start);
|
|
492
|
+
return oldEl;
|
|
404
493
|
}
|
|
405
494
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
495
|
+
// Different tag — must replace (can't morph <div> into <span>)
|
|
496
|
+
const clone = newEl.cloneNode(true);
|
|
497
|
+
oldEl.parentNode.replaceChild(clone, oldEl);
|
|
498
|
+
if (start) window.__zqMorphHook(clone, performance.now() - start);
|
|
499
|
+
return clone;
|
|
500
|
+
}
|
|
410
501
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
502
|
+
// Aliases for the concat build — core.js imports these as _morph / _morphElement,
|
|
503
|
+
// but the build strips `import … as` lines, so the aliases must exist at runtime.
|
|
504
|
+
const _morph = morph;
|
|
505
|
+
const _morphElement = morphElement;
|
|
416
506
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Reconcile children of `oldParent` to match `newParent`.
|
|
509
|
+
*
|
|
510
|
+
* @param {Element} oldParent — live DOM parent
|
|
511
|
+
* @param {Element} newParent — desired state parent
|
|
512
|
+
*/
|
|
513
|
+
function _morphChildren(oldParent, newParent) {
|
|
514
|
+
// Snapshot live NodeLists into arrays — childNodes is live and
|
|
515
|
+
// mutates during insertBefore/removeChild. Using a for loop to push
|
|
516
|
+
// avoids spread operator overhead for large child lists.
|
|
517
|
+
const oldCN = oldParent.childNodes;
|
|
518
|
+
const newCN = newParent.childNodes;
|
|
519
|
+
const oldLen = oldCN.length;
|
|
520
|
+
const newLen = newCN.length;
|
|
521
|
+
const oldChildren = new Array(oldLen);
|
|
522
|
+
const newChildren = new Array(newLen);
|
|
523
|
+
for (let i = 0; i < oldLen; i++) oldChildren[i] = oldCN[i];
|
|
524
|
+
for (let i = 0; i < newLen; i++) newChildren[i] = newCN[i];
|
|
426
525
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!el.parentElement) return;
|
|
431
|
-
const all = [...el.parentElement.children].filter(c => c !== el);
|
|
432
|
-
sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
|
|
433
|
-
});
|
|
434
|
-
return new ZQueryCollection(sibs);
|
|
435
|
-
}
|
|
526
|
+
// Scan for keyed elements — only build maps if keys exist
|
|
527
|
+
let hasKeys = false;
|
|
528
|
+
let oldKeyMap, newKeyMap;
|
|
436
529
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
530
|
+
for (let i = 0; i < oldLen; i++) {
|
|
531
|
+
if (_getKey(oldChildren[i]) != null) { hasKeys = true; break; }
|
|
440
532
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
533
|
+
if (!hasKeys) {
|
|
534
|
+
for (let i = 0; i < newLen; i++) {
|
|
535
|
+
if (_getKey(newChildren[i]) != null) { hasKeys = true; break; }
|
|
536
|
+
}
|
|
445
537
|
}
|
|
446
538
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
539
|
+
if (hasKeys) {
|
|
540
|
+
oldKeyMap = new Map();
|
|
541
|
+
newKeyMap = new Map();
|
|
542
|
+
for (let i = 0; i < oldLen; i++) {
|
|
543
|
+
const key = _getKey(oldChildren[i]);
|
|
544
|
+
if (key != null) oldKeyMap.set(key, i);
|
|
545
|
+
}
|
|
546
|
+
for (let i = 0; i < newLen; i++) {
|
|
547
|
+
const key = _getKey(newChildren[i]);
|
|
548
|
+
if (key != null) newKeyMap.set(key, i);
|
|
549
|
+
}
|
|
550
|
+
_morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap);
|
|
551
|
+
} else {
|
|
552
|
+
_morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
|
|
457
553
|
}
|
|
554
|
+
}
|
|
458
555
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Unkeyed reconciliation — positional matching.
|
|
558
|
+
*/
|
|
559
|
+
function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
560
|
+
const oldLen = oldChildren.length;
|
|
561
|
+
const newLen = newChildren.length;
|
|
562
|
+
const minLen = oldLen < newLen ? oldLen : newLen;
|
|
563
|
+
|
|
564
|
+
// Morph overlapping range
|
|
565
|
+
for (let i = 0; i < minLen; i++) {
|
|
566
|
+
_morphNode(oldParent, oldChildren[i], newChildren[i]);
|
|
470
567
|
}
|
|
471
568
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (!selector || sib.matches(selector)) result.push(sib);
|
|
478
|
-
sib = sib.previousElementSibling;
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
return new ZQueryCollection(result);
|
|
569
|
+
// Append new nodes
|
|
570
|
+
if (newLen > oldLen) {
|
|
571
|
+
for (let i = oldLen; i < newLen; i++) {
|
|
572
|
+
oldParent.appendChild(newChildren[i].cloneNode(true));
|
|
573
|
+
}
|
|
482
574
|
}
|
|
483
575
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (selector && sib.matches(selector)) break;
|
|
490
|
-
if (!filter || sib.matches(filter)) result.push(sib);
|
|
491
|
-
sib = sib.previousElementSibling;
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
return new ZQueryCollection(result);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
parents(selector) {
|
|
498
|
-
const result = [];
|
|
499
|
-
this.elements.forEach(el => {
|
|
500
|
-
let parent = el.parentElement;
|
|
501
|
-
while (parent) {
|
|
502
|
-
if (!selector || parent.matches(selector)) result.push(parent);
|
|
503
|
-
parent = parent.parentElement;
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
return new ZQueryCollection([...new Set(result)]);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
parentsUntil(selector, filter) {
|
|
510
|
-
const result = [];
|
|
511
|
-
this.elements.forEach(el => {
|
|
512
|
-
let parent = el.parentElement;
|
|
513
|
-
while (parent) {
|
|
514
|
-
if (selector && parent.matches(selector)) break;
|
|
515
|
-
if (!filter || parent.matches(filter)) result.push(parent);
|
|
516
|
-
parent = parent.parentElement;
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
return new ZQueryCollection([...new Set(result)]);
|
|
576
|
+
// Remove excess old nodes (iterate backwards to avoid index shifting)
|
|
577
|
+
if (oldLen > newLen) {
|
|
578
|
+
for (let i = oldLen - 1; i >= newLen; i--) {
|
|
579
|
+
oldParent.removeChild(oldChildren[i]);
|
|
580
|
+
}
|
|
520
581
|
}
|
|
582
|
+
}
|
|
521
583
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Keyed reconciliation — match by z-key, reorder with minimal moves
|
|
586
|
+
* using Longest Increasing Subsequence (LIS) to find the maximum set
|
|
587
|
+
* of nodes that are already in the correct relative order, then only
|
|
588
|
+
* move the remaining nodes.
|
|
589
|
+
*/
|
|
590
|
+
function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap) {
|
|
591
|
+
const consumed = new Set();
|
|
592
|
+
const newLen = newChildren.length;
|
|
593
|
+
const matched = new Array(newLen);
|
|
527
594
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
595
|
+
// Step 1: Match new children to old children by key
|
|
596
|
+
for (let i = 0; i < newLen; i++) {
|
|
597
|
+
const key = _getKey(newChildren[i]);
|
|
598
|
+
if (key != null && oldKeyMap.has(key)) {
|
|
599
|
+
const oldIdx = oldKeyMap.get(key);
|
|
600
|
+
matched[i] = oldChildren[oldIdx];
|
|
601
|
+
consumed.add(oldIdx);
|
|
602
|
+
} else {
|
|
603
|
+
matched[i] = null;
|
|
531
604
|
}
|
|
532
|
-
return new ZQueryCollection(this.elements.filter(el => el.matches(selector)));
|
|
533
605
|
}
|
|
534
606
|
|
|
535
|
-
not
|
|
536
|
-
|
|
537
|
-
|
|
607
|
+
// Step 2: Remove old keyed nodes not in the new tree
|
|
608
|
+
for (let i = oldChildren.length - 1; i >= 0; i--) {
|
|
609
|
+
if (!consumed.has(i)) {
|
|
610
|
+
const key = _getKey(oldChildren[i]);
|
|
611
|
+
if (key != null && !newKeyMap.has(key)) {
|
|
612
|
+
oldParent.removeChild(oldChildren[i]);
|
|
613
|
+
}
|
|
538
614
|
}
|
|
539
|
-
return new ZQueryCollection(this.elements.filter(el => !el.matches(selector)));
|
|
540
615
|
}
|
|
541
616
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
617
|
+
// Step 3: Build index array for LIS of matched old indices.
|
|
618
|
+
// This finds the largest set of keyed nodes already in order,
|
|
619
|
+
// so we only need to move the rest — O(n log n) instead of O(n²).
|
|
620
|
+
const oldIndices = []; // Maps new-position → old-position (or -1)
|
|
621
|
+
for (let i = 0; i < newLen; i++) {
|
|
622
|
+
if (matched[i]) {
|
|
623
|
+
const key = _getKey(newChildren[i]);
|
|
624
|
+
oldIndices.push(oldKeyMap.get(key));
|
|
625
|
+
} else {
|
|
626
|
+
oldIndices.push(-1);
|
|
549
627
|
}
|
|
550
|
-
return this.elements.some(el => el.matches(selector));
|
|
551
628
|
}
|
|
552
629
|
|
|
553
|
-
|
|
554
|
-
return new ZQueryCollection(this.elements.slice(start, end));
|
|
555
|
-
}
|
|
630
|
+
const lisSet = _lis(oldIndices);
|
|
556
631
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
632
|
+
// Step 4: Insert / reorder / morph — walk new children forward,
|
|
633
|
+
// using LIS to decide which nodes stay in place.
|
|
634
|
+
let cursor = oldParent.firstChild;
|
|
635
|
+
const unkeyedOld = [];
|
|
636
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
637
|
+
if (!consumed.has(i) && _getKey(oldChildren[i]) == null) {
|
|
638
|
+
unkeyedOld.push(oldChildren[i]);
|
|
639
|
+
}
|
|
564
640
|
}
|
|
641
|
+
let unkeyedIdx = 0;
|
|
565
642
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
643
|
+
for (let i = 0; i < newLen; i++) {
|
|
644
|
+
const newNode = newChildren[i];
|
|
645
|
+
const newKey = _getKey(newNode);
|
|
646
|
+
let oldNode = matched[i];
|
|
570
647
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const el = this.first();
|
|
574
|
-
if (!el || !el.parentElement) return -1;
|
|
575
|
-
return Array.from(el.parentElement.children).indexOf(el);
|
|
648
|
+
if (!oldNode && newKey == null) {
|
|
649
|
+
oldNode = unkeyedOld[unkeyedIdx++] || null;
|
|
576
650
|
}
|
|
577
|
-
const target = (typeof selector === 'string')
|
|
578
|
-
? document.querySelector(selector)
|
|
579
|
-
: selector;
|
|
580
|
-
return this.elements.indexOf(target);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// --- Classes -------------------------------------------------------------
|
|
584
651
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
652
|
+
if (oldNode) {
|
|
653
|
+
// If this node is NOT part of the LIS, it needs to be moved
|
|
654
|
+
if (!lisSet.has(i)) {
|
|
655
|
+
oldParent.insertBefore(oldNode, cursor);
|
|
656
|
+
}
|
|
657
|
+
// Capture next sibling BEFORE _morphNode — if _morphNode calls
|
|
658
|
+
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
659
|
+
const nextSib = oldNode.nextSibling;
|
|
660
|
+
_morphNode(oldParent, oldNode, newNode);
|
|
661
|
+
cursor = nextSib;
|
|
662
|
+
} else {
|
|
663
|
+
// Insert new node
|
|
664
|
+
const clone = newNode.cloneNode(true);
|
|
665
|
+
if (cursor) {
|
|
666
|
+
oldParent.insertBefore(clone, cursor);
|
|
667
|
+
} else {
|
|
668
|
+
oldParent.appendChild(clone);
|
|
669
|
+
}
|
|
591
670
|
}
|
|
592
|
-
const classes = names.flatMap(n => n.split(/\s+/));
|
|
593
|
-
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(...classes);
|
|
594
|
-
return this;
|
|
595
671
|
}
|
|
596
672
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
673
|
+
// Remove remaining unkeyed old nodes
|
|
674
|
+
while (unkeyedIdx < unkeyedOld.length) {
|
|
675
|
+
const leftover = unkeyedOld[unkeyedIdx++];
|
|
676
|
+
if (leftover.parentNode === oldParent) {
|
|
677
|
+
oldParent.removeChild(leftover);
|
|
602
678
|
}
|
|
603
|
-
const classes = names.flatMap(n => n.split(/\s+/));
|
|
604
|
-
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(...classes);
|
|
605
|
-
return this;
|
|
606
679
|
}
|
|
607
680
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
force !== undefined ? this.elements[i].classList.toggle(c, force) : this.elements[i].classList.toggle(c);
|
|
615
|
-
}
|
|
616
|
-
return this;
|
|
617
|
-
}
|
|
618
|
-
const classes = args.flatMap(n => n.split(/\s+/));
|
|
619
|
-
for (let i = 0; i < this.elements.length; i++) {
|
|
620
|
-
const el = this.elements[i];
|
|
621
|
-
for (let j = 0; j < classes.length; j++) {
|
|
622
|
-
force !== undefined ? el.classList.toggle(classes[j], force) : el.classList.toggle(classes[j]);
|
|
681
|
+
// Remove any remaining keyed old nodes that weren't consumed
|
|
682
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
683
|
+
if (!consumed.has(i)) {
|
|
684
|
+
const node = oldChildren[i];
|
|
685
|
+
if (node.parentNode === oldParent && _getKey(node) != null && !newKeyMap.has(_getKey(node))) {
|
|
686
|
+
oldParent.removeChild(node);
|
|
623
687
|
}
|
|
624
688
|
}
|
|
625
|
-
return this;
|
|
626
689
|
}
|
|
690
|
+
}
|
|
627
691
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
692
|
+
/**
|
|
693
|
+
* Compute the Longest Increasing Subsequence of an index array.
|
|
694
|
+
* Returns a Set of positions (in the input) that form the LIS.
|
|
695
|
+
* Entries with value -1 (unmatched) are excluded.
|
|
696
|
+
*
|
|
697
|
+
* O(n log n) — same algorithm used by Vue 3 and ivi.
|
|
698
|
+
*
|
|
699
|
+
* @param {number[]} arr — array of old-tree indices (-1 = unmatched)
|
|
700
|
+
* @returns {Set<number>} — positions in arr belonging to the LIS
|
|
701
|
+
*/
|
|
702
|
+
function _lis(arr) {
|
|
703
|
+
const len = arr.length;
|
|
704
|
+
const result = new Set();
|
|
705
|
+
if (len === 0) return result;
|
|
631
706
|
|
|
632
|
-
//
|
|
707
|
+
// tails[i] = index in arr of the smallest tail element for LIS of length i+1
|
|
708
|
+
const tails = [];
|
|
709
|
+
// prev[i] = predecessor index in arr for the LIS ending at arr[i]
|
|
710
|
+
const prev = new Array(len).fill(-1);
|
|
711
|
+
const tailIndices = []; // parallel to tails: actual positions
|
|
633
712
|
|
|
634
|
-
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
713
|
+
for (let i = 0; i < len; i++) {
|
|
714
|
+
if (arr[i] === -1) continue;
|
|
715
|
+
const val = arr[i];
|
|
716
|
+
|
|
717
|
+
// Binary search for insertion point in tails
|
|
718
|
+
let lo = 0, hi = tails.length;
|
|
719
|
+
while (lo < hi) {
|
|
720
|
+
const mid = (lo + hi) >> 1;
|
|
721
|
+
if (tails[mid] < val) lo = mid + 1;
|
|
722
|
+
else hi = mid;
|
|
639
723
|
}
|
|
640
|
-
|
|
641
|
-
|
|
724
|
+
|
|
725
|
+
tails[lo] = val;
|
|
726
|
+
tailIndices[lo] = i;
|
|
727
|
+
prev[i] = lo > 0 ? tailIndices[lo - 1] : -1;
|
|
642
728
|
}
|
|
643
729
|
|
|
644
|
-
|
|
645
|
-
|
|
730
|
+
// Reconstruct: walk backwards from the last element of LIS
|
|
731
|
+
let k = tailIndices[tails.length - 1];
|
|
732
|
+
for (let i = tails.length - 1; i >= 0; i--) {
|
|
733
|
+
result.add(k);
|
|
734
|
+
k = prev[k];
|
|
646
735
|
}
|
|
647
736
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return this.each((_, el) => { el[name] = value; });
|
|
651
|
-
}
|
|
737
|
+
return result;
|
|
738
|
+
}
|
|
652
739
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
740
|
+
/**
|
|
741
|
+
* Morph a single node in place.
|
|
742
|
+
*/
|
|
743
|
+
function _morphNode(parent, oldNode, newNode) {
|
|
744
|
+
// Text / comment nodes — just update content
|
|
745
|
+
if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
|
|
746
|
+
if (newNode.nodeType === oldNode.nodeType) {
|
|
747
|
+
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
748
|
+
oldNode.nodeValue = newNode.nodeValue;
|
|
749
|
+
}
|
|
750
|
+
return;
|
|
658
751
|
}
|
|
659
|
-
|
|
752
|
+
// Different node types — replace
|
|
753
|
+
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
754
|
+
return;
|
|
660
755
|
}
|
|
661
756
|
|
|
662
|
-
//
|
|
757
|
+
// Different node types or tag names — replace entirely
|
|
758
|
+
if (oldNode.nodeType !== newNode.nodeType ||
|
|
759
|
+
oldNode.nodeName !== newNode.nodeName) {
|
|
760
|
+
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
663
763
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
764
|
+
// Both are elements — diff attributes then recurse children
|
|
765
|
+
if (oldNode.nodeType === 1) {
|
|
766
|
+
// z-skip: developer opt-out — skip diffing this subtree entirely.
|
|
767
|
+
// Useful for third-party widgets, canvas, video, or large static content.
|
|
768
|
+
if (oldNode.hasAttribute('z-skip')) return;
|
|
769
|
+
|
|
770
|
+
// Fast bail-out: if the elements are identical, skip everything.
|
|
771
|
+
// isEqualNode() is a native C++ comparison — much faster than walking
|
|
772
|
+
// attributes + children in JS when trees haven't changed.
|
|
773
|
+
if (oldNode.isEqualNode(newNode)) return;
|
|
774
|
+
|
|
775
|
+
_morphAttributes(oldNode, newNode);
|
|
776
|
+
|
|
777
|
+
// Special elements: don't recurse into their children
|
|
778
|
+
const tag = oldNode.nodeName;
|
|
779
|
+
if (tag === 'INPUT') {
|
|
780
|
+
_syncInputValue(oldNode, newNode);
|
|
781
|
+
return;
|
|
667
782
|
}
|
|
668
|
-
if (
|
|
669
|
-
|
|
670
|
-
|
|
783
|
+
if (tag === 'TEXTAREA') {
|
|
784
|
+
if (oldNode.value !== newNode.textContent) {
|
|
785
|
+
oldNode.value = newNode.textContent || '';
|
|
786
|
+
}
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (tag === 'SELECT') {
|
|
790
|
+
_morphChildren(oldNode, newNode);
|
|
791
|
+
if (oldNode.value !== newNode.value) {
|
|
792
|
+
oldNode.value = newNode.value;
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
671
795
|
}
|
|
672
|
-
return this.each((_, el) => Object.assign(el.style, props));
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
width() { return this.first()?.getBoundingClientRect().width; }
|
|
676
|
-
height() { return this.first()?.getBoundingClientRect().height; }
|
|
677
796
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null;
|
|
797
|
+
// Generic element — recurse children
|
|
798
|
+
_morphChildren(oldNode, newNode);
|
|
681
799
|
}
|
|
800
|
+
}
|
|
682
801
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
802
|
+
/**
|
|
803
|
+
* Sync attributes from newEl onto oldEl.
|
|
804
|
+
* Uses a single pass: build a set of new attribute names, iterate
|
|
805
|
+
* old attrs for removals, then apply new attrs.
|
|
806
|
+
*/
|
|
807
|
+
function _morphAttributes(oldEl, newEl) {
|
|
808
|
+
const newAttrs = newEl.attributes;
|
|
809
|
+
const oldAttrs = oldEl.attributes;
|
|
810
|
+
const newLen = newAttrs.length;
|
|
811
|
+
const oldLen = oldAttrs.length;
|
|
687
812
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
813
|
+
// Fast path: if both have same number of attributes, check if they're identical
|
|
814
|
+
if (newLen === oldLen) {
|
|
815
|
+
let same = true;
|
|
816
|
+
for (let i = 0; i < newLen; i++) {
|
|
817
|
+
const na = newAttrs[i];
|
|
818
|
+
if (oldEl.getAttribute(na.name) !== na.value) { same = false; break; }
|
|
692
819
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
820
|
+
if (same) {
|
|
821
|
+
// Also verify no extra old attrs (names mismatch)
|
|
822
|
+
for (let i = 0; i < oldLen; i++) {
|
|
823
|
+
if (!newEl.hasAttribute(oldAttrs[i].name)) { same = false; break; }
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (same) return;
|
|
697
827
|
}
|
|
698
828
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
829
|
+
// Build set of new attr names for O(1) lookup during removal pass
|
|
830
|
+
const newNames = new Set();
|
|
831
|
+
for (let i = 0; i < newLen; i++) {
|
|
832
|
+
const attr = newAttrs[i];
|
|
833
|
+
newNames.add(attr.name);
|
|
834
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
835
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
703
836
|
}
|
|
704
|
-
return this.each((_, el) => {
|
|
705
|
-
if (el === window) window.scrollTo(value, window.scrollY);
|
|
706
|
-
else el.scrollLeft = value;
|
|
707
|
-
});
|
|
708
837
|
}
|
|
709
838
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
839
|
+
// Remove stale attributes — snapshot names first because oldAttrs
|
|
840
|
+
// is a live NamedNodeMap that mutates on removeAttribute().
|
|
841
|
+
const oldNames = new Array(oldLen);
|
|
842
|
+
for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
|
|
843
|
+
for (let i = oldNames.length - 1; i >= 0; i--) {
|
|
844
|
+
if (!newNames.has(oldNames[i])) {
|
|
845
|
+
oldEl.removeAttribute(oldNames[i]);
|
|
846
|
+
}
|
|
713
847
|
}
|
|
848
|
+
}
|
|
714
849
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Sync input element value, checked, disabled states.
|
|
852
|
+
*
|
|
853
|
+
* Only updates the value when the new HTML explicitly carries a `value`
|
|
854
|
+
* attribute. Templates that use z-model manage values through reactive
|
|
855
|
+
* state + _bindModels — the morph engine should not interfere by wiping
|
|
856
|
+
* a live input's content to '' just because the template has no `value`
|
|
857
|
+
* attr. This prevents the wipe-then-restore cycle that resets cursor
|
|
858
|
+
* position on every keystroke.
|
|
859
|
+
*/
|
|
860
|
+
function _syncInputValue(oldEl, newEl) {
|
|
861
|
+
const type = (oldEl.type || '').toLowerCase();
|
|
719
862
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (
|
|
725
|
-
|
|
726
|
-
w += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
|
|
863
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
864
|
+
if (oldEl.checked !== newEl.checked) oldEl.checked = newEl.checked;
|
|
865
|
+
} else {
|
|
866
|
+
const newVal = newEl.getAttribute('value');
|
|
867
|
+
if (newVal !== null && oldEl.value !== newVal) {
|
|
868
|
+
oldEl.value = newVal;
|
|
727
869
|
}
|
|
728
|
-
return w;
|
|
729
870
|
}
|
|
730
871
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
let h = el.offsetHeight;
|
|
735
|
-
if (includeMargin) {
|
|
736
|
-
const style = getComputedStyle(el);
|
|
737
|
-
h += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
|
|
738
|
-
}
|
|
739
|
-
return h;
|
|
740
|
-
}
|
|
872
|
+
// Sync disabled
|
|
873
|
+
if (oldEl.disabled !== newEl.disabled) oldEl.disabled = newEl.disabled;
|
|
874
|
+
}
|
|
741
875
|
|
|
742
|
-
|
|
876
|
+
/**
|
|
877
|
+
* Get the reconciliation key from a node.
|
|
878
|
+
*
|
|
879
|
+
* Priority: z-key attribute → id attribute → data-id / data-key.
|
|
880
|
+
* Auto-detected keys use a `\0` prefix to avoid collisions with
|
|
881
|
+
* explicit z-key values.
|
|
882
|
+
*
|
|
883
|
+
* This means the LIS-optimised keyed path activates automatically
|
|
884
|
+
* whenever elements carry `id` or `data-id` / `data-key` attributes
|
|
885
|
+
* — no extra markup required.
|
|
886
|
+
*
|
|
887
|
+
* @returns {string|null}
|
|
888
|
+
*/
|
|
889
|
+
function _getKey(node) {
|
|
890
|
+
if (node.nodeType !== 1) return null;
|
|
743
891
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
// to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
|
|
748
|
-
// Empty elements get raw innerHTML for fast first-paint — same strategy
|
|
749
|
-
// the component system uses (first render = innerHTML, updates = morph).
|
|
750
|
-
return this.each((_, el) => {
|
|
751
|
-
if (el.childNodes.length > 0) {
|
|
752
|
-
_morph(el, content);
|
|
753
|
-
} else {
|
|
754
|
-
el.innerHTML = content;
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
}
|
|
892
|
+
// Explicit z-key — highest priority
|
|
893
|
+
const zk = node.getAttribute('z-key');
|
|
894
|
+
if (zk) return zk;
|
|
758
895
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
896
|
+
// Auto-key: id attribute (unique by spec)
|
|
897
|
+
if (node.id) return '\0id:' + node.id;
|
|
762
898
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
899
|
+
// Auto-key: data-id or data-key attributes
|
|
900
|
+
const ds = node.dataset;
|
|
901
|
+
if (ds) {
|
|
902
|
+
if (ds.id) return '\0data-id:' + ds.id;
|
|
903
|
+
if (ds.key) return '\0data-key:' + ds.key;
|
|
766
904
|
}
|
|
767
905
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// --- src/core.js -------------------------------------------------
|
|
910
|
+
/**
|
|
911
|
+
* zQuery Core — Selector engine & chainable DOM collection
|
|
912
|
+
*
|
|
913
|
+
* Extends the quick-ref pattern (Id, Class, Classes, Children)
|
|
914
|
+
* into a full jQuery-like chainable wrapper with modern APIs.
|
|
915
|
+
*/
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
919
|
+
// ZQueryCollection — wraps an array of elements with chainable methods
|
|
920
|
+
// ---------------------------------------------------------------------------
|
|
921
|
+
class ZQueryCollection {
|
|
922
|
+
constructor(elements) {
|
|
923
|
+
this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
|
|
924
|
+
this.length = this.elements.length;
|
|
925
|
+
this.elements.forEach((el, i) => { this[i] = el; });
|
|
771
926
|
}
|
|
772
927
|
|
|
773
|
-
// ---
|
|
928
|
+
// --- Iteration -----------------------------------------------------------
|
|
774
929
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c));
|
|
779
|
-
else if (content instanceof Node) el.appendChild(content);
|
|
780
|
-
});
|
|
930
|
+
each(fn) {
|
|
931
|
+
this.elements.forEach((el, i) => fn.call(el, i, el));
|
|
932
|
+
return this;
|
|
781
933
|
}
|
|
782
934
|
|
|
783
|
-
|
|
784
|
-
return this.
|
|
785
|
-
if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content);
|
|
786
|
-
else if (content instanceof Node) el.insertBefore(content, el.firstChild);
|
|
787
|
-
});
|
|
935
|
+
map(fn) {
|
|
936
|
+
return this.elements.map((el, i) => fn.call(el, i, el));
|
|
788
937
|
}
|
|
789
938
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling);
|
|
794
|
-
});
|
|
939
|
+
forEach(fn) {
|
|
940
|
+
this.elements.forEach((el, i) => fn(el, i, this.elements));
|
|
941
|
+
return this;
|
|
795
942
|
}
|
|
796
943
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
});
|
|
802
|
-
}
|
|
944
|
+
first() { return this.elements[0] || null; }
|
|
945
|
+
last() { return this.elements[this.length - 1] || null; }
|
|
946
|
+
eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
|
|
947
|
+
toArray(){ return [...this.elements]; }
|
|
803
948
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
949
|
+
[Symbol.iterator]() { return this.elements[Symbol.iterator](); }
|
|
950
|
+
|
|
951
|
+
// --- Traversal -----------------------------------------------------------
|
|
952
|
+
|
|
953
|
+
find(selector) {
|
|
954
|
+
const found = [];
|
|
955
|
+
this.elements.forEach(el => found.push(...el.querySelectorAll(selector)));
|
|
956
|
+
return new ZQueryCollection(found);
|
|
811
957
|
}
|
|
812
958
|
|
|
813
|
-
|
|
814
|
-
|
|
959
|
+
parent() {
|
|
960
|
+
const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))];
|
|
961
|
+
return new ZQueryCollection(parents);
|
|
815
962
|
}
|
|
816
963
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
964
|
+
closest(selector) {
|
|
965
|
+
return new ZQueryCollection(
|
|
966
|
+
this.elements.map(el => el.closest(selector)).filter(Boolean)
|
|
967
|
+
);
|
|
820
968
|
}
|
|
821
969
|
|
|
822
|
-
|
|
823
|
-
|
|
970
|
+
children(selector) {
|
|
971
|
+
const kids = [];
|
|
972
|
+
this.elements.forEach(el => {
|
|
973
|
+
kids.push(...(selector
|
|
974
|
+
? el.querySelectorAll(`:scope > ${selector}`)
|
|
975
|
+
: el.children));
|
|
976
|
+
});
|
|
977
|
+
return new ZQueryCollection([...kids]);
|
|
824
978
|
}
|
|
825
979
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
} else if (content instanceof Node) {
|
|
833
|
-
el.parentNode.replaceChild(content, el);
|
|
834
|
-
}
|
|
980
|
+
siblings(selector) {
|
|
981
|
+
const sibs = [];
|
|
982
|
+
this.elements.forEach(el => {
|
|
983
|
+
if (!el.parentElement) return;
|
|
984
|
+
const all = [...el.parentElement.children].filter(c => c !== el);
|
|
985
|
+
sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
|
|
835
986
|
});
|
|
987
|
+
return new ZQueryCollection(sibs);
|
|
836
988
|
}
|
|
837
989
|
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
return this;
|
|
990
|
+
next(selector) {
|
|
991
|
+
const els = this.elements.map(el => el.nextElementSibling).filter(Boolean);
|
|
992
|
+
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
842
993
|
}
|
|
843
994
|
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
return this;
|
|
995
|
+
prev(selector) {
|
|
996
|
+
const els = this.elements.map(el => el.previousElementSibling).filter(Boolean);
|
|
997
|
+
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
848
998
|
}
|
|
849
999
|
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
1000
|
+
nextAll(selector) {
|
|
1001
|
+
const result = [];
|
|
1002
|
+
this.elements.forEach(el => {
|
|
1003
|
+
let sib = el.nextElementSibling;
|
|
1004
|
+
while (sib) {
|
|
1005
|
+
if (!selector || sib.matches(selector)) result.push(sib);
|
|
1006
|
+
sib = sib.nextElementSibling;
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
return new ZQueryCollection(result);
|
|
854
1010
|
}
|
|
855
1011
|
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
1012
|
+
nextUntil(selector, filter) {
|
|
1013
|
+
const result = [];
|
|
1014
|
+
this.elements.forEach(el => {
|
|
1015
|
+
let sib = el.nextElementSibling;
|
|
1016
|
+
while (sib) {
|
|
1017
|
+
if (selector && sib.matches(selector)) break;
|
|
1018
|
+
if (!filter || sib.matches(filter)) result.push(sib);
|
|
1019
|
+
sib = sib.nextElementSibling;
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
return new ZQueryCollection(result);
|
|
860
1023
|
}
|
|
861
1024
|
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1025
|
+
prevAll(selector) {
|
|
1026
|
+
const result = [];
|
|
1027
|
+
this.elements.forEach(el => {
|
|
1028
|
+
let sib = el.previousElementSibling;
|
|
1029
|
+
while (sib) {
|
|
1030
|
+
if (!selector || sib.matches(selector)) result.push(sib);
|
|
1031
|
+
sib = sib.previousElementSibling;
|
|
1032
|
+
}
|
|
870
1033
|
});
|
|
871
|
-
return
|
|
1034
|
+
return new ZQueryCollection(result);
|
|
872
1035
|
}
|
|
873
1036
|
|
|
874
|
-
|
|
1037
|
+
prevUntil(selector, filter) {
|
|
1038
|
+
const result = [];
|
|
875
1039
|
this.elements.forEach(el => {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1040
|
+
let sib = el.previousElementSibling;
|
|
1041
|
+
while (sib) {
|
|
1042
|
+
if (selector && sib.matches(selector)) break;
|
|
1043
|
+
if (!filter || sib.matches(filter)) result.push(sib);
|
|
1044
|
+
sib = sib.previousElementSibling;
|
|
1045
|
+
}
|
|
880
1046
|
});
|
|
881
|
-
return
|
|
1047
|
+
return new ZQueryCollection(result);
|
|
882
1048
|
}
|
|
883
1049
|
|
|
884
|
-
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1050
|
+
parents(selector) {
|
|
1051
|
+
const result = [];
|
|
1052
|
+
this.elements.forEach(el => {
|
|
1053
|
+
let parent = el.parentElement;
|
|
1054
|
+
while (parent) {
|
|
1055
|
+
if (!selector || parent.matches(selector)) result.push(parent);
|
|
1056
|
+
parent = parent.parentElement;
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
return new ZQueryCollection([...new Set(result)]);
|
|
891
1060
|
}
|
|
892
1061
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1062
|
+
parentsUntil(selector, filter) {
|
|
1063
|
+
const result = [];
|
|
1064
|
+
this.elements.forEach(el => {
|
|
1065
|
+
let parent = el.parentElement;
|
|
1066
|
+
while (parent) {
|
|
1067
|
+
if (selector && parent.matches(selector)) break;
|
|
1068
|
+
if (!filter || parent.matches(filter)) result.push(parent);
|
|
1069
|
+
parent = parent.parentElement;
|
|
1070
|
+
}
|
|
898
1071
|
});
|
|
1072
|
+
return new ZQueryCollection([...new Set(result)]);
|
|
899
1073
|
}
|
|
900
1074
|
|
|
901
|
-
|
|
902
|
-
|
|
1075
|
+
contents() {
|
|
1076
|
+
const result = [];
|
|
1077
|
+
this.elements.forEach(el => result.push(...el.childNodes));
|
|
1078
|
+
return new ZQueryCollection(result);
|
|
903
1079
|
}
|
|
904
1080
|
|
|
905
|
-
|
|
1081
|
+
filter(selector) {
|
|
1082
|
+
if (typeof selector === 'function') {
|
|
1083
|
+
return new ZQueryCollection(this.elements.filter(selector));
|
|
1084
|
+
}
|
|
1085
|
+
return new ZQueryCollection(this.elements.filter(el => el.matches(selector)));
|
|
1086
|
+
}
|
|
906
1087
|
|
|
907
|
-
|
|
908
|
-
|
|
1088
|
+
not(selector) {
|
|
1089
|
+
if (typeof selector === 'function') {
|
|
1090
|
+
return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el)));
|
|
1091
|
+
}
|
|
1092
|
+
return new ZQueryCollection(this.elements.filter(el => !el.matches(selector)));
|
|
909
1093
|
}
|
|
910
1094
|
|
|
911
|
-
|
|
912
|
-
return this.
|
|
1095
|
+
has(selector) {
|
|
1096
|
+
return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
|
|
913
1097
|
}
|
|
914
1098
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
});
|
|
1099
|
+
is(selector) {
|
|
1100
|
+
if (typeof selector === 'function') {
|
|
1101
|
+
return this.elements.some((el, i) => selector.call(el, i, el));
|
|
1102
|
+
}
|
|
1103
|
+
return this.elements.some(el => el.matches(selector));
|
|
921
1104
|
}
|
|
922
1105
|
|
|
923
|
-
|
|
1106
|
+
slice(start, end) {
|
|
1107
|
+
return new ZQueryCollection(this.elements.slice(start, end));
|
|
1108
|
+
}
|
|
924
1109
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
} else if (typeof selectorOrHandler === 'string') {
|
|
933
|
-
// Delegated event — store wrapper so off() can remove it
|
|
934
|
-
const wrapper = (e) => {
|
|
935
|
-
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
936
|
-
const target = e.target.closest(selectorOrHandler);
|
|
937
|
-
if (target && el.contains(target)) handler.call(target, e);
|
|
938
|
-
};
|
|
939
|
-
wrapper._zqOriginal = handler;
|
|
940
|
-
wrapper._zqSelector = selectorOrHandler;
|
|
941
|
-
el.addEventListener(evt, wrapper);
|
|
942
|
-
// Track delegated handlers for removal
|
|
943
|
-
if (!el._zqDelegated) el._zqDelegated = [];
|
|
944
|
-
el._zqDelegated.push({ evt, wrapper });
|
|
945
|
-
}
|
|
946
|
-
});
|
|
947
|
-
});
|
|
1110
|
+
add(selector, context) {
|
|
1111
|
+
const toAdd = (selector instanceof ZQueryCollection)
|
|
1112
|
+
? selector.elements
|
|
1113
|
+
: (selector instanceof Node)
|
|
1114
|
+
? [selector]
|
|
1115
|
+
: Array.from((context || document).querySelectorAll(selector));
|
|
1116
|
+
return new ZQueryCollection([...this.elements, ...toAdd]);
|
|
948
1117
|
}
|
|
949
1118
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
return this.
|
|
953
|
-
events.forEach(evt => {
|
|
954
|
-
// Try direct removal first
|
|
955
|
-
el.removeEventListener(evt, handler);
|
|
956
|
-
// Also check delegated handlers
|
|
957
|
-
if (el._zqDelegated) {
|
|
958
|
-
el._zqDelegated = el._zqDelegated.filter(d => {
|
|
959
|
-
if (d.evt === evt && d.wrapper._zqOriginal === handler) {
|
|
960
|
-
el.removeEventListener(evt, d.wrapper);
|
|
961
|
-
return false;
|
|
962
|
-
}
|
|
963
|
-
return true;
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
});
|
|
1119
|
+
get(index) {
|
|
1120
|
+
if (index === undefined) return [...this.elements];
|
|
1121
|
+
return index < 0 ? this.elements[this.length + index] : this.elements[index];
|
|
968
1122
|
}
|
|
969
1123
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
el.
|
|
973
|
-
|
|
1124
|
+
index(selector) {
|
|
1125
|
+
if (selector === undefined) {
|
|
1126
|
+
const el = this.first();
|
|
1127
|
+
if (!el || !el.parentElement) return -1;
|
|
1128
|
+
return Array.from(el.parentElement.children).indexOf(el);
|
|
1129
|
+
}
|
|
1130
|
+
const target = (typeof selector === 'string')
|
|
1131
|
+
? document.querySelector(selector)
|
|
1132
|
+
: selector;
|
|
1133
|
+
return this.elements.indexOf(target);
|
|
974
1134
|
}
|
|
975
1135
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1136
|
+
// --- Classes -------------------------------------------------------------
|
|
1137
|
+
|
|
1138
|
+
addClass(...names) {
|
|
1139
|
+
// Fast path: single class, no spaces — avoids flatMap + regex split allocation
|
|
1140
|
+
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
1141
|
+
const c = names[0];
|
|
1142
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(c);
|
|
1143
|
+
return this;
|
|
1144
|
+
}
|
|
1145
|
+
const classes = names.flatMap(n => n.split(/\s+/));
|
|
1146
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(...classes);
|
|
1147
|
+
return this;
|
|
980
1148
|
}
|
|
981
1149
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1150
|
+
removeClass(...names) {
|
|
1151
|
+
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
1152
|
+
const c = names[0];
|
|
1153
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(c);
|
|
1154
|
+
return this;
|
|
1155
|
+
}
|
|
1156
|
+
const classes = names.flatMap(n => n.split(/\s+/));
|
|
1157
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(...classes);
|
|
1158
|
+
return this;
|
|
990
1159
|
}
|
|
991
1160
|
|
|
992
|
-
|
|
1161
|
+
toggleClass(...args) {
|
|
1162
|
+
const force = typeof args[args.length - 1] === 'boolean' ? args.pop() : undefined;
|
|
1163
|
+
// Fast path: single class, no spaces
|
|
1164
|
+
if (args.length === 1 && args[0].indexOf(' ') === -1) {
|
|
1165
|
+
const c = args[0];
|
|
1166
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
1167
|
+
force !== undefined ? this.elements[i].classList.toggle(c, force) : this.elements[i].classList.toggle(c);
|
|
1168
|
+
}
|
|
1169
|
+
return this;
|
|
1170
|
+
}
|
|
1171
|
+
const classes = args.flatMap(n => n.split(/\s+/));
|
|
1172
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
1173
|
+
const el = this.elements[i];
|
|
1174
|
+
for (let j = 0; j < classes.length; j++) {
|
|
1175
|
+
force !== undefined ? el.classList.toggle(classes[j], force) : el.classList.toggle(classes[j]);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return this;
|
|
1179
|
+
}
|
|
993
1180
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
Object.assign(el.style, props);
|
|
1005
|
-
const onEnd = () => {
|
|
1006
|
-
el.removeEventListener('transitionend', onEnd);
|
|
1007
|
-
el.style.transition = '';
|
|
1008
|
-
if (!resolved && ++count.done >= this.length) {
|
|
1009
|
-
resolved = true;
|
|
1010
|
-
resolve(this);
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
|
-
el.addEventListener('transitionend', onEnd);
|
|
1014
|
-
listeners.push({ el, onEnd });
|
|
1015
|
-
});
|
|
1181
|
+
hasClass(name) {
|
|
1182
|
+
return this.first()?.classList.contains(name) || false;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// --- Attributes ----------------------------------------------------------
|
|
1186
|
+
|
|
1187
|
+
attr(name, value) {
|
|
1188
|
+
if (typeof name === 'object' && name !== null) {
|
|
1189
|
+
return this.each((_, el) => {
|
|
1190
|
+
for (const [k, v] of Object.entries(name)) el.setAttribute(k, v);
|
|
1016
1191
|
});
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
resolved = true;
|
|
1021
|
-
// Clean up any remaining transitionend listeners
|
|
1022
|
-
for (const { el, onEnd } of listeners) {
|
|
1023
|
-
el.removeEventListener('transitionend', onEnd);
|
|
1024
|
-
el.style.transition = '';
|
|
1025
|
-
}
|
|
1026
|
-
resolve(this);
|
|
1027
|
-
}
|
|
1028
|
-
}, duration + 50);
|
|
1029
|
-
});
|
|
1192
|
+
}
|
|
1193
|
+
if (value === undefined) return this.first()?.getAttribute(name);
|
|
1194
|
+
return this.each((_, el) => el.setAttribute(name, value));
|
|
1030
1195
|
}
|
|
1031
1196
|
|
|
1032
|
-
|
|
1033
|
-
return this.
|
|
1197
|
+
removeAttr(name) {
|
|
1198
|
+
return this.each((_, el) => el.removeAttribute(name));
|
|
1034
1199
|
}
|
|
1035
1200
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1201
|
+
prop(name, value) {
|
|
1202
|
+
if (value === undefined) return this.first()?.[name];
|
|
1203
|
+
return this.each((_, el) => { el[name] = value; });
|
|
1038
1204
|
}
|
|
1039
1205
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1206
|
+
data(key, value) {
|
|
1207
|
+
if (value === undefined) {
|
|
1208
|
+
if (key === undefined) return this.first()?.dataset;
|
|
1209
|
+
const raw = this.first()?.dataset[key];
|
|
1210
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
1211
|
+
}
|
|
1212
|
+
return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; });
|
|
1047
1213
|
}
|
|
1048
1214
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1215
|
+
// --- CSS / Dimensions ----------------------------------------------------
|
|
1216
|
+
|
|
1217
|
+
css(props, value) {
|
|
1218
|
+
if (typeof props === 'string' && value !== undefined) {
|
|
1219
|
+
return this.each((_, el) => { el.style[props] = value; });
|
|
1220
|
+
}
|
|
1221
|
+
if (typeof props === 'string') {
|
|
1222
|
+
const el = this.first();
|
|
1223
|
+
return el ? getComputedStyle(el)[props] : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
return this.each((_, el) => Object.assign(el.style, props));
|
|
1051
1226
|
}
|
|
1052
1227
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1060
|
-
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1061
|
-
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1062
|
-
});
|
|
1228
|
+
width() { return this.first()?.getBoundingClientRect().width; }
|
|
1229
|
+
height() { return this.first()?.getBoundingClientRect().height; }
|
|
1230
|
+
|
|
1231
|
+
offset() {
|
|
1232
|
+
const r = this.first()?.getBoundingClientRect();
|
|
1233
|
+
return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null;
|
|
1063
1234
|
}
|
|
1064
1235
|
|
|
1065
|
-
|
|
1236
|
+
position() {
|
|
1237
|
+
const el = this.first();
|
|
1238
|
+
return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
scrollTop(value) {
|
|
1242
|
+
if (value === undefined) {
|
|
1243
|
+
const el = this.first();
|
|
1244
|
+
return el === window ? window.scrollY : el?.scrollTop;
|
|
1245
|
+
}
|
|
1066
1246
|
return this.each((_, el) => {
|
|
1067
|
-
el.
|
|
1068
|
-
el.
|
|
1069
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1070
|
-
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1071
|
-
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1247
|
+
if (el === window) window.scrollTo(window.scrollX, value);
|
|
1248
|
+
else el.scrollTop = value;
|
|
1072
1249
|
});
|
|
1073
1250
|
}
|
|
1074
1251
|
|
|
1075
|
-
|
|
1252
|
+
scrollLeft(value) {
|
|
1253
|
+
if (value === undefined) {
|
|
1254
|
+
const el = this.first();
|
|
1255
|
+
return el === window ? window.scrollX : el?.scrollLeft;
|
|
1256
|
+
}
|
|
1076
1257
|
return this.each((_, el) => {
|
|
1077
|
-
if (el
|
|
1078
|
-
|
|
1079
|
-
el.style.overflow = 'hidden';
|
|
1080
|
-
const h = el.scrollHeight + 'px';
|
|
1081
|
-
el.style.maxHeight = '0';
|
|
1082
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1083
|
-
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1084
|
-
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1085
|
-
} else {
|
|
1086
|
-
el.style.overflow = 'hidden';
|
|
1087
|
-
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1088
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1089
|
-
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1090
|
-
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1091
|
-
}
|
|
1258
|
+
if (el === window) window.scrollTo(value, window.scrollY);
|
|
1259
|
+
else el.scrollLeft = value;
|
|
1092
1260
|
});
|
|
1093
1261
|
}
|
|
1094
1262
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const form = this.first();
|
|
1099
|
-
if (!form || form.tagName !== 'FORM') return '';
|
|
1100
|
-
return new URLSearchParams(new FormData(form)).toString();
|
|
1263
|
+
innerWidth() {
|
|
1264
|
+
const el = this.first();
|
|
1265
|
+
return el?.clientWidth;
|
|
1101
1266
|
}
|
|
1102
1267
|
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
const obj = {};
|
|
1107
|
-
new FormData(form).forEach((v, k) => {
|
|
1108
|
-
if (obj[k] !== undefined) {
|
|
1109
|
-
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
1110
|
-
obj[k].push(v);
|
|
1111
|
-
} else {
|
|
1112
|
-
obj[k] = v;
|
|
1113
|
-
}
|
|
1114
|
-
});
|
|
1115
|
-
return obj;
|
|
1268
|
+
innerHeight() {
|
|
1269
|
+
const el = this.first();
|
|
1270
|
+
return el?.clientHeight;
|
|
1116
1271
|
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
// ---------------------------------------------------------------------------
|
|
1121
|
-
// Helper — create document fragment from HTML string
|
|
1122
|
-
// ---------------------------------------------------------------------------
|
|
1123
|
-
function createFragment(html) {
|
|
1124
|
-
const tpl = document.createElement('template');
|
|
1125
|
-
tpl.innerHTML = html.trim();
|
|
1126
|
-
return tpl.content;
|
|
1127
|
-
}
|
|
1128
1272
|
|
|
1273
|
+
outerWidth(includeMargin = false) {
|
|
1274
|
+
const el = this.first();
|
|
1275
|
+
if (!el) return undefined;
|
|
1276
|
+
let w = el.offsetWidth;
|
|
1277
|
+
if (includeMargin) {
|
|
1278
|
+
const style = getComputedStyle(el);
|
|
1279
|
+
w += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
|
|
1280
|
+
}
|
|
1281
|
+
return w;
|
|
1282
|
+
}
|
|
1129
1283
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1284
|
+
outerHeight(includeMargin = false) {
|
|
1285
|
+
const el = this.first();
|
|
1286
|
+
if (!el) return undefined;
|
|
1287
|
+
let h = el.offsetHeight;
|
|
1288
|
+
if (includeMargin) {
|
|
1289
|
+
const style = getComputedStyle(el);
|
|
1290
|
+
h += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
|
|
1291
|
+
}
|
|
1292
|
+
return h;
|
|
1293
|
+
}
|
|
1136
1294
|
|
|
1137
|
-
//
|
|
1138
|
-
if (selector instanceof ZQueryCollection) return selector;
|
|
1295
|
+
// --- Content -------------------------------------------------------------
|
|
1139
1296
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1297
|
+
html(content) {
|
|
1298
|
+
if (content === undefined) return this.first()?.innerHTML;
|
|
1299
|
+
// Auto-morph: if the element already has children, use the diff engine
|
|
1300
|
+
// to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
|
|
1301
|
+
// Empty elements get raw innerHTML for fast first-paint — same strategy
|
|
1302
|
+
// the component system uses (first render = innerHTML, updates = morph).
|
|
1303
|
+
return this.each((_, el) => {
|
|
1304
|
+
if (el.childNodes.length > 0) {
|
|
1305
|
+
_morph(el, content);
|
|
1306
|
+
} else {
|
|
1307
|
+
el.innerHTML = content;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1143
1310
|
}
|
|
1144
1311
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return new ZQueryCollection(Array.from(selector));
|
|
1312
|
+
morph(content) {
|
|
1313
|
+
return this.each((_, el) => { _morph(el, content); });
|
|
1148
1314
|
}
|
|
1149
1315
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1316
|
+
text(content) {
|
|
1317
|
+
if (content === undefined) return this.first()?.textContent;
|
|
1318
|
+
return this.each((_, el) => { el.textContent = content; });
|
|
1154
1319
|
}
|
|
1155
1320
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1160
|
-
: document;
|
|
1161
|
-
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1321
|
+
val(value) {
|
|
1322
|
+
if (value === undefined) return this.first()?.value;
|
|
1323
|
+
return this.each((_, el) => { el.value = value; });
|
|
1162
1324
|
}
|
|
1163
1325
|
|
|
1164
|
-
|
|
1165
|
-
}
|
|
1326
|
+
// --- DOM Manipulation ----------------------------------------------------
|
|
1166
1327
|
|
|
1328
|
+
append(content) {
|
|
1329
|
+
return this.each((_, el) => {
|
|
1330
|
+
if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content);
|
|
1331
|
+
else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c));
|
|
1332
|
+
else if (content instanceof Node) el.appendChild(content);
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1167
1335
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1336
|
+
prepend(content) {
|
|
1337
|
+
return this.each((_, el) => {
|
|
1338
|
+
if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content);
|
|
1339
|
+
else if (content instanceof Node) el.insertBefore(content, el.firstChild);
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1174
1342
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1343
|
+
after(content) {
|
|
1344
|
+
return this.each((_, el) => {
|
|
1345
|
+
if (typeof content === 'string') el.insertAdjacentHTML('afterend', content);
|
|
1346
|
+
else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling);
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1177
1349
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1350
|
+
before(content) {
|
|
1351
|
+
return this.each((_, el) => {
|
|
1352
|
+
if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content);
|
|
1353
|
+
else if (content instanceof Node) el.parentNode.insertBefore(content, el);
|
|
1354
|
+
});
|
|
1181
1355
|
}
|
|
1182
1356
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1357
|
+
wrap(wrapper) {
|
|
1358
|
+
return this.each((_, el) => {
|
|
1359
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
1360
|
+
if (!w || !el.parentNode) return;
|
|
1361
|
+
el.parentNode.insertBefore(w, el);
|
|
1362
|
+
w.appendChild(el);
|
|
1363
|
+
});
|
|
1186
1364
|
}
|
|
1187
1365
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const fragment = createFragment(selector);
|
|
1191
|
-
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1366
|
+
remove() {
|
|
1367
|
+
return this.each((_, el) => el.remove());
|
|
1192
1368
|
}
|
|
1193
1369
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1198
|
-
: document;
|
|
1199
|
-
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1370
|
+
empty() {
|
|
1371
|
+
// textContent = '' clears all children without invoking the HTML parser
|
|
1372
|
+
return this.each((_, el) => { el.textContent = ''; });
|
|
1200
1373
|
}
|
|
1201
1374
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1375
|
+
clone(deep = true) {
|
|
1376
|
+
return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep)));
|
|
1377
|
+
}
|
|
1204
1378
|
|
|
1379
|
+
replaceWith(content) {
|
|
1380
|
+
return this.each((_, el) => {
|
|
1381
|
+
if (typeof content === 'string') {
|
|
1382
|
+
// Auto-morph: diff attributes + children when the tag name matches
|
|
1383
|
+
// instead of destroying and re-creating the element.
|
|
1384
|
+
_morphElement(el, content);
|
|
1385
|
+
} else if (content instanceof Node) {
|
|
1386
|
+
el.parentNode.replaceChild(content, el);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1205
1390
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
query.classes = (name) => new ZQueryCollection(Array.from(document.getElementsByClassName(name)));
|
|
1212
|
-
query.tag = (name) => new ZQueryCollection(Array.from(document.getElementsByTagName(name)));
|
|
1213
|
-
Object.defineProperty(query, 'name', {
|
|
1214
|
-
value: (name) => new ZQueryCollection(Array.from(document.getElementsByName(name))),
|
|
1215
|
-
writable: true, configurable: true
|
|
1216
|
-
});
|
|
1217
|
-
query.children = (parentId) => {
|
|
1218
|
-
const p = document.getElementById(parentId);
|
|
1219
|
-
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1220
|
-
};
|
|
1221
|
-
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
1222
|
-
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
1391
|
+
appendTo(target) {
|
|
1392
|
+
const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1393
|
+
if (dest) this.each((_, el) => dest.appendChild(el));
|
|
1394
|
+
return this;
|
|
1395
|
+
}
|
|
1223
1396
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
if (k === 'class') el.className = v;
|
|
1229
|
-
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1230
|
-
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
1231
|
-
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1232
|
-
else el.setAttribute(k, v);
|
|
1397
|
+
prependTo(target) {
|
|
1398
|
+
const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1399
|
+
if (dest) this.each((_, el) => dest.insertBefore(el, dest.firstChild));
|
|
1400
|
+
return this;
|
|
1233
1401
|
}
|
|
1234
|
-
children.flat().forEach(child => {
|
|
1235
|
-
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
1236
|
-
else if (child instanceof Node) el.appendChild(child);
|
|
1237
|
-
});
|
|
1238
|
-
return new ZQueryCollection(el);
|
|
1239
|
-
};
|
|
1240
1402
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1403
|
+
insertAfter(target) {
|
|
1404
|
+
const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1405
|
+
if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref.nextSibling));
|
|
1406
|
+
return this;
|
|
1407
|
+
}
|
|
1246
1408
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
query.on = (event, selectorOrHandler, handler) => {
|
|
1252
|
-
if (typeof selectorOrHandler === 'function') {
|
|
1253
|
-
// 2-arg: direct document listener (keydown, resize, etc.)
|
|
1254
|
-
document.addEventListener(event, selectorOrHandler);
|
|
1255
|
-
return;
|
|
1409
|
+
insertBefore(target) {
|
|
1410
|
+
const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1411
|
+
if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref));
|
|
1412
|
+
return this;
|
|
1256
1413
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1414
|
+
|
|
1415
|
+
replaceAll(target) {
|
|
1416
|
+
const targets = typeof target === 'string'
|
|
1417
|
+
? Array.from(document.querySelectorAll(target))
|
|
1418
|
+
: target instanceof ZQueryCollection ? target.elements : [target];
|
|
1419
|
+
targets.forEach((t, i) => {
|
|
1420
|
+
const nodes = i === 0 ? this.elements : this.elements.map(el => el.cloneNode(true));
|
|
1421
|
+
nodes.forEach(el => t.parentNode.insertBefore(el, t));
|
|
1422
|
+
t.remove();
|
|
1423
|
+
});
|
|
1424
|
+
return this;
|
|
1261
1425
|
}
|
|
1262
|
-
// 3-arg string: delegated
|
|
1263
|
-
document.addEventListener(event, (e) => {
|
|
1264
|
-
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1265
|
-
const target = e.target.closest(selectorOrHandler);
|
|
1266
|
-
if (target) handler.call(target, e);
|
|
1267
|
-
});
|
|
1268
|
-
};
|
|
1269
1426
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1427
|
+
unwrap(selector) {
|
|
1428
|
+
this.elements.forEach(el => {
|
|
1429
|
+
const parent = el.parentElement;
|
|
1430
|
+
if (!parent || parent === document.body) return;
|
|
1431
|
+
if (selector && !parent.matches(selector)) return;
|
|
1432
|
+
parent.replaceWith(...parent.childNodes);
|
|
1433
|
+
});
|
|
1434
|
+
return this;
|
|
1435
|
+
}
|
|
1274
1436
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
* evaluates expressions safely without violating Content Security Policy.
|
|
1284
|
-
*
|
|
1285
|
-
* Supports:
|
|
1286
|
-
* - Property access: user.name, items[0], items[i]
|
|
1287
|
-
* - Method calls: items.length, str.toUpperCase()
|
|
1288
|
-
* - Arithmetic: a + b, count * 2, i % 2
|
|
1289
|
-
* - Comparison: a === b, count > 0, x != null
|
|
1290
|
-
* - Logical: a && b, a || b, !a
|
|
1291
|
-
* - Ternary: a ? b : c
|
|
1292
|
-
* - Typeof: typeof x
|
|
1293
|
-
* - Unary: -a, +a, !a
|
|
1294
|
-
* - Literals: 42, 'hello', "world", true, false, null, undefined
|
|
1295
|
-
* - Template literals: `Hello ${name}`
|
|
1296
|
-
* - Array literals: [1, 2, 3]
|
|
1297
|
-
* - Object literals: { foo: 'bar', baz: 1 }
|
|
1298
|
-
* - Grouping: (a + b) * c
|
|
1299
|
-
* - Nullish coalescing: a ?? b
|
|
1300
|
-
* - Optional chaining: a?.b, a?.[b], a?.()
|
|
1301
|
-
* - Arrow functions: x => x.id, (a, b) => a + b
|
|
1302
|
-
*/
|
|
1303
|
-
|
|
1304
|
-
// Token types
|
|
1305
|
-
const T = {
|
|
1306
|
-
NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
|
-
// Operator precedence (higher = binds tighter)
|
|
1310
|
-
const PREC = {
|
|
1311
|
-
'??': 2,
|
|
1312
|
-
'||': 3,
|
|
1313
|
-
'&&': 4,
|
|
1314
|
-
'==': 8, '!=': 8, '===': 8, '!==': 8,
|
|
1315
|
-
'<': 9, '>': 9, '<=': 9, '>=': 9, 'instanceof': 9, 'in': 9,
|
|
1316
|
-
'+': 11, '-': 11,
|
|
1317
|
-
'*': 12, '/': 12, '%': 12,
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
|
-
const KEYWORDS = new Set([
|
|
1321
|
-
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
1322
|
-
'new', 'void'
|
|
1323
|
-
]);
|
|
1324
|
-
|
|
1325
|
-
// ---------------------------------------------------------------------------
|
|
1326
|
-
// Tokenizer
|
|
1327
|
-
// ---------------------------------------------------------------------------
|
|
1328
|
-
function tokenize(expr) {
|
|
1329
|
-
const tokens = [];
|
|
1330
|
-
let i = 0;
|
|
1331
|
-
const len = expr.length;
|
|
1332
|
-
|
|
1333
|
-
while (i < len) {
|
|
1334
|
-
const ch = expr[i];
|
|
1335
|
-
|
|
1336
|
-
// Whitespace
|
|
1337
|
-
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; }
|
|
1338
|
-
|
|
1339
|
-
// Numbers
|
|
1340
|
-
if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < len && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {
|
|
1341
|
-
let num = '';
|
|
1342
|
-
if (ch === '0' && i + 1 < len && (expr[i + 1] === 'x' || expr[i + 1] === 'X')) {
|
|
1343
|
-
num = '0x'; i += 2;
|
|
1344
|
-
while (i < len && /[0-9a-fA-F]/.test(expr[i])) num += expr[i++];
|
|
1345
|
-
} else {
|
|
1346
|
-
while (i < len && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) num += expr[i++];
|
|
1347
|
-
if (i < len && (expr[i] === 'e' || expr[i] === 'E')) {
|
|
1348
|
-
num += expr[i++];
|
|
1349
|
-
if (i < len && (expr[i] === '+' || expr[i] === '-')) num += expr[i++];
|
|
1350
|
-
while (i < len && expr[i] >= '0' && expr[i] <= '9') num += expr[i++];
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
tokens.push({ t: T.NUM, v: Number(num) });
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// Strings
|
|
1358
|
-
if (ch === "'" || ch === '"') {
|
|
1359
|
-
const quote = ch;
|
|
1360
|
-
let str = '';
|
|
1361
|
-
i++;
|
|
1362
|
-
while (i < len && expr[i] !== quote) {
|
|
1363
|
-
if (expr[i] === '\\' && i + 1 < len) {
|
|
1364
|
-
const esc = expr[++i];
|
|
1365
|
-
if (esc === 'n') str += '\n';
|
|
1366
|
-
else if (esc === 't') str += '\t';
|
|
1367
|
-
else if (esc === 'r') str += '\r';
|
|
1368
|
-
else if (esc === '\\') str += '\\';
|
|
1369
|
-
else if (esc === quote) str += quote;
|
|
1370
|
-
else str += esc;
|
|
1371
|
-
} else {
|
|
1372
|
-
str += expr[i];
|
|
1373
|
-
}
|
|
1374
|
-
i++;
|
|
1375
|
-
}
|
|
1376
|
-
i++; // closing quote
|
|
1377
|
-
tokens.push({ t: T.STR, v: str });
|
|
1378
|
-
continue;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Template literals
|
|
1382
|
-
if (ch === '`') {
|
|
1383
|
-
const parts = []; // alternating: string, expr, string, expr, ...
|
|
1384
|
-
let str = '';
|
|
1385
|
-
i++;
|
|
1386
|
-
while (i < len && expr[i] !== '`') {
|
|
1387
|
-
if (expr[i] === '$' && i + 1 < len && expr[i + 1] === '{') {
|
|
1388
|
-
parts.push(str);
|
|
1389
|
-
str = '';
|
|
1390
|
-
i += 2;
|
|
1391
|
-
let depth = 1;
|
|
1392
|
-
let inner = '';
|
|
1393
|
-
while (i < len && depth > 0) {
|
|
1394
|
-
if (expr[i] === '{') depth++;
|
|
1395
|
-
else if (expr[i] === '}') { depth--; if (depth === 0) break; }
|
|
1396
|
-
inner += expr[i++];
|
|
1397
|
-
}
|
|
1398
|
-
i++; // closing }
|
|
1399
|
-
parts.push({ expr: inner });
|
|
1400
|
-
} else {
|
|
1401
|
-
if (expr[i] === '\\' && i + 1 < len) { str += expr[++i]; }
|
|
1402
|
-
else str += expr[i];
|
|
1403
|
-
i++;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
i++; // closing backtick
|
|
1407
|
-
parts.push(str);
|
|
1408
|
-
tokens.push({ t: T.TMPL, v: parts });
|
|
1409
|
-
continue;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Identifiers & keywords
|
|
1413
|
-
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
1414
|
-
let ident = '';
|
|
1415
|
-
while (i < len && /[\w$]/.test(expr[i])) ident += expr[i++];
|
|
1416
|
-
tokens.push({ t: T.IDENT, v: ident });
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// Multi-char operators
|
|
1421
|
-
const two = expr.slice(i, i + 3);
|
|
1422
|
-
if (two === '===' || two === '!==' || two === '?.') {
|
|
1423
|
-
if (two === '?.') {
|
|
1424
|
-
tokens.push({ t: T.OP, v: '?.' });
|
|
1425
|
-
i += 2;
|
|
1426
|
-
} else {
|
|
1427
|
-
tokens.push({ t: T.OP, v: two });
|
|
1428
|
-
i += 3;
|
|
1429
|
-
}
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
const pair = expr.slice(i, i + 2);
|
|
1433
|
-
if (pair === '==' || pair === '!=' || pair === '<=' || pair === '>=' ||
|
|
1434
|
-
pair === '&&' || pair === '||' || pair === '??' || pair === '?.' ||
|
|
1435
|
-
pair === '=>') {
|
|
1436
|
-
tokens.push({ t: T.OP, v: pair });
|
|
1437
|
-
i += 2;
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Single char operators and punctuation
|
|
1442
|
-
if ('+-*/%'.includes(ch)) {
|
|
1443
|
-
tokens.push({ t: T.OP, v: ch });
|
|
1444
|
-
i++; continue;
|
|
1445
|
-
}
|
|
1446
|
-
if ('<>=!'.includes(ch)) {
|
|
1447
|
-
tokens.push({ t: T.OP, v: ch });
|
|
1448
|
-
i++; continue;
|
|
1449
|
-
}
|
|
1450
|
-
// Spread operator: ...
|
|
1451
|
-
if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
|
|
1452
|
-
tokens.push({ t: T.OP, v: '...' });
|
|
1453
|
-
i += 3; continue;
|
|
1454
|
-
}
|
|
1455
|
-
if ('()[]{},.?:'.includes(ch)) {
|
|
1456
|
-
tokens.push({ t: T.PUNC, v: ch });
|
|
1457
|
-
i++; continue;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Unknown — skip
|
|
1461
|
-
i++;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
tokens.push({ t: T.EOF, v: null });
|
|
1465
|
-
return tokens;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// ---------------------------------------------------------------------------
|
|
1469
|
-
// Parser — Pratt (precedence climbing)
|
|
1470
|
-
// ---------------------------------------------------------------------------
|
|
1471
|
-
class Parser {
|
|
1472
|
-
constructor(tokens, scope) {
|
|
1473
|
-
this.tokens = tokens;
|
|
1474
|
-
this.pos = 0;
|
|
1475
|
-
this.scope = scope;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
peek() { return this.tokens[this.pos]; }
|
|
1479
|
-
next() { return this.tokens[this.pos++]; }
|
|
1480
|
-
|
|
1481
|
-
expect(type, val) {
|
|
1482
|
-
const t = this.next();
|
|
1483
|
-
if (t.t !== type || (val !== undefined && t.v !== val)) {
|
|
1484
|
-
throw new Error(`Expected ${val || type} but got ${t.v}`);
|
|
1485
|
-
}
|
|
1486
|
-
return t;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
match(type, val) {
|
|
1490
|
-
const t = this.peek();
|
|
1491
|
-
if (t.t === type && (val === undefined || t.v === val)) {
|
|
1492
|
-
return this.next();
|
|
1493
|
-
}
|
|
1494
|
-
return null;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Main entry
|
|
1498
|
-
parse() {
|
|
1499
|
-
const result = this.parseExpression(0);
|
|
1500
|
-
return result;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// Precedence climbing
|
|
1504
|
-
parseExpression(minPrec) {
|
|
1505
|
-
let left = this.parseUnary();
|
|
1506
|
-
|
|
1507
|
-
while (true) {
|
|
1508
|
-
const tok = this.peek();
|
|
1509
|
-
|
|
1510
|
-
// Ternary
|
|
1511
|
-
if (tok.t === T.PUNC && tok.v === '?') {
|
|
1512
|
-
// Distinguish ternary ? from optional chaining ?.
|
|
1513
|
-
if (this.tokens[this.pos + 1]?.v !== '.') {
|
|
1514
|
-
if (1 <= minPrec) break; // ternary has very low precedence
|
|
1515
|
-
this.next(); // consume ?
|
|
1516
|
-
const truthy = this.parseExpression(0);
|
|
1517
|
-
this.expect(T.PUNC, ':');
|
|
1518
|
-
const falsy = this.parseExpression(0);
|
|
1519
|
-
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
1520
|
-
continue;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// Binary operators
|
|
1525
|
-
if (tok.t === T.OP && tok.v in PREC) {
|
|
1526
|
-
const prec = PREC[tok.v];
|
|
1527
|
-
if (prec <= minPrec) break;
|
|
1528
|
-
this.next();
|
|
1529
|
-
const right = this.parseExpression(prec);
|
|
1530
|
-
left = { type: 'binary', op: tok.v, left, right };
|
|
1531
|
-
continue;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// instanceof and in as binary operators
|
|
1535
|
-
if (tok.t === T.IDENT && (tok.v === 'instanceof' || tok.v === 'in') && PREC[tok.v] > minPrec) {
|
|
1536
|
-
const prec = PREC[tok.v];
|
|
1537
|
-
this.next();
|
|
1538
|
-
const right = this.parseExpression(prec);
|
|
1539
|
-
left = { type: 'binary', op: tok.v, left, right };
|
|
1540
|
-
continue;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
break;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return left;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
parseUnary() {
|
|
1550
|
-
const tok = this.peek();
|
|
1551
|
-
|
|
1552
|
-
// typeof
|
|
1553
|
-
if (tok.t === T.IDENT && tok.v === 'typeof') {
|
|
1554
|
-
this.next();
|
|
1555
|
-
const arg = this.parseUnary();
|
|
1556
|
-
return { type: 'typeof', arg };
|
|
1557
|
-
}
|
|
1437
|
+
wrapAll(wrapper) {
|
|
1438
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
1439
|
+
const first = this.first();
|
|
1440
|
+
if (!first) return this;
|
|
1441
|
+
first.parentNode.insertBefore(w, first);
|
|
1442
|
+
this.each((_, el) => w.appendChild(el));
|
|
1443
|
+
return this;
|
|
1444
|
+
}
|
|
1558
1445
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1446
|
+
wrapInner(wrapper) {
|
|
1447
|
+
return this.each((_, el) => {
|
|
1448
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
1449
|
+
while (el.firstChild) w.appendChild(el.firstChild);
|
|
1450
|
+
el.appendChild(w);
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1565
1453
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const arg = this.parseUnary();
|
|
1570
|
-
return { type: 'not', arg };
|
|
1571
|
-
}
|
|
1454
|
+
detach() {
|
|
1455
|
+
return this.each((_, el) => el.remove());
|
|
1456
|
+
}
|
|
1572
1457
|
|
|
1573
|
-
|
|
1574
|
-
if (tok.t === T.OP && (tok.v === '-' || tok.v === '+')) {
|
|
1575
|
-
this.next();
|
|
1576
|
-
const arg = this.parseUnary();
|
|
1577
|
-
return { type: 'unary', op: tok.v, arg };
|
|
1578
|
-
}
|
|
1458
|
+
// --- Visibility ----------------------------------------------------------
|
|
1579
1459
|
|
|
1580
|
-
|
|
1460
|
+
show(display = '') {
|
|
1461
|
+
return this.each((_, el) => { el.style.display = display; });
|
|
1581
1462
|
}
|
|
1582
1463
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1464
|
+
hide() {
|
|
1465
|
+
return this.each((_, el) => { el.style.display = 'none'; });
|
|
1466
|
+
}
|
|
1585
1467
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1468
|
+
toggle(display = '') {
|
|
1469
|
+
return this.each((_, el) => {
|
|
1470
|
+
// Check inline style first (cheap) before forcing layout via getComputedStyle
|
|
1471
|
+
const hidden = el.style.display === 'none' || (el.style.display !== '' ? false : getComputedStyle(el).display === 'none');
|
|
1472
|
+
el.style.display = hidden ? display : 'none';
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1588
1475
|
|
|
1589
|
-
|
|
1590
|
-
if (tok.t === T.PUNC && tok.v === '.') {
|
|
1591
|
-
this.next();
|
|
1592
|
-
const prop = this.next();
|
|
1593
|
-
left = { type: 'member', obj: left, prop: prop.v, computed: false };
|
|
1594
|
-
// Check for method call: a.b()
|
|
1595
|
-
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1596
|
-
left = this._parseCall(left);
|
|
1597
|
-
}
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1476
|
+
// --- Events --------------------------------------------------------------
|
|
1600
1477
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
}
|
|
1478
|
+
on(event, selectorOrHandler, handler) {
|
|
1479
|
+
// Support multiple events: "click mouseenter"
|
|
1480
|
+
const events = event.split(/\s+/);
|
|
1481
|
+
return this.each((_, el) => {
|
|
1482
|
+
events.forEach(evt => {
|
|
1483
|
+
if (typeof selectorOrHandler === 'function') {
|
|
1484
|
+
el.addEventListener(evt, selectorOrHandler);
|
|
1485
|
+
} else if (typeof selectorOrHandler === 'string') {
|
|
1486
|
+
// Delegated event — store wrapper so off() can remove it
|
|
1487
|
+
const wrapper = (e) => {
|
|
1488
|
+
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1489
|
+
const target = e.target.closest(selectorOrHandler);
|
|
1490
|
+
if (target && el.contains(target)) handler.call(target, e);
|
|
1491
|
+
};
|
|
1492
|
+
wrapper._zqOriginal = handler;
|
|
1493
|
+
wrapper._zqSelector = selectorOrHandler;
|
|
1494
|
+
el.addEventListener(evt, wrapper);
|
|
1495
|
+
// Track delegated handlers for removal
|
|
1496
|
+
if (!el._zqDelegated) el._zqDelegated = [];
|
|
1497
|
+
el._zqDelegated.push({ evt, wrapper });
|
|
1621
1498
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1499
|
+
});
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1624
1502
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
//
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1503
|
+
off(event, handler) {
|
|
1504
|
+
const events = event.split(/\s+/);
|
|
1505
|
+
return this.each((_, el) => {
|
|
1506
|
+
events.forEach(evt => {
|
|
1507
|
+
// Try direct removal first
|
|
1508
|
+
el.removeEventListener(evt, handler);
|
|
1509
|
+
// Also check delegated handlers
|
|
1510
|
+
if (el._zqDelegated) {
|
|
1511
|
+
el._zqDelegated = el._zqDelegated.filter(d => {
|
|
1512
|
+
if (d.evt === evt && d.wrapper._zqOriginal === handler) {
|
|
1513
|
+
el.removeEventListener(evt, d.wrapper);
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
return true;
|
|
1517
|
+
});
|
|
1634
1518
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1637
1522
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1523
|
+
one(event, handler) {
|
|
1524
|
+
return this.each((_, el) => {
|
|
1525
|
+
el.addEventListener(event, handler, { once: true });
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1643
1528
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1529
|
+
trigger(event, detail) {
|
|
1530
|
+
return this.each((_, el) => {
|
|
1531
|
+
el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1646
1534
|
|
|
1647
|
-
|
|
1535
|
+
// Convenience event shorthands
|
|
1536
|
+
click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); }
|
|
1537
|
+
submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
|
|
1538
|
+
focus() { this.first()?.focus(); return this; }
|
|
1539
|
+
blur() { this.first()?.blur(); return this; }
|
|
1540
|
+
hover(enterFn, leaveFn) {
|
|
1541
|
+
this.on('mouseenter', enterFn);
|
|
1542
|
+
return this.on('mouseleave', leaveFn || enterFn);
|
|
1648
1543
|
}
|
|
1649
1544
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1545
|
+
// --- Animation -----------------------------------------------------------
|
|
1546
|
+
|
|
1547
|
+
animate(props, duration = 300, easing = 'ease') {
|
|
1548
|
+
// Empty collection — resolve immediately
|
|
1549
|
+
if (this.length === 0) return Promise.resolve(this);
|
|
1550
|
+
return new Promise(resolve => {
|
|
1551
|
+
let resolved = false;
|
|
1552
|
+
const count = { done: 0 };
|
|
1553
|
+
const listeners = [];
|
|
1554
|
+
this.each((_, el) => {
|
|
1555
|
+
el.style.transition = `all ${duration}ms ${easing}`;
|
|
1556
|
+
requestAnimationFrame(() => {
|
|
1557
|
+
Object.assign(el.style, props);
|
|
1558
|
+
const onEnd = () => {
|
|
1559
|
+
el.removeEventListener('transitionend', onEnd);
|
|
1560
|
+
el.style.transition = '';
|
|
1561
|
+
if (!resolved && ++count.done >= this.length) {
|
|
1562
|
+
resolved = true;
|
|
1563
|
+
resolve(this);
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
el.addEventListener('transitionend', onEnd);
|
|
1567
|
+
listeners.push({ el, onEnd });
|
|
1568
|
+
});
|
|
1569
|
+
});
|
|
1570
|
+
// Fallback in case transitionend doesn't fire
|
|
1571
|
+
setTimeout(() => {
|
|
1572
|
+
if (!resolved) {
|
|
1573
|
+
resolved = true;
|
|
1574
|
+
// Clean up any remaining transitionend listeners
|
|
1575
|
+
for (const { el, onEnd } of listeners) {
|
|
1576
|
+
el.removeEventListener('transitionend', onEnd);
|
|
1577
|
+
el.style.transition = '';
|
|
1578
|
+
}
|
|
1579
|
+
resolve(this);
|
|
1580
|
+
}
|
|
1581
|
+
}, duration + 50);
|
|
1582
|
+
});
|
|
1653
1583
|
}
|
|
1654
1584
|
|
|
1655
|
-
|
|
1656
|
-
this.
|
|
1657
|
-
const args = [];
|
|
1658
|
-
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
1659
|
-
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
1660
|
-
this.next();
|
|
1661
|
-
args.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
1662
|
-
} else {
|
|
1663
|
-
args.push(this.parseExpression(0));
|
|
1664
|
-
}
|
|
1665
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1666
|
-
}
|
|
1667
|
-
this.expect(T.PUNC, ')');
|
|
1668
|
-
return args;
|
|
1585
|
+
fadeIn(duration = 300) {
|
|
1586
|
+
return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration);
|
|
1669
1587
|
}
|
|
1670
1588
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1589
|
+
fadeOut(duration = 300) {
|
|
1590
|
+
return this.animate({ opacity: '0' }, duration).then(col => col.hide());
|
|
1591
|
+
}
|
|
1673
1592
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1593
|
+
fadeToggle(duration = 300) {
|
|
1594
|
+
return Promise.all(this.elements.map(el => {
|
|
1595
|
+
const cs = getComputedStyle(el);
|
|
1596
|
+
const visible = cs.opacity !== '0' && cs.display !== 'none';
|
|
1597
|
+
const col = new ZQueryCollection([el]);
|
|
1598
|
+
return visible ? col.fadeOut(duration) : col.fadeIn(duration);
|
|
1599
|
+
})).then(() => this);
|
|
1600
|
+
}
|
|
1679
1601
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
return { type: 'literal', value: tok.v };
|
|
1684
|
-
}
|
|
1602
|
+
fadeTo(duration, opacity) {
|
|
1603
|
+
return this.animate({ opacity: String(opacity) }, duration);
|
|
1604
|
+
}
|
|
1685
1605
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1606
|
+
slideDown(duration = 300) {
|
|
1607
|
+
return this.each((_, el) => {
|
|
1608
|
+
el.style.display = '';
|
|
1609
|
+
el.style.overflow = 'hidden';
|
|
1610
|
+
const h = el.scrollHeight + 'px';
|
|
1611
|
+
el.style.maxHeight = '0';
|
|
1612
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1613
|
+
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1614
|
+
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1691
1617
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1618
|
+
slideUp(duration = 300) {
|
|
1619
|
+
return this.each((_, el) => {
|
|
1620
|
+
el.style.overflow = 'hidden';
|
|
1621
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1622
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1623
|
+
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1624
|
+
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1699
1627
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1628
|
+
slideToggle(duration = 300) {
|
|
1629
|
+
return this.each((_, el) => {
|
|
1630
|
+
if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
|
|
1631
|
+
el.style.display = '';
|
|
1632
|
+
el.style.overflow = 'hidden';
|
|
1633
|
+
const h = el.scrollHeight + 'px';
|
|
1634
|
+
el.style.maxHeight = '0';
|
|
1635
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1636
|
+
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1637
|
+
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1702
1638
|
} else {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
this.next();
|
|
1709
|
-
} else {
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
} else {
|
|
1713
|
-
couldBeArrow = false;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1639
|
+
el.style.overflow = 'hidden';
|
|
1640
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1641
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1642
|
+
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1643
|
+
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1716
1644
|
}
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1717
1647
|
|
|
1718
|
-
|
|
1719
|
-
this.next(); // consume )
|
|
1720
|
-
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
1721
|
-
this.next(); // consume =>
|
|
1722
|
-
const body = this.parseExpression(0);
|
|
1723
|
-
return { type: 'arrow', params, body };
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1648
|
+
// --- Form helpers --------------------------------------------------------
|
|
1726
1649
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
return expr;
|
|
1733
|
-
}
|
|
1650
|
+
serialize() {
|
|
1651
|
+
const form = this.first();
|
|
1652
|
+
if (!form || form.tagName !== 'FORM') return '';
|
|
1653
|
+
return new URLSearchParams(new FormData(form)).toString();
|
|
1654
|
+
}
|
|
1734
1655
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1656
|
+
serializeObject() {
|
|
1657
|
+
const form = this.first();
|
|
1658
|
+
if (!form || form.tagName !== 'FORM') return {};
|
|
1659
|
+
const obj = {};
|
|
1660
|
+
new FormData(form).forEach((v, k) => {
|
|
1661
|
+
if (obj[k] !== undefined) {
|
|
1662
|
+
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
1663
|
+
obj[k].push(v);
|
|
1664
|
+
} else {
|
|
1665
|
+
obj[k] = v;
|
|
1747
1666
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1667
|
+
});
|
|
1668
|
+
return obj;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1751
1671
|
|
|
1752
|
-
// Object literal
|
|
1753
|
-
if (tok.t === T.PUNC && tok.v === '{') {
|
|
1754
|
-
this.next();
|
|
1755
|
-
const properties = [];
|
|
1756
|
-
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
1757
|
-
// Spread in object: { ...obj }
|
|
1758
|
-
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
1759
|
-
this.next();
|
|
1760
|
-
properties.push({ spread: true, value: this.parseExpression(0) });
|
|
1761
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1762
|
-
continue;
|
|
1763
|
-
}
|
|
1764
1672
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1673
|
+
// ---------------------------------------------------------------------------
|
|
1674
|
+
// Helper — create document fragment from HTML string
|
|
1675
|
+
// ---------------------------------------------------------------------------
|
|
1676
|
+
function createFragment(html) {
|
|
1677
|
+
const tpl = document.createElement('template');
|
|
1678
|
+
tpl.innerHTML = html.trim();
|
|
1679
|
+
return tpl.content;
|
|
1680
|
+
}
|
|
1770
1681
|
|
|
1771
|
-
// Shorthand property: { foo } means { foo: foo }
|
|
1772
|
-
if (this.peek().t === T.PUNC && (this.peek().v === ',' || this.peek().v === '}')) {
|
|
1773
|
-
properties.push({ key, value: { type: 'ident', name: key } });
|
|
1774
|
-
} else {
|
|
1775
|
-
this.expect(T.PUNC, ':');
|
|
1776
|
-
properties.push({ key, value: this.parseExpression(0) });
|
|
1777
|
-
}
|
|
1778
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1779
|
-
}
|
|
1780
|
-
this.expect(T.PUNC, '}');
|
|
1781
|
-
return { type: 'object', properties };
|
|
1782
|
-
}
|
|
1783
1682
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1683
|
+
// ---------------------------------------------------------------------------
|
|
1684
|
+
// $() — main selector / creator (returns ZQueryCollection, like jQuery)
|
|
1685
|
+
// ---------------------------------------------------------------------------
|
|
1686
|
+
function query(selector, context) {
|
|
1687
|
+
// null / undefined
|
|
1688
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1787
1689
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
if (tok.v === 'false') return { type: 'literal', value: false };
|
|
1791
|
-
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
1792
|
-
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
1690
|
+
// Already a collection — return as-is
|
|
1691
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1793
1692
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
while (this.peek().t === T.PUNC && this.peek().v === '.') {
|
|
1799
|
-
this.next();
|
|
1800
|
-
const prop = this.next();
|
|
1801
|
-
classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
|
|
1802
|
-
}
|
|
1803
|
-
let args = [];
|
|
1804
|
-
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1805
|
-
args = this._parseArgs();
|
|
1806
|
-
}
|
|
1807
|
-
return { type: 'new', callee: classExpr, args };
|
|
1808
|
-
}
|
|
1693
|
+
// DOM element or Window — wrap in collection
|
|
1694
|
+
if (selector instanceof Node || selector === window) {
|
|
1695
|
+
return new ZQueryCollection([selector]);
|
|
1696
|
+
}
|
|
1809
1697
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
return { type: 'arrow', params: [tok.v], body };
|
|
1815
|
-
}
|
|
1698
|
+
// NodeList / HTMLCollection / Array — wrap in collection
|
|
1699
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1700
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1701
|
+
}
|
|
1816
1702
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1703
|
+
// HTML string → create elements, wrap in collection
|
|
1704
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
1705
|
+
const fragment = createFragment(selector);
|
|
1706
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1707
|
+
}
|
|
1819
1708
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1709
|
+
// CSS selector string → querySelectorAll (collection)
|
|
1710
|
+
if (typeof selector === 'string') {
|
|
1711
|
+
const root = context
|
|
1712
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1713
|
+
: document;
|
|
1714
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1823
1715
|
}
|
|
1716
|
+
|
|
1717
|
+
return new ZQueryCollection([]);
|
|
1824
1718
|
}
|
|
1825
1719
|
|
|
1720
|
+
|
|
1826
1721
|
// ---------------------------------------------------------------------------
|
|
1827
|
-
//
|
|
1722
|
+
// $.all() — collection selector (returns ZQueryCollection for CSS selectors)
|
|
1828
1723
|
// ---------------------------------------------------------------------------
|
|
1724
|
+
function queryAll(selector, context) {
|
|
1725
|
+
// null / undefined
|
|
1726
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1829
1727
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
1833
|
-
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
1834
|
-
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
1835
|
-
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
1836
|
-
]);
|
|
1837
|
-
|
|
1838
|
-
const SAFE_STRING_METHODS = new Set([
|
|
1839
|
-
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
1840
|
-
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
1841
|
-
'toUpperCase', 'split', 'replace', 'replaceAll', 'match', 'search',
|
|
1842
|
-
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'at',
|
|
1843
|
-
'toString', 'valueOf',
|
|
1844
|
-
]);
|
|
1845
|
-
|
|
1846
|
-
const SAFE_NUMBER_METHODS = new Set([
|
|
1847
|
-
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
1848
|
-
]);
|
|
1728
|
+
// Already a collection
|
|
1729
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1849
1730
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
]);
|
|
1731
|
+
// DOM element or Window
|
|
1732
|
+
if (selector instanceof Node || selector === window) {
|
|
1733
|
+
return new ZQueryCollection([selector]);
|
|
1734
|
+
}
|
|
1853
1735
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
]);
|
|
1736
|
+
// NodeList / HTMLCollection / Array
|
|
1737
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1738
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1739
|
+
}
|
|
1859
1740
|
|
|
1860
|
-
|
|
1741
|
+
// HTML string → create elements
|
|
1742
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
1743
|
+
const fragment = createFragment(selector);
|
|
1744
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1745
|
+
}
|
|
1861
1746
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
1870
|
-
'call', 'apply', 'bind',
|
|
1871
|
-
]);
|
|
1872
|
-
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
1747
|
+
// CSS selector string → querySelectorAll (collection)
|
|
1748
|
+
if (typeof selector === 'string') {
|
|
1749
|
+
const root = context
|
|
1750
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1751
|
+
: document;
|
|
1752
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1753
|
+
}
|
|
1873
1754
|
|
|
1874
|
-
|
|
1875
|
-
if (obj !== null && obj !== undefined && (typeof obj === 'object' || typeof obj === 'function')) return true;
|
|
1876
|
-
if (typeof obj === 'string') return SAFE_STRING_METHODS.has(prop);
|
|
1877
|
-
if (typeof obj === 'number') return SAFE_NUMBER_METHODS.has(prop);
|
|
1878
|
-
return false;
|
|
1755
|
+
return new ZQueryCollection([]);
|
|
1879
1756
|
}
|
|
1880
1757
|
|
|
1881
|
-
function evaluate(node, scope) {
|
|
1882
|
-
if (!node) return undefined;
|
|
1883
|
-
|
|
1884
|
-
switch (node.type) {
|
|
1885
|
-
case 'literal':
|
|
1886
|
-
return node.value;
|
|
1887
1758
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
if (name === 'parseInt') return parseInt;
|
|
1906
|
-
if (name === 'parseFloat') return parseFloat;
|
|
1907
|
-
if (name === 'isNaN') return isNaN;
|
|
1908
|
-
if (name === 'isFinite') return isFinite;
|
|
1909
|
-
if (name === 'Infinity') return Infinity;
|
|
1910
|
-
if (name === 'NaN') return NaN;
|
|
1911
|
-
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
1912
|
-
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
1913
|
-
if (name === 'console') return console;
|
|
1914
|
-
if (name === 'Map') return Map;
|
|
1915
|
-
if (name === 'Set') return Set;
|
|
1916
|
-
if (name === 'RegExp') return RegExp;
|
|
1917
|
-
if (name === 'Error') return Error;
|
|
1918
|
-
if (name === 'URL') return URL;
|
|
1919
|
-
if (name === 'URLSearchParams') return URLSearchParams;
|
|
1920
|
-
return undefined;
|
|
1921
|
-
}
|
|
1759
|
+
// ---------------------------------------------------------------------------
|
|
1760
|
+
// Quick-ref shortcuts, on $ namespace)
|
|
1761
|
+
// ---------------------------------------------------------------------------
|
|
1762
|
+
query.id = (id) => document.getElementById(id);
|
|
1763
|
+
query.class = (name) => document.querySelector(`.${name}`);
|
|
1764
|
+
query.classes = (name) => new ZQueryCollection(Array.from(document.getElementsByClassName(name)));
|
|
1765
|
+
query.tag = (name) => new ZQueryCollection(Array.from(document.getElementsByTagName(name)));
|
|
1766
|
+
Object.defineProperty(query, 'name', {
|
|
1767
|
+
value: (name) => new ZQueryCollection(Array.from(document.getElementsByName(name))),
|
|
1768
|
+
writable: true, configurable: true
|
|
1769
|
+
});
|
|
1770
|
+
query.children = (parentId) => {
|
|
1771
|
+
const p = document.getElementById(parentId);
|
|
1772
|
+
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1773
|
+
};
|
|
1774
|
+
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
1775
|
+
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
1922
1776
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1777
|
+
// Create element shorthand — returns ZQueryCollection for chaining
|
|
1778
|
+
query.create = (tag, attrs = {}, ...children) => {
|
|
1779
|
+
const el = document.createElement(tag);
|
|
1780
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1781
|
+
if (k === 'class') el.className = v;
|
|
1782
|
+
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1783
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
1784
|
+
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1785
|
+
else el.setAttribute(k, v);
|
|
1786
|
+
}
|
|
1787
|
+
children.flat().forEach(child => {
|
|
1788
|
+
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
1789
|
+
else if (child instanceof Node) el.appendChild(child);
|
|
1790
|
+
});
|
|
1791
|
+
return new ZQueryCollection(el);
|
|
1792
|
+
};
|
|
1935
1793
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
return obj[prop];
|
|
1942
|
-
}
|
|
1794
|
+
// DOM ready
|
|
1795
|
+
query.ready = (fn) => {
|
|
1796
|
+
if (document.readyState !== 'loading') fn();
|
|
1797
|
+
else document.addEventListener('DOMContentLoaded', fn);
|
|
1798
|
+
};
|
|
1943
1799
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1800
|
+
// Global event listeners — supports direct, delegated, and target-bound forms
|
|
1801
|
+
// $.on('keydown', handler) → direct listener on document
|
|
1802
|
+
// $.on('click', '.btn', handler) → delegated via closest()
|
|
1803
|
+
// $.on('scroll', window, handler) → direct listener on target
|
|
1804
|
+
query.on = (event, selectorOrHandler, handler) => {
|
|
1805
|
+
if (typeof selectorOrHandler === 'function') {
|
|
1806
|
+
// 2-arg: direct document listener (keydown, resize, etc.)
|
|
1807
|
+
document.addEventListener(event, selectorOrHandler);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
// EventTarget (window, element, etc.) — direct listener on target
|
|
1811
|
+
if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
|
|
1812
|
+
selectorOrHandler.addEventListener(event, handler);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
// 3-arg string: delegated
|
|
1816
|
+
document.addEventListener(event, (e) => {
|
|
1817
|
+
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1818
|
+
const target = e.target.closest(selectorOrHandler);
|
|
1819
|
+
if (target) handler.call(target, e);
|
|
1820
|
+
});
|
|
1821
|
+
};
|
|
1951
1822
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1823
|
+
// Remove a direct global listener
|
|
1824
|
+
query.off = (event, handler) => {
|
|
1825
|
+
document.removeEventListener(event, handler);
|
|
1826
|
+
};
|
|
1956
1827
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1828
|
+
// Extend collection prototype (like $.fn in jQuery)
|
|
1829
|
+
query.fn = ZQueryCollection.prototype;
|
|
1830
|
+
|
|
1831
|
+
// --- src/expression.js -------------------------------------------
|
|
1832
|
+
/**
|
|
1833
|
+
* zQuery Expression Parser — CSP-safe expression evaluator
|
|
1834
|
+
*
|
|
1835
|
+
* Replaces `new Function()` / `eval()` with a hand-written parser that
|
|
1836
|
+
* evaluates expressions safely without violating Content Security Policy.
|
|
1837
|
+
*
|
|
1838
|
+
* Supports:
|
|
1839
|
+
* - Property access: user.name, items[0], items[i]
|
|
1840
|
+
* - Method calls: items.length, str.toUpperCase()
|
|
1841
|
+
* - Arithmetic: a + b, count * 2, i % 2
|
|
1842
|
+
* - Comparison: a === b, count > 0, x != null
|
|
1843
|
+
* - Logical: a && b, a || b, !a
|
|
1844
|
+
* - Ternary: a ? b : c
|
|
1845
|
+
* - Typeof: typeof x
|
|
1846
|
+
* - Unary: -a, +a, !a
|
|
1847
|
+
* - Literals: 42, 'hello', "world", true, false, null, undefined
|
|
1848
|
+
* - Template literals: `Hello ${name}`
|
|
1849
|
+
* - Array literals: [1, 2, 3]
|
|
1850
|
+
* - Object literals: { foo: 'bar', baz: 1 }
|
|
1851
|
+
* - Grouping: (a + b) * c
|
|
1852
|
+
* - Nullish coalescing: a ?? b
|
|
1853
|
+
* - Optional chaining: a?.b, a?.[b], a?.()
|
|
1854
|
+
* - Arrow functions: x => x.id, (a, b) => a + b
|
|
1855
|
+
*/
|
|
1975
1856
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
1981
|
-
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
1982
|
-
const args = _evalArgs(node.args, scope);
|
|
1983
|
-
return new Ctor(...args);
|
|
1984
|
-
}
|
|
1985
|
-
return undefined;
|
|
1986
|
-
}
|
|
1857
|
+
// Token types
|
|
1858
|
+
const T = {
|
|
1859
|
+
NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
|
|
1860
|
+
};
|
|
1987
1861
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1862
|
+
// Operator precedence (higher = binds tighter)
|
|
1863
|
+
const PREC = {
|
|
1864
|
+
'??': 2,
|
|
1865
|
+
'||': 3,
|
|
1866
|
+
'&&': 4,
|
|
1867
|
+
'==': 8, '!=': 8, '===': 8, '!==': 8,
|
|
1868
|
+
'<': 9, '>': 9, '<=': 9, '>=': 9, 'instanceof': 9, 'in': 9,
|
|
1869
|
+
'+': 11, '-': 11,
|
|
1870
|
+
'*': 12, '/': 12, '%': 12,
|
|
1871
|
+
};
|
|
1990
1872
|
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1873
|
+
const KEYWORDS = new Set([
|
|
1874
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
1875
|
+
'new', 'void'
|
|
1876
|
+
]);
|
|
1995
1877
|
|
|
1996
|
-
|
|
1997
|
-
|
|
1878
|
+
// ---------------------------------------------------------------------------
|
|
1879
|
+
// Tokenizer
|
|
1880
|
+
// ---------------------------------------------------------------------------
|
|
1881
|
+
function tokenize(expr) {
|
|
1882
|
+
const tokens = [];
|
|
1883
|
+
let i = 0;
|
|
1884
|
+
const len = expr.length;
|
|
1998
1885
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
return typeof evaluate(node.arg, scope);
|
|
2002
|
-
} catch {
|
|
2003
|
-
return 'undefined';
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
1886
|
+
while (i < len) {
|
|
1887
|
+
const ch = expr[i];
|
|
2006
1888
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1889
|
+
// Whitespace
|
|
1890
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; }
|
|
1891
|
+
|
|
1892
|
+
// Numbers
|
|
1893
|
+
if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < len && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {
|
|
1894
|
+
let num = '';
|
|
1895
|
+
if (ch === '0' && i + 1 < len && (expr[i + 1] === 'x' || expr[i + 1] === 'X')) {
|
|
1896
|
+
num = '0x'; i += 2;
|
|
1897
|
+
while (i < len && /[0-9a-fA-F]/.test(expr[i])) num += expr[i++];
|
|
1898
|
+
} else {
|
|
1899
|
+
while (i < len && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) num += expr[i++];
|
|
1900
|
+
if (i < len && (expr[i] === 'e' || expr[i] === 'E')) {
|
|
1901
|
+
num += expr[i++];
|
|
1902
|
+
if (i < len && (expr[i] === '+' || expr[i] === '-')) num += expr[i++];
|
|
1903
|
+
while (i < len && expr[i] >= '0' && expr[i] <= '9') num += expr[i++];
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
tokens.push({ t: T.NUM, v: Number(num) });
|
|
1907
|
+
continue;
|
|
2010
1908
|
}
|
|
2011
1909
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
1910
|
+
// Strings
|
|
1911
|
+
if (ch === "'" || ch === '"') {
|
|
1912
|
+
const quote = ch;
|
|
1913
|
+
let str = '';
|
|
1914
|
+
i++;
|
|
1915
|
+
while (i < len && expr[i] !== quote) {
|
|
1916
|
+
if (expr[i] === '\\' && i + 1 < len) {
|
|
1917
|
+
const esc = expr[++i];
|
|
1918
|
+
if (esc === 'n') str += '\n';
|
|
1919
|
+
else if (esc === 't') str += '\t';
|
|
1920
|
+
else if (esc === 'r') str += '\r';
|
|
1921
|
+
else if (esc === '\\') str += '\\';
|
|
1922
|
+
else if (esc === quote) str += quote;
|
|
1923
|
+
else str += esc;
|
|
2020
1924
|
} else {
|
|
2021
|
-
|
|
1925
|
+
str += expr[i];
|
|
2022
1926
|
}
|
|
1927
|
+
i++;
|
|
2023
1928
|
}
|
|
2024
|
-
|
|
1929
|
+
i++; // closing quote
|
|
1930
|
+
tokens.push({ t: T.STR, v: str });
|
|
1931
|
+
continue;
|
|
2025
1932
|
}
|
|
2026
1933
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
1934
|
+
// Template literals
|
|
1935
|
+
if (ch === '`') {
|
|
1936
|
+
const parts = []; // alternating: string, expr, string, expr, ...
|
|
1937
|
+
let str = '';
|
|
1938
|
+
i++;
|
|
1939
|
+
while (i < len && expr[i] !== '`') {
|
|
1940
|
+
if (expr[i] === '$' && i + 1 < len && expr[i + 1] === '{') {
|
|
1941
|
+
parts.push(str);
|
|
1942
|
+
str = '';
|
|
1943
|
+
i += 2;
|
|
1944
|
+
let depth = 1;
|
|
1945
|
+
let inner = '';
|
|
1946
|
+
while (i < len && depth > 0) {
|
|
1947
|
+
if (expr[i] === '{') depth++;
|
|
1948
|
+
else if (expr[i] === '}') { depth--; if (depth === 0) break; }
|
|
1949
|
+
inner += expr[i++];
|
|
2034
1950
|
}
|
|
1951
|
+
i++; // closing }
|
|
1952
|
+
parts.push({ expr: inner });
|
|
2035
1953
|
} else {
|
|
2036
|
-
|
|
1954
|
+
if (expr[i] === '\\' && i + 1 < len) { str += expr[++i]; }
|
|
1955
|
+
else str += expr[i];
|
|
1956
|
+
i++;
|
|
2037
1957
|
}
|
|
2038
1958
|
}
|
|
2039
|
-
|
|
1959
|
+
i++; // closing backtick
|
|
1960
|
+
parts.push(str);
|
|
1961
|
+
tokens.push({ t: T.TMPL, v: parts });
|
|
1962
|
+
continue;
|
|
2040
1963
|
}
|
|
2041
1964
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
2049
|
-
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
2050
|
-
};
|
|
1965
|
+
// Identifiers & keywords
|
|
1966
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
1967
|
+
let ident = '';
|
|
1968
|
+
while (i < len && /[\w$]/.test(expr[i])) ident += expr[i++];
|
|
1969
|
+
tokens.push({ t: T.IDENT, v: ident });
|
|
1970
|
+
continue;
|
|
2051
1971
|
}
|
|
2052
1972
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
const result = [];
|
|
2063
|
-
for (const a of argNodes) {
|
|
2064
|
-
if (a.type === 'spread') {
|
|
2065
|
-
const iterable = evaluate(a.arg, scope);
|
|
2066
|
-
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2067
|
-
for (const v of iterable) result.push(v);
|
|
1973
|
+
// Multi-char operators
|
|
1974
|
+
const two = expr.slice(i, i + 3);
|
|
1975
|
+
if (two === '===' || two === '!==' || two === '?.') {
|
|
1976
|
+
if (two === '?.') {
|
|
1977
|
+
tokens.push({ t: T.OP, v: '?.' });
|
|
1978
|
+
i += 2;
|
|
1979
|
+
} else {
|
|
1980
|
+
tokens.push({ t: T.OP, v: two });
|
|
1981
|
+
i += 3;
|
|
2068
1982
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
const pair = expr.slice(i, i + 2);
|
|
1986
|
+
if (pair === '==' || pair === '!=' || pair === '<=' || pair === '>=' ||
|
|
1987
|
+
pair === '&&' || pair === '||' || pair === '??' || pair === '?.' ||
|
|
1988
|
+
pair === '=>') {
|
|
1989
|
+
tokens.push({ t: T.OP, v: pair });
|
|
1990
|
+
i += 2;
|
|
1991
|
+
continue;
|
|
2071
1992
|
}
|
|
2072
|
-
}
|
|
2073
|
-
return result;
|
|
2074
|
-
}
|
|
2075
1993
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
1994
|
+
// Single char operators and punctuation
|
|
1995
|
+
if ('+-*/%'.includes(ch)) {
|
|
1996
|
+
tokens.push({ t: T.OP, v: ch });
|
|
1997
|
+
i++; continue;
|
|
1998
|
+
}
|
|
1999
|
+
if ('<>=!'.includes(ch)) {
|
|
2000
|
+
tokens.push({ t: T.OP, v: ch });
|
|
2001
|
+
i++; continue;
|
|
2002
|
+
}
|
|
2003
|
+
// Spread operator: ...
|
|
2004
|
+
if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
|
|
2005
|
+
tokens.push({ t: T.OP, v: '...' });
|
|
2006
|
+
i += 3; continue;
|
|
2007
|
+
}
|
|
2008
|
+
if ('()[]{},.?:'.includes(ch)) {
|
|
2009
|
+
tokens.push({ t: T.PUNC, v: ch });
|
|
2010
|
+
i++; continue;
|
|
2011
|
+
}
|
|
2082
2012
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const obj = evaluate(callee.obj, scope);
|
|
2086
|
-
if (obj == null) return undefined;
|
|
2087
|
-
const prop = callee.computed ? evaluate(callee.prop, scope) : callee.prop;
|
|
2088
|
-
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2089
|
-
const fn = obj[prop];
|
|
2090
|
-
if (typeof fn !== 'function') return undefined;
|
|
2091
|
-
return fn.apply(obj, args);
|
|
2013
|
+
// Unknown — skip
|
|
2014
|
+
i++;
|
|
2092
2015
|
}
|
|
2093
2016
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (typeof fn !== 'function') return undefined;
|
|
2097
|
-
return fn(...args);
|
|
2017
|
+
tokens.push({ t: T.EOF, v: null });
|
|
2018
|
+
return tokens;
|
|
2098
2019
|
}
|
|
2099
2020
|
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
}
|
|
2109
|
-
if (node.op === '||') {
|
|
2110
|
-
const left = evaluate(node.left, scope);
|
|
2111
|
-
return left ? left : evaluate(node.right, scope);
|
|
2112
|
-
}
|
|
2113
|
-
if (node.op === '??') {
|
|
2114
|
-
const left = evaluate(node.left, scope);
|
|
2115
|
-
return left != null ? left : evaluate(node.right, scope);
|
|
2021
|
+
// ---------------------------------------------------------------------------
|
|
2022
|
+
// Parser — Pratt (precedence climbing)
|
|
2023
|
+
// ---------------------------------------------------------------------------
|
|
2024
|
+
class Parser {
|
|
2025
|
+
constructor(tokens, scope) {
|
|
2026
|
+
this.tokens = tokens;
|
|
2027
|
+
this.pos = 0;
|
|
2028
|
+
this.scope = scope;
|
|
2116
2029
|
}
|
|
2117
2030
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2031
|
+
peek() { return this.tokens[this.pos]; }
|
|
2032
|
+
next() { return this.tokens[this.pos++]; }
|
|
2120
2033
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
case '==': return left == right;
|
|
2128
|
-
case '!=': return left != right;
|
|
2129
|
-
case '===': return left === right;
|
|
2130
|
-
case '!==': return left !== right;
|
|
2131
|
-
case '<': return left < right;
|
|
2132
|
-
case '>': return left > right;
|
|
2133
|
-
case '<=': return left <= right;
|
|
2134
|
-
case '>=': return left >= right;
|
|
2135
|
-
case 'instanceof': return left instanceof right;
|
|
2136
|
-
case 'in': return left in right;
|
|
2137
|
-
default: return undefined;
|
|
2034
|
+
expect(type, val) {
|
|
2035
|
+
const t = this.next();
|
|
2036
|
+
if (t.t !== type || (val !== undefined && t.v !== val)) {
|
|
2037
|
+
throw new Error(`Expected ${val || type} but got ${t.v}`);
|
|
2038
|
+
}
|
|
2039
|
+
return t;
|
|
2138
2040
|
}
|
|
2139
|
-
}
|
|
2140
2041
|
|
|
2042
|
+
match(type, val) {
|
|
2043
|
+
const t = this.peek();
|
|
2044
|
+
if (t.t === type && (val === undefined || t.v === val)) {
|
|
2045
|
+
return this.next();
|
|
2046
|
+
}
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2141
2049
|
|
|
2142
|
-
//
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
* Safely evaluate a JS expression string against scope layers.
|
|
2148
|
-
*
|
|
2149
|
-
* @param {string} expr — expression string
|
|
2150
|
-
* @param {object[]} scope — array of scope objects, checked in order
|
|
2151
|
-
* Typical: [loopVars, state, { props, refs, $ }]
|
|
2152
|
-
* @returns {*} — evaluation result, or undefined on error
|
|
2153
|
-
*/
|
|
2050
|
+
// Main entry
|
|
2051
|
+
parse() {
|
|
2052
|
+
const result = this.parseExpression(0);
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2154
2055
|
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const _astCache = new Map();
|
|
2159
|
-
const _AST_CACHE_MAX = 512;
|
|
2056
|
+
// Precedence climbing
|
|
2057
|
+
parseExpression(minPrec) {
|
|
2058
|
+
let left = this.parseUnary();
|
|
2160
2059
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
const trimmed = expr.trim();
|
|
2164
|
-
if (!trimmed) return undefined;
|
|
2060
|
+
while (true) {
|
|
2061
|
+
const tok = this.peek();
|
|
2165
2062
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2063
|
+
// Ternary
|
|
2064
|
+
if (tok.t === T.PUNC && tok.v === '?') {
|
|
2065
|
+
// Distinguish ternary ? from optional chaining ?.
|
|
2066
|
+
if (this.tokens[this.pos + 1]?.v !== '.') {
|
|
2067
|
+
if (1 <= minPrec) break; // ternary has very low precedence
|
|
2068
|
+
this.next(); // consume ?
|
|
2069
|
+
const truthy = this.parseExpression(0);
|
|
2070
|
+
this.expect(T.PUNC, ':');
|
|
2071
|
+
const falsy = this.parseExpression(0);
|
|
2072
|
+
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
2073
|
+
continue;
|
|
2172
2074
|
}
|
|
2173
2075
|
}
|
|
2174
|
-
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2175
|
-
}
|
|
2176
2076
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2077
|
+
// Binary operators
|
|
2078
|
+
if (tok.t === T.OP && tok.v in PREC) {
|
|
2079
|
+
const prec = PREC[tok.v];
|
|
2080
|
+
if (prec <= minPrec) break;
|
|
2081
|
+
this.next();
|
|
2082
|
+
const right = this.parseExpression(prec);
|
|
2083
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2186
2086
|
|
|
2187
|
-
//
|
|
2188
|
-
if (
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2087
|
+
// instanceof and in as binary operators
|
|
2088
|
+
if (tok.t === T.IDENT && (tok.v === 'instanceof' || tok.v === 'in') && PREC[tok.v] > minPrec) {
|
|
2089
|
+
const prec = PREC[tok.v];
|
|
2090
|
+
this.next();
|
|
2091
|
+
const right = this.parseExpression(prec);
|
|
2092
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
2093
|
+
continue;
|
|
2191
2094
|
}
|
|
2192
|
-
_astCache.set(trimmed, ast);
|
|
2193
|
-
}
|
|
2194
2095
|
|
|
2195
|
-
|
|
2196
|
-
} catch (err) {
|
|
2197
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
2198
|
-
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
2096
|
+
break;
|
|
2199
2097
|
}
|
|
2200
|
-
|
|
2098
|
+
|
|
2099
|
+
return left;
|
|
2201
2100
|
}
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
// --- src/diff.js -------------------------------------------------
|
|
2205
|
-
/**
|
|
2206
|
-
* zQuery Diff — Lightweight DOM morphing engine
|
|
2207
|
-
*
|
|
2208
|
-
* Patches an existing DOM tree to match new HTML without destroying nodes
|
|
2209
|
-
* that haven't changed. Preserves focus, scroll positions, third-party
|
|
2210
|
-
* widget state, video playback, and other live DOM state.
|
|
2211
|
-
*
|
|
2212
|
-
* Approach: walk old and new trees in parallel, reconcile node by node.
|
|
2213
|
-
* Keyed elements (via `z-key`) get matched across position changes.
|
|
2214
|
-
*
|
|
2215
|
-
* Performance advantages over virtual DOM (React/Angular):
|
|
2216
|
-
* - No virtual tree allocation or diffing — works directly on real DOM
|
|
2217
|
-
* - Skips unchanged subtrees via fast isEqualNode() check
|
|
2218
|
-
* - z-skip attribute to opt out of diffing entire subtrees
|
|
2219
|
-
* - Reuses a single template element for HTML parsing (zero GC pressure)
|
|
2220
|
-
* - Keyed reconciliation uses LIS (Longest Increasing Subsequence) to
|
|
2221
|
-
* minimize DOM moves — same algorithm as Vue 3 / ivi
|
|
2222
|
-
* - Minimal attribute diffing with early bail-out
|
|
2223
|
-
*/
|
|
2224
2101
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
// ---------------------------------------------------------------------------
|
|
2228
|
-
let _tpl = null;
|
|
2102
|
+
parseUnary() {
|
|
2103
|
+
const tok = this.peek();
|
|
2229
2104
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2105
|
+
// typeof
|
|
2106
|
+
if (tok.t === T.IDENT && tok.v === 'typeof') {
|
|
2107
|
+
this.next();
|
|
2108
|
+
const arg = this.parseUnary();
|
|
2109
|
+
return { type: 'typeof', arg };
|
|
2110
|
+
}
|
|
2234
2111
|
|
|
2235
|
-
//
|
|
2236
|
-
|
|
2237
|
-
|
|
2112
|
+
// void
|
|
2113
|
+
if (tok.t === T.IDENT && tok.v === 'void') {
|
|
2114
|
+
this.next();
|
|
2115
|
+
this.parseUnary(); // evaluate but discard
|
|
2116
|
+
return { type: 'literal', value: undefined };
|
|
2117
|
+
}
|
|
2238
2118
|
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
*/
|
|
2246
|
-
function morph(rootEl, newHTML) {
|
|
2247
|
-
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
2248
|
-
const tpl = _getTemplate();
|
|
2249
|
-
tpl.innerHTML = newHTML;
|
|
2250
|
-
const newRoot = tpl.content;
|
|
2119
|
+
// !expr
|
|
2120
|
+
if (tok.t === T.OP && tok.v === '!') {
|
|
2121
|
+
this.next();
|
|
2122
|
+
const arg = this.parseUnary();
|
|
2123
|
+
return { type: 'not', arg };
|
|
2124
|
+
}
|
|
2251
2125
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2126
|
+
// -expr, +expr
|
|
2127
|
+
if (tok.t === T.OP && (tok.v === '-' || tok.v === '+')) {
|
|
2128
|
+
this.next();
|
|
2129
|
+
const arg = this.parseUnary();
|
|
2130
|
+
return { type: 'unary', op: tok.v, arg };
|
|
2131
|
+
}
|
|
2256
2132
|
|
|
2257
|
-
|
|
2133
|
+
return this.parsePostfix();
|
|
2134
|
+
}
|
|
2258
2135
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2136
|
+
parsePostfix() {
|
|
2137
|
+
let left = this.parsePrimary();
|
|
2261
2138
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
* without replacing the node reference. Useful for replaceWith-style
|
|
2265
|
-
* updates where you want to keep the element identity when the tag
|
|
2266
|
-
* name matches.
|
|
2267
|
-
*
|
|
2268
|
-
* If the new HTML produces a different tag, falls back to native replace.
|
|
2269
|
-
*
|
|
2270
|
-
* @param {Element} oldEl — The live DOM element to patch
|
|
2271
|
-
* @param {string} newHTML — HTML string for the replacement element
|
|
2272
|
-
* @returns {Element} — The resulting element (same ref if morphed, new if replaced)
|
|
2273
|
-
*/
|
|
2274
|
-
function morphElement(oldEl, newHTML) {
|
|
2275
|
-
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
2276
|
-
const tpl = _getTemplate();
|
|
2277
|
-
tpl.innerHTML = newHTML;
|
|
2278
|
-
const newEl = tpl.content.firstElementChild;
|
|
2279
|
-
if (!newEl) return oldEl;
|
|
2139
|
+
while (true) {
|
|
2140
|
+
const tok = this.peek();
|
|
2280
2141
|
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2142
|
+
// Property access: a.b
|
|
2143
|
+
if (tok.t === T.PUNC && tok.v === '.') {
|
|
2144
|
+
this.next();
|
|
2145
|
+
const prop = this.next();
|
|
2146
|
+
left = { type: 'member', obj: left, prop: prop.v, computed: false };
|
|
2147
|
+
// Check for method call: a.b()
|
|
2148
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2149
|
+
left = this._parseCall(left);
|
|
2150
|
+
}
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2288
2153
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2154
|
+
// Optional chaining: a?.b, a?.[b], a?.()
|
|
2155
|
+
if (tok.t === T.OP && tok.v === '?.') {
|
|
2156
|
+
this.next();
|
|
2157
|
+
const next = this.peek();
|
|
2158
|
+
if (next.t === T.PUNC && next.v === '[') {
|
|
2159
|
+
// a?.[expr]
|
|
2160
|
+
this.next();
|
|
2161
|
+
const prop = this.parseExpression(0);
|
|
2162
|
+
this.expect(T.PUNC, ']');
|
|
2163
|
+
left = { type: 'optional_member', obj: left, prop, computed: true };
|
|
2164
|
+
} else if (next.t === T.PUNC && next.v === '(') {
|
|
2165
|
+
// a?.()
|
|
2166
|
+
left = { type: 'optional_call', callee: left, args: this._parseArgs() };
|
|
2167
|
+
} else {
|
|
2168
|
+
// a?.b
|
|
2169
|
+
const prop = this.next();
|
|
2170
|
+
left = { type: 'optional_member', obj: left, prop: prop.v, computed: false };
|
|
2171
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2172
|
+
left = this._parseCall(left);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2295
2177
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2178
|
+
// Computed access: a[b]
|
|
2179
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
2180
|
+
this.next();
|
|
2181
|
+
const prop = this.parseExpression(0);
|
|
2182
|
+
this.expect(T.PUNC, ']');
|
|
2183
|
+
left = { type: 'member', obj: left, prop, computed: true };
|
|
2184
|
+
// Check for method call: a[b]()
|
|
2185
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2186
|
+
left = this._parseCall(left);
|
|
2187
|
+
}
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// Function call: fn()
|
|
2192
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
2193
|
+
left = this._parseCall(left);
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2314
2196
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
let oldKeyMap, newKeyMap;
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2318
2199
|
|
|
2319
|
-
|
|
2320
|
-
if (_getKey(oldChildren[i]) != null) { hasKeys = true; break; }
|
|
2200
|
+
return left;
|
|
2321
2201
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2202
|
+
|
|
2203
|
+
_parseCall(callee) {
|
|
2204
|
+
const args = this._parseArgs();
|
|
2205
|
+
return { type: 'call', callee, args };
|
|
2326
2206
|
}
|
|
2327
2207
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2208
|
+
_parseArgs() {
|
|
2209
|
+
this.expect(T.PUNC, '(');
|
|
2210
|
+
const args = [];
|
|
2211
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
2212
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
2213
|
+
this.next();
|
|
2214
|
+
args.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
2215
|
+
} else {
|
|
2216
|
+
args.push(this.parseExpression(0));
|
|
2217
|
+
}
|
|
2218
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2338
2219
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
_morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
|
|
2220
|
+
this.expect(T.PUNC, ')');
|
|
2221
|
+
return args;
|
|
2342
2222
|
}
|
|
2343
|
-
}
|
|
2344
2223
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
*/
|
|
2348
|
-
function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
2349
|
-
const oldLen = oldChildren.length;
|
|
2350
|
-
const newLen = newChildren.length;
|
|
2351
|
-
const minLen = oldLen < newLen ? oldLen : newLen;
|
|
2224
|
+
parsePrimary() {
|
|
2225
|
+
const tok = this.peek();
|
|
2352
2226
|
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2227
|
+
// Number literal
|
|
2228
|
+
if (tok.t === T.NUM) {
|
|
2229
|
+
this.next();
|
|
2230
|
+
return { type: 'literal', value: tok.v };
|
|
2231
|
+
}
|
|
2357
2232
|
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2233
|
+
// String literal
|
|
2234
|
+
if (tok.t === T.STR) {
|
|
2235
|
+
this.next();
|
|
2236
|
+
return { type: 'literal', value: tok.v };
|
|
2362
2237
|
}
|
|
2363
|
-
}
|
|
2364
2238
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2239
|
+
// Template literal
|
|
2240
|
+
if (tok.t === T.TMPL) {
|
|
2241
|
+
this.next();
|
|
2242
|
+
return { type: 'template', parts: tok.v };
|
|
2369
2243
|
}
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
2244
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
const consumed = new Set();
|
|
2381
|
-
const newLen = newChildren.length;
|
|
2382
|
-
const matched = new Array(newLen);
|
|
2245
|
+
// Arrow function with parens: () =>, (a) =>, (a, b) =>
|
|
2246
|
+
// or regular grouping: (expr)
|
|
2247
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
2248
|
+
const savedPos = this.pos;
|
|
2249
|
+
this.next(); // consume (
|
|
2250
|
+
const params = [];
|
|
2251
|
+
let couldBeArrow = true;
|
|
2383
2252
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2253
|
+
if (this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
2254
|
+
// () => ... — no params
|
|
2255
|
+
} else {
|
|
2256
|
+
while (couldBeArrow) {
|
|
2257
|
+
const p = this.peek();
|
|
2258
|
+
if (p.t === T.IDENT && !KEYWORDS.has(p.v)) {
|
|
2259
|
+
params.push(this.next().v);
|
|
2260
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') {
|
|
2261
|
+
this.next();
|
|
2262
|
+
} else {
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
2265
|
+
} else {
|
|
2266
|
+
couldBeArrow = false;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2395
2270
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2271
|
+
if (couldBeArrow && this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
2272
|
+
this.next(); // consume )
|
|
2273
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
2274
|
+
this.next(); // consume =>
|
|
2275
|
+
const body = this.parseExpression(0);
|
|
2276
|
+
return { type: 'arrow', params, body };
|
|
2277
|
+
}
|
|
2402
2278
|
}
|
|
2279
|
+
|
|
2280
|
+
// Not an arrow — restore and parse as grouping
|
|
2281
|
+
this.pos = savedPos;
|
|
2282
|
+
this.next(); // consume (
|
|
2283
|
+
const expr = this.parseExpression(0);
|
|
2284
|
+
this.expect(T.PUNC, ')');
|
|
2285
|
+
return expr;
|
|
2403
2286
|
}
|
|
2404
|
-
}
|
|
2405
2287
|
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2288
|
+
// Array literal
|
|
2289
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
2290
|
+
this.next();
|
|
2291
|
+
const elements = [];
|
|
2292
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
2293
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
2294
|
+
this.next();
|
|
2295
|
+
elements.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
2296
|
+
} else {
|
|
2297
|
+
elements.push(this.parseExpression(0));
|
|
2298
|
+
}
|
|
2299
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2300
|
+
}
|
|
2301
|
+
this.expect(T.PUNC, ']');
|
|
2302
|
+
return { type: 'array', elements };
|
|
2416
2303
|
}
|
|
2417
|
-
}
|
|
2418
2304
|
|
|
2419
|
-
|
|
2305
|
+
// Object literal
|
|
2306
|
+
if (tok.t === T.PUNC && tok.v === '{') {
|
|
2307
|
+
this.next();
|
|
2308
|
+
const properties = [];
|
|
2309
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
2310
|
+
// Spread in object: { ...obj }
|
|
2311
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
2312
|
+
this.next();
|
|
2313
|
+
properties.push({ spread: true, value: this.parseExpression(0) });
|
|
2314
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2420
2317
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2318
|
+
const keyTok = this.next();
|
|
2319
|
+
let key;
|
|
2320
|
+
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
2321
|
+
else if (keyTok.t === T.NUM) key = String(keyTok.v);
|
|
2322
|
+
else throw new Error('Invalid object key: ' + keyTok.v);
|
|
2323
|
+
|
|
2324
|
+
// Shorthand property: { foo } means { foo: foo }
|
|
2325
|
+
if (this.peek().t === T.PUNC && (this.peek().v === ',' || this.peek().v === '}')) {
|
|
2326
|
+
properties.push({ key, value: { type: 'ident', name: key } });
|
|
2327
|
+
} else {
|
|
2328
|
+
this.expect(T.PUNC, ':');
|
|
2329
|
+
properties.push({ key, value: this.parseExpression(0) });
|
|
2330
|
+
}
|
|
2331
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2332
|
+
}
|
|
2333
|
+
this.expect(T.PUNC, '}');
|
|
2334
|
+
return { type: 'object', properties };
|
|
2428
2335
|
}
|
|
2429
|
-
}
|
|
2430
|
-
let unkeyedIdx = 0;
|
|
2431
2336
|
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
let oldNode = matched[i];
|
|
2337
|
+
// Identifiers & keywords
|
|
2338
|
+
if (tok.t === T.IDENT) {
|
|
2339
|
+
this.next();
|
|
2436
2340
|
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2341
|
+
// Keywords
|
|
2342
|
+
if (tok.v === 'true') return { type: 'literal', value: true };
|
|
2343
|
+
if (tok.v === 'false') return { type: 'literal', value: false };
|
|
2344
|
+
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
2345
|
+
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
2440
2346
|
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
oldParent.insertBefore(clone, cursor);
|
|
2456
|
-
} else {
|
|
2457
|
-
oldParent.appendChild(clone);
|
|
2347
|
+
// new keyword
|
|
2348
|
+
if (tok.v === 'new') {
|
|
2349
|
+
let classExpr = this.parsePrimary();
|
|
2350
|
+
// Handle member access (e.g. ns.MyClass) without consuming call args
|
|
2351
|
+
while (this.peek().t === T.PUNC && this.peek().v === '.') {
|
|
2352
|
+
this.next();
|
|
2353
|
+
const prop = this.next();
|
|
2354
|
+
classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
|
|
2355
|
+
}
|
|
2356
|
+
let args = [];
|
|
2357
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2358
|
+
args = this._parseArgs();
|
|
2359
|
+
}
|
|
2360
|
+
return { type: 'new', callee: classExpr, args };
|
|
2458
2361
|
}
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
// Remove remaining unkeyed old nodes
|
|
2463
|
-
while (unkeyedIdx < unkeyedOld.length) {
|
|
2464
|
-
const leftover = unkeyedOld[unkeyedIdx++];
|
|
2465
|
-
if (leftover.parentNode === oldParent) {
|
|
2466
|
-
oldParent.removeChild(leftover);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
2362
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
oldParent.removeChild(node);
|
|
2363
|
+
// Arrow function: x => expr
|
|
2364
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
2365
|
+
this.next(); // consume =>
|
|
2366
|
+
const body = this.parseExpression(0);
|
|
2367
|
+
return { type: 'arrow', params: [tok.v], body };
|
|
2476
2368
|
}
|
|
2369
|
+
|
|
2370
|
+
return { type: 'ident', name: tok.v };
|
|
2477
2371
|
}
|
|
2372
|
+
|
|
2373
|
+
// Fallback — return undefined for unparseable
|
|
2374
|
+
this.next();
|
|
2375
|
+
return { type: 'literal', value: undefined };
|
|
2478
2376
|
}
|
|
2479
2377
|
}
|
|
2480
2378
|
|
|
2379
|
+
// ---------------------------------------------------------------------------
|
|
2380
|
+
// Evaluator — walks the AST, resolves against scope
|
|
2381
|
+
// ---------------------------------------------------------------------------
|
|
2382
|
+
|
|
2383
|
+
/** Safe property access whitelist for built-in prototypes */
|
|
2384
|
+
const SAFE_ARRAY_METHODS = new Set([
|
|
2385
|
+
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
2386
|
+
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
2387
|
+
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
2388
|
+
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
2389
|
+
]);
|
|
2390
|
+
|
|
2391
|
+
const SAFE_STRING_METHODS = new Set([
|
|
2392
|
+
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
2393
|
+
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
2394
|
+
'toUpperCase', 'split', 'replace', 'replaceAll', 'match', 'search',
|
|
2395
|
+
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'at',
|
|
2396
|
+
'toString', 'valueOf',
|
|
2397
|
+
]);
|
|
2398
|
+
|
|
2399
|
+
const SAFE_NUMBER_METHODS = new Set([
|
|
2400
|
+
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
2401
|
+
]);
|
|
2402
|
+
|
|
2403
|
+
const SAFE_OBJECT_METHODS = new Set([
|
|
2404
|
+
'hasOwnProperty', 'toString', 'valueOf',
|
|
2405
|
+
]);
|
|
2406
|
+
|
|
2407
|
+
const SAFE_MATH_PROPS = new Set([
|
|
2408
|
+
'PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2',
|
|
2409
|
+
'abs', 'ceil', 'floor', 'round', 'trunc', 'max', 'min', 'pow',
|
|
2410
|
+
'sqrt', 'sign', 'random', 'log', 'log2', 'log10',
|
|
2411
|
+
]);
|
|
2412
|
+
|
|
2413
|
+
const SAFE_JSON_PROPS = new Set(['parse', 'stringify']);
|
|
2414
|
+
|
|
2481
2415
|
/**
|
|
2482
|
-
*
|
|
2483
|
-
* Returns a Set of positions (in the input) that form the LIS.
|
|
2484
|
-
* Entries with value -1 (unmatched) are excluded.
|
|
2485
|
-
*
|
|
2486
|
-
* O(n log n) — same algorithm used by Vue 3 and ivi.
|
|
2487
|
-
*
|
|
2488
|
-
* @param {number[]} arr — array of old-tree indices (-1 = unmatched)
|
|
2489
|
-
* @returns {Set<number>} — positions in arr belonging to the LIS
|
|
2416
|
+
* Check if property access is safe
|
|
2490
2417
|
*/
|
|
2491
|
-
function
|
|
2492
|
-
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2418
|
+
function _isSafeAccess(obj, prop) {
|
|
2419
|
+
// Never allow access to dangerous properties
|
|
2420
|
+
const BLOCKED = new Set([
|
|
2421
|
+
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
2422
|
+
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
2423
|
+
'call', 'apply', 'bind',
|
|
2424
|
+
]);
|
|
2425
|
+
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
2495
2426
|
|
|
2496
|
-
//
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2427
|
+
// Always allow plain object/function property access and array index access
|
|
2428
|
+
if (obj !== null && obj !== undefined && (typeof obj === 'object' || typeof obj === 'function')) return true;
|
|
2429
|
+
if (typeof obj === 'string') return SAFE_STRING_METHODS.has(prop);
|
|
2430
|
+
if (typeof obj === 'number') return SAFE_NUMBER_METHODS.has(prop);
|
|
2431
|
+
return false;
|
|
2432
|
+
}
|
|
2501
2433
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const val = arr[i];
|
|
2434
|
+
function evaluate(node, scope) {
|
|
2435
|
+
if (!node) return undefined;
|
|
2505
2436
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2437
|
+
switch (node.type) {
|
|
2438
|
+
case 'literal':
|
|
2439
|
+
return node.value;
|
|
2440
|
+
|
|
2441
|
+
case 'ident': {
|
|
2442
|
+
const name = node.name;
|
|
2443
|
+
// Check scope layers in order
|
|
2444
|
+
for (const layer of scope) {
|
|
2445
|
+
if (layer && typeof layer === 'object' && name in layer) {
|
|
2446
|
+
return layer[name];
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
// Built-in globals (safe ones only)
|
|
2450
|
+
if (name === 'Math') return Math;
|
|
2451
|
+
if (name === 'JSON') return JSON;
|
|
2452
|
+
if (name === 'Date') return Date;
|
|
2453
|
+
if (name === 'Array') return Array;
|
|
2454
|
+
if (name === 'Object') return Object;
|
|
2455
|
+
if (name === 'String') return String;
|
|
2456
|
+
if (name === 'Number') return Number;
|
|
2457
|
+
if (name === 'Boolean') return Boolean;
|
|
2458
|
+
if (name === 'parseInt') return parseInt;
|
|
2459
|
+
if (name === 'parseFloat') return parseFloat;
|
|
2460
|
+
if (name === 'isNaN') return isNaN;
|
|
2461
|
+
if (name === 'isFinite') return isFinite;
|
|
2462
|
+
if (name === 'Infinity') return Infinity;
|
|
2463
|
+
if (name === 'NaN') return NaN;
|
|
2464
|
+
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
2465
|
+
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
2466
|
+
if (name === 'console') return console;
|
|
2467
|
+
if (name === 'Map') return Map;
|
|
2468
|
+
if (name === 'Set') return Set;
|
|
2469
|
+
if (name === 'RegExp') return RegExp;
|
|
2470
|
+
if (name === 'Error') return Error;
|
|
2471
|
+
if (name === 'URL') return URL;
|
|
2472
|
+
if (name === 'URLSearchParams') return URLSearchParams;
|
|
2473
|
+
return undefined;
|
|
2512
2474
|
}
|
|
2513
2475
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2476
|
+
case 'template': {
|
|
2477
|
+
// Template literal with interpolation
|
|
2478
|
+
let result = '';
|
|
2479
|
+
for (const part of node.parts) {
|
|
2480
|
+
if (typeof part === 'string') {
|
|
2481
|
+
result += part;
|
|
2482
|
+
} else if (part && part.expr) {
|
|
2483
|
+
result += String(safeEval(part.expr, scope) ?? '');
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return result;
|
|
2487
|
+
}
|
|
2518
2488
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2489
|
+
case 'member': {
|
|
2490
|
+
const obj = evaluate(node.obj, scope);
|
|
2491
|
+
if (obj == null) return undefined;
|
|
2492
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
2493
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2494
|
+
return obj[prop];
|
|
2495
|
+
}
|
|
2525
2496
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2497
|
+
case 'optional_member': {
|
|
2498
|
+
const obj = evaluate(node.obj, scope);
|
|
2499
|
+
if (obj == null) return undefined;
|
|
2500
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
2501
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2502
|
+
return obj[prop];
|
|
2503
|
+
}
|
|
2528
2504
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2505
|
+
case 'call': {
|
|
2506
|
+
const result = _resolveCall(node, scope, false);
|
|
2507
|
+
return result;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
case 'optional_call': {
|
|
2511
|
+
const calleeNode = node.callee;
|
|
2512
|
+
const args = _evalArgs(node.args, scope);
|
|
2513
|
+
// Method call: obj?.method() — bind `this` to obj
|
|
2514
|
+
if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
|
|
2515
|
+
const obj = evaluate(calleeNode.obj, scope);
|
|
2516
|
+
if (obj == null) return undefined;
|
|
2517
|
+
const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
|
|
2518
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2519
|
+
const fn = obj[prop];
|
|
2520
|
+
if (typeof fn !== 'function') return undefined;
|
|
2521
|
+
return fn.apply(obj, args);
|
|
2538
2522
|
}
|
|
2539
|
-
|
|
2523
|
+
const callee = evaluate(calleeNode, scope);
|
|
2524
|
+
if (callee == null) return undefined;
|
|
2525
|
+
if (typeof callee !== 'function') return undefined;
|
|
2526
|
+
return callee(...args);
|
|
2540
2527
|
}
|
|
2541
|
-
// Different node types — replace
|
|
2542
|
-
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
2528
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2529
|
+
case 'new': {
|
|
2530
|
+
const Ctor = evaluate(node.callee, scope);
|
|
2531
|
+
if (typeof Ctor !== 'function') return undefined;
|
|
2532
|
+
// Only allow safe constructors
|
|
2533
|
+
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
2534
|
+
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
2535
|
+
const args = _evalArgs(node.args, scope);
|
|
2536
|
+
return new Ctor(...args);
|
|
2537
|
+
}
|
|
2538
|
+
return undefined;
|
|
2539
|
+
}
|
|
2552
2540
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
// z-skip: developer opt-out — skip diffing this subtree entirely.
|
|
2556
|
-
// Useful for third-party widgets, canvas, video, or large static content.
|
|
2557
|
-
if (oldNode.hasAttribute('z-skip')) return;
|
|
2541
|
+
case 'binary':
|
|
2542
|
+
return _evalBinary(node, scope);
|
|
2558
2543
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2544
|
+
case 'unary': {
|
|
2545
|
+
const val = evaluate(node.arg, scope);
|
|
2546
|
+
return node.op === '-' ? -val : +val;
|
|
2547
|
+
}
|
|
2563
2548
|
|
|
2564
|
-
|
|
2549
|
+
case 'not':
|
|
2550
|
+
return !evaluate(node.arg, scope);
|
|
2551
|
+
|
|
2552
|
+
case 'typeof': {
|
|
2553
|
+
try {
|
|
2554
|
+
return typeof evaluate(node.arg, scope);
|
|
2555
|
+
} catch {
|
|
2556
|
+
return 'undefined';
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2565
2559
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
_syncInputValue(oldNode, newNode);
|
|
2570
|
-
return;
|
|
2560
|
+
case 'ternary': {
|
|
2561
|
+
const cond = evaluate(node.cond, scope);
|
|
2562
|
+
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
2571
2563
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2564
|
+
|
|
2565
|
+
case 'array': {
|
|
2566
|
+
const arr = [];
|
|
2567
|
+
for (const e of node.elements) {
|
|
2568
|
+
if (e.type === 'spread') {
|
|
2569
|
+
const iterable = evaluate(e.arg, scope);
|
|
2570
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2571
|
+
for (const v of iterable) arr.push(v);
|
|
2572
|
+
}
|
|
2573
|
+
} else {
|
|
2574
|
+
arr.push(evaluate(e, scope));
|
|
2575
|
+
}
|
|
2575
2576
|
}
|
|
2576
|
-
return;
|
|
2577
|
+
return arr;
|
|
2577
2578
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2579
|
+
|
|
2580
|
+
case 'object': {
|
|
2581
|
+
const obj = {};
|
|
2582
|
+
for (const prop of node.properties) {
|
|
2583
|
+
if (prop.spread) {
|
|
2584
|
+
const source = evaluate(prop.value, scope);
|
|
2585
|
+
if (source != null && typeof source === 'object') {
|
|
2586
|
+
Object.assign(obj, source);
|
|
2587
|
+
}
|
|
2588
|
+
} else {
|
|
2589
|
+
obj[prop.key] = evaluate(prop.value, scope);
|
|
2590
|
+
}
|
|
2582
2591
|
}
|
|
2583
|
-
return;
|
|
2592
|
+
return obj;
|
|
2584
2593
|
}
|
|
2585
2594
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2595
|
+
case 'arrow': {
|
|
2596
|
+
const paramNames = node.params;
|
|
2597
|
+
const bodyNode = node.body;
|
|
2598
|
+
const closedScope = scope;
|
|
2599
|
+
return function(...args) {
|
|
2600
|
+
const arrowScope = {};
|
|
2601
|
+
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
2602
|
+
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
default:
|
|
2607
|
+
return undefined;
|
|
2588
2608
|
}
|
|
2589
2609
|
}
|
|
2590
2610
|
|
|
2591
2611
|
/**
|
|
2592
|
-
*
|
|
2593
|
-
* Uses a single pass: build a set of new attribute names, iterate
|
|
2594
|
-
* old attrs for removals, then apply new attrs.
|
|
2612
|
+
* Evaluate a list of argument AST nodes, flattening any spread elements.
|
|
2595
2613
|
*/
|
|
2596
|
-
function
|
|
2597
|
-
const
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
if (newLen === oldLen) {
|
|
2604
|
-
let same = true;
|
|
2605
|
-
for (let i = 0; i < newLen; i++) {
|
|
2606
|
-
const na = newAttrs[i];
|
|
2607
|
-
if (oldEl.getAttribute(na.name) !== na.value) { same = false; break; }
|
|
2608
|
-
}
|
|
2609
|
-
if (same) {
|
|
2610
|
-
// Also verify no extra old attrs (names mismatch)
|
|
2611
|
-
for (let i = 0; i < oldLen; i++) {
|
|
2612
|
-
if (!newEl.hasAttribute(oldAttrs[i].name)) { same = false; break; }
|
|
2614
|
+
function _evalArgs(argNodes, scope) {
|
|
2615
|
+
const result = [];
|
|
2616
|
+
for (const a of argNodes) {
|
|
2617
|
+
if (a.type === 'spread') {
|
|
2618
|
+
const iterable = evaluate(a.arg, scope);
|
|
2619
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2620
|
+
for (const v of iterable) result.push(v);
|
|
2613
2621
|
}
|
|
2622
|
+
} else {
|
|
2623
|
+
result.push(evaluate(a, scope));
|
|
2614
2624
|
}
|
|
2615
|
-
if (same) return;
|
|
2616
2625
|
}
|
|
2626
|
+
return result;
|
|
2627
|
+
}
|
|
2617
2628
|
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
oldEl.setAttribute(attr.name, attr.value);
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2629
|
+
/**
|
|
2630
|
+
* Resolve and execute a function call safely.
|
|
2631
|
+
*/
|
|
2632
|
+
function _resolveCall(node, scope) {
|
|
2633
|
+
const callee = node.callee;
|
|
2634
|
+
const args = _evalArgs(node.args, scope);
|
|
2627
2635
|
|
|
2628
|
-
//
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (!
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
+
// Method call: obj.method() — bind `this` to obj
|
|
2637
|
+
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
2638
|
+
const obj = evaluate(callee.obj, scope);
|
|
2639
|
+
if (obj == null) return undefined;
|
|
2640
|
+
const prop = callee.computed ? evaluate(callee.prop, scope) : callee.prop;
|
|
2641
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2642
|
+
const fn = obj[prop];
|
|
2643
|
+
if (typeof fn !== 'function') return undefined;
|
|
2644
|
+
return fn.apply(obj, args);
|
|
2636
2645
|
}
|
|
2646
|
+
|
|
2647
|
+
// Direct call: fn(args)
|
|
2648
|
+
const fn = evaluate(callee, scope);
|
|
2649
|
+
if (typeof fn !== 'function') return undefined;
|
|
2650
|
+
return fn(...args);
|
|
2637
2651
|
}
|
|
2638
2652
|
|
|
2639
2653
|
/**
|
|
2640
|
-
*
|
|
2654
|
+
* Evaluate binary expression.
|
|
2641
2655
|
*/
|
|
2642
|
-
function
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2656
|
+
function _evalBinary(node, scope) {
|
|
2657
|
+
// Short-circuit for logical ops
|
|
2658
|
+
if (node.op === '&&') {
|
|
2659
|
+
const left = evaluate(node.left, scope);
|
|
2660
|
+
return left ? evaluate(node.right, scope) : left;
|
|
2661
|
+
}
|
|
2662
|
+
if (node.op === '||') {
|
|
2663
|
+
const left = evaluate(node.left, scope);
|
|
2664
|
+
return left ? left : evaluate(node.right, scope);
|
|
2665
|
+
}
|
|
2666
|
+
if (node.op === '??') {
|
|
2667
|
+
const left = evaluate(node.left, scope);
|
|
2668
|
+
return left != null ? left : evaluate(node.right, scope);
|
|
2651
2669
|
}
|
|
2652
2670
|
|
|
2653
|
-
|
|
2654
|
-
|
|
2671
|
+
const left = evaluate(node.left, scope);
|
|
2672
|
+
const right = evaluate(node.right, scope);
|
|
2673
|
+
|
|
2674
|
+
switch (node.op) {
|
|
2675
|
+
case '+': return left + right;
|
|
2676
|
+
case '-': return left - right;
|
|
2677
|
+
case '*': return left * right;
|
|
2678
|
+
case '/': return left / right;
|
|
2679
|
+
case '%': return left % right;
|
|
2680
|
+
case '==': return left == right;
|
|
2681
|
+
case '!=': return left != right;
|
|
2682
|
+
case '===': return left === right;
|
|
2683
|
+
case '!==': return left !== right;
|
|
2684
|
+
case '<': return left < right;
|
|
2685
|
+
case '>': return left > right;
|
|
2686
|
+
case '<=': return left <= right;
|
|
2687
|
+
case '>=': return left >= right;
|
|
2688
|
+
case 'instanceof': return left instanceof right;
|
|
2689
|
+
case 'in': return left in right;
|
|
2690
|
+
default: return undefined;
|
|
2691
|
+
}
|
|
2655
2692
|
}
|
|
2656
2693
|
|
|
2694
|
+
|
|
2695
|
+
// ---------------------------------------------------------------------------
|
|
2696
|
+
// Public API
|
|
2697
|
+
// ---------------------------------------------------------------------------
|
|
2698
|
+
|
|
2657
2699
|
/**
|
|
2658
|
-
*
|
|
2659
|
-
*
|
|
2660
|
-
* Priority: z-key attribute → id attribute → data-id / data-key.
|
|
2661
|
-
* Auto-detected keys use a `\0` prefix to avoid collisions with
|
|
2662
|
-
* explicit z-key values.
|
|
2663
|
-
*
|
|
2664
|
-
* This means the LIS-optimised keyed path activates automatically
|
|
2665
|
-
* whenever elements carry `id` or `data-id` / `data-key` attributes
|
|
2666
|
-
* — no extra markup required.
|
|
2700
|
+
* Safely evaluate a JS expression string against scope layers.
|
|
2667
2701
|
*
|
|
2668
|
-
* @
|
|
2702
|
+
* @param {string} expr — expression string
|
|
2703
|
+
* @param {object[]} scope — array of scope objects, checked in order
|
|
2704
|
+
* Typical: [loopVars, state, { props, refs, $ }]
|
|
2705
|
+
* @returns {*} — evaluation result, or undefined on error
|
|
2669
2706
|
*/
|
|
2670
|
-
function _getKey(node) {
|
|
2671
|
-
if (node.nodeType !== 1) return null;
|
|
2672
2707
|
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2708
|
+
// AST cache (LRU) — avoids re-tokenizing and re-parsing the same expression.
|
|
2709
|
+
// Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
|
|
2710
|
+
// Eviction removes the least-recently-used (first) entry when at capacity.
|
|
2711
|
+
const _astCache = new Map();
|
|
2712
|
+
const _AST_CACHE_MAX = 512;
|
|
2676
2713
|
|
|
2677
|
-
|
|
2678
|
-
|
|
2714
|
+
function safeEval(expr, scope) {
|
|
2715
|
+
try {
|
|
2716
|
+
const trimmed = expr.trim();
|
|
2717
|
+
if (!trimmed) return undefined;
|
|
2679
2718
|
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2719
|
+
// Fast path for simple identifiers: "count", "name", "visible"
|
|
2720
|
+
// Avoids full tokenize→parse→evaluate overhead for the most common case.
|
|
2721
|
+
if (/^[a-zA-Z_$][\w$]*$/.test(trimmed)) {
|
|
2722
|
+
for (const layer of scope) {
|
|
2723
|
+
if (layer && typeof layer === 'object' && trimmed in layer) {
|
|
2724
|
+
return layer[trimmed];
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2728
|
+
}
|
|
2686
2729
|
|
|
2687
|
-
|
|
2730
|
+
// Check AST cache (LRU: move to end on hit)
|
|
2731
|
+
let ast = _astCache.get(trimmed);
|
|
2732
|
+
if (ast) {
|
|
2733
|
+
_astCache.delete(trimmed);
|
|
2734
|
+
_astCache.set(trimmed, ast);
|
|
2735
|
+
} else {
|
|
2736
|
+
const tokens = tokenize(trimmed);
|
|
2737
|
+
const parser = new Parser(tokens, scope);
|
|
2738
|
+
ast = parser.parse();
|
|
2739
|
+
|
|
2740
|
+
// Evict oldest entries when cache is full
|
|
2741
|
+
if (_astCache.size >= _AST_CACHE_MAX) {
|
|
2742
|
+
const first = _astCache.keys().next().value;
|
|
2743
|
+
_astCache.delete(first);
|
|
2744
|
+
}
|
|
2745
|
+
_astCache.set(trimmed, ast);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
return evaluate(ast, scope);
|
|
2749
|
+
} catch (err) {
|
|
2750
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
2751
|
+
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
2752
|
+
}
|
|
2753
|
+
return undefined;
|
|
2754
|
+
}
|
|
2688
2755
|
}
|
|
2689
2756
|
|
|
2690
2757
|
// --- src/component.js --------------------------------------------
|
|
@@ -3191,11 +3258,14 @@ class Component {
|
|
|
3191
3258
|
this._bindRefs();
|
|
3192
3259
|
this._bindModels();
|
|
3193
3260
|
|
|
3194
|
-
// Restore focus if the morph replaced the focused element
|
|
3261
|
+
// Restore focus if the morph replaced the focused element.
|
|
3262
|
+
// Always restore selectionRange — even when the element is still
|
|
3263
|
+
// the activeElement — because _bindModels or morph attribute syncing
|
|
3264
|
+
// can alter the value and move the cursor.
|
|
3195
3265
|
if (_focusInfo) {
|
|
3196
3266
|
const el = this._el.querySelector(_focusInfo.selector);
|
|
3197
|
-
if (el
|
|
3198
|
-
el.focus();
|
|
3267
|
+
if (el) {
|
|
3268
|
+
if (el !== document.activeElement) el.focus();
|
|
3199
3269
|
try {
|
|
3200
3270
|
if (_focusInfo.start !== null && _focusInfo.start !== undefined) {
|
|
3201
3271
|
el.setSelectionRange(_focusInfo.start, _focusInfo.end, _focusInfo.dir);
|
|
@@ -5833,12 +5903,15 @@ $.onError = onError;
|
|
|
5833
5903
|
$.ZQueryError = ZQueryError;
|
|
5834
5904
|
$.ErrorCode = ErrorCode;
|
|
5835
5905
|
$.guardCallback = guardCallback;
|
|
5906
|
+
$.guardAsync = guardAsync;
|
|
5836
5907
|
$.validate = validate;
|
|
5908
|
+
$.formatError = formatError;
|
|
5837
5909
|
|
|
5838
5910
|
// --- Meta ------------------------------------------------------------------
|
|
5839
|
-
$.version
|
|
5840
|
-
$.libSize
|
|
5841
|
-
$.
|
|
5911
|
+
$.version = '0.9.9';
|
|
5912
|
+
$.libSize = '~101 KB';
|
|
5913
|
+
$.unitTests = {"passed":1808,"failed":0,"total":1808,"suites":493,"duration":2896,"ok":true};
|
|
5914
|
+
$.meta = {}; // populated at build time by CLI bundler
|
|
5842
5915
|
|
|
5843
5916
|
$.noConflict = () => {
|
|
5844
5917
|
if (typeof window !== 'undefined' && window.$ === $) {
|