zero-query 0.6.3 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +39 -29
  2. package/cli/commands/build.js +113 -4
  3. package/cli/commands/bundle.js +392 -29
  4. package/cli/commands/create.js +1 -1
  5. package/cli/commands/dev/devtools/index.js +56 -0
  6. package/cli/commands/dev/devtools/js/components.js +49 -0
  7. package/cli/commands/dev/devtools/js/core.js +409 -0
  8. package/cli/commands/dev/devtools/js/elements.js +413 -0
  9. package/cli/commands/dev/devtools/js/network.js +166 -0
  10. package/cli/commands/dev/devtools/js/performance.js +73 -0
  11. package/cli/commands/dev/devtools/js/router.js +105 -0
  12. package/cli/commands/dev/devtools/js/source.js +132 -0
  13. package/cli/commands/dev/devtools/js/stats.js +35 -0
  14. package/cli/commands/dev/devtools/js/tabs.js +79 -0
  15. package/cli/commands/dev/devtools/panel.html +95 -0
  16. package/cli/commands/dev/devtools/styles.css +244 -0
  17. package/cli/commands/dev/index.js +29 -4
  18. package/cli/commands/dev/logger.js +6 -1
  19. package/cli/commands/dev/overlay.js +428 -2
  20. package/cli/commands/dev/server.js +42 -5
  21. package/cli/commands/dev/watcher.js +59 -1
  22. package/cli/help.js +8 -5
  23. package/cli/scaffold/{scripts → app}/app.js +16 -23
  24. package/cli/scaffold/{scripts → app}/components/about.js +4 -4
  25. package/cli/scaffold/{scripts → app}/components/api-demo.js +1 -1
  26. package/cli/scaffold/{scripts → app}/components/contacts/contacts.css +0 -7
  27. package/cli/scaffold/{scripts → app}/components/contacts/contacts.html +3 -3
  28. package/cli/scaffold/app/components/home.js +137 -0
  29. package/cli/scaffold/{scripts → app}/routes.js +1 -1
  30. package/cli/scaffold/{scripts → app}/store.js +6 -6
  31. package/cli/scaffold/assets/.gitkeep +0 -0
  32. package/cli/scaffold/{styles/styles.css → global.css} +4 -2
  33. package/cli/scaffold/index.html +12 -11
  34. package/cli/utils.js +111 -6
  35. package/dist/zquery.dist.zip +0 -0
  36. package/dist/zquery.js +1122 -158
  37. package/dist/zquery.min.js +3 -16
  38. package/index.d.ts +129 -1290
  39. package/index.js +15 -10
  40. package/package.json +7 -6
  41. package/src/component.js +172 -49
  42. package/src/core.js +359 -18
  43. package/src/diff.js +256 -58
  44. package/src/expression.js +33 -3
  45. package/src/reactive.js +37 -5
  46. package/src/router.js +243 -7
  47. package/tests/component.test.js +886 -0
  48. package/tests/core.test.js +977 -0
  49. package/tests/diff.test.js +525 -0
  50. package/tests/errors.test.js +162 -0
  51. package/tests/expression.test.js +482 -0
  52. package/tests/http.test.js +289 -0
  53. package/tests/reactive.test.js +339 -0
  54. package/tests/router.test.js +649 -0
  55. package/tests/store.test.js +379 -0
  56. package/tests/utils.test.js +512 -0
  57. package/types/collection.d.ts +383 -0
  58. package/types/component.d.ts +217 -0
  59. package/types/errors.d.ts +103 -0
  60. package/types/http.d.ts +81 -0
  61. package/types/misc.d.ts +179 -0
  62. package/types/reactive.d.ts +76 -0
  63. package/types/router.d.ts +161 -0
  64. package/types/ssr.d.ts +49 -0
  65. package/types/store.d.ts +107 -0
  66. package/types/utils.d.ts +142 -0
  67. package/cli/commands/dev.old.js +0 -520
  68. package/cli/scaffold/scripts/components/home.js +0 -137
  69. /package/cli/scaffold/{scripts → app}/components/contacts/contacts.js +0 -0
  70. /package/cli/scaffold/{scripts → app}/components/counter.js +0 -0
  71. /package/cli/scaffold/{scripts → app}/components/not-found.js +0 -0
  72. /package/cli/scaffold/{scripts → app}/components/todos.js +0 -0
