@bquery/bquery 1.0.2 → 1.1.1

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 (41) hide show
  1. package/README.md +61 -7
  2. package/dist/component/index.d.ts +8 -0
  3. package/dist/component/index.d.ts.map +1 -1
  4. package/dist/component.es.mjs +80 -53
  5. package/dist/component.es.mjs.map +1 -1
  6. package/dist/core/collection.d.ts +46 -0
  7. package/dist/core/collection.d.ts.map +1 -1
  8. package/dist/core/element.d.ts +124 -22
  9. package/dist/core/element.d.ts.map +1 -1
  10. package/dist/core/utils.d.ts +13 -0
  11. package/dist/core/utils.d.ts.map +1 -1
  12. package/dist/core.es.mjs +298 -55
  13. package/dist/core.es.mjs.map +1 -1
  14. package/dist/full.d.ts +2 -2
  15. package/dist/full.d.ts.map +1 -1
  16. package/dist/full.es.mjs +38 -33
  17. package/dist/full.iife.js +1 -1
  18. package/dist/full.iife.js.map +1 -1
  19. package/dist/full.umd.js +1 -1
  20. package/dist/full.umd.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.es.mjs +38 -33
  23. package/dist/reactive/index.d.ts +2 -2
  24. package/dist/reactive/index.d.ts.map +1 -1
  25. package/dist/reactive/signal.d.ts +107 -0
  26. package/dist/reactive/signal.d.ts.map +1 -1
  27. package/dist/reactive.es.mjs +92 -55
  28. package/dist/reactive.es.mjs.map +1 -1
  29. package/dist/security/sanitize.d.ts.map +1 -1
  30. package/dist/security.es.mjs +136 -66
  31. package/dist/security.es.mjs.map +1 -1
  32. package/package.json +16 -16
  33. package/src/component/index.ts +414 -360
  34. package/src/core/collection.ts +454 -339
  35. package/src/core/element.ts +740 -493
  36. package/src/core/utils.ts +444 -425
  37. package/src/full.ts +106 -101
  38. package/src/index.ts +27 -27
  39. package/src/reactive/index.ts +22 -9
  40. package/src/reactive/signal.ts +506 -347
  41. package/src/security/sanitize.ts +553 -446
