tiny-essentials 1.13.2 → 1.15.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 (36) hide show
  1. package/dist/v1/TinyBasicsEs.js +656 -26
  2. package/dist/v1/TinyBasicsEs.min.js +1 -1
  3. package/dist/v1/TinyDragger.js +196 -26
  4. package/dist/v1/TinyEssentials.js +896 -26
  5. package/dist/v1/TinyEssentials.min.js +1 -1
  6. package/dist/v1/TinyNotifications.js +408 -0
  7. package/dist/v1/TinyNotifications.min.js +1 -0
  8. package/dist/v1/TinyUploadClicker.js +198 -26
  9. package/dist/v1/basics/collision.cjs +413 -0
  10. package/dist/v1/basics/collision.d.mts +187 -0
  11. package/dist/v1/basics/collision.mjs +350 -0
  12. package/dist/v1/basics/html.cjs +201 -26
  13. package/dist/v1/basics/html.d.mts +71 -7
  14. package/dist/v1/basics/html.mjs +186 -23
  15. package/dist/v1/basics/index.cjs +24 -0
  16. package/dist/v1/basics/index.d.mts +24 -1
  17. package/dist/v1/basics/index.mjs +4 -3
  18. package/dist/v1/basics/text.cjs +43 -0
  19. package/dist/v1/basics/text.d.mts +16 -0
  20. package/dist/v1/basics/text.mjs +37 -0
  21. package/dist/v1/build/TinyNotifications.cjs +7 -0
  22. package/dist/v1/build/TinyNotifications.d.mts +3 -0
  23. package/dist/v1/build/TinyNotifications.mjs +2 -0
  24. package/dist/v1/index.cjs +26 -0
  25. package/dist/v1/index.d.mts +25 -1
  26. package/dist/v1/index.mjs +5 -3
  27. package/dist/v1/libs/TinyNotifications.cjs +238 -0
  28. package/dist/v1/libs/TinyNotifications.d.mts +106 -0
  29. package/dist/v1/libs/TinyNotifications.mjs +211 -0
  30. package/docs/v1/README.md +5 -3
  31. package/docs/v1/basics/array.md +16 -43
  32. package/docs/v1/basics/collision.md +237 -0
  33. package/docs/v1/basics/html.md +117 -28
  34. package/docs/v1/basics/text.md +44 -14
  35. package/docs/v1/libs/TinyNotifications.md +189 -0
  36. package/package.json +5 -2
