@bquery/bquery 1.0.1 → 1.1.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.
- package/README.md +79 -25
- package/dist/component/index.d.ts +8 -0
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component.es.mjs +80 -53
- package/dist/component.es.mjs.map +1 -1
- package/dist/core/collection.d.ts +46 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +124 -22
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/utils.d.ts +13 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core.es.mjs +298 -55
- package/dist/core.es.mjs.map +1 -1
- package/dist/full.d.ts +2 -2
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +38 -33
- package/dist/full.iife.js +1 -1
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +1 -1
- package/dist/full.umd.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.es.mjs +38 -33
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/signal.d.ts +107 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive.es.mjs +92 -55
- package/dist/reactive.es.mjs.map +1 -1
- package/dist/security/sanitize.d.ts.map +1 -1
- package/dist/security.es.mjs +136 -66
- package/dist/security.es.mjs.map +1 -1
- package/package.json +120 -120
- package/src/component/index.ts +414 -360
- package/src/core/collection.ts +454 -339
- package/src/core/element.ts +740 -493
- package/src/core/utils.ts +444 -425
- package/src/full.ts +106 -101
- package/src/index.ts +27 -27
- package/src/reactive/index.ts +22 -9
- package/src/reactive/signal.ts +506 -347
- package/src/security/sanitize.ts +553 -446
package/src/core/collection.ts
CHANGED
|
@@ -1,339 +1,454 @@
|
|
|
1
|
-
import { sanitizeHtml } from '../security/sanitize';
|
|
2
|
-
import { BQueryElement } from './element';
|
|
3
|
-
import { applyAll } from './shared';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* @
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* @param
|
|
163
|
-
* @returns
|
|
164
|
-
*/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
applyAll(this.elements, (el) =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
applyAll(this.elements, (el) => {
|
|
212
|
-
el.innerHTML =
|
|
213
|
-
});
|
|
214
|
-
return this;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* @param
|
|
221
|
-
* @
|
|
222
|
-
* @
|
|
223
|
-
*/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
});
|
|
253
|
-
return this;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* @
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* @returns The instance for method chaining
|
|
274
|
-
*/
|
|
275
|
-
|
|
276
|
-
applyAll(this.elements, (el) =>
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* @
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* @
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* @
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return this;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* @
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}
|