@bquery/bquery 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +527 -501
  2. package/dist/{batch-4LAvfLE7.js → batch-x7b2eZST.js} +2 -2
  3. package/dist/{batch-4LAvfLE7.js.map → batch-x7b2eZST.js.map} +1 -1
  4. package/dist/component.es.mjs +1 -1
  5. package/dist/core/collection.d.ts +19 -3
  6. package/dist/core/collection.d.ts.map +1 -1
  7. package/dist/core/element.d.ts +23 -4
  8. package/dist/core/element.d.ts.map +1 -1
  9. package/dist/core/index.d.ts +1 -0
  10. package/dist/core/index.d.ts.map +1 -1
  11. package/dist/core/utils/function.d.ts +21 -4
  12. package/dist/core/utils/function.d.ts.map +1 -1
  13. package/dist/{core-COenAZjD.js → core-BhpuvPhy.js} +62 -37
  14. package/dist/core-BhpuvPhy.js.map +1 -0
  15. package/dist/core.es.mjs +174 -131
  16. package/dist/core.es.mjs.map +1 -1
  17. package/dist/full.es.mjs +7 -7
  18. package/dist/full.iife.js +2 -2
  19. package/dist/full.iife.js.map +1 -1
  20. package/dist/full.umd.js +2 -2
  21. package/dist/full.umd.js.map +1 -1
  22. package/dist/index.es.mjs +7 -7
  23. package/dist/motion.es.mjs.map +1 -1
  24. package/dist/{persisted-Dz_ryNuC.js → persisted-DHoi3uEs.js} +4 -4
  25. package/dist/{persisted-Dz_ryNuC.js.map → persisted-DHoi3uEs.js.map} +1 -1
  26. package/dist/platform/storage.d.ts.map +1 -1
  27. package/dist/platform.es.mjs +12 -7
  28. package/dist/platform.es.mjs.map +1 -1
  29. package/dist/reactive/core.d.ts +12 -0
  30. package/dist/reactive/core.d.ts.map +1 -1
  31. package/dist/reactive/effect.d.ts.map +1 -1
  32. package/dist/reactive/internals.d.ts +6 -0
  33. package/dist/reactive/internals.d.ts.map +1 -1
  34. package/dist/reactive.es.mjs +6 -6
  35. package/dist/router.es.mjs +1 -1
  36. package/dist/{sanitize-1FBEPAFH.js → sanitize-Cxvxa-DX.js} +50 -39
  37. package/dist/sanitize-Cxvxa-DX.js.map +1 -0
  38. package/dist/security/sanitize-core.d.ts.map +1 -1
  39. package/dist/security.es.mjs +2 -2
  40. package/dist/store.es.mjs +2 -2
  41. package/dist/type-guards-BdKlYYlS.js +32 -0
  42. package/dist/type-guards-BdKlYYlS.js.map +1 -0
  43. package/dist/untrack-DNnnqdlR.js +6 -0
  44. package/dist/{untrack-BuEQKH7_.js.map → untrack-DNnnqdlR.js.map} +1 -1
  45. package/dist/view/evaluate.d.ts.map +1 -1
  46. package/dist/view.es.mjs +157 -151
  47. package/dist/view.es.mjs.map +1 -1
  48. package/dist/{watch-CXyaBC_9.js → watch-DXXv3iAI.js} +3 -3
  49. package/dist/{watch-CXyaBC_9.js.map → watch-DXXv3iAI.js.map} +1 -1
  50. package/package.json +132 -132
  51. package/src/core/collection.ts +628 -588
  52. package/src/core/element.ts +774 -746
  53. package/src/core/index.ts +48 -47
  54. package/src/core/utils/function.ts +151 -110
  55. package/src/motion/animate.ts +113 -113
  56. package/src/motion/flip.ts +176 -176
  57. package/src/motion/scroll.ts +57 -57
  58. package/src/motion/spring.ts +150 -150
  59. package/src/motion/timeline.ts +246 -246
  60. package/src/motion/transition.ts +51 -51
  61. package/src/platform/storage.ts +215 -208
  62. package/src/reactive/core.ts +114 -93
  63. package/src/reactive/effect.ts +54 -43
  64. package/src/reactive/internals.ts +122 -105
  65. package/src/security/sanitize-core.ts +364 -343
  66. package/src/view/evaluate.ts +290 -274
  67. package/dist/core-COenAZjD.js.map +0 -1
  68. package/dist/sanitize-1FBEPAFH.js.map +0 -1
  69. package/dist/type-guards-DRma3-Kc.js +0 -16
  70. package/dist/type-guards-DRma3-Kc.js.map +0 -1
  71. package/dist/untrack-BuEQKH7_.js +0 -6