@@ -3439,6 +3439,7 @@ class ColorSafeStringify {
3439
3439
  ;// ./src/v1/basics/html.mjs
3440
3440
 
3441
3441
 
3442
+
3442
3443
  /**
3443
3444
  * Checks if two DOM elements are colliding on the screen.
3444
3445
  *
@@ -3449,44 +3450,157 @@ class ColorSafeStringify {
3449
3450
  function areHtmlElsColliding(elem1, elem2) {
3450
3451
  const rect1 = elem1.getBoundingClientRect();
3451
3452
  const rect2 = elem2.getBoundingClientRect();
3453
+ return areElsColliding(rect1, rect2);
3454
+ }
3452
3455
 
3453
- return !(
3454
- rect1.right < rect2.left ||
3455
- rect1.left > rect2.right ||
3456
- rect1.bottom < rect2.top ||
3457
- rect1.top > rect2.bottom
3458
- );
3456
+ /**
3457
+ * Checks if two DOM elements are colliding on the screen, and locks the collision
3458
+ * until the element exits through the same side it entered.
3459
+ *
3460
+ * @param {Element} elem1 - First DOM element (e.g. draggable or moving element).
3461
+ * @param {Element} elem2 - Second DOM element (e.g. a container or boundary element).
3462
+ * @param {'top'|'bottom'|'left'|'right'} lockDirection - Direction that must be respected to unlock the collision.
3463
+ * @param {WeakMap<Element, string>} stateMap - A shared WeakMap to track persistent entry direction per element.
3464
+ * @returns {boolean} True if collision is still active.
3465
+ */
3466
+ function areHtmlElsCollidingWithLock(elem1, elem2, lockDirection, stateMap) {
3467
+ const rect1 = elem1.getBoundingClientRect();
3468
+ const rect2 = elem2.getBoundingClientRect();
3469
+ const isColliding = areElsColliding(rect1, rect2);
3470
+
3471
+ if (isColliding) {
3472
+ // Save entry direction
3473
+ if (!stateMap.has(elem1)) {
3474
+ stateMap.set(elem1, lockDirection);
3475
+ }
3476
+ return true;
3477
+ }
3478
+
3479
+ // Handle unlock logic
3480
+ if (stateMap.has(elem1)) {
3481
+ const lastDirection = stateMap.get(elem1);
3482
+
3483
+ switch (lastDirection) {
3484
+ case 'top':
3485
+ if (areElsCollTop(rect1, rect2)) stateMap.delete(elem1); // exited from top
3486
+ break;
3487
+ case 'bottom':
3488
+ if (areElsCollBottom(rect1, rect2)) stateMap.delete(elem1); // exited from bottom
3489
+ break;
3490
+ case 'left':
3491
+ if (areElsCollLeft(rect1, rect2)) stateMap.delete(elem1); // exited from left
3492
+ break;
3493
+ case 'right':
3494
+ if (areElsCollRight(rect1, rect2)) stateMap.delete(elem1); // exited from right
3495
+ break;
3496
+ }
3497
+
3498
+ return stateMap.has(elem1); // still colliding (locked)
3499
+ }
3500
+
3501
+ return false;
3459
3502
  }
3460
3503
 
3461
3504
  /**
3462
- * Reads and parses a JSON data using FileReader.
3463
- * Throws an error if the content is not valid JSON.
3464
- * @param {File} file
3465
- * @returns {Promise<any>}
3505
+ * Reads the contents of a file using the specified FileReader method.
3506
+ *
3507
+ * @param {File} file - The file to be read.
3508
+ * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
3509
+ * The FileReader method to use for reading the file.
3510
+ * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
3511
+ * @throws {Error} - If an unexpected error occurs while handling the result.
3512
+ * @throws {DOMException} - If the FileReader encounters an error while reading the file.
3466
3513
  */
3467
- function readJsonBlob(file) {
3514
+ function readFileBlob(file, method) {
3468
3515
  return new Promise((resolve, reject) => {
3469
3516
  const reader = new FileReader();
3470
-
3471
3517
  reader.onload = () => {
3472
3518
  try {
3473
- // @ts-ignore
3474
- const result = JSON.parse(reader.result);
3475
- resolve(result);
3519
+ resolve(reader.result);
3476
3520
  } catch (error) {
3477
- // @ts-ignore
3478
- reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
3521
+ reject(error);
3479
3522
  }
3480
3523
  };
3481
-
3482
3524
  reader.onerror = () => {
3483
- reject(new Error(`Error reading file: ${file.name}`));
3525
+ reject(reader.error);
3484
3526
  };
3527
+ reader[method](file);
3528
+ });
3529
+ }
3485
3530
 
3486
- reader.readAsText(file);
3531
+ /**
3532
+ * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
3533
+ *
3534
+ * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
3535
+ *
3536
+ * @param {File} file - The file to be read.
3537
+ * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
3538
+ * if a string is passed, it is used as the MIME type in the data URL.
3539
+ * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
3540
+ *
3541
+ * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
3542
+ * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
3543
+ * @throws {DOMException} - If the FileReader fails to read the file.
3544
+ */
3545
+ function readBase64Blob(file, isDataUrl = false) {
3546
+ return new Promise((resolve, reject) => {
3547
+ if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
3548
+ reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
3549
+ readFileBlob(file, 'readAsDataURL')
3550
+ .then(
3551
+ /**
3552
+ * Ensure that the URL format is correct in the required pattern
3553
+ * @param {string} base64Data
3554
+ */ (base64Data) => {
3555
+ if (typeof base64Data !== 'string')
3556
+ throw new TypeError('Expected file content to be a string.');
3557
+
3558
+ const match = base64Data.match(/^data:(.+);base64,(.*)$/);
3559
+ if (!match || !match[2])
3560
+ throw new Error('Invalid data URL format or missing Base64 content.');
3561
+ const [, mimeType, base64] = match;
3562
+ if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
3563
+
3564
+ if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
3565
+ if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
3566
+ throw new Error(`Invalid MIME type string: ${isDataUrl}`);
3567
+
3568
+ return resolve(`data:${isDataUrl};base64,${base64}`);
3569
+ },
3570
+ )
3571
+ .catch(reject);
3487
3572
  });
3488
3573
  }
3489
3574
 
3575
+ /**
3576
+ * Reads a file and strictly validates its content as proper JSON using FileReader.
3577
+ *
3578
+ * Performs several checks to ensure the file contains valid, parsable JSON data.
3579
+ *
3580
+ * @param {File} file - The file to be read. It must contain valid JSON as plain text.
3581
+ * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
3582
+ *
3583
+ * @throws {SyntaxError} - If the file content is not valid JSON syntax.
3584
+ * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
3585
+ * @throws {Error} - If the result is empty or structurally invalid as JSON.
3586
+ * @throws {DOMException} - If the FileReader fails to read the file.
3587
+ */
3588
+ function readJsonBlob(file) {
3589
+ return new Promise((resolve, reject) =>
3590
+ readFileBlob(file, 'readAsText')
3591
+ .then((data) => {
3592
+ if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
3593
+ const trimmed = data.trim();
3594
+ if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
3595
+ const parsed = JSON.parse(trimmed);
3596
+ if (typeof parsed !== 'object' || parsed === null)
3597
+ throw new Error('Parsed content is not a valid JSON object or array.');
3598
+ resolve(parsed);
3599
+ })
3600
+ .catch(reject),
3601
+ );
3602
+ }
3603
+
3490
3604
  /**
3491
3605
  * Saves a JSON object as a downloadable file.
3492
3606
  * @param {string} filename
@@ -3589,7 +3703,8 @@ async function fetchJson(
3589
3703
 
3590
3704
  const data = await response.json();
3591
3705
 
3592
- if (!isJsonObject(data)) throw new Error('Received invalid data instead of valid JSON.');
3706
+ if (!Array.isArray(data) && !isJsonObject(data))
3707
+ throw new Error('Received invalid data instead of valid JSON.');
3593
3708
 
3594
3709
  return data;
3595
3710
  } catch (err) {
@@ -3691,19 +3806,34 @@ const getHtmlElPadding = (el) => {
3691
3806
 
3692
3807
  /**
3693
3808
  * Installs a script that toggles CSS classes on a given element
3694
- * based on the page's visibility or focus state.
3809
+ * based on the page's visibility or focus state, and optionally
3810
+ * triggers callbacks on visibility changes.
3695
3811
  *
3696
3812
  * @param {Object} [settings={}]
3697
3813
  * @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
3698
3814
  * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
3699
3815
  * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
3816
+ * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
3817
+ * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
3700
3818
  * @returns {() => void} Function that removes all installed event listeners.
3819
+ * @throws {TypeError} If any provided setting is invalid.
3701
3820
  */
3702
3821
  function installWindowHiddenScript({
3703
3822
  element = document.body,
3704
3823
  hiddenClass = 'windowHidden',
3705
3824
  visibleClass = 'windowVisible',
3825
+ onVisible,
3826
+ onHidden,
3706
3827
  } = {}) {
3828
+ if (!(element instanceof HTMLElement))
3829
+ throw new TypeError(`"element" must be an instance of HTMLElement.`);
3830
+ if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
3831
+ if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
3832
+ if (onVisible !== undefined && typeof onVisible !== 'function')
3833
+ throw new TypeError(`"onVisible" must be a function if provided.`);
3834
+ if (onHidden !== undefined && typeof onHidden !== 'function')
3835
+ throw new TypeError(`"onHidden" must be a function if provided.`);
3836
+
3707
3837
  const removeClass = () => {
3708
3838
  element.classList.remove(hiddenClass);
3709
3839
  element.classList.remove(visibleClass);
@@ -3711,8 +3841,6 @@ function installWindowHiddenScript({
3711
3841
 
3712
3842
  /** @type {string|null} */
3713
3843
  let hiddenProp = null;
3714
- /** @type {(this: any, evt: Event) => void} */
3715
- let handler;
3716
3844
 
3717
3845
  const visibilityEvents = [
3718
3846
  'visibilitychange',
@@ -3730,7 +3858,8 @@ function installWindowHiddenScript({
3730
3858
  }
3731
3859
  }
3732
3860
 
3733
- handler = function (evt) {
3861
+ /** @type {(this: any, evt: Event) => void} */
3862
+ const handler = function (evt) {
3734
3863
  removeClass();
3735
3864
 
3736
3865
  const type = evt?.type;
@@ -3742,10 +3871,18 @@ function installWindowHiddenScript({
3742
3871
 
3743
3872
  if (visibleEvents.includes(type)) {
3744
3873
  element.classList.add(visibleClass);
3874
+ onVisible?.();
3745
3875
  } else if (hiddenEvents.includes(type)) {
3746
3876
  element.classList.add(hiddenClass);
3877
+ onHidden?.();
3747
3878
  } else {
3748
- element.classList.add(isHidden ? hiddenClass : visibleClass);
3879
+ if (isHidden) {
3880
+ element.classList.add(hiddenClass);
3881
+ onHidden?.();
3882
+ } else {
3883
+ element.classList.add(visibleClass);
3884
+ onVisible?.();
3885
+ }
3749
3886
  }
3750
3887
  };
3751
3888
 
@@ -3782,6 +3919,7 @@ function installWindowHiddenScript({
3782
3919
  };
3783
3920
  }
3784
3921
 
3922
+ // Trigger initial state
3785
3923
  // @ts-ignore
3786
3924
  const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
3787
3925
  handler(simulatedEvent);
@@ -3789,6 +3927,38 @@ function installWindowHiddenScript({
3789
3927
  return uninstall;
3790
3928
  }
3791
3929
 
3930
+ /**
3931
+ * Checks if the given element is at least partially visible in the viewport.
3932
+ *
3933
+ * @param {HTMLElement} element - The DOM element to check.
3934
+ * @returns {boolean} True if the element is partially in the viewport, false otherwise.
3935
+ */
3936
+ function isInViewport(element) {
3937
+ const elementTop = element.offsetTop;
3938
+ const elementBottom = elementTop + element.offsetHeight;
3939
+
3940
+ const viewportTop = window.scrollY;
3941
+ const viewportBottom = viewportTop + window.innerHeight;
3942
+
3943
+ return elementBottom > viewportTop && elementTop < viewportBottom;
3944
+ }
3945
+
3946
+ /**
3947
+ * Checks if the given element is fully visible in the viewport (top and bottom).
3948
+ *
3949
+ * @param {HTMLElement} element - The DOM element to check.
3950
+ * @returns {boolean} True if the element is fully visible in the viewport, false otherwise.
3951
+ */
3952
+ function isScrolledIntoView(element) {
3953
+ const viewportTop = window.scrollY;
3954
+ const viewportBottom = viewportTop + window.innerHeight;
3955
+
3956
+ const elemTop = element.offsetTop;
3957
+ const elemBottom = elemTop + element.offsetHeight;
3958
+
3959
+ return elemBottom <= viewportBottom && elemTop >= viewportTop;
3960
+ }
3961
+
3792
3962
  // EXTERNAL MODULE: fs (ignored)
3793
3963
  var fs_ignored_ = __webpack_require__(133);
3794
3964
  // EXTERNAL MODULE: ./node_modules/path-browserify/index.js
@@ -5389,6 +5559,8 @@ class TinyDragger {
5389
5559
 
5390
5560
 
5391
5561
 
5562
+
5563
+
5392
5564
 
5393
5565
 
5394
5566