tiny-essentials 1.18.0 → 1.19.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 (93) hide show
  1. package/README.md +17 -3
  2. package/dist/legacy/get/countObj.cjs +2 -2
  3. package/dist/legacy/get/countObj.d.mts +1 -1
  4. package/dist/legacy/get/countObj.mjs +1 -1
  5. package/dist/legacy/index.cjs +2 -1
  6. package/dist/node_modules/firebase-functions/lib/common/trace.cjs +0 -1
  7. package/dist/node_modules/firebase-functions/lib/logger/index.cjs +1 -0
  8. package/dist/v1/ColorSafeStringify.min.js +1 -1
  9. package/dist/v1/TinyAfterScrollWatcher.min.js +1 -1
  10. package/dist/v1/TinyBasicsEs.js +402 -390
  11. package/dist/v1/TinyBasicsEs.min.js +1 -1
  12. package/dist/v1/TinyClipboard.min.js +1 -1
  13. package/dist/v1/TinyColorConverter.js +617 -0
  14. package/dist/v1/TinyColorConverter.min.js +1 -0
  15. package/dist/v1/TinyDomReadyManager.min.js +1 -1
  16. package/dist/v1/TinyDragger.js +6 -2433
  17. package/dist/v1/TinyDragger.min.js +1 -2
  18. package/dist/v1/TinyEssentials.js +3546 -1388
  19. package/dist/v1/TinyEssentials.min.js +1 -1
  20. package/dist/v1/TinyEvents.js +402 -0
  21. package/dist/v1/TinyEvents.min.js +1 -0
  22. package/dist/v1/TinyHtml.min.js +1 -1
  23. package/dist/v1/TinyLocalStorage.js +1292 -0
  24. package/dist/v1/TinyLocalStorage.min.js +1 -0
  25. package/dist/v1/TinyNotifications.min.js +1 -1
  26. package/dist/v1/TinyNotifyCenter.min.js +1 -1
  27. package/dist/v1/TinyPromiseQueue.min.js +1 -1
  28. package/dist/v1/TinyRateLimiter.js +2 -1
  29. package/dist/v1/TinyRateLimiter.min.js +1 -1
  30. package/dist/v1/TinySmartScroller.js +570 -52
  31. package/dist/v1/TinySmartScroller.min.js +1 -1
  32. package/dist/v1/TinyTextRangeEditor.min.js +1 -1
  33. package/dist/v1/TinyTimeout.js +233 -0
  34. package/dist/v1/TinyTimeout.min.js +1 -0
  35. package/dist/v1/TinyToastNotify.min.js +1 -1
  36. package/dist/v1/TinyUploadClicker.js +2087 -1129
  37. package/dist/v1/TinyUploadClicker.min.js +1 -1
  38. package/dist/v1/UltraRandomMsgGen.min.js +1 -1
  39. package/dist/v1/basics/html.cjs +16 -9
  40. package/dist/v1/basics/html.d.mts +12 -4
  41. package/dist/v1/basics/html.mjs +14 -7
  42. package/dist/v1/basics/index.cjs +3 -2
  43. package/dist/v1/basics/index.d.mts +2 -2
  44. package/dist/v1/basics/index.mjs +2 -1
  45. package/dist/v1/basics/objChecker.cjs +46 -0
  46. package/dist/v1/basics/objChecker.d.mts +29 -0
  47. package/dist/v1/basics/objChecker.mjs +45 -0
  48. package/dist/v1/basics/objFilter.cjs +4 -45
  49. package/dist/v1/basics/objFilter.d.mts +3 -28
  50. package/dist/v1/basics/objFilter.mjs +2 -45
  51. package/dist/v1/build/TinyColorConverter.cjs +7 -0
  52. package/dist/v1/build/TinyColorConverter.d.mts +3 -0
  53. package/dist/v1/build/TinyColorConverter.mjs +2 -0
  54. package/dist/v1/build/TinyEvents.cjs +7 -0
  55. package/dist/v1/build/TinyEvents.d.mts +3 -0
  56. package/dist/v1/build/TinyEvents.mjs +2 -0
  57. package/dist/v1/build/TinyLocalStorage.cjs +7 -0
  58. package/dist/v1/build/TinyLocalStorage.d.mts +3 -0
  59. package/dist/v1/build/TinyLocalStorage.mjs +2 -0
  60. package/dist/v1/build/TinyTimeout.cjs +7 -0
  61. package/dist/v1/build/TinyTimeout.d.mts +3 -0
  62. package/dist/v1/build/TinyTimeout.mjs +2 -0
  63. package/dist/v1/index.cjs +11 -2
  64. package/dist/v1/index.d.mts +7 -3
  65. package/dist/v1/index.mjs +7 -2
  66. package/dist/v1/libs/TinyColorConverter.cjs +578 -0
  67. package/dist/v1/libs/TinyColorConverter.d.mts +396 -0
  68. package/dist/v1/libs/TinyColorConverter.mjs +520 -0
  69. package/dist/v1/libs/TinyDragger.cjs +3 -3
  70. package/dist/v1/libs/TinyDragger.mjs +1 -1
  71. package/dist/v1/libs/TinyEvents.cjs +363 -0
  72. package/dist/v1/libs/TinyEvents.d.mts +160 -0
  73. package/dist/v1/libs/TinyEvents.mjs +328 -0
  74. package/dist/v1/libs/TinyLocalStorage.cjs +847 -0
  75. package/dist/v1/libs/TinyLocalStorage.d.mts +407 -0
  76. package/dist/v1/libs/TinyLocalStorage.mjs +740 -0
  77. package/dist/v1/libs/TinySmartScroller.cjs +207 -52
  78. package/dist/v1/libs/TinySmartScroller.d.mts +164 -16
  79. package/dist/v1/libs/TinySmartScroller.mjs +181 -52
  80. package/dist/v1/libs/TinyTimeout.cjs +194 -0
  81. package/dist/v1/libs/TinyTimeout.d.mts +89 -0
  82. package/dist/v1/libs/TinyTimeout.mjs +179 -0
  83. package/dist/v1/libs/TinyUploadClicker.cjs +6 -4
  84. package/docs/v1/README.md +5 -0
  85. package/docs/v1/basics/objChecker.md +47 -0
  86. package/docs/v1/basics/objFilter.md +0 -40
  87. package/docs/v1/libs/TinyColorConverter.md +220 -0
  88. package/docs/v1/libs/TinyEvents.md +199 -0
  89. package/docs/v1/libs/TinyLocalStorage.md +350 -0
  90. package/docs/v1/libs/TinyRateLimiter.md +0 -3
  91. package/docs/v1/libs/TinyTimeout.md +190 -0
  92. package/package.json +28 -5
  93. package/dist/v1/TinyDragger.min.js.LICENSE.txt +0 -8
