@webalternatif/js-core 1.6.3 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/Mouse.js +26 -15
  3. package/dist/cjs/Translator.js +7 -1
  4. package/dist/cjs/array.js +5 -35
  5. package/dist/cjs/dom.js +820 -209
  6. package/dist/cjs/eventDispatcher.js +6 -1
  7. package/dist/cjs/index.js +6 -13
  8. package/dist/cjs/is.js +44 -1
  9. package/dist/cjs/math.js +56 -31
  10. package/dist/cjs/onOff.js +13 -9
  11. package/dist/cjs/traversal.js +2 -3
  12. package/dist/cjs/utils.js +155 -38
  13. package/dist/esm/Mouse.js +26 -15
  14. package/dist/esm/Translator.js +7 -1
  15. package/dist/esm/array.js +4 -35
  16. package/dist/esm/dom.js +819 -207
  17. package/dist/esm/eventDispatcher.js +7 -1
  18. package/dist/esm/index.js +7 -8
  19. package/dist/esm/is.js +43 -0
  20. package/dist/esm/math.js +58 -32
  21. package/dist/esm/onOff.js +13 -9
  22. package/dist/esm/traversal.js +2 -3
  23. package/dist/esm/utils.js +156 -39
  24. package/dist/umd/Translator.umd.js +1 -0
  25. package/dist/umd/dom.umd.js +1 -0
  26. package/dist/umd/eventDispatcher.umd.js +1 -0
  27. package/dist/umd/mouse.umd.js +1 -0
  28. package/dist/umd/webf.umd.js +1 -0
  29. package/docs/array.md +41 -8
  30. package/docs/dom.md +1063 -269
  31. package/docs/is.md +244 -0
  32. package/docs/math.md +87 -7
  33. package/docs/mouse.md +43 -0
  34. package/docs/translator.md +14 -14
  35. package/docs/traversal.md +16 -16
  36. package/docs/utils.md +173 -20
  37. package/package.json +10 -4
  38. package/src/Mouse.js +81 -0
  39. package/src/Translator.js +148 -0
  40. package/src/array.js +136 -0
  41. package/src/dom.js +1553 -0
  42. package/src/eventDispatcher.js +118 -0
  43. package/src/index.js +106 -0
  44. package/src/is.js +201 -0
  45. package/src/math.js +113 -0
  46. package/src/onOff.js +313 -0
  47. package/src/random.js +38 -0
  48. package/src/string.js +662 -0
  49. package/src/stringPrototype.js +16 -0
  50. package/src/traversal.js +236 -0
  51. package/src/utils.js +242 -0
  52. package/types/Mouse.d.ts +14 -3
  53. package/types/Translator.d.ts +6 -5
  54. package/types/array.d.ts +0 -1
  55. package/types/dom.d.ts +763 -204
  56. package/types/index.d.ts +22 -21
  57. package/types/is.d.ts +3 -0
  58. package/types/math.d.ts +6 -5
  59. package/types/traversal.d.ts +1 -1
  60. package/types/utils.d.ts +4 -4
  61. package/types/i18n.d.ts +0 -4
