@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.
- package/README.md +3 -3
- package/dist/cjs/Mouse.js +26 -15
- package/dist/cjs/Translator.js +7 -1
- package/dist/cjs/array.js +5 -35
- package/dist/cjs/dom.js +820 -209
- package/dist/cjs/eventDispatcher.js +6 -1
- package/dist/cjs/index.js +6 -13
- package/dist/cjs/is.js +44 -1
- package/dist/cjs/math.js +56 -31
- package/dist/cjs/onOff.js +13 -9
- package/dist/cjs/traversal.js +2 -3
- package/dist/cjs/utils.js +155 -38
- package/dist/esm/Mouse.js +26 -15
- package/dist/esm/Translator.js +7 -1
- package/dist/esm/array.js +4 -35
- package/dist/esm/dom.js +819 -207
- package/dist/esm/eventDispatcher.js +7 -1
- package/dist/esm/index.js +7 -8
- package/dist/esm/is.js +43 -0
- package/dist/esm/math.js +58 -32
- package/dist/esm/onOff.js +13 -9
- package/dist/esm/traversal.js +2 -3
- package/dist/esm/utils.js +156 -39
- package/dist/umd/Translator.umd.js +1 -0
- package/dist/umd/dom.umd.js +1 -0
- package/dist/umd/eventDispatcher.umd.js +1 -0
- package/dist/umd/mouse.umd.js +1 -0
- package/dist/umd/webf.umd.js +1 -0
- package/docs/array.md +41 -8
- package/docs/dom.md +1063 -269
- package/docs/is.md +244 -0
- package/docs/math.md +87 -7
- package/docs/mouse.md +43 -0
- package/docs/translator.md +14 -14
- package/docs/traversal.md +16 -16
- package/docs/utils.md +173 -20
- package/package.json +10 -4
- package/src/Mouse.js +81 -0
- package/src/Translator.js +148 -0
- package/src/array.js +136 -0
- package/src/dom.js +1553 -0
- package/src/eventDispatcher.js +118 -0
- package/src/index.js +106 -0
- package/src/is.js +201 -0
- package/src/math.js +113 -0
- package/src/onOff.js +313 -0
- package/src/random.js +38 -0
- package/src/string.js +662 -0
- package/src/stringPrototype.js +16 -0
- package/src/traversal.js +236 -0
- package/src/utils.js +242 -0
- package/types/Mouse.d.ts +14 -3
- package/types/Translator.d.ts +6 -5
- package/types/array.d.ts +0 -1
- package/types/dom.d.ts +763 -204
- package/types/index.d.ts +22 -21
- package/types/is.d.ts +3 -0
- package/types/math.d.ts +6 -5
- package/types/traversal.d.ts +1 -1
- package/types/utils.d.ts +4 -4
- 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
|
+
}
|