tiny-essentials 1.21.9 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.vscode/extensions.json +30 -0
  2. package/.vscode/settings.json +53 -0
  3. package/LICENSE +160 -669
  4. package/dist/v1/TinyBasicsEs.min.js +1 -1
  5. package/dist/v1/TinyDragger.min.js +1 -1
  6. package/dist/v1/TinyElementObserver.min.js +1 -0
  7. package/dist/v1/TinyEssentials.min.js +1 -1
  8. package/dist/v1/TinyHtml.min.js +1 -1
  9. package/dist/v1/TinySmartScroller.min.js +1 -1
  10. package/dist/v1/TinyUploadClicker.min.js +1 -1
  11. package/dist/v1/basics/array.cjs +12 -0
  12. package/dist/v1/basics/array.d.mts +9 -0
  13. package/dist/v1/basics/array.mjs +10 -0
  14. package/dist/v1/basics/index.cjs +2 -0
  15. package/dist/v1/basics/index.d.mts +3 -1
  16. package/dist/v1/basics/index.mjs +3 -3
  17. package/dist/v1/basics/text.cjs +44 -9
  18. package/dist/v1/basics/text.d.mts +14 -2
  19. package/dist/v1/basics/text.mjs +38 -9
  20. package/dist/v1/build/TinyElementObserver.cjs +7 -0
  21. package/dist/v1/build/TinyElementObserver.d.mts +3 -0
  22. package/dist/v1/build/TinyElementObserver.mjs +2 -0
  23. package/dist/v1/index.cjs +4 -0
  24. package/dist/v1/index.d.mts +4 -1
  25. package/dist/v1/index.mjs +4 -3
  26. package/dist/v1/libs/TinyElementObserver.cjs +292 -0
  27. package/dist/v1/libs/TinyElementObserver.d.mts +154 -0
  28. package/dist/v1/libs/TinyElementObserver.mjs +266 -0
  29. package/dist/v1/libs/TinyGamepad.d.mts +1 -1
  30. package/dist/v1/libs/TinyHtml.cjs +1489 -173
  31. package/dist/v1/libs/TinyHtml.d.mts +543 -42
  32. package/dist/v1/libs/TinyHtml.mjs +1200 -61
  33. package/dist/v1/libs/TinyInventory.d.mts +1 -1
  34. package/dist/v1/libs/UltraRandomMsgGen.d.mts +7 -7
  35. package/docs/v1/README.md +9 -0
  36. package/docs/v1/Regex-Helpers.md +72 -0
  37. package/docs/v1/basics/array.md +20 -0
  38. package/docs/v1/basics/text.md +38 -7
  39. package/docs/v1/libs/TinyElementObserver.md +107 -0
  40. package/docs/v1/libs/TinyHtml.md +803 -8
  41. package/package.json +2 -2
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var array = require('../basics/array.cjs');
4
+ var text = require('../basics/text.cjs');
3
5
  var collision = require('../basics/collision.cjs');
6
+ var TinyElementObserver = require('./TinyElementObserver.cjs');
4
7
 
