zero-query 0.9.0 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/cli/commands/bundle.js +15 -2
- package/cli/commands/dev/devtools/js/elements.js +5 -0
- package/cli/scaffold/app/app.js +1 -1
- package/cli/scaffold/global.css +1 -2
- package/cli/scaffold/index.html +2 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +544 -71
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +51 -3
- package/index.js +32 -4
- package/package.json +1 -1
- package/src/component.js +60 -10
- package/src/core.js +65 -13
- package/src/diff.js +11 -5
- package/src/expression.js +104 -16
- package/src/http.js +23 -3
- package/src/reactive.js +8 -2
- package/src/router.js +43 -9
- package/src/ssr.js +1 -1
- package/src/store.js +5 -0
- package/src/utils.js +203 -11
- package/tests/audit.test.js +4030 -0
- package/tests/cli.test.js +456 -0
- package/tests/component.test.js +1387 -0
- package/tests/core.test.js +934 -1
- package/tests/diff.test.js +891 -0
- package/tests/errors.test.js +179 -0
- package/tests/expression.test.js +569 -0
- package/tests/http.test.js +160 -1
- package/tests/reactive.test.js +320 -0
- package/tests/router.test.js +1187 -0
- package/tests/ssr.test.js +261 -0
- package/tests/store.test.js +210 -0
- package/tests/utils.test.js +729 -1
- package/types/store.d.ts +3 -0
- package/types/utils.d.ts +103 -0
package/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Lightweight modern frontend library — jQuery-like selectors, reactive
|
|
5
5
|
* components, SPA router, state management, HTTP client & utilities.
|
|
6
6
|
*
|
|
7
|
-
* @version 0.9.
|
|
7
|
+
* @version 0.9.5
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @see https://z-query.com/docs
|
|
10
10
|
*/
|
|
@@ -74,6 +74,7 @@ export {
|
|
|
74
74
|
once,
|
|
75
75
|
sleep,
|
|
76
76
|
escapeHtml,
|
|
77
|
+
stripHtml,
|
|
77
78
|
html,
|
|
78
79
|
TrustedHTML,
|
|
79
80
|
trust,
|
|
@@ -90,6 +91,23 @@ export {
|
|
|
90
91
|
session,
|
|
91
92
|
EventBus,
|
|
92
93
|
bus,
|
|
94
|
+
range,
|
|
95
|
+
unique,
|
|
96
|
+
chunk,
|
|
97
|
+
groupBy,
|
|
98
|
+
pick,
|
|
99
|
+
omit,
|
|
100
|
+
getPath,
|
|
101
|
+
setPath,
|
|
102
|
+
isEmpty,
|
|
103
|
+
capitalize,
|
|
104
|
+
truncate,
|
|
105
|
+
clamp,
|
|
106
|
+
MemoizedFunction,
|
|
107
|
+
memoize,
|
|
108
|
+
RetryOptions,
|
|
109
|
+
retry,
|
|
110
|
+
timeout,
|
|
93
111
|
} from './types/utils';
|
|
94
112
|
|
|
95
113
|
export {
|
|
@@ -122,11 +140,15 @@ import type { createStore, getStore } from './types/store';
|
|
|
122
140
|
import type { HttpClient } from './types/http';
|
|
123
141
|
import type {
|
|
124
142
|
debounce, throttle, pipe, once, sleep,
|
|
125
|
-
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
143
|
+
escapeHtml, stripHtml, html, trust, uuid, camelCase, kebabCase,
|
|
126
144
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
127
145
|
StorageWrapper, EventBus,
|
|
146
|
+
range, unique, chunk, groupBy,
|
|
147
|
+
pick, omit, getPath, setPath, isEmpty,
|
|
148
|
+
capitalize, truncate, clamp,
|
|
149
|
+
MemoizedFunction, memoize, RetryOptions, retry, timeout,
|
|
128
150
|
} from './types/utils';
|
|
129
|
-
import type { onError, ZQueryError, ErrorCode } from './types/errors';
|
|
151
|
+
import type { onError, ZQueryError, ErrorCode, guardCallback, validate } from './types/errors';
|
|
130
152
|
import type { morph, morphElement, safeEval } from './types/misc';
|
|
131
153
|
|
|
132
154
|
/**
|
|
@@ -169,6 +191,10 @@ interface ZQueryStatic {
|
|
|
169
191
|
name(name: string): ZQueryCollection;
|
|
170
192
|
/** Children of `#parentId` as `ZQueryCollection`. */
|
|
171
193
|
children(parentId: string): ZQueryCollection;
|
|
194
|
+
/** `document.querySelector(selector)` — raw Element or null. */
|
|
195
|
+
qs(selector: string, context?: Element | Document): Element | null;
|
|
196
|
+
/** `document.querySelectorAll(selector)` — as a real `Array<Element>`. */
|
|
197
|
+
qsa(selector: string, context?: Element | Document): Element[];
|
|
172
198
|
|
|
173
199
|
// -- Static helpers ------------------------------------------------------
|
|
174
200
|
/**
|
|
@@ -245,6 +271,10 @@ interface ZQueryStatic {
|
|
|
245
271
|
ZQueryError: typeof ZQueryError;
|
|
246
272
|
/** Frozen map of all error code constants. */
|
|
247
273
|
ErrorCode: typeof ErrorCode;
|
|
274
|
+
/** Wrap a callback so thrown errors are caught and reported via the global handler. */
|
|
275
|
+
guardCallback: typeof guardCallback;
|
|
276
|
+
/** Validate a required value is defined and of the expected type. */
|
|
277
|
+
validate: typeof validate;
|
|
248
278
|
|
|
249
279
|
// -- Utilities -----------------------------------------------------------
|
|
250
280
|
debounce: typeof debounce;
|
|
@@ -254,6 +284,7 @@ interface ZQueryStatic {
|
|
|
254
284
|
sleep: typeof sleep;
|
|
255
285
|
|
|
256
286
|
escapeHtml: typeof escapeHtml;
|
|
287
|
+
stripHtml: typeof stripHtml;
|
|
257
288
|
html: typeof html;
|
|
258
289
|
trust: typeof trust;
|
|
259
290
|
uuid: typeof uuid;
|
|
@@ -269,8 +300,25 @@ interface ZQueryStatic {
|
|
|
269
300
|
|
|
270
301
|
storage: StorageWrapper;
|
|
271
302
|
session: StorageWrapper;
|
|
303
|
+
EventBus: typeof EventBus;
|
|
272
304
|
bus: EventBus;
|
|
273
305
|
|
|
306
|
+
range: typeof range;
|
|
307
|
+
unique: typeof unique;
|
|
308
|
+
chunk: typeof chunk;
|
|
309
|
+
groupBy: typeof groupBy;
|
|
310
|
+
pick: typeof pick;
|
|
311
|
+
omit: typeof omit;
|
|
312
|
+
getPath: typeof getPath;
|
|
313
|
+
setPath: typeof setPath;
|
|
314
|
+
isEmpty: typeof isEmpty;
|
|
315
|
+
capitalize: typeof capitalize;
|
|
316
|
+
truncate: typeof truncate;
|
|
317
|
+
clamp: typeof clamp;
|
|
318
|
+
memoize: typeof memoize;
|
|
319
|
+
retry: typeof retry;
|
|
320
|
+
timeout: typeof timeout;
|
|
321
|
+
|
|
274
322
|
// -- Meta ----------------------------------------------------------------
|
|
275
323
|
/** Library version string. */
|
|
276
324
|
version: string;
|
package/index.js
CHANGED
|
@@ -19,9 +19,13 @@ import { morph, morphElement } from './src/diff.js';
|
|
|
19
19
|
import { safeEval } from './src/expression.js';
|
|
20
20
|
import {
|
|
21
21
|
debounce, throttle, pipe, once, sleep,
|
|
22
|
-
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
22
|
+
escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
|
|
23
23
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
24
|
-
storage, session, bus,
|
|
24
|
+
storage, session, EventBus, bus,
|
|
25
|
+
range, unique, chunk, groupBy,
|
|
26
|
+
pick, omit, getPath, setPath, isEmpty,
|
|
27
|
+
capitalize, truncate, clamp,
|
|
28
|
+
memoize, retry, timeout,
|
|
25
29
|
} from './src/utils.js';
|
|
26
30
|
import { ZQueryError, ErrorCode, onError, reportError, guardCallback, validate } from './src/errors.js';
|
|
27
31
|
|
|
@@ -61,6 +65,8 @@ Object.defineProperty($, 'name', {
|
|
|
61
65
|
value: query.name, writable: true, configurable: true
|
|
62
66
|
});
|
|
63
67
|
$.children = query.children;
|
|
68
|
+
$.qs = query.qs;
|
|
69
|
+
$.qsa = query.qsa;
|
|
64
70
|
|
|
65
71
|
// --- Collection selector ---------------------------------------------------
|
|
66
72
|
/**
|
|
@@ -129,8 +135,10 @@ $.pipe = pipe;
|
|
|
129
135
|
$.once = once;
|
|
130
136
|
$.sleep = sleep;
|
|
131
137
|
$.escapeHtml = escapeHtml;
|
|
138
|
+
$.stripHtml = stripHtml;
|
|
132
139
|
$.html = html;
|
|
133
140
|
$.trust = trust;
|
|
141
|
+
$.TrustedHTML = TrustedHTML;
|
|
134
142
|
$.uuid = uuid;
|
|
135
143
|
$.camelCase = camelCase;
|
|
136
144
|
$.kebabCase = kebabCase;
|
|
@@ -141,7 +149,23 @@ $.param = param;
|
|
|
141
149
|
$.parseQuery = parseQuery;
|
|
142
150
|
$.storage = storage;
|
|
143
151
|
$.session = session;
|
|
152
|
+
$.EventBus = EventBus;
|
|
144
153
|
$.bus = bus;
|
|
154
|
+
$.range = range;
|
|
155
|
+
$.unique = unique;
|
|
156
|
+
$.chunk = chunk;
|
|
157
|
+
$.groupBy = groupBy;
|
|
158
|
+
$.pick = pick;
|
|
159
|
+
$.omit = omit;
|
|
160
|
+
$.getPath = getPath;
|
|
161
|
+
$.setPath = setPath;
|
|
162
|
+
$.isEmpty = isEmpty;
|
|
163
|
+
$.capitalize = capitalize;
|
|
164
|
+
$.truncate = truncate;
|
|
165
|
+
$.clamp = clamp;
|
|
166
|
+
$.memoize = memoize;
|
|
167
|
+
$.retry = retry;
|
|
168
|
+
$.timeout = timeout;
|
|
145
169
|
|
|
146
170
|
// --- Error handling --------------------------------------------------------
|
|
147
171
|
$.onError = onError;
|
|
@@ -189,9 +213,13 @@ export {
|
|
|
189
213
|
http,
|
|
190
214
|
ZQueryError, ErrorCode, onError, reportError, guardCallback, validate,
|
|
191
215
|
debounce, throttle, pipe, once, sleep,
|
|
192
|
-
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
216
|
+
escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
|
|
193
217
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
194
|
-
storage, session, bus,
|
|
218
|
+
storage, session, EventBus, bus,
|
|
219
|
+
range, unique, chunk, groupBy,
|
|
220
|
+
pick, omit, getPath, setPath, isEmpty,
|
|
221
|
+
capitalize, truncate, clamp,
|
|
222
|
+
memoize, retry, timeout,
|
|
195
223
|
};
|
|
196
224
|
|
|
197
225
|
export default $;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/component.js
CHANGED
|
@@ -264,7 +264,7 @@ class Component {
|
|
|
264
264
|
if (!watchers) return;
|
|
265
265
|
for (const [key, handler] of Object.entries(watchers)) {
|
|
266
266
|
// Match exact key or parent key (e.g. watcher on 'user' fires when 'user.name' changes)
|
|
267
|
-
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.')
|
|
267
|
+
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.')) {
|
|
268
268
|
const currentVal = _getPath(this.state.__raw || this.state, key);
|
|
269
269
|
const prevVal = this._prevWatchValues?.[key];
|
|
270
270
|
if (currentVal !== prevVal) {
|
|
@@ -413,20 +413,26 @@ class Component {
|
|
|
413
413
|
if (!this._mounted && combinedStyles) {
|
|
414
414
|
const scopeAttr = `z-s${this._uid}`;
|
|
415
415
|
this._el.setAttribute(scopeAttr, '');
|
|
416
|
-
let
|
|
416
|
+
let noScopeDepth = 0; // brace depth at which a no-scope @-rule started (0 = none active)
|
|
417
|
+
let braceDepth = 0; // overall brace depth
|
|
417
418
|
const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
|
|
418
419
|
if (match === '}') {
|
|
419
|
-
if (
|
|
420
|
+
if (noScopeDepth > 0 && braceDepth <= noScopeDepth) noScopeDepth = 0;
|
|
421
|
+
braceDepth--;
|
|
420
422
|
return match;
|
|
421
423
|
}
|
|
424
|
+
braceDepth++;
|
|
422
425
|
const trimmed = selector.trim();
|
|
423
|
-
// Don't scope @-rules
|
|
426
|
+
// Don't scope @-rules themselves
|
|
424
427
|
if (trimmed.startsWith('@')) {
|
|
425
|
-
|
|
428
|
+
// @keyframes and @font-face contain non-selector content — skip scoping inside them
|
|
429
|
+
if (/^@(keyframes|font-face)\b/.test(trimmed)) {
|
|
430
|
+
noScopeDepth = braceDepth;
|
|
431
|
+
}
|
|
426
432
|
return match;
|
|
427
433
|
}
|
|
428
|
-
//
|
|
429
|
-
if (
|
|
434
|
+
// Inside @keyframes or @font-face — don't scope inner rules
|
|
435
|
+
if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
|
|
430
436
|
return match;
|
|
431
437
|
}
|
|
432
438
|
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
@@ -614,15 +620,44 @@ class Component {
|
|
|
614
620
|
const handler = (e) => {
|
|
615
621
|
// Read bindings from live map — always up to date after re-renders
|
|
616
622
|
const currentBindings = this._eventBindings?.get(event) || [];
|
|
617
|
-
|
|
618
|
-
|
|
623
|
+
|
|
624
|
+
// Collect matching bindings with their matched elements, then sort
|
|
625
|
+
// deepest-first so .stop correctly prevents ancestor handlers
|
|
626
|
+
// (mimics real DOM bubbling order within delegated events).
|
|
627
|
+
const hits = [];
|
|
628
|
+
for (const binding of currentBindings) {
|
|
629
|
+
const matched = e.target.closest(binding.selector);
|
|
630
|
+
if (!matched) continue;
|
|
631
|
+
hits.push({ ...binding, matched });
|
|
632
|
+
}
|
|
633
|
+
hits.sort((a, b) => {
|
|
634
|
+
if (a.matched === b.matched) return 0;
|
|
635
|
+
return a.matched.contains(b.matched) ? 1 : -1;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
let stoppedAt = null; // Track elements that called .stop
|
|
639
|
+
for (const { selector, methodExpr, modifiers, el, matched } of hits) {
|
|
640
|
+
|
|
641
|
+
// In delegated events, .stop should prevent ancestor bindings from
|
|
642
|
+
// firing — stopPropagation alone only stops real DOM bubbling.
|
|
643
|
+
if (stoppedAt) {
|
|
644
|
+
let blocked = false;
|
|
645
|
+
for (const stopped of stoppedAt) {
|
|
646
|
+
if (matched.contains(stopped) && matched !== stopped) { blocked = true; break; }
|
|
647
|
+
}
|
|
648
|
+
if (blocked) continue;
|
|
649
|
+
}
|
|
619
650
|
|
|
620
651
|
// .self — only fire if target is the element itself
|
|
621
652
|
if (modifiers.includes('self') && e.target !== el) continue;
|
|
622
653
|
|
|
623
654
|
// Handle modifiers
|
|
624
655
|
if (modifiers.includes('prevent')) e.preventDefault();
|
|
625
|
-
if (modifiers.includes('stop'))
|
|
656
|
+
if (modifiers.includes('stop')) {
|
|
657
|
+
e.stopPropagation();
|
|
658
|
+
if (!stoppedAt) stoppedAt = [];
|
|
659
|
+
stoppedAt.push(matched);
|
|
660
|
+
}
|
|
626
661
|
|
|
627
662
|
// Build the invocation function
|
|
628
663
|
const invoke = (evt) => {
|
|
@@ -1054,6 +1089,21 @@ class Component {
|
|
|
1054
1089
|
this._listeners = [];
|
|
1055
1090
|
this._delegatedEvents = null;
|
|
1056
1091
|
this._eventBindings = null;
|
|
1092
|
+
// Clear any pending debounce/throttle timers to prevent stale closures.
|
|
1093
|
+
// Timers are keyed by individual child elements, so iterate all descendants.
|
|
1094
|
+
const allEls = this._el.querySelectorAll('*');
|
|
1095
|
+
allEls.forEach(child => {
|
|
1096
|
+
const dTimers = _debounceTimers.get(child);
|
|
1097
|
+
if (dTimers) {
|
|
1098
|
+
for (const key in dTimers) clearTimeout(dTimers[key]);
|
|
1099
|
+
_debounceTimers.delete(child);
|
|
1100
|
+
}
|
|
1101
|
+
const tTimers = _throttleTimers.get(child);
|
|
1102
|
+
if (tTimers) {
|
|
1103
|
+
for (const key in tTimers) clearTimeout(tTimers[key]);
|
|
1104
|
+
_throttleTimers.delete(child);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1057
1107
|
if (this._styleEl) this._styleEl.remove();
|
|
1058
1108
|
_instances.delete(this._el);
|
|
1059
1109
|
this._el.innerHTML = '';
|
package/src/core.js
CHANGED
|
@@ -12,7 +12,7 @@ import { morph as _morph, morphElement as _morphElement } from './diff.js';
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
export class ZQueryCollection {
|
|
14
14
|
constructor(elements) {
|
|
15
|
-
this.elements = Array.isArray(elements) ? elements : [elements];
|
|
15
|
+
this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
|
|
16
16
|
this.length = this.elements.length;
|
|
17
17
|
this.elements.forEach((el, i) => { this[i] = el; });
|
|
18
18
|
}
|
|
@@ -69,10 +69,12 @@ export class ZQueryCollection {
|
|
|
69
69
|
return new ZQueryCollection([...kids]);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
siblings() {
|
|
72
|
+
siblings(selector) {
|
|
73
73
|
const sibs = [];
|
|
74
74
|
this.elements.forEach(el => {
|
|
75
|
-
|
|
75
|
+
if (!el.parentElement) return;
|
|
76
|
+
const all = [...el.parentElement.children].filter(c => c !== el);
|
|
77
|
+
sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
|
|
76
78
|
});
|
|
77
79
|
return new ZQueryCollection(sibs);
|
|
78
80
|
}
|
|
@@ -214,7 +216,8 @@ export class ZQueryCollection {
|
|
|
214
216
|
index(selector) {
|
|
215
217
|
if (selector === undefined) {
|
|
216
218
|
const el = this.first();
|
|
217
|
-
|
|
219
|
+
if (!el || !el.parentElement) return -1;
|
|
220
|
+
return Array.from(el.parentElement.children).indexOf(el);
|
|
218
221
|
}
|
|
219
222
|
const target = (typeof selector === 'string')
|
|
220
223
|
? document.querySelector(selector)
|
|
@@ -274,6 +277,11 @@ export class ZQueryCollection {
|
|
|
274
277
|
// --- Attributes ----------------------------------------------------------
|
|
275
278
|
|
|
276
279
|
attr(name, value) {
|
|
280
|
+
if (typeof name === 'object' && name !== null) {
|
|
281
|
+
return this.each((_, el) => {
|
|
282
|
+
for (const [k, v] of Object.entries(name)) el.setAttribute(k, v);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
277
285
|
if (value === undefined) return this.first()?.getAttribute(name);
|
|
278
286
|
return this.each((_, el) => el.setAttribute(name, value));
|
|
279
287
|
}
|
|
@@ -298,7 +306,10 @@ export class ZQueryCollection {
|
|
|
298
306
|
|
|
299
307
|
// --- CSS / Dimensions ----------------------------------------------------
|
|
300
308
|
|
|
301
|
-
css(props) {
|
|
309
|
+
css(props, value) {
|
|
310
|
+
if (typeof props === 'string' && value !== undefined) {
|
|
311
|
+
return this.each((_, el) => { el.style[props] = value; });
|
|
312
|
+
}
|
|
302
313
|
if (typeof props === 'string') {
|
|
303
314
|
const el = this.first();
|
|
304
315
|
return el ? getComputedStyle(el)[props] : undefined;
|
|
@@ -438,6 +449,7 @@ export class ZQueryCollection {
|
|
|
438
449
|
wrap(wrapper) {
|
|
439
450
|
return this.each((_, el) => {
|
|
440
451
|
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
452
|
+
if (!w || !el.parentNode) return;
|
|
441
453
|
el.parentNode.insertBefore(w, el);
|
|
442
454
|
w.appendChild(el);
|
|
443
455
|
});
|
|
@@ -563,12 +575,18 @@ export class ZQueryCollection {
|
|
|
563
575
|
if (typeof selectorOrHandler === 'function') {
|
|
564
576
|
el.addEventListener(evt, selectorOrHandler);
|
|
565
577
|
} else if (typeof selectorOrHandler === 'string') {
|
|
566
|
-
// Delegated event —
|
|
567
|
-
|
|
578
|
+
// Delegated event — store wrapper so off() can remove it
|
|
579
|
+
const wrapper = (e) => {
|
|
568
580
|
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
569
581
|
const target = e.target.closest(selectorOrHandler);
|
|
570
582
|
if (target && el.contains(target)) handler.call(target, e);
|
|
571
|
-
}
|
|
583
|
+
};
|
|
584
|
+
wrapper._zqOriginal = handler;
|
|
585
|
+
wrapper._zqSelector = selectorOrHandler;
|
|
586
|
+
el.addEventListener(evt, wrapper);
|
|
587
|
+
// Track delegated handlers for removal
|
|
588
|
+
if (!el._zqDelegated) el._zqDelegated = [];
|
|
589
|
+
el._zqDelegated.push({ evt, wrapper });
|
|
572
590
|
}
|
|
573
591
|
});
|
|
574
592
|
});
|
|
@@ -577,7 +595,20 @@ export class ZQueryCollection {
|
|
|
577
595
|
off(event, handler) {
|
|
578
596
|
const events = event.split(/\s+/);
|
|
579
597
|
return this.each((_, el) => {
|
|
580
|
-
events.forEach(evt =>
|
|
598
|
+
events.forEach(evt => {
|
|
599
|
+
// Try direct removal first
|
|
600
|
+
el.removeEventListener(evt, handler);
|
|
601
|
+
// Also check delegated handlers
|
|
602
|
+
if (el._zqDelegated) {
|
|
603
|
+
el._zqDelegated = el._zqDelegated.filter(d => {
|
|
604
|
+
if (d.evt === evt && d.wrapper._zqOriginal === handler) {
|
|
605
|
+
el.removeEventListener(evt, d.wrapper);
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
});
|
|
581
612
|
});
|
|
582
613
|
}
|
|
583
614
|
|
|
@@ -606,8 +637,12 @@ export class ZQueryCollection {
|
|
|
606
637
|
// --- Animation -----------------------------------------------------------
|
|
607
638
|
|
|
608
639
|
animate(props, duration = 300, easing = 'ease') {
|
|
640
|
+
// Empty collection — resolve immediately
|
|
641
|
+
if (this.length === 0) return Promise.resolve(this);
|
|
609
642
|
return new Promise(resolve => {
|
|
643
|
+
let resolved = false;
|
|
610
644
|
const count = { done: 0 };
|
|
645
|
+
const listeners = [];
|
|
611
646
|
this.each((_, el) => {
|
|
612
647
|
el.style.transition = `all ${duration}ms ${easing}`;
|
|
613
648
|
requestAnimationFrame(() => {
|
|
@@ -615,13 +650,27 @@ export class ZQueryCollection {
|
|
|
615
650
|
const onEnd = () => {
|
|
616
651
|
el.removeEventListener('transitionend', onEnd);
|
|
617
652
|
el.style.transition = '';
|
|
618
|
-
if (++count.done >= this.length)
|
|
653
|
+
if (!resolved && ++count.done >= this.length) {
|
|
654
|
+
resolved = true;
|
|
655
|
+
resolve(this);
|
|
656
|
+
}
|
|
619
657
|
};
|
|
620
658
|
el.addEventListener('transitionend', onEnd);
|
|
659
|
+
listeners.push({ el, onEnd });
|
|
621
660
|
});
|
|
622
661
|
});
|
|
623
662
|
// Fallback in case transitionend doesn't fire
|
|
624
|
-
setTimeout(() =>
|
|
663
|
+
setTimeout(() => {
|
|
664
|
+
if (!resolved) {
|
|
665
|
+
resolved = true;
|
|
666
|
+
// Clean up any remaining transitionend listeners
|
|
667
|
+
for (const { el, onEnd } of listeners) {
|
|
668
|
+
el.removeEventListener('transitionend', onEnd);
|
|
669
|
+
el.style.transition = '';
|
|
670
|
+
}
|
|
671
|
+
resolve(this);
|
|
672
|
+
}
|
|
673
|
+
}, duration + 50);
|
|
625
674
|
});
|
|
626
675
|
}
|
|
627
676
|
|
|
@@ -635,7 +684,8 @@ export class ZQueryCollection {
|
|
|
635
684
|
|
|
636
685
|
fadeToggle(duration = 300) {
|
|
637
686
|
return Promise.all(this.elements.map(el => {
|
|
638
|
-
const
|
|
687
|
+
const cs = getComputedStyle(el);
|
|
688
|
+
const visible = cs.opacity !== '0' && cs.display !== 'none';
|
|
639
689
|
const col = new ZQueryCollection([el]);
|
|
640
690
|
return visible ? col.fadeOut(duration) : col.fadeIn(duration);
|
|
641
691
|
})).then(() => this);
|
|
@@ -813,6 +863,8 @@ query.children = (parentId) => {
|
|
|
813
863
|
const p = document.getElementById(parentId);
|
|
814
864
|
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
815
865
|
};
|
|
866
|
+
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
867
|
+
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
816
868
|
|
|
817
869
|
// Create element shorthand — returns ZQueryCollection for chaining
|
|
818
870
|
query.create = (tag, attrs = {}, ...children) => {
|
|
@@ -820,7 +872,7 @@ query.create = (tag, attrs = {}, ...children) => {
|
|
|
820
872
|
for (const [k, v] of Object.entries(attrs)) {
|
|
821
873
|
if (k === 'class') el.className = v;
|
|
822
874
|
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
823
|
-
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
|
|
875
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
824
876
|
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
825
877
|
else el.setAttribute(k, v);
|
|
826
878
|
}
|
package/src/diff.js
CHANGED
|
@@ -239,8 +239,11 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
239
239
|
if (!lisSet.has(i)) {
|
|
240
240
|
oldParent.insertBefore(oldNode, cursor);
|
|
241
241
|
}
|
|
242
|
+
// Capture next sibling BEFORE _morphNode — if _morphNode calls
|
|
243
|
+
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
244
|
+
const nextSib = oldNode.nextSibling;
|
|
242
245
|
_morphNode(oldParent, oldNode, newNode);
|
|
243
|
-
cursor =
|
|
246
|
+
cursor = nextSib;
|
|
244
247
|
} else {
|
|
245
248
|
// Insert new node
|
|
246
249
|
const clone = newNode.cloneNode(true);
|
|
@@ -418,10 +421,13 @@ function _morphAttributes(oldEl, newEl) {
|
|
|
418
421
|
}
|
|
419
422
|
}
|
|
420
423
|
|
|
421
|
-
// Remove stale attributes
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
424
|
+
// Remove stale attributes — snapshot names first because oldAttrs
|
|
425
|
+
// is a live NamedNodeMap that mutates on removeAttribute().
|
|
426
|
+
const oldNames = new Array(oldLen);
|
|
427
|
+
for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
|
|
428
|
+
for (let i = oldNames.length - 1; i >= 0; i--) {
|
|
429
|
+
if (!newNames.has(oldNames[i])) {
|
|
430
|
+
oldEl.removeAttribute(oldNames[i]);
|
|
425
431
|
}
|
|
426
432
|
}
|
|
427
433
|
}
|