package/src/core.js CHANGED
@@ -5,6 +5,8 @@
5
5
  * into a full jQuery-like chainable wrapper with modern APIs.
6
6
  */
7
7
 
8
+ import { morph as _morph, morphElement as _morphElement } from './diff.js';
9
+
8
10
  // ---------------------------------------------------------------------------
9
11
  // ZQueryCollection — wraps an array of elements with chainable methods
10
12
  // ---------------------------------------------------------------------------
@@ -75,8 +77,96 @@ export class ZQueryCollection {
75
77
  return new ZQueryCollection(sibs);
76
78
  }
77
79
 
78
- next() { return new ZQueryCollection(this.elements.map(el => el.nextElementSibling).filter(Boolean)); }
79
- prev() { return new ZQueryCollection(this.elements.map(el => el.previousElementSibling).filter(Boolean)); }
80
+ next(selector) {
81
+ const els = this.elements.map(el => el.nextElementSibling).filter(Boolean);
82
+ return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
83
+ }
84
+
85
+ prev(selector) {
86
+ const els = this.elements.map(el => el.previousElementSibling).filter(Boolean);
87
+ return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
88
+ }
89
+
90
+ nextAll(selector) {
91
+ const result = [];
92
+ this.elements.forEach(el => {
93
+ let sib = el.nextElementSibling;
94
+ while (sib) {
95
+ if (!selector || sib.matches(selector)) result.push(sib);
96
+ sib = sib.nextElementSibling;
97
+ }
98
+ });
99
+ return new ZQueryCollection(result);
100
+ }
101
+
102
+ nextUntil(selector, filter) {
103
+ const result = [];
104
+ this.elements.forEach(el => {
105
+ let sib = el.nextElementSibling;
106
+ while (sib) {
107
+ if (selector && sib.matches(selector)) break;
108
+ if (!filter || sib.matches(filter)) result.push(sib);
109
+ sib = sib.nextElementSibling;
110
+ }
111
+ });
112
+ return new ZQueryCollection(result);
113
+ }
114
+
115
+ prevAll(selector) {
116
+ const result = [];
117
+ this.elements.forEach(el => {
118
+ let sib = el.previousElementSibling;
119
+ while (sib) {
120
+ if (!selector || sib.matches(selector)) result.push(sib);
121
+ sib = sib.previousElementSibling;
122
+ }
123
+ });
124
+ return new ZQueryCollection(result);
125
+ }
126
+
127
+ prevUntil(selector, filter) {
128
+ const result = [];
129
+ this.elements.forEach(el => {
130
+ let sib = el.previousElementSibling;
131
+ while (sib) {
132
+ if (selector && sib.matches(selector)) break;
133
+ if (!filter || sib.matches(filter)) result.push(sib);
134
+ sib = sib.previousElementSibling;
135
+ }
136
+ });
137
+ return new ZQueryCollection(result);
138
+ }
139
+
140
+ parents(selector) {
141
+ const result = [];
142
+ this.elements.forEach(el => {
143
+ let parent = el.parentElement;
144
+ while (parent) {
145
+ if (!selector || parent.matches(selector)) result.push(parent);
146
+ parent = parent.parentElement;
147
+ }
148
+ });
149
+ return new ZQueryCollection([...new Set(result)]);
150
+ }
151
+
152
+ parentsUntil(selector, filter) {
153
+ const result = [];
154
+ this.elements.forEach(el => {
155
+ let parent = el.parentElement;
156
+ while (parent) {
157
+ if (selector && parent.matches(selector)) break;
158
+ if (!filter || parent.matches(filter)) result.push(parent);
159
+ parent = parent.parentElement;
160
+ }
161
+ });
162
+ return new ZQueryCollection([...new Set(result)]);
163
+ }
164
+
165
+ contents() {
166
+ const result = [];
167
+ this.elements.forEach(el => result.push(...el.childNodes));
168
+ return new ZQueryCollection(result);
169
+ }
80
170
 