@@ -1,13 +1,6 @@
1
1
  /******/ (() => { // webpackBootstrap
2
2
  /******/ var __webpack_modules__ = ({
3
3
 
4
- /***/ 133:
5
- /***/ (() => {
6
-
7
- /* (ignored) */
8
-
9
- /***/ }),
10
-
11
4
  /***/ 251:
12
5
  /***/ ((__unused_webpack_module, exports) => {
13
6
 
@@ -1924,13 +1917,6 @@ var hexSliceLookupTable = (function () {
1924
1917
  })()
1925
1918
 
1926
1919
 
1927
- /***/ }),
1928
-
1929
- /***/ 370:
1930
- /***/ (() => {
1931
-
1932
- /* (ignored) */
1933
-
1934
1920
  /***/ }),
1935
1921
 
1936
1922
  /***/ 526:
@@ -2912,9 +2898,55 @@ __webpack_require__.d(collision_namespaceObject, {
2912
2898
 
2913
2899
  // EXTERNAL MODULE: ./node_modules/buffer/index.js
2914
2900
  var buffer = __webpack_require__(287);
2901
+ ;// ./src/v1/basics/objChecker.mjs
2902
+ /**
2903
+ * Counts the number of elements in an array or the number of properties in an object.
2904
+ *
2905
+ * @param {Array<*>|Record<string | number | symbol, any>} obj - The array or object to count.
2906
+ * @returns {number} - The count of items (array elements or object keys), or `0` if the input is neither an array nor an object.
2907
+ *
2908
+ * @example
2909
+ * countObj([1, 2, 3]); // 3
2910
+ * countObj({ a: 1, b: 2 }); // 2
2911
+ * countObj('not an object'); // 0
2912
+ */
2913
+ function countObj(obj) {
2914
+ // Is Array
2915
+ if (Array.isArray(obj)) return obj.length;
2916
+ // Object
2917
+ if (isJsonObject(obj)) return Object.keys(obj).length;
2918
+ // Nothing
2919
+ return 0;
2920
+ }
2921
+
2922
+ /**
2923
+ * Determines whether a given value is a pure JSON object (plain object).
2924
+ *
2925
+ * A pure object satisfies the following:
2926
+ * - It is not null.
2927
+ * - Its type is "object".
2928
+ * - Its internal [[Class]] is "[object Object]".
2929
+ * - It is not an instance of built-in types like Array, Date, Map, Set, etc.
2930
+ *
2931
+ * This function is useful for strict data validation when you want to ensure
2932
+ * a value is a clean JSON-compatible object, free of class instances or special types.
2933
+ *
2934
+ * @param {unknown} value - The value to test.
2935
+ * @returns {value is Record<string | number | symbol, unknown>} Returns true if the value is a pure object.
2936
+ */
2937
+ function isJsonObject(value) {
2938
+ if (value === null || typeof value !== 'object') return false;
2939
+ if (Array.isArray(value)) return false;
2940
+ if (Object.prototype.toString.call(value) !== '[object Object]') return false;
2941
+ return true;
2942
+ }
2943
+
2915
2944
  ;// ./src/v1/basics/objFilter.mjs
2916
2945
 
2917
2946
 
2947
+
2948
+
2949
+
2918
2950
  const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
2919
2951
 
2920
2952
  /**
@@ -3107,48 +3139,6 @@ function getCheckObj() {
3107
3139
  return Object.fromEntries(Object.entries(typeValidator.items).map(([key, fn]) => [key, fn]));
3108
3140
  }
3109
3141
 
3110
- /**
3111
- * Counts the number of elements in an array or the number of properties in an object.
3112
- *
3113
- * @param {Array<*>|Record<string|number, any>} obj - The array or object to count.
3114
- * @returns {number} - The count of items (array elements or object keys), or `0` if the input is neither an array nor an object.
3115
- *
3116
- * @example
3117
- * countObj([1, 2, 3]); // 3
3118
- * countObj({ a: 1, b: 2 }); // 2
3119
- * countObj('not an object'); // 0
3120
- */
3121
- function countObj(obj) {
3122
- // Is Array
3123
- if (Array.isArray(obj)) return obj.length;
3124
- // Object
3125
- if (objType(obj, 'object')) return Object.keys(obj).length;
3126
- // Nothing
3127
- return 0;
3128
- }
3129
-
3130
- /**
3131
- * Determines whether a given value is a pure JSON object (plain object).
3132
- *
3133
- * A pure object satisfies the following:
3134
- * - It is not null.
3135
- * - Its type is "object".
3136
- * - Its internal [[Class]] is "[object Object]".
3137
- * - It is not an instance of built-in types like Array, Date, Map, Set, etc.
3138
- *
3139
- * This function is useful for strict data validation when you want to ensure
3140
- * a value is a clean JSON-compatible object, free of class instances or special types.
3141
- *
3142
- * @param {unknown} value - The value to test.
3143
- * @returns {value is Record<string | number | symbol, unknown>} Returns true if the value is a pure object.
3144
- */
3145
- function objFilter_isJsonObject(value) {
3146
- if (value === null || typeof value !== 'object') return false;
3147
- if (Array.isArray(value)) return false;
3148
- if (Object.prototype.toString.call(value) !== '[object Object]') return false;
3149
- return true;
3150
- }
3151
-
3152
3142
  // Insert obj types
3153
3143
 
3154
3144
  extendObjType([
@@ -3271,7 +3261,7 @@ extendObjType([
3271
3261
  [
3272
3262
  'object',
3273
3263
  /** @param {*} val @returns {val is Record<string | number | symbol, unknown>} */
3274
- (val) => objFilter_isJsonObject(val),
3264
+ (val) => isJsonObject(val),
3275
3265
  ],
3276
3266
  ]);
3277
3267
 
@@ -3471,404 +3461,6 @@ class ColorSafeStringify {
3471
3461
 
3472
3462
  /* harmony default export */ const libs_ColorSafeStringify = ((/* unused pure expression or super */ null && (ColorSafeStringify)));
3473
3463
 
3474
- ;// ./src/v1/basics/html.mjs
3475
-
3476
-
3477
- /////////////////////////////////////////////////////////////////
3478
-
3479
- /**
3480
- * Reads the contents of a file using the specified FileReader method.
3481
- *
3482
- * @param {File} file - The file to be read.
3483
- * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
3484
- * The FileReader method to use for reading the file.
3485
- * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
3486
- * @throws {Error} - If an unexpected error occurs while handling the result.
3487
- * @throws {DOMException} - If the FileReader encounters an error while reading the file.
3488
- */
3489
- function readFileBlob(file, method) {
3490
- return new Promise((resolve, reject) => {
3491
- const reader = new FileReader();
3492
- reader.onload = () => {
3493
- try {
3494
- resolve(reader.result);
3495
- } catch (error) {
3496
- reject(error);
3497
- }
3498
- };
3499
- reader.onerror = () => {
3500
- reject(reader.error);
3501
- };
3502
- reader[method](file);
3503
- });
3504
- }
3505
-
3506
- /**
3507
- * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
3508
- *
3509
- * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
3510
- *
3511
- * @param {File} file - The file to be read.
3512
- * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
3513
- * if a string is passed, it is used as the MIME type in the data URL.
3514
- * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
3515
- *
3516
- * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
3517
- * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
3518
- * @throws {DOMException} - If the FileReader fails to read the file.
3519
- */
3520
- function readBase64Blob(file, isDataUrl = false) {
3521
- return new Promise((resolve, reject) => {
3522
- if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
3523
- reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
3524
- readFileBlob(file, 'readAsDataURL')
3525
- .then(
3526
- /**
3527
- * Ensure that the URL format is correct in the required pattern
3528
- * @param {string} base64Data
3529
- */ (base64Data) => {
3530
- if (typeof base64Data !== 'string')
3531
- throw new TypeError('Expected file content to be a string.');
3532
-
3533
- const match = base64Data.match(/^data:(.+);base64,(.*)$/);
3534
- if (!match || !match[2])
3535
- throw new Error('Invalid data URL format or missing Base64 content.');
3536
- const [, mimeType, base64] = match;
3537
- if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
3538
-
3539
- if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
3540
- if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
3541
- throw new Error(`Invalid MIME type string: ${isDataUrl}`);
3542
-
3543
- return resolve(`data:${isDataUrl};base64,${base64}`);
3544
- },
3545
- )
3546
- .catch(reject);
3547
- });
3548
- }
3549
-
3550
- /**
3551
- * Reads a file and strictly validates its content as proper JSON using FileReader.
3552
- *
3553
- * Performs several checks to ensure the file contains valid, parsable JSON data.
3554
- *
3555
- * @param {File} file - The file to be read. It must contain valid JSON as plain text.
3556
- * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
3557
- *
3558
- * @throws {SyntaxError} - If the file content is not valid JSON syntax.
3559
- * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
3560
- * @throws {Error} - If the result is empty or structurally invalid as JSON.
3561
- * @throws {DOMException} - If the FileReader fails to read the file.
3562
- */
3563
- function readJsonBlob(file) {
3564
- return new Promise((resolve, reject) =>
3565
- readFileBlob(file, 'readAsText')
3566
- .then((data) => {
3567
- if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
3568
- const trimmed = data.trim();
3569
- if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
3570
- const parsed = JSON.parse(trimmed);
3571
- if (typeof parsed !== 'object' || parsed === null)
3572
- throw new Error('Parsed content is not a valid JSON object or array.');
3573
- resolve(parsed);
3574
- })
3575
- .catch(reject),
3576
- );
3577
- }
3578
-
3579
- /**
3580
- * Saves a JSON object as a downloadable file.
3581
- * @param {string} filename
3582
- * @param {any} data
3583
- * @param {number} [spaces=2]
3584
- */
3585
- function saveJsonFile(filename, data, spaces = 2) {
3586
- const json = JSON.stringify(data, null, spaces);
3587
- const blob = new Blob([json], { type: 'application/json' });
3588
- const url = URL.createObjectURL(blob);
3589
-
3590
- const link = document.createElement('a');
3591
- link.href = url;
3592
- link.download = filename;
3593
-
3594
- document.body.appendChild(link);
3595
- link.click();
3596
- document.body.removeChild(link);
3597
-
3598
- URL.revokeObjectURL(url);
3599
- }
3600
-
3601
- /**
3602
- * @typedef {Object} FetchTemplateOptions
3603
- * @property {string} [method="GET"] - HTTP method to use (GET, POST, etc.).
3604
- * @property {any} [body] - Request body (only for methods like POST, PUT).
3605
- * @property {number} [timeout=0] - Timeout in milliseconds (ignored if signal is provided).
3606
- * @property {number} [retries=0] - Number of retry attempts (ignored if signal is provided).
3607
- * @property {Headers|Record<string, *>} [headers={}] - Additional headers.
3608
- * @property {AbortSignal|null} [signal] - External AbortSignal; disables timeout and retries.
3609
- */
3610
-
3611
- /**
3612
- * @param {string} url - The full URL to fetch data from.
3613
- * @param {FetchTemplateOptions} [options] - Optional settings.
3614
- * @returns {Promise<Response>} Result data.
3615
- * @throws {Error} Throws if fetch fails, times out.
3616
- */
3617
- async function fetchTemplate(
3618
- url,
3619
- { method = 'GET', body, timeout = 0, retries = 0, headers = {}, signal = null } = {},
3620
- ) {
3621
- if (
3622
- typeof url !== 'string' ||
3623
- (!url.startsWith('../') &&
3624
- !url.startsWith('./') &&
3625
- !url.startsWith('/') &&
3626
- !url.startsWith('https://') &&
3627
- !url.startsWith('http://'))
3628
- )
3629
- throw new Error('Invalid URL: must be a valid http or https address.');
3630
-
3631
- if (typeof method !== 'string' || !method.trim())
3632
- throw new Error('Invalid method: must be a non-empty string.');
3633
-
3634
- if (!signal) {
3635
- if (
3636
- typeof timeout !== 'number' ||
3637
- !Number.isFinite(timeout) ||
3638
- Number.isNaN(timeout) ||
3639
- timeout < 0
3640
- )
3641
- throw new Error('Invalid timeout: must be a positive number.');
3642
-
3643
- if (
3644
- typeof retries !== 'number' ||
3645
- !Number.isFinite(retries) ||
3646
- Number.isNaN(retries) ||
3647
- retries < 0
3648
- )
3649
- throw new Error('Invalid retries: must be a positive number.');
3650
- }
3651
-
3652
- const attempts = signal ? 1 : retries + 1;
3653
- /** @type {Error|null} */
3654
- let lastError = null;
3655
-
3656
- for (let attempt = 0; attempt < attempts; attempt++) {
3657
- const controller = signal ? null : new AbortController();
3658
- const localSignal = signal || (controller?.signal ?? null);
3659
- const timer =
3660
- !signal && timeout && controller ? setTimeout(() => controller.abort(), timeout) : null;
3661
-
3662
- try {
3663
- const response = await fetch(url, {
3664
- method: method.toUpperCase(),
3665
- headers: {
3666
- Accept: 'application/json',
3667
- ...headers,
3668
- },
3669
- body: body !== undefined ? (isJsonObject(body) ? JSON.stringify(body) : body) : undefined,
3670
- signal: localSignal,
3671
- });
3672
-
3673
- if (timer) clearTimeout(timer);
3674
-
3675
- if (!response.ok) throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
3676
- return response;
3677
- } catch (err) {
3678
- lastError = /** @type {Error} */ (err);
3679
- if (signal) break; // if an external signal came, it does not retry
3680
- if (attempt < retries)
3681
- await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)));
3682
- }
3683
- }
3684
-
3685
- throw new Error(
3686
- `Failed to fetch JSON from "${url}"${lastError ? `: ${lastError.message}` : '.'}`,
3687
- );
3688
- }
3689
-
3690
- /**
3691
- * Loads and parses a JSON from a remote URL using Fetch API.
3692
- *
3693
- * @param {string} url - The full URL to fetch JSON from.
3694
- * @param {Object} [options] - Optional settings.
3695
- * @returns {Promise<any[] | Record<string | number | symbol, unknown>>} Parsed JSON object.
3696
- * @throws {Error} Throws if fetch fails, times out, or returns invalid JSON.
3697
- */
3698
- async function fetchJson(url, options) {
3699
- return new Promise((resolve, reject) => {
3700
- fetchTemplate(url, options)
3701
- .then(async (res) => {
3702
- const contentType = res.headers.get('content-type') || '';
3703
- if (!contentType.includes('application/json'))
3704
- throw new Error(`Unexpected content-type: ${contentType}`);
3705
-
3706
- const data = await res.json();
3707
-
3708
- if (!Array.isArray(data) && !isJsonObject(data))
3709
- throw new Error('Received invalid data instead of valid JSON.');
3710
-
3711
- return resolve(data);
3712
- })
3713
- .catch(reject);
3714
- });
3715
- }
3716
-
3717
- /**
3718
- * Loads a remote file as a Blob using Fetch API.
3719
- *
3720
- * @param {string} url - The full URL to fetch the file from.
3721
- * @param {Object} [options] - Optional fetch options.
3722
- * @param {string[]} [allowedMimeTypes] - Optional list of accepted MIME types (e.g., ['image/jpeg']).
3723
- * @returns {Promise<Blob>} - The fetched file as a Blob.
3724
- * @throws {Error} Throws if fetch fails, response is not ok, or MIME type is not allowed.
3725
- */
3726
- async function fetchBlob(url, allowedMimeTypes, options) {
3727
- return new Promise((resolve, reject) => {
3728
- fetchTemplate(url, options)
3729
- .then(async (res) => {
3730
- const contentType = res.headers.get('content-type') || '';
3731
-
3732
- if (
3733
- Array.isArray(allowedMimeTypes) &&
3734
- allowedMimeTypes.length > 0 &&
3735
- !allowedMimeTypes.some((type) => contentType.includes(type))
3736
- ) {
3737
- throw new Error(`Blocked MIME type: ${contentType}`);
3738
- }
3739
-
3740
- const data = await res.blob();
3741
- return resolve(data);
3742
- })
3743
- .catch(reject);
3744
- });
3745
- }
3746
-
3747
- ///////////////////////////////////////////////////////////////////////////////
3748
-
3749
- /**
3750
- * Installs a script that toggles CSS classes on a given element
3751
- * based on the page's visibility or focus state, and optionally
3752
- * triggers callbacks on visibility changes.
3753
- *
3754
- * @param {Object} [settings={}]
3755
- * @param {Element} [settings.element=document.body] - The element to receive visibility classes.
3756
- * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
3757
- * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
3758
- * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
3759
- * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
3760
- * @returns {() => void} Function that removes all installed event listeners.
3761
- * @throws {TypeError} If any provided setting is invalid.
3762
- */
3763
- function installWindowHiddenScript({
3764
- element = document.body,
3765
- hiddenClass = 'windowHidden',
3766
- visibleClass = 'windowVisible',
3767
- onVisible,
3768
- onHidden,
3769
- } = {}) {
3770
- if (!(element instanceof Element))
3771
- throw new TypeError(`"element" must be an instance of Element.`);
3772
- if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
3773
- if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
3774
- if (onVisible !== undefined && typeof onVisible !== 'function')
3775
- throw new TypeError(`"onVisible" must be a function if provided.`);
3776
- if (onHidden !== undefined && typeof onHidden !== 'function')
3777
- throw new TypeError(`"onHidden" must be a function if provided.`);
3778
-
3779
- const removeClass = () => {
3780
- element.classList.remove(hiddenClass);
3781
- element.classList.remove(visibleClass);
3782
- };
3783
-
3784
- /** @type {string|null} */
3785
- let hiddenProp = null;
3786
-
3787
- const visibilityEvents = [
3788
- 'visibilitychange',
3789
- 'mozvisibilitychange',
3790
- 'webkitvisibilitychange',
3791
- 'msvisibilitychange',
3792
- ];
3793
-
3794
- const visibilityProps = ['hidden', 'mozHidden', 'webkitHidden', 'msHidden'];
3795
-
3796
- for (let i = 0; i < visibilityProps.length; i++) {
3797
- if (visibilityProps[i] in document) {
3798
- hiddenProp = visibilityProps[i];
3799
- break;
3800
- }
3801
- }
3802
-
3803
- /** @type {(this: any, evt: Event) => void} */
3804
- const handler = function (evt) {
3805
- removeClass();
3806
-
3807
- const type = evt?.type;
3808
- // @ts-ignore
3809
- const isHidden = hiddenProp && document[hiddenProp];
3810
-
3811
- const visibleEvents = ['focus', 'focusin', 'pageshow'];
3812
- const hiddenEvents = ['blur', 'focusout', 'pagehide'];
3813
-
3814
- if (visibleEvents.includes(type)) {
3815
- element.classList.add(visibleClass);
3816
- onVisible?.();
3817
- } else if (hiddenEvents.includes(type)) {
3818
- element.classList.add(hiddenClass);
3819
- onHidden?.();
3820
- } else {
3821
- if (isHidden) {
3822
- element.classList.add(hiddenClass);
3823
- onHidden?.();
3824
- } else {
3825
- element.classList.add(visibleClass);
3826
- onVisible?.();
3827
- }
3828
- }
3829
- };
3830
-
3831
- /** @type {() => void} */
3832
- let uninstall = () => {};
3833
-
3834
- if (hiddenProp) {
3835
- const eventType = visibilityEvents[visibilityProps.indexOf(hiddenProp)];
3836
- document.addEventListener(eventType, handler);
3837
- window.addEventListener('focus', handler);
3838
- window.addEventListener('blur', handler);
3839
-
3840
- uninstall = () => {
3841
- document.removeEventListener(eventType, handler);
3842
- window.removeEventListener('focus', handler);
3843
- window.removeEventListener('blur', handler);
3844
- removeClass();
3845
- };
3846
- } else if ('onfocusin' in document) {
3847
- // Fallback for IE9 and older
3848
- // @ts-ignore
3849
- document.onfocusin = document.onfocusout = handler;
3850
- uninstall = () => {
3851
- // @ts-ignore
3852
- document.onfocusin = document.onfocusout = null;
3853
- removeClass();
3854
- };
3855
- } else {
3856
- // Last resort fallback
3857
- window.onpageshow = window.onpagehide = window.onfocus = window.onblur = handler;
3858
- uninstall = () => {
3859
- window.onpageshow = window.onpagehide = window.onfocus = window.onblur = null;
3860
- removeClass();
3861
- };
3862
- }
3863
-
3864
- // Trigger initial state
3865
- // @ts-ignore
3866
- const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
3867
- handler(simulatedEvent);
3868
-
3869
- return uninstall;
3870
- }
3871
-
3872
3464
  ;// ./src/v1/basics/collision.mjs
3873
3465
  /**
3874
3466
  * A direction relative to a rectangle.
@@ -9455,8 +9047,6 @@ function isScrolledIntoView(element) {
9455
9047
  return elemBottom <= viewportBottom && elemTop >= viewportTop;
9456
9048
  }
9457
9049
 
9458
- // EXTERNAL MODULE: fs (ignored)
9459
- var fs_ignored_ = __webpack_require__(133);
9460
9050
  // EXTERNAL MODULE: ./node_modules/path-browserify/index.js
9461
9051
  var path_browserify = __webpack_require__(975);
9462
9052
  ;// ./src/v1/fileManager/normalFuncs.mjs
@@ -9865,8 +9455,6 @@ function renameFilePadNumbers(dirPath, totalDigits = 3, extensions = []) {
9865
9455
  );
9866
9456
  }
9867
9457
 
9868
- // EXTERNAL MODULE: fs/promises (ignored)
9869
- var promises_ignored_ = __webpack_require__(370);
9870
9458
  ;// ./src/v1/fileManager/asyncFuncs.mjs
9871
9459
 
9872
9460
 
@@ -10223,7 +9811,7 @@ class TinyDragger {
10223
9811
  if (
10224
9812
  options.vibration !== undefined &&
10225
9813
  options.vibration !== false &&
10226
- !objFilter_isJsonObject(options.vibration)
9814
+ !isJsonObject(options.vibration)
10227
9815
  )
10228
9816
  throw new Error('The "vibration" option must be an object or false.');
10229
9817
 
@@ -10267,7 +9855,7 @@ class TinyDragger {
10267
9855
  const vibrationTemplate = { start: false, end: false, collide: false, move: false };
10268
9856
  this.#vibration = Object.assign(
10269
9857
  vibrationTemplate,
10270
- objFilter_isJsonObject(options.vibration) ? options.vibration : {},
9858
+ isJsonObject(options.vibration) ? options.vibration : {},
10271
9859
  );
10272
9860
 
10273
9861
  if (typeof options.classDragging === 'string') this.#classDragging = options.classDragging;
@@ -11106,574 +10694,1092 @@ class TinyDragger {
11106
10694
 
11107
10695
  /* harmony default export */ const libs_TinyDragger = ((/* unused pure expression or super */ null && (TinyDragger)));
11108
10696
 
11109
- ;// ./src/v1/libs/TinySmartScroller.mjs
11110
-
11111
-
11112
-
11113
- /**
11114
- * Represents the dimensions of a DOM element.
11115
- *
11116
- * @typedef {Object} NodeSizes
11117
- * @property {number} height - The height of the element in pixels.
11118
- * @property {number} width - The width of the element in pixels.
11119
- */
11120
-
10697
+ ;// ./src/v1/libs/TinyEvents.mjs
11121
10698
  /**
11122
- * A callback function that receives node size change data and optionally returns a modified NodeSizes object.
11123
- *
11124
- * @callback NodeSizesEvent
11125
- * @param {Element} elem - The DOM element whose size is being tracked.
11126
- * @param {{ old: NodeSizes, now: NodeSizes }} sizes - The old and new size measurements of the element.
11127
- * @param {{ old: number, now: number }} elemAmount - The number of matching elements before and after the update.
11128
- * @returns {NodeSizes|undefined} A modified NodeSizes object to override the default measurement, or undefined to use the original.
11129
- */
11130
-
11131
- /**
11132
- * A generic scroll-related event listener callback function.
10699
+ * A generic event listener callback function.
11133
10700
  *
11134
- * @callback ScrollListenersFunc
11135
- * @param {any} payload - The data payload passed when the scroll event is triggered. The type may vary depending on the event.
10701
+ * @callback handler
10702
+ * @param {...any} payload - The data payload passed when the event is triggered.
11136
10703
  * @returns {void}
11137
10704
  */
11138
10705
 
11139
10706
  /**
11140
- * TinySmartScroller is a utility class designed to enhance and manage scroll behaviors within containers or the window.
10707
+ * TinyEvents provides a minimalistic event emitter system similar to Node.js's EventEmitter,
10708
+ * enabling components to subscribe to, emit, and manage events and their listeners.
11141
10709
  *
11142
- * It enables advanced scroll monitoring, auto-scrolling to bottom, preserving scroll position during DOM changes,
11143
- * and detecting visibility changes of elements. This is particularly useful for dynamic UIs like chat applications,
11144
- * feed viewers, or live content containers.
10710
+ * Features include:
10711
+ * - Adding/removing event listeners (`on`, `off`, `offAll`, `offAllTypes`)
10712
+ * - One-time listeners (`once`)
10713
+ * - Emitting events (`emit`)
10714
+ * - Listener inspection and limits (`listenerCount`, `listeners`, `eventNames`)
10715
+ * - Maximum listener control (`setMaxListeners`, `getMaxListeners`)
11145
10716
  *
11146
- * Features:
11147
- * - Detects when the scroll reaches the top, bottom, or custom boundaries
11148
- * - Supports automatic scrolling to the bottom unless the user scrolls away
11149
- * - Observes DOM mutations and resizes, and adapts scroll position accordingly
11150
- * - Emits scroll-related events such as 'onScrollBoundary', 'onAutoScroll', and 'onScrollPause'
11151
- * - Includes customizable scroll correction filters for layout shift mitigation
11152
- * - Handles media element load events (e.g. `<img>`, `<iframe>`, `<video>`) to prevent sudden scroll jumps
10717
+ * This class is useful for lightweight, dependency-free publish/subscribe event handling
10718
+ * within modular JavaScript applications.
11153
10719
  *
11154
- * This class is **not framework-dependent** and works with vanilla DOM elements and the window object.
10720
+ * @class
11155
10721
  */
11156
- class TinySmartScroller {
11157
- static Utils = { ...collision_namespaceObject, TinyHtml: libs_TinyHtml };
10722
+ class TinyEvents {
10723
+ /** @type {Map<string, { handler: handler; config: { once: boolean } }[]>} */
10724
+ #listeners = new Map();
11158
10725
 
11159
- /** @type {WeakMap<Element, NodeSizes>} */
11160
- #oldSizes = new WeakMap();
11161
- /** @type {WeakMap<Element, NodeSizes>} */
11162
- #newSizes = new WeakMap();
10726
+ /** @type {number} */
10727
+ #maxListeners = 10;
11163
10728
 
11164
- /** @type {WeakMap<Element, boolean>} */
11165
- #newVisibles = new WeakMap();
11166
- /** @type {WeakMap<Element, boolean>} */
11167
- #oldVisibles = new WeakMap();
11168
-
11169
- /** @type {WeakMap<Element, boolean>} */
11170
- #newVisiblesByTime = new WeakMap();
11171
- /** @type {WeakMap<Element, boolean>} */
11172
- #oldVisiblesByTime = new WeakMap();
11173
-
11174
- /** @type {Record<string, ScrollListenersFunc[]>} */
11175
- #scrollListeners = {};
11176
-
11177
- /** @type {ResizeObserver|null} */
11178
- #resizeObserver = null;
11179
- /** @type {MutationObserver|null} */
11180
- #mutationObserver = null;
11181
-
11182
- /** @type {Set<string>} */
11183
- #loadTags = new Set(['IMG', 'IFRAME', 'VIDEO']);
11184
-
11185
- /** @type {null|EventListenerOrEventListenerObject} */
11186
- #handler = null;
11187
-
11188
- #isAtBottom = false;
11189
- #isAtTop = false;
11190
- #isAtCustomTop = false;
11191
- #isAtCustomBottom = false;
11192
-
11193
- #querySelector = '';
11194
- #useWindow = false;
11195
- #destroyed = false;
11196
- #scrollPaused = false;
11197
- #autoScrollBottom = false;
11198
- #observeMutations = false;
11199
- #preserveScrollOnLayoutShift = false;
11200
- #debounceTime = 0;
11201
- #elemAmount = 0;
11202
- #elemOldAmount = 0;
11203
- #lastKnownScrollBottomOffset = 0;
11204
- #extraScrollBoundary = 0;
11205
-
11206
- /** @type {Set<string>} */
11207
- #attributeFilter;
11208
-
11209
- /** @type {Element} */
11210
- #target;
11211
-
11212
- /** @type {Set<NodeSizesEvent>} */
11213
- #sizeFilter = new Set();
10729
+ /** @type {boolean} */
10730
+ #throwMaxListeners = false;
11214
10731
 
11215
10732
  /**
11216
- * Creates a new instance of TinySmartScroller, attaching scroll and resize observers to manage
11217
- * automatic scroll behaviors, layout shift correction, and visibility tracking.
10733
+ * Enables or disables throwing an error when the maximum number of listeners is exceeded.
11218
10734
  *
11219
- * @param {Element|Window} target - The scroll container to monitor. Can be an element or `window`.
11220
- * @param {Object} [options={}] - Optional settings to configure scroll behavior.
11221
- * @param {number} [options.extraScrollBoundary=0] - Extra margin in pixels to extend scroll boundary detection.
11222
- * @param {boolean} [options.autoScrollBottom=true] - Whether to auto-scroll to bottom on layout updates.
11223
- * @param {boolean} [options.observeMutations=true] - Enables MutationObserver to detect DOM changes.
11224
- * @param {boolean} [options.preserveScrollOnLayoutShift=true] - Prevents scroll jumps when layout changes.
11225
- * @param {number} [options.debounceTime=100] - Debounce time in milliseconds for scroll events.
11226
- * @param {string|null} [options.querySelector=null] - Optional CSS selector to filter observed child nodes.
11227
- * @param {string[]|Set<string>|null} [options.attributeFilter=['class', 'style', 'src', 'data-*', 'height', 'width']]
11228
- * - Which attributes to observe for changes.
10735
+ * @param {boolean} shouldThrow - If true, an error will be thrown when the max is exceeded.
11229
10736
  */
11230
- constructor(
11231
- target,
11232
- {
11233
- extraScrollBoundary = 0,
11234
- autoScrollBottom = true,
11235
- observeMutations = true,
11236
- preserveScrollOnLayoutShift = true,
11237
- debounceTime = 100,
11238
- querySelector = null,
11239
- attributeFilter = ['class', 'style', 'src', 'data-*', 'height', 'width'],
11240
- } = {},
11241
- ) {
11242
- // === target ===
11243
- if (!(target instanceof Element || target === window))
11244
- throw new TypeError(
11245
- `TinySmartScroller: 'target' must be a DOM Element or 'window', but got ${typeof target}`,
11246
- );
11247
- // === extraScrollBoundary ===
11248
- if (typeof extraScrollBoundary !== 'number' || Number.isNaN(extraScrollBoundary))
11249
- throw new TypeError(
11250
- `TinySmartScroller: 'extraScrollBoundary' must be a valid number, received ${extraScrollBoundary}`,
11251
- );
11252
- // === autoScrollBottom ===
11253
- if (typeof autoScrollBottom !== 'boolean')
11254
- throw new TypeError(
11255
- `TinySmartScroller: 'autoScrollBottom' must be a boolean, received ${typeof autoScrollBottom}`,
11256
- );
11257
- // === observeMutations ===
11258
- if (typeof observeMutations !== 'boolean')
11259
- throw new TypeError(
11260
- `TinySmartScroller: 'observeMutations' must be a boolean, received ${typeof observeMutations}`,
11261
- );
11262
- // === preserveScrollOnLayoutShift ===
11263
- if (typeof preserveScrollOnLayoutShift !== 'boolean')
11264
- throw new TypeError(
11265
- `TinySmartScroller: 'preserveScrollOnLayoutShift' must be a boolean, received ${typeof preserveScrollOnLayoutShift}`,
11266
- );
11267
- // === debounceTime ===
11268
- if (typeof debounceTime !== 'number' || debounceTime < 0 || Number.isNaN(debounceTime))
11269
- throw new TypeError(
11270
- `TinySmartScroller: 'debounceTime' must be a non-negative number, received ${debounceTime}`,
11271
- );
11272
- // === querySelector ===
11273
- if (querySelector !== null && typeof querySelector !== 'string')
11274
- throw new TypeError(
11275
- `TinySmartScroller: 'querySelector' must be a string or null, received ${typeof querySelector}`,
11276
- );
11277
- // === attributeFilter ===
11278
- const isValidAttrList =
11279
- attributeFilter === null || Array.isArray(attributeFilter) || attributeFilter instanceof Set;
11280
- if (!isValidAttrList)
11281
- throw new TypeError(
11282
- `TinySmartScroller: 'attributeFilter' must be an array, Set, or null. Got ${typeof attributeFilter}`,
11283
- );
11284
-
11285
- // Start values
11286
- this.#target = target instanceof Window ? document.documentElement : target;
11287
- this.#useWindow = target instanceof Window;
11288
- this.#autoScrollBottom = autoScrollBottom;
11289
- this.#observeMutations = observeMutations;
11290
- this.#preserveScrollOnLayoutShift = preserveScrollOnLayoutShift;
11291
- this.#debounceTime = debounceTime;
11292
- this.#extraScrollBoundary = extraScrollBoundary;
11293
- this.#querySelector = querySelector || '';
11294
- this.#attributeFilter = new Set(attributeFilter || undefined);
11295
-
11296
- // Bind scroll
11297
- /** @type {NodeJS.Timeout} */
11298
- let timeout;
11299
- this.#handler = () => {
11300
- this._scrollDataUpdater();
11301
- clearTimeout(timeout);
11302
- timeout = setTimeout(() => this._onScroll(), this.#debounceTime);
11303
- };
11304
- (this.#useWindow ? window : this.#target).addEventListener('scroll', this.#handler, {
11305
- passive: true,
11306
- });
11307
-
11308
- // Mutations
11309
- if (this.#observeMutations) {
11310
- this._observeMutations();
11311
- this._observeResizes(this.#target.children);
11312
- }
11313
-
11314
- this._scrollDataUpdater();
10737
+ setThrowOnMaxListeners(shouldThrow) {
10738
+ if (typeof shouldThrow !== 'boolean')
10739
+ throw new TypeError('setThrowOnMaxListeners(value): value must be a boolean');
10740
+ this.#throwMaxListeners = shouldThrow;
11315
10741
  }
11316
10742
 
11317
10743
  /**
11318
- * Returns a size difference callback that only reacts when height changes, filtered by tag name.
10744
+ * Checks whether an error will be thrown when the max listener limit is exceeded.
11319
10745
  *
11320
- * @param {string[]} filter - List of tag names to allow. If empty, all tags are accepted.
11321
- * @returns {NodeSizesEvent} A function that compares previous and current height, returning height delta.
10746
+ * @returns {boolean} True if an error will be thrown, false if only a warning is shown.
11322
10747
  */
11323
- getSimpleOnHeight(filter = []) {
11324
- if (!Array.isArray(filter))
11325
- throw new TypeError('getSimpleOnHeight(filter): filter must be an array of tag names');
11326
- return (elem, sizes, amounts) => {
11327
- if ((filter.length > 0 && !filter.includes(elem.tagName)) || amounts.now !== amounts.old)
11328
- return;
11329
- const oldSize = sizes.old;
11330
- const newSize = sizes.now;
11331
- const height = newSize.height - oldSize.height;
11332
- return { height, width: 0 };
11333
- };
10748
+ getThrowOnMaxListeners() {
10749
+ return this.#throwMaxListeners;
11334
10750
  }
11335
10751
 
11336
- /**
11337
- * Adds a height difference callback to the size filter system.
11338
- *
11339
- * @param {string[]} filter - List of tag names to allow.
11340
- * @returns {NodeSizesEvent} The added size difference callback.
11341
- */
11342
- addSimpleOnHeight(filter) {
11343
- if (!Array.isArray(filter))
11344
- throw new TypeError('addSimpleOnHeight(filter): filter must be an array of tag names');
11345
- const result = this.getSimpleOnHeight(filter);
11346
- this.onSize(result);
11347
- return result;
11348
- }
10752
+ ///////////////////////////////////////////////////
11349
10753
 
11350
10754
  /**
11351
- * Returns a list of all currently tracked load tags.
10755
+ * Internal method to prepend a listener with options.
11352
10756
  *
11353
- * @returns {string[]} Array of tag names.
10757
+ * @param {string} event - Event name.
10758
+ * @param {handler} handler - The callback function.
10759
+ * @param {Object} [settings={}] - Optional settings.
10760
+ * @param {boolean} [settings.once=false] - If the listener should be executed once.
11354
10761
  */
11355
- getLoadTags() {
11356
- return Array.from(this.#loadTags);
10762
+ #prepend(event, handler, { once = false } = {}) {
10763
+ let eventData = this.#listeners.get(event);
10764
+ if (!Array.isArray(eventData)) {
10765
+ eventData = [];
10766
+ this.#listeners.set(event, eventData);
10767
+ }
10768
+ eventData.unshift({ handler, config: { once } });
10769
+
10770
+ const max = this.#maxListeners;
10771
+ if (max > 0 && eventData.length > max) {
10772
+ const warnMessage =
10773
+ `Possible memory leak detected. ${eventData.length} "${event}" listeners added. ` +
10774
+ `Use setMaxListeners() to increase limit.`;
10775
+ if (!this.#throwMaxListeners) console.warn(warnMessage);
10776
+ else throw new Error(warnMessage);
10777
+ }
11357
10778
  }
11358
10779
 
11359
10780
  /**
11360
- * Adds a new tag to the set of load tags.
10781
+ * Adds a listener to the beginning of the listeners array for the specified event.
11361
10782
  *
11362
- * @param {string} tag - The tag name to add (e.g., 'IMG').
10783
+ * @param {string} event - Event name.
10784
+ * @param {handler} handler - The callback function.
11363
10785
  */
11364
- addLoadTag(tag) {
11365
- if (typeof tag !== 'string') throw new TypeError('addLoadTag(tag): tag must be a string');
11366
- this.#loadTags.add(tag.toUpperCase());
10786
+ prependListener(event, handler) {
10787
+ if (typeof event !== 'string')
10788
+ throw new TypeError('prepend(event, handler): event name must be a string');
10789
+ if (typeof handler !== 'function')
10790
+ throw new TypeError('prepend(event, handler): handler must be a function');
10791
+ this.#prepend(event, handler);
11367
10792
  }
11368
10793
 
11369
10794
  /**
11370
- * Removes a tag from the set of load tags.
10795
+ * Adds a one-time listener to the beginning of the listeners array for the specified event.
11371
10796
  *
11372
- * @param {string} tag - The tag name to remove.
10797
+ * @param {string} event - Event name.
10798
+ * @param {handler} handler - The callback function.
10799
+ * @returns {handler} - The wrapped handler used internally.
11373
10800
  */
11374
- removeLoadTag(tag) {
11375
- if (typeof tag !== 'string') throw new TypeError('removeLoadTag(tag): tag must be a string');
11376
- this.#loadTags.delete(tag.toUpperCase());
10801
+ prependListenerOnce(event, handler) {
10802
+ if (typeof event !== 'string')
10803
+ throw new TypeError('prependOnceListener(event, handler): event name must be a string');
10804
+ if (typeof handler !== 'function')
10805
+ throw new TypeError('prependOnceListener(event, handler): handler must be a function');
10806
+
10807
+ /** @type {handler} */
10808
+ const wrapped = (...args) => {
10809
+ this.off(event, wrapped);
10810
+ handler(...args);
10811
+ };
10812
+
10813
+ this.#prepend(event, wrapped, { once: true });
10814
+ return wrapped;
11377
10815
  }
11378
10816
 
10817
+ ////////////////////////////////////////////////////////////
10818
+
11379
10819
  /**
11380
- * Checks whether a tag is tracked as a load tag.
10820
+ * Adds a event listener.
11381
10821
  *
11382
- * @param {string} tag - The tag name to check.
11383
- * @returns {boolean} True if the tag is being tracked.
10822
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
10823
+ * @param {handler} handler - Callback function to be called when event fires.
10824
+ * @param {Object} [settings={}] - Optional settings.
10825
+ * @param {boolean} [settings.once=false] - This is a once event.
11384
10826
  */
11385
- hasLoadTag(tag) {
11386
- if (typeof tag !== 'string') throw new TypeError('hasLoadTag(tag): tag must be a string');
11387
- return this.#loadTags.has(tag.toUpperCase());
10827
+ #on(event, handler, { once = false } = {}) {
10828
+ let eventData = this.#listeners.get(event);
10829
+ if (!Array.isArray(eventData)) {
10830
+ eventData = [];
10831
+ this.#listeners.set(event, eventData);
10832
+ }
10833
+ eventData.push({ handler, config: { once } });
10834
+ // Warn if listener count exceeds the max allowed
10835
+ const max = this.#maxListeners;
10836
+ if (max > 0 && eventData.length > max) {
10837
+ const warnMessage =
10838
+ `Possible memory leak detected. ${eventData.length} "${event}" listeners added. ` +
10839
+ `Use setMaxListeners() to increase limit.`;
10840
+ if (!this.#throwMaxListeners) console.warn(warnMessage);
10841
+ else throw new Error(warnMessage);
10842
+ }
11388
10843
  }
11389
10844
 
11390
10845
  /**
11391
- * Clears the set of load tags. If `addDefault` is true, it will reset to the default tags: 'IMG', 'IFRAME', and 'VIDEO'.
10846
+ * Adds a event listener.
11392
10847
  *
11393
- * @param {boolean} [addDefault=false] - Whether to restore the default tags after clearing.
11394
- * @throws {TypeError} If `addDefault` is not a boolean.
10848
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
10849
+ * @param {handler} handler - Callback function to be called when event fires.
11395
10850
  */
11396
- resetLoadTags(addDefault = false) {
11397
- if (typeof addDefault !== 'boolean')
11398
- throw new TypeError('resetLoadTags(addDefault): addDefault must be a boolean');
11399
- this.#loadTags.clear();
11400
- if (!addDefault) return;
11401
- this.#loadTags.add('IMG');
11402
- this.#loadTags.add('IFRAME');
11403
- this.#loadTags.add('VIDEO');
10851
+ on(event, handler) {
10852
+ if (typeof event !== 'string')
10853
+ throw new TypeError('on(event, handler): event name must be a string');
10854
+ if (typeof handler !== 'function')
10855
+ throw new TypeError('on(event, handler): handler must be a function');
10856
+ return this.#on(event, handler);
11404
10857
  }
11405
10858
 
11406
10859
  /**
11407
- * Returns a list of all currently tracked attribute filters.
10860
+ * Registers an event listener that runs only once, then is removed.
11408
10861
  *
11409
- * @returns {string[]} Array of attribute names.
10862
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
10863
+ * @param {handler} handler - The callback function to run on event.
10864
+ * @returns {handler} - The wrapped version of the handler.
11410
10865
  */
11411
- getAttributeFilters() {
11412
- return Array.from(this.#attributeFilter);
10866
+ once(event, handler) {
10867
+ if (typeof event !== 'string') throw new TypeError('The event name must be a string.');
10868
+ if (typeof handler !== 'function')
10869
+ throw new TypeError('once(event, handler): handler must be a function');
10870
+
10871
+ /** @type {handler} */
10872
+ const wrapped = (e) => {
10873
+ this.off(event, wrapped);
10874
+ if (typeof handler === 'function') handler(e);
10875
+ };
10876
+ this.#on(event, wrapped, { once: true });
10877
+ return wrapped;
11413
10878
  }
11414
10879
 
11415
10880
  /**
11416
- * Adds an attribute to the filter list.
10881
+ * Adds a event listener.
11417
10882
  *
11418
- * @param {string} attr - The attribute name to add.
10883
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
10884
+ * @param {handler} handler - Callback function to be called when event fires.
11419
10885
  */
11420
- addAttributeFilter(attr) {
11421
- if (typeof attr !== 'string')
11422
- throw new TypeError('addAttributeFilter(attr): attr must be a string');
11423
- this.#attributeFilter.add(attr);
10886
+ appendListener(event, handler) {
10887
+ return this.on(event, handler);
11424
10888
  }
11425
10889
 
11426
10890
  /**
11427
- * Removes an attribute from the filter list.
10891
+ * Registers an event listener that runs only once, then is removed.
11428
10892
  *
11429
- * @param {string} attr - The attribute name to remove.
10893
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
10894
+ * @param {handler} handler - The callback function to run on event.
10895
+ * @returns {handler} - The wrapped version of the handler.
11430
10896
  */
11431
- removeAttributeFilter(attr) {
11432
- if (typeof attr !== 'string')
11433
- throw new TypeError('removeAttributeFilter(attr): attr must be a string');
11434
- this.#attributeFilter.delete(attr);
10897
+ appendListenerOnce(event, handler) {
10898
+ return this.once(event, handler);
11435
10899
  }
11436
10900
 
10901
+ ///////////////////////////////////////////////
10902
+
11437
10903
  /**
11438
- * Checks whether a specific attribute is being filtered.
10904
+ * Removes a previously registered event listener.
11439
10905
  *
11440
- * @param {string} attr - The attribute name to check.
11441
- * @returns {boolean} True if the attribute is being filtered.
10906
+ * @param {string} event - The name of the event to remove the handler from.
10907
+ * @param {handler} handler - The specific callback function to remove.
11442
10908
  */
11443
- hasAttributeFilter(attr) {
11444
- if (typeof attr !== 'string')
11445
- throw new TypeError('hasAttributeFilter(attr): attr must be a string');
11446
- return this.#attributeFilter.has(attr);
10909
+ off(event, handler) {
10910
+ if (typeof event !== 'string')
10911
+ throw new TypeError('off(event, handler): event name must be a string');
10912
+ if (typeof handler !== 'function')
10913
+ throw new TypeError('off(event, handler): handler must be a function');
10914
+
10915
+ const listeners = this.#listeners.get(event);
10916
+ if (!Array.isArray(listeners)) return;
10917
+
10918
+ const index = listeners.findIndex((listener) => listener.handler === handler);
10919
+ if (index !== -1) listeners.splice(index, 1);
10920
+
10921
+ // Optionally clean up empty arrays (optional)
10922
+ if (listeners.length === 0) this.#listeners.delete(event);
11447
10923
  }
11448
10924
 
11449
10925
  /**
11450
- * Clears the set of observed attribute filters. If `addDefault` is true, it will reset to the default attributes:
11451
- * 'class', 'style', 'src', 'data-*', 'height', and 'width'.
10926
+ * Removes all event listeners of a specific type from the element.
11452
10927
  *
11453
- * @param {boolean} [addDefault=false] - Whether to restore the default attribute filters after clearing.
11454
- * @throws {TypeError} If `addDefault` is not a boolean.
10928
+ * @param {string} event - The event type to remove (e.g. 'onScrollBoundary').
11455
10929
  */
11456
- resetAttributeFilters(addDefault = false) {
11457
- if (typeof addDefault !== 'boolean')
11458
- throw new TypeError('resetAttributeFilters(addDefault): addDefault must be a boolean');
11459
- this.#attributeFilter.clear();
11460
- if (!addDefault) return;
11461
- ['class', 'style', 'src', 'data-*', 'height', 'width'].forEach((attr) =>
11462
- this.#attributeFilter.add(attr),
11463
- );
10930
+ offAll(event) {
10931
+ if (typeof event !== 'string') throw new TypeError('The event name must be a string.');
10932
+ this.#listeners.delete(event);
11464
10933
  }
11465
10934
 
11466
10935
  /**
11467
- * Registers a custom node size change handler to the internal size filter set.
11468
- *
11469
- * @param {NodeSizesEvent} handler - Function that compares old and new sizes.
10936
+ * Removes all event listeners of all types from the element.
11470
10937
  */
11471
- onSize(handler) {
11472
- if (this.#destroyed) return;
11473
- if (typeof handler !== 'function')
11474
- throw new TypeError('onSize(handler): handler must be a function');
11475
- this.#sizeFilter.add(handler);
10938
+ offAllTypes() {
10939
+ this.#listeners.clear();
11476
10940
  }
11477
10941
 
10942
+ /////////////////////////////////////////////
10943
+
11478
10944
  /**
11479
- * Unregisters a previously registered size handler from the internal filter set.
10945
+ * Returns the number of listeners for a given event.
11480
10946
  *
11481
- * @param {NodeSizesEvent} handler - The handler function to remove.
10947
+ * @param {string} event - The name of the event.
10948
+ * @returns {number} Number of listeners for the event.
11482
10949
  */
11483
- offSize(handler) {
11484
- if (this.#destroyed) return;
11485
- if (typeof handler !== 'function')
11486
- throw new TypeError('offSize(handler): handler must be a function');
11487
- this.#sizeFilter.delete(handler);
10950
+ listenerCount(event) {
10951
+ if (typeof event !== 'string')
10952
+ throw new TypeError('listenerCount(event): event name must be a string');
10953
+
10954
+ const listeners = this.#listeners.get(event);
10955
+ return Array.isArray(listeners) ? listeners.length : 0;
11488
10956
  }
11489
10957
 
11490
10958
  /**
11491
- * Adds a scroll-related event listener.
10959
+ * Returns a copy of the array of listeners for the specified event.
11492
10960
  *
11493
- * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
11494
- * @param {ScrollListenersFunc} handler - Callback function to be called when event fires.
10961
+ * @param {string} event - The name of the event.
10962
+ * @returns {handler[]} Array of listener functions.
11495
10963
  */
11496
- on(event, handler) {
11497
- if (this.#destroyed) return;
10964
+ listeners(event) {
11498
10965
  if (typeof event !== 'string')
11499
- throw new TypeError('on(event, handler): event name must be a string');
11500
- if (typeof handler !== 'function')
11501
- throw new TypeError('on(event, handler): handler must be a function');
10966
+ throw new TypeError('listeners(event): event name must be a string');
11502
10967
 
11503
- if (!this.#scrollListeners[event]) this.#scrollListeners[event] = [];
11504
- this.#scrollListeners[event].push(handler);
10968
+ const listeners = this.#listeners.get(event);
10969
+ return Array.isArray(listeners)
10970
+ ? [...listeners]
10971
+ .filter((listener) => !listener.config.once)
10972
+ .map((listener) => listener.handler)
10973
+ : [];
11505
10974
  }
11506
10975
 
11507
10976
  /**
11508
- * Removes a previously registered scroll-related event listener.
10977
+ * Returns a copy of the array of listeners for the specified event.
11509
10978
  *
11510
- * @param {string} event - The name of the event to remove the handler from.
11511
- * @param {ScrollListenersFunc} handler - The specific callback function to remove.
10979
+ * @param {string} event - The name of the event.
10980
+ * @returns {handler[]} Array of listener functions.
11512
10981
  */
11513
- off(event, handler) {
11514
- if (this.#destroyed) return;
10982
+ onceListeners(event) {
11515
10983
  if (typeof event !== 'string')
11516
- throw new TypeError('off(event, handler): event name must be a string');
11517
- if (typeof handler !== 'function')
11518
- throw new TypeError('off(event, handler): handler must be a function');
10984
+ throw new TypeError('onceListeners(event): event name must be a string');
11519
10985
 
11520
- const listeners = this.#scrollListeners[event];
11521
- if (!Array.isArray(listeners)) return;
10986
+ const listeners = this.#listeners.get(event);
10987
+ return Array.isArray(listeners)
10988
+ ? [...listeners]
10989
+ .filter((listener) => listener.config.once)
10990
+ .map((listener) => listener.handler)
10991
+ : [];
10992
+ }
11522
10993
 
11523
- const index = listeners.indexOf(handler);
11524
- if (index !== -1) listeners.splice(index, 1);
10994
+ /**
10995
+ * Returns a copy of the internal listeners array for the specified event,
10996
+ * including wrapper functions like those used by `.once()`.
10997
+ * @param {string | symbol} event - The event name.
10998
+ * @returns {handler[]} An array of raw listener functions.
10999
+ */
11000
+ allListeners(event) {
11001
+ if (typeof event !== 'string')
11002
+ throw new TypeError('allListeners(event): event name must be a string');
11003
+ const listeners = this.#listeners.get(event);
11004
+ return Array.isArray(listeners) ? [...listeners].map((listener) => listener.handler) : [];
11005
+ }
11525
11006
 
11526
- // Optionally clean up empty arrays (optional)
11527
- if (listeners.length === 0) delete this.#scrollListeners[event];
11007
+ /**
11008
+ * Returns an array of event names for which there are registered listeners.
11009
+ *
11010
+ * @returns {string[]} Array of registered event names.
11011
+ */
11012
+ eventNames() {
11013
+ return [...this.#listeners.keys()];
11528
11014
  }
11529
11015
 
11530
11016
  /**
11531
- * Checks which elements inside the target are currently visible and updates internal maps.
11017
+ * Emits an event, triggering all registered handlers for that event.
11532
11018
  *
11533
- * @returns {Map<Element, { oldIsVisible: boolean; isVisible: boolean }>} Visibility comparison results.
11019
+ * @param {string} event - The event name to emit.
11020
+ * @param {...any} payload - Optional data to pass to each handler.
11021
+ * @returns {boolean} True if any listeners were called, false otherwise.
11534
11022
  */
11535
- _scrollDataUpdater() {
11536
- const results = new Map();
11537
- this.#target.querySelectorAll(this.#querySelector || '*').forEach((target) => {
11538
- const oldIsVisible = this.#newVisibles.get(target) ?? false;
11539
- this.#oldVisibles.set(target, oldIsVisible);
11023
+ emit(event, ...payload) {
11024
+ if (typeof event !== 'string')
11025
+ throw new TypeError('emit(event, data): event name must be a string');
11540
11026
 
11541
- const isVisible = libs_TinyHtml.isInContainer(this.#target, target);
11542
- this.#newVisibles.set(target, isVisible);
11027
+ const listeners = this.#listeners.get(event);
11028
+ if (!Array.isArray(listeners) || listeners.length === 0) return false;
11543
11029
 
11544
- results.set(target, { oldIsVisible, isVisible });
11545
- });
11546
- return results;
11030
+ // Call all listeners with the provided data
11031
+ listeners.forEach((listener) => listener.handler(...payload));
11032
+ return true;
11547
11033
  }
11548
11034
 
11035
+ ///////////////////////////////////
11036
+
11549
11037
  /**
11550
- * Emits a scroll-related event to all registered listeners.
11038
+ * Sets the maximum number of listeners per event before a warning is shown.
11551
11039
  *
11552
- * @param {string} event - Event name.
11553
- * @param {*} [payload] - Optional event data payload.
11040
+ * @param {number} n - The maximum number of listeners.
11554
11041
  */
11555
- _emit(event, payload) {
11556
- if (this.#destroyed) return;
11557
- if (typeof event !== 'string')
11558
- throw new TypeError('_emit(event, payload): event name must be a string');
11559
- (this.#scrollListeners[event] || []).forEach((fn) => fn(payload));
11042
+ setMaxListeners(n) {
11043
+ if (!Number.isInteger(n) || n < 0)
11044
+ throw new TypeError('setMaxListeners(n): n must be a non-negative integer');
11045
+ this.#maxListeners = n;
11560
11046
  }
11561
11047
 
11562
11048
  /**
11563
- * Handles scroll events, calculates position-related statuses, and emits appropriate events.
11049
+ * Gets the maximum number of listeners allowed per event.
11050
+ *
11051
+ * @returns {number} The maximum number of listeners.
11564
11052
  */
11565
- _onScroll() {
11566
- if (this.#destroyed) return;
11567
- // Get values
11568
- const scrollCache = this._scrollDataUpdater();
11569
- const el = this.#target;
11570
- const scrollTop = el.scrollTop;
11571
- const scrollHeight = el.scrollHeight;
11572
- const clientHeight = el.clientHeight;
11053
+ getMaxListeners() {
11054
+ return this.#maxListeners;
11055
+ }
11056
+ }
11573
11057
 
11574
- // Prepare sroll values
11575
- const scrollResult = { scrollTop, scrollHeight, clientHeight };
11576
- let atResult = null;
11577
- let atCustomResult = null;
11058
+ /* harmony default export */ const libs_TinyEvents = (TinyEvents);
11578
11059
 
11579
- const atTop = scrollTop === 0;
11580
- const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
11060
+ ;// ./src/v1/libs/TinySmartScroller.mjs
11581
11061
 
11582
- const atCustomTop = scrollTop <= 0 + this.#extraScrollBoundary;
11583
- const atCustomBottom = scrollTop + clientHeight >= scrollHeight - 1 - this.#extraScrollBoundary;
11584
11062
 
11585
- // Scroll results
11586
- if (atTop && atBottom) atResult = 'all';
11587
- else if (atTop) atResult = 'top';
11588
- else if (atBottom) atResult = 'bottom';
11589
11063
 
11590
- if (atCustomTop && atCustomBottom) atCustomResult = 'all';
11591
- else if (atCustomTop) atCustomResult = 'top';
11592
- else if (atCustomBottom) atCustomResult = 'bottom';
11593
11064
 
11594
- this.#isAtTop = atTop;
11595
- this.#isAtBottom = atBottom;
11065
+ /**
11066
+ * Represents the dimensions of a DOM element.
11067
+ *
11068
+ * @typedef {Object} NodeSizes
11069
+ * @property {number} height - The height of the element in pixels.
11070
+ * @property {number} width - The width of the element in pixels.
11071
+ */
11596
11072
 
11597
- this.#isAtCustomTop = atCustomTop;
11598
- this.#isAtCustomBottom = atCustomBottom;
11073
+ /**
11074
+ * A callback function that receives node size change data and optionally returns a modified NodeSizes object.
11075
+ *
11076
+ * @callback NodeSizesEvent
11077
+ * @param {Element} elem - The DOM element whose size is being tracked.
11078
+ * @param {{ old: NodeSizes, now: NodeSizes }} sizes - The old and new size measurements of the element.
11079
+ * @param {{ old: number, now: number }} elemAmount - The number of matching elements before and after the update.
11080
+ * @returns {NodeSizes|undefined} A modified NodeSizes object to override the default measurement, or undefined to use the original.
11081
+ */
11599
11082
 
11600
- this.#scrollPaused = !(this.#autoScrollBottom && this.#isAtBottom);
11083
+ /**
11084
+ * A generic scroll-related event listener callback function.
11085
+ *
11086
+ * @callback ScrollListenersFunc
11087
+ * @param {any} payload - The data payload passed when the scroll event is triggered. The type may vary depending on the event.
11088
+ * @returns {void}
11089
+ */
11601
11090
 
11602
- this.#lastKnownScrollBottomOffset = scrollHeight - scrollTop - clientHeight;
11091
+ /**
11092
+ * TinySmartScroller is a utility class designed to enhance and manage scroll behaviors within containers or the window.
11093
+ *
11094
+ * It enables advanced scroll monitoring, auto-scrolling to bottom, preserving scroll position during DOM changes,
11095
+ * and detecting visibility changes of elements. This is particularly useful for dynamic UIs like chat applications,
11096
+ * feed viewers, or live content containers.
11097
+ *
11098
+ * Features:
11099
+ * - Detects when the scroll reaches the top, bottom, or custom boundaries
11100
+ * - Supports automatic scrolling to the bottom unless the user scrolls away
11101
+ * - Observes DOM mutations and resizes, and adapts scroll position accordingly
11102
+ * - Emits scroll-related events such as 'onScrollBoundary', 'onAutoScroll', and 'onScrollPause'
11103
+ * - Includes customizable scroll correction filters for layout shift mitigation
11104
+ * - Handles media element load events (e.g. `<img>`, `<iframe>`, `<video>`) to prevent sudden scroll jumps
11105
+ *
11106
+ * This class is **not framework-dependent** and works with vanilla DOM elements and the window object.
11107
+ */
11108
+ class TinySmartScroller {
11109
+ static Utils = { ...collision_namespaceObject, TinyHtml: libs_TinyHtml };
11603
11110
 
11604
- // Send results
11605
- this._emit('onScrollBoundary', { status: atResult, ...scrollResult, scrollCache });
11606
- this._emit('onExtraScrollBoundary', { status: atCustomResult, ...scrollResult, scrollCache });
11111
+ #events = new libs_TinyEvents();
11607
11112
 
11608
- if (!this.#scrollPaused) {
11609
- this._emit('onAutoScroll', { ...scrollResult, scrollCache });
11610
- } else {
11611
- this._emit('onScrollPause', { ...scrollResult, scrollCache });
11612
- }
11113
+ /**
11114
+ * Enables or disables throwing an error when the maximum number of listeners is exceeded.
11115
+ *
11116
+ * @param {boolean} shouldThrow - If true, an error will be thrown when the max is exceeded.
11117
+ */
11118
+ setThrowOnMaxListeners(shouldThrow) {
11119
+ return this.#events.setThrowOnMaxListeners(shouldThrow);
11613
11120
  }
11614
11121
 
11615
11122
  /**
11616
- * Attempts to correct the scroll position when layout shifts happen, preserving the user position if needed.
11123
+ * Checks whether an error will be thrown when the max listener limit is exceeded.
11617
11124
  *
11618
- * @param {Element[]} [targets=[]] - List of elements involved in the size change.
11125
+ * @returns {boolean} True if an error will be thrown, false if only a warning is shown.
11619
11126
  */
11620
- _fixScroll(targets = []) {
11621
- if (this.#destroyed) return;
11622
- // === Validation ===
11623
- if (!Array.isArray(targets))
11624
- throw new TypeError('_fixScroll: targets must be an array of Elements');
11625
-
11626
- // Get Scroll data
11627
- const prevScrollHeight = this.#target.scrollHeight;
11628
- const prevScrollTop = this.#target.scrollTop;
11629
- const prevBottomOffset =
11630
- this.#target.scrollHeight - this.#target.scrollTop - this.#target.clientHeight;
11127
+ getThrowOnMaxListeners() {
11128
+ return this.#events.getThrowOnMaxListeners();
11129
+ }
11631
11130
 
11632
- // Get new size
11633
- const newScrollHeight = this.#target.scrollHeight;
11634
- const heightDelta = newScrollHeight - prevScrollHeight;
11131
+ /////////////////////////////////////////////////////////////
11635
11132
 
11636
- /** @type {() => NodeSizes} */
11637
- const calculateScrollSize = () => {
11638
- // Run size getter
11639
- const scrollSize = { height: 0, width: 0 };
11640
- for (const target of targets) {
11641
- const tgOs = this.#oldSizes.get(target) || { height: 0, width: 0 };
11642
- const tgNs = this.#newSizes.get(target) || { height: 0, width: 0 };
11643
- this.#sizeFilter.forEach((fn) => {
11644
- /** @type {NodeSizes| undefined} */
11645
- const sizes = fn(
11646
- target,
11647
- { old: tgOs, now: tgNs },
11648
- { old: this.#elemOldAmount, now: this.#elemAmount },
11649
- );
11133
+ /**
11134
+ * Adds a listener to the beginning of the listeners array for the specified event.
11135
+ *
11136
+ * @param {string} event - Event name.
11137
+ * @param {ScrollListenersFunc} handler - The callback function.
11138
+ */
11139
+ prependListener(event, handler) {
11140
+ return this.#events.prependListener(event, handler);
11141
+ }
11650
11142
 
11651
- // Fix size
11652
- if (this.#newVisibles.get(target) || this.#newVisiblesByTime.get(target)) {
11653
- if (typeof sizes !== 'undefined' && typeof sizes !== 'object')
11654
- throw new Error('_fixScroll: size filter must return an object or undefined');
11655
- if (typeof sizes === 'undefined') return;
11656
- scrollSize.height = sizes.height;
11657
- scrollSize.width = sizes.width;
11658
- }
11659
- });
11660
- }
11143
+ /**
11144
+ * Adds a one-time listener to the beginning of the listeners array for the specified event.
11145
+ *
11146
+ * @param {string} event - Event name.
11147
+ * @param {ScrollListenersFunc} handler - The callback function.
11148
+ * @returns {ScrollListenersFunc} - The wrapped handler used internally.
11149
+ */
11150
+ prependListenerOnce(event, handler) {
11151
+ return this.#events.prependListenerOnce(event, handler);
11152
+ }
11661
11153
 
11662
- // Checker
11663
- if (typeof scrollSize.height !== 'number' && scrollSize.height < 0)
11664
- throw new Error('_fixScroll: invalid scrollSize.height value');
11665
- if (typeof scrollSize.width !== 'number' && scrollSize.width < 0)
11666
- throw new Error('_fixScroll: invalid scrollSize.width value');
11154
+ //////////////////////////////////////////////////////////////////////
11667
11155
 
11668
- if (scrollSize.height !== 0 || scrollSize.width !== 0) {
11669
- for (const target of targets) {
11670
- this.#newVisiblesByTime.set(target, this.#newVisibles.get(target) ?? false);
11671
- this.#oldVisiblesByTime.set(target, this.#oldVisibles.get(target) ?? false);
11672
- }
11673
- }
11156
+ /**
11157
+ * Adds a event listener.
11158
+ *
11159
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
11160
+ * @param {ScrollListenersFunc} handler - Callback function to be called when event fires.
11161
+ */
11162
+ appendListener(event, handler) {
11163
+ return this.#events.appendListener(event, handler);
11164
+ }
11674
11165
 
11675
- return scrollSize;
11676
- };
11166
+ /**
11167
+ * Registers an event listener that runs only once, then is removed.
11168
+ *
11169
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
11170
+ * @param {ScrollListenersFunc} handler - The callback function to run on event.
11171
+ * @returns {ScrollListenersFunc} - The wrapped version of the handler.
11172
+ */
11173
+ appendListenerOnce(event, handler) {
11174
+ return this.#events.appendListenerOnce(event, handler);
11175
+ }
11176
+
11177
+ /**
11178
+ * Adds a event listener.
11179
+ *
11180
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
11181
+ * @param {ScrollListenersFunc} handler - Callback function to be called when event fires.
11182
+ */
11183
+ on(event, handler) {
11184
+ return this.#events.on(event, handler);
11185
+ }
11186
+
11187
+ /**
11188
+ * Registers an event listener that runs only once, then is removed.
11189
+ *
11190
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
11191
+ * @param {ScrollListenersFunc} handler - The callback function to run on event.
11192
+ * @returns {ScrollListenersFunc} - The wrapped version of the handler.
11193
+ */
11194
+ once(event, handler) {
11195
+ return this.#events.once(event, handler);
11196
+ }
11197
+
11198
+ ////////////////////////////////////////////////////////////////////
11199
+
11200
+ /**
11201
+ * Removes a previously registered event listener.
11202
+ *
11203
+ * @param {string} event - The name of the event to remove the handler from.
11204
+ * @param {ScrollListenersFunc} handler - The specific callback function to remove.
11205
+ */
11206
+ off(event, handler) {
11207
+ return this.#events.off(event, handler);
11208
+ }
11209
+
11210
+ /**
11211
+ * Removes all event listeners of a specific type from the element.
11212
+ *
11213
+ * @param {string} event - The event type to remove (e.g. 'onScrollBoundary').
11214
+ */
11215
+ offAll(event) {
11216
+ return this.#events.offAll(event);
11217
+ }
11218
+
11219
+ /**
11220
+ * Removes all event listeners of all types from the element.
11221
+ */
11222
+ offAllTypes() {
11223
+ return this.#events.offAllTypes();
11224
+ }
11225
+
11226
+ ////////////////////////////////////////////////////////////
11227
+
11228
+ /**
11229
+ * Returns the number of listeners for a given event.
11230
+ *
11231
+ * @param {string} event - The name of the event.
11232
+ * @returns {number} Number of listeners for the event.
11233
+ */
11234
+ listenerCount(event) {
11235
+ return this.#events.listenerCount(event);
11236
+ }
11237
+
11238
+ /**
11239
+ * Returns a copy of the array of listeners for the specified event.
11240
+ *
11241
+ * @param {string} event - The name of the event.
11242
+ * @returns {ScrollListenersFunc[]} Array of listener functions.
11243
+ */
11244
+ listeners(event) {
11245
+ return this.#events.listeners(event);
11246
+ }
11247
+
11248
+ /**
11249
+ * Returns a copy of the array of listeners for the specified event.
11250
+ *
11251
+ * @param {string} event - The name of the event.
11252
+ * @returns {ScrollListenersFunc[]} Array of listener functions.
11253
+ */
11254
+ onceListeners(event) {
11255
+ return this.#events.onceListeners(event);
11256
+ }
11257
+
11258
+ /**
11259
+ * Returns a copy of the internal listeners array for the specified event,
11260
+ * including wrapper functions like those used by `.once()`.
11261
+ * @param {string | symbol} event - The event name.
11262
+ * @returns {ScrollListenersFunc[]} An array of raw listener functions.
11263
+ */
11264
+ allListeners(event) {
11265
+ return this.#events.allListeners(event);
11266
+ }
11267
+
11268
+ /**
11269
+ * Returns an array of event names for which there are registered listeners.
11270
+ *
11271
+ * @returns {string[]} Array of registered event names.
11272
+ */
11273
+ eventNames() {
11274
+ return this.#events.eventNames();
11275
+ }
11276
+
11277
+ //////////////////////////////////////////////////////
11278
+
11279
+ /**
11280
+ * Emits an event, triggering all registered handlers for that event.
11281
+ *
11282
+ * @param {string} event - The event name to emit.
11283
+ * @param {...any} payload - Optional data to pass to each handler.
11284
+ * @returns {boolean} True if any listeners were called, false otherwise.
11285
+ */
11286
+ emit(event, ...payload) {
11287
+ return this.#events.emit(event, ...payload);
11288
+ }
11289
+
11290
+ /**
11291
+ * Sets the maximum number of listeners per event before a warning is shown.
11292
+ *
11293
+ * @param {number} n - The maximum number of listeners.
11294
+ */
11295
+ setMaxListeners(n) {
11296
+ return this.#events.setMaxListeners(n);
11297
+ }
11298
+
11299
+ /**
11300
+ * Gets the maximum number of listeners allowed per event.
11301
+ *
11302
+ * @returns {number} The maximum number of listeners.
11303
+ */
11304
+ getMaxListeners() {
11305
+ return this.#events.getMaxListeners();
11306
+ }
11307
+
11308
+ ///////////////////////////////////////////////////
11309
+
11310
+ /** @type {WeakMap<Element, NodeSizes>} */
11311
+ #oldSizes = new WeakMap();
11312
+ /** @type {WeakMap<Element, NodeSizes>} */
11313
+ #newSizes = new WeakMap();
11314
+
11315
+ /** @type {WeakMap<Element, boolean>} */
11316
+ #newVisibles = new WeakMap();
11317
+ /** @type {WeakMap<Element, boolean>} */
11318
+ #oldVisibles = new WeakMap();
11319
+
11320
+ /** @type {WeakMap<Element, boolean>} */
11321
+ #newVisiblesByTime = new WeakMap();
11322
+ /** @type {WeakMap<Element, boolean>} */
11323
+ #oldVisiblesByTime = new WeakMap();
11324
+
11325
+ /** @type {ResizeObserver|null} */
11326
+ #resizeObserver = null;
11327
+ /** @type {MutationObserver|null} */
11328
+ #mutationObserver = null;
11329
+
11330
+ /** @type {Set<string>} */
11331
+ #loadTags = new Set(['IMG', 'IFRAME', 'VIDEO']);
11332
+
11333
+ /** @type {null|EventListenerOrEventListenerObject} */
11334
+ #handler = null;
11335
+
11336
+ #isAtBottom = false;
11337
+ #isAtTop = false;
11338
+ #isAtCustomTop = false;
11339
+ #isAtCustomBottom = false;
11340
+
11341
+ #querySelector = '';
11342
+ #useWindow = false;
11343
+ #destroyed = false;
11344
+ #scrollPaused = false;
11345
+ #autoScrollBottom = false;
11346
+ #observeMutations = false;
11347
+ #preserveScrollOnLayoutShift = false;
11348
+ #debounceTime = 0;
11349
+ #elemAmount = 0;
11350
+ #elemOldAmount = 0;
11351
+ #lastKnownScrollBottomOffset = 0;
11352
+ #extraScrollBoundary = 0;
11353
+
11354
+ /** @type {Set<string>} */
11355
+ #attributeFilter;
11356
+
11357
+ /** @type {Element} */
11358
+ #target;
11359
+
11360
+ /** @type {Set<NodeSizesEvent>} */
11361
+ #sizeFilter = new Set();
11362
+
11363
+ /**
11364
+ * Creates a new instance of TinySmartScroller, attaching scroll and resize observers to manage
11365
+ * automatic scroll behaviors, layout shift correction, and visibility tracking.
11366
+ *
11367
+ * @param {Element|Window} target - The scroll container to monitor. Can be an element or `window`.
11368
+ * @param {Object} [options={}] - Optional settings to configure scroll behavior.
11369
+ * @param {number} [options.extraScrollBoundary=0] - Extra margin in pixels to extend scroll boundary detection.
11370
+ * @param {boolean} [options.autoScrollBottom=true] - Whether to auto-scroll to bottom on layout updates.
11371
+ * @param {boolean} [options.observeMutations=true] - Enables MutationObserver to detect DOM changes.
11372
+ * @param {boolean} [options.preserveScrollOnLayoutShift=true] - Prevents scroll jumps when layout changes.
11373
+ * @param {number} [options.debounceTime=100] - Debounce time in milliseconds for scroll events.
11374
+ * @param {string|null} [options.querySelector=null] - Optional CSS selector to filter observed child nodes.
11375
+ * @param {string[]|Set<string>|null} [options.attributeFilter=['class', 'style', 'src', 'data-*', 'height', 'width']]
11376
+ * - Which attributes to observe for changes.
11377
+ */
11378
+ constructor(
11379
+ target,
11380
+ {
11381
+ extraScrollBoundary = 0,
11382
+ autoScrollBottom = true,
11383
+ observeMutations = true,
11384
+ preserveScrollOnLayoutShift = true,
11385
+ debounceTime = 100,
11386
+ querySelector = null,
11387
+ attributeFilter = ['class', 'style', 'src', 'data-*', 'height', 'width'],
11388
+ } = {},
11389
+ ) {
11390
+ // === target ===
11391
+ if (!(target instanceof Element || target === window))
11392
+ throw new TypeError(
11393
+ `TinySmartScroller: 'target' must be a DOM Element or 'window', but got ${typeof target}`,
11394
+ );
11395
+ // === extraScrollBoundary ===
11396
+ if (typeof extraScrollBoundary !== 'number' || Number.isNaN(extraScrollBoundary))
11397
+ throw new TypeError(
11398
+ `TinySmartScroller: 'extraScrollBoundary' must be a valid number, received ${extraScrollBoundary}`,
11399
+ );
11400
+ // === autoScrollBottom ===
11401
+ if (typeof autoScrollBottom !== 'boolean')
11402
+ throw new TypeError(
11403
+ `TinySmartScroller: 'autoScrollBottom' must be a boolean, received ${typeof autoScrollBottom}`,
11404
+ );
11405
+ // === observeMutations ===
11406
+ if (typeof observeMutations !== 'boolean')
11407
+ throw new TypeError(
11408
+ `TinySmartScroller: 'observeMutations' must be a boolean, received ${typeof observeMutations}`,
11409
+ );
11410
+ // === preserveScrollOnLayoutShift ===
11411
+ if (typeof preserveScrollOnLayoutShift !== 'boolean')
11412
+ throw new TypeError(
11413
+ `TinySmartScroller: 'preserveScrollOnLayoutShift' must be a boolean, received ${typeof preserveScrollOnLayoutShift}`,
11414
+ );
11415
+ // === debounceTime ===
11416
+ if (typeof debounceTime !== 'number' || debounceTime < 0 || Number.isNaN(debounceTime))
11417
+ throw new TypeError(
11418
+ `TinySmartScroller: 'debounceTime' must be a non-negative number, received ${debounceTime}`,
11419
+ );
11420
+ // === querySelector ===
11421
+ if (querySelector !== null && typeof querySelector !== 'string')
11422
+ throw new TypeError(
11423
+ `TinySmartScroller: 'querySelector' must be a string or null, received ${typeof querySelector}`,
11424
+ );
11425
+ // === attributeFilter ===
11426
+ const isValidAttrList =
11427
+ attributeFilter === null || Array.isArray(attributeFilter) || attributeFilter instanceof Set;
11428
+ if (!isValidAttrList)
11429
+ throw new TypeError(
11430
+ `TinySmartScroller: 'attributeFilter' must be an array, Set, or null. Got ${typeof attributeFilter}`,
11431
+ );
11432
+
11433
+ // Start values
11434
+ this.#target = target instanceof Window ? document.documentElement : target;
11435
+ this.#useWindow = target instanceof Window;
11436
+ this.#autoScrollBottom = autoScrollBottom;
11437
+ this.#observeMutations = observeMutations;
11438
+ this.#preserveScrollOnLayoutShift = preserveScrollOnLayoutShift;
11439
+ this.#debounceTime = debounceTime;
11440
+ this.#extraScrollBoundary = extraScrollBoundary;
11441
+ this.#querySelector = querySelector || '';
11442
+ this.#attributeFilter = new Set(attributeFilter || undefined);
11443
+
11444
+ // Bind scroll
11445
+ /** @type {NodeJS.Timeout} */
11446
+ let timeout;
11447
+ this.#handler = () => {
11448
+ this._scrollDataUpdater();
11449
+ clearTimeout(timeout);
11450
+ timeout = setTimeout(() => this._onScroll(), this.#debounceTime);
11451
+ };
11452
+ (this.#useWindow ? window : this.#target).addEventListener('scroll', this.#handler, {
11453
+ passive: true,
11454
+ });
11455
+
11456
+ // Mutations
11457
+ if (this.#observeMutations) {
11458
+ this._observeMutations();
11459
+ this._observeResizes(this.#target.children);
11460
+ }
11461
+
11462
+ this._scrollDataUpdater();
11463
+ }
11464
+
11465
+ /**
11466
+ * Returns a size difference callback that only reacts when height changes, filtered by tag name.
11467
+ *
11468
+ * @param {string[]} filter - List of tag names to allow. If empty, all tags are accepted.
11469
+ * @returns {NodeSizesEvent} A function that compares previous and current height, returning height delta.
11470
+ */
11471
+ getSimpleOnHeight(filter = []) {
11472
+ if (!Array.isArray(filter))
11473
+ throw new TypeError('getSimpleOnHeight(filter): filter must be an array of tag names');
11474
+ return (elem, sizes, amounts) => {
11475
+ if ((filter.length > 0 && !filter.includes(elem.tagName)) || amounts.now !== amounts.old)
11476
+ return;
11477
+ const oldSize = sizes.old;
11478
+ const newSize = sizes.now;
11479
+ const height = newSize.height - oldSize.height;
11480
+ return { height, width: 0 };
11481
+ };
11482
+ }
11483
+
11484
+ /**
11485
+ * Adds a height difference callback to the size filter system.
11486
+ *
11487
+ * @param {string[]} filter - List of tag names to allow.
11488
+ * @returns {NodeSizesEvent} The added size difference callback.
11489
+ */
11490
+ addSimpleOnHeight(filter) {
11491
+ if (!Array.isArray(filter))
11492
+ throw new TypeError('addSimpleOnHeight(filter): filter must be an array of tag names');
11493
+ const result = this.getSimpleOnHeight(filter);
11494
+ this.onSize(result);
11495
+ return result;
11496
+ }
11497
+
11498
+ /**
11499
+ * Returns a list of all currently tracked load tags.
11500
+ *
11501
+ * @returns {string[]} Array of tag names.
11502
+ */
11503
+ getLoadTags() {
11504
+ return Array.from(this.#loadTags);
11505
+ }
11506
+
11507
+ /**
11508
+ * Adds a new tag to the set of load tags.
11509
+ *
11510
+ * @param {string} tag - The tag name to add (e.g., 'IMG').
11511
+ */
11512
+ addLoadTag(tag) {
11513
+ if (typeof tag !== 'string') throw new TypeError('addLoadTag(tag): tag must be a string');
11514
+ this.#loadTags.add(tag.toUpperCase());
11515
+ }
11516
+
11517
+ /**
11518
+ * Removes a tag from the set of load tags.
11519
+ *
11520
+ * @param {string} tag - The tag name to remove.
11521
+ */
11522
+ removeLoadTag(tag) {
11523
+ if (typeof tag !== 'string') throw new TypeError('removeLoadTag(tag): tag must be a string');
11524
+ this.#loadTags.delete(tag.toUpperCase());
11525
+ }
11526
+
11527
+ /**
11528
+ * Checks whether a tag is tracked as a load tag.
11529
+ *
11530
+ * @param {string} tag - The tag name to check.
11531
+ * @returns {boolean} True if the tag is being tracked.
11532
+ */
11533
+ hasLoadTag(tag) {
11534
+ if (typeof tag !== 'string') throw new TypeError('hasLoadTag(tag): tag must be a string');
11535
+ return this.#loadTags.has(tag.toUpperCase());
11536
+ }
11537
+
11538
+ /**
11539
+ * Clears the set of load tags. If `addDefault` is true, it will reset to the default tags: 'IMG', 'IFRAME', and 'VIDEO'.
11540
+ *
11541
+ * @param {boolean} [addDefault=false] - Whether to restore the default tags after clearing.
11542
+ * @throws {TypeError} If `addDefault` is not a boolean.
11543
+ */
11544
+ resetLoadTags(addDefault = false) {
11545
+ if (typeof addDefault !== 'boolean')
11546
+ throw new TypeError('resetLoadTags(addDefault): addDefault must be a boolean');
11547
+ this.#loadTags.clear();
11548
+ if (!addDefault) return;
11549
+ this.#loadTags.add('IMG');
11550
+ this.#loadTags.add('IFRAME');
11551
+ this.#loadTags.add('VIDEO');
11552
+ }
11553
+
11554
+ /**
11555
+ * Returns a list of all currently tracked attribute filters.
11556
+ *
11557
+ * @returns {string[]} Array of attribute names.
11558
+ */
11559
+ getAttributeFilters() {
11560
+ return Array.from(this.#attributeFilter);
11561
+ }
11562
+
11563
+ /**
11564
+ * Adds an attribute to the filter list.
11565
+ *
11566
+ * @param {string} attr - The attribute name to add.
11567
+ */
11568
+ addAttributeFilter(attr) {
11569
+ if (typeof attr !== 'string')
11570
+ throw new TypeError('addAttributeFilter(attr): attr must be a string');
11571
+ this.#attributeFilter.add(attr);
11572
+ }
11573
+
11574
+ /**
11575
+ * Removes an attribute from the filter list.
11576
+ *
11577
+ * @param {string} attr - The attribute name to remove.
11578
+ */
11579
+ removeAttributeFilter(attr) {
11580
+ if (typeof attr !== 'string')
11581
+ throw new TypeError('removeAttributeFilter(attr): attr must be a string');
11582
+ this.#attributeFilter.delete(attr);
11583
+ }
11584
+
11585
+ /**
11586
+ * Checks whether a specific attribute is being filtered.
11587
+ *
11588
+ * @param {string} attr - The attribute name to check.
11589
+ * @returns {boolean} True if the attribute is being filtered.
11590
+ */
11591
+ hasAttributeFilter(attr) {
11592
+ if (typeof attr !== 'string')
11593
+ throw new TypeError('hasAttributeFilter(attr): attr must be a string');
11594
+ return this.#attributeFilter.has(attr);
11595
+ }
11596
+
11597
+ /**
11598
+ * Clears the set of observed attribute filters. If `addDefault` is true, it will reset to the default attributes:
11599
+ * 'class', 'style', 'src', 'data-*', 'height', and 'width'.
11600
+ *
11601
+ * @param {boolean} [addDefault=false] - Whether to restore the default attribute filters after clearing.
11602
+ * @throws {TypeError} If `addDefault` is not a boolean.
11603
+ */
11604
+ resetAttributeFilters(addDefault = false) {
11605
+ if (typeof addDefault !== 'boolean')
11606
+ throw new TypeError('resetAttributeFilters(addDefault): addDefault must be a boolean');
11607
+ this.#attributeFilter.clear();
11608
+ if (!addDefault) return;
11609
+ ['class', 'style', 'src', 'data-*', 'height', 'width'].forEach((attr) =>
11610
+ this.#attributeFilter.add(attr),
11611
+ );
11612
+ }
11613
+
11614
+ /**
11615
+ * Registers a custom node size change handler to the internal size filter set.
11616
+ *
11617
+ * @param {NodeSizesEvent} handler - Function that compares old and new sizes.
11618
+ */
11619
+ onSize(handler) {
11620
+ if (this.#destroyed) return;
11621
+ if (typeof handler !== 'function')
11622
+ throw new TypeError('onSize(handler): handler must be a function');
11623
+ this.#sizeFilter.add(handler);
11624
+ }
11625
+
11626
+ /**
11627
+ * Unregisters a previously registered size handler from the internal filter set.
11628
+ *
11629
+ * @param {NodeSizesEvent} handler - The handler function to remove.
11630
+ */
11631
+ offSize(handler) {
11632
+ if (this.#destroyed) return;
11633
+ if (typeof handler !== 'function')
11634
+ throw new TypeError('offSize(handler): handler must be a function');
11635
+ this.#sizeFilter.delete(handler);
11636
+ }
11637
+
11638
+ /**
11639
+ * Checks which elements inside the target are currently visible and updates internal maps.
11640
+ *
11641
+ * @returns {Map<Element, { oldIsVisible: boolean; isVisible: boolean }>} Visibility comparison results.
11642
+ */
11643
+ _scrollDataUpdater() {
11644
+ const results = new Map();
11645
+ this.#target.querySelectorAll(this.#querySelector || '*').forEach((target) => {
11646
+ const oldIsVisible = this.#newVisibles.get(target) ?? false;
11647
+ this.#oldVisibles.set(target, oldIsVisible);
11648
+
11649
+ const isVisible = libs_TinyHtml.isInContainer(this.#target, target);
11650
+ this.#newVisibles.set(target, isVisible);
11651
+
11652
+ results.set(target, { oldIsVisible, isVisible });
11653
+ });
11654
+ return results;
11655
+ }
11656
+
11657
+ /**
11658
+ * Emits a scroll-related event to all registered listeners.
11659
+ *
11660
+ * @param {string} event - Event name.
11661
+ * @param {*} [payload] - Optional event data payload.
11662
+ * @deprecated - Use emit() instead.
11663
+ */
11664
+ _emit(event, payload) {
11665
+ this.emit(event, payload);
11666
+ }
11667
+
11668
+ /**
11669
+ * Handles scroll events, calculates position-related statuses, and emits appropriate events.
11670
+ */
11671
+ _onScroll() {
11672
+ if (this.#destroyed) return;
11673
+ // Get values
11674
+ const scrollCache = this._scrollDataUpdater();
11675
+ const el = this.#target;
11676
+ const scrollTop = el.scrollTop;
11677
+ const scrollHeight = el.scrollHeight;
11678
+ const clientHeight = el.clientHeight;
11679
+
11680
+ // Prepare sroll values
11681
+ const scrollResult = { scrollTop, scrollHeight, clientHeight };
11682
+ let atResult = null;
11683
+ let atCustomResult = null;
11684
+
11685
+ const atTop = scrollTop === 0;
11686
+ const atBottom = scrollTop + clientHeight >= scrollHeight - 1;
11687
+
11688
+ const atCustomTop = scrollTop <= 0 + this.#extraScrollBoundary;
11689
+ const atCustomBottom = scrollTop + clientHeight >= scrollHeight - 1 - this.#extraScrollBoundary;
11690
+
11691
+ // Scroll results
11692
+ if (atTop && atBottom) atResult = 'all';
11693
+ else if (atTop) atResult = 'top';
11694
+ else if (atBottom) atResult = 'bottom';
11695
+
11696
+ if (atCustomTop && atCustomBottom) atCustomResult = 'all';
11697
+ else if (atCustomTop) atCustomResult = 'top';
11698
+ else if (atCustomBottom) atCustomResult = 'bottom';
11699
+
11700
+ this.#isAtTop = atTop;
11701
+ this.#isAtBottom = atBottom;
11702
+
11703
+ this.#isAtCustomTop = atCustomTop;
11704
+ this.#isAtCustomBottom = atCustomBottom;
11705
+
11706
+ this.#scrollPaused = !(this.#autoScrollBottom && this.#isAtBottom);
11707
+
11708
+ this.#lastKnownScrollBottomOffset = scrollHeight - scrollTop - clientHeight;
11709
+
11710
+ // Send results
11711
+ this.emit('onScrollBoundary', { status: atResult, ...scrollResult, scrollCache });
11712
+ this.emit('onExtraScrollBoundary', { status: atCustomResult, ...scrollResult, scrollCache });
11713
+
11714
+ if (!this.#scrollPaused) {
11715
+ this.emit('onAutoScroll', { ...scrollResult, scrollCache });
11716
+ } else {
11717
+ this.emit('onScrollPause', { ...scrollResult, scrollCache });
11718
+ }
11719
+ }
11720
+
11721
+ /**
11722
+ * Attempts to correct the scroll position when layout shifts happen, preserving the user position if needed.
11723
+ *
11724
+ * @param {Element[]} [targets=[]] - List of elements involved in the size change.
11725
+ */
11726
+ _fixScroll(targets = []) {
11727
+ if (this.#destroyed) return;
11728
+ // === Validation ===
11729
+ if (!Array.isArray(targets))
11730
+ throw new TypeError('_fixScroll: targets must be an array of Elements');
11731
+
11732
+ // Get Scroll data
11733
+ const prevScrollHeight = this.#target.scrollHeight;
11734
+ const prevScrollTop = this.#target.scrollTop;
11735
+ const prevBottomOffset =
11736
+ this.#target.scrollHeight - this.#target.scrollTop - this.#target.clientHeight;
11737
+
11738
+ // Get new size
11739
+ const newScrollHeight = this.#target.scrollHeight;
11740
+ const heightDelta = newScrollHeight - prevScrollHeight;
11741
+
11742
+ /** @type {() => NodeSizes} */
11743
+ const calculateScrollSize = () => {
11744
+ // Run size getter
11745
+ const scrollSize = { height: 0, width: 0 };
11746
+ for (const target of targets) {
11747
+ const tgOs = this.#oldSizes.get(target) || { height: 0, width: 0 };
11748
+ const tgNs = this.#newSizes.get(target) || { height: 0, width: 0 };
11749
+ this.#sizeFilter.forEach((fn) => {
11750
+ /** @type {NodeSizes| undefined} */
11751
+ const sizes = fn(
11752
+ target,
11753
+ { old: tgOs, now: tgNs },
11754
+ { old: this.#elemOldAmount, now: this.#elemAmount },
11755
+ );
11756
+
11757
+ // Fix size
11758
+ if (this.#newVisibles.get(target) || this.#newVisiblesByTime.get(target)) {
11759
+ if (typeof sizes !== 'undefined' && typeof sizes !== 'object')
11760
+ throw new Error('_fixScroll: size filter must return an object or undefined');
11761
+ if (typeof sizes === 'undefined') return;
11762
+ scrollSize.height = sizes.height;
11763
+ scrollSize.width = sizes.width;
11764
+ }
11765
+ });
11766
+ }
11767
+
11768
+ // Checker
11769
+ if (typeof scrollSize.height !== 'number' && scrollSize.height < 0)
11770
+ throw new Error('_fixScroll: invalid scrollSize.height value');
11771
+ if (typeof scrollSize.width !== 'number' && scrollSize.width < 0)
11772
+ throw new Error('_fixScroll: invalid scrollSize.width value');
11773
+
11774
+ if (scrollSize.height !== 0 || scrollSize.width !== 0) {
11775
+ for (const target of targets) {
11776
+ this.#newVisiblesByTime.set(target, this.#newVisibles.get(target) ?? false);
11777
+ this.#oldVisiblesByTime.set(target, this.#oldVisibles.get(target) ?? false);
11778
+ }
11779
+ }
11780
+
11781
+ return scrollSize;
11782
+ };
11677
11783
 
11678
11784
  // Fix scroll size
11679
11785
  if (
@@ -11691,398 +11797,1250 @@ class TinySmartScroller {
11691
11797
  this.#target.scrollLeft = this.#target.scrollLeft + scrollSize.width;
11692
11798
  }
11693
11799
 
11694
- // Normal stuff
11695
- else if (!this.#scrollPaused && this.#autoScrollBottom) {
11696
- calculateScrollSize();
11697
- this.scrollToBottom();
11698
- } else if (!this.#autoScrollBottom && !this.#isAtBottom) {
11699
- calculateScrollSize();
11700
- this.#target.scrollTop =
11701
- this.#target.scrollHeight - this.#target.clientHeight - prevBottomOffset;
11800
+ // Normal stuff
11801
+ else if (!this.#scrollPaused && this.#autoScrollBottom) {
11802
+ calculateScrollSize();
11803
+ this.scrollToBottom();
11804
+ } else if (!this.#autoScrollBottom && !this.#isAtBottom) {
11805
+ calculateScrollSize();
11806
+ this.#target.scrollTop =
11807
+ this.#target.scrollHeight - this.#target.clientHeight - prevBottomOffset;
11808
+ }
11809
+ }
11810
+ /**
11811
+ * Sets up a MutationObserver to watch for DOM changes and react accordingly to maintain scroll consistency.
11812
+ */
11813
+ _observeMutations() {
11814
+ this.#mutationObserver = new MutationObserver((mutations) => {
11815
+ if (this.#destroyed) return;
11816
+ this._scrollDataUpdater();
11817
+ this.#elemOldAmount = this.#elemAmount;
11818
+ this.#elemAmount = this.#target.childElementCount;
11819
+
11820
+ mutations.forEach((mutation) => {
11821
+ mutation.addedNodes.forEach((node) => {
11822
+ if (!(node instanceof Element) || node.nodeType !== 1) return;
11823
+
11824
+ this._observeResizes([node]);
11825
+ this._listenLoadEvents(node);
11826
+
11827
+ if (this.#querySelector) {
11828
+ const children = node.querySelectorAll(this.#querySelector);
11829
+ this._observeResizes(children);
11830
+ this._listenLoadEvents(children);
11831
+ }
11832
+ });
11833
+ });
11834
+
11835
+ this._fixScroll();
11836
+ });
11837
+
11838
+ // Install observer
11839
+ this.#mutationObserver.observe(this.#target, {
11840
+ childList: true,
11841
+ subtree: true,
11842
+ attributes: true,
11843
+ attributeFilter:
11844
+ this.#attributeFilter.size > 0 ? Array.from(this.#attributeFilter) : undefined,
11845
+ });
11846
+ }
11847
+
11848
+ /**
11849
+ * Adds a ResizeObserver to monitor elements' size changes and trigger layout adjustments.
11850
+ *
11851
+ * @param {NodeListOf<Element>|Element[]|HTMLCollection} elements - Elements to observe.
11852
+ */
11853
+ _observeResizes(elements) {
11854
+ // Add resize observer
11855
+ if (!this.#resizeObserver) {
11856
+ this.#resizeObserver = new ResizeObserver((entries) => {
11857
+ if (this.#destroyed) return;
11858
+ this._scrollDataUpdater();
11859
+ /** @type {Element[]} */
11860
+ const targets = [];
11861
+ for (const entry of entries) {
11862
+ // Target
11863
+ const target = entry.target;
11864
+
11865
+ // Update old size
11866
+ const oldSize = this.#newSizes.get(target);
11867
+ if (oldSize) this.#oldSizes.set(target, oldSize);
11868
+
11869
+ // Set new size
11870
+ const { width, height } = entry.contentRect;
11871
+ this.#newSizes.set(target, { width, height });
11872
+ targets.push(target);
11873
+ }
11874
+
11875
+ // Animation frame
11876
+ this._fixScroll(targets);
11877
+ });
11878
+ }
11879
+
11880
+ // Execute observer
11881
+ Array.from(elements).forEach((el) => {
11882
+ if (!this.#resizeObserver)
11883
+ throw new Error('_observeResizes: ResizeObserver instance is not initialized');
11884
+ this.#resizeObserver.observe(el);
11885
+ });
11886
+ }
11887
+
11888
+ /**
11889
+ * Listens for media/content load events (e.g., images, iframes, videos) to trigger scroll updates.
11890
+ *
11891
+ * @param {NodeListOf<Element>|Element} elements - Target element(s) to listen on.
11892
+ */
11893
+ _listenLoadEvents(elements) {
11894
+ if (this.#destroyed) return;
11895
+ const list = elements instanceof NodeList ? Array.from(elements) : [elements];
11896
+
11897
+ list.forEach((el) => {
11898
+ if (this.#loadTags.has(el.tagName)) {
11899
+ // @ts-ignore
11900
+ if (!el.complete) {
11901
+ el.addEventListener('load', () => {
11902
+ this._scrollDataUpdater();
11903
+ if (!this.#scrollPaused && this.#autoScrollBottom) {
11904
+ this.scrollToBottom();
11905
+ }
11906
+ });
11907
+ }
11908
+ }
11909
+ });
11910
+ }
11911
+
11912
+ /**
11913
+ * Returns the internal scroll container element being monitored.
11914
+ *
11915
+ * @returns {Element} The DOM element used as the scroll container target.
11916
+ */
11917
+ get target() {
11918
+ return this.#target;
11919
+ }
11920
+
11921
+ /**
11922
+ * Returns the previous size of a given element, or undefined if not tracked.
11923
+ *
11924
+ * @param {Element} el - The DOM element to query.
11925
+ * @returns {NodeSizes|null} The old size, or undefined.
11926
+ */
11927
+ getOldSize(el) {
11928
+ return this.#oldSizes.get(el) ?? null;
11929
+ }
11930
+
11931
+ /**
11932
+ * Returns the current size of a given element, or undefined if not tracked.
11933
+ *
11934
+ * @param {Element} el - The DOM element to query.
11935
+ * @returns {NodeSizes|null} The new size, or undefined.
11936
+ */
11937
+ getNewSize(el) {
11938
+ return this.#newSizes.get(el) ?? null;
11939
+ }
11940
+
11941
+ /**
11942
+ * Returns whether the given element was visible in the last scroll update.
11943
+ *
11944
+ * @param {Element} el - The DOM element to check.
11945
+ * @returns {boolean} True if visible, false if not, or undefined if not tracked.
11946
+ */
11947
+ wasVisible(el) {
11948
+ return this.#oldVisibles.get(el) ?? false;
11949
+ }
11950
+
11951
+ /**
11952
+ * Returns whether the given element is currently visible.
11953
+ *
11954
+ * @param {Element} el - The DOM element to check.
11955
+ * @returns {boolean} True if visible, false if not, or undefined if not tracked.
11956
+ */
11957
+ isVisible(el) {
11958
+ return this.#newVisibles.get(el) ?? false;
11959
+ }
11960
+
11961
+ /**
11962
+ * Returns whether the element was visible in the last time-based visibility check.
11963
+ *
11964
+ * @param {Element} el - The DOM element to check.
11965
+ * @returns {boolean} Visibility state from the previous timed check.
11966
+ */
11967
+ wasTimedVisible(el) {
11968
+ return this.#oldVisiblesByTime.get(el) ?? false;
11969
+ }
11970
+
11971
+ /**
11972
+ * Returns whether the element is currently visible in the time-based check.
11973
+ *
11974
+ * @param {Element} el - The DOM element to check.
11975
+ * @returns {boolean} Visibility state from the current timed check.
11976
+ */
11977
+ isTimedVisible(el) {
11978
+ return this.#newVisiblesByTime.get(el) ?? false;
11979
+ }
11980
+
11981
+ /**
11982
+ * Sets the extra scroll boundary margin used when determining if the user is at a "custom" bottom or top.
11983
+ *
11984
+ * @param {number} value - Pixels of additional margin to use.
11985
+ */
11986
+ setExtraScrollBoundary(value) {
11987
+ if (typeof value !== 'number' || Number.isNaN(value))
11988
+ throw new TypeError('setExtraScrollBoundary(value): value must be a valid number');
11989
+ this.#extraScrollBoundary = value;
11990
+ }
11991
+
11992
+ /**
11993
+ * Returns the current extra scroll boundary setting.
11994
+ *
11995
+ * @returns {number}
11996
+ */
11997
+ getExtraScrollBoundary() {
11998
+ return this.#extraScrollBoundary;
11999
+ }
12000
+
12001
+ /**
12002
+ * Returns the last known distance (in pixels) from the bottom of the scroll container.
12003
+ *
12004
+ * @returns {number}
12005
+ */
12006
+ getLastKnownScrollBottomOffset() {
12007
+ return this.#lastKnownScrollBottomOffset;
12008
+ }
12009
+
12010
+ /**
12011
+ * Forces the scroll position to move to the very bottom of the target.
12012
+ */
12013
+ scrollToBottom() {
12014
+ this.#target.scrollTop = this.#target.scrollHeight;
12015
+ }
12016
+
12017
+ /**
12018
+ * Forces the scroll position to move to the very top of the target.
12019
+ */
12020
+ scrollToTop() {
12021
+ this.#target.scrollTop = 0;
12022
+ }
12023
+
12024
+ /**
12025
+ * Checks if the user is within the defined extra scroll boundary from the bottom.
12026
+ *
12027
+ * @returns {boolean}
12028
+ */
12029
+ isUserAtCustomBottom() {
12030
+ return this.#isAtCustomBottom;
12031
+ }
12032
+
12033
+ /**
12034
+ * Checks if the user is within the defined extra scroll boundary from the top.
12035
+ *
12036
+ * @returns {boolean}
12037
+ */
12038
+ isUserAtCustomTop() {
12039
+ return this.#isAtCustomTop;
12040
+ }
12041
+
12042
+ /**
12043
+ * Returns true if the user is currently scrolled to the bottom of the element.
12044
+ *
12045
+ * @returns {boolean}
12046
+ */
12047
+ isUserAtBottom() {
12048
+ return this.#isAtBottom;
12049
+ }
12050
+
12051
+ /**
12052
+ * Returns true if the user is currently scrolled to the top of the element.
12053
+ *
12054
+ * @returns {boolean}
12055
+ */
12056
+ isUserAtTop() {
12057
+ return this.#isAtTop;
12058
+ }
12059
+
12060
+ /**
12061
+ * Returns true if automatic scrolling is currently paused.
12062
+ *
12063
+ * @returns {boolean}
12064
+ */
12065
+ isScrollPaused() {
12066
+ return this.#scrollPaused;
12067
+ }
12068
+
12069
+ /**
12070
+ * Returns whether the target is the window object.
12071
+ *
12072
+ * @returns {boolean} True if the scroll target is window, false otherwise.
12073
+ */
12074
+ isWindow() {
12075
+ return this.#useWindow;
12076
+ }
12077
+
12078
+ /**
12079
+ * Returns whether the instance has been destroyed.
12080
+ *
12081
+ * @returns {boolean} True if the instance is destroyed, false otherwise.
12082
+ */
12083
+ isDestroyed() {
12084
+ return this.#destroyed;
12085
+ }
12086
+
12087
+ /**
12088
+ * Returns whether auto-scroll-to-bottom is enabled.
12089
+ *
12090
+ * @returns {boolean} True if auto-scroll is active, false otherwise.
12091
+ */
12092
+ getAutoScrollBottom() {
12093
+ return this.#autoScrollBottom;
12094
+ }
12095
+
12096
+ /**
12097
+ * Returns whether MutationObserver is enabled.
12098
+ *
12099
+ * @returns {boolean} True if mutation observation is active, false otherwise.
12100
+ */
12101
+ getObserveMutations() {
12102
+ return this.#observeMutations;
12103
+ }
12104
+
12105
+ /**
12106
+ * Returns whether layout shift protection is enabled.
12107
+ *
12108
+ * @returns {boolean} True if scroll preservation is active, false otherwise.
12109
+ */
12110
+ getPreserveScrollOnLayoutShift() {
12111
+ return this.#preserveScrollOnLayoutShift;
12112
+ }
12113
+
12114
+ /**
12115
+ * Returns the debounce delay in milliseconds used for scroll events.
12116
+ *
12117
+ * @returns {number} Debounce delay time.
12118
+ */
12119
+ getDebounceTime() {
12120
+ return this.#debounceTime;
12121
+ }
12122
+
12123
+ /**
12124
+ * Returns the current number of matching elements observed inside the scroll target.
12125
+ *
12126
+ * @returns {number} Current count of matching elements.
12127
+ */
12128
+ getElemAmount() {
12129
+ return this.#elemAmount;
12130
+ }
12131
+
12132
+ /**
12133
+ * Returns the previous known count of matching elements from the last update.
12134
+ *
12135
+ * @returns {number} Previous count of matching elements.
12136
+ */
12137
+ getPrevElemAmount() {
12138
+ return this.#elemOldAmount;
12139
+ }
12140
+
12141
+ /**
12142
+ * Returns the query selector string used to filter observed elements.
12143
+ *
12144
+ * @returns {string} The CSS selector string, or an empty string if none was provided.
12145
+ */
12146
+ getQuerySelector() {
12147
+ return this.#querySelector;
12148
+ }
12149
+
12150
+ /**
12151
+ * Disconnects all listeners, observers, and clears memory structures.
12152
+ * Once destroyed, this instance should no longer be used.
12153
+ */
12154
+ destroy() {
12155
+ if (this.#destroyed) return;
12156
+ this.#destroyed = true;
12157
+
12158
+ // Disconnects MutationObserver
12159
+ if (this.#mutationObserver) {
12160
+ this.#mutationObserver.disconnect();
12161
+ this.#mutationObserver = null;
12162
+ }
12163
+
12164
+ // Disconnect ResizeObserver
12165
+ if (this.#resizeObserver) {
12166
+ this.#resizeObserver.disconnect();
12167
+ this.#resizeObserver = null;
11702
12168
  }
12169
+
12170
+ // Removes scroll listener
12171
+ const target = this.#useWindow ? window : this.#target;
12172
+ if (this.#handler) target.removeEventListener('scroll', this.#handler);
12173
+
12174
+ // Clean the WeakMaps
12175
+ this.#oldSizes = new WeakMap();
12176
+ this.#newSizes = new WeakMap();
12177
+ this.#newVisibles = new WeakMap();
12178
+ this.#oldVisibles = new WeakMap();
12179
+ this.#newVisiblesByTime = new WeakMap();
12180
+ this.#oldVisiblesByTime = new WeakMap();
12181
+
12182
+ // Cleans listeners and filters
12183
+ this.#events.offAllTypes();
12184
+ this.#sizeFilter.clear();
12185
+ this.#loadTags.clear();
11703
12186
  }
12187
+ }
12188
+
12189
+ /* harmony default export */ const libs_TinySmartScroller = ((/* unused pure expression or super */ null && (TinySmartScroller)));
12190
+
12191
+ ;// ./src/v1/libs/TinyLocalStorage.mjs
12192
+
12193
+
12194
+
12195
+ /** @type {Map<any, EncodeFn>} */
12196
+ const customEncoders = new Map();
12197
+
12198
+ /** @type {Map<any, DecodeFn>} */
12199
+ const customDecoders = new Map();
12200
+
12201
+ /**
12202
+ * A function that encodes a value into a serializable JSON-compatible format.
12203
+ *
12204
+ * @callback EncodeFn
12205
+ * @param {any} value - The value to encode.
12206
+ * @param {encodeSpecialJson} encodeSpecialJson - Recursive encoder helper.
12207
+ * @returns {any} The encoded value.
12208
+ */
12209
+
12210
+ /**
12211
+ * An object that defines how to check and decode a specific serialized type.
12212
+ *
12213
+ * @typedef {Object} DecodeFn
12214
+ * @property {(value: any) => any} check - Checks if the value matches the custom encoded structure.
12215
+ * @property {(value: any, decodeSpecialJson: decodeSpecialJson) => any} decode - Decodes the structure back into its original form.
12216
+ */
12217
+
12218
+ /**
12219
+ * Encodes extended JSON-compatible structures recursively.
12220
+ * @callback encodeSpecialJson
12221
+ * @param {any} value
12222
+ * @returns {any}
12223
+ */
12224
+
12225
+ /**
12226
+ * Decodes extended JSON-compatible structures recursively.
12227
+ * @callback decodeSpecialJson
12228
+ * @param {any} value
12229
+ * @returns {any}
12230
+ */
12231
+
12232
+ /**
12233
+ * Represents a value that can be safely stored and restored using JSON in `localStorage`,
12234
+ * including structures like arrays, plain objects, Map and Set.
12235
+ *
12236
+ * - `Record<string|number|symbol, any>` → plain object (e.g., `{ key: value }`)
12237
+ * - `any[]` → array of any JSON-serializable values
12238
+ * - `Map<string|number|symbol, any>` → converted to `{ __map__: true, data: [[k, v], ...] }`
12239
+ * - `Set<any>` → converted to `{ __set__: true, data: [v1, v2, ...] }`
12240
+ *
12241
+ * These conversions allow complex structures to be restored after JSON serialization.
12242
+ *
12243
+ * @typedef {(Record<string|number|symbol, any> | any[] | Map<string|number|symbol, any> | Set<any>)} LocalStorageJsonValue
12244
+ */
12245
+
12246
+ /**
12247
+ * A powerful wrapper for Web Storage (`localStorage` or `sessionStorage`) that supports
12248
+ * type-safe methods and full JSON-like structure encoding and decoding.
12249
+ *
12250
+ * `TinyLocalStorage` allows storing and retrieving complex types such as:
12251
+ * - `Map`, `Set`
12252
+ * - `Date`, `RegExp`
12253
+ * - `BigInt`, `Symbol`
12254
+ * - `undefined`, `null`
12255
+ * - Plain objects and arrays
12256
+ *
12257
+ * Includes:
12258
+ * - Type-specific `set` and `get` methods (`setDate`, `getBool`, etc.)
12259
+ * - `getValue()` to retrieve any structure regardless of type
12260
+ * - Auto-encoding/decoding with support for custom types via `registerJsonType`
12261
+ * - Built-in event system (`TinyEvents`) to listen for changes
12262
+ * - Optional fallback values on decoding errors
12263
+ *
12264
+ * Supports registering and unregistering custom types via:
12265
+ * - `registerJsonType(...)`
12266
+ * - `deleteJsonType(...)`
12267
+ *
12268
+ * This class is suitable for applications that require structured persistence in the browser.
12269
+ */
12270
+ class TinyLocalStorage {
12271
+ /** @typedef {import('./TinyEvents.mjs').handler} handler */
12272
+
12273
+ #events = new libs_TinyEvents();
12274
+
11704
12275
  /**
11705
- * Sets up a MutationObserver to watch for DOM changes and react accordingly to maintain scroll consistency.
12276
+ * Enables or disables throwing an error when the maximum number of listeners is exceeded.
12277
+ *
12278
+ * @param {boolean} shouldThrow - If true, an error will be thrown when the max is exceeded.
11706
12279
  */
11707
- _observeMutations() {
11708
- this.#mutationObserver = new MutationObserver((mutations) => {
11709
- if (this.#destroyed) return;
11710
- this._scrollDataUpdater();
11711
- this.#elemOldAmount = this.#elemAmount;
11712
- this.#elemAmount = this.#target.childElementCount;
12280
+ setThrowOnMaxListeners(shouldThrow) {
12281
+ return this.#events.setThrowOnMaxListeners(shouldThrow);
12282
+ }
11713
12283
 
11714
- mutations.forEach((mutation) => {
11715
- mutation.addedNodes.forEach((node) => {
11716
- if (!(node instanceof Element) || node.nodeType !== 1) return;
12284
+ /**
12285
+ * Checks whether an error will be thrown when the max listener limit is exceeded.
12286
+ *
12287
+ * @returns {boolean} True if an error will be thrown, false if only a warning is shown.
12288
+ */
12289
+ getThrowOnMaxListeners() {
12290
+ return this.#events.getThrowOnMaxListeners();
12291
+ }
11717
12292
 
11718
- this._observeResizes([node]);
11719
- this._listenLoadEvents(node);
12293
+ /////////////////////////////////////////////////////////////
11720
12294
 
11721
- if (this.#querySelector) {
11722
- const children = node.querySelectorAll(this.#querySelector);
11723
- this._observeResizes(children);
11724
- this._listenLoadEvents(children);
11725
- }
11726
- });
11727
- });
12295
+ /**
12296
+ * Adds a listener to the beginning of the listeners array for the specified event.
12297
+ *
12298
+ * @param {string} event - Event name.
12299
+ * @param {handler} handler - The callback function.
12300
+ */
12301
+ prependListener(event, handler) {
12302
+ return this.#events.prependListener(event, handler);
12303
+ }
11728
12304
 
11729
- this._fixScroll();
11730
- });
12305
+ /**
12306
+ * Adds a one-time listener to the beginning of the listeners array for the specified event.
12307
+ *
12308
+ * @param {string} event - Event name.
12309
+ * @param {handler} handler - The callback function.
12310
+ * @returns {handler} - The wrapped handler used internally.
12311
+ */
12312
+ prependListenerOnce(event, handler) {
12313
+ return this.#events.prependListenerOnce(event, handler);
12314
+ }
11731
12315
 
11732
- // Install observer
11733
- this.#mutationObserver.observe(this.#target, {
11734
- childList: true,
11735
- subtree: true,
11736
- attributes: true,
11737
- attributeFilter:
11738
- this.#attributeFilter.size > 0 ? Array.from(this.#attributeFilter) : undefined,
11739
- });
12316
+ //////////////////////////////////////////////////////////////////////
12317
+
12318
+ /**
12319
+ * Adds a event listener.
12320
+ *
12321
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
12322
+ * @param {handler} handler - Callback function to be called when event fires.
12323
+ */
12324
+ appendListener(event, handler) {
12325
+ return this.#events.appendListener(event, handler);
11740
12326
  }
11741
12327
 
11742
12328
  /**
11743
- * Adds a ResizeObserver to monitor elements' size changes and trigger layout adjustments.
12329
+ * Registers an event listener that runs only once, then is removed.
11744
12330
  *
11745
- * @param {NodeListOf<Element>|Element[]|HTMLCollection} elements - Elements to observe.
12331
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
12332
+ * @param {handler} handler - The callback function to run on event.
12333
+ * @returns {handler} - The wrapped version of the handler.
11746
12334
  */
11747
- _observeResizes(elements) {
11748
- // Add resize observer
11749
- if (!this.#resizeObserver) {
11750
- this.#resizeObserver = new ResizeObserver((entries) => {
11751
- if (this.#destroyed) return;
11752
- this._scrollDataUpdater();
11753
- /** @type {Element[]} */
11754
- const targets = [];
11755
- for (const entry of entries) {
11756
- // Target
11757
- const target = entry.target;
12335
+ appendListenerOnce(event, handler) {
12336
+ return this.#events.appendListenerOnce(event, handler);
12337
+ }
12338
+
12339
+ /**
12340
+ * Adds a event listener.
12341
+ *
12342
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
12343
+ * @param {handler} handler - Callback function to be called when event fires.
12344
+ */
12345
+ on(event, handler) {
12346
+ return this.#events.on(event, handler);
12347
+ }
12348
+
12349
+ /**
12350
+ * Registers an event listener that runs only once, then is removed.
12351
+ *
12352
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
12353
+ * @param {handler} handler - The callback function to run on event.
12354
+ * @returns {handler} - The wrapped version of the handler.
12355
+ */
12356
+ once(event, handler) {
12357
+ return this.#events.once(event, handler);
12358
+ }
12359
+
12360
+ ////////////////////////////////////////////////////////////////////
12361
+
12362
+ /**
12363
+ * Removes a previously registered event listener.
12364
+ *
12365
+ * @param {string} event - The name of the event to remove the handler from.
12366
+ * @param {handler} handler - The specific callback function to remove.
12367
+ */
12368
+ off(event, handler) {
12369
+ return this.#events.off(event, handler);
12370
+ }
12371
+
12372
+ /**
12373
+ * Removes all event listeners of a specific type from the element.
12374
+ *
12375
+ * @param {string} event - The event type to remove (e.g. 'onScrollBoundary').
12376
+ */
12377
+ offAll(event) {
12378
+ return this.#events.offAll(event);
12379
+ }
12380
+
12381
+ /**
12382
+ * Removes all event listeners of all types from the element.
12383
+ */
12384
+ offAllTypes() {
12385
+ return this.#events.offAllTypes();
12386
+ }
12387
+
12388
+ ////////////////////////////////////////////////////////////
12389
+
12390
+ /**
12391
+ * Returns the number of listeners for a given event.
12392
+ *
12393
+ * @param {string} event - The name of the event.
12394
+ * @returns {number} Number of listeners for the event.
12395
+ */
12396
+ listenerCount(event) {
12397
+ return this.#events.listenerCount(event);
12398
+ }
12399
+
12400
+ /**
12401
+ * Returns a copy of the array of listeners for the specified event.
12402
+ *
12403
+ * @param {string} event - The name of the event.
12404
+ * @returns {handler[]} Array of listener functions.
12405
+ */
12406
+ listeners(event) {
12407
+ return this.#events.listeners(event);
12408
+ }
12409
+
12410
+ /**
12411
+ * Returns a copy of the array of listeners for the specified event.
12412
+ *
12413
+ * @param {string} event - The name of the event.
12414
+ * @returns {handler[]} Array of listener functions.
12415
+ */
12416
+ onceListeners(event) {
12417
+ return this.#events.onceListeners(event);
12418
+ }
12419
+
12420
+ /**
12421
+ * Returns a copy of the internal listeners array for the specified event,
12422
+ * including wrapper functions like those used by `.once()`.
12423
+ * @param {string | symbol} event - The event name.
12424
+ * @returns {handler[]} An array of raw listener functions.
12425
+ */
12426
+ allListeners(event) {
12427
+ return this.#events.allListeners(event);
12428
+ }
12429
+
12430
+ /**
12431
+ * Returns an array of event names for which there are registered listeners.
12432
+ *
12433
+ * @returns {string[]} Array of registered event names.
12434
+ */
12435
+ eventNames() {
12436
+ return this.#events.eventNames();
12437
+ }
12438
+
12439
+ //////////////////////////////////////////////////////
12440
+
12441
+ /**
12442
+ * Emits an event, triggering all registered handlers for that event.
12443
+ *
12444
+ * @param {string} event - The event name to emit.
12445
+ * @param {...any} payload - Optional data to pass to each handler.
12446
+ * @returns {boolean} True if any listeners were called, false otherwise.
12447
+ */
12448
+ emit(event, ...payload) {
12449
+ return this.#events.emit(event, ...payload);
12450
+ }
12451
+
12452
+ /**
12453
+ * Sets the maximum number of listeners per event before a warning is shown.
12454
+ *
12455
+ * @param {number} n - The maximum number of listeners.
12456
+ */
12457
+ setMaxListeners(n) {
12458
+ return this.#events.setMaxListeners(n);
12459
+ }
12460
+
12461
+ /**
12462
+ * Gets the maximum number of listeners allowed per event.
12463
+ *
12464
+ * @returns {number} The maximum number of listeners.
12465
+ */
12466
+ getMaxListeners() {
12467
+ return this.#events.getMaxListeners();
12468
+ }
12469
+
12470
+ ///////////////////////////////////////////////////
12471
+
12472
+ /**
12473
+ * Registers a new JSON-serializable type with its encoder and decoder.
12474
+ *
12475
+ * @param {any} type - The type or primitive type name (e.g. `"bigint"`, `"symbol"`, etc).
12476
+ * @param {EncodeFn} encodeFn - The function that encodes the value.
12477
+ * @param {DecodeFn} decodeFn - An object with `check` and `decode` methods for restoring the value.
12478
+ */
12479
+ static registerJsonType(type, encodeFn, decodeFn) {
12480
+ customEncoders.set(type, encodeFn);
12481
+ customDecoders.set(type, decodeFn);
12482
+ }
11758
12483
 
11759
- // Update old size
11760
- const oldSize = this.#newSizes.get(target);
11761
- if (oldSize) this.#oldSizes.set(target, oldSize);
12484
+ /**
12485
+ * Removes a previously registered custom type from the encoding/decoding system.
12486
+ *
12487
+ * @param {string} type - The primitive name or constructor reference used in registration.
12488
+ */
12489
+ static deleteJsonType(type) {
12490
+ customEncoders.delete(type);
12491
+ customDecoders.delete(type);
12492
+ }
11762
12493
 
11763
- // Set new size
11764
- const { width, height } = entry.contentRect;
11765
- this.#newSizes.set(target, { width, height });
11766
- targets.push(target);
11767
- }
12494
+ //////////////////////////////////////////////////////
11768
12495
 
11769
- // Animation frame
11770
- this._fixScroll(targets);
11771
- });
12496
+ /**
12497
+ * Recursively serializes a value to a JSON-compatible format.
12498
+ *
12499
+ * This includes custom types (via `registerJsonType`), plus support for:
12500
+ * - `undefined` → `{ __undefined__: true }`
12501
+ * - `null` → `{ __null__: true }`
12502
+ *
12503
+ * @type {encodeSpecialJson}
12504
+ */
12505
+ static encodeSpecialJson(value) {
12506
+ if (typeof value === 'undefined') return { __undefined__: true };
12507
+ if (value === null) return { __null__: true };
12508
+ for (const [type, encoder] of customEncoders.entries()) {
12509
+ if ((typeof type !== 'string' && value instanceof type) || typeof value === type) {
12510
+ return encoder(value, TinyLocalStorage.encodeSpecialJson);
12511
+ }
11772
12512
  }
11773
12513
 
11774
- // Execute observer
11775
- Array.from(elements).forEach((el) => {
11776
- if (!this.#resizeObserver)
11777
- throw new Error('_observeResizes: ResizeObserver instance is not initialized');
11778
- this.#resizeObserver.observe(el);
11779
- });
12514
+ if (Array.isArray(value)) {
12515
+ return value.map(TinyLocalStorage.encodeSpecialJson);
12516
+ }
12517
+
12518
+ if (isJsonObject(value)) {
12519
+ const encoded = {};
12520
+ for (const key in value) {
12521
+ // @ts-ignore
12522
+ encoded[key] = TinyLocalStorage.encodeSpecialJson(value[key]);
12523
+ }
12524
+ return encoded;
12525
+ }
12526
+
12527
+ return value;
11780
12528
  }
11781
12529
 
11782
12530
  /**
11783
- * Listens for media/content load events (e.g., images, iframes, videos) to trigger scroll updates.
12531
+ * Recursively deserializes a JSON-compatible value into its original structure.
11784
12532
  *
11785
- * @param {NodeListOf<Element>|Element} elements - Target element(s) to listen on.
12533
+ * Automatically handles:
12534
+ * - `__undefined__` → `undefined`
12535
+ * - `__null__` → `null`
12536
+ * - Any type registered via `registerJsonType`
12537
+ *
12538
+ * @type {decodeSpecialJson}
11786
12539
  */
11787
- _listenLoadEvents(elements) {
11788
- if (this.#destroyed) return;
11789
- const list = elements instanceof NodeList ? Array.from(elements) : [elements];
12540
+ static decodeSpecialJson(value) {
12541
+ const isJson = isJsonObject(value);
12542
+ if (isJson) {
12543
+ if (value.__undefined__) return undefined;
12544
+ if (value.__null__) return null;
12545
+ }
11790
12546
 
11791
- list.forEach((el) => {
11792
- if (this.#loadTags.has(el.tagName)) {
11793
- // @ts-ignore
11794
- if (!el.complete) {
11795
- el.addEventListener('load', () => {
11796
- this._scrollDataUpdater();
11797
- if (!this.#scrollPaused && this.#autoScrollBottom) {
11798
- this.scrollToBottom();
11799
- }
11800
- });
12547
+ if (Array.isArray(value)) {
12548
+ return value.map(TinyLocalStorage.decodeSpecialJson);
12549
+ }
12550
+
12551
+ if (isJson) {
12552
+ for (const [type, decoder] of customDecoders.entries()) {
12553
+ if (decoder.check && decoder.check(value)) {
12554
+ return decoder.decode(value, TinyLocalStorage.decodeSpecialJson);
11801
12555
  }
11802
12556
  }
11803
- });
12557
+
12558
+ const decoded = {};
12559
+ for (const key in value) {
12560
+ // @ts-ignore
12561
+ decoded[key] = TinyLocalStorage.decodeSpecialJson(value[key]);
12562
+ }
12563
+ return decoded;
12564
+ }
12565
+
12566
+ return value;
11804
12567
  }
11805
12568
 
12569
+ //////////////////////////////////////////////////////
12570
+
12571
+ /** @type {Storage} */
12572
+ #localStorage = window.localStorage;
12573
+
12574
+ /** @type {(ev: StorageEvent) => any} */
12575
+ #storageEvent = (ev) => this.emit('storage', ev);
12576
+
11806
12577
  /**
11807
- * Returns the internal scroll container element being monitored.
12578
+ * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
11808
12579
  *
11809
- * @returns {Element} The DOM element used as the scroll container target.
12580
+ * Adds listener for the native `storage` event to support tab synchronization.
11810
12581
  */
11811
- get target() {
11812
- return this.#target;
12582
+ constructor() {
12583
+ window.addEventListener('storage', this.#storageEvent);
11813
12584
  }
11814
12585
 
11815
12586
  /**
11816
- * Returns the previous size of a given element, or undefined if not tracked.
12587
+ * Defines a custom storage interface (e.g. `sessionStorage`).
11817
12588
  *
11818
- * @param {Element} el - The DOM element to query.
11819
- * @returns {NodeSizes|null} The old size, or undefined.
12589
+ * @param {Storage} localstorage - A valid Storage object (localStorage or sessionStorage).
11820
12590
  */
11821
- getOldSize(el) {
11822
- return this.#oldSizes.get(el) ?? null;
12591
+ setLocalStorage(localstorage) {
12592
+ if (!(localstorage instanceof Storage))
12593
+ throw new Error('Argument must be a valid instance of Storage.');
12594
+ this.#localStorage = localstorage;
11823
12595
  }
11824
12596
 
11825
12597
  /**
11826
- * Returns the current size of a given element, or undefined if not tracked.
12598
+ * Checks if `localStorage` is supported by the current environment.
11827
12599
  *
11828
- * @param {Element} el - The DOM element to query.
11829
- * @returns {NodeSizes|null} The new size, or undefined.
12600
+ * @returns {boolean} True if `localStorage` exists, false otherwise.
11830
12601
  */
11831
- getNewSize(el) {
11832
- return this.#newSizes.get(el) ?? null;
12602
+ localStorageExists() {
12603
+ return this.#localStorage instanceof Storage;
11833
12604
  }
11834
12605
 
11835
12606
  /**
11836
- * Returns whether the given element was visible in the last scroll update.
12607
+ * Automatically serializes nested instances.
11837
12608
  *
11838
- * @param {Element} el - The DOM element to check.
11839
- * @returns {boolean} True if visible, false if not, or undefined if not tracked.
12609
+ * @param {string} name - The key under which to store the data.
12610
+ * @param {*} data - The data to be serialized.
12611
+ * @returns {*}
11840
12612
  */
11841
- wasVisible(el) {
11842
- return this.#oldVisibles.get(el) ?? false;
12613
+ #setJson(name, data) {
12614
+ if (typeof name !== 'string' || !name.length)
12615
+ throw new Error('Key must be a non-empty string.');
12616
+ return TinyLocalStorage.encodeSpecialJson(data);
11843
12617
  }
11844
12618
 
11845
12619
  /**
11846
- * Returns whether the given element is currently visible.
12620
+ * Stores a JSON-compatible value in `localStorage`.
11847
12621
  *
11848
- * @param {Element} el - The DOM element to check.
11849
- * @returns {boolean} True if visible, false if not, or undefined if not tracked.
12622
+ * Automatically serializes nested `Map` and `Set` instances.
12623
+ *
12624
+ * @param {string} name - The key under which to store the data.
12625
+ * @param {LocalStorageJsonValue} data - The data to be serialized and stored.
11850
12626
  */
11851
- isVisible(el) {
11852
- return this.#newVisibles.get(el) ?? false;
12627
+ setJson(name, data) {
12628
+ if (
12629
+ !isJsonObject(data) &&
12630
+ !Array.isArray(data) &&
12631
+ !(data instanceof Map) &&
12632
+ !(data instanceof Set)
12633
+ ) {
12634
+ throw new Error('The storage value is not a valid JSON-compatible structure.');
12635
+ }
12636
+ const encoded = this.#setJson(name, data);
12637
+ this.emit('setJson', name, data);
12638
+ this.#localStorage.setItem(name, JSON.stringify(encoded));
11853
12639
  }
11854
12640
 
11855
12641
  /**
11856
- * Returns whether the element was visible in the last time-based visibility check.
12642
+ * Retrieves a value from `localStorage`.
11857
12643
  *
11858
- * @param {Element} el - The DOM element to check.
11859
- * @returns {boolean} Visibility state from the previous timed check.
12644
+ * Automatically restores nested instances.
12645
+ *
12646
+ * @param {string} name - The key to retrieve.
12647
+ * @param {'array'|'obj'|'map'|'set'|'null'} [defaultData] - Default fallback format if value is invalid.
12648
+ * @returns {{ decoded: any, fallback: any }} The parsed object or fallback.
11860
12649
  */
11861
- wasTimedVisible(el) {
11862
- return this.#oldVisiblesByTime.get(el) ?? false;
12650
+ #getJson(name, defaultData) {
12651
+ if (typeof name !== 'string' || !name.length)
12652
+ throw new Error('Key must be a non-empty string.');
12653
+
12654
+ const raw = this.#localStorage.getItem(name);
12655
+ const fallbackTypes = {
12656
+ obj: () => ({}),
12657
+ array: () => [],
12658
+ map: () => new Map(),
12659
+ set: () => new Set(),
12660
+ };
12661
+
12662
+ const fallback =
12663
+ // @ts-ignore
12664
+ typeof fallbackTypes[defaultData] === 'function' ? fallbackTypes[defaultData]() : null;
12665
+
12666
+ let parsed;
12667
+
12668
+ try {
12669
+ // @ts-ignore
12670
+ parsed = JSON.parse(raw);
12671
+ } catch {
12672
+ // @ts-ignore
12673
+ return fallback;
12674
+ }
12675
+
12676
+ return { decoded: TinyLocalStorage.decodeSpecialJson(parsed), fallback };
11863
12677
  }
11864
12678
 
11865
12679
  /**
11866
- * Returns whether the element is currently visible in the time-based check.
12680
+ * Retrieves and parses a JSON value from `localStorage`.
11867
12681
  *
11868
- * @param {Element} el - The DOM element to check.
11869
- * @returns {boolean} Visibility state from the current timed check.
12682
+ * Automatically restores nested `Map` and `Set` instances.
12683
+ *
12684
+ * @param {string} name - The key to retrieve.
12685
+ * @param {'array'|'obj'|'map'|'set'|'null'} [defaultData] - Default fallback format if value is invalid.
12686
+ * @returns {LocalStorageJsonValue|null} The parsed object or fallback.
11870
12687
  */
11871
- isTimedVisible(el) {
11872
- return this.#newVisiblesByTime.get(el) ?? false;
12688
+ getJson(name, defaultData) {
12689
+ const { decoded, fallback } = this.#getJson(name, defaultData);
12690
+ if (
12691
+ decoded instanceof Map ||
12692
+ decoded instanceof Set ||
12693
+ Array.isArray(decoded) ||
12694
+ isJsonObject(decoded)
12695
+ )
12696
+ return decoded;
12697
+ return fallback;
11873
12698
  }
11874
12699
 
11875
12700
  /**
11876
- * Sets the extra scroll boundary margin used when determining if the user is at a "custom" bottom or top.
11877
- *
11878
- * @param {number} value - Pixels of additional margin to use.
12701
+ * Stores a Date in localStorage.
12702
+ * @param {string} name
12703
+ * @param {Date} data
11879
12704
  */
11880
- setExtraScrollBoundary(value) {
11881
- if (typeof value !== 'number' || Number.isNaN(value))
11882
- throw new TypeError('setExtraScrollBoundary(value): value must be a valid number');
11883
- this.#extraScrollBoundary = value;
12705
+ setDate(name, data) {
12706
+ if (!(data instanceof Date)) throw new Error('Value must be a Date.');
12707
+ const encoded = this.#setJson(name, data);
12708
+ this.#localStorage.setItem(name, JSON.stringify(encoded));
12709
+ this.emit('setDate', name, data);
11884
12710
  }
11885
12711
 
11886
12712
  /**
11887
- * Returns the current extra scroll boundary setting.
11888
- *
11889
- * @returns {number}
12713
+ * Retrieves a Date from localStorage.
12714
+ * @param {string} name
12715
+ * @returns {Date|null}
11890
12716
  */
11891
- getExtraScrollBoundary() {
11892
- return this.#extraScrollBoundary;
12717
+ getDate(name) {
12718
+ const value = this.#getJson(name).decoded;
12719
+ return value instanceof Date ? value : null;
11893
12720
  }
11894
12721
 
11895
12722
  /**
11896
- * Returns the last known distance (in pixels) from the bottom of the scroll container.
11897
- *
11898
- * @returns {number}
12723
+ * Stores a RegExp in localStorage.
12724
+ * @param {string} name
12725
+ * @param {RegExp} data
11899
12726
  */
11900
- getLastKnownScrollBottomOffset() {
11901
- return this.#lastKnownScrollBottomOffset;
12727
+ setRegExp(name, data) {
12728
+ if (!(data instanceof RegExp)) throw new Error('Value must be a RegExp.');
12729
+ const encoded = this.#setJson(name, data);
12730
+ this.#localStorage.setItem(name, JSON.stringify(encoded));
12731
+ this.emit('setRegExp', name, data);
11902
12732
  }
11903
12733
 
11904
12734
  /**
11905
- * Forces the scroll position to move to the very bottom of the target.
12735
+ * Retrieves a RegExp from localStorage.
12736
+ * @param {string} name
12737
+ * @returns {RegExp|null}
11906
12738
  */
11907
- scrollToBottom() {
11908
- this.#target.scrollTop = this.#target.scrollHeight;
12739
+ getRegExp(name) {
12740
+ const value = this.#getJson(name).decoded;
12741
+ return value instanceof RegExp ? value : null;
11909
12742
  }
11910
12743
 
11911
12744
  /**
11912
- * Forces the scroll position to move to the very top of the target.
12745
+ * Stores a BigInt in localStorage.
12746
+ * @param {string} name
12747
+ * @param {bigint} data
11913
12748
  */
11914
- scrollToTop() {
11915
- this.#target.scrollTop = 0;
12749
+ setBigInt(name, data) {
12750
+ if (typeof data !== 'bigint') throw new Error('Value must be a BigInt.');
12751
+ const encoded = this.#setJson(name, data);
12752
+ this.#localStorage.setItem(name, JSON.stringify(encoded));
12753
+ this.emit('setBigInt', name, data);
11916
12754
  }
11917
12755
 
11918
12756
  /**
11919
- * Checks if the user is within the defined extra scroll boundary from the bottom.
11920
- *
11921
- * @returns {boolean}
12757
+ * Retrieves a BigInt from localStorage.
12758
+ * @param {string} name
12759
+ * @returns {bigint|null}
11922
12760
  */
11923
- isUserAtCustomBottom() {
11924
- return this.#isAtCustomBottom;
12761
+ getBigInt(name) {
12762
+ const value = this.#getJson(name).decoded;
12763
+ return typeof value === 'bigint' ? value : null;
11925
12764
  }
11926
12765
 
11927
12766
  /**
11928
- * Checks if the user is within the defined extra scroll boundary from the top.
11929
- *
11930
- * @returns {boolean}
12767
+ * Stores a Symbol in localStorage.
12768
+ * Only global symbols (`Symbol.for`) will preserve the key.
12769
+ * @param {string} name
12770
+ * @param {symbol} data
11931
12771
  */
11932
- isUserAtCustomTop() {
11933
- return this.#isAtCustomTop;
12772
+ setSymbol(name, data) {
12773
+ if (typeof data !== 'symbol') throw new Error('Value must be a Symbol.');
12774
+ const encoded = this.#setJson(name, data);
12775
+ this.#localStorage.setItem(name, JSON.stringify(encoded));
12776
+ this.emit('setSymbol', name, data);
11934
12777
  }
11935
12778
 
11936
12779
  /**
11937
- * Returns true if the user is currently scrolled to the bottom of the element.
11938
- *
11939
- * @returns {boolean}
12780
+ * Retrieves a Symbol from localStorage.
12781
+ * @param {string} name
12782
+ * @returns {symbol|null}
11940
12783
  */
11941
- isUserAtBottom() {
11942
- return this.#isAtBottom;
12784
+ getSymbol(name) {
12785
+ const value = this.#getJson(name).decoded;
12786
+ return typeof value === 'symbol' ? value : null;
11943
12787
  }
11944
12788
 
11945
12789
  /**
11946
- * Returns true if the user is currently scrolled to the top of the element.
12790
+ * Retrieves a value from `localStorage`.
11947
12791
  *
11948
- * @returns {boolean}
12792
+ * @param {string} name - The key to retrieve.
12793
+ * @returns {any} The stored value or null if not found.
11949
12794
  */
11950
- isUserAtTop() {
11951
- return this.#isAtTop;
12795
+ getValue(name) {
12796
+ return this.#getJson(name).decoded ?? null;
11952
12797
  }
11953
12798
 
11954
12799
  /**
11955
- * Returns true if automatic scrolling is currently paused.
12800
+ * Stores a raw string value in `localStorage`.
11956
12801
  *
11957
- * @returns {boolean}
12802
+ * @param {string} name - The key to use.
12803
+ * @param {any} data - The data to store.
11958
12804
  */
11959
- isScrollPaused() {
11960
- return this.#scrollPaused;
12805
+ setItem(name, data) {
12806
+ if (typeof name !== 'string' || !name.length)
12807
+ throw new Error('Key must be a non-empty string.');
12808
+ this.emit('setItem', name, data);
12809
+ return this.#localStorage.setItem(name, data);
11961
12810
  }
11962
12811
 
11963
12812
  /**
11964
- * Returns whether the target is the window object.
12813
+ * Retrieves a raw string value from `localStorage`.
11965
12814
  *
11966
- * @returns {boolean} True if the scroll target is window, false otherwise.
12815
+ * @param {string} name - The key to retrieve.
12816
+ * @returns {string|null} The stored value or null if not found.
11967
12817
  */
11968
- isWindow() {
11969
- return this.#useWindow;
12818
+ getItem(name) {
12819
+ if (typeof name !== 'string' || !name.length)
12820
+ throw new Error('Key must be a non-empty string.');
12821
+ return this.#localStorage.getItem(name);
11970
12822
  }
11971
12823
 
11972
12824
  /**
11973
- * Returns whether the instance has been destroyed.
12825
+ * Stores a string in `localStorage`, ensuring the data is a valid string.
11974
12826
  *
11975
- * @returns {boolean} True if the instance is destroyed, false otherwise.
12827
+ * @param {string} name - The key to store the string under.
12828
+ * @param {string} data - The string to store.
11976
12829
  */
11977
- isDestroyed() {
11978
- return this.#destroyed;
12830
+ setString(name, data) {
12831
+ if (typeof name !== 'string' || !name.length)
12832
+ throw new Error('Key must be a non-empty string.');
12833
+ if (typeof data !== 'string') throw new Error('Value must be a string.');
12834
+
12835
+ this.emit('setString', name, data);
12836
+ return this.#localStorage.setItem(name, data);
11979
12837
  }
11980
12838
 
11981
12839
  /**
11982
- * Returns whether auto-scroll-to-bottom is enabled.
12840
+ * Retrieves a string value from `localStorage`.
11983
12841
  *
11984
- * @returns {boolean} True if auto-scroll is active, false otherwise.
12842
+ * @param {string} name - The key to retrieve.
12843
+ * @returns {string|null} The string if valid, or null.
11985
12844
  */
11986
- getAutoScrollBottom() {
11987
- return this.#autoScrollBottom;
12845
+ getString(name) {
12846
+ if (typeof name !== 'string' || !name.length)
12847
+ throw new Error('Key must be a non-empty string.');
12848
+ let value = this.#localStorage.getItem(name);
12849
+ if (typeof value === 'string') return value;
12850
+ return null;
11988
12851
  }
11989
12852
 
11990
12853
  /**
11991
- * Returns whether MutationObserver is enabled.
12854
+ * Stores a number value in `localStorage`.
11992
12855
  *
11993
- * @returns {boolean} True if mutation observation is active, false otherwise.
12856
+ * @param {string} name - The key to use.
12857
+ * @param {number} data - The number to store.
11994
12858
  */
11995
- getObserveMutations() {
11996
- return this.#observeMutations;
12859
+ setNumber(name, data) {
12860
+ if (typeof name !== 'string' || !name.length)
12861
+ throw new Error('Key must be a non-empty string.');
12862
+ if (typeof data !== 'number') throw new Error('Value must be a number.');
12863
+ this.emit('setNumber', name, data);
12864
+ return this.#localStorage.setItem(name, String(data));
11997
12865
  }
11998
12866
 
11999
12867
  /**
12000
- * Returns whether layout shift protection is enabled.
12868
+ * Retrieves a number from `localStorage`.
12001
12869
  *
12002
- * @returns {boolean} True if scroll preservation is active, false otherwise.
12870
+ * @param {string} name - The key to retrieve.
12871
+ * @returns {number|null} The number or null if invalid.
12003
12872
  */
12004
- getPreserveScrollOnLayoutShift() {
12005
- return this.#preserveScrollOnLayoutShift;
12873
+ getNumber(name) {
12874
+ if (typeof name !== 'string' || !name.length)
12875
+ throw new Error('Key must be a non-empty string.');
12876
+
12877
+ /** @type {number|string|null} */
12878
+ let number = this.#localStorage.getItem(name);
12879
+ if (typeof number === 'number') return number;
12880
+ if (typeof number === 'string' && number.length > 0) {
12881
+ number = parseFloat(number);
12882
+ if (!Number.isNaN(number)) return number;
12883
+ }
12884
+ return null;
12006
12885
  }
12007
12886
 
12008
12887
  /**
12009
- * Returns the debounce delay in milliseconds used for scroll events.
12888
+ * Stores a boolean value in `localStorage`.
12010
12889
  *
12011
- * @returns {number} Debounce delay time.
12890
+ * @param {string} name - The key to use.
12891
+ * @param {boolean} data - The boolean value to store.
12012
12892
  */
12013
- getDebounceTime() {
12014
- return this.#debounceTime;
12893
+ setBool(name, data) {
12894
+ if (typeof name !== 'string' || !name.length)
12895
+ throw new Error('Key must be a non-empty string.');
12896
+ if (typeof data !== 'boolean') throw new Error('Value must be a boolean.');
12897
+ this.emit('setBool', name, data);
12898
+ return this.#localStorage.setItem(name, String(data));
12015
12899
  }
12016
12900
 
12017
12901
  /**
12018
- * Returns the current number of matching elements observed inside the scroll target.
12902
+ * Retrieves a boolean value from `localStorage`.
12019
12903
  *
12020
- * @returns {number} Current count of matching elements.
12904
+ * @param {string} name - The key to retrieve.
12905
+ * @returns {boolean|null} The boolean or null if invalid.
12021
12906
  */
12022
- getElemAmount() {
12023
- return this.#elemAmount;
12907
+ getBool(name) {
12908
+ if (typeof name !== 'string' || !name.length)
12909
+ throw new Error('Key must be a non-empty string.');
12910
+
12911
+ const value = this.#localStorage.getItem(name);
12912
+ if (typeof value === 'boolean') return value;
12913
+ if (typeof value === 'string') {
12914
+ if (value === 'true') return true;
12915
+ if (value === 'false') return false;
12916
+ }
12917
+
12918
+ return null;
12024
12919
  }
12025
12920
 
12026
12921
  /**
12027
- * Returns the previous known count of matching elements from the last update.
12922
+ * Removes a value from `localStorage`.
12028
12923
  *
12029
- * @returns {number} Previous count of matching elements.
12924
+ * @param {string} name - The key to remove.
12030
12925
  */
12031
- getPrevElemAmount() {
12032
- return this.#elemOldAmount;
12926
+ removeItem(name) {
12927
+ if (typeof name !== 'string' || !name.length)
12928
+ throw new Error('Key must be a non-empty string.');
12929
+
12930
+ this.emit('removeItem', name);
12931
+ return this.#localStorage.removeItem(name);
12033
12932
  }
12034
12933
 
12035
12934
  /**
12036
- * Returns the query selector string used to filter observed elements.
12037
- *
12038
- * @returns {string} The CSS selector string, or an empty string if none was provided.
12935
+ * Clears all data from `localStorage`.
12039
12936
  */
12040
- getQuerySelector() {
12041
- return this.#querySelector;
12937
+ clearLocalStorage() {
12938
+ this.#localStorage.clear();
12042
12939
  }
12043
12940
 
12044
12941
  /**
12045
- * Disconnects all listeners, observers, and clears memory structures.
12046
- * Once destroyed, this instance should no longer be used.
12942
+ * Destroys the storage instance by removing the storage event listener.
12047
12943
  */
12048
12944
  destroy() {
12049
- if (this.#destroyed) return;
12050
- this.#destroyed = true;
12945
+ window.removeEventListener('storage', this.#storageEvent);
12946
+ this.#events.offAllTypes();
12947
+ }
12948
+ }
12051
12949
 
12052
- // Disconnects MutationObserver
12053
- if (this.#mutationObserver) {
12054
- this.#mutationObserver.disconnect();
12055
- this.#mutationObserver = null;
12056
- }
12950
+ // First registers
12057
12951
 
12058
- // Disconnect ResizeObserver
12059
- if (this.#resizeObserver) {
12060
- this.#resizeObserver.disconnect();
12061
- this.#resizeObserver = null;
12062
- }
12952
+ // Map
12953
+ TinyLocalStorage.registerJsonType(
12954
+ Map,
12955
+ (value, encodeSpecialJson) => ({
12956
+ __map__: true,
12957
+ data: Array.from(value.entries()).map(([k, v]) => [k, encodeSpecialJson(v)]),
12958
+ }),
12959
+ {
12960
+ check: (value) => value.__map__,
12961
+ /** @param {{ data: any[] }} value */
12962
+ decode: (value, decodeSpecialJson) =>
12963
+ new Map(value.data.map(([k, v]) => [k, decodeSpecialJson(v)])),
12964
+ },
12965
+ );
12966
+
12967
+ // Set
12968
+ TinyLocalStorage.registerJsonType(
12969
+ Set,
12970
+ (value, encodeSpecialJson) => ({
12971
+ __set__: true,
12972
+ data: Array.from(value).map(encodeSpecialJson),
12973
+ }),
12974
+ {
12975
+ check: (value) => value.__set__,
12976
+ decode: (value, decodeSpecialJson) => new Set(value.data.map(decodeSpecialJson)),
12977
+ },
12978
+ );
12979
+
12980
+ // Date
12981
+ TinyLocalStorage.registerJsonType(
12982
+ Date,
12983
+ (value) => ({
12984
+ __date__: true,
12985
+ value: value.toISOString(),
12986
+ }),
12987
+ {
12988
+ check: (value) => value.__date__,
12989
+ decode: (value) => new Date(value.value),
12990
+ },
12991
+ );
12992
+
12993
+ // Regex
12994
+ TinyLocalStorage.registerJsonType(
12995
+ RegExp,
12996
+ (value) => ({
12997
+ __regexp__: true,
12998
+ source: value.source,
12999
+ flags: value.flags,
13000
+ }),
13001
+ {
13002
+ check: (value) => value.__regexp__,
13003
+ decode: (value) => new RegExp(value.source, value.flags),
13004
+ },
13005
+ );
13006
+
13007
+ // Big Int
13008
+ TinyLocalStorage.registerJsonType(
13009
+ 'bigint',
13010
+ (value) => ({
13011
+ __bigint__: true,
13012
+ value: value.toString(),
13013
+ }),
13014
+ {
13015
+ check: (value) => value.__bigint__,
13016
+ decode: (value) => BigInt(value.value),
13017
+ },
13018
+ );
13019
+
13020
+ // Symbol
13021
+ TinyLocalStorage.registerJsonType(
13022
+ 'symbol',
13023
+ (value) => ({
13024
+ __symbol__: true,
13025
+ key: Symbol.keyFor(value) ?? value.description ?? null,
13026
+ }),
13027
+ {
13028
+ check: (value) => value.__symbol__,
13029
+ decode: (value) => {
13030
+ const key = value.key;
13031
+ return key != null ? Symbol.for(key) : Symbol();
13032
+ },
13033
+ },
13034
+ );
13035
+
13036
+ /* harmony default export */ const libs_TinyLocalStorage = ((/* unused pure expression or super */ null && (TinyLocalStorage)));
13037
+
13038
+ ;// ./src/v1/index.mjs
12063
13039
 
12064
- // Removes scroll listener
12065
- const target = this.#useWindow ? window : this.#target;
12066
- if (this.#handler) target.removeEventListener('scroll', this.#handler);
12067
13040
 
12068
- // Clean the WeakMaps
12069
- this.#oldSizes = new WeakMap();
12070
- this.#newSizes = new WeakMap();
12071
- this.#newVisibles = new WeakMap();
12072
- this.#oldVisibles = new WeakMap();
12073
- this.#newVisiblesByTime = new WeakMap();
12074
- this.#oldVisiblesByTime = new WeakMap();
12075
13041
 
12076
- // Cleans listeners and filters
12077
- this.#scrollListeners = {};
12078
- this.#sizeFilter.clear();
12079
- this.#loadTags.clear();
12080
- }
12081
- }
12082
13042
 
12083
- /* harmony default export */ const libs_TinySmartScroller = ((/* unused pure expression or super */ null && (TinySmartScroller)));
12084
13043
 
12085
- ;// ./src/v1/index.mjs
12086
13044
 
12087
13045
 
12088
13046
 
@@ -12193,7 +13151,7 @@ class TinyUploadClicker {
12193
13151
  * @throws {TypeError} If the config is invalid or required options are missing.
12194
13152
  */
12195
13153
  constructor(options) {
12196
- if (!objFilter_isJsonObject(options))
13154
+ if (!isJsonObject(options))
12197
13155
  throw new TypeError('TinyUploadClicker: "options" must be a valid object.');
12198
13156
 
12199
13157
  this.#config = {
@@ -12259,10 +13217,10 @@ class TinyUploadClicker {
12259
13217
  )
12260
13218
  throw new TypeError('TinyUploadClicker: "onFileLoad" must be a function or null.');
12261
13219
 
12262
- if (options.inputAttributes !== undefined && !objFilter_isJsonObject(options.inputAttributes))
13220
+ if (options.inputAttributes !== undefined && !isJsonObject(options.inputAttributes))
12263
13221
  throw new TypeError('TinyUploadClicker: "inputAttributes" must be an object.');
12264
13222
 
12265
- if (options.inputStyles !== undefined && !objFilter_isJsonObject(options.inputStyles))
13223
+ if (options.inputStyles !== undefined && !isJsonObject(options.inputStyles))
12266
13224
  throw new TypeError('TinyUploadClicker: "inputStyles" must be an object.');
12267
13225
 
12268
13226
  this.#boundClick = this.#handleClick.bind(this);