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
@@ -0,0 +1,350 @@
1
+ /**
2
+ * A direction relative to a rectangle.
3
+ *
4
+ * Represents one of the four cardinal directions from the perspective of the element.
5
+ *
6
+ * @typedef {'top' | 'bottom' | 'left' | 'right'} Dirs
7
+ */
8
+ /**
9
+ * Represents all directional aspects of a collision.
10
+ *
11
+ * @typedef {Object} CollDirs
12
+ * @property {Dirs | 'center' | null} in - The dominant direction of entry. `'center'` if all sides are equally overlapped. `null` if no collision.
13
+ * @property {Dirs | null} x - The horizontal direction (`'left'` or `'right'`) the collision is biased toward, or `null`.
14
+ * @property {Dirs | null} y - The vertical direction (`'top'` or `'bottom'`) the collision is biased toward, or `null`.
15
+ */
16
+ /**
17
+ * Indicates if a collision is in the negative direction (rect2 is outside rect1).
18
+ *
19
+ * @typedef {Object} NegCollDirs
20
+ * @property {Dirs | null} x - Horizontal direction of negative overlap, if any.
21
+ * @property {Dirs | null} y - Vertical direction of negative overlap, if any.
22
+ */
23
+ /**
24
+ * Collision depth values from each side of rect2 inside rect1.
25
+ *
26
+ * Positive values indicate penetration; negative values indicate gaps.
27
+ *
28
+ * @typedef {Object} CollData
29
+ * @property {number} top - Depth from rect2's top into rect1.
30
+ * @property {number} bottom - Depth from rect2's bottom into rect1.
31
+ * @property {number} left - Depth from rect2's left into rect1.
32
+ * @property {number} right - Depth from rect2's right into rect1.
33
+ */
34
+ /**
35
+ * X and Y offset representing center difference between two rectangles.
36
+ *
37
+ * Useful to measure how far one element's center is from another.
38
+ *
39
+ * @typedef {Object} CollCenter
40
+ * @property {number} x - Horizontal distance in pixels from rect1's center to rect2's center.
41
+ * @property {number} y - Vertical distance in pixels from rect1's center to rect2's center.
42
+ */
43
+ /**
44
+ * Represents a rectangular area in absolute pixel values.
45
+ *
46
+ * Similar to `DOMRect`, this object describes the dimensions and position of a box
47
+ * in the 2D plane, typically representing an element's bounding box.
48
+ *
49
+ * @typedef {Object} ObjRect
50
+ * @property {number} height - The total height of the rectangle in pixels.
51
+ * @property {number} width - The total width of the rectangle in pixels.
52
+ * @property {number} top - The Y-coordinate of the top edge of the rectangle.
53
+ * @property {number} bottom - The Y-coordinate of the bottom edge of the rectangle.
54
+ * @property {number} left - The X-coordinate of the left edge of the rectangle.
55
+ * @property {number} right - The X-coordinate of the right edge of the rectangle.
56
+ */
57
+ // Normal collision checks (loose overlap detection)
58
+ /**
59
+ * Checks if rect1 is completely above rect2 (no vertical overlap).
60
+ *
61
+ * @param {ObjRect} rect1 - The bounding rectangle of the first element.
62
+ * @param {ObjRect} rect2 - The bounding rectangle of the second element.
63
+ * @returns {boolean} True if rect1 is entirely above rect2.
64
+ */
65
+ export const areElsCollTop = (rect1, rect2) => rect1.bottom < rect2.top;
66
+ /**
67
+ * Checks if rect1 is completely below rect2 (no vertical overlap).
68
+ *
69
+ * @param {ObjRect} rect1
70
+ * @param {ObjRect} rect2
71
+ * @returns {boolean} True if rect1 is entirely below rect2.
72
+ */
73
+ export const areElsCollBottom = (rect1, rect2) => rect1.top > rect2.bottom;
74
+ /**
75
+ * Checks if rect1 is completely to the left of rect2 (no horizontal overlap).
76
+ *
77
+ * @param {ObjRect} rect1
78
+ * @param {ObjRect} rect2
79
+ * @returns {boolean} True if rect1 is entirely to the left of rect2.
80
+ */
81
+ export const areElsCollLeft = (rect1, rect2) => rect1.right < rect2.left;
82
+ /**
83
+ * Checks if rect1 is completely to the right of rect2 (no horizontal overlap).
84
+ *
85
+ * @param {ObjRect} rect1
86
+ * @param {ObjRect} rect2
87
+ * @returns {boolean} True if rect1 is entirely to the right of rect2.
88
+ */
89
+ export const areElsCollRight = (rect1, rect2) => rect1.left > rect2.right;
90
+ // Perfect collision checks (touch included)
91
+ /**
92
+ * Checks if rect1 is perfectly above rect2 (no vertical touch or overlap).
93
+ *
94
+ * @param {ObjRect} rect1
95
+ * @param {ObjRect} rect2
96
+ * @returns {boolean} True if rect1 is fully above or touching rect2's top.
97
+ */
98
+ export const areElsCollPerfTop = (rect1, rect2) => rect1.bottom <= rect2.top;
99
+ /**
100
+ * Checks if rect1 is perfectly below rect2 (no vertical touch or overlap).
101
+ *
102
+ * @param {ObjRect} rect1
103
+ * @param {ObjRect} rect2
104
+ * @returns {boolean} True if rect1 is fully below or touching rect2's bottom.
105
+ */
106
+ export const areElsCollPerfBottom = (rect1, rect2) => rect1.top >= rect2.bottom;
107
+ /**
108
+ * Checks if rect1 is perfectly to the left of rect2 (no horizontal touch or overlap).
109
+ *
110
+ * @param {ObjRect} rect1
111
+ * @param {ObjRect} rect2
112
+ * @returns {boolean} True if rect1 is fully left or touching rect2's left.
113
+ */
114
+ export const areElsCollPerfLeft = (rect1, rect2) => rect1.right <= rect2.left;
115
+ /**
116
+ * Checks if rect1 is perfectly to the right of rect2 (no horizontal touch or overlap).
117
+ *
118
+ * @param {ObjRect} rect1
119
+ * @param {ObjRect} rect2
120
+ * @returns {boolean} True if rect1 is fully right or touching rect2's right.
121
+ */
122
+ export const areElsCollPerfRight = (rect1, rect2) => rect1.left >= rect2.right;
123
+ // Main collision check
124
+ /**
125
+ * Returns true if rect1 and rect2 are colliding (partially or fully overlapping).
126
+ *
127
+ * @param {ObjRect} rect1
128
+ * @param {ObjRect} rect2
129
+ * @returns {boolean} True if there's any collision between rect1 and rect2.
130
+ */
131
+ export const areElsColliding = (rect1, rect2) => !(areElsCollLeft(rect1, rect2) ||
132
+ areElsCollRight(rect1, rect2) ||
133
+ areElsCollTop(rect1, rect2) ||
134
+ areElsCollBottom(rect1, rect2));
135
+ /**
136
+ * Returns true if rect1 and rect2 are colliding or perfectly touching.
137
+ *
138
+ * @param {ObjRect} rect1
139
+ * @param {ObjRect} rect2
140
+ * @returns {boolean} True if there's any contact or overlap.
141
+ */
142
+ export const areElsPerfColliding = (rect1, rect2) => !(areElsCollPerfLeft(rect1, rect2) ||
143
+ areElsCollPerfRight(rect1, rect2) ||
144
+ areElsCollPerfTop(rect1, rect2) ||
145
+ areElsCollPerfBottom(rect1, rect2));
146
+ // Collision direction guess (loose and perfect)
147
+ /**
148
+ * Attempts to determine the direction rect1 entered rect2 based on loose overlap rules.
149
+ *
150
+ * @param {ObjRect} rect1
151
+ * @param {ObjRect} rect2
152
+ * @returns {string|null} 'top' | 'bottom' | 'left' | 'right' | null
153
+ */
154
+ export const getElsColliding = (rect1, rect2) => {
155
+ if (areElsCollLeft(rect1, rect2))
156
+ return 'left';
157
+ else if (areElsCollRight(rect1, rect2))
158
+ return 'right';
159
+ else if (areElsCollTop(rect1, rect2))
160
+ return 'top';
161
+ else if (areElsCollBottom(rect1, rect2))
162
+ return 'bottom';
163
+ return null;
164
+ };
165
+ /**
166
+ * Attempts to determine the direction rect1 touched or entered rect2 using perfect mode.
167
+ *
168
+ * @param {ObjRect} rect1
169
+ * @param {ObjRect} rect2
170
+ * @returns {'top' | 'bottom' | 'left' | 'right' | null}
171
+ */
172
+ export const getElsPerfColliding = (rect1, rect2) => {
173
+ if (areElsCollPerfLeft(rect1, rect2))
174
+ return 'left';
175
+ else if (areElsCollPerfRight(rect1, rect2))
176
+ return 'right';
177
+ else if (areElsCollPerfTop(rect1, rect2))
178
+ return 'top';
179
+ else if (areElsCollPerfBottom(rect1, rect2))
180
+ return 'bottom';
181
+ return null;
182
+ };
183
+ // Overlap Calculation
184
+ /**
185
+ * Calculates overlap values between rect1 and rect2 in all directions.
186
+ *
187
+ * @param {ObjRect} rect1
188
+ * @param {ObjRect} rect2
189
+ * @returns {{
190
+ * overlapLeft: number,
191
+ * overlapRight: number,
192
+ * overlapTop: number,
193
+ * overlapBottom: number
194
+ * }} Distance of overlap from each direction (can be negative).
195
+ */
196
+ export const getElsCollOverlap = (rect1, rect2) => ({
197
+ overlapLeft: rect2.right - rect1.left,
198
+ overlapRight: rect1.right - rect2.left,
199
+ overlapTop: rect2.bottom - rect1.top,
200
+ overlapBottom: rect1.bottom - rect2.top,
201
+ });
202
+ /**
203
+ * Determines directional collision based on overlap depth.
204
+ *
205
+ * @param {Object} [settings={}]
206
+ * @param {number} [settings.overlapLeft]
207
+ * @param {number} [settings.overlapRight]
208
+ * @param {number} [settings.overlapTop]
209
+ * @param {number} [settings.overlapBottom]
210
+ * @returns {{ dirX: Dirs, dirY: Dirs }} Direction of strongest X/Y overlap.
211
+ */
212
+ export const getElsCollOverlapPos = ({ overlapLeft = -1, overlapRight = -1, overlapTop = -1, overlapBottom = -1, } = {}) => ({
213
+ dirX: overlapLeft < overlapRight ? 'right' : 'left',
214
+ dirY: overlapTop < overlapBottom ? 'bottom' : 'top',
215
+ });
216
+ // Center utils
217
+ /**
218
+ * Calculates the center point (X and Y) of a given Rect.
219
+ *
220
+ * @param {ObjRect} rect - The bounding rectangle of the element.
221
+ * @returns {{ x: number, y: number }} An object with the `x` and `y` coordinates of the center.
222
+ */
223
+ export const getRectCenter = (rect) => ({
224
+ x: rect.left + rect.width / 2,
225
+ y: rect.top + rect.height / 2,
226
+ });
227
+ /**
228
+ * Calculates the offset between the center of rect2 and the center of rect1.
229
+ *
230
+ * The values will be 0 when rect1 is perfectly centered over rect2.
231
+ *
232
+ * @param {ObjRect} rect1 - The bounding rectangle of the reference element.
233
+ * @param {ObjRect} rect2 - The bounding rectangle of the element being compared.
234
+ * @returns {{
235
+ * x: number,
236
+ * y: number
237
+ * }} An object with the X and Y offset in pixels from rect1's center to rect2's center.
238
+ */
239
+ export function getElsRelativeCenterOffset(rect1, rect2) {
240
+ const center1X = rect1.left + rect1.width / 2;
241
+ const center1Y = rect1.top + rect1.height / 2;
242
+ const center2X = rect2.left + rect2.width / 2;
243
+ const center2Y = rect2.top + rect2.height / 2;
244
+ return {
245
+ x: center2X - center1X,
246
+ y: center2Y - center1Y,
247
+ };
248
+ }
249
+ // Direction & Depth detection
250
+ /**
251
+ * Detects the direction of the dominant collision between two elements
252
+ * and calculates how deep the overlap is in both x and y axes.
253
+ *
254
+ * @param {ObjRect} rect1 - The bounding rectangle of the first element.
255
+ * @param {ObjRect} rect2 - The bounding rectangle of the second element.
256
+ * @returns {{
257
+ * inDir: Dirs | null;
258
+ * dirX: Dirs | null;
259
+ * dirY: Dirs | null;
260
+ * depthX: number;
261
+ * depthY: number;
262
+ * }} An object containing the collision direction and how deep the overlap is.
263
+ */
264
+ export function getElsCollDirDepth(rect1, rect2) {
265
+ if (!areElsPerfColliding(rect1, rect2))
266
+ return {
267
+ inDir: null,
268
+ dirX: null,
269
+ dirY: null,
270
+ depthX: 0,
271
+ depthY: 0,
272
+ };
273
+ const { overlapLeft, overlapRight, overlapTop, overlapBottom } = getElsCollOverlap(rect1, rect2);
274
+ const { dirX, dirY } = getElsCollOverlapPos({
275
+ overlapLeft,
276
+ overlapRight,
277
+ overlapTop,
278
+ overlapBottom,
279
+ });
280
+ const depthX = Math.min(overlapLeft, overlapRight);
281
+ const depthY = Math.min(overlapTop, overlapBottom);
282
+ /** @type {Dirs} */
283
+ let inDir;
284
+ if (depthX < depthY)
285
+ inDir = dirX;
286
+ else
287
+ inDir = dirY;
288
+ return { inDir, dirX, dirY, depthX, depthY };
289
+ }
290
+ // Full detail report
291
+ /**
292
+ * Detects the collision direction and depth between two DOMRects.
293
+ *
294
+ * @param {ObjRect} rect1 - The bounding rectangle of the first element.
295
+ * @param {ObjRect} rect2 - The bounding rectangle of the second element.
296
+ * @returns {{ depth: CollData; dirs: CollDirs; isNeg: NegCollDirs; }} Collision info or null if no collision is detected.
297
+ */
298
+ export function getElsCollDetails(rect1, rect2) {
299
+ const isColliding = areElsPerfColliding(rect1, rect2);
300
+ /** @type {CollDirs} */
301
+ const dirs = { in: null, x: null, y: null };
302
+ /** @type {NegCollDirs} */
303
+ const isNeg = { y: null, x: null };
304
+ /** @type {Record<Dirs, number>} */
305
+ const depth = { top: 0, bottom: 0, left: 0, right: 0 };
306
+ // Depth
307
+ // Yes, it's actually reversed the values orders
308
+ const { overlapLeft, overlapRight, overlapTop, overlapBottom } = getElsCollOverlap(rect2, rect1);
309
+ depth.top = overlapTop;
310
+ depth.bottom = overlapBottom;
311
+ depth.left = overlapLeft;
312
+ depth.right = overlapRight;
313
+ // Dirs
314
+ /**
315
+ * Detect the direction with the smallest positive overlap (entry point)
316
+ * @type {[Dirs, number][]}
317
+ */
318
+ // @ts-ignore
319
+ const entries = Object.entries(depth)
320
+ .filter(([, val]) => val > 0)
321
+ .sort((a, b) => a[1] - b[1]);
322
+ // Yes, it's actually reversed the values orders here too
323
+ const { dirX, dirY } = getElsCollOverlapPos({
324
+ overlapLeft: overlapRight,
325
+ overlapRight: overlapLeft,
326
+ overlapTop: overlapBottom,
327
+ overlapBottom: overlapTop,
328
+ });
329
+ dirs.y = dirY;
330
+ dirs.x = dirX;
331
+ // isNeg
332
+ if (depth.bottom < 0)
333
+ isNeg.y = 'bottom';
334
+ else if (depth.top < 0)
335
+ isNeg.y = 'top';
336
+ if (depth.left < 0)
337
+ isNeg.x = 'left';
338
+ else if (depth.right < 0)
339
+ isNeg.x = 'right';
340
+ // Inside Dir
341
+ dirs.in = isColliding
342
+ ? depth.top === depth.bottom && depth.bottom === depth.left && depth.left === depth.right
343
+ ? 'center'
344
+ : entries.length
345
+ ? entries[0][0]
346
+ : 'top'
347
+ : null; // fallback in case of exact match
348
+ // Complete
349
+ return { dirs, depth, isNeg };
350
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var objFilter = require('./objFilter.cjs');
4
+ var collision = require('./collision.cjs');
4
5
 
5
6
  /**
6
7
  * Checks if two DOM elements are colliding on the screen.
@@ -12,44 +13,157 @@ var objFilter = require('./objFilter.cjs');
12
13
  function areHtmlElsColliding(elem1, elem2) {
13
14
  const rect1 = elem1.getBoundingClientRect();
14
15
  const rect2 = elem2.getBoundingClientRect();
16
+ return collision.areElsColliding(rect1, rect2);
17
+ }
15
18
 
16
- return !(
17
- rect1.right < rect2.left ||
18
- rect1.left > rect2.right ||
19
- rect1.bottom < rect2.top ||
20
- rect1.top > rect2.bottom
21
- );
19
+ /**
20
+ * Checks if two DOM elements are colliding on the screen, and locks the collision
21
+ * until the element exits through the same side it entered.
22
+ *
23
+ * @param {Element} elem1 - First DOM element (e.g. draggable or moving element).
24
+ * @param {Element} elem2 - Second DOM element (e.g. a container or boundary element).
25
+ * @param {'top'|'bottom'|'left'|'right'} lockDirection - Direction that must be respected to unlock the collision.
26
+ * @param {WeakMap<Element, string>} stateMap - A shared WeakMap to track persistent entry direction per element.
27
+ * @returns {boolean} True if collision is still active.
28
+ */
29
+ function areHtmlElsCollidingWithLock(elem1, elem2, lockDirection, stateMap) {
30
+ const rect1 = elem1.getBoundingClientRect();
31
+ const rect2 = elem2.getBoundingClientRect();
32
+ const isColliding = collision.areElsColliding(rect1, rect2);
33
+
34
+ if (isColliding) {
35
+ // Save entry direction
36
+ if (!stateMap.has(elem1)) {
37
+ stateMap.set(elem1, lockDirection);
38
+ }
39
+ return true;
40
+ }
41
+
42
+ // Handle unlock logic
43
+ if (stateMap.has(elem1)) {
44
+ const lastDirection = stateMap.get(elem1);
45
+
46
+ switch (lastDirection) {
47
+ case 'top':
48
+ if (collision.areElsCollTop(rect1, rect2)) stateMap.delete(elem1); // exited from top
49
+ break;
50
+ case 'bottom':
51
+ if (collision.areElsCollBottom(rect1, rect2)) stateMap.delete(elem1); // exited from bottom
52
+ break;
53
+ case 'left':
54
+ if (collision.areElsCollLeft(rect1, rect2)) stateMap.delete(elem1); // exited from left
55
+ break;
56
+ case 'right':
57
+ if (collision.areElsCollRight(rect1, rect2)) stateMap.delete(elem1); // exited from right
58
+ break;
59
+ }
60
+
61
+ return stateMap.has(elem1); // still colliding (locked)
62
+ }
63
+
64
+ return false;
22
65
  }
23
66
 
24
67
  /**
25
- * Reads and parses a JSON data using FileReader.
26
- * Throws an error if the content is not valid JSON.
27
- * @param {File} file
28
- * @returns {Promise<any>}
68
+ * Reads the contents of a file using the specified FileReader method.
69
+ *
70
+ * @param {File} file - The file to be read.
71
+ * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
72
+ * The FileReader method to use for reading the file.
73
+ * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
74
+ * @throws {Error} - If an unexpected error occurs while handling the result.
75
+ * @throws {DOMException} - If the FileReader encounters an error while reading the file.
29
76
  */
30
- function readJsonBlob(file) {
77
+ function readFileBlob(file, method) {
31
78
  return new Promise((resolve, reject) => {
32
79
  const reader = new FileReader();
33
-
34
80
  reader.onload = () => {
35
81
  try {
36
- // @ts-ignore
37
- const result = JSON.parse(reader.result);
38
- resolve(result);
82
+ resolve(reader.result);
39
83
  } catch (error) {
40
- // @ts-ignore
41
- reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
84
+ reject(error);
42
85
  }
43
86
  };
44
-
45
87
  reader.onerror = () => {
46
- reject(new Error(`Error reading file: ${file.name}`));
88
+ reject(reader.error);
47
89
  };
90
+ reader[method](file);
91
+ });
92
+ }
48
93
 
49
- reader.readAsText(file);
94
+ /**
95
+ * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
96
+ *
97
+ * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
98
+ *
99
+ * @param {File} file - The file to be read.
100
+ * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
101
+ * if a string is passed, it is used as the MIME type in the data URL.
102
+ * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
103
+ *
104
+ * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
105
+ * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
106
+ * @throws {DOMException} - If the FileReader fails to read the file.
107
+ */
108
+ function readBase64Blob(file, isDataUrl = false) {
109
+ return new Promise((resolve, reject) => {
110
+ if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
111
+ reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
112
+ readFileBlob(file, 'readAsDataURL')
113
+ .then(
114
+ /**
115
+ * Ensure that the URL format is correct in the required pattern
116
+ * @param {string} base64Data
117
+ */ (base64Data) => {
118
+ if (typeof base64Data !== 'string')
119
+ throw new TypeError('Expected file content to be a string.');
120
+
121
+ const match = base64Data.match(/^data:(.+);base64,(.*)$/);
122
+ if (!match || !match[2])
123
+ throw new Error('Invalid data URL format or missing Base64 content.');
124
+ const [, mimeType, base64] = match;
125
+ if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
126
+
127
+ if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
128
+ if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
129
+ throw new Error(`Invalid MIME type string: ${isDataUrl}`);
130
+
131
+ return resolve(`data:${isDataUrl};base64,${base64}`);
132
+ },
133
+ )
134
+ .catch(reject);
50
135
  });
51
136
  }
52
137
 
138
+ /**
139
+ * Reads a file and strictly validates its content as proper JSON using FileReader.
140
+ *
141
+ * Performs several checks to ensure the file contains valid, parsable JSON data.
142
+ *
143
+ * @param {File} file - The file to be read. It must contain valid JSON as plain text.
144
+ * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
145
+ *
146
+ * @throws {SyntaxError} - If the file content is not valid JSON syntax.
147
+ * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
148
+ * @throws {Error} - If the result is empty or structurally invalid as JSON.
149
+ * @throws {DOMException} - If the FileReader fails to read the file.
150
+ */
151
+ function readJsonBlob(file) {
152
+ return new Promise((resolve, reject) =>
153
+ readFileBlob(file, 'readAsText')
154
+ .then((data) => {
155
+ if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
156
+ const trimmed = data.trim();
157
+ if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
158
+ const parsed = JSON.parse(trimmed);
159
+ if (typeof parsed !== 'object' || parsed === null)
160
+ throw new Error('Parsed content is not a valid JSON object or array.');
161
+ resolve(parsed);
162
+ })
163
+ .catch(reject),
164
+ );
165
+ }
166
+
53
167
  /**
54
168
  * Saves a JSON object as a downloadable file.
55
169
  * @param {string} filename
@@ -152,7 +266,8 @@ async function fetchJson(
152
266
 
153
267
  const data = await response.json();
154
268
 
155
- if (!objFilter.isJsonObject(data)) throw new Error('Received invalid data instead of valid JSON.');
269
+ if (!Array.isArray(data) && !objFilter.isJsonObject(data))
270
+ throw new Error('Received invalid data instead of valid JSON.');
156
271
 
157
272
  return data;
158
273
  } catch (err) {
@@ -254,19 +369,34 @@ const getHtmlElPadding = (el) => {
254
369
 
255
370
  /**
256
371
  * Installs a script that toggles CSS classes on a given element
257
- * based on the page's visibility or focus state.
372
+ * based on the page's visibility or focus state, and optionally
373
+ * triggers callbacks on visibility changes.
258
374
  *
259
375
  * @param {Object} [settings={}]
260
376
  * @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
261
377
  * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
262
378
  * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
379
+ * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
380
+ * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
263
381
  * @returns {() => void} Function that removes all installed event listeners.
382
+ * @throws {TypeError} If any provided setting is invalid.
264
383
  */
265
384
  function installWindowHiddenScript({
266
385
  element = document.body,
267
386
  hiddenClass = 'windowHidden',
268
387
  visibleClass = 'windowVisible',
388
+ onVisible,
389
+ onHidden,
269
390
  } = {}) {
391
+ if (!(element instanceof HTMLElement))
392
+ throw new TypeError(`"element" must be an instance of HTMLElement.`);
393
+ if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
394
+ if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
395
+ if (onVisible !== undefined && typeof onVisible !== 'function')
396
+ throw new TypeError(`"onVisible" must be a function if provided.`);
397
+ if (onHidden !== undefined && typeof onHidden !== 'function')
398
+ throw new TypeError(`"onHidden" must be a function if provided.`);
399
+
270
400
  const removeClass = () => {
271
401
  element.classList.remove(hiddenClass);
272
402
  element.classList.remove(visibleClass);
@@ -274,8 +404,6 @@ function installWindowHiddenScript({
274
404
 
275
405
  /** @type {string|null} */
276
406
  let hiddenProp = null;
277
- /** @type {(this: any, evt: Event) => void} */
278
- let handler;
279
407
 
280
408
  const visibilityEvents = [
281
409
  'visibilitychange',
@@ -293,7 +421,8 @@ function installWindowHiddenScript({
293
421
  }
294
422
  }
295
423
 
296
- handler = function (evt) {
424
+ /** @type {(this: any, evt: Event) => void} */
425
+ const handler = function (evt) {
297
426
  removeClass();
298
427
 
299
428
  const type = evt?.type;
@@ -305,10 +434,18 @@ function installWindowHiddenScript({
305
434
 
306
435
  if (visibleEvents.includes(type)) {
307
436
  element.classList.add(visibleClass);
437
+ onVisible?.();
308
438
  } else if (hiddenEvents.includes(type)) {
309
439
  element.classList.add(hiddenClass);
440
+ onHidden?.();
310
441
  } else {
311
- element.classList.add(isHidden ? hiddenClass : visibleClass);
442
+ if (isHidden) {
443
+ element.classList.add(hiddenClass);
444
+ onHidden?.();
445
+ } else {
446
+ element.classList.add(visibleClass);
447
+ onVisible?.();
448
+ }
312
449
  }
313
450
  };
314
451
 
@@ -345,6 +482,7 @@ function installWindowHiddenScript({
345
482
  };
346
483
  }
347
484
 
485
+ // Trigger initial state
348
486
  // @ts-ignore
349
487
  const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
350
488
  handler(simulatedEvent);
@@ -352,12 +490,49 @@ function installWindowHiddenScript({
352
490
  return uninstall;
353
491
  }
354
492
 
493
+ /**
494
+ * Checks if the given element is at least partially visible in the viewport.
495
+ *
496
+ * @param {HTMLElement} element - The DOM element to check.
497
+ * @returns {boolean} True if the element is partially in the viewport, false otherwise.
498
+ */
499
+ function isInViewport(element) {
500
+ const elementTop = element.offsetTop;
501
+ const elementBottom = elementTop + element.offsetHeight;
502
+
503
+ const viewportTop = window.scrollY;
504
+ const viewportBottom = viewportTop + window.innerHeight;
505
+
506
+ return elementBottom > viewportTop && elementTop < viewportBottom;
507
+ }
508
+
509
+ /**
510
+ * Checks if the given element is fully visible in the viewport (top and bottom).
511
+ *
512
+ * @param {HTMLElement} element - The DOM element to check.
513
+ * @returns {boolean} True if the element is fully visible in the viewport, false otherwise.
514
+ */
515
+ function isScrolledIntoView(element) {
516
+ const viewportTop = window.scrollY;
517
+ const viewportBottom = viewportTop + window.innerHeight;
518
+
519
+ const elemTop = element.offsetTop;
520
+ const elemBottom = elemTop + element.offsetHeight;
521
+
522
+ return elemBottom <= viewportBottom && elemTop >= viewportTop;
523
+ }
524
+
355
525
  exports.areHtmlElsColliding = areHtmlElsColliding;
526
+ exports.areHtmlElsCollidingWithLock = areHtmlElsCollidingWithLock;
356
527
  exports.fetchJson = fetchJson;
357
528
  exports.getHtmlElBorders = getHtmlElBorders;
358
529
  exports.getHtmlElBordersWidth = getHtmlElBordersWidth;
359
530
  exports.getHtmlElMargin = getHtmlElMargin;
360
531
  exports.getHtmlElPadding = getHtmlElPadding;
361
532
  exports.installWindowHiddenScript = installWindowHiddenScript;
533
+ exports.isInViewport = isInViewport;
534
+ exports.isScrolledIntoView = isScrolledIntoView;
535
+ exports.readBase64Blob = readBase64Blob;
536
+ exports.readFileBlob = readFileBlob;
362
537
  exports.readJsonBlob = readJsonBlob;
363
538
  exports.saveJsonFile = saveJsonFile;