81
171
  filter(selector) {
82
172
  if (typeof selector === 'function') {
@@ -96,20 +186,85 @@ export class ZQueryCollection {
96
186
  return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
97
187
  }
98
188
 
189
+ is(selector) {
190
+ if (typeof selector === 'function') {
191
+ return this.elements.some((el, i) => selector.call(el, i, el));
192
+ }
193
+ return this.elements.some(el => el.matches(selector));
194
+ }
195
+
196
+ slice(start, end) {
197
+ return new ZQueryCollection(this.elements.slice(start, end));
198
+ }
199
+
200
+ add(selector, context) {
201
+ const toAdd = (selector instanceof ZQueryCollection)
202
+ ? selector.elements
203
+ : (selector instanceof Node)
204
+ ? [selector]
205
+ : Array.from((context || document).querySelectorAll(selector));
206
+ return new ZQueryCollection([...this.elements, ...toAdd]);
207
+ }
208
+
209
+ get(index) {
210
+ if (index === undefined) return [...this.elements];
211
+ return index < 0 ? this.elements[this.length + index] : this.elements[index];
212
+ }
213
+
214
+ index(selector) {
215
+ if (selector === undefined) {
216
+ const el = this.first();
217
+ return el ? Array.from(el.parentElement.children).indexOf(el) : -1;
218
+ }
219
+ const target = (typeof selector === 'string')
220
+ ? document.querySelector(selector)
221
+ : selector;
222
+ return this.elements.indexOf(target);
223
+ }
224
+
99
225
  // --- Classes -------------------------------------------------------------
100
226
 
101
227
  addClass(...names) {
228
+ // Fast path: single class, no spaces — avoids flatMap + regex split allocation
229
+ if (names.length === 1 && names[0].indexOf(' ') === -1) {
230
+ const c = names[0];
231
+ for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(c);
232
+ return this;
233
+ }
102
234
  const classes = names.flatMap(n => n.split(/\s+/));
103
- return this.each((_, el) => el.classList.add(...classes));
235
+ for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(...classes);
236
+ return this;
104
237
  }
105
238
 
106
239
  removeClass(...names) {
240
+ if (names.length === 1 && names[0].indexOf(' ') === -1) {
241
+ const c = names[0];
242
+ for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(c);
243
+ return this;
244
+ }
107
245
  const classes = names.flatMap(n => n.split(/\s+/));
108
- return this.each((_, el) => el.classList.remove(...classes));
246
+ for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(...classes);
247
+ return this;
109
248
  }
110
249
 
111
- toggleClass(name, force) {
112
- return this.each((_, el) => el.classList.toggle(name, force));
250
+ toggleClass(...args) {
251
+ const force = typeof args[args.length - 1] === 'boolean' ? args.pop() : undefined;
252
+ // Fast path: single class, no spaces
253
+ if (args.length === 1 && args[0].indexOf(' ') === -1) {
254
+ const c = args[0];
255
+ for (let i = 0; i < this.elements.length; i++) {
256
+ force !== undefined ? this.elements[i].classList.toggle(c, force) : this.elements[i].classList.toggle(c);
257
+ }
258
+ return this;
259
+ }
260
+ const classes = args.flatMap(n => n.split(/\s+/));
261
+ for (let i = 0; i < this.elements.length; i++) {
262
+ const el = this.elements[i];
263
+ for (let j = 0; j < classes.length; j++) {
264
+ force !== undefined ? el.classList.toggle(classes[j], force) : el.classList.toggle(classes[j]);
265
+ }
266
+ }
267
+ return this;
113
268
  }
114
269
 
115
270
  hasClass(name) {
@@ -145,7 +300,8 @@ export class ZQueryCollection {
145
300
 
146
301
  css(props) {
147
302
  if (typeof props === 'string') {
148
- return getComputedStyle(this.first())[props];
303
+ const el = this.first();
304
+ return el ? getComputedStyle(el)[props] : undefined;
149
305
  }
150
306
  return this.each((_, el) => Object.assign(el.style, props));
151
307
  }
@@ -163,11 +319,79 @@ export class ZQueryCollection {
163
319
  return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
164
320
  }
165
321
 
322
+ scrollTop(value) {
323
+ if (value === undefined) {
324
+ const el = this.first();
325
+ return el === window ? window.scrollY : el?.scrollTop;
326
+ }
327
+ return this.each((_, el) => {
328
+ if (el === window) window.scrollTo(window.scrollX, value);
329
+ else el.scrollTop = value;
330
+ });
331
+ }
332
+
333
+ scrollLeft(value) {
334
+ if (value === undefined) {
335
+ const el = this.first();
336
+ return el === window ? window.scrollX : el?.scrollLeft;
337
+ }
338
+ return this.each((_, el) => {
339
+ if (el === window) window.scrollTo(value, window.scrollY);
340
+ else el.scrollLeft = value;
341
+ });
342
+ }
343
+
344
+ innerWidth() {
345
+ const el = this.first();
346
+ return el?.clientWidth;
347
+ }
348
+
349
+ innerHeight() {
350
+ const el = this.first();
351
+ return el?.clientHeight;
352
+ }
353
+
354
+ outerWidth(includeMargin = false) {
355
+ const el = this.first();
356
+ if (!el) return undefined;
357
+ let w = el.offsetWidth;
358
+ if (includeMargin) {
359
+ const style = getComputedStyle(el);
360
+ w += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
361
+ }
362
+ return w;
363
+ }
364
+
365
+ outerHeight(includeMargin = false) {
366
+ const el = this.first();
367
+ if (!el) return undefined;
368
+ let h = el.offsetHeight;
369
+ if (includeMargin) {
370
+ const style = getComputedStyle(el);
371
+ h += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
372
+ }
373
+ return h;
374
+ }
375
+
166
376
  // --- Content -------------------------------------------------------------
167
377
 
168
378
  html(content) {
169
379
  if (content === undefined) return this.first()?.innerHTML;
170
- return this.each((_, el) => { el.innerHTML = content; });
380
+ // Auto-morph: if the element already has children, use the diff engine
381
+ // to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
382
+ // Empty elements get raw innerHTML for fast first-paint — same strategy
383
+ // the component system uses (first render = innerHTML, updates = morph).
384
+ return this.each((_, el) => {
385
+ if (el.childNodes.length > 0) {
386
+ _morph(el, content);
387
+ } else {
388
+ el.innerHTML = content;
389
+ }
390
+ });
391
+ }
392
+
393
+ morph(content) {
394
+ return this.each((_, el) => { _morph(el, content); });
171
395
  }
172
396
 
173
397
  text(content) {
@@ -224,7 +448,8 @@ export class ZQueryCollection {
224
448
  }
225
449
 
226
450
  empty() {
227
- return this.each((_, el) => { el.innerHTML = ''; });
451
+ // textContent = '' clears all children without invoking the HTML parser
452
+ return this.each((_, el) => { el.textContent = ''; });
228
453
  }
229
454
 
230
455
  clone(deep = true) {
@@ -234,14 +459,82 @@ export class ZQueryCollection {
234
459
  replaceWith(content) {
235
460
  return this.each((_, el) => {
236
461
  if (typeof content === 'string') {
237
- el.insertAdjacentHTML('afterend', content);
238
- el.remove();
462
+ // Auto-morph: diff attributes + children when the tag name matches
463
+ // instead of destroying and re-creating the element.
464
+ _morphElement(el, content);
239
465
  } else if (content instanceof Node) {
240
466
  el.parentNode.replaceChild(content, el);
241
467
  }
242
468
  });
243
469
  }
244
470
 
471
+ appendTo(target) {
472
+ const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
473
+ if (dest) this.each((_, el) => dest.appendChild(el));
474
+ return this;
475
+ }
476
+
477
+ prependTo(target) {
478
+ const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
479
+ if (dest) this.each((_, el) => dest.insertBefore(el, dest.firstChild));
480
+ return this;
481
+ }
482
+
483
+ insertAfter(target) {
484
+ const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
485
+ if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref.nextSibling));
486
+ return this;
487
+ }
488
+
489
+ insertBefore(target) {
490
+ const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
491
+ if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref));
492
+ return this;
493
+ }
494
+
495
+ replaceAll(target) {
496
+ const targets = typeof target === 'string'
497
+ ? Array.from(document.querySelectorAll(target))
498
+ : target instanceof ZQueryCollection ? target.elements : [target];
499
+ targets.forEach((t, i) => {
500
+ const nodes = i === 0 ? this.elements : this.elements.map(el => el.cloneNode(true));
501
+ nodes.forEach(el => t.parentNode.insertBefore(el, t));
502
+ t.remove();
503
+ });
504
+ return this;
505
+ }
506
+
507
+ unwrap(selector) {
508
+ this.elements.forEach(el => {
509
+ const parent = el.parentElement;
510
+ if (!parent || parent === document.body) return;
511
+ if (selector && !parent.matches(selector)) return;
512
+ parent.replaceWith(...parent.childNodes);
513
+ });
514
+ return this;
515
+ }
516
+
517
+ wrapAll(wrapper) {
518
+ const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
519
+ const first = this.first();
520
+ if (!first) return this;
521
+ first.parentNode.insertBefore(w, first);
522
+ this.each((_, el) => w.appendChild(el));
523
+ return this;
524
+ }
525
+
526
+ wrapInner(wrapper) {
527
+ return this.each((_, el) => {
528
+ const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
529
+ while (el.firstChild) w.appendChild(el.firstChild);
530
+ el.appendChild(w);
531
+ });
532
+ }
533
+
534
+ detach() {
535
+ return this.each((_, el) => el.remove());
536
+ }
537
+
245
538
  // --- Visibility ----------------------------------------------------------
246
539
 
247
540
  show(display = '') {
@@ -254,7 +547,9 @@ export class ZQueryCollection {
254
547
 
255
548
  toggle(display = '') {
256
549
  return this.each((_, el) => {
257
- el.style.display = (el.style.display === 'none' || getComputedStyle(el).display === 'none') ? display : 'none';
550
+ // Check inline style first (cheap) before forcing layout via getComputedStyle
551
+ const hidden = el.style.display === 'none' || (el.style.display !== '' ? false : getComputedStyle(el).display === 'none');
552
+ el.style.display = hidden ? display : 'none';
258
553
  });
259
554
  }
260
555
 
@@ -267,9 +562,10 @@ export class ZQueryCollection {
267
562
  events.forEach(evt => {
268
563
  if (typeof selectorOrHandler === 'function') {
269
564
  el.addEventListener(evt, selectorOrHandler);
270
- } else {
271
- // Delegated event
565
+ } else if (typeof selectorOrHandler === 'string') {
566
+ // Delegated event — only works on elements that support closest()
272
567
  el.addEventListener(evt, (e) => {
568
+ if (!e.target || typeof e.target.closest !== 'function') return;
273
569
  const target = e.target.closest(selectorOrHandler);
274
570
  if (target && el.contains(target)) handler.call(target, e);
275
571
  });
@@ -302,6 +598,10 @@ export class ZQueryCollection {
302
598
  submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
303
599
  focus() { this.first()?.focus(); return this; }
304
600
  blur() { this.first()?.blur(); return this; }
601
+ hover(enterFn, leaveFn) {
602
+ this.on('mouseenter', enterFn);
603
+ return this.on('mouseleave', leaveFn || enterFn);
604
+ }
305
605
 
306
606
  // --- Animation -----------------------------------------------------------
307
607
 
@@ -333,6 +633,40 @@ export class ZQueryCollection {
333
633
  return this.animate({ opacity: '0' }, duration).then(col => col.hide());
334
634
  }
335
635
 
636
+ fadeToggle(duration = 300) {
637
+ return Promise.all(this.elements.map(el => {
638
+ const visible = getComputedStyle(el).opacity !== '0' && getComputedStyle(el).display !== 'none';
639
+ const col = new ZQueryCollection([el]);
640
+ return visible ? col.fadeOut(duration) : col.fadeIn(duration);
641
+ })).then(() => this);
642
+ }
643
+
644
+ fadeTo(duration, opacity) {
645
+ return this.animate({ opacity: String(opacity) }, duration);
646
+ }
647
+
648
+ slideDown(duration = 300) {
649
+ return this.each((_, el) => {
650
+ el.style.display = '';
651
+ el.style.overflow = 'hidden';
652
+ const h = el.scrollHeight + 'px';
653
+ el.style.maxHeight = '0';
654
+ el.style.transition = `max-height ${duration}ms ease`;
655
+ requestAnimationFrame(() => { el.style.maxHeight = h; });
656
+ setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
657
+ });
658
+ }
659
+
660
+ slideUp(duration = 300) {
661
+ return this.each((_, el) => {
662
+ el.style.overflow = 'hidden';
663
+ el.style.maxHeight = el.scrollHeight + 'px';
664
+ el.style.transition = `max-height ${duration}ms ease`;
665
+ requestAnimationFrame(() => { el.style.maxHeight = '0'; });
666
+ setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
667
+ });
668
+ }
669
+
336
670
  slideToggle(duration = 300) {
337
671
  return this.each((_, el) => {
338
672
  if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
@@ -480,7 +814,7 @@ query.children = (parentId) => {
480
814
  return new ZQueryCollection(p ? Array.from(p.children) : []);
481
815
  };
482
816
 
483
- // Create element shorthand
817
+ // Create element shorthand — returns ZQueryCollection for chaining
484
818
  query.create = (tag, attrs = {}, ...children) => {
485
819
  const el = document.createElement(tag);
486
820
  for (const [k, v] of Object.entries(attrs)) {
@@ -494,7 +828,7 @@ query.create = (tag, attrs = {}, ...children) => {
494
828
  if (typeof child === 'string') el.appendChild(document.createTextNode(child));
495
829
  else if (child instanceof Node) el.appendChild(child);
496
830
  });
497
- return el;
831
+ return new ZQueryCollection(el);
498
832
  };
499
833
 
500
834
  // DOM ready
@@ -503,17 +837,24 @@ query.ready = (fn) => {
503
837
  else document.addEventListener('DOMContentLoaded', fn);
504
838
  };
505
839
 
506
- // Global event listeners — supports direct and delegated forms
840
+ // Global event listeners — supports direct, delegated, and target-bound forms
507
841
  // $.on('keydown', handler) → direct listener on document
508
842
  // $.on('click', '.btn', handler) → delegated via closest()
843
+ // $.on('scroll', window, handler) → direct listener on target
509
844
  query.on = (event, selectorOrHandler, handler) => {
510
845
  if (typeof selectorOrHandler === 'function') {
511
846
  // 2-arg: direct document listener (keydown, resize, etc.)
512
847
  document.addEventListener(event, selectorOrHandler);
513
848
  return;
514
849
  }
515
- // 3-arg: delegated
850
+ // EventTarget (window, element, etc.) — direct listener on target
851
+ if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
852
+ selectorOrHandler.addEventListener(event, handler);
853
+ return;
854
+ }
855
+ // 3-arg string: delegated
516
856
  document.addEventListener(event, (e) => {
857
+ if (!e.target || typeof e.target.closest !== 'function') return;
517
858
  const target = e.target.closest(selectorOrHandler);
518
859
  if (target) handler.call(target, e);
519
860
  });