@@ -1,339 +1,454 @@
1
- import { sanitizeHtml } from '../security/sanitize';
2
- import { BQueryElement } from './element';
3
- import { applyAll } from './shared';
4
-
5
- /**
6
- * Wrapper for multiple DOM elements.
7
- * Provides batch operations on a collection of elements with chainable API.
8
- *
9
- * This class enables jQuery-like operations across multiple elements:
10
- * - All mutating methods apply to every element in the collection
11
- * - Getter methods return data from the first element
12
- * - Supports iteration via forEach, map, filter, and reduce
13
- *
14
- * @example
15
- * ```ts
16
- * $$('.items')
17
- * .addClass('highlight')
18
- * .css({ opacity: '0.8' })
19
- * .on('click', () => console.log('clicked'));
20
- * ```
21
- */
22
- export class BQueryCollection {
23
- /**
24
- * Creates a new collection wrapper.
25
- * @param elements - Array of DOM elements to wrap
26
- */
27
- constructor(public readonly elements: Element[]) {}
28
-
29
- /**
30
- * Gets the number of elements in the collection.
31
- */
32
- get length(): number {
33
- return this.elements.length;
34
- }
35
-
36
- /**
37
- * Gets the first element in the collection, if any.
38
- * @internal
39
- */
40
- private first(): Element | undefined {
41
- return this.elements[0];
42
- }
43
-
44
- /**
45
- * Gets a single element as a BQueryElement wrapper.
46
- *
47
- * @param index - Zero-based index of the element
48
- * @returns BQueryElement wrapper or undefined if out of range
49
- */
50
- eq(index: number): BQueryElement | undefined {
51
- const el = this.elements[index];
52
- return el ? new BQueryElement(el) : undefined;
53
- }
54
-
55
- /**
56
- * Gets the first element as a BQueryElement wrapper.
57
- *
58
- * @returns BQueryElement wrapper or undefined if empty
59
- */
60
- firstEl(): BQueryElement | undefined {
61
- return this.eq(0);
62
- }
63
-
64
- /**
65
- * Gets the last element as a BQueryElement wrapper.
66
- *
67
- * @returns BQueryElement wrapper or undefined if empty
68
- */
69
- lastEl(): BQueryElement | undefined {
70
- return this.eq(this.elements.length - 1);
71
- }
72
-
73
- /**
74
- * Iterates over each element in the collection.
75
- *
76
- * @param callback - Function to call for each wrapped element
77
- * @returns The instance for method chaining
78
- */
79
- each(callback: (element: BQueryElement, index: number) => void): this {
80
- this.elements.forEach((element, index) => {
81
- callback(new BQueryElement(element), index);
82
- });
83
- return this;
84
- }
85
-
86
- /**
87
- * Maps each element to a new value.
88
- *
89
- * @param callback - Function to transform each element
90
- * @returns Array of transformed values
91
- */
92
- map<T>(callback: (element: Element, index: number) => T): T[] {
93
- return this.elements.map(callback);
94
- }
95
-
96
- /**
97
- * Filters elements based on a predicate.
98
- *
99
- * @param predicate - Function to test each element
100
- * @returns New BQueryCollection with matching elements
101
- */
102
- filter(predicate: (element: Element, index: number) => boolean): BQueryCollection {
103
- return new BQueryCollection(this.elements.filter(predicate));
104
- }
105
-
106
- /**
107
- * Reduces the collection to a single value.
108
- *
109
- * @param callback - Reducer function
110
- * @param initialValue - Initial accumulator value
111
- * @returns Accumulated result
112
- */
113
- reduce<T>(callback: (accumulator: T, element: Element, index: number) => T, initialValue: T): T {
114
- return this.elements.reduce(callback, initialValue);
115
- }
116
-
117
- /**
118
- * Converts the collection to an array of BQueryElement wrappers.
119
- *
120
- * @returns Array of BQueryElement instances
121
- */
122
- toArray(): BQueryElement[] {
123
- return this.elements.map((el) => new BQueryElement(el));
124
- }
125
-
126
- /** Add one or more classes to all elements. */
127
- addClass(...classNames: string[]): this {
128
- applyAll(this.elements, (el) => el.classList.add(...classNames));
129
- return this;
130
- }
131
-
132
- /** Remove one or more classes from all elements. */
133
- removeClass(...classNames: string[]): this {
134
- applyAll(this.elements, (el) => el.classList.remove(...classNames));
135
- return this;
136
- }
137
-
138
- /** Toggle a class on all elements. */
139
- toggleClass(className: string, force?: boolean): this {
140
- applyAll(this.elements, (el) => el.classList.toggle(className, force));
141
- return this;
142
- }
143
-
144
- /**
145
- * Sets an attribute on all elements or gets from first.
146
- *
147
- * @param name - Attribute name
148
- * @param value - Value to set (optional)
149
- * @returns Attribute value when getting, instance when setting
150
- */
151
- attr(name: string, value?: string): string | this {
152
- if (value === undefined) {
153
- return this.first()?.getAttribute(name) ?? '';
154
- }
155
- applyAll(this.elements, (el) => el.setAttribute(name, value));
156
- return this;
157
- }
158
-
159
- /**
160
- * Removes an attribute from all elements.
161
- *
162
- * @param name - Attribute name to remove
163
- * @returns The instance for method chaining
164
- */
165
- removeAttr(name: string): this {
166
- applyAll(this.elements, (el) => el.removeAttribute(name));
167
- return this;
168
- }
169
-
170
- /**
171
- * Sets text content on all elements or gets from first.
172
- *
173
- * @param value - Text to set (optional)
174
- * @returns Text content when getting, instance when setting
175
- */
176
- text(value?: string): string | this {
177
- if (value === undefined) {
178
- return this.first()?.textContent ?? '';
179
- }
180
- applyAll(this.elements, (el) => {
181
- el.textContent = value;
182
- });
183
- return this;
184
- }
185
-
186
- /**
187
- * Sets sanitized HTML on all elements or gets from first.
188
- *
189
- * @param value - HTML to set (optional, will be sanitized)
190
- * @returns HTML content when getting, instance when setting
191
- */
192
- html(value?: string): string | this {
193
- if (value === undefined) {
194
- return this.first()?.innerHTML ?? '';
195
- }
196
- const sanitized = sanitizeHtml(value);
197
- applyAll(this.elements, (el) => {
198
- el.innerHTML = sanitized;
199
- });
200
- return this;
201
- }
202
-
203
- /**
204
- * Sets HTML on all elements without sanitization.
205
- *
206
- * @param value - Raw HTML to set
207
- * @returns The instance for method chaining
208
- * @warning Bypasses XSS protection
209
- */
210
- htmlUnsafe(value: string): this {
211
- applyAll(this.elements, (el) => {
212
- el.innerHTML = value;
213
- });
214
- return this;
215
- }
216
-
217
- /**
218
- * Applies CSS styles to all elements.
219
- *
220
- * @param property - Property name or object of properties
221
- * @param value - Value when setting single property
222
- * @returns The instance for method chaining
223
- */
224
- css(property: string | Record<string, string>, value?: string): this {
225
- if (typeof property === 'string') {
226
- if (value !== undefined) {
227
- applyAll(this.elements, (el) => {
228
- (el as HTMLElement).style.setProperty(property, value);
229
- });
230
- }
231
- return this;
232
- }
233
-
234
- applyAll(this.elements, (el) => {
235
- for (const [key, val] of Object.entries(property)) {
236
- (el as HTMLElement).style.setProperty(key, val);
237
- }
238
- });
239
- return this;
240
- }
241
-
242
- /**
243
- * Shows all elements.
244
- *
245
- * @param display - Optional display value (default: '')
246
- * @returns The instance for method chaining
247
- */
248
- show(display: string = ''): this {
249
- applyAll(this.elements, (el) => {
250
- el.removeAttribute('hidden');
251
- (el as HTMLElement).style.display = display;
252
- });
253
- return this;
254
- }
255
-
256
- /**
257
- * Hides all elements.
258
- *
259
- * @returns The instance for method chaining
260
- */
261
- hide(): this {
262
- applyAll(this.elements, (el) => {
263
- (el as HTMLElement).style.display = 'none';
264
- });
265
- return this;
266
- }
267
-
268
- /**
269
- * Adds an event listener to all elements.
270
- *
271
- * @param event - Event type
272
- * @param handler - Event handler
273
- * @returns The instance for method chaining
274
- */
275
- on(event: string, handler: EventListenerOrEventListenerObject): this {
276
- applyAll(this.elements, (el) => el.addEventListener(event, handler));
277
- return this;
278
- }
279
-
280
- /**
281
- * Adds a one-time event listener to all elements.
282
- *
283
- * @param event - Event type
284
- * @param handler - Event handler
285
- * @returns The instance for method chaining
286
- */
287
- once(event: string, handler: EventListener): this {
288
- applyAll(this.elements, (el) => el.addEventListener(event, handler, { once: true }));
289
- return this;
290
- }
291
-
292
- /**
293
- * Removes an event listener from all elements.
294
- *
295
- * @param event - Event type
296
- * @param handler - The handler to remove
297
- * @returns The instance for method chaining
298
- */
299
- off(event: string, handler: EventListenerOrEventListenerObject): this {
300
- applyAll(this.elements, (el) => el.removeEventListener(event, handler));
301
- return this;
302
- }
303
-
304
- /**
305
- * Triggers a custom event on all elements.
306
- *
307
- * @param event - Event type
308
- * @param detail - Optional event detail
309
- * @returns The instance for method chaining
310
- */
311
- trigger(event: string, detail?: unknown): this {
312
- applyAll(this.elements, (el) => {
313
- el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
314
- });
315
- return this;
316
- }
317
-
318
- /**
319
- * Removes all elements from the DOM.
320
- *
321
- * @returns The instance for method chaining
322
- */
323
- remove(): this {
324
- applyAll(this.elements, (el) => el.remove());
325
- return this;
326
- }
327
-
328
- /**
329
- * Clears all child nodes from all elements.
330
- *
331
- * @returns The instance for method chaining
332
- */
333
- empty(): this {
334
- applyAll(this.elements, (el) => {
335
- el.innerHTML = '';
336
- });
337
- return this;
338
- }
339
- }
1
+ import { sanitizeHtml } from '../security/sanitize';
2
+ import { BQueryElement } from './element';
3
+ import { applyAll } from './shared';
4
+
5
+ /** Handler signature for delegated events */
6
+ type DelegatedHandler = (event: Event, target: Element) => void;
7
+
8
+ /**
9
+ * Wrapper for multiple DOM elements.
10
+ * Provides batch operations on a collection of elements with chainable API.
11
+ *
12
+ * This class enables jQuery-like operations across multiple elements:
13
+ * - All mutating methods apply to every element in the collection
14
+ * - Getter methods return data from the first element
15
+ * - Supports iteration via forEach, map, filter, and reduce
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * $$('.items')
20
+ * .addClass('highlight')
21
+ * .css({ opacity: '0.8' })
22
+ * .on('click', () => console.log('clicked'));
23
+ * ```
24
+ */
25
+ export class BQueryCollection {
26
+ /**
27
+ * Stores delegated event handlers for cleanup via undelegate().
28
+ * Outer map: element -> (key -> (handler -> wrapper))
29
+ * Key format: `${event}:${selector}`
30
+ * @internal
31
+ */
32
+ private readonly delegatedHandlers = new WeakMap<
33
+ Element,
34
+ Map<string, Map<DelegatedHandler, EventListener>>
35
+ >();
36
+
37
+ /**
38
+ * Creates a new collection wrapper.
39
+ * @param elements - Array of DOM elements to wrap
40
+ */
41
+ constructor(public readonly elements: Element[]) {}
42
+
43
+ /**
44
+ * Gets the number of elements in the collection.
45
+ */
46
+ get length(): number {
47
+ return this.elements.length;
48
+ }
49
+
50
+ /**
51
+ * Gets the first element in the collection, if any.
52
+ * @internal
53
+ */
54
+ private first(): Element | undefined {
55
+ return this.elements[0];
56
+ }
57
+
58
+ /**
59
+ * Gets a single element as a BQueryElement wrapper.
60
+ *
61
+ * @param index - Zero-based index of the element
62
+ * @returns BQueryElement wrapper or undefined if out of range
63
+ */
64
+ eq(index: number): BQueryElement | undefined {
65
+ const el = this.elements[index];
66
+ return el ? new BQueryElement(el) : undefined;
67
+ }
68
+
69
+ /**
70
+ * Gets the first element as a BQueryElement wrapper.
71
+ *
72
+ * @returns BQueryElement wrapper or undefined if empty
73
+ */
74
+ firstEl(): BQueryElement | undefined {
75
+ return this.eq(0);
76
+ }
77
+
78
+ /**
79
+ * Gets the last element as a BQueryElement wrapper.
80
+ *
81
+ * @returns BQueryElement wrapper or undefined if empty
82
+ */
83
+ lastEl(): BQueryElement | undefined {
84
+ return this.eq(this.elements.length - 1);
85
+ }
86
+
87
+ /**
88
+ * Iterates over each element in the collection.
89
+ *
90
+ * @param callback - Function to call for each wrapped element
91
+ * @returns The instance for method chaining
92
+ */
93
+ each(callback: (element: BQueryElement, index: number) => void): this {
94
+ this.elements.forEach((element, index) => {
95
+ callback(new BQueryElement(element), index);
96
+ });
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Maps each element to a new value.
102
+ *
103
+ * @param callback - Function to transform each element
104
+ * @returns Array of transformed values
105
+ */
106
+ map<T>(callback: (element: Element, index: number) => T): T[] {
107
+ return this.elements.map(callback);
108
+ }
109
+
110
+ /**
111
+ * Filters elements based on a predicate.
112
+ *
113
+ * @param predicate - Function to test each element
114
+ * @returns New BQueryCollection with matching elements
115
+ */
116
+ filter(predicate: (element: Element, index: number) => boolean): BQueryCollection {
117
+ return new BQueryCollection(this.elements.filter(predicate));
118
+ }
119
+
120
+ /**
121
+ * Reduces the collection to a single value.
122
+ *
123
+ * @param callback - Reducer function
124
+ * @param initialValue - Initial accumulator value
125
+ * @returns Accumulated result
126
+ */
127
+ reduce<T>(callback: (accumulator: T, element: Element, index: number) => T, initialValue: T): T {
128
+ return this.elements.reduce(callback, initialValue);
129
+ }
130
+
131
+ /**
132
+ * Converts the collection to an array of BQueryElement wrappers.
133
+ *
134
+ * @returns Array of BQueryElement instances
135
+ */
136
+ toArray(): BQueryElement[] {
137
+ return this.elements.map((el) => new BQueryElement(el));
138
+ }
139
+
140
+ /** Add one or more classes to all elements. */
141
+ addClass(...classNames: string[]): this {
142
+ applyAll(this.elements, (el) => el.classList.add(...classNames));
143
+ return this;
144
+ }
145
+
146
+ /** Remove one or more classes from all elements. */
147
+ removeClass(...classNames: string[]): this {
148
+ applyAll(this.elements, (el) => el.classList.remove(...classNames));
149
+ return this;
150
+ }
151
+
152
+ /** Toggle a class on all elements. */
153
+ toggleClass(className: string, force?: boolean): this {
154
+ applyAll(this.elements, (el) => el.classList.toggle(className, force));
155
+ return this;
156
+ }
157
+
158
+ /**
159
+ * Sets an attribute on all elements or gets from first.
160
+ *
161
+ * @param name - Attribute name
162
+ * @param value - Value to set (optional)
163
+ * @returns Attribute value when getting, instance when setting
164
+ */
165
+ attr(name: string, value?: string): string | this {
166
+ if (value === undefined) {
167
+ return this.first()?.getAttribute(name) ?? '';
168
+ }
169
+ applyAll(this.elements, (el) => el.setAttribute(name, value));
170
+ return this;
171
+ }
172
+
173
+ /**
174
+ * Removes an attribute from all elements.
175
+ *
176
+ * @param name - Attribute name to remove
177
+ * @returns The instance for method chaining
178
+ */
179
+ removeAttr(name: string): this {
180
+ applyAll(this.elements, (el) => el.removeAttribute(name));
181
+ return this;
182
+ }
183
+
184
+ /**
185
+ * Sets text content on all elements or gets from first.
186
+ *
187
+ * @param value - Text to set (optional)
188
+ * @returns Text content when getting, instance when setting
189
+ */
190
+ text(value?: string): string | this {
191
+ if (value === undefined) {
192
+ return this.first()?.textContent ?? '';
193
+ }
194
+ applyAll(this.elements, (el) => {
195
+ el.textContent = value;
196
+ });
197
+ return this;
198
+ }
199
+
200
+ /**
201
+ * Sets sanitized HTML on all elements or gets from first.
202
+ *
203
+ * @param value - HTML to set (optional, will be sanitized)
204
+ * @returns HTML content when getting, instance when setting
205
+ */
206
+ html(value?: string): string | this {
207
+ if (value === undefined) {
208
+ return this.first()?.innerHTML ?? '';
209
+ }
210
+ const sanitized = sanitizeHtml(value);
211
+ applyAll(this.elements, (el) => {
212
+ el.innerHTML = sanitized;
213
+ });
214
+ return this;
215
+ }
216
+
217
+ /**
218
+ * Sets HTML on all elements without sanitization.
219
+ *
220
+ * @param value - Raw HTML to set
221
+ * @returns The instance for method chaining
222
+ * @warning Bypasses XSS protection
223
+ */
224
+ htmlUnsafe(value: string): this {
225
+ applyAll(this.elements, (el) => {
226
+ el.innerHTML = value;
227
+ });
228
+ return this;
229
+ }
230
+
231
+ /**
232
+ * Applies CSS styles to all elements.
233
+ *
234
+ * @param property - Property name or object of properties
235
+ * @param value - Value when setting single property
236
+ * @returns The instance for method chaining
237
+ */
238
+ css(property: string | Record<string, string>, value?: string): this {
239
+ if (typeof property === 'string') {
240
+ if (value !== undefined) {
241
+ applyAll(this.elements, (el) => {
242
+ (el as HTMLElement).style.setProperty(property, value);
243
+ });
244
+ }
245
+ return this;
246
+ }
247
+
248
+ applyAll(this.elements, (el) => {
249
+ for (const [key, val] of Object.entries(property)) {
250
+ (el as HTMLElement).style.setProperty(key, val);
251
+ }
252
+ });
253
+ return this;
254
+ }
255
+
256
+ /**
257
+ * Shows all elements.
258
+ *
259
+ * @param display - Optional display value (default: '')
260
+ * @returns The instance for method chaining
261
+ */
262
+ show(display: string = ''): this {
263
+ applyAll(this.elements, (el) => {
264
+ el.removeAttribute('hidden');
265
+ (el as HTMLElement).style.display = display;
266
+ });
267
+ return this;
268
+ }
269
+
270
+ /**
271
+ * Hides all elements.
272
+ *
273
+ * @returns The instance for method chaining
274
+ */
275
+ hide(): this {
276
+ applyAll(this.elements, (el) => {
277
+ (el as HTMLElement).style.display = 'none';
278
+ });
279
+ return this;
280
+ }
281
+
282
+ /**
283
+ * Adds an event listener to all elements.
284
+ *
285
+ * @param event - Event type
286
+ * @param handler - Event handler
287
+ * @returns The instance for method chaining
288
+ */
289
+ on(event: string, handler: EventListenerOrEventListenerObject): this {
290
+ applyAll(this.elements, (el) => el.addEventListener(event, handler));
291
+ return this;
292
+ }
293
+
294
+ /**
295
+ * Adds a one-time event listener to all elements.
296
+ *
297
+ * @param event - Event type
298
+ * @param handler - Event handler
299
+ * @returns The instance for method chaining
300
+ */
301
+ once(event: string, handler: EventListener): this {
302
+ applyAll(this.elements, (el) => el.addEventListener(event, handler, { once: true }));
303
+ return this;
304
+ }
305
+
306
+ /**
307
+ * Removes an event listener from all elements.
308
+ *
309
+ * @param event - Event type
310
+ * @param handler - The handler to remove
311
+ * @returns The instance for method chaining
312
+ */
313
+ off(event: string, handler: EventListenerOrEventListenerObject): this {
314
+ applyAll(this.elements, (el) => el.removeEventListener(event, handler));
315
+ return this;
316
+ }
317
+
318
+ /**
319
+ * Triggers a custom event on all elements.
320
+ *
321
+ * @param event - Event type
322
+ * @param detail - Optional event detail
323
+ * @returns The instance for method chaining
324
+ */
325
+ trigger(event: string, detail?: unknown): this {
326
+ applyAll(this.elements, (el) => {
327
+ el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
328
+ });
329
+ return this;
330
+ }
331
+
332
+ /**
333
+ * Adds a delegated event listener to all elements.
334
+ * Events are delegated to matching descendants.
335
+ *
336
+ * Use `undelegate()` to remove the listener later.
337
+ *
338
+ * @param event - Event type to listen for
339
+ * @param selector - CSS selector to match against event targets
340
+ * @param handler - Event handler function
341
+ * @returns The instance for method chaining
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * const handler = (e, target) => console.log('Clicked:', target.textContent);
346
+ * $$('.container').delegate('click', '.item', handler);
347
+ *
348
+ * // Later, remove the delegated listener:
349
+ * $$('.container').undelegate('click', '.item', handler);
350
+ * ```
351
+ */
352
+ delegate(
353
+ event: string,
354
+ selector: string,
355
+ handler: (event: Event, target: Element) => void
356
+ ): this {
357
+ const key = `${event}:${selector}`;
358
+
359
+ applyAll(this.elements, (el) => {
360
+ const wrapper: EventListener = (e: Event) => {
361
+ const target = (e.target as Element).closest(selector);
362
+ if (target && el.contains(target)) {
363
+ handler(e, target);
364
+ }
365
+ };
366
+
367
+ // Get or create the handler maps for this element
368
+ if (!this.delegatedHandlers.has(el)) {
369
+ this.delegatedHandlers.set(el, new Map());
370
+ }
371
+ const elementHandlers = this.delegatedHandlers.get(el)!;
372
+
373
+ if (!elementHandlers.has(key)) {
374
+ elementHandlers.set(key, new Map());
375
+ }
376
+ elementHandlers.get(key)!.set(handler, wrapper);
377
+
378
+ el.addEventListener(event, wrapper);
379
+ });
380
+
381
+ return this;
382
+ }
383
+
384
+ /**
385
+ * Removes a delegated event listener previously added with `delegate()`.
386
+ *
387
+ * @param event - Event type that was registered
388
+ * @param selector - CSS selector that was used
389
+ * @param handler - The original handler function passed to delegate()
390
+ * @returns The instance for method chaining
391
+ *
392
+ * @example
393
+ * ```ts
394
+ * const handler = (e, target) => console.log('Clicked:', target.textContent);
395
+ * $$('.container').delegate('click', '.item', handler);
396
+ *
397
+ * // Remove the delegated listener:
398
+ * $$('.container').undelegate('click', '.item', handler);
399
+ * ```
400
+ */
401
+ undelegate(
402
+ event: string,
403
+ selector: string,
404
+ handler: (event: Event, target: Element) => void
405
+ ): this {
406
+ const key = `${event}:${selector}`;
407
+
408
+ applyAll(this.elements, (el) => {
409
+ const elementHandlers = this.delegatedHandlers.get(el);
410
+ if (!elementHandlers) return;
411
+
412
+ const handlers = elementHandlers.get(key);
413
+ if (!handlers) return;
414
+
415
+ const wrapper = handlers.get(handler);
416
+ if (wrapper) {
417
+ el.removeEventListener(event, wrapper);
418
+ handlers.delete(handler);
419
+
420
+ // Clean up empty maps
421
+ if (handlers.size === 0) {
422
+ elementHandlers.delete(key);
423
+ }
424
+ if (elementHandlers.size === 0) {
425
+ this.delegatedHandlers.delete(el);
426
+ }
427
+ }
428
+ });
429
+
430
+ return this;
431
+ }
432
+
433
+ /**
434
+ * Removes all elements from the DOM.
435
+ *
436
+ * @returns The instance for method chaining
437
+ */
438
+ remove(): this {
439
+ applyAll(this.elements, (el) => el.remove());
440
+ return this;
441
+ }
442
+
443
+ /**
444
+ * Clears all child nodes from all elements.
445
+ *
446
+ * @returns The instance for method chaining
447
+ */
448
+ empty(): this {
449
+ applyAll(this.elements, (el) => {
450
+ el.innerHTML = '';
451
+ });
452
+ return this;
453
+ }
454
+ }