package/src/dom.js ADDED
@@ -0,0 +1,1553 @@
1
+ import {
2
+ isArray,
3
+ isArrayLike,
4
+ isDocument,
5
+ isFunction,
6
+ isPlainObject,
7
+ isString,
8
+ isWindow,
9
+ } from './is.js'
10
+ import { camelCase } from './string.js'
11
+ import { each, foreach, map } from './traversal.js'
12
+ import { inArray } from './array.js'
13
+ import { on, off, __resetCustomEventsForTests } from './onOff.js'
14
+
15
+ const cssNumber = [
16
+ 'animationIterationCount',
17
+ 'aspectRatio',
18
+ 'borderImageSlice',
19
+ 'columnCount',
20
+ 'flexGrow',
21
+ 'flexShrink',
22
+ 'fontWeight',
23
+ 'gridArea',
24
+ 'gridColumn',
25
+ 'gridColumnEnd',
26
+ 'gridColumnStart',
27
+ 'gridRow',
28
+ 'gridRowEnd',
29
+ 'gridRowStart',
30
+ 'lineHeight',
31
+ 'opacity',
32
+ 'order',
33
+ 'orphans',
34
+ 'scale',
35
+ 'widows',
36
+ 'zIndex',
37
+ 'zoom',
38
+ 'fillOpacity',
39
+ 'floodOpacity',
40
+ 'stopOpacity',
41
+ 'strokeMiterlimit',
42
+ 'strokeOpacity',
43
+ ]
44
+
45
+ const dom = {
46
+ /**
47
+ * Returns the direct children of an element.
48
+ * If a selector is provided, only children matching the selector are returned.
49
+ *
50
+ * @example
51
+ * // <ul id="list"><li class="a"></li><li class="b"></li></ul>
52
+ * const list = document.getElementById('list')
53
+ *
54
+ * dom.children(list) // [[<li class="a">], [<li class="b">]]
55
+ * dom.children(list, '.a') // [<li class="a">]
56
+ *
57
+ * @param {Element} el - Parent element
58
+ * @param {string} [selector] - Optional CSS selector to filter direct children
59
+ * @returns {Element[]} Direct children, optionally filtered
60
+ */
61
+ children(el, selector) {
62
+ return selector ? this.find(el, `:scope > ${selector}`) : Array.from(el.children)
63
+ },
64
+
65
+ /**
66
+ * Returns the first direct child of an element matching `selector`.
67
+ *
68
+ * @example
69
+ * // <ul id="list"><li class="a"></li><li class="b"></li></ul>
70
+ * const list = document.getElementById('list')
71
+ *
72
+ * dom.child(list) // <li class="a">
73
+ * dom.child(list, '.b') // <li class="b">
74
+ * dom.child(list, '.c') // null
75
+ *
76
+ * @param {Element} el - Parent element
77
+ * @param {string} [selector] - Optional CSS selector to filter direct children
78
+ * @returns {Element|null} - The first matching direct child, or null if none found
79
+ */
80
+ child(el, selector) {
81
+ return this.first(this.children(el, selector))
82
+ },
83
+
84
+ /**
85
+ * Finds elements based on a selector or a collection
86
+ *
87
+ * If only one argument is provided, the search is performed from `document`.
88
+ *
89
+ * The `selector` can be:
90
+ * - a CSS selector string
91
+ * - a single Element
92
+ * - a NodeList or array-like collection of Elements
93
+ *
94
+ * @example
95
+ * dom.find('.item') // All elements matching .item in document
96
+ *
97
+ * const container = document.getElementById('box')
98
+ * dom.find(container, '.item') // All .item inside #box
99
+ *
100
+ * const el = document.querySelector('.item')
101
+ * dom.find(container, el) // [el] if inside container, otherwise []
102
+ *
103
+ * const list = document.querySelectorAll('.item')
104
+ * dom.find(container, list) // Only those inside container
105
+ *
106
+ * @param {Element|Document|DocumentFragment|string} refEl - Reference element or selector (if used alone)
107
+ * @param {string|Element|NodeList|Array<Element>} [selector] - What to find
108
+ * @returns {Element[]} - An array of matched elements
109
+ */
110
+ find(refEl, selector) {
111
+ if (undefined === selector) {
112
+ selector = refEl
113
+ refEl = document
114
+ }
115
+
116
+ if (selector instanceof Element) {
117
+ selector = [selector]
118
+ }
119
+
120
+ if (isArrayLike(selector)) {
121
+ return map(Array.from(selector), (i, el) => {
122
+ if (el instanceof Element) {
123
+ return refEl === el || refEl.contains(el) ? el : null
124
+ }
125
+
126
+ return null
127
+ })
128
+ }
129
+
130
+ try {
131
+ return Array.from(refEl.querySelectorAll(selector))
132
+ } catch {
133
+ return []
134
+ }
135
+ },
136
+
137
+ /**
138
+ * Finds the first element matching a selector or collection.
139
+ *
140
+ * Behaves like `dom.find` but returns only the first matched element.
141
+ * Returns `null` if no element matches.
142
+ *
143
+ * @param {Element|Document|DocumentFragment|string} refEl - Reference element or selector (if used alone)
144
+ * @param {string|Element|NodeList|Array<Element>} [selector] - What to find
145
+ * @returns {Element|null} - The first matched Element, or null if none found
146
+ */
147
+ findOne(refEl, selector) {
148
+ return this.find(refEl, selector)[0] ?? null
149
+ },
150
+
151
+ /**
152
+ * Finds elements by a data-* attribute.
153
+ *
154
+ * If `value` is provided, only elements with an exact matching value are returned.
155
+ * If `value` is omitted, all elements having the data attribute are returned.
156
+ *
157
+ * @example
158
+ * // <div data-user-id="42"></div>
159
+ *
160
+ * dom.findByData(document, 'user-id') // all elements with [data-user-id]
161
+ * dom.findByData(document, 'user-id', '42') // elements with [data-user-id="42"]
162
+ *
163
+ * @param {Element|Document|string} el - Reference element or selector (if used alone)
164
+ * @param {string} data - The data-* key without the "data-" prefix
165
+ * @param {string} [value] - Optional value to match exactly
166
+ * @returns {Element[]} - Matching elements
167
+ */
168
+ findByData(el, data, value) {
169
+ if (undefined === value) return this.find(el, `[data-${data}]`)
170
+
171
+ const escapeValue = CSS.escape(value + '')
172
+
173
+ return this.find(el, `[data-${data}="${escapeValue}"]`)
174
+ },
175
+
176
+ /**
177
+ * Finds the first element matching a data-* attribute.
178
+ *
179
+ * If `value` is provided, returns the first element whose data attribute
180
+ * exactly matches the given value. If omitted, returns the first element
181
+ * that has the data attribute.
182
+ *
183
+ * @example
184
+ * // <div data-user-id="42"></div>
185
+ *
186
+ * dom.findOneByData(document, 'user-id') // first element with [data-user-id]
187
+ * dom.findOneByData(document, 'user-id', '42') // element with [data-user-id="42"]
188
+ * dom.findOneByData(document, 'user-id', '99') // null
189
+ *
190
+ * @param {Element|Document|string} el - Reference element or selector (if used alone)
191
+ * @param {string} data - The data-* key without the "data-" prefix
192
+ * @param {string} [value] - Optional value to match exactly
193
+ * @returns {Element|null} The first matching element, or null if none found
194
+ */
195
+ findOneByData(el, data, value) {
196
+ return this.findByData(el, data, value)[0] ?? null
197
+ },
198
+
199
+ /**
200
+ * Adds one or more CSS classes to one or multiple elements.
201
+ *
202
+ * Multiple classes can be provided as a space-separated string.
203
+ * Accepts a single Element, a NodeList, or an array of Elements.
204
+ *
205
+ * @example
206
+ * const el = document.querySelector('#box')
207
+ * dom.addClass(el, 'active')
208
+ *
209
+ * const items = document.querySelectorAll('.item')
210
+ * dom.addClass(items, 'selected active')
211
+ *
212
+ * @param {Element|NodeList|Element[]} el - Element(s) to update
213
+ * @param {string} className - One or more class names separated by spaces
214
+ * @returns {Element|NodeList|Element[]} The original input
215
+ */
216
+ addClass(el, className) {
217
+ if (!className) return el
218
+
219
+ const classNames = className
220
+ .split(' ')
221
+ .map((c) => c.trim())
222
+ .filter(Boolean)
223
+
224
+ const elements = el instanceof Element ? [el] : Array.from(el)
225
+
226
+ elements.forEach((e) => {
227
+ if (e instanceof Element) {
228
+ e.classList.add(...classNames)
229
+ }
230
+ })
231
+
232
+ return el
233
+ },
234
+
235
+ /**
236
+ * Removes one or more CSS classes from one or multiple elements.
237
+ *
238
+ * Multiple classes can be provided as a space-separated string.
239
+ * Accepts a single Element, a NodeList, or an array of Elements.
240
+ *
241
+ * @example
242
+ * const el = document.querySelector('#box')
243
+ * dom.removeClass(el, 'active')
244
+ *
245
+ * const items = document.querySelectorAll('.item')
246
+ * dom.removeClass(items, 'selected highlighted')
247
+ *
248
+ * @param {Element|NodeList|Array<Element>} el - Element(s) to update
249
+ * @param {string} className - One or more class names separated by spaces
250
+ * @returns {Element|NodeList|Array<Element>} The original input
251
+ */
252
+ removeClass(el, className) {
253
+ if (!className) return el
254
+
255
+ const classNames = className
256
+ .split(' ')
257
+ .map((c) => c.trim())
258
+ .filter(Boolean)
259
+
260
+ const elements = el instanceof Element ? [el] : Array.from(el)
261
+
262
+ elements.forEach((e) => {
263
+ if (e instanceof Element) {
264
+ e.classList.remove(...classNames)
265
+ }
266
+ })
267
+
268
+ return el
269
+ },
270
+
271
+ /**
272
+ * Toggles one or more CSS classes on an element.
273
+ *
274
+ * Multiple classes can be provided as a space-separated string.
275
+ * If `force` is provided, it explicitly adds (`true`) or removes (`false`)
276
+ * the class instead of toggling it.
277
+ *
278
+ * @example
279
+ * const el = document.querySelector('#box')
280
+ *
281
+ * dom.toggleClass(el, 'active') // toggles "active"
282
+ * dom.toggleClass(el, 'a b') // toggles both classes
283
+ * dom.toggleClass(el, 'active', true) // ensures "active" is present
284
+ * dom.toggleClass(el, 'active', false) // ensures "active" is removed
285
+ *
286
+ * @param {Element} el - Element to update
287
+ * @param {string} classNames - One or more class names separated by spaces
288
+ * @param {boolean} [force] - Optional force flag passed to classList.toggle
289
+ * @returns {Element} The element
290
+ */
291
+ toggleClass(el, classNames, force) {
292
+ foreach(
293
+ classNames
294
+ .split(' ')
295
+ .map((c) => c.trim())
296
+ .filter(Boolean),
297
+ (c) => el.classList.toggle(c, force),
298
+ )
299
+
300
+ return el
301
+ },
302
+
303
+ /**
304
+ * Checks whether an element has all the given CSS classes.
305
+ *
306
+ * Multiple classes can be provided as a space-separated string.
307
+ * Returns `true` only if the element contains every class.
308
+ *
309
+ * @example
310
+ * // <div class="box active large"></div>
311
+ *
312
+ * dom.hasClass(el, 'active') // true
313
+ * dom.hasClass(el, 'active large') // true
314
+ * dom.hasClass(el, 'active missing') // false
315
+ * dom.hasClass(el, '') // false
316
+ *
317
+ * @param {Element} el - Element to test
318
+ * @param {string} classNames - One or more class names separated by spaces
319
+ * @returns {boolean} - `true` if the element has all the classes
320
+ */
321
+ hasClass(el, classNames) {
322
+ if (!classNames) return false
323
+
324
+ let foundClasses = true
325
+
326
+ foreach(
327
+ classNames
328
+ .split(' ')
329
+ .map((c) => c.trim())
330
+ .filter(Boolean),
331
+ (c) => {
332
+ if (inArray(c, Array.from(el.classList))) return
333
+
334
+ foundClasses = false
335
+ return false
336
+ },
337
+ )
338
+
339
+ return foundClasses
340
+ },
341
+
342
+ /**
343
+ * Appends one or more children to a node.
344
+ *
345
+ * Children can be DOM nodes or HTML strings.
346
+ *
347
+ * @example
348
+ * const box = document.createElement('div')
349
+ * dom.append(box, document.createElement('span'))
350
+ * dom.append(box, '<b>Hello</b>', '<i>world</i>')
351
+ *
352
+ * @param {Node} node - The parent node
353
+ * @param {...(Node|string)} children - Nodes or HTML strings to append
354
+ * @returns {Node} The parent node
355
+ */
356
+ append(node, ...children) {
357
+ foreach(children, (child) => {
358
+ if (isString(child)) {
359
+ child = this.create(child)
360
+ }
361
+
362
+ child && node.append(child)
363
+ })
364
+
365
+ return node
366
+ },
367
+
368
+ /**
369
+ * Prepends one or more children to a node.
370
+ *
371
+ * Children can be DOM nodes or HTML strings.
372
+ * HTML strings are converted to nodes using `dom.create`.
373
+ * When multiple children are provided, their original order is preserved.
374
+ *
375
+ * @example
376
+ * const box = document.createElement('div')
377
+ *
378
+ * dom.prepend(box, document.createElement('span'))
379
+ * dom.prepend(box, '<b>Hello</b>', '<i>world</i>')
380
+ *
381
+ * @param {Node} node - The parent node
382
+ * @param {...(Node|string)} children - Nodes or HTML strings to prepend
383
+ * @returns {Node} - The parent node
384
+ */
385
+ prepend(node, ...children) {
386
+ foreach([...children].reverse(), (child) => {
387
+ if (isString(child)) {
388
+ child = this.create(child)
389
+ }
390
+
391
+ child && node.prepend(child)
392
+ })
393
+
394
+ return node
395
+ },
396
+
397
+ /**
398
+ * @param {...(Element|NodeListOf<Element>|Iterable<Element>|string)} els
399
+ * @returns {void}
400
+ */
401
+ remove(...els) {
402
+ els.forEach((el) => {
403
+ if (el instanceof Element) {
404
+ el.remove()
405
+ } else if (el instanceof NodeList || isArray(el)) {
406
+ Array.from(el).forEach((e) => e.remove())
407
+ } else {
408
+ this.find(el).forEach((e) => e.remove())
409
+ }
410
+ })
411
+ },
412
+
413
+ /**
414
+ * Returns the closest ancestor of an element matching a selector or a specific element.
415
+ *
416
+ * If a DOM element is provided as `selector`, the function walks up the DOM
417
+ * tree and returns it if found among the ancestors (or the element itself).
418
+ * If a CSS selector string is provided, it delegates to `Element.closest()`.
419
+ * If `selector` is omitted, the element itself is returned.
420
+ *
421
+ * @example
422
+ * const item = document.querySelector('.item')
423
+ * const container = document.querySelector('.container')
424
+ *
425
+ * dom.closest(item, '.container') // container
426
+ * dom.closest(item, container) // container
427
+ * dom.closest(item) // item
428
+ *
429
+ * @param {Element} el - The starting element
430
+ * @param {string|Element} [selector] - CSS selector or specific ancestor element
431
+ * @returns {Element|null} - The matching ancestor, or null if none found
432
+ */
433
+ closest(el, selector) {
434
+ if (selector instanceof Element) {
435
+ if (el === selector) return el
436
+
437
+ let parentEl = el.parentElement
438
+
439
+ while (parentEl) {
440
+ if (parentEl === selector) {
441
+ return parentEl
442
+ }
443
+
444
+ parentEl = parentEl.parentElement
445
+ }
446
+
447
+ return null
448
+ }
449
+
450
+ if (undefined === selector) {
451
+ return el
452
+ }
453
+
454
+ return el.closest(selector)
455
+ },
456
+
457
+ /**
458
+ * Returns the next sibling element of a node.
459
+ *
460
+ * If a selector is provided, the next sibling is returned only if it matches
461
+ * the selector. This function does not search beyond the immediate sibling.
462
+ *
463
+ * @example
464
+ * // <div class="a"></div><div class="b"></div><div class="c"></div>
465
+ * const a = document.querySelector('.a')
466
+ *
467
+ * dom.next(a) // <div class="b">
468
+ * dom.next(a, '.b') // <div class="b">
469
+ * dom.next(a, '.c') // null
470
+ *
471
+ * @param {Element} el - Reference element
472
+ * @param {string|null} [selector] - CSS selector to filter the sibling
473
+ * @returns {Element|null} - The next sibling element, or null if not found/matching
474
+ */
475
+ next(el, selector = null) {
476
+ let sibling = el.nextElementSibling
477
+
478
+ if (!selector) return sibling
479
+
480
+ if (sibling && sibling.matches(selector)) {
481
+ return sibling
482
+ }
483
+
484
+ return null
485
+ },
486
+
487
+ /**
488
+ * Returns the previous sibling element of a node.
489
+ *
490
+ * If a selector is provided, the previous sibling is returned only if it matches
491
+ * the selector. This function does not search beyond the immediate sibling.
492
+ *
493
+ * @example
494
+ * // <div class="a"></div><div class="b"></div><div class="c"></div>
495
+ * const c = document.querySelector('.c')
496
+ *
497
+ * dom.prev(c) // <div class="b">
498
+ * dom.prev(c, '.b') // <div class="b">
499
+ * dom.prev(c, '.a') // null
500
+ *
501
+ * @param {Element} el - Reference element
502
+ * @param {string|null} [selector] - CSS selector to filter the sibling
503
+ * @returns {Element|null} - The previous sibling element, or null if not found/matching
504
+ */
505
+ prev(el, selector = null) {
506
+ let sibling = el.previousElementSibling
507
+
508
+ if (!selector) return sibling
509
+
510
+ if (sibling && sibling.matches(selector)) {
511
+ return sibling
512
+ }
513
+
514
+ return null
515
+ },
516
+
517
+ /**
518
+ * Returns all following sibling elements of a node.
519
+ *
520
+ * If a selector is provided, only siblings matching the selector are included.
521
+ * Traversal continues through all next siblings in document order.
522
+ *
523
+ * @example
524
+ * // <div class="a"></div><div class="b"></div><div class="c"></div>
525
+ * const a = document.querySelector('.a')
526
+ *
527
+ * dom.nextAll(a) // [<div class="b">, <div class="c">]
528
+ * dom.nextAll(a, '.c') // [<div class="c">]
529
+ *
530
+ * @param {Element} el - Reference element
531
+ * @param {string} [selector] - CSS selector to filter siblings
532
+ * @returns {Element[]} - Array of matching following siblings
533
+ */
534
+ nextAll(el, selector) {
535
+ const siblings = []
536
+
537
+ let sibling = el.nextElementSibling
538
+
539
+ while (sibling) {
540
+ if (undefined === selector || sibling.matches(selector)) {
541
+ siblings.push(sibling)
542
+ }
543
+
544
+ sibling = sibling.nextElementSibling
545
+ }
546
+
547
+ return siblings
548
+ },
549
+
550
+ /**
551
+ * Returns all preceding sibling elements of a node.
552
+ *
553
+ * If a selector is provided, only siblings matching the selector are included.
554
+ * Traversal continues through all previous siblings in reverse document order.
555
+ *
556
+ * @example
557
+ * // <div class="a"></div><div class="b"></div><div class="c"></div>
558
+ * const c = document.querySelector('.c')
559
+ *
560
+ * dom.prevAll(c) // [<div class="b">, <div class="a">]
561
+ * dom.prevAll(c, '.a') // [<div class="a">]
562
+ *
563
+ * @param {Element} el - Reference element
564
+ * @param {string} [selector] - CSS selector to filter siblings
565
+ * @returns {Element[]} - Array of matching preceding siblings
566
+ */
567
+ prevAll(el, selector) {
568
+ const siblings = []
569
+
570
+ let sibling = el.previousElementSibling
571
+
572
+ while (sibling) {
573
+ if (undefined === selector || sibling.matches(selector)) {
574
+ siblings.push(sibling)
575
+ }
576
+
577
+ sibling = sibling.previousElementSibling
578
+ }
579
+
580
+ return siblings
581
+ },
582
+
583
+ /**
584
+ * Returns the index of a node among its preceding siblings.
585
+ *
586
+ * If a selector is provided, only matching siblings are considered.
587
+ *
588
+ * @example
589
+ * // <div class="a"></div><div class="b"></div><div class="c"></div>
590
+ * const c = document.querySelector('.c')
591
+ *
592
+ * dom.index(a) // 0
593
+ * dom.index(c) // 2
594
+ * dom.prevAll(c, '.a') // 1
595
+ *
596
+ * @param {Element} el - Reference element
597
+ * @param {string} [selector] - CSS selector to filter siblings
598
+ * @returns {number} - The index of `el`
599
+ */
600
+ index(el, selector) {
601
+ return this.prevAll(el, selector).length
602
+ },
603
+
604
+ /**
605
+ * Returns all following sibling elements until a matching element is reached.
606
+ *
607
+ * Traversal stops before the first sibling that matches the given selector
608
+ * or equals the provided element. That matching element is not included.
609
+ *
610
+ * @example
611
+ * // <div class="a"></div><div class="b"></div><div class="c"></div><div class="d"></div>
612
+ * const a = document.querySelector('.a')
613
+ * const d = document.querySelector('.d')
614
+ *
615
+ * dom.nextUntil(a, '.d') // [<div class="b">, <div class="c">]
616
+ * dom.nextUntil(a, d) // [<div class="b">, <div class="c">]
617
+ *
618
+ * @param {Element} el - Reference element
619
+ * @param {Element|string} selector - CSS selector or element to stop at
620
+ * @returns {Element[]} - Array of siblings until the stop condition
621
+ */
622
+ nextUntil(el, selector) {
623
+ let selectorIsElement = false
624
+ const list = []
625
+
626
+ if (selector instanceof Element) {
627
+ selectorIsElement = true
628
+ }
629
+
630
+ let nextSibling = el.nextElementSibling
631
+
632
+ while (nextSibling) {
633
+ const found = selectorIsElement
634
+ ? nextSibling === selector
635
+ : nextSibling.matches(selector)
636
+
637
+ if (found) break
638
+
639
+ list.push(nextSibling)
640
+
641
+ nextSibling = nextSibling.nextElementSibling
642
+ }
643
+
644
+ return list
645
+ },
646
+
647
+ /**
648
+ * Returns all preceding sibling elements until a matching element is reached.
649
+ *
650
+ * Traversal stops before the first sibling that matches the given selector
651
+ * or equals the provided element. That matching element is not included.
652
+ *
653
+ * @example
654
+ * // <div class="a"></div><div class="b"></div><div class="c"></div><div class="d"></div>
655
+ *
656
+ * const d = document.querySelector('.d')
657
+ * const a = document.querySelector('.a')
658
+ *
659
+ * dom.prevUntil(d, '.a') // [<div class="c">, <div class="b">]
660
+ * dom.prevUntil(d, a) // [<div class="c">, <div class="b">]
661
+ *
662
+ * @param {Element} el - Reference element
663
+ * @param {Element|string} selector - CSS selector or element to stop at
664
+ * @returns {Element[]} - Array of siblings until the stop condition
665
+ */
666
+ prevUntil(el, selector) {
667
+ let selectorIsElement = false
668
+ const list = []
669
+
670
+ if (selector instanceof Element) {
671
+ selectorIsElement = true
672
+ }
673
+
674
+ let prevSibling = el.previousElementSibling
675
+
676
+ while (prevSibling) {
677
+ const found = selectorIsElement
678
+ ? prevSibling === selector
679
+ : prevSibling.matches(selector)
680
+
681
+ if (found) break
682
+
683
+ list.push(prevSibling)
684
+
685
+ prevSibling = prevSibling.previousElementSibling
686
+ }
687
+
688
+ return list
689
+ },
690
+
691
+ /**
692
+ * Wraps an element inside another element.
693
+ *
694
+ * If the wrapping element is not already in the DOM, it is inserted
695
+ * just before the target element. The target element is then appended
696
+ * inside the wrapper.
697
+ *
698
+ * @example
699
+ * const el = document.querySelector('.item')
700
+ * const wrapper = document.createElement('div')
701
+ *
702
+ * dom.wrap(el, wrapper)
703
+ * // <div><div class="item"></div></div>
704
+ *
705
+ * @param {Element} el - The element to wrap
706
+ * @param {Element} wrappingElement - The wrapper element
707
+ * @returns {Element} - The original wrapped element
708
+ */
709
+ wrap(el, wrappingElement) {
710
+ if (!wrappingElement.isConnected) {
711
+ el.parentNode.insertBefore(wrappingElement, el)
712
+ }
713
+
714
+ this.append(wrappingElement, el)
715
+
716
+ return el
717
+ },
718
+
719
+ /**
720
+ * Gets, sets, or removes an attribute on an element.
721
+ *
722
+ * - If `value` is omitted, returns the attribute value (or null if not present).
723
+ * - If `value` is `null`, the attribute is removed.
724
+ * - Otherwise, the attribute is set to the provided value.
725
+ *
726
+ * @example
727
+ * dom.attr(el, 'id') // "my-id"
728
+ * dom.attr(el, 'title', 'Hello') // sets title="Hello"
729
+ * dom.attr(el, 'disabled', null) // removes the attribute
730
+ *
731
+ * @param {Element} el - Target element
732
+ * @param {string} name - Attribute name
733
+ * @param {string|null} [value] - Value to set, or null to remove
734
+ * @returns {Element|string|null} - The attribute value when reading, otherwise the element
735
+ */
736
+ attr(el, name, value) {
737
+ if (undefined === value) return el.getAttribute(name)
738
+
739
+ if (null === value) {
740
+ el.removeAttribute(name)
741
+ } else {
742
+ el.setAttribute(name, value)
743
+ }
744
+
745
+ return el
746
+ },
747
+
748
+ /**
749
+ * Gets or sets a property directly on a DOM element.
750
+ *
751
+ * Unlike `dom.attr`, this interacts with the live DOM property,
752
+ * not the HTML attribute.
753
+ *
754
+ * - If `value` is omitted, returns the property value.
755
+ * - Otherwise, sets the property.
756
+ *
757
+ * @example
758
+ * dom.prop(input, 'checked') // true/false
759
+ * dom.prop(input, 'checked', true) // checks the checkbox
760
+ *
761
+ * dom.prop(img, 'src') // full resolved URL
762
+ *
763
+ * @param {Element} el - Target element
764
+ * @param {string} name - Property name
765
+ * @param {any} [value] - Value to set
766
+ * @returns {*|Element} - The property value when reading, otherwise the element
767
+ */
768
+ prop(el, name, value) {
769
+ if (undefined === value) {
770
+ return el[name]
771
+ }
772
+
773
+ el[name] = value
774
+ return el
775
+ },
776
+
777
+ /**
778
+ * Gets or sets the HTML content of an element.
779
+ *
780
+ * - If `html` is omitted, returns the element's current `innerHTML`.
781
+ * - Otherwise, replaces the element's content with the provided HTML string.
782
+ *
783
+ * @example
784
+ * dom.html(el) // "<span>Hello</span>"
785
+ * dom.html(el, '<b>Hi</b>') // sets inner HTML
786
+ *
787
+ * @param {Element} el - Target element
788
+ * @param {string} [html] - HTML string to set
789
+ * @returns {Element|string} The HTML string when reading, otherwise the element
790
+ */
791
+ html(el, html) {
792
+ if (undefined === html) return el.innerHTML
793
+
794
+ el.innerHTML = html
795
+ return el
796
+ },
797
+
798
+ /**
799
+ * Gets or sets the text content of an element.
800
+ *
801
+ * - If `text` is omitted, returns the element's visible text (`innerText`).
802
+ * - Otherwise, replaces the element's text content.
803
+ *
804
+ * @example
805
+ * dom.text(el) // "Hello world"
806
+ * dom.text(el, 'New text') // sets visible text content
807
+ *
808
+ * @param {Element} el - Target element
809
+ * @param {string} [text] - Text to set
810
+ * @returns {Element|string} - The text when reading, otherwise the element
811
+ */
812
+ text(el, text) {
813
+ if (undefined === text) return el.innerText
814
+
815
+ el.innerText = text
816
+ return el
817
+ },
818
+
819
+ /**
820
+ * Hides an element by setting `display: none`, while preserving its original display value.
821
+ *
822
+ * The original computed `display` value is stored internally so it can be
823
+ * restored later (typically by the corresponding `show()` method).
824
+ *
825
+ * @example
826
+ * dom.hide(el) // element becomes hidden
827
+ *
828
+ * @param {Element} el - Element to hide
829
+ * @returns {Element} The element
830
+ */
831
+ hide(el) {
832
+ if (undefined === this.data(el, '__display__')) {
833
+ let display = ''
834
+
835
+ if (isFunction(window.getComputedStyle)) {
836
+ display = window.getComputedStyle(el).display
837
+ } else {
838
+ display = el.style.display
839
+ }
840
+
841
+ this.data(el, '__display__', display)
842
+ }
843
+
844
+ el.style.display = 'none'
845
+ return el
846
+ },
847
+
848
+ /**
849
+ * Shows an element by restoring its original `display` value.
850
+ *
851
+ * If the element was previously hidden using `hide`, its original
852
+ * computed display value is restored. Otherwise, the inline `display`
853
+ * style is simply removed.
854
+ *
855
+ * @example
856
+ * dom.hide(el)
857
+ * dom.show(el) // element becomes visible again with its original display
858
+ *
859
+ * @param {Element} el - Element to show
860
+ * @returns {Element} - The element
861
+ */
862
+ show(el) {
863
+ const dataDisplay = this.data(el, '__display__')
864
+
865
+ if (undefined === dataDisplay) {
866
+ el.style.removeProperty('display')
867
+ } else {
868
+ el.style.display = dataDisplay
869
+ this.removeData(el, '__display__')
870
+ }
871
+
872
+ return el
873
+ },
874
+
875
+ /**
876
+ * Toggles the visibility of an element using `dom.hide` and `dom.show`.
877
+ *
878
+ * The visibility state is determined from the computed display value,
879
+ * not only the inline style.
880
+ *
881
+ * @example
882
+ * dom.toggle(el) // hides if visible, shows if hidden
883
+ *
884
+ * @param {Element} el - Element to toggle
885
+ * @returns {Element} - The element
886
+ */
887
+ toggle(el) {
888
+ return 'none' === this.css(el, 'display') ? this.show(el) : this.hide(el)
889
+ },
890
+
891
+ /**
892
+ * Gets, sets, or removes data-* attributes on an element.
893
+ *
894
+ * - If called with no arguments, returns the element's `dataset`.
895
+ * - If `name` is an object, sets multiple data entries.
896
+ * - If `value` is omitted, returns the value of the data key.
897
+ * - If `value` is `null`, removes the data attribute.
898
+ * - Otherwise, sets the data value.
899
+ *
900
+ * Keys can be provided in camelCase (`userId`) or kebab-case with `data-` prefix (`data-user-id`).
901
+ *
902
+ * @example
903
+ * dom.data(el) // DOMStringMap of all data attributes
904
+ *
905
+ * dom.data(el, 'userId') // value of data-user-id
906
+ * dom.data(el, 'userId', '42') // sets data-user-id="42"
907
+ *
908
+ * dom.data(el, 'data-role', 'admin') // also works
909
+ *
910
+ * dom.data(el, { userId: '42', role: 'admin' }) // sets multiple values
911
+ *
912
+ * dom.data(el, 'userId', null) // removes data-user-id
913
+ *
914
+ * @param {Element} el - Target element
915
+ * @param {Object<string, string>|string} [name] - Data key or object of key/value pairs
916
+ * @param {string|null} [value] - Value to set, or null to remove
917
+ * @returns {Element|DOMStringMap|string|undefined} - Dataset, value, or element
918
+ */
919
+ data(el, name, value) {
920
+ if (undefined === name && undefined === value) {
921
+ return el.dataset
922
+ }
923
+
924
+ if (isPlainObject(name)) {
925
+ each(name, (k, v) => this.data(el, k, v))
926
+ return el
927
+ }
928
+
929
+ const isAttr = /^data-/.test(name + '')
930
+ const key = camelCase(isAttr ? (name + '').replace(/^data-/, '') : name + '')
931
+
932
+ if (undefined === value) return el.dataset[key]
933
+
934
+ if (null === value) {
935
+ delete el.dataset[key]
936
+
937
+ return el
938
+ }
939
+
940
+ el.dataset[key] = value
941
+
942
+ return el
943
+ },
944
+
945
+ /**
946
+ * Removes a data-* attribute from an element.
947
+ *
948
+ * The key can be provided in camelCase, kebab-case, or with the `data-` prefix.
949
+ *
950
+ * @example
951
+ * dom.removeData(el, 'userId') // removes data-user-id
952
+ * dom.removeData(el, 'user-id') // removes data-user-id
953
+ * dom.removeData(el, 'data-role') // removes data-role
954
+ *
955
+ * @param {Element} el - Target element
956
+ * @param {string} name - Data key to remove
957
+ * @returns {Element} - The element
958
+ */
959
+ removeData(el, name) {
960
+ return this.data(el, name, null)
961
+ },
962
+
963
+ /**
964
+ * Gets or sets CSS styles on an element.
965
+ *
966
+ * - If `style` is a string and `value` is omitted, returns the computed style value.
967
+ * - If `style` is a string and `value` is provided, sets the style.
968
+ * - If `style` is an object, sets multiple styles at once.
969
+ *
970
+ * handles :
971
+ * - camelCase and kebab-case properties
972
+ * - CSS custom properties (`--var`)
973
+ * - Adding `px` to numeric values where appropriate
974
+ *
975
+ * @example
976
+ * dom.css(el, 'color') // "rgb(255, 0, 0)"
977
+ * dom.css(el, 'background-color', 'blue')
978
+ * dom.css(el, 'width', 200) // sets "200px"
979
+ *
980
+ * dom.css(el, {
981
+ * width: 100,
982
+ * height: 50,
983
+ * backgroundColor: 'red'
984
+ * })
985
+ *
986
+ * dom.css(el, '--my-var', '10px') // CSS custom property
987
+ *
988
+ * @param {HTMLElement} el - Target element
989
+ * @param {Object<string, string|number>|string} style - CSS property or object of properties
990
+ * @param {string|number} [value] - Value to set
991
+ * @returns {Element|string} - The style value when reading, otherwise the element
992
+ */
993
+ css(el, style, value) {
994
+ if (isString(style)) {
995
+ const prop = style.startsWith('--') ? style : camelCase(style)
996
+
997
+ if (undefined === value) {
998
+ if (window.getComputedStyle) {
999
+ const computedStyle = window.getComputedStyle(el, null)
1000
+
1001
+ return (
1002
+ computedStyle.getPropertyValue(style) ||
1003
+ computedStyle[camelCase(style)] ||
1004
+ ''
1005
+ )
1006
+ }
1007
+
1008
+ return el.style[camelCase(style)] || ''
1009
+ }
1010
+
1011
+ if (prop.startsWith('--')) {
1012
+ el.style.setProperty(prop, String(value))
1013
+ } else {
1014
+ if (typeof value === 'number' && !inArray(prop, cssNumber)) value += 'px'
1015
+
1016
+ el.style[prop] = value
1017
+ }
1018
+ } else {
1019
+ each(style, (name, v) => {
1020
+ this.css(el, name, v)
1021
+ })
1022
+ }
1023
+
1024
+ return el
1025
+ },
1026
+
1027
+ /**
1028
+ * Finds elements matching a selector inside the closest ancestor
1029
+ * that matches another selector.
1030
+ *
1031
+ * First finds the closest ancestor of `el` matching `selectorClosest`,
1032
+ * then searches inside it for elements matching `selectorFind`.
1033
+ *
1034
+ * @example
1035
+ * // <div class="card"><button class="btn"></button><span class="label"></span></div>
1036
+ *
1037
+ * dom.closestFind(button, '.card', '.label')
1038
+ * // => finds .label inside the closest .card ancestor
1039
+ *
1040
+ * @param {Element} el - Starting element
1041
+ * @param {string} selectorClosest - Selector used to find the closest ancestor
1042
+ * @param {string} selectorFind - Selector used to find elements inside that ancestor
1043
+ * @returns {Element[]} - Array of matched elements, or empty array if none found
1044
+ */
1045
+ closestFind(el, selectorClosest, selectorFind) {
1046
+ const closest = this.closest(el, selectorClosest)
1047
+
1048
+ if (closest) {
1049
+ return this.find(closest, selectorFind)
1050
+ }
1051
+
1052
+ return []
1053
+ },
1054
+
1055
+ /**
1056
+ * Finds the first element matching a selector inside the closest ancestor
1057
+ * that matches another selector.
1058
+ *
1059
+ * First finds the closest ancestor of `el` matching `selectorClosest`,
1060
+ * then searches inside it for the first element matching `selectorFindOne`.
1061
+ *
1062
+ * @example
1063
+ * // <div class="card"><button class="btn"></button><span class="label"></span></div>
1064
+ *
1065
+ * dom.closestFindOne(button, '.card', '.label')
1066
+ * // => finds the first .label inside the closest .card ancestor
1067
+ *
1068
+ * @param {Element} el - Starting element
1069
+ * @param {string} selectorClosest - Selector used to find the closest ancestor
1070
+ * @param {string} selectorFindOne - Selector used to find a single element inside that ancestor
1071
+ * @returns {Element|null} - The matched element, or null if none found
1072
+ */
1073
+ closestFindOne(el, selectorClosest, selectorFindOne) {
1074
+ const closest = this.closest(el, selectorClosest)
1075
+
1076
+ if (closest) {
1077
+ return this.findOne(closest, selectorFindOne)
1078
+ }
1079
+
1080
+ return null
1081
+ },
1082
+
1083
+ /**
1084
+ * Returns the first element from a collection or the element itself.
1085
+ *
1086
+ * Accepts a single Element, a NodeList, or an array of Elements.
1087
+ * Returns `null` if the collection is empty.
1088
+ *
1089
+ * @example
1090
+ * dom.first(document.querySelectorAll('.item')) // first .item
1091
+ * dom.first([el1, el2]) // el1
1092
+ * dom.first(el) // el
1093
+ *
1094
+ * @param {NodeList|Element|Element[]} nodeList - Collection or single element
1095
+ * @returns {Element|null} - The first element, or null if none found
1096
+ */
1097
+ first(nodeList) {
1098
+ if (nodeList instanceof Element) return nodeList
1099
+ return Array.from(nodeList)[0] ?? null
1100
+ },
1101
+
1102
+ /**
1103
+ * Returns the last element from a collection or the element itself.
1104
+ *
1105
+ * Accepts a NodeList or an array of Elements.
1106
+ * Returns `null` if the collection is empty.
1107
+ *
1108
+ * @example
1109
+ * dom.last(document.querySelectorAll('.item')) // last .item
1110
+ * dom.last([el1, el2]) // el2
1111
+ *
1112
+ * @param {NodeList|Element|Element[]} nodeList - Collection or single element
1113
+ * @returns {Element|null} - The last element, or null if none found
1114
+ */
1115
+ last(nodeList) {
1116
+ if (nodeList instanceof Element) return nodeList
1117
+ const arr = Array.from(nodeList)
1118
+ return arr[arr.length - 1] ?? null
1119
+ },
1120
+
1121
+ /**
1122
+ * Creates DOM node(s) from a tag name or an HTML string.
1123
+ *
1124
+ * - If a simple tag name is provided (e.g. `"div"`), a new element is created.
1125
+ * - If an HTML string is provided, it is parsed using a `<template>` element.
1126
+ * - If the HTML contains a single root element, that element is returned.
1127
+ * - If multiple root nodes are present, a `DocumentFragment` is returned.
1128
+ *
1129
+ * @example
1130
+ * dom.create('div') // <div></div>
1131
+ * dom.create('<span>Hello</span>') // <span>Hello</span>
1132
+ * dom.create('<li>One</li><li>Two</li>') // DocumentFragment containing both <li>
1133
+ *
1134
+ * @param {string} html - Tag name or HTML string
1135
+ * @returns {Element|DocumentFragment|null} - Created node(s), or null if input is invalid
1136
+ */
1137
+ create(html) {
1138
+ if (!isString(html)) return null
1139
+
1140
+ const isTagName = (s) => /^[A-Za-z][A-Za-z0-9-]*$/.test(s)
1141
+
1142
+ if (isTagName(html)) {
1143
+ return document.createElement(html)
1144
+ }
1145
+
1146
+ const tpl = document.createElement('template')
1147
+ tpl.innerHTML = html.trim()
1148
+
1149
+ const frag = tpl.content
1150
+
1151
+ if (frag.childElementCount === 1 && frag.children.length === 1) {
1152
+ return frag.firstElementChild
1153
+ }
1154
+
1155
+ return frag.cloneNode(true)
1156
+ },
1157
+
1158
+ /**
1159
+ * Returns the element at a given index from a collection.
1160
+ *
1161
+ * Supports negative indexes to count from the end of the list.
1162
+ * Returns `null` if the index is out of bounds.
1163
+ *
1164
+ * @example
1165
+ * const items = document.querySelectorAll('.item')
1166
+ *
1167
+ * dom.eq(items, 0) // first element
1168
+ * dom.eq(items, 2) // third element
1169
+ * dom.eq(items, -1) // last element
1170
+ * dom.eq(items, -2) // second to last
1171
+ *
1172
+ * @param {NodeList|Element[]} nodeList - Collection of elements
1173
+ * @param {number} [index=0] - Index of the element (can be negative)
1174
+ * @returns {Element|null} - The element at the given index, or null if not found
1175
+ */
1176
+ eq(nodeList, index = 0) {
1177
+ nodeList = Array.from(nodeList)
1178
+
1179
+ if (Math.abs(index) >= nodeList.length) return null
1180
+
1181
+ if (index < 0) {
1182
+ index = nodeList.length + index
1183
+ }
1184
+
1185
+ return nodeList[index]
1186
+ },
1187
+
1188
+ /**
1189
+ * Inserts a new element or HTML string immediately after a reference element.
1190
+ *
1191
+ * If `newEl` is a string, it is converted to a node using `dom.create`.
1192
+ * Returns the inserted node, or `null` if the reference element has no parent.
1193
+ *
1194
+ * @example
1195
+ * dom.after(el, '<span>New</span>')
1196
+ * dom.after(el, document.createElement('div'))
1197
+ *
1198
+ * @param {Element} el - Reference element
1199
+ * @param {Element|string} newEl - Element or HTML string to insert
1200
+ * @returns {Element|DocumentFragment|null} - The inserted node, or null if insertion failed
1201
+ */
1202
+ after(el, newEl) {
1203
+ if (!el.parentElement) return null
1204
+
1205
+ if (isString(newEl)) {
1206
+ newEl = this.create(newEl)
1207
+ }
1208
+
1209
+ return el.parentElement.insertBefore(newEl, el.nextElementSibling)
1210
+ },
1211
+
1212
+ /**
1213
+ * Inserts a new element or HTML string immediately before a reference element.
1214
+ *
1215
+ * If `newEl` is a string, it is converted to a node using `dom.create`.
1216
+ * Returns the inserted node, or `null` if the reference element has no parent.
1217
+ *
1218
+ * @example
1219
+ * dom.before(el, '<span>New</span>')
1220
+ * dom.before(el, document.createElement('div'))
1221
+ *
1222
+ * @param {Element} el - Reference element
1223
+ * @param {Element|string} newEl - Element or HTML string to insert
1224
+ * @returns {Element|DocumentFragment|null} - The inserted node, or null if insertion failed
1225
+ */
1226
+ before(el, newEl) {
1227
+ if (!el.parentElement) return null
1228
+
1229
+ if (isString(newEl)) {
1230
+ newEl = this.create(newEl)
1231
+ }
1232
+
1233
+ return el.parentElement.insertBefore(newEl, el)
1234
+ },
1235
+
1236
+ /**
1237
+ * Removes all child nodes from an element.
1238
+ *
1239
+ * @example
1240
+ * dom.empty(el) // el now has no children
1241
+ *
1242
+ * @param {Element} el - Element to clear
1243
+ * @returns {Element} - The element
1244
+ */
1245
+ empty(el) {
1246
+ while (el.firstChild) {
1247
+ el.removeChild(el.firstChild)
1248
+ }
1249
+
1250
+ return el
1251
+ },
1252
+
1253
+ /**
1254
+ * Filters a collection of elements by excluding those matching a selector
1255
+ * or a specific element.
1256
+ *
1257
+ * Accepts a single Element, a NodeList, or an array of Elements.
1258
+ * If `selector` is a string, elements matching it are excluded.
1259
+ * If `selector` is an Element, that exact element is excluded.
1260
+ *
1261
+ * @example
1262
+ * const items = document.querySelectorAll('.item')
1263
+ *
1264
+ * dom.not(items, '.active') // all .item elements except those with .active
1265
+ * dom.not(items, someElement) // all elements except that specific one
1266
+ *
1267
+ * dom.not(el, '.hidden') // returns [] if el matches, otherwise [el]
1268
+ *
1269
+ * @param {Element|NodeList|Element[]} el - Element(s) to filter
1270
+ * @param {string|Element} selector - CSS selector or element to exclude
1271
+ * @returns {Element[]} - Filtered array of elements
1272
+ */
1273
+ not(el, selector) {
1274
+ const elements = el instanceof Element ? [el] : Array.from(el)
1275
+
1276
+ const selectorIsString = isString(selector)
1277
+
1278
+ return elements.filter((e) => {
1279
+ return selectorIsString ? !e.matches(selector) : e !== selector
1280
+ })
1281
+ },
1282
+
1283
+ /**
1284
+ * Checks whether two elements visually collide (overlap) in the viewport.
1285
+ *
1286
+ * Returns `true` if their rectangles intersect.
1287
+ *
1288
+ * @example
1289
+ * if (dom.collide(box1, box2)) {
1290
+ * console.log('Elements overlap')
1291
+ * }
1292
+ *
1293
+ * @param {Element} elem1 - First element
1294
+ * @param {Element} elem2 - Second element
1295
+ * @returns {boolean} - `true` if the elements overlap, otherwise false
1296
+ */
1297
+ collide(elem1, elem2) {
1298
+ const rect1 = elem1.getBoundingClientRect()
1299
+ const rect2 = elem2.getBoundingClientRect()
1300
+
1301
+ return (
1302
+ rect1.x < rect2.x + rect2.width &&
1303
+ rect1.x + rect1.width > rect2.x &&
1304
+ rect1.y < rect2.y + rect2.height &&
1305
+ rect1.y + rect1.height > rect2.y
1306
+ )
1307
+ },
1308
+
1309
+ /**
1310
+ * Checks whether an element matches a selector or is equal to another element.
1311
+ *
1312
+ * If `selector` is a string, uses `Element.matches()`.
1313
+ * If `selector` is an Element, checks strict equality.
1314
+ *
1315
+ * @example
1316
+ * dom.matches(el, '.active') // true if el has class "active"
1317
+ * dom.matches(el, otherEl) // true if el === otherEl
1318
+ *
1319
+ * @param {Element} el - Element to test
1320
+ * @param {string|Element} selector - CSS selector or element to compare
1321
+ * @returns {boolean} - `true` if the element matches, otherwise false
1322
+ */
1323
+ matches(el, selector) {
1324
+ if (!el) return false
1325
+
1326
+ return selector instanceof Element ? selector === el : el.matches(selector)
1327
+ },
1328
+
1329
+ /**
1330
+ * Replaces a child node of an element with another node.
1331
+ *
1332
+ * @example
1333
+ * dom.replaceChild(parent, newEl, oldEl)
1334
+ *
1335
+ * @param {Element} el - Parent element
1336
+ * @param {Element} child - New child node
1337
+ * @param {Element} oldChild - Existing child node to replace
1338
+ * @returns {Element} - The replaced node
1339
+ */
1340
+ replaceChild(el, child, oldChild) {
1341
+ return el.replaceChild(child, oldChild)
1342
+ },
1343
+
1344
+ /**
1345
+ * Replaces all children of an element with new nodes or HTML strings.
1346
+ *
1347
+ * Strings are converted to DOM nodes using `dom.create`.
1348
+ *
1349
+ * @example
1350
+ * dom.replaceChildren(el, '<span>A</span>', '<span>B</span>')
1351
+ * dom.replaceChildren(el, document.createElement('div'))
1352
+ *
1353
+ * @param {Element} el - Target element
1354
+ * @param {...(Element|string)} children - New children to insert
1355
+ * @returns {Element} - The element
1356
+ */
1357
+ replaceChildren(el, ...children) {
1358
+ const nodes = []
1359
+
1360
+ foreach(children, (child) => {
1361
+ if (isString(child)) {
1362
+ child = this.create(child)
1363
+ }
1364
+
1365
+ nodes.push(child)
1366
+ })
1367
+
1368
+ el.replaceChildren(...nodes)
1369
+ return el
1370
+ },
1371
+
1372
+ /**
1373
+ * Returns the page offset of an element, document, or window.
1374
+ *
1375
+ * - For `window`, returns the current scroll position.
1376
+ * - For `document`, returns the scroll position of the root element.
1377
+ * - For an element, returns its position relative to the top-left of the page.
1378
+ *
1379
+ * @example
1380
+ * dom.offset(window) // { top: scrollY, left: scrollX }
1381
+ * dom.offset(document) // { top: scrollTop, left: scrollLeft }
1382
+ * dom.offset(el) // position of el relative to the page
1383
+ *
1384
+ * @param {Element|Document|Window} el - Target element, document, or window
1385
+ * @returns {{top: number, left: number}} - The offset relative to the page
1386
+ */
1387
+ offset(el) {
1388
+ if (isWindow(el)) {
1389
+ return {
1390
+ top: el.scrollY,
1391
+ left: el.scrollX,
1392
+ }
1393
+ } else if (isDocument(el)) {
1394
+ return {
1395
+ top: el.documentElement.scrollTop,
1396
+ left: el.documentElement.scrollLeft,
1397
+ }
1398
+ }
1399
+
1400
+ const rect = el.getBoundingClientRect()
1401
+ const wOffset = this.offset(window)
1402
+
1403
+ return {
1404
+ top: rect.top + wOffset.top,
1405
+ left: rect.left + wOffset.left,
1406
+ }
1407
+ },
1408
+
1409
+ /**
1410
+ * Checks whether a node is inside an editable context.
1411
+ *
1412
+ * Returns true if the element itself, or one of its ancestors,
1413
+ * is an editable form control or has `contenteditable="true"`.
1414
+ * Text nodes are automatically resolved to their parent element.
1415
+ *
1416
+ * @example
1417
+ * dom.isEditable(inputEl) // true
1418
+ * dom.isEditable(textareaEl) // true
1419
+ * dom.isEditable(selectEl) // true
1420
+ *
1421
+ * dom.isEditable(divWithContentEditable) // true
1422
+ * dom.isEditable(spanInsideContentEditable) // true
1423
+ *
1424
+ * dom.isEditable(document.body) // false
1425
+ *
1426
+ * @param {Node} el - Node to test
1427
+ * @returns {boolean} True if the node is in an editable context
1428
+ */
1429
+ isEditable(el) {
1430
+ if (el?.nodeType === 3) el = el.parentElement
1431
+
1432
+ if (!(el instanceof HTMLElement)) return false
1433
+
1434
+ return (
1435
+ inArray(el.tagName, ['INPUT', 'TEXTAREA', 'SELECT']) ||
1436
+ el.isContentEditable ||
1437
+ !!this.closest(el, '[contenteditable="true"]')
1438
+ )
1439
+ },
1440
+
1441
+ /**
1442
+ * Checks whether a node is currently attached to the main document.
1443
+ *
1444
+ * @example
1445
+ * dom.isInDOM(el) // true if element is in the document
1446
+ *
1447
+ * const frag = document.createDocumentFragment()
1448
+ * dom.isInDOM(frag) // false
1449
+ *
1450
+ * @param {Node} node - Node to test
1451
+ * @returns {boolean} - `true` if the node is attached to the document
1452
+ */
1453
+ isInDOM(node) {
1454
+ if (!(node instanceof Node)) return false
1455
+
1456
+ const root = node.getRootNode({ composed: true })
1457
+ return root === document
1458
+ },
1459
+
1460
+ /**
1461
+ * Attaches one or more event listeners to an element, document, or window.
1462
+ *
1463
+ * Supports:
1464
+ * - Multiple events (space-separated)
1465
+ * - Event namespaces (e.g. "click.menu")
1466
+ * - Event delegation via CSS selector
1467
+ * - Custom events
1468
+ *
1469
+ * Custom Events :
1470
+ *
1471
+ * The following custom events are available:
1472
+ *
1473
+ * `longtap`
1474
+ * Fired when the user presses and holds on an element for a short duration
1475
+ * (useful for touch interfaces and long-press interactions).
1476
+ *
1477
+ * `dbltap`
1478
+ * Fired when the user performs a quick double tap on touch devices.
1479
+ *
1480
+ * These events are automatically enabled the first time they are used.
1481
+ *
1482
+ * @example
1483
+ * // Simple binding
1484
+ * dom.on(button, 'click', (ev) => {})
1485
+ *
1486
+ * // Multiple events
1487
+ * dom.on(input, 'focus blur', handler)
1488
+ *
1489
+ * // Namespaced event
1490
+ * dom.on(button, 'click.menu', handler)
1491
+ *
1492
+ * // Delegated event
1493
+ * dom.on(list, 'click', '.item', (ev) => {})
1494
+ *
1495
+ * // With options
1496
+ * dom.on(window, 'scroll', handler, { passive: true })
1497
+ *
1498
+ * @example
1499
+ * dom.on(el, 'longtap', handler)
1500
+ * dom.on(el, 'dbltap', handler)
1501
+ *
1502
+ * @param {Element|Document|Window} el - Element to bind the listener to
1503
+ * @param {string} events - Space-separated list of events (optionally namespaced)
1504
+ * @param {string|function} [selector] - CSS selector for delegation, or handler if no delegation
1505
+ * @param {function|AddEventListenerOptions|boolean} [handler] - Event handler
1506
+ * @param {AddEventListenerOptions|boolean} [options] - Native event listener options
1507
+ * @returns {Element} - The element
1508
+ */
1509
+ on,
1510
+
1511
+ /**
1512
+ * Removes event listeners previously attached with `dom.on`.
1513
+ *
1514
+ * You can remove listeners by:
1515
+ * - Event type
1516
+ * - Namespace
1517
+ * - Handler reference
1518
+ * - Selector (for delegated events)
1519
+ * - Options
1520
+ *
1521
+ * If no event is provided, all listeners on the element are removed.
1522
+ *
1523
+ * @example
1524
+ * dom.off(button, 'click')
1525
+ * dom.off(button, 'click.menu')
1526
+ * dom.off(button, 'click', handler)
1527
+ * dom.off(list, 'click', '.item', handler)
1528
+ * dom.off(button) // removes all listeners
1529
+ *
1530
+ * @param {Element|Document|Window} el - Element to unbind listeners from
1531
+ * @param {string} [events] - Space-separated events (optionally namespaced)
1532
+ * @param {string|function} [selector] - Delegation selector or handler
1533
+ * @param {function|AddEventListenerOptions|boolean} [handler] - Specific handler to remove
1534
+ * @param {AddEventListenerOptions|boolean} [options] - Listener options to match
1535
+ * @returns {Element} - The element
1536
+ */
1537
+ off,
1538
+ }
1539
+
1540
+ /* istanbul ignore next */
1541
+ if ('test' === process.env.NODE_ENV) {
1542
+ dom.__resetCustomEventsForTests = function () {
1543
+ __resetCustomEventsForTests()
1544
+ }
1545
+ }
1546
+
1547
+ export default dom
1548
+
1549
+ /* istanbul ignore next */
1550
+ if ('undefined' !== typeof window) {
1551
+ window.webf = window.webf || {}
1552
+ window.webf.dom = dom
1553
+ }