@@ -1,588 +1,628 @@
1
- import {
2
- createElementFromHtml,
3
- insertContent,
4
- sanitizeContent,
5
- type InsertableContent,
6
- } from './dom';
7
- import { BQueryElement } from './element';
8
- import { applyAll, toElementList } from './shared';
9
-
10
- /** Handler signature for delegated events */
11
- type DelegatedHandler = (event: Event, target: Element) => void;
12
-
13
- /**
14
- * Wrapper for multiple DOM elements.
15
- * Provides batch operations on a collection of elements with chainable API.
16
- *
17
- * This class enables jQuery-like operations across multiple elements:
18
- * - All mutating methods apply to every element in the collection
19
- * - Getter methods return data from the first element
20
- * - Supports iteration via forEach, map, filter, and reduce
21
- *
22
- * @example
23
- * ```ts
24
- * $$('.items')
25
- * .addClass('highlight')
26
- * .css({ opacity: '0.8' })
27
- * .on('click', () => console.log('clicked'));
28
- * ```
29
- */
30
- export class BQueryCollection {
31
- /**
32
- * Stores delegated event handlers for cleanup via undelegate().
33
- * Outer map: element -> (key -> (handler -> wrapper))
34
- * Key format: `${event}:${selector}`
35
- * @internal
36
- */
37
- private readonly delegatedHandlers = new WeakMap<
38
- Element,
39
- Map<string, Map<DelegatedHandler, EventListener>>
40
- >();
41
-
42
- /**
43
- * Creates a new collection wrapper.
44
- * @param elements - Array of DOM elements to wrap
45
- */
46
- constructor(public readonly elements: Element[]) {}
47
-
48
- /**
49
- * Gets the number of elements in the collection.
50
- */
51
- get length(): number {
52
- return this.elements.length;
53
- }
54
-
55
- /**
56
- * Gets the first element in the collection, if any.
57
- * @internal
58
- */
59
- private first(): Element | undefined {
60
- return this.elements[0];
61
- }
62
-
63
- /**
64
- * Gets a single element as a BQueryElement wrapper.
65
- *
66
- * @param index - Zero-based index of the element
67
- * @returns BQueryElement wrapper or undefined if out of range
68
- */
69
- eq(index: number): BQueryElement | undefined {
70
- const el = this.elements[index];
71
- return el ? new BQueryElement(el) : undefined;
72
- }
73
-
74
- /**
75
- * Gets the first element as a BQueryElement wrapper.
76
- *
77
- * @returns BQueryElement wrapper or undefined if empty
78
- */
79
- firstEl(): BQueryElement | undefined {
80
- return this.eq(0);
81
- }
82
-
83
- /**
84
- * Gets the last element as a BQueryElement wrapper.
85
- *
86
- * @returns BQueryElement wrapper or undefined if empty
87
- */
88
- lastEl(): BQueryElement | undefined {
89
- return this.eq(this.elements.length - 1);
90
- }
91
-
92
- /**
93
- * Iterates over each element in the collection.
94
- *
95
- * @param callback - Function to call for each wrapped element
96
- * @returns The instance for method chaining
97
- */
98
- each(callback: (element: BQueryElement, index: number) => void): this {
99
- this.elements.forEach((element, index) => {
100
- callback(new BQueryElement(element), index);
101
- });
102
- return this;
103
- }
104
-
105
- /**
106
- * Maps each element to a new value.
107
- *
108
- * @param callback - Function to transform each element
109
- * @returns Array of transformed values
110
- */
111
- map<T>(callback: (element: Element, index: number) => T): T[] {
112
- return this.elements.map(callback);
113
- }
114
-
115
- /**
116
- * Filters elements based on a predicate.
117
- *
118
- * @param predicate - Function to test each element
119
- * @returns New BQueryCollection with matching elements
120
- */
121
- filter(predicate: (element: Element, index: number) => boolean): BQueryCollection {
122
- return new BQueryCollection(this.elements.filter(predicate));
123
- }
124
-
125
- /**
126
- * Reduces the collection to a single value.
127
- *
128
- * @param callback - Reducer function
129
- * @param initialValue - Initial accumulator value
130
- * @returns Accumulated result
131
- */
132
- reduce<T>(callback: (accumulator: T, element: Element, index: number) => T, initialValue: T): T {
133
- return this.elements.reduce(callback, initialValue);
134
- }
135
-
136
- /**
137
- * Converts the collection to an array of BQueryElement wrappers.
138
- *
139
- * @returns Array of BQueryElement instances
140
- */
141
- toArray(): BQueryElement[] {
142
- return this.elements.map((el) => new BQueryElement(el));
143
- }
144
-
145
- /** Add one or more classes to all elements. */
146
- addClass(...classNames: string[]): this {
147
- applyAll(this.elements, (el) => el.classList.add(...classNames));
148
- return this;
149
- }
150
-
151
- /** Remove one or more classes from all elements. */
152
- removeClass(...classNames: string[]): this {
153
- applyAll(this.elements, (el) => el.classList.remove(...classNames));
154
- return this;
155
- }
156
-
157
- /** Toggle a class on all elements. */
158
- toggleClass(className: string, force?: boolean): this {
159
- applyAll(this.elements, (el) => el.classList.toggle(className, force));
160
- return this;
161
- }
162
-
163
- /**
164
- * Sets an attribute on all elements or gets from first.
165
- *
166
- * @param name - Attribute name
167
- * @param value - Value to set (optional)
168
- * @returns Attribute value when getting, instance when setting
169
- */
170
- attr(name: string, value?: string): string | this {
171
- if (value === undefined) {
172
- return this.first()?.getAttribute(name) ?? '';
173
- }
174
- applyAll(this.elements, (el) => el.setAttribute(name, value));
175
- return this;
176
- }
177
-
178
- /**
179
- * Removes an attribute from all elements.
180
- *
181
- * @param name - Attribute name to remove
182
- * @returns The instance for method chaining
183
- */
184
- removeAttr(name: string): this {
185
- applyAll(this.elements, (el) => el.removeAttribute(name));
186
- return this;
187
- }
188
-
189
- /** Toggle an attribute on all elements. */
190
- toggleAttr(name: string, force?: boolean): this {
191
- applyAll(this.elements, (el) => {
192
- const hasAttr = el.hasAttribute(name);
193
- const shouldAdd = force ?? !hasAttr;
194
- if (shouldAdd) {
195
- el.setAttribute(name, '');
196
- } else {
197
- el.removeAttribute(name);
198
- }
199
- });
200
- return this;
201
- }
202
-
203
- /**
204
- * Sets text content on all elements or gets from first.
205
- *
206
- * @param value - Text to set (optional)
207
- * @returns Text content when getting, instance when setting
208
- */
209
- text(value?: string): string | this {
210
- if (value === undefined) {
211
- return this.first()?.textContent ?? '';
212
- }
213
- applyAll(this.elements, (el) => {
214
- el.textContent = value;
215
- });
216
- return this;
217
- }
218
-
219
- /**
220
- * Sets sanitized HTML on all elements or gets from first.
221
- *
222
- * @param value - HTML to set (optional, will be sanitized)
223
- * @returns HTML content when getting, instance when setting
224
- */
225
- html(value?: string): string | this {
226
- if (value === undefined) {
227
- return this.first()?.innerHTML ?? '';
228
- }
229
- const sanitized = sanitizeContent(value);
230
- applyAll(this.elements, (el) => {
231
- el.innerHTML = sanitized;
232
- });
233
- return this;
234
- }
235
-
236
- /**
237
- * Sets HTML on all elements without sanitization.
238
- *
239
- * @param value - Raw HTML to set
240
- * @returns The instance for method chaining
241
- * @warning Bypasses XSS protection
242
- */
243
- htmlUnsafe(value: string): this {
244
- applyAll(this.elements, (el) => {
245
- el.innerHTML = value;
246
- });
247
- return this;
248
- }
249
-
250
- /** Append content to all elements. */
251
- append(content: InsertableContent): this {
252
- this.insertAll(content, 'beforeend');
253
- return this;
254
- }
255
-
256
- /** Prepend content to all elements. */
257
- prepend(content: InsertableContent): this {
258
- this.insertAll(content, 'afterbegin');
259
- return this;
260
- }
261
-
262
- /** Insert content before all elements. */
263
- before(content: InsertableContent): this {
264
- this.insertAll(content, 'beforebegin');
265
- return this;
266
- }
267
-
268
- /** Insert content after all elements. */
269
- after(content: InsertableContent): this {
270
- this.insertAll(content, 'afterend');
271
- return this;
272
- }
273
-
274
- /**
275
- * Applies CSS styles to all elements.
276
- *
277
- * @param property - Property name or object of properties
278
- * @param value - Value when setting single property
279
- * @returns The instance for method chaining
280
- */
281
- css(property: string | Record<string, string>, value?: string): this {
282
- if (typeof property === 'string') {
283
- if (value !== undefined) {
284
- applyAll(this.elements, (el) => {
285
- (el as HTMLElement).style.setProperty(property, value);
286
- });
287
- }
288
- return this;
289
- }
290
-
291
- applyAll(this.elements, (el) => {
292
- for (const [key, val] of Object.entries(property)) {
293
- (el as HTMLElement).style.setProperty(key, val);
294
- }
295
- });
296
- return this;
297
- }
298
-
299
- /** Wrap each element with a wrapper element or tag. */
300
- wrap(wrapper: string | Element): this {
301
- this.elements.forEach((el, index) => {
302
- const wrapperEl =
303
- typeof wrapper === 'string'
304
- ? document.createElement(wrapper)
305
- : index === 0
306
- ? wrapper
307
- : (wrapper.cloneNode(true) as Element);
308
- el.parentNode?.insertBefore(wrapperEl, el);
309
- wrapperEl.appendChild(el);
310
- });
311
- return this;
312
- }
313
-
314
- /**
315
- * Remove the parent element of each element, keeping the elements in place.
316
- *
317
- * **Important**: This method unwraps ALL children of each parent element,
318
- * not just the elements in the collection. If you call `unwrap()` on a
319
- * collection containing only some children of a parent, all siblings will
320
- * also be unwrapped. This behavior is consistent with jQuery's `.unwrap()`.
321
- *
322
- * @returns The collection for chaining
323
- *
324
- * @example
325
- * ```ts
326
- * // HTML: <div><section><span>A</span><span>B</span></section></div>
327
- * const spans = $$('span');
328
- * spans.unwrap(); // Removes <section>, both spans move to <div>
329
- * // Result: <div><span>A</span><span>B</span></div>
330
- * ```
331
- */
332
- unwrap(): this {
333
- // Collect unique parent elements to avoid removing the same parent multiple times.
334
- const parents = new Set<Element>();
335
- for (const el of this.elements) {
336
- if (el.parentElement) {
337
- parents.add(el.parentElement);
338
- }
339
- }
340
-
341
- // Unwrap each parent once: move all children out, then remove the wrapper.
342
- parents.forEach((parent) => {
343
- const grandParent = parent.parentNode;
344
- if (!grandParent) return;
345
-
346
- while (parent.firstChild) {
347
- grandParent.insertBefore(parent.firstChild, parent);
348
- }
349
-
350
- parent.remove();
351
- });
352
- return this;
353
- }
354
-
355
- /** Replace each element with provided content. */
356
- replaceWith(content: string | Element): BQueryCollection {
357
- const replacements: Element[] = [];
358
- this.elements.forEach((el, index) => {
359
- const replacement =
360
- typeof content === 'string'
361
- ? createElementFromHtml(content)
362
- : index === 0
363
- ? content
364
- : (content.cloneNode(true) as Element);
365
- el.replaceWith(replacement);
366
- replacements.push(replacement);
367
- });
368
- return new BQueryCollection(replacements);
369
- }
370
-
371
- /**
372
- * Shows all elements.
373
- *
374
- * @param display - Optional display value (default: '')
375
- * @returns The instance for method chaining
376
- */
377
- show(display: string = ''): this {
378
- applyAll(this.elements, (el) => {
379
- el.removeAttribute('hidden');
380
- (el as HTMLElement).style.display = display;
381
- });
382
- return this;
383
- }
384
-
385
- /**
386
- * Hides all elements.
387
- *
388
- * @returns The instance for method chaining
389
- */
390
- hide(): this {
391
- applyAll(this.elements, (el) => {
392
- (el as HTMLElement).style.display = 'none';
393
- });
394
- return this;
395
- }
396
-
397
- /**
398
- * Adds an event listener to all elements.
399
- *
400
- * @param event - Event type
401
- * @param handler - Event handler
402
- * @returns The instance for method chaining
403
- */
404
- on(event: string, handler: EventListenerOrEventListenerObject): this {
405
- applyAll(this.elements, (el) => el.addEventListener(event, handler));
406
- return this;
407
- }
408
-
409
- /**
410
- * Adds a one-time event listener to all elements.
411
- *
412
- * @param event - Event type
413
- * @param handler - Event handler
414
- * @returns The instance for method chaining
415
- */
416
- once(event: string, handler: EventListener): this {
417
- applyAll(this.elements, (el) => el.addEventListener(event, handler, { once: true }));
418
- return this;
419
- }
420
-
421
- /**
422
- * Removes an event listener from all elements.
423
- *
424
- * @param event - Event type
425
- * @param handler - The handler to remove
426
- * @returns The instance for method chaining
427
- */
428
- off(event: string, handler: EventListenerOrEventListenerObject): this {
429
- applyAll(this.elements, (el) => el.removeEventListener(event, handler));
430
- return this;
431
- }
432
-
433
- /**
434
- * Triggers a custom event on all elements.
435
- *
436
- * @param event - Event type
437
- * @param detail - Optional event detail
438
- * @returns The instance for method chaining
439
- */
440
- trigger(event: string, detail?: unknown): this {
441
- applyAll(this.elements, (el) => {
442
- el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
443
- });
444
- return this;
445
- }
446
-
447
- /**
448
- * Adds a delegated event listener to all elements.
449
- * Events are delegated to matching descendants.
450
- *
451
- * Use `undelegate()` to remove the listener later.
452
- *
453
- * @param event - Event type to listen for
454
- * @param selector - CSS selector to match against event targets
455
- * @param handler - Event handler function
456
- * @returns The instance for method chaining
457
- *
458
- * @example
459
- * ```ts
460
- * const handler = (e, target) => console.log('Clicked:', target.textContent);
461
- * $$('.container').delegate('click', '.item', handler);
462
- *
463
- * // Later, remove the delegated listener:
464
- * $$('.container').undelegate('click', '.item', handler);
465
- * ```
466
- */
467
- delegate(
468
- event: string,
469
- selector: string,
470
- handler: (event: Event, target: Element) => void
471
- ): this {
472
- const key = `${event}:${selector}`;
473
-
474
- applyAll(this.elements, (el) => {
475
- const wrapper: EventListener = (e: Event) => {
476
- const target = (e.target as Element).closest(selector);
477
- if (target && el.contains(target)) {
478
- handler(e, target);
479
- }
480
- };
481
-
482
- // Get or create the handler maps for this element
483
- if (!this.delegatedHandlers.has(el)) {
484
- this.delegatedHandlers.set(el, new Map());
485
- }
486
- const elementHandlers = this.delegatedHandlers.get(el)!;
487
-
488
- if (!elementHandlers.has(key)) {
489
- elementHandlers.set(key, new Map());
490
- }
491
- elementHandlers.get(key)!.set(handler, wrapper);
492
-
493
- el.addEventListener(event, wrapper);
494
- });
495
-
496
- return this;
497
- }
498
-
499
- /**
500
- * Removes a delegated event listener previously added with `delegate()`.
501
- *
502
- * @param event - Event type that was registered
503
- * @param selector - CSS selector that was used
504
- * @param handler - The original handler function passed to delegate()
505
- * @returns The instance for method chaining
506
- *
507
- * @example
508
- * ```ts
509
- * const handler = (e, target) => console.log('Clicked:', target.textContent);
510
- * $$('.container').delegate('click', '.item', handler);
511
- *
512
- * // Remove the delegated listener:
513
- * $$('.container').undelegate('click', '.item', handler);
514
- * ```
515
- */
516
- undelegate(
517
- event: string,
518
- selector: string,
519
- handler: (event: Event, target: Element) => void
520
- ): this {
521
- const key = `${event}:${selector}`;
522
-
523
- applyAll(this.elements, (el) => {
524
- const elementHandlers = this.delegatedHandlers.get(el);
525
- if (!elementHandlers) return;
526
-
527
- const handlers = elementHandlers.get(key);
528
- if (!handlers) return;
529
-
530
- const wrapper = handlers.get(handler);
531
- if (wrapper) {
532
- el.removeEventListener(event, wrapper);
533
- handlers.delete(handler);
534
-
535
- // Clean up empty maps
536
- if (handlers.size === 0) {
537
- elementHandlers.delete(key);
538
- }
539
- if (elementHandlers.size === 0) {
540
- this.delegatedHandlers.delete(el);
541
- }
542
- }
543
- });
544
-
545
- return this;
546
- }
547
-
548
- /**
549
- * Removes all elements from the DOM.
550
- *
551
- * @returns The instance for method chaining
552
- */
553
- remove(): this {
554
- applyAll(this.elements, (el) => el.remove());
555
- return this;
556
- }
557
-
558
- /**
559
- * Clears all child nodes from all elements.
560
- *
561
- * @returns The instance for method chaining
562
- */
563
- empty(): this {
564
- applyAll(this.elements, (el) => {
565
- el.innerHTML = '';
566
- });
567
- return this;
568
- }
569
-
570
- /** @internal */
571
- private insertAll(content: InsertableContent, position: InsertPosition): void {
572
- if (typeof content === 'string') {
573
- // Sanitize once and reuse for all elements
574
- const sanitized = sanitizeContent(content);
575
- applyAll(this.elements, (el) => {
576
- el.insertAdjacentHTML(position, sanitized);
577
- });
578
- return;
579
- }
580
-
581
- const elements = toElementList(content);
582
- this.elements.forEach((el, index) => {
583
- const nodes =
584
- index === 0 ? elements : elements.map((node) => node.cloneNode(true) as Element);
585
- insertContent(el, nodes, position);
586
- });
587
- }
588
- }
1
+ import {
2
+ createElementFromHtml,
3
+ insertContent,
4
+ sanitizeContent,
5
+ type InsertableContent,
6
+ } from './dom';
7
+ import { BQueryElement } from './element';
8
+ import { applyAll, toElementList } from './shared';
9
+
10
+ /** Handler signature for delegated events */
11
+ type DelegatedHandler = (event: Event, target: Element) => void;
12
+
13
+ /**
14
+ * Wrapper for multiple DOM elements.
15
+ * Provides batch operations on a collection of elements with chainable API.
16
+ *
17
+ * This class enables jQuery-like operations across multiple elements:
18
+ * - All mutating methods apply to every element in the collection
19
+ * - Getter methods return data from the first element
20
+ * - Supports iteration via forEach, map, filter, and reduce
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * $$('.items')
25
+ * .addClass('highlight')
26
+ * .css({ opacity: '0.8' })
27
+ * .on('click', () => console.log('clicked'));
28
+ * ```
29
+ */
30
+ export class BQueryCollection {
31
+ /**
32
+ * Stores delegated event handlers for cleanup via undelegate().
33
+ * Outer map: element -> (key -> (handler -> wrapper))
34
+ * Key format: `${event}:${selector}`
35
+ * @internal
36
+ */
37
+ private readonly delegatedHandlers = new WeakMap<
38
+ Element,
39
+ Map<string, Map<DelegatedHandler, EventListener>>
40
+ >();
41
+
42
+ /**
43
+ * Creates a new collection wrapper.
44
+ * @param elements - Array of DOM elements to wrap
45
+ */
46
+ constructor(public readonly elements: Element[]) {}
47
+
48
+ /**
49
+ * Gets the number of elements in the collection.
50
+ */
51
+ get length(): number {
52
+ return this.elements.length;
53
+ }
54
+
55
+ /**
56
+ * Gets the first element in the collection, if any.
57
+ * @internal
58
+ */
59
+ private first(): Element | undefined {
60
+ return this.elements[0];
61
+ }
62
+
63
+ /**
64
+ * Gets a single element as a BQueryElement wrapper.
65
+ *
66
+ * @param index - Zero-based index of the element
67
+ * @returns BQueryElement wrapper or undefined if out of range
68
+ */
69
+ eq(index: number): BQueryElement | undefined {
70
+ const el = this.elements[index];
71
+ return el ? new BQueryElement(el) : undefined;
72
+ }
73
+
74
+ /**
75
+ * Gets the first element as a BQueryElement wrapper.
76
+ *
77
+ * @returns BQueryElement wrapper or undefined if empty
78
+ */
79
+ firstEl(): BQueryElement | undefined {
80
+ return this.eq(0);
81
+ }
82
+
83
+ /**
84
+ * Gets the last element as a BQueryElement wrapper.
85
+ *
86
+ * @returns BQueryElement wrapper or undefined if empty
87
+ */
88
+ lastEl(): BQueryElement | undefined {
89
+ return this.eq(this.elements.length - 1);
90
+ }
91
+
92
+ /**
93
+ * Iterates over each element in the collection.
94
+ *
95
+ * @param callback - Function to call for each wrapped element
96
+ * @returns The instance for method chaining
97
+ */
98
+ each(callback: (element: BQueryElement, index: number) => void): this {
99
+ this.elements.forEach((element, index) => {
100
+ callback(new BQueryElement(element), index);
101
+ });
102
+ return this;
103
+ }
104
+
105
+ /**
106
+ * Maps each element to a new value.
107
+ *
108
+ * @param callback - Function to transform each element
109
+ * @returns Array of transformed values
110
+ */
111
+ map<T>(callback: (element: Element, index: number) => T): T[] {
112
+ return this.elements.map(callback);
113
+ }
114
+
115
+ /**
116
+ * Filters elements based on a predicate.
117
+ *
118
+ * @param predicate - Function to test each element
119
+ * @returns New BQueryCollection with matching elements
120
+ */
121
+ filter(predicate: (element: Element, index: number) => boolean): BQueryCollection {
122
+ return new BQueryCollection(this.elements.filter(predicate));
123
+ }
124
+
125
+ /**
126
+ * Reduces the collection to a single value.
127
+ *
128
+ * @param callback - Reducer function
129
+ * @param initialValue - Initial accumulator value
130
+ * @returns Accumulated result
131
+ */
132
+ reduce<T>(callback: (accumulator: T, element: Element, index: number) => T, initialValue: T): T {
133
+ return this.elements.reduce(callback, initialValue);
134
+ }
135
+
136
+ /**
137
+ * Converts the collection to an array of BQueryElement wrappers.
138
+ *
139
+ * @returns Array of BQueryElement instances
140
+ */
141
+ toArray(): BQueryElement[] {
142
+ return this.elements.map((el) => new BQueryElement(el));
143
+ }
144
+
145
+ /** Add one or more classes to all elements. */
146
+ addClass(...classNames: string[]): this {
147
+ applyAll(this.elements, (el) => el.classList.add(...classNames));
148
+ return this;
149
+ }
150
+
151
+ /** Remove one or more classes from all elements. */
152
+ removeClass(...classNames: string[]): this {
153
+ applyAll(this.elements, (el) => el.classList.remove(...classNames));
154
+ return this;
155
+ }
156
+
157
+ /** Toggle a class on all elements. */
158
+ toggleClass(className: string, force?: boolean): this {
159
+ applyAll(this.elements, (el) => el.classList.toggle(className, force));
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Sets an attribute on all elements or gets from first.
165
+ *
166
+ * @param name - Attribute name
167
+ * @param value - Value to set (optional)
168
+ * @returns Attribute value when getting, instance when setting
169
+ */
170
+ attr(name: string, value?: string): string | this {
171
+ if (value === undefined) {
172
+ return this.first()?.getAttribute(name) ?? '';
173
+ }
174
+ applyAll(this.elements, (el) => el.setAttribute(name, value));
175
+ return this;
176
+ }
177
+
178
+ /**
179
+ * Removes an attribute from all elements.
180
+ *
181
+ * @param name - Attribute name to remove
182
+ * @returns The instance for method chaining
183
+ */
184
+ removeAttr(name: string): this {
185
+ applyAll(this.elements, (el) => el.removeAttribute(name));
186
+ return this;
187
+ }
188
+
189
+ /** Toggle an attribute on all elements. */
190
+ toggleAttr(name: string, force?: boolean): this {
191
+ applyAll(this.elements, (el) => {
192
+ const hasAttr = el.hasAttribute(name);
193
+ const shouldAdd = force ?? !hasAttr;
194
+ if (shouldAdd) {
195
+ el.setAttribute(name, '');
196
+ } else {
197
+ el.removeAttribute(name);
198
+ }
199
+ });
200
+ return this;
201
+ }
202
+
203
+ /**
204
+ * Sets text content on all elements or gets from first.
205
+ *
206
+ * @param value - Text to set (optional)
207
+ * @returns Text content when getting, instance when setting
208
+ */
209
+ text(value?: string): string | this {
210
+ if (value === undefined) {
211
+ return this.first()?.textContent ?? '';
212
+ }
213
+ applyAll(this.elements, (el) => {
214
+ el.textContent = value;
215
+ });
216
+ return this;
217
+ }
218
+
219
+ /**
220
+ * Sets sanitized HTML on all elements or gets from first.
221
+ *
222
+ * @param value - HTML to set (optional, will be sanitized)
223
+ * @returns HTML content when getting, instance when setting
224
+ */
225
+ html(value?: string): string | this {
226
+ if (value === undefined) {
227
+ return this.first()?.innerHTML ?? '';
228
+ }
229
+ const sanitized = sanitizeContent(value);
230
+ applyAll(this.elements, (el) => {
231
+ el.innerHTML = sanitized;
232
+ });
233
+ return this;
234
+ }
235
+
236
+ /**
237
+ * Sets HTML on all elements without sanitization.
238
+ *
239
+ * @param value - Raw HTML to set
240
+ * @returns The instance for method chaining
241
+ * @warning Bypasses XSS protection
242
+ */
243
+ htmlUnsafe(value: string): this {
244
+ applyAll(this.elements, (el) => {
245
+ el.innerHTML = value;
246
+ });
247
+ return this;
248
+ }
249
+
250
+ /** Append content to all elements. */
251
+ append(content: InsertableContent): this {
252
+ this.insertAll(content, 'beforeend');
253
+ return this;
254
+ }
255
+
256
+ /** Prepend content to all elements. */
257
+ prepend(content: InsertableContent): this {
258
+ this.insertAll(content, 'afterbegin');
259
+ return this;
260
+ }
261
+
262
+ /** Insert content before all elements. */
263
+ before(content: InsertableContent): this {
264
+ this.insertAll(content, 'beforebegin');
265
+ return this;
266
+ }
267
+
268
+ /** Insert content after all elements. */
269
+ after(content: InsertableContent): this {
270
+ this.insertAll(content, 'afterend');
271
+ return this;
272
+ }
273
+
274
+ /**
275
+ * Gets or sets CSS styles on all elements.
276
+ * When getting, returns the computed style value from the first element.
277
+ *
278
+ * @param property - Property name or object of properties
279
+ * @param value - Value when setting single property
280
+ * @returns The computed style value when getting, instance when setting
281
+ */
282
+ css(property: string): string;
283
+ css(property: string, value: string): this;
284
+ css(property: Record<string, string>): this;
285
+ css(property: string | Record<string, string>, value?: string): string | this {
286
+ if (typeof property === 'string') {
287
+ if (value !== undefined) {
288
+ applyAll(this.elements, (el) => {
289
+ (el as HTMLElement).style.setProperty(property, value);
290
+ });
291
+ return this;
292
+ }
293
+ const first = this.first();
294
+ if (!first) {
295
+ return '';
296
+ }
297
+ const view = first.ownerDocument?.defaultView;
298
+ if (!view || typeof view.getComputedStyle !== 'function') {
299
+ return '';
300
+ }
301
+ return view.getComputedStyle(first).getPropertyValue(property);
302
+ }
303
+
304
+ applyAll(this.elements, (el) => {
305
+ for (const [key, val] of Object.entries(property)) {
306
+ (el as HTMLElement).style.setProperty(key, val);
307
+ }
308
+ });
309
+ return this;
310
+ }
311
+
312
+ /** Wrap each element with a wrapper element or tag. */
313
+ wrap(wrapper: string | Element): this {
314
+ this.elements.forEach((el, index) => {
315
+ const wrapperEl =
316
+ typeof wrapper === 'string'
317
+ ? document.createElement(wrapper)
318
+ : index === 0
319
+ ? wrapper
320
+ : (wrapper.cloneNode(true) as Element);
321
+ el.parentNode?.insertBefore(wrapperEl, el);
322
+ wrapperEl.appendChild(el);
323
+ });
324
+ return this;
325
+ }
326
+
327
+ /**
328
+ * Remove the parent element of each element, keeping the elements in place.
329
+ *
330
+ * **Important**: This method unwraps ALL children of each parent element,
331
+ * not just the elements in the collection. If you call `unwrap()` on a
332
+ * collection containing only some children of a parent, all siblings will
333
+ * also be unwrapped. This behavior is consistent with jQuery's `.unwrap()`.
334
+ *
335
+ * @returns The collection for chaining
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * // HTML: <div><section><span>A</span><span>B</span></section></div>
340
+ * const spans = $$('span');
341
+ * spans.unwrap(); // Removes <section>, both spans move to <div>
342
+ * // Result: <div><span>A</span><span>B</span></div>
343
+ * ```
344
+ */
345
+ unwrap(): this {
346
+ // Collect unique parent elements to avoid removing the same parent multiple times.
347
+ const parents = new Set<Element>();
348
+ for (const el of this.elements) {
349
+ if (el.parentElement) {
350
+ parents.add(el.parentElement);
351
+ }
352
+ }
353
+
354
+ // Unwrap each parent once: move all children out, then remove the wrapper.
355
+ parents.forEach((parent) => {
356
+ const grandParent = parent.parentNode;
357
+ if (!grandParent) return;
358
+
359
+ while (parent.firstChild) {
360
+ grandParent.insertBefore(parent.firstChild, parent);
361
+ }
362
+
363
+ parent.remove();
364
+ });
365
+ return this;
366
+ }
367
+
368
+ /** Replace each element with provided content. */
369
+ replaceWith(content: string | Element): BQueryCollection {
370
+ const replacements: Element[] = [];
371
+ this.elements.forEach((el, index) => {
372
+ const replacement =
373
+ typeof content === 'string'
374
+ ? createElementFromHtml(content)
375
+ : index === 0
376
+ ? content
377
+ : (content.cloneNode(true) as Element);
378
+ el.replaceWith(replacement);
379
+ replacements.push(replacement);
380
+ });
381
+ return new BQueryCollection(replacements);
382
+ }
383
+
384
+ /**
385
+ * Shows all elements.
386
+ *
387
+ * @param display - Optional display value (default: '')
388
+ * @returns The instance for method chaining
389
+ */
390
+ show(display: string = ''): this {
391
+ applyAll(this.elements, (el) => {
392
+ el.removeAttribute('hidden');
393
+ (el as HTMLElement).style.display = display;
394
+ });
395
+ return this;
396
+ }
397
+
398
+ /**
399
+ * Hides all elements.
400
+ *
401
+ * @returns The instance for method chaining
402
+ */
403
+ hide(): this {
404
+ applyAll(this.elements, (el) => {
405
+ (el as HTMLElement).style.display = 'none';
406
+ });
407
+ return this;
408
+ }
409
+
410
+ /**
411
+ * Adds an event listener to all elements.
412
+ *
413
+ * @param event - Event type
414
+ * @param handler - Event handler
415
+ * @returns The instance for method chaining
416
+ */
417
+ on(event: string, handler: EventListenerOrEventListenerObject): this {
418
+ applyAll(this.elements, (el) => el.addEventListener(event, handler));
419
+ return this;
420
+ }
421
+
422
+ /**
423
+ * Adds a one-time event listener to all elements.
424
+ *
425
+ * @param event - Event type
426
+ * @param handler - Event handler
427
+ * @returns The instance for method chaining
428
+ */
429
+ once(event: string, handler: EventListener): this {
430
+ applyAll(this.elements, (el) => el.addEventListener(event, handler, { once: true }));
431
+ return this;
432
+ }
433
+
434
+ /**
435
+ * Removes an event listener from all elements.
436
+ *
437
+ * @param event - Event type
438
+ * @param handler - The handler to remove
439
+ * @returns The instance for method chaining
440
+ */
441
+ off(event: string, handler: EventListenerOrEventListenerObject): this {
442
+ applyAll(this.elements, (el) => el.removeEventListener(event, handler));
443
+ return this;
444
+ }
445
+
446
+ /**
447
+ * Triggers a custom event on all elements.
448
+ *
449
+ * @param event - Event type
450
+ * @param detail - Optional event detail
451
+ * @returns The instance for method chaining
452
+ */
453
+ trigger(event: string, detail?: unknown): this {
454
+ applyAll(this.elements, (el) => {
455
+ el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
456
+ });
457
+ return this;
458
+ }
459
+
460
+ /**
461
+ * Adds a delegated event listener to all elements.
462
+ * Events are delegated to matching descendants.
463
+ *
464
+ * Use `undelegate()` to remove the listener later.
465
+ *
466
+ * @param event - Event type to listen for
467
+ * @param selector - CSS selector to match against event targets
468
+ * @param handler - Event handler function
469
+ * @returns The instance for method chaining
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * const handler = (e, target) => console.log('Clicked:', target.textContent);
474
+ * $$('.container').delegate('click', '.item', handler);
475
+ *
476
+ * // Later, remove the delegated listener:
477
+ * $$('.container').undelegate('click', '.item', handler);
478
+ * ```
479
+ */
480
+ delegate(
481
+ event: string,
482
+ selector: string,
483
+ handler: (event: Event, target: Element) => void
484
+ ): this {
485
+ const key = `${event}:${selector}`;
486
+
487
+ applyAll(this.elements, (el) => {
488
+ const wrapper: EventListener = (e: Event) => {
489
+ const target = (e.target as Element).closest(selector);
490
+ if (target && el.contains(target)) {
491
+ handler(e, target);
492
+ }
493
+ };
494
+
495
+ // Get or create the handler maps for this element
496
+ if (!this.delegatedHandlers.has(el)) {
497
+ this.delegatedHandlers.set(el, new Map());
498
+ }
499
+ const elementHandlers = this.delegatedHandlers.get(el)!;
500
+
501
+ if (!elementHandlers.has(key)) {
502
+ elementHandlers.set(key, new Map());
503
+ }
504
+ elementHandlers.get(key)!.set(handler, wrapper);
505
+
506
+ el.addEventListener(event, wrapper);
507
+ });
508
+
509
+ return this;
510
+ }
511
+
512
+ /**
513
+ * Removes a delegated event listener previously added with `delegate()`.
514
+ *
515
+ * @param event - Event type that was registered
516
+ * @param selector - CSS selector that was used
517
+ * @param handler - The original handler function passed to delegate()
518
+ * @returns The instance for method chaining
519
+ *
520
+ * @example
521
+ * ```ts
522
+ * const handler = (e, target) => console.log('Clicked:', target.textContent);
523
+ * $$('.container').delegate('click', '.item', handler);
524
+ *
525
+ * // Remove the delegated listener:
526
+ * $$('.container').undelegate('click', '.item', handler);
527
+ * ```
528
+ */
529
+ undelegate(
530
+ event: string,
531
+ selector: string,
532
+ handler: (event: Event, target: Element) => void
533
+ ): this {
534
+ const key = `${event}:${selector}`;
535
+
536
+ applyAll(this.elements, (el) => {
537
+ const elementHandlers = this.delegatedHandlers.get(el);
538
+ if (!elementHandlers) return;
539
+
540
+ const handlers = elementHandlers.get(key);
541
+ if (!handlers) return;
542
+
543
+ const wrapper = handlers.get(handler);
544
+ if (wrapper) {
545
+ el.removeEventListener(event, wrapper);
546
+ handlers.delete(handler);
547
+
548
+ // Clean up empty maps
549
+ if (handlers.size === 0) {
550
+ elementHandlers.delete(key);
551
+ }
552
+ if (elementHandlers.size === 0) {
553
+ this.delegatedHandlers.delete(el);
554
+ }
555
+ }
556
+ });
557
+
558
+ return this;
559
+ }
560
+
561
+ /**
562
+ * Finds all descendant elements matching the selector across all elements
563
+ * in the collection. Returns a new BQueryCollection with the results.
564
+ *
565
+ * @param selector - CSS selector to match
566
+ * @returns A new BQueryCollection with all matching descendants
567
+ *
568
+ * @example
569
+ * ```ts
570
+ * $$('.container').find('.item').addClass('highlight');
571
+ * ```
572
+ */
573
+ find(selector: string): BQueryCollection {
574
+ const seen = new Set<Element>();
575
+ const results: Element[] = [];
576
+ for (const el of this.elements) {
577
+ const found = el.querySelectorAll(selector);
578
+ for (let i = 0; i < found.length; i++) {
579
+ if (!seen.has(found[i])) {
580
+ seen.add(found[i]);
581
+ results.push(found[i]);
582
+ }
583
+ }
584
+ }
585
+ return new BQueryCollection(results);
586
+ }
587
+
588
+ /**
589
+ * Removes all elements from the DOM.
590
+ *
591
+ * @returns The instance for method chaining
592
+ */
593
+ remove(): this {
594
+ applyAll(this.elements, (el) => el.remove());
595
+ return this;
596
+ }
597
+
598
+ /**
599
+ * Clears all child nodes from all elements.
600
+ *
601
+ * @returns The instance for method chaining
602
+ */
603
+ empty(): this {
604
+ applyAll(this.elements, (el) => {
605
+ el.innerHTML = '';
606
+ });
607
+ return this;
608
+ }
609
+
610
+ /** @internal */
611
+ private insertAll(content: InsertableContent, position: InsertPosition): void {
612
+ if (typeof content === 'string') {
613
+ // Sanitize once and reuse for all elements
614
+ const sanitized = sanitizeContent(content);
615
+ applyAll(this.elements, (el) => {
616
+ el.insertAdjacentHTML(position, sanitized);
617
+ });
618
+ return;
619
+ }
620
+
621
+ const elements = toElementList(content);
622
+ this.elements.forEach((el, index) => {
623
+ const nodes =
624
+ index === 0 ? elements : elements.map((node) => node.cloneNode(true) as Element);
625
+ insertContent(el, nodes, position);
626
+ });
627
+ }
628
+ }