8
+ // TITLE: Introduction
5
9
  const {
6
10
  areElsColliding,
7
11
  areElsPerfColliding,
@@ -16,6 +20,55 @@ const {
16
20
  * @typedef {TinyHtml<ConstructorElValues>} TinyHtmlAny
17
21
  */
18
22
 
23
+ /**
24
+ * Callback function used for hover events.
25
+ * @callback HoverEventCallback
26
+ * @param {MouseEvent} ev - The mouse event triggered on enter or leave.
27
+ * @returns {void} Returns nothing.
28
+ */
29
+
30
+ /**
31
+ * Represents a collection of active style-based animations.
32
+ *
33
+ * Each HTMLElement is associated with an array of its currently running
34
+ * `Animation` objects (from the Web Animations API).
35
+ *
36
+ * @typedef {Map<HTMLElement, Animation|null>} StyleFxResult
37
+ */
38
+
39
+ /**
40
+ * Represents a collection of animation keyframe data mapped by CSS property.
41
+ *
42
+ * - The **key** is the CSS property name (e.g. `"height"`, `"opacity"`).
43
+ * - The **value** is an array of values representing the start and end
44
+ * states of the property during the animation.
45
+ *
46
+ * @typedef {Record<string, (string|number)[]>} AnimationSfxData
47
+ */
48
+
49
+ /**
50
+ * Function signature for style effects repeat detectors.
51
+ *
52
+ * @typedef {(effects: AnimationSfxData) => boolean} StyleEffectsRdFn
53
+ */
54
+
55
+ /**
56
+ * Function signature for style effect property handlers.
57
+ *
58
+ * @typedef {(
59
+ * el: HTMLElement,
60
+ * keyframes: AnimationSfxData,
61
+ * prop: string,
62
+ * style: CSSStyleDeclaration
63
+ * ) => void} StyleEffectsFn
64
+ */
65
+
66
+ /**
67
+ * A collection of style effect property handlers.
68
+ *
69
+ * @typedef {Record<string, StyleEffectsFn>} StyleEffectsProps
70
+ */
71
+
19
72
  /**
20
73
  * Callback invoked on each animation frame with the current scroll position,
21
74
  * normalized animation time (`0` to `1`), and a completion flag.
@@ -176,6 +229,34 @@ const __eventRegistry = new WeakMap();
176
229
  */
177
230
  const __elementDataMap = new WeakMap();
178
231
 
232
+ /**
233
+ * Internal storage for animation-related data, associated with elements.
234
+ * Used to remember original dimensions (height/width) and other properties
235
+ * so that animations like `slideUp` and `slideDown` can restore or continue
236
+ * smoothly, mimicking jQuery's behavior.
237
+ *
238
+ * Each element is mapped to a plain object with keys such as `origHeight`,
239
+ * `origWidth`, etc.
240
+ *
241
+ * @type {WeakMap<HTMLElement, Record<string, string|number>>}
242
+ */
243
+ const __elementAnimateData = new WeakMap();
244
+
245
+ /**
246
+ * Stores the currently active animation for each element,
247
+ * allowing cancellation or replacement of ongoing animations.
248
+ *
249
+ * @type {WeakMap<HTMLElement, { animation: Animation, id: string }>}
250
+ */
251
+ const __elementCurrentAnimation = new WeakMap();
252
+
253
+ /**
254
+ * Mapping of animation shortcuts to their effect definitions.
255
+ * Similar to jQuery's predefined effects (slideDown, fadeIn, etc.).
256
+ *
257
+ * @typedef {Record<string, string|(string|number)[]>} StyleEffects
258
+ */
259
+
179
260
  /**
180
261
  * Stores directional collision locks separately.
181
262
  * Each direction has its own WeakMap to allow independent locking.
@@ -240,6 +321,8 @@ const __elemCollision = {
240
321
  * ]} HtmlParsed
241
322
  */
242
323
 
324
+ // TITLE: Class Intro
325
+
243
326
  /**
244
327
  * TinyHtml is a utility class that provides static and instance-level methods
245
328
  * for precise dimension and position computations on HTML elements.
@@ -256,6 +339,132 @@ class TinyHtml {
256
339
 
257
340
  static Utils = { ...collision };
258
341
 
342
+ /**
343
+ * Parse inline styles into an object.
344
+ * @param {string} styleText
345
+ * @returns {Record<string,string>}
346
+ */
347
+ static parseStyle(styleText) {
348
+ /** @type {Record<string,string>}} */
349
+ const styles = {};
350
+ styleText.split(';').forEach((rule) => {
351
+ const [prop, value] = rule.split(':').map((s) => s && s.trim());
352
+ if (prop && value) {
353
+ styles[prop] = value;
354
+ }
355
+ });
356
+ return styles;
357
+ }
358
+
359
+ /////////////////////////////////////////////////////////////////////
360
+
361
+ /**
362
+ * Flag to determine if element observer should start automatically.
363
+ * @type {boolean}
364
+ */
365
+ static #autoStartElemObserver = true;
366
+
367
+ /**
368
+ * Get the auto-start flag for the observer.
369
+ * @returns {boolean}
370
+ */
371
+ static get autoStartElemObserver() {
372
+ return TinyHtml.#autoStartElemObserver;
373
+ }
374
+
375
+ /**
376
+ * Set the auto-start flag for the observer.
377
+ * @param {boolean} value
378
+ */
379
+ static set autoStartElemObserver(value) {
380
+ if (typeof value !== 'boolean') throw new TypeError('autoStartElemObserver must be a boolean.');
381
+ TinyHtml.#autoStartElemObserver = value;
382
+ }
383
+
384
+ /** @type {TinyElementObserver} */
385
+ static #tinyObserver = new TinyElementObserver({
386
+ el:
387
+ typeof window !== 'undefined' && typeof window.document !== 'undefined'
388
+ ? window.document.documentElement
389
+ : undefined,
390
+ initDetectors: [
391
+ // Style Detector
392
+ [
393
+ 'tinyStyleEvent',
394
+ (mutation) => {
395
+ if (
396
+ mutation.type !== 'attributes' ||
397
+ mutation.attributeName !== 'style' ||
398
+ !(mutation.target instanceof HTMLElement)
399
+ )
400
+ return;
401
+ const oldVal = mutation.oldValue || '';
402
+ const newVal = mutation.target.getAttribute('style') || '';
403
+
404
+ const oldStyles = TinyHtml.parseStyle(oldVal);
405
+ const newStyles = TinyHtml.parseStyle(newVal);
406
+
407
+ const changes = text.diffStrings(oldStyles, newStyles);
408
+
409
+ if (
410
+ Object.keys(changes.added).length ||
411
+ Object.keys(changes.removed).length ||
412
+ Object.keys(changes.modified).length
413
+ ) {
414
+ mutation.target.dispatchEvent(
415
+ new CustomEvent('tinyhtml.stylechanged', {
416
+ detail: changes,
417
+ }),
418
+ );
419
+ }
420
+ },
421
+ ],
422
+
423
+ // Class Detector
424
+ [
425
+ 'tinyClassEvent',
426
+ (mutation) => {
427
+ if (
428
+ mutation.type !== 'attributes' ||
429
+ mutation.attributeName !== 'class' ||
430
+ !(mutation.target instanceof HTMLElement)
431
+ )
432
+ return;
433
+ const oldVal = mutation.oldValue || '';
434
+ const newVal = mutation.target.className || '';
435
+
436
+ const oldClasses = oldVal.split(/\s+/).filter(Boolean);
437
+ const newClasses = newVal.split(/\s+/).filter(Boolean);
438
+
439
+ const changes = array.diffArrayList(oldClasses, newClasses);
440
+
441
+ if (changes.added.length || changes.removed.length) {
442
+ mutation.target.dispatchEvent(
443
+ new CustomEvent('tinyhtml.classchanged', {
444
+ detail: changes,
445
+ }),
446
+ );
447
+ }
448
+ },
449
+ ],
450
+ ],
451
+ initCfg: {
452
+ attributeOldValue: true,
453
+ attributes: true,
454
+ subtree: true,
455
+ attributeFilter: ['style', 'class'],
456
+ },
457
+ });
458
+
459
+ /** @returns {TinyElementObserver} */
460
+ static get tinyObserver() {
461
+ return TinyHtml.#tinyObserver;
462
+ }
463
+
464
+ ///////////////////////////////////////////////////////////////////
465
+
466
+ // TITLE: Fetch Html List
467
+
259
468
  /**
260
469
  * Fetches an HTML file from the given URL, parses it to JSON.
261
470
  *
@@ -302,6 +511,8 @@ class TinyHtml {
302
511
  return TinyHtml.toTinyElm(nodes);
303
512
  }
304
513
 
514
+ // TITLE: Fetch Template List
515
+
305
516
  /**
306
517
  * Converts the content of a <template> to an array of HtmlParsed.
307
518
  *
@@ -347,6 +558,8 @@ class TinyHtml {
347
558
  return TinyHtml.toTinyElm(TinyHtml.templateToNodes(nodes));
348
559
  }
349
560
 
561
+ // TITLE: Fetch Json List
562
+
350
563
  /**
351
564
  * Parses a full HTML string into a JSON-like structure.
352
565
  *
@@ -438,6 +651,8 @@ class TinyHtml {
438
651
  return TinyHtml.toTinyElm(TinyHtml.jsonToNodes(jsonArray));
439
652
  }
440
653
 
654
+ // TITLE: Element Creator
655
+
441
656
  /**
442
657
  * Creates a new TinyHtml element from a tag name and optional attributes.
443
658
  *
@@ -521,6 +736,9 @@ class TinyHtml {
521
736
  return new TinyHtml(template.content.firstChild);
522
737
  }
523
738
 
739
+ ///////////////////////////////////////////////////
740
+ // TITLE: Query Script
741
+
524
742
  /**
525
743
  * Queries the document for the first element matching the CSS selector and wraps it in a TinyHtml instance.
526
744
  *
@@ -639,6 +857,8 @@ class TinyHtml {
639
857
 
640
858
  //////////////////////////////////////////////////////////////////
641
859
 
860
+ // TITLE: Element getter
861
+
642
862
  /**
643
863
  * Iterates over all elements, executing the provided callback on each.
644
864
  * @param {(element: TinyHtmlAny, index: number, items: TinyHtmlAny[]) => void} callback - Function invoked for each element.
@@ -695,6 +915,10 @@ class TinyHtml {
695
915
  return [...this.#el];
696
916
  }
697
917
 
918
+ ////////////////////////////////////////////////
919
+
920
+ // TITLE: Element Getter (Private) (Pt1)
921
+
698
922
  /**
699
923
  * Returns the current Element held by this instance.
700
924
  *
@@ -737,6 +961,8 @@ class TinyHtml {
737
961
 
738
962
  //////////////////////////////////////////////////////
739
963
 
964
+ // TITLE: Element Getter (Private) (Pt2)
965
+
740
966
  /**
741
967
  * Prepares and validates multiple elements against allowed types.
742
968
  *
@@ -1069,6 +1295,10 @@ class TinyHtml {
1069
1295
  return TinyHtml._preElemTemplate(elems, where, [Element, Document], ['Element', 'Document']);
1070
1296
  }
1071
1297
 
1298
+ //////////////////////////////////////////////////////
1299
+
1300
+ // TITLE: Converter DOM <--> TinyHtml
1301
+
1072
1302
  /**
1073
1303
  * Normalizes and converts one or more DOM elements (or TinyHtml instances)
1074
1304
  * into an array of `TinyHtml` instances.
@@ -1122,6 +1352,10 @@ class TinyHtml {
1122
1352
  return checkElement(elems);
1123
1353
  }
1124
1354
 
1355
+ //////////////////////////////////////////////////
1356
+
1357
+ // TITLE: DOM Getter
1358
+
1125
1359
  /**
1126
1360
  * Filters an array of elements based on a selector, function, element, or array of elements.
1127
1361
  *
@@ -1348,6 +1582,8 @@ class TinyHtml {
1348
1582
 
1349
1583
  //////////////////////////////////////////////////////////////////
1350
1584
 
1585
+ // TITLE: Data Manager
1586
+
1351
1587
  /**
1352
1588
  * Internal data storage for element information.
1353
1589
  * @type {ElementDataStore}
@@ -1464,6 +1700,8 @@ class TinyHtml {
1464
1700
 
1465
1701
  //////////////////////////////////////////////////////
1466
1702
 
1703
+ // TITLE: DOM Getter 2
1704
+
1467
1705
  /**
1468
1706
  * Get the sibling element in a given direction.
1469
1707
  *
@@ -1763,6 +2001,10 @@ class TinyHtml {
1763
2001
  return TinyHtml.contents(this);
1764
2002
  }
1765
2003
 
2004
+ ////////////////////////////////////////////
2005
+
2006
+ // TITLE: Clone Dom
2007
+
1766
2008
  /**
1767
2009
  * Clone each element.
1768
2010
  * @param {TinyNode|TinyNode[]} el
@@ -1783,6 +2025,10 @@ class TinyHtml {
1783
2025
  return TinyHtml.clone(this, deep)[0];
1784
2026
  }
1785
2027
 
2028
+ //////////////////////////////////////////////////
2029
+
2030
+ // TITLE: Append Content
2031
+
1786
2032
  /**
1787
2033
  * Normalize and validate nodes before DOM insertion.
1788
2034
  * Converts TinyNode-like structures or strings into DOM-compatible nodes.
@@ -2073,6 +2319,8 @@ class TinyHtml {
2073
2319
 
2074
2320
  //////////////////////////////////////////////////////
2075
2321
 
2322
+ // TITLE: Constructor Base
2323
+
2076
2324
  /**
2077
2325
  * The target HTML element for instance-level operations.
2078
2326
  * @type {ConstructorElValues[]}
@@ -2141,6 +2389,8 @@ class TinyHtml {
2141
2389
 
2142
2390
  /////////////////////////////////////////////////////
2143
2391
 
2392
+ // TITLE: CSS Stuff
2393
+
2144
2394
  /**
2145
2395
  * Returns the full computed CSS styles for the given element.
2146
2396
  *
@@ -2274,6 +2524,8 @@ class TinyHtml {
2274
2524
 
2275
2525
  //////////////////////////////////////////////////////////////////////
2276
2526
 
2527
+ // TITLE: Style Stuff
2528
+
2277
2529
  /**
2278
2530
  * Stores camelCase to kebab-case CSS property aliases.
2279
2531
  *
@@ -2789,6 +3041,8 @@ class TinyHtml {
2789
3041
 
2790
3042
  //////////////////////////////////////////////////////////////////////
2791
3043
 
3044
+ // TITLE: Focus/Blur
3045
+
2792
3046
  /**
2793
3047
  * Focus the element.
2794
3048
  *
@@ -2831,6 +3085,10 @@ class TinyHtml {
2831
3085
  return TinyHtml.blur(this);
2832
3086
  }
2833
3087
 
3088
+ /////////////////////////////////////////////////////////
3089
+
3090
+ // TITLE: Select
3091
+
2834
3092
  /**
2835
3093
  * Select the text content of an input or textarea element.
2836
3094
  *
@@ -2853,6 +3111,8 @@ class TinyHtml {
2853
3111
  return TinyHtml.select(this);
2854
3112
  }
2855
3113
 
3114
+ // TITLE: Bool Checker
3115
+
2856
3116
  /**
2857
3117
  * Interprets a value as a boolean `true` if it matches a common truthy representation.
2858
3118
  *
@@ -2879,6 +3139,8 @@ class TinyHtml {
2879
3139
 
2880
3140
  //////////////////////////////////////////////////////////////////////
2881
3141
 
3142
+ // TITLE: Window Getter/Setter
3143
+
2882
3144
  /**
2883
3145
  * Sets the vertical scroll position of the window.
2884
3146
  * @param {number} value - Sets the scroll position.
@@ -3213,194 +3475,1143 @@ class TinyHtml {
3213
3475
 
3214
3476
  //////////////////////////////////////////////////
3215
3477
 
3478
+ // TITLE: Animate DOM (Data)
3479
+
3216
3480
  /**
3217
- * Applies an animation to one or multiple TinyElement instances.
3481
+ * Retrieves stored animation data for a given element and key.
3482
+ * If no data exists yet, initializes storage for that element.
3218
3483
  *
3219
- * @template {TinyElement|TinyElement[]} T
3220
- * @param {T} el - A single TinyElement or an array of TinyElements to animate.
3221
- * @param {Keyframe[] | PropertyIndexedKeyframes | null} keyframes - The keyframes used to define the animation.
3222
- * @param {number | KeyframeAnimationOptions} [ops] - Timing or configuration options for the animation.
3223
- * @returns {T}
3484
+ * @param {HTMLElement} el - The element whose data should be retrieved.
3485
+ * @param {string} where - The key to read (e.g., "origHeight").
3486
+ * @returns {string|number|undefined} - The stored value, or undefined.
3224
3487
  */
3225
- static animate(el, keyframes, ops) {
3226
- TinyHtml._preElems(el, 'animate').forEach((elem) => elem.animate(keyframes, ops));
3227
- return el;
3488
+ static getAnimateData(el, where) {
3489
+ let dataset = __elementAnimateData.get(el);
3490
+ if (!dataset) {
3491
+ dataset = {};
3492
+ __elementAnimateData.set(el, dataset);
3493
+ }
3494
+ return dataset[where];
3228
3495
  }
3229
3496
 
3230
3497
  /**
3231
- * Applies an animation to one or multiple TinyElement instances.
3498
+ * Stores animation data for a given element and key.
3499
+ * Used to cache original size/values for animations.
3232
3500
  *
3233
- * @param {Keyframe[] | PropertyIndexedKeyframes | null} keyframes - The keyframes used to define the animation.
3234
- * @param {number | KeyframeAnimationOptions} [ops] - Timing or configuration options for the animation.
3235
- * @returns {this}
3501
+ * @param {HTMLElement} el - The element whose data should be set.
3502
+ * @param {string} where - The key to store under (e.g., "origHeight").
3503
+ * @param {string|number} value - The value to store.
3236
3504
  */
3237
- animate(keyframes, ops) {
3238
- return TinyHtml.animate(this, keyframes, ops);
3505
+ static setAnimateData(el, where, value) {
3506
+ if (!(el instanceof HTMLElement))
3507
+ throw new TypeError('setAnimateData: el must be an HTMLElement.');
3508
+ if (typeof where !== 'string') throw new TypeError('setAnimateData: where must be a string.');
3509
+ if (!(typeof value === 'string' || typeof value === 'number'))
3510
+ throw new TypeError('setAnimateData: value must be a string or number.');
3511
+
3512
+ let dataset = __elementAnimateData.get(el);
3513
+ if (!dataset) {
3514
+ dataset = {};
3515
+ __elementAnimateData.set(el, dataset);
3516
+ }
3517
+ dataset[where] = value;
3239
3518
  }
3240
3519
 
3520
+ // TITLE: Animate DOM (cancelOldStyleFx)
3521
+
3241
3522
  /**
3242
- * Gets the offset of the element relative to the document.
3243
- * @param {TinyElement} el - Target element.
3244
- * @returns {{top: number, left: number}}
3523
+ * Global configuration flag controlling whether old style-based animations
3524
+ * are cancelled before a new one starts. Defaults to true.
3525
+ *
3526
+ * @type {boolean}
3245
3527
  */
3246
- static offset(el) {
3247
- const elem = TinyHtml._preElem(el, 'offset');
3248
- const rect = elem.getBoundingClientRect();
3249
- const scrollTop = window.scrollY || document.documentElement.scrollTop;
3250
- const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
3251
-
3252
- return {
3253
- top: rect.top + scrollTop,
3254
- left: rect.left + scrollLeft,
3255
- };
3256
- }
3528
+ static #cancelOldStyleFx = true;
3257
3529
 
3258
3530
  /**
3259
- * Gets the offset of the element relative to the document.
3260
- * @returns {{top: number, left: number}}
3531
+ * Returns the current global setting for cancelling old style-based animations.
3532
+ *
3533
+ * @returns {boolean} True if old animations are cancelled by default, false otherwise.
3261
3534
  */
3262
- offset() {
3263
- return TinyHtml.offset(this);
3535
+ static get cancelOldStyleFx() {
3536
+ return TinyHtml.#cancelOldStyleFx;
3264
3537
  }
3265
3538
 
3266
3539
  /**
3267
- * Gets the position of the element relative to its offset parent.
3268
- * @param {TinyHtmlElement} el - Target element.
3269
- * @returns {{top: number, left: number}}
3540
+ * Updates the global setting that determines whether old style-based animations
3541
+ * are cancelled before new ones start.
3542
+ *
3543
+ * @param {boolean} value - True to cancel old animations by default, false to keep them running.
3544
+ * @throws {TypeError} If the value is not a boolean.
3270
3545
  */
3271
- static position(el) {
3272
- const elem = TinyHtml._preHtmlElem(el, 'position');
3273
-
3274
- let offsetParent;
3275
- let offset;
3276
- let parentOffset = { top: 0, left: 0 };
3546
+ static set cancelOldStyleFx(value) {
3547
+ if (typeof value !== 'boolean') throw new TypeError('Expected a boolean value.');
3548
+ TinyHtml.#cancelOldStyleFx = value;
3549
+ }
3277
3550
 
3278
- const computedStyle = window.getComputedStyle(elem);
3551
+ // TITLE: Animate DOM (styleFxSpeeds)
3279
3552
 
3280
- if (computedStyle.position === 'fixed') {
3281
- offset = elem.getBoundingClientRect();
3282
- } else {
3283
- offset = TinyHtml.offset(elem);
3553
+ /**
3554
+ * Predefined animation speed options, inspired by jQuery.fx.speeds.
3555
+ * Each entry can be either a number (duration in ms) or a KeyframeAnimationOptions object.
3556
+ *
3557
+ * Usage example:
3558
+ * TinyHtml.animate(el, keyframes, TinyHtml.#fxSpeeds.fast);
3559
+ * TinyHtml.slideDown(el, TinyHtml.#fxSpeeds.slow);
3560
+ *
3561
+ * @type {Record<string, number | KeyframeAnimationOptions>}
3562
+ */
3563
+ static #styleFxSpeeds = {
3564
+ slow: { duration: 600, easing: 'ease' },
3565
+ fast: { duration: 200, easing: 'ease-out' },
3566
+ _default: { duration: 400, easing: 'linear' },
3567
+ };
3284
3568
 
3285
- offsetParent = elem.offsetParent || document.documentElement;
3286
- const { position } = window.getComputedStyle(offsetParent);
3569
+ /**
3570
+ * Get a cloned copy of predefined animation speeds.
3571
+ * @returns {Record<string, number | KeyframeAnimationOptions>}
3572
+ */
3573
+ static get styleFxSpeeds() {
3574
+ /** @type {Record<string, number | KeyframeAnimationOptions>} */
3575
+ const data = {};
3576
+ for (const name in TinyHtml.#styleFxSpeeds) {
3577
+ const info = TinyHtml.#styleFxSpeeds[name];
3578
+ data[name] = typeof info === 'object' ? { ...info } : info;
3579
+ }
3580
+ return data;
3581
+ }
3287
3582
 
3288
- while (
3289
- offsetParent instanceof HTMLElement &&
3290
- (offsetParent === document.body || offsetParent === document.documentElement) &&
3291
- position === 'static'
3292
- ) {
3293
- offsetParent = offsetParent.parentNode;
3294
- }
3583
+ /**
3584
+ * Replace the predefined animation speeds.
3585
+ * @param {Record<string, number | KeyframeAnimationOptions>} speeds
3586
+ * @throws {TypeError} If input is not a valid object of speed definitions.
3587
+ */
3588
+ static set styleFxSpeeds(speeds) {
3589
+ if (typeof speeds !== 'object' || speeds === null || Array.isArray(speeds))
3590
+ throw new TypeError('styleFxSpeeds must be an object.');
3295
3591
 
3296
- if (
3297
- offsetParent instanceof HTMLElement &&
3298
- offsetParent !== elem &&
3299
- offsetParent.nodeType === 1
3300
- ) {
3301
- const { borderTopWidth, borderLeftWidth } = TinyHtml.cssFloats(offsetParent, [
3302
- 'borderTopWidth',
3303
- 'borderLeftWidth',
3304
- ]);
3305
- parentOffset = TinyHtml.offset(offsetParent);
3306
- parentOffset.top += borderTopWidth;
3307
- parentOffset.left += borderLeftWidth;
3308
- }
3592
+ for (const [k, v] of Object.entries(speeds)) {
3593
+ if (!(typeof v === 'number' || typeof v === 'object'))
3594
+ throw new TypeError(`styleFxSpeeds["${k}"] must be a number or KeyframeAnimationOptions.`);
3309
3595
  }
3310
3596
 
3311
- return {
3312
- top: offset.top - parentOffset.top - TinyHtml.cssFloat(elem, 'marginTop'),
3313
- left: offset.left - parentOffset.left - TinyHtml.cssFloat(elem, 'marginLeft'),
3314
- };
3597
+ TinyHtml.#styleFxSpeeds = {};
3598
+ for (const [k, v] of Object.entries(speeds)) TinyHtml.setStyleFxSpeed(k, v);
3315
3599
  }
3316
3600
 
3317
3601
  /**
3318
- * Gets the position of the element relative to its offset parent.
3319
- * @returns {{top: number, left: number}}
3602
+ * Get a predefined animation speed by name.
3603
+ *
3604
+ * @param {string} name - The name of the speed entry.
3605
+ * @returns {number | KeyframeAnimationOptions | undefined} A cloned value of the speed entry, or undefined if not found.
3320
3606
  */
3321
- position() {
3322
- return TinyHtml.position(this);
3607
+ static getStyleFxSpeed(name) {
3608
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3609
+ const spd = TinyHtml.#styleFxSpeeds[name];
3610
+ return typeof spd === 'object' ? { ...spd } : spd;
3323
3611
  }
3324
3612
 
3325
3613
  /**
3326
- * Gets the closest positioned ancestor element.
3327
- * @param {TinyHtmlElement} el - Target element.
3328
- * @returns {HTMLElement} - Offset parent element.
3614
+ * Set or overwrite a predefined animation speed.
3615
+ *
3616
+ * @param {string} name - The name of the speed entry.
3617
+ * @param {number | KeyframeAnimationOptions} value - The value to store.
3618
+ * @throws {TypeError} If value is not a number or KeyframeAnimationOptions object.
3329
3619
  */
3330
- static offsetParent(el) {
3331
- const elem = TinyHtml._preHtmlElem(el, 'offsetParent');
3332
- let offsetParent = elem.offsetParent;
3333
-
3334
- while (
3335
- offsetParent instanceof HTMLElement &&
3336
- window.getComputedStyle(offsetParent).position === 'static'
3337
- ) {
3338
- offsetParent = offsetParent.offsetParent;
3339
- }
3340
-
3341
- // Fallback to document.documentElement
3342
- return offsetParent instanceof HTMLElement ? offsetParent : document.documentElement;
3620
+ static setStyleFxSpeed(name, value) {
3621
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3622
+ if (!(typeof value === 'number' || typeof value === 'object'))
3623
+ throw new TypeError('styleFxSpeed must be a number or KeyframeAnimationOptions');
3624
+ TinyHtml.#styleFxSpeeds[name] = typeof value === 'object' ? { ...value } : value;
3343
3625
  }
3344
3626
 
3345
3627
  /**
3346
- * Gets the closest positioned ancestor element.
3347
- * @returns {HTMLElement} - Offset parent element.
3628
+ * Delete a predefined animation speed by name.
3629
+ *
3630
+ * @param {string} name - The name of the speed entry to delete.
3631
+ * @returns {boolean} True if the property was deleted, false otherwise.
3348
3632
  */
3349
- offsetParent() {
3350
- return TinyHtml.offsetParent(this);
3633
+ static deleteStyleFxSpeed(name) {
3634
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3635
+ return delete TinyHtml.#styleFxSpeeds[name];
3351
3636
  }
3352
3637
 
3353
3638
  /**
3354
- * Gets the vertical scroll position.
3355
- * @param {TinyElementAndWindow} el - Element or window.
3356
- * @returns {number}
3639
+ * Check if a predefined animation speed exists.
3640
+ *
3641
+ * @param {string} name - The name of the speed entry to check.
3642
+ * @returns {boolean} True if the speed entry exists, false otherwise.
3357
3643
  */
3358
- static scrollTop(el) {
3359
- const elem = TinyHtml._preElemAndWindow(el, 'scrollTop');
3360
- if (TinyHtml.isWindow(elem)) return elem.pageYOffset;
3361
- // @ts-ignore
3362
- if (elem.nodeType === 9) return elem.defaultView.pageYOffset;
3363
- return elem.scrollTop;
3644
+ static hasStyleFxSpeed(name) {
3645
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3646
+ return Object.prototype.hasOwnProperty.call(TinyHtml.#styleFxSpeeds, name);
3364
3647
  }
3365
3648
 
3649
+ // TITLE: Animate DOM (cssExpand)
3650
+
3366
3651
  /**
3367
- * Gets the vertical scroll position.
3368
- * @returns {number}
3652
+ * CSS expansion shorthand used by genStyleFx to include margin/padding values.
3653
+ * @typedef {['Top', 'Right', 'Bottom', 'Left']}
3369
3654
  */
3370
- scrollTop() {
3371
- return TinyHtml.scrollTop(this);
3372
- }
3655
+ static #cssExpand = ['Top', 'Right', 'Bottom', 'Left'];
3656
+
3657
+ // TITLE: Animate DOM (styleEffects)
3373
3658
 
3374
3659
  /**
3375
- * Gets the horizontal scroll position.
3376
- * @param {TinyElementAndWindow} el - Element or window.
3377
- * @returns {number}
3660
+ * Generate shortcuts
3661
+ * @type {Record<string, StyleEffects>}
3378
3662
  */
3379
- static scrollLeft(el) {
3380
- const elem = TinyHtml._preElemAndWindow(el, 'scrollLeft');
3381
- if (TinyHtml.isWindow(elem)) return elem.pageXOffset;
3382
- // @ts-ignore
3383
- if (elem.nodeType === 9) return elem.defaultView.pageXOffset;
3384
- return elem.scrollLeft;
3385
- }
3663
+ static #styleEffects = {
3664
+ slideDown: TinyHtml.genStyleFx('show'),
3665
+ slideUp: TinyHtml.genStyleFx('hide'),
3666
+ fadeIn: { opacity: 'show' },
3667
+ fadeOut: { opacity: 'hide' },
3668
+ };
3386
3669
 
3387
3670
  /**
3388
- * Gets the horizontal scroll position.
3389
- * @returns {number}
3671
+ * Returns a deep-cloned copy of the registered style effects.
3672
+ *
3673
+ * @returns {Record<string, StyleEffects>}
3390
3674
  */
3391
- scrollLeft() {
3392
- return TinyHtml.scrollLeft(this);
3675
+ static get styleEffects() {
3676
+ return JSON.parse(JSON.stringify(TinyHtml.#styleEffects));
3393
3677
  }
3394
3678
 
3395
3679
  /**
3396
- * Collection of easing functions used for scroll and animation calculations.
3397
- * Each function receives a normalized time value (`t` from 0 to 1) and returns the eased progress.
3680
+ * Replace the entire styleEffects map with a new one.
3398
3681
  *
3399
- * @type {Record<string, (t: number) => number>}
3682
+ * @param {Record<string, StyleEffects>} value
3400
3683
  */
3401
- static easings = {
3402
- linear: (t) => t,
3403
- easeInQuad: (t) => t * t,
3684
+ static set styleEffects(value) {
3685
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
3686
+ throw new TypeError('styleEffects must be an object');
3687
+ for (const name in value) {
3688
+ for (const [prop, mode] of Object.entries(value[name])) {
3689
+ if (
3690
+ typeof mode !== 'string' &&
3691
+ (!Array.isArray(mode) ||
3692
+ !mode.every((v) => typeof v === 'string' || typeof v === 'number'))
3693
+ )
3694
+ throw new TypeError(`Invalid styleEffect["${prop}"]`);
3695
+ }
3696
+ }
3697
+
3698
+ TinyHtml.#styleEffects = {};
3699
+ for (const name in value) TinyHtml.setStyleEffect(name, value[name]);
3700
+ }
3701
+
3702
+ /**
3703
+ * Get a deep-cloned style effect by name.
3704
+ * @param {string} name
3705
+ * @returns {StyleEffects|undefined}
3706
+ */
3707
+ static getStyleEffect(name) {
3708
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3709
+ const eff = TinyHtml.#styleEffects[name];
3710
+ return eff ? JSON.parse(JSON.stringify(eff)) : undefined;
3711
+ }
3712
+
3713
+ /**
3714
+ * Set or replace a style effect.
3715
+ * @param {string} name
3716
+ * @param {StyleEffects} value
3717
+ */
3718
+ static setStyleEffect(name, value) {
3719
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3720
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
3721
+ throw new TypeError('styleEffect must be an object');
3722
+ }
3723
+ /** @type {StyleEffects} */
3724
+ const copy = {};
3725
+ for (const [prop, mode] of Object.entries(value)) {
3726
+ if (typeof mode === 'string') {
3727
+ copy[prop] = mode;
3728
+ } else if (
3729
+ Array.isArray(mode) &&
3730
+ mode.every((v) => typeof v === 'string' || typeof v === 'number')
3731
+ ) {
3732
+ copy[prop] = [...mode];
3733
+ } else {
3734
+ throw new TypeError(`Invalid styleEffect["${prop}"]`);
3735
+ }
3736
+ }
3737
+ TinyHtml.#styleEffects[name] = copy;
3738
+ }
3739
+
3740
+ /**
3741
+ * Delete a style effect.
3742
+ * @param {string} name
3743
+ * @returns {boolean} True if deleted.
3744
+ */
3745
+ static deleteStyleEffect(name) {
3746
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3747
+ return delete TinyHtml.#styleEffects[name];
3748
+ }
3749
+
3750
+ /**
3751
+ * Check if a style effect exists.
3752
+ * @param {string} name
3753
+ * @returns {boolean}
3754
+ */
3755
+ static hasStyleEffect(name) {
3756
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3757
+ return Object.prototype.hasOwnProperty.call(TinyHtml.#styleEffects, name);
3758
+ }
3759
+
3760
+ // TITLE: Animate DOM (styleEffectInverse)
3761
+
3762
+ /**
3763
+ * Style Effect Inverse Values
3764
+ * @type {Record<string, string>}
3765
+ */
3766
+ static #styleEffectInverse = {
3767
+ slideDown: 'slideUp',
3768
+ slideUp: 'slideDown',
3769
+ fadeIn: 'fadeOut',
3770
+ fadeOut: 'fadeIn',
3771
+ };
3772
+
3773
+ /**
3774
+ * Get a cloned copy of style inverse values.
3775
+ * @returns {Record<string, string>}
3776
+ */
3777
+ static get styleEffectInverse() {
3778
+ return { ...TinyHtml.#styleEffectInverse };
3779
+ }
3780
+
3781
+ /**
3782
+ * Replace the style inverse values.
3783
+ * @param {Record<string, string>} values
3784
+ * @throws {TypeError} If not a valid object with functions as values.
3785
+ */
3786
+ static set styleEffectInverse(values) {
3787
+ if (typeof values !== 'object' || values === null)
3788
+ throw new TypeError('styleEffectInverse must be an object.');
3789
+
3790
+ for (const [k, v] of Object.entries(values)) {
3791
+ if (typeof v !== 'string')
3792
+ throw new TypeError(`styleEffectInverse["${k}"] must be a string.`);
3793
+ }
3794
+
3795
+ TinyHtml.#styleEffectInverse = {};
3796
+ for (const [k, v] of Object.entries(values)) TinyHtml.setStyleEffectInverse(k, v);
3797
+ }
3798
+
3799
+ /**
3800
+ * Get a registered inverse values.
3801
+ *
3802
+ * @param {string} name - The detector name.
3803
+ * @returns {string | null} The value if found, otherwise undefined.
3804
+ */
3805
+ static getStyleEffectInverse(name) {
3806
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3807
+ return TinyHtml.#styleEffectInverse[name] || null;
3808
+ }
3809
+
3810
+ /**
3811
+ * Register or overwrite a inverse value.
3812
+ *
3813
+ * @param {string} name - The detector name.
3814
+ * @param {string} value - The inverse value.
3815
+ * @throws {TypeError} If fn is not a string.
3816
+ */
3817
+ static setStyleEffectInverse(name, value) {
3818
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3819
+ if (typeof value !== 'string')
3820
+ throw new TypeError(`styleEffectInverse["${name}"] must be a string.`);
3821
+ TinyHtml.#styleEffectInverse[name] = value;
3822
+ }
3823
+
3824
+ /**
3825
+ * Delete a inverse value by name.
3826
+ *
3827
+ * @param {string} name - The inverse value name to delete.
3828
+ * @returns {boolean} True if deleted, false otherwise.
3829
+ */
3830
+ static deleteStyleEffectInverse(name) {
3831
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3832
+ return delete TinyHtml.#styleEffectInverse[name];
3833
+ }
3834
+
3835
+ /**
3836
+ * Check if a inverse value is registered.
3837
+ *
3838
+ * @param {string} name - The inverse value name to check.
3839
+ * @returns {boolean} True if registered, false otherwise.
3840
+ */
3841
+ static hasStyleEffectInverse(name) {
3842
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3843
+ return Object.prototype.hasOwnProperty.call(TinyHtml.#styleEffectInverse, name);
3844
+ }
3845
+
3846
+ // TITLE: Animate DOM (styleEffectsRd)
3847
+
3848
+ /**
3849
+ * Style Effect Repeat Detector
3850
+ * @type {Record<string, StyleEffectsRdFn>}
3851
+ */
3852
+ static #styleEffectsRd = {
3853
+ slideDown: (effects) =>
3854
+ effects.height[0] === effects.height[1] &&
3855
+ (!effects.width || effects.width[0] === effects.width[1]),
3856
+ slideUp: (effects) =>
3857
+ effects.height[0] === effects.height[1] &&
3858
+ (!effects.width || effects.width[0] === effects.width[1]),
3859
+ fadeIn: (effects) => Number(effects.opacity[0]) === Number(effects.opacity[1]),
3860
+ fadeOut: (effects) => Number(effects.opacity[0]) === Number(effects.opacity[1]),
3861
+ };
3862
+
3863
+ /**
3864
+ * Get a cloned copy of style effects repeat detectors.
3865
+ * @returns {Record<string, StyleEffectsRdFn>}
3866
+ */
3867
+ static get styleEffectsRd() {
3868
+ return { ...TinyHtml.#styleEffectsRd };
3869
+ }
3870
+
3871
+ /**
3872
+ * Replace the style effects repeat detectors.
3873
+ * @param {Record<string, StyleEffectsRdFn>} detectors
3874
+ * @throws {TypeError} If not a valid object with functions as values.
3875
+ */
3876
+ static set styleEffectsRd(detectors) {
3877
+ if (typeof detectors !== 'object' || detectors === null)
3878
+ throw new TypeError('styleEffectsRd must be an object.');
3879
+
3880
+ for (const [k, v] of Object.entries(detectors)) {
3881
+ if (typeof v !== 'function')
3882
+ throw new TypeError(`styleEffectsRd["${k}"] must be a function.`);
3883
+ }
3884
+
3885
+ TinyHtml.#styleEffectsRd = {};
3886
+ for (const [k, v] of Object.entries(detectors)) TinyHtml.setStyleEffectRd(k, v);
3887
+ }
3888
+
3889
+ /**
3890
+ * Get a registered repeat detector function by name.
3891
+ *
3892
+ * @param {string} name - The detector name.
3893
+ * @returns {StyleEffectsRdFn | null} The function if found, otherwise undefined.
3894
+ */
3895
+ static getStyleEffectRd(name) {
3896
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3897
+ return TinyHtml.#styleEffectsRd[name] || null;
3898
+ }
3899
+
3900
+ /**
3901
+ * Register or overwrite a repeat detector function.
3902
+ *
3903
+ * @param {string} name - The detector name.
3904
+ * @param {StyleEffectsRdFn} fn - The detector function.
3905
+ * @throws {TypeError} If fn is not a function.
3906
+ */
3907
+ static setStyleEffectRd(name, fn) {
3908
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3909
+ if (typeof fn !== 'function')
3910
+ throw new TypeError(`styleEffectsRd["${name}"] must be a function.`);
3911
+ TinyHtml.#styleEffectsRd[name] = fn;
3912
+ }
3913
+
3914
+ /**
3915
+ * Delete a repeat detector function by name.
3916
+ *
3917
+ * @param {string} name - The detector name to delete.
3918
+ * @returns {boolean} True if deleted, false otherwise.
3919
+ */
3920
+ static deleteStyleEffectRd(name) {
3921
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3922
+ return delete TinyHtml.#styleEffectsRd[name];
3923
+ }
3924
+
3925
+ /**
3926
+ * Check if a repeat detector function is registered.
3927
+ *
3928
+ * @param {string} name - The detector name to check.
3929
+ * @returns {boolean} True if registered, false otherwise.
3930
+ */
3931
+ static hasStyleEffectRd(name) {
3932
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
3933
+ return Object.prototype.hasOwnProperty.call(TinyHtml.#styleEffectsRd, name);
3934
+ }
3935
+
3936
+ // TITLE: Animate DOM (styleEffectsPromps)
3937
+
3938
+ /**
3939
+ * Effect property handlers for show, hide, and toggle.
3940
+ * Each function builds keyframes depending on the property being animated.
3941
+ *
3942
+ * @type {StyleEffectsProps}
3943
+ */
3944
+ static #styleEffectsProps = {
3945
+ show: (el, keyframes, prop, style) => {
3946
+ if (prop === 'height' || prop === 'width') {
3947
+ const targetSize = TinyHtml.getAnimateData(el, `orig${prop}`) || el.scrollHeight + 'px';
3948
+ TinyHtml.setAnimateData(el, `orig${prop}`, targetSize);
3949
+
3950
+ const current = style[prop]; // pega valor atual
3951
+ keyframes[prop] = [current, targetSize];
3952
+ } else if (prop.startsWith('margin') || prop.startsWith('padding')) {
3953
+ // @ts-ignore
3954
+ const orig = TinyHtml.getAnimateData(el, prop) || style[prop];
3955
+ TinyHtml.setAnimateData(el, prop, orig);
3956
+ keyframes[prop] = ['0px', orig];
3957
+ } else if (prop === 'opacity') {
3958
+ const current = style.opacity;
3959
+ keyframes[prop] = [current, 1];
3960
+ } else {
3961
+ // @ts-ignore
3962
+ const current = TinyHtml.getAnimateData(el, prop) || style[prop];
3963
+ TinyHtml.setAnimateData(el, prop, current);
3964
+ // @ts-ignore
3965
+ keyframes[prop] = [current, style[prop] || 1];
3966
+ }
3967
+ },
3968
+ hide: (el, keyframes, prop, style) => {
3969
+ if (prop === 'height' || prop === 'width') {
3970
+ const targetSize = TinyHtml.getAnimateData(el, `orig${prop}`) || style[prop];
3971
+ TinyHtml.setAnimateData(el, `orig${prop}`, targetSize);
3972
+
3973
+ const current = style[prop]; // pega valor atual
3974
+ keyframes[prop] = [current, 0];
3975
+ } else if (prop.startsWith('margin') || prop.startsWith('padding')) {
3976
+ // @ts-ignore
3977
+ const orig = TinyHtml.getAnimateData(el, prop) || style[prop];
3978
+ TinyHtml.setAnimateData(el, prop, orig);
3979
+
3980
+ // @ts-ignore
3981
+ keyframes[prop] = [style[prop], '0px'];
3982
+ } else if (prop === 'opacity') {
3983
+ const current = style.opacity;
3984
+ keyframes[prop] = [current, 0];
3985
+ } else {
3986
+ // @ts-ignore
3987
+ const current = TinyHtml.getAnimateData(el, prop) || style[prop];
3988
+ TinyHtml.setAnimateData(el, prop, current);
3989
+ keyframes[prop] = [current, 0];
3990
+ }
3991
+ },
3992
+ };
3993
+
3994
+ /**
3995
+ * Returns a shallow-cloned copy of the property effect handlers.
3996
+ *
3997
+ * @returns {StyleEffectsProps}
3998
+ */
3999
+ static get styleEffectsProps() {
4000
+ return { ...TinyHtml.#styleEffectsProps };
4001
+ }
4002
+
4003
+ /**
4004
+ * Replace the entire styleEffectsProps map with a new one.
4005
+ *
4006
+ * @param {StyleEffectsProps} value
4007
+ */
4008
+ static set styleEffectsProps(value) {
4009
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
4010
+ throw new TypeError('styleEffectsProps must be an object');
4011
+
4012
+ for (const [k, fn] of Object.entries(value)) {
4013
+ if (typeof fn !== 'function')
4014
+ throw new TypeError(`styleEffectsProps["${k}"] must be a function`);
4015
+ }
4016
+
4017
+ TinyHtml.#styleEffectsProps = {};
4018
+ for (const [k, fn] of Object.entries(value)) TinyHtml.setStyleEffectProp(k, fn);
4019
+ }
4020
+
4021
+ /**
4022
+ * Get a style effect property handler by name.
4023
+ *
4024
+ * @param {string} name - The property handler name.
4025
+ * @returns {StyleEffectsFn | null} The handler function, or undefined if not found.
4026
+ */
4027
+ static getStyleEffectProp(name) {
4028
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
4029
+ return TinyHtml.#styleEffectsProps[name] || null;
4030
+ }
4031
+
4032
+ /**
4033
+ * Register or overwrite a style effect property handler.
4034
+ *
4035
+ * @param {string} name - The property handler name.
4036
+ * @param {StyleEffectsFn} fn - The handler function.
4037
+ * @throws {TypeError} If fn is not a function.
4038
+ */
4039
+ static setStyleEffectProp(name, fn) {
4040
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
4041
+ if (typeof fn !== 'function')
4042
+ throw new TypeError(`styleEffectsProps["${name}"] must be a function`);
4043
+ TinyHtml.#styleEffectsProps[name] = fn;
4044
+ }
4045
+
4046
+ /**
4047
+ * Delete a style effect property handler by name.
4048
+ *
4049
+ * @param {string} name - The property handler name to delete.
4050
+ * @returns {boolean} True if deleted, false otherwise.
4051
+ */
4052
+ static deleteStyleEffectProp(name) {
4053
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
4054
+ return delete TinyHtml.#styleEffectsProps[name];
4055
+ }
4056
+
4057
+ /**
4058
+ * Check if a style effect property handler exists.
4059
+ *
4060
+ * @param {string} name - The property handler name to check.
4061
+ * @returns {boolean} True if the handler exists, false otherwise.
4062
+ */
4063
+ static hasStyleEffectProp(name) {
4064
+ if (typeof name !== 'string') throw new TypeError('The "name" parameter must be a string.');
4065
+ return Object.prototype.hasOwnProperty.call(TinyHtml.#styleEffectsProps, name);
4066
+ }
4067
+
4068
+ // TITLE: Gen Style FX Manager
4069
+
4070
+ /**
4071
+ * Generates effect parameters to create standard animations.
4072
+ *
4073
+ * @param {string} type - The effect type.
4074
+ * @param {boolean} [includeWidth=false] - Whether width (and opacity) should be included.
4075
+ * @returns {Record<string, string>} - A mapping of CSS properties to effect type.
4076
+ */
4077
+ static genStyleFx(type, includeWidth = false) {
4078
+ if (typeof type !== 'string') throw new TypeError('genStyleFx: type must be a string.');
4079
+ if (typeof includeWidth !== 'boolean')
4080
+ throw new TypeError('genStyleFx: includeWidth must be a boolean.');
4081
+
4082
+ /** @type {string} */
4083
+ let which;
4084
+ /** @type {number} */
4085
+ let i = 0;
4086
+ /** @type {Record<string, string>} */
4087
+ const attrs = { height: type };
4088
+
4089
+ const includeWidthNb = includeWidth ? 1 : 0;
4090
+ for (; i < 4; i += 2 - includeWidthNb) {
4091
+ which = TinyHtml.#cssExpand[i];
4092
+ attrs['margin' + which] = type;
4093
+ attrs['padding' + which] = type;
4094
+ }
4095
+
4096
+ if (includeWidth) {
4097
+ attrs.opacity = type;
4098
+ attrs.width = type;
4099
+ }
4100
+
4101
+ return attrs;
4102
+ }
4103
+
4104
+ /**
4105
+ * Applies style-based effects (slide, fade) to one or more elements.
4106
+ * Converts abstract effect definitions (e.g., `{ height: "show" }`)
4107
+ * into concrete Web Animations API keyframes.
4108
+ *
4109
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el - A single element or an array of elements.
4110
+ * @param {string} id - The style effect id.
4111
+ * @param {StyleEffects} props - The style effect definition.
4112
+ * @param {number | KeyframeAnimationOptions | string} [ops] - Timing options.
4113
+ * @returns {StyleFxResult}
4114
+ */
4115
+ static applyStyleFx(el, id, props, ops) {
4116
+ if (typeof id !== 'string') throw new TypeError('applyStyleFx: id must be a string.');
4117
+ if (typeof props !== 'object' || props === null)
4118
+ throw new TypeError('applyStyleFx: props must be a non-null object.');
4119
+ if (
4120
+ ops !== undefined &&
4121
+ !(
4122
+ typeof ops === 'number' ||
4123
+ typeof ops === 'string' ||
4124
+ (typeof ops === 'object' && ops !== null)
4125
+ )
4126
+ )
4127
+ throw new TypeError(
4128
+ 'applyStyleFx: ops must be a number, string, KeyframeAnimationOptions, or undefined.',
4129
+ );
4130
+
4131
+ /** @type {StyleFxResult} */
4132
+ const results = new Map();
4133
+ TinyHtml._preHtmlElems(el, 'applyStyleFx').forEach((first) => {
4134
+ /**
4135
+ * Generate keyframes based on props
4136
+ * @type {AnimationSfxData}
4137
+ */
4138
+ const keyframes = {};
4139
+ for (const [prop, action] of Object.entries(props)) {
4140
+ if (typeof action === 'string' && this.#styleEffectsProps[action]) {
4141
+ const style = getComputedStyle(first);
4142
+ this.#styleEffectsProps[action](first, keyframes, prop, style);
4143
+ } else if (typeof action === 'object') {
4144
+ keyframes[prop] = action;
4145
+ } else {
4146
+ throw new TypeError(
4147
+ `applyStyleFx: invalid action for prop "${prop}". Must be string or array.`,
4148
+ );
4149
+ }
4150
+ }
4151
+
4152
+ const isRepeat =
4153
+ typeof TinyHtml.#styleEffectsRd[id] === 'function'
4154
+ ? TinyHtml.#styleEffectsRd[id](keyframes)
4155
+ : false;
4156
+ if (isRepeat) {
4157
+ results.set(first, null);
4158
+ return;
4159
+ }
4160
+
4161
+ /**
4162
+ * Build Web Animations keyframe objects
4163
+ * @type {Keyframe[]}
4164
+ */
4165
+ const kf = [];
4166
+ const numFrames = keyframes[Object.keys(keyframes)[0]].length; // assume que todos têm o mesmo tamanho
4167
+ for (let i = 0; i < numFrames; i++) {
4168
+ /** @type {Keyframe} */
4169
+ const frame = {};
4170
+ for (const prop in keyframes) {
4171
+ frame[prop] = keyframes[prop][i];
4172
+ }
4173
+ kf.push(frame);
4174
+ }
4175
+
4176
+ results.set(first, TinyHtml.animate(el, kf, ops, id)[0]);
4177
+ });
4178
+ return results;
4179
+ }
4180
+
4181
+ /**
4182
+ * Applies style-based effects (slide, fade) to one or more elements.
4183
+ * Converts abstract effect definitions (e.g., `{ height: "show" }`)
4184
+ * into concrete Web Animations API keyframes.
4185
+ *
4186
+ * @param {string} id - The style effect id.
4187
+ * @param {StyleEffects} props - The style effect definition.
4188
+ * @param {number | KeyframeAnimationOptions} [ops] - Timing options.
4189
+ * @returns {StyleFxResult}
4190
+ */
4191
+ applyStyleFx(id, props, ops) {
4192
+ return TinyHtml.applyStyleFx(this, id, props, ops);
4193
+ }
4194
+
4195
+ // TITLE: Animate Stuff
4196
+
4197
+ /**
4198
+ * Get the current animation entry for a given element.
4199
+ * @param {HTMLElement} el - The target element.
4200
+ * @returns {string|null|undefined} Returns string or null to animation.
4201
+ */
4202
+ static getCurrentAnimationId(el) {
4203
+ if (!(el instanceof HTMLElement)) throw new TypeError('Expected an HTMLElement.');
4204
+ return __elementCurrentAnimation.get(el)?.id ?? undefined;
4205
+ }
4206
+
4207
+ /**
4208
+ * Applies an animation to one or multiple TinyElement instances.
4209
+ *
4210
+ * If `cancelOldAnim` is true, any currently running animation on the same element
4211
+ * will be cancelled before the new one starts.
4212
+ *
4213
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el - A single TinyElement or an array of TinyElements to animate.
4214
+ * @param {Keyframe[] | PropertyIndexedKeyframes | null} keyframes - Keyframes that define the animation.
4215
+ * @param {number | KeyframeAnimationOptions | string} [ops] - Timing or configuration options for the animation.
4216
+ * @param {string|null} [id] - The style effect id.
4217
+ * @param {boolean} [cancelOldAnim=TinyHtml.cancelOldStyleFx] - Whether to cancel previous animations on the same element.
4218
+ * @returns {Animation[]}
4219
+ */
4220
+ static animate(
4221
+ el,
4222
+ keyframes,
4223
+ ops = TinyHtml.#styleFxSpeeds._default,
4224
+ id = null,
4225
+ cancelOldAnim = TinyHtml.#cancelOldStyleFx,
4226
+ ) {
4227
+ /** @type {number | KeyframeAnimationOptions} */
4228
+ const fxSpeed =
4229
+ typeof ops === 'string'
4230
+ ? (TinyHtml.#styleFxSpeeds[ops] ?? undefined)
4231
+ : typeof ops !== 'number'
4232
+ ? { ...ops }
4233
+ : ops;
4234
+
4235
+ if (typeof fxSpeed === 'object' && typeof fxSpeed.fill === 'undefined') {
4236
+ fxSpeed.fill = 'forwards';
4237
+ }
4238
+
4239
+ /** @type {Animation[]} */
4240
+ const results = [];
4241
+ TinyHtml._preHtmlElems(el, 'animate').forEach((elem) => {
4242
+ if (cancelOldAnim) {
4243
+ const prevAnim = __elementCurrentAnimation.get(elem);
4244
+ if (prevAnim) prevAnim.animation.cancel();
4245
+ }
4246
+
4247
+ const anim = elem.animate(keyframes, fxSpeed);
4248
+ results.push(anim);
4249
+ __elementCurrentAnimation.set(elem, { animation: anim, id: id ?? '' });
4250
+ anim.addEventListener('finish', () => {
4251
+ if (__elementCurrentAnimation.get(elem)?.animation === anim)
4252
+ __elementCurrentAnimation.delete(elem);
4253
+ });
4254
+ });
4255
+
4256
+ return results;
4257
+ }
4258
+
4259
+ /**
4260
+ * Applies an animation to one or multiple TinyElement instances.
4261
+ *
4262
+ * If `cancelOldAnim` is true, any currently running animation on the same element
4263
+ * will be cancelled before the new one starts.
4264
+ *
4265
+ * @param {Keyframe[] | PropertyIndexedKeyframes | null} keyframes - Keyframes that define the animation.
4266
+ * @param {number | KeyframeAnimationOptions | string} [ops] - Timing or configuration options for the animation.
4267
+ * @param {string|null} [id] - The style effect id.
4268
+ * @param {boolean} [cancelOldAnim=TinyHtml.cancelOldStyleFx] - Whether to cancel previous animations on the same element.
4269
+ * @returns {Animation[]}
4270
+ */
4271
+ animate(keyframes, ops, id, cancelOldAnim) {
4272
+ return TinyHtml.animate(this, keyframes, ops, id, cancelOldAnim);
4273
+ }
4274
+
4275
+ /**
4276
+ * Stops the current animation(s) on one or multiple TinyElement instances.
4277
+ *
4278
+ * If an animation is running on the element(s), it will be cancelled immediately.
4279
+ *
4280
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el - A single TinyElement or an array of TinyElements.
4281
+ * @returns {boolean[]} The same element(s) provided as input, for chaining.
4282
+ */
4283
+ static stop(el) {
4284
+ /** @type {boolean[]} */
4285
+ const results = [];
4286
+ TinyHtml._preHtmlElems(el, 'stop').forEach((elem) => {
4287
+ const anim = __elementCurrentAnimation.get(elem);
4288
+ if (anim) {
4289
+ anim.animation.cancel();
4290
+ __elementCurrentAnimation.delete(elem);
4291
+ results.push(true);
4292
+ } else results.push(false);
4293
+ });
4294
+ return results;
4295
+ }
4296
+
4297
+ /**
4298
+ * Stops the current animation(s) on this TinyElement instance.
4299
+ *
4300
+ * @returns {boolean[]}
4301
+ */
4302
+ stop() {
4303
+ return TinyHtml.stop(this);
4304
+ }
4305
+
4306
+ // TITLE: Animate FXs
4307
+
4308
+ /**
4309
+ * Show animation (slideDown).
4310
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el
4311
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4312
+ * @returns {StyleFxResult}
4313
+ */
4314
+ static slideDown(el, ops) {
4315
+ return TinyHtml.applyStyleFx(
4316
+ el,
4317
+ 'slideDown',
4318
+ this.#styleEffects['slideDown'],
4319
+ ops ?? { duration: 500, easing: 'ease-out' },
4320
+ );
4321
+ }
4322
+
4323
+ /**
4324
+ * Show animation (slideDown).
4325
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4326
+ * @returns {StyleFxResult}
4327
+ */
4328
+ slideDown(ops) {
4329
+ return TinyHtml.slideDown(this, ops);
4330
+ }
4331
+
4332
+ /**
4333
+ * Hide animation (slideUp).
4334
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el
4335
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4336
+ * @returns {StyleFxResult}
4337
+ */
4338
+ static slideUp(el, ops) {
4339
+ return TinyHtml.applyStyleFx(
4340
+ el,
4341
+ 'slideUp',
4342
+ this.#styleEffects['slideUp'],
4343
+ ops ?? { duration: 500, easing: 'ease-out' },
4344
+ );
4345
+ }
4346
+
4347
+ /**
4348
+ * Hide animation (slideUp).
4349
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4350
+ * @returns {StyleFxResult}
4351
+ */
4352
+ slideUp(ops) {
4353
+ return TinyHtml.slideUp(this, ops);
4354
+ }
4355
+
4356
+ /**
4357
+ * Toggle slide animation.
4358
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el
4359
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4360
+ * @returns {StyleFxResult}
4361
+ */
4362
+ static slideToggle(el, ops) {
4363
+ const first = TinyHtml._preHtmlElem(el, 'slideToggle');
4364
+ const style = getComputedStyle(first);
4365
+ const id = TinyHtml.getCurrentAnimationId(first);
4366
+ const isHidden = style.display === 'none' || first.offsetHeight === 0;
4367
+ return (typeof id === 'string' ? id === 'slideUp' : isHidden)
4368
+ ? TinyHtml.slideDown(el, ops)
4369
+ : TinyHtml.slideUp(el, ops);
4370
+ }
4371
+
4372
+ /**
4373
+ * Toggle slide animation.
4374
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4375
+ * @returns {StyleFxResult}
4376
+ */
4377
+ slideToggle(ops) {
4378
+ return TinyHtml.slideToggle(this, ops);
4379
+ }
4380
+
4381
+ /**
4382
+ * Fade in animation.
4383
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el
4384
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4385
+ * @returns {StyleFxResult}
4386
+ */
4387
+ static fadeIn(el, ops) {
4388
+ return TinyHtml.applyStyleFx(el, 'fadeIn', this.#styleEffects['fadeIn'], ops);
4389
+ }
4390
+
4391
+ /**
4392
+ * Fade in animation.
4393
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4394
+ * @returns {StyleFxResult}
4395
+ */
4396
+ fadeIn(ops) {
4397
+ return TinyHtml.fadeIn(this, ops);
4398
+ }
4399
+
4400
+ /**
4401
+ * Fade out animation.
4402
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el
4403
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4404
+ * @returns {StyleFxResult}
4405
+ */
4406
+ static fadeOut(el, ops) {
4407
+ return TinyHtml.applyStyleFx(el, 'fadeOut', this.#styleEffects['fadeOut'], ops);
4408
+ }
4409
+
4410
+ /**
4411
+ * Fade out animation.
4412
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4413
+ * @returns {StyleFxResult}
4414
+ */
4415
+ fadeOut(ops) {
4416
+ return TinyHtml.fadeOut(this, ops);
4417
+ }
4418
+
4419
+ /**
4420
+ * Fade toggle animation.
4421
+ * @param {TinyHtmlElement|TinyHtmlElement[]} el
4422
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4423
+ * @returns {StyleFxResult}
4424
+ */
4425
+ static fadeToggle(el, ops) {
4426
+ const first = TinyHtml._preHtmlElem(el, 'fadeToggle');
4427
+ const style = getComputedStyle(first);
4428
+ const id = TinyHtml.getCurrentAnimationId(first);
4429
+ const isHidden = style.opacity === '0' || first.offsetParent === null;
4430
+ return (typeof id === 'string' ? id === 'fadeOut' : isHidden)
4431
+ ? TinyHtml.fadeIn(el, ops)
4432
+ : TinyHtml.fadeOut(el, ops);
4433
+ }
4434
+
4435
+ /**
4436
+ * Fade toggle animation.
4437
+ * @param {number | KeyframeAnimationOptions | string} [ops]
4438
+ * @returns {StyleFxResult}
4439
+ */
4440
+ fadeToggle(ops) {
4441
+ return TinyHtml.fadeToggle(this, ops);
4442
+ }
4443
+
4444
+ ///////////////////////////////////////////////////////////////
4445
+
4446
+ // TITLE: DOM Positions
4447
+
4448
+ /**
4449
+ * Gets the offset of the element relative to the document.
4450
+ * @param {TinyElement} el - Target element.
4451
+ * @returns {{top: number, left: number}}
4452
+ */
4453
+ static offset(el) {
4454
+ const elem = TinyHtml._preElem(el, 'offset');
4455
+ const rect = elem.getBoundingClientRect();
4456
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
4457
+ const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
4458
+
4459
+ return {
4460
+ top: rect.top + scrollTop,
4461
+ left: rect.left + scrollLeft,
4462
+ };
4463
+ }
4464
+
4465
+ /**
4466
+ * Gets the offset of the element relative to the document.
4467
+ * @returns {{top: number, left: number}}
4468
+ */
4469
+ offset() {
4470
+ return TinyHtml.offset(this);
4471
+ }
4472
+
4473
+ /**
4474
+ * Gets the position of the element relative to its offset parent.
4475
+ * @param {TinyHtmlElement} el - Target element.
4476
+ * @returns {{top: number, left: number}}
4477
+ */
4478
+ static position(el) {
4479
+ const elem = TinyHtml._preHtmlElem(el, 'position');
4480
+
4481
+ let offsetParent;
4482
+ let offset;
4483
+ let parentOffset = { top: 0, left: 0 };
4484
+
4485
+ const computedStyle = window.getComputedStyle(elem);
4486
+
4487
+ if (computedStyle.position === 'fixed') {
4488
+ offset = elem.getBoundingClientRect();
4489
+ } else {
4490
+ offset = TinyHtml.offset(elem);
4491
+
4492
+ offsetParent = elem.offsetParent || document.documentElement;
4493
+ const { position } = window.getComputedStyle(offsetParent);
4494
+
4495
+ while (
4496
+ offsetParent instanceof HTMLElement &&
4497
+ (offsetParent === document.body || offsetParent === document.documentElement) &&
4498
+ position === 'static'
4499
+ ) {
4500
+ offsetParent = offsetParent.parentNode;
4501
+ }
4502
+
4503
+ if (
4504
+ offsetParent instanceof HTMLElement &&
4505
+ offsetParent !== elem &&
4506
+ offsetParent.nodeType === 1
4507
+ ) {
4508
+ const { borderTopWidth, borderLeftWidth } = TinyHtml.cssFloats(offsetParent, [
4509
+ 'borderTopWidth',
4510
+ 'borderLeftWidth',
4511
+ ]);
4512
+ parentOffset = TinyHtml.offset(offsetParent);
4513
+ parentOffset.top += borderTopWidth;
4514
+ parentOffset.left += borderLeftWidth;
4515
+ }
4516
+ }
4517
+
4518
+ return {
4519
+ top: offset.top - parentOffset.top - TinyHtml.cssFloat(elem, 'marginTop'),
4520
+ left: offset.left - parentOffset.left - TinyHtml.cssFloat(elem, 'marginLeft'),
4521
+ };
4522
+ }
4523
+
4524
+ /**
4525
+ * Gets the position of the element relative to its offset parent.
4526
+ * @returns {{top: number, left: number}}
4527
+ */
4528
+ position() {
4529
+ return TinyHtml.position(this);
4530
+ }
4531
+
4532
+ /**
4533
+ * Gets the closest positioned ancestor element.
4534
+ * @param {TinyHtmlElement} el - Target element.
4535
+ * @returns {HTMLElement} - Offset parent element.
4536
+ */
4537
+ static offsetParent(el) {
4538
+ const elem = TinyHtml._preHtmlElem(el, 'offsetParent');
4539
+ let offsetParent = elem.offsetParent;
4540
+
4541
+ while (
4542
+ offsetParent instanceof HTMLElement &&
4543
+ window.getComputedStyle(offsetParent).position === 'static'
4544
+ ) {
4545
+ offsetParent = offsetParent.offsetParent;
4546
+ }
4547
+
4548
+ // Fallback to document.documentElement
4549
+ return offsetParent instanceof HTMLElement ? offsetParent : document.documentElement;
4550
+ }
4551
+
4552
+ /**
4553
+ * Gets the closest positioned ancestor element.
4554
+ * @returns {HTMLElement} - Offset parent element.
4555
+ */
4556
+ offsetParent() {
4557
+ return TinyHtml.offsetParent(this);
4558
+ }
4559
+
4560
+ /////////////////////////////////////////////////
4561
+
4562
+ // TITLE: Scroll Stuff
4563
+
4564
+ /**
4565
+ * Gets the vertical scroll position.
4566
+ * @param {TinyElementAndWindow} el - Element or window.
4567
+ * @returns {number}
4568
+ */
4569
+ static scrollTop(el) {
4570
+ const elem = TinyHtml._preElemAndWindow(el, 'scrollTop');
4571
+ if (TinyHtml.isWindow(elem)) return elem.pageYOffset;
4572
+ // @ts-ignore
4573
+ if (elem.nodeType === 9) return elem.defaultView.pageYOffset;
4574
+ return elem.scrollTop;
4575
+ }
4576
+
4577
+ /**
4578
+ * Gets the vertical scroll position.
4579
+ * @returns {number}
4580
+ */
4581
+ scrollTop() {
4582
+ return TinyHtml.scrollTop(this);
4583
+ }
4584
+
4585
+ /**
4586
+ * Gets the horizontal scroll position.
4587
+ * @param {TinyElementAndWindow} el - Element or window.
4588
+ * @returns {number}
4589
+ */
4590
+ static scrollLeft(el) {
4591
+ const elem = TinyHtml._preElemAndWindow(el, 'scrollLeft');
4592
+ if (TinyHtml.isWindow(elem)) return elem.pageXOffset;
4593
+ // @ts-ignore
4594
+ if (elem.nodeType === 9) return elem.defaultView.pageXOffset;
4595
+ return elem.scrollLeft;
4596
+ }
4597
+
4598
+ /**
4599
+ * Gets the horizontal scroll position.
4600
+ * @returns {number}
4601
+ */
4602
+ scrollLeft() {
4603
+ return TinyHtml.scrollLeft(this);
4604
+ }
4605
+
4606
+ /**
4607
+ * Collection of easing functions used for scroll and animation calculations.
4608
+ * Each function receives a normalized time value (`t` from 0 to 1) and returns the eased progress.
4609
+ *
4610
+ * @type {Record<string, (t: number) => number>}
4611
+ */
4612
+ static easings = {
4613
+ linear: (t) => t,
4614
+ easeInQuad: (t) => t * t,
3404
4615
  easeOutQuad: (t) => t * (2 - t),
3405
4616
  easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
3406
4617
  easeInCubic: (t) => t * t * t,
@@ -3566,6 +4777,10 @@ class TinyHtml {
3566
4777
  return TinyHtml.setScrollLeft(this, value);
3567
4778
  }
3568
4779
 
4780
+ ///////////////////////////////////////////
4781
+
4782
+ // TITLE: Border Stuff
4783
+
3569
4784
  /**
3570
4785
  * Returns the total border width and individual sides from `border{Side}Width` CSS properties.
3571
4786
  *
@@ -3689,6 +4904,8 @@ class TinyHtml {
3689
4904
 
3690
4905
  /////////////////////////////////////////////
3691
4906
 
4907
+ // TITLE: Class Stuff
4908
+
3692
4909
  /**
3693
4910
  * Adds one or more CSS class names to the element.
3694
4911
  * @template {TinyElement|TinyElement[]} T
@@ -3877,6 +5094,8 @@ class TinyHtml {
3877
5094
 
3878
5095
  /////////////////////////////////////////
3879
5096
 
5097
+ // TITLE: Tag Stuff
5098
+
3880
5099
  /**
3881
5100
  * Returns the tag name of the element.
3882
5101
  * @param {TinyElement} el - Target element.
@@ -3913,6 +5132,10 @@ class TinyHtml {
3913
5132
  return TinyHtml.id(this);
3914
5133
  }
3915
5134
 
5135
+ //////////////////////////////////////////////////////////////
5136
+
5137
+ // TITLE: DOM Content Stuff
5138
+
3916
5139
  /**
3917
5140
  * Returns the BigInt content of the element.
3918
5141
  * @param {TinyElement} el - Target element.
@@ -4220,6 +5443,10 @@ class TinyHtml {
4220
5443
  return TinyHtml.setText(this, value);
4221
5444
  }
4222
5445
 
5446
+ //////////////////////////////////////////////////////
5447
+
5448
+ // TITLE: DOM Content Stuff (Part 2)
5449
+
4223
5450
  /**
4224
5451
  * Remove all child nodes from each element.
4225
5452
  * @template {TinyElement|TinyElement[]} T
@@ -4281,6 +5508,10 @@ class TinyHtml {
4281
5508
  return TinyHtml.setHtml(this, value);
4282
5509
  }
4283
5510
 
5511
+ ///////////////////////////////////////////////
5512
+
5513
+ // TITLE: Value Stuff
5514
+
4284
5515
  /** @readonly */
4285
5516
  static _valHooks = {
4286
5517
  option: {
@@ -4721,6 +5952,8 @@ class TinyHtml {
4721
5952
 
4722
5953
  ////////////////////////////////////////////
4723
5954
 
5955
+ // TITLE: Listen for Paste
5956
+
4724
5957
  /**
4725
5958
  * Registers a listener for the "paste" event to extract files and text from the clipboard (e.g., when the user presses Ctrl+V).
4726
5959
  *
@@ -4777,6 +6010,10 @@ class TinyHtml {
4777
6010
  return TinyHtml.listenForPaste(this, { onFilePaste, onTextPaste });
4778
6011
  }
4779
6012
 
6013
+ /////////////////////////////////////////////////////
6014
+
6015
+ // TITLE: Events Stuff
6016
+
4780
6017
  /**
4781
6018
  * Checks if the element has a listener for a specific event.
4782
6019
  *
@@ -4829,6 +6066,42 @@ class TinyHtml {
4829
6066
  return TinyHtml.hasExactEventListener(this, event, handler);
4830
6067
  }
4831
6068
 
6069
+ /**
6070
+ * Hover event shortcut.
6071
+ *
6072
+ * Adds `mouseenter` and `mouseleave` event listeners to the target.
6073
+ *
6074
+ * @template {TinyEventTarget|TinyEventTarget[]} T
6075
+ * @param {T} el - The target element(s) to attach the hover handlers.
6076
+ * @param {HoverEventCallback} fnOver - Callback executed when the mouse enters the target.
6077
+ * @param {HoverEventCallback|null} [fnOut] - Optional callback executed when the mouse leaves
6078
+ * the target. If not provided, `fnOver` will be used for both events.
6079
+ * @param {EventRegistryOptions} [options] - Optional event listener options.
6080
+ * @returns {T}
6081
+ */
6082
+ static hover(el, fnOver, fnOut, options) {
6083
+ // @ts-ignore
6084
+ TinyHtml.on(el, 'mouseenter', fnOver, options);
6085
+ // @ts-ignore
6086
+ TinyHtml.on(el, 'mouseleave', fnOut || fnOver, options);
6087
+ return el;
6088
+ }
6089
+
6090
+ /**
6091
+ * Hover event shortcut.
6092
+ *
6093
+ * Adds `mouseenter` and `mouseleave` event listeners to the target.
6094
+ *
6095
+ * @param {HoverEventCallback} fnOver - Callback executed when the mouse enters the target.
6096
+ * @param {HoverEventCallback|null} [fnOut] - Optional callback executed when the mouse leaves
6097
+ * the target. If not provided, `fnOver` will be used for both events.
6098
+ * @param {EventRegistryOptions} [options] - Optional event listener options.
6099
+ * @returns {this}
6100
+ */
6101
+ hover(fnOver, fnOut, options) {
6102
+ return TinyHtml.hover(this, fnOver, fnOut, options);
6103
+ }
6104
+
4832
6105
  /**
4833
6106
  * Registers an event listener on the specified element.
4834
6107
  *
@@ -5088,6 +6361,8 @@ class TinyHtml {
5088
6361
 
5089
6362
  ///////////////////////////////////////////////////////////////
5090
6363
 
6364
+ // TITLE: ProFix Manager
6365
+
5091
6366
  /**
5092
6367
  * Internal property name normalization map (similar to jQuery's `propFix`).
5093
6368
  * Maps attribute-like names to their JavaScript DOM property equivalents.
@@ -5182,28 +6457,46 @@ class TinyHtml {
5182
6457
  }
5183
6458
 
5184
6459
  /**
5185
- * Set an attribute on an element.
6460
+ * Set one or multiple attributes on an element.
5186
6461
  * @template {TinyElement|TinyElement[]} T
5187
- * @param {T} el
5188
- * @param {string} name
5189
- * @param {string|null} [value=null]
6462
+ * @param {T} el - Target element(s).
6463
+ * @param {string|Record<string, string|null>} name - Attribute name or an object of attributes.
6464
+ * @param {string|null} [value=null] - Attribute value (ignored if "name" is an object).
5190
6465
  * @returns {T}
5191
6466
  */
5192
6467
  static setAttr(el, name, value = null) {
6468
+ const elems = TinyHtml._preElems(el, 'setAttr');
6469
+
6470
+ // Multiple attributes at once
6471
+ if (typeof name === 'object' && name !== null) {
6472
+ Object.entries(name).forEach(([attr, val]) => {
6473
+ if (val !== null && typeof val !== 'string')
6474
+ throw new TypeError(`The value for "${attr}" must be a string or null.`);
6475
+ elems.forEach((elem) => {
6476
+ if (val === null) elem.removeAttribute(TinyHtml.getAttrName(attr));
6477
+ else elem.setAttribute(TinyHtml.getAttrName(attr), val);
6478
+ });
6479
+ });
6480
+ return el;
6481
+ }
6482
+
6483
+ // Single attribute
5193
6484
  if (typeof name !== 'string') throw new TypeError('The "name" must be a string.');
5194
6485
  if (value !== null && typeof value !== 'string')
5195
6486
  throw new TypeError('The "value" must be a string.');
5196
- TinyHtml._preElems(el, 'setAttr').forEach((elem) => {
6487
+
6488
+ elems.forEach((elem) => {
5197
6489
  if (value === null) elem.removeAttribute(TinyHtml.getAttrName(name));
5198
6490
  else elem.setAttribute(TinyHtml.getAttrName(name), value);
5199
6491
  });
6492
+
5200
6493
  return el;
5201
6494
  }
5202
6495
 
5203
6496
  /**
5204
- * Set an attribute on an element.
5205
- * @param {string} name
5206
- * @param {string|null} [value=null]
6497
+ * Set one or multiple attributes on an element.
6498
+ * @param {string|Record<string, string|null>} name - Attribute name or an object of attributes.
6499
+ * @param {string|null} [value=null] - Attribute value (ignored if "name" is an object).
5207
6500
  * @returns {this}
5208
6501
  */
5209
6502
  setAttr(name, value) {
@@ -5278,81 +6571,92 @@ class TinyHtml {
5278
6571
  }
5279
6572
 
5280
6573
  /**
5281
- * Set a property on an element.
6574
+ * Set one or multiple properties on an element.
5282
6575
  * @template {TinyElement|TinyElement[]} T
5283
- * @param {T} el
5284
- * @param {string} name
6576
+ * @param {T} el - Target element(s).
6577
+ * @param {...string} names - Property name(s).
5285
6578
  * @returns {T}
5286
6579
  */
5287
- static addProp(el, name) {
5288
- if (typeof name !== 'string') throw new TypeError('The "name" must be a string.');
6580
+ static addProp(el, ...names) {
5289
6581
  TinyHtml._preElems(el, 'addProp').forEach((elem) => {
5290
- // @ts-ignore
5291
- elem[TinyHtml.getPropName(name)] = true;
6582
+ names.forEach((name) => {
6583
+ if (typeof name !== 'string') throw new TypeError('Each "name" must be a string.');
6584
+ // @ts-ignore
6585
+ elem[TinyHtml.getPropName(name)] = true;
6586
+ });
5292
6587
  });
5293
6588
  return el;
5294
6589
  }
5295
6590
 
5296
6591
  /**
5297
6592
  * Set a property on an element.
5298
- * @param {string} name
6593
+ * @param {...string} names - Property name(s).
5299
6594
  * @returns {this}
5300
6595
  */
5301
- addProp(name) {
5302
- return TinyHtml.addProp(this, name);
6596
+ addProp(...names) {
6597
+ return TinyHtml.addProp(this, ...names);
5303
6598
  }
5304
6599
 
5305
6600
  /**
5306
- * Remove a property from an element.
6601
+ * Remove one or multiple properties from an element.
5307
6602
  * @template {TinyElement|TinyElement[]} T
5308
- * @param {T} el
5309
- * @param {string} name
6603
+ * @param {T} el - Target element(s).
6604
+ * @param {...string} names - Property name(s).
5310
6605
  * @returns {T}
5311
6606
  */
5312
- static removeProp(el, name) {
5313
- if (typeof name !== 'string') throw new TypeError('The "name" must be a string.');
6607
+ static removeProp(el, ...names) {
5314
6608
  TinyHtml._preElems(el, 'removeProp').forEach((elem) => {
5315
- // @ts-ignore
5316
- elem[TinyHtml.getPropName(name)] = false;
6609
+ names.forEach((name) => {
6610
+ if (typeof name !== 'string') throw new TypeError('Each "name" must be a string.');
6611
+ // @ts-ignore
6612
+ elem[TinyHtml.getPropName(name)] = false;
6613
+ });
5317
6614
  });
5318
6615
  return el;
5319
6616
  }
5320
6617
 
5321
6618
  /**
5322
6619
  * Remove a property from an element.
5323
- * @param {string} name
6620
+ * @param {...string} names - Property name(s).
5324
6621
  * @returns {this}
5325
6622
  */
5326
- removeProp(name) {
5327
- return TinyHtml.removeProp(this, name);
6623
+ removeProp(...names) {
6624
+ return TinyHtml.removeProp(this, ...names);
5328
6625
  }
5329
6626
 
5330
6627
  /**
5331
- * Toggle a boolean property.
6628
+ * Toggle one or multiple boolean properties.
5332
6629
  * @template {TinyElement|TinyElement[]} T
5333
- * @param {T} el
5334
- * @param {string} name
5335
- * @param {boolean} [force]
6630
+ * @param {T} el - Target element(s).
6631
+ * @param {string|string[]} name - Property name or a list of property names.
6632
+ * @param {boolean} [force] - Force true/false instead of toggling.
5336
6633
  * @returns {T}
5337
6634
  */
5338
6635
  static toggleProp(el, name, force) {
5339
- if (typeof name !== 'string') throw new TypeError('The "name" must be a string.');
6636
+ const elems = TinyHtml._preElems(el, 'toggleProp');
5340
6637
  if (typeof force !== 'undefined' && typeof force !== 'boolean')
5341
6638
  throw new TypeError('The "force" must be a boolean.');
5342
- TinyHtml._preElems(el, 'toggleProp').forEach((elem) => {
5343
- // @ts-ignore
5344
- const shouldEnable = force === undefined ? !elem[TinyHtml.getPropName(name)] : force;
5345
- // @ts-ignore
5346
- if (shouldEnable) TinyHtml.addProp(elem, name);
5347
- else TinyHtml.removeProp(elem, name);
6639
+
6640
+ // Normalize list of properties
6641
+ const props = Array.isArray(name) ? name : [name];
6642
+
6643
+ props.forEach((prop) => {
6644
+ if (typeof prop !== 'string') throw new TypeError('Each property name must be a string.');
6645
+ elems.forEach((elem) => {
6646
+ // @ts-ignore
6647
+ const shouldEnable = force === undefined ? !elem[TinyHtml.getPropName(prop)] : force;
6648
+ if (shouldEnable) TinyHtml.addProp(elem, prop);
6649
+ else TinyHtml.removeProp(elem, prop);
6650
+ });
5348
6651
  });
6652
+
5349
6653
  return el;
5350
6654
  }
5351
6655
 
5352
6656
  /**
5353
- * Toggle a boolean property.
5354
- * @param {string} name
5355
- * @param {boolean} [force]
6657
+ * Toggle one or multiple boolean properties.
6658
+ * @param {string|string[]} name - Property name or a list of property names.
6659
+ * @param {boolean} [force] - Force true/false instead of toggling.
5356
6660
  * @returns {this}
5357
6661
  */
5358
6662
  toggleProp(name, force) {
@@ -5361,6 +6665,8 @@ class TinyHtml {
5361
6665
 
5362
6666
  /////////////////////////////////////////////////////
5363
6667
 
6668
+ // TITLE: Remove Element
6669
+
5364
6670
  /**
5365
6671
  * Removes an element from the DOM.
5366
6672
  * @template {TinyElement|TinyElement[]} T
@@ -5380,6 +6686,10 @@ class TinyHtml {
5380
6686
  return TinyHtml.remove(this);
5381
6687
  }
5382
6688
 
6689
+ ///////////////////////////////////////////////////
6690
+
6691
+ // TITLE: Index Manager
6692
+
5383
6693
  /**
5384
6694
  * Returns the index of the first element within its parent or relative to a selector/element.
5385
6695
  *
@@ -5416,6 +6726,8 @@ class TinyHtml {
5416
6726
 
5417
6727
  ////////////////////////////////////////////////////////////////////
5418
6728
 
6729
+ // TITLE: Collision Stuff
6730
+
5419
6731
  /**
5420
6732
  * Creates a new DOMRect object by copying the base rect and applying optional additional dimensions.
5421
6733
  *
@@ -5710,6 +7022,8 @@ class TinyHtml {
5710
7022
 
5711
7023
  //////////////////////////////////////////////////////////////////////////////
5712
7024
 
7025
+ // TITLE: Viewport Stuff
7026
+
5713
7027
  /**
5714
7028
  * Checks if the given element is at least partially visible in the viewport.
5715
7029
  *
@@ -5860,6 +7174,8 @@ class TinyHtml {
5860
7174
  return TinyHtml.isFullyInContainer(this, cont);
5861
7175
  }
5862
7176
 
7177
+ // TITLE: Has Scroll
7178
+
5863
7179
  /**
5864
7180
  * Checks if an element has scrollable content.
5865
7181
  *