tiny-essentials 1.13.2 → 1.14.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.
@@ -22,34 +22,105 @@ function areHtmlElsColliding(elem1, elem2) {
22
22
  }
23
23
 
24
24
  /**
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>}
25
+ * Reads the contents of a file using the specified FileReader method.
26
+ *
27
+ * @param {File} file - The file to be read.
28
+ * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
29
+ * The FileReader method to use for reading the file.
30
+ * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
31
+ * @throws {Error} - If an unexpected error occurs while handling the result.
32
+ * @throws {DOMException} - If the FileReader encounters an error while reading the file.
29
33
  */
30
- function readJsonBlob(file) {
34
+ function readFileBlob(file, method) {
31
35
  return new Promise((resolve, reject) => {
32
36
  const reader = new FileReader();
33
-
34
37
  reader.onload = () => {
35
38
  try {
36
- // @ts-ignore
37
- const result = JSON.parse(reader.result);
38
- resolve(result);
39
+ resolve(reader.result);
39
40
  } catch (error) {
40
- // @ts-ignore
41
- reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
41
+ reject(error);
42
42
  }
43
43
  };
44
-
45
44
  reader.onerror = () => {
46
- reject(new Error(`Error reading file: ${file.name}`));
45
+ reject(reader.error);
47
46
  };
47
+ reader[method](file);
48
+ });
49
+ }
48
50
 
49
- reader.readAsText(file);
51
+ /**
52
+ * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
53
+ *
54
+ * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
55
+ *
56
+ * @param {File} file - The file to be read.
57
+ * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
58
+ * if a string is passed, it is used as the MIME type in the data URL.
59
+ * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
60
+ *
61
+ * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
62
+ * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
63
+ * @throws {DOMException} - If the FileReader fails to read the file.
64
+ */
65
+ function readBase64Blob(file, isDataUrl = false) {
66
+ return new Promise((resolve, reject) => {
67
+ if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
68
+ reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
69
+ readFileBlob(file, 'readAsDataURL')
70
+ .then(
71
+ /**
72
+ * Ensure that the URL format is correct in the required pattern
73
+ * @param {string} base64Data
74
+ */ (base64Data) => {
75
+ if (typeof base64Data !== 'string')
76
+ throw new TypeError('Expected file content to be a string.');
77
+
78
+ const match = base64Data.match(/^data:(.+);base64,(.*)$/);
79
+ if (!match || !match[2])
80
+ throw new Error('Invalid data URL format or missing Base64 content.');
81
+ const [, mimeType, base64] = match;
82
+ if (!/^[\w/+]+=*$/.test(base64)) throw new Error('Base64 content is malformed.');
83
+
84
+ if (typeof isDataUrl === 'boolean') return resolve(isDataUrl ? base64Data : base64);
85
+ if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
86
+ throw new Error(`Invalid MIME type string: ${isDataUrl}`);
87
+
88
+ return resolve(`data:${isDataUrl};base64,${base64}`);
89
+ },
90
+ )
91
+ .catch(reject);
50
92
  });
51
93
  }
52
94
 
95
+ /**
96
+ * Reads a file and strictly validates its content as proper JSON using FileReader.
97
+ *
98
+ * Performs several checks to ensure the file contains valid, parsable JSON data.
99
+ *
100
+ * @param {File} file - The file to be read. It must contain valid JSON as plain text.
101
+ * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
102
+ *
103
+ * @throws {SyntaxError} - If the file content is not valid JSON syntax.
104
+ * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
105
+ * @throws {Error} - If the result is empty or structurally invalid as JSON.
106
+ * @throws {DOMException} - If the FileReader fails to read the file.
107
+ */
108
+ function readJsonBlob(file) {
109
+ return new Promise((resolve, reject) =>
110
+ readFileBlob(file, 'readAsText')
111
+ .then((data) => {
112
+ if (typeof data !== 'string') throw new TypeError('Expected file content to be a string.');
113
+ const trimmed = data.trim();
114
+ if (trimmed.length === 0) throw new Error('File is empty or contains only whitespace.');
115
+ const parsed = JSON.parse(trimmed);
116
+ if (typeof parsed !== 'object' || parsed === null)
117
+ throw new Error('Parsed content is not a valid JSON object or array.');
118
+ resolve(parsed);
119
+ })
120
+ .catch(reject),
121
+ );
122
+ }
123
+
53
124
  /**
54
125
  * Saves a JSON object as a downloadable file.
55
126
  * @param {string} filename
@@ -152,7 +223,8 @@ async function fetchJson(
152
223
 
153
224
  const data = await response.json();
154
225
 
155
- if (!objFilter.isJsonObject(data)) throw new Error('Received invalid data instead of valid JSON.');
226
+ if (!Array.isArray(data) && !objFilter.isJsonObject(data))
227
+ throw new Error('Received invalid data instead of valid JSON.');
156
228
 
157
229
  return data;
158
230
  } catch (err) {
@@ -254,19 +326,34 @@ const getHtmlElPadding = (el) => {
254
326
 
255
327
  /**
256
328
  * Installs a script that toggles CSS classes on a given element
257
- * based on the page's visibility or focus state.
329
+ * based on the page's visibility or focus state, and optionally
330
+ * triggers callbacks on visibility changes.
258
331
  *
259
332
  * @param {Object} [settings={}]
260
333
  * @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
261
334
  * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
262
335
  * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
336
+ * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
337
+ * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
263
338
  * @returns {() => void} Function that removes all installed event listeners.
339
+ * @throws {TypeError} If any provided setting is invalid.
264
340
  */
265
341
  function installWindowHiddenScript({
266
342
  element = document.body,
267
343
  hiddenClass = 'windowHidden',
268
344
  visibleClass = 'windowVisible',
345
+ onVisible,
346
+ onHidden,
269
347
  } = {}) {
348
+ if (!(element instanceof HTMLElement))
349
+ throw new TypeError(`"element" must be an instance of HTMLElement.`);
350
+ if (typeof hiddenClass !== 'string') throw new TypeError(`"hiddenClass" must be a string.`);
351
+ if (typeof visibleClass !== 'string') throw new TypeError(`"visibleClass" must be a string.`);
352
+ if (onVisible !== undefined && typeof onVisible !== 'function')
353
+ throw new TypeError(`"onVisible" must be a function if provided.`);
354
+ if (onHidden !== undefined && typeof onHidden !== 'function')
355
+ throw new TypeError(`"onHidden" must be a function if provided.`);
356
+
270
357
  const removeClass = () => {
271
358
  element.classList.remove(hiddenClass);
272
359
  element.classList.remove(visibleClass);
@@ -274,8 +361,6 @@ function installWindowHiddenScript({
274
361
 
275
362
  /** @type {string|null} */
276
363
  let hiddenProp = null;
277
- /** @type {(this: any, evt: Event) => void} */
278
- let handler;
279
364
 
280
365
  const visibilityEvents = [
281
366
  'visibilitychange',
@@ -293,7 +378,8 @@ function installWindowHiddenScript({
293
378
  }
294
379
  }
295
380
 
296
- handler = function (evt) {
381
+ /** @type {(this: any, evt: Event) => void} */
382
+ const handler = function (evt) {
297
383
  removeClass();
298
384
 
299
385
  const type = evt?.type;
@@ -305,10 +391,18 @@ function installWindowHiddenScript({
305
391
 
306
392
  if (visibleEvents.includes(type)) {
307
393
  element.classList.add(visibleClass);
394
+ onVisible?.();
308
395
  } else if (hiddenEvents.includes(type)) {
309
396
  element.classList.add(hiddenClass);
397
+ onHidden?.();
310
398
  } else {
311
- element.classList.add(isHidden ? hiddenClass : visibleClass);
399
+ if (isHidden) {
400
+ element.classList.add(hiddenClass);
401
+ onHidden?.();
402
+ } else {
403
+ element.classList.add(visibleClass);
404
+ onVisible?.();
405
+ }
312
406
  }
313
407
  };
314
408
 
@@ -345,6 +439,7 @@ function installWindowHiddenScript({
345
439
  };
346
440
  }
347
441
 
442
+ // Trigger initial state
348
443
  // @ts-ignore
349
444
  const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
350
445
  handler(simulatedEvent);
@@ -359,5 +454,7 @@ exports.getHtmlElBordersWidth = getHtmlElBordersWidth;
359
454
  exports.getHtmlElMargin = getHtmlElMargin;
360
455
  exports.getHtmlElPadding = getHtmlElPadding;
361
456
  exports.installWindowHiddenScript = installWindowHiddenScript;
457
+ exports.readBase64Blob = readBase64Blob;
458
+ exports.readFileBlob = readFileBlob;
362
459
  exports.readJsonBlob = readJsonBlob;
363
460
  exports.saveJsonFile = saveJsonFile;
@@ -7,12 +7,45 @@
7
7
  */
8
8
  export function areHtmlElsColliding(elem1: Element, elem2: Element): boolean;
9
9
  /**
10
- * Reads and parses a JSON data using FileReader.
11
- * Throws an error if the content is not valid JSON.
12
- * @param {File} file
13
- * @returns {Promise<any>}
10
+ * Reads the contents of a file using the specified FileReader method.
11
+ *
12
+ * @param {File} file - The file to be read.
13
+ * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
14
+ * The FileReader method to use for reading the file.
15
+ * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
16
+ * @throws {Error} - If an unexpected error occurs while handling the result.
17
+ * @throws {DOMException} - If the FileReader encounters an error while reading the file.
18
+ */
19
+ export function readFileBlob(file: File, method: "readAsArrayBuffer" | "readAsDataURL" | "readAsText" | "readAsBinaryString"): Promise<any>;
20
+ /**
21
+ * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
22
+ *
23
+ * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
24
+ *
25
+ * @param {File} file - The file to be read.
26
+ * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
27
+ * if a string is passed, it is used as the MIME type in the data URL.
28
+ * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
29
+ *
30
+ * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
31
+ * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
32
+ * @throws {DOMException} - If the FileReader fails to read the file.
33
+ */
34
+ export function readBase64Blob(file: File, isDataUrl?: boolean | string): Promise<string>;
35
+ /**
36
+ * Reads a file and strictly validates its content as proper JSON using FileReader.
37
+ *
38
+ * Performs several checks to ensure the file contains valid, parsable JSON data.
39
+ *
40
+ * @param {File} file - The file to be read. It must contain valid JSON as plain text.
41
+ * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
42
+ *
43
+ * @throws {SyntaxError} - If the file content is not valid JSON syntax.
44
+ * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
45
+ * @throws {Error} - If the result is empty or structurally invalid as JSON.
46
+ * @throws {DOMException} - If the FileReader fails to read the file.
14
47
  */
15
- export function readJsonBlob(file: File): Promise<any>;
48
+ export function readJsonBlob(file: File): Promise<Record<string | number | symbol, any> | any[]>;
16
49
  /**
17
50
  * Saves a JSON object as a downloadable file.
18
51
  * @param {string} filename
@@ -44,18 +77,24 @@ export function fetchJson(url: string, { method, body, timeout, retries, headers
44
77
  }): Promise<any>;
45
78
  /**
46
79
  * Installs a script that toggles CSS classes on a given element
47
- * based on the page's visibility or focus state.
80
+ * based on the page's visibility or focus state, and optionally
81
+ * triggers callbacks on visibility changes.
48
82
  *
49
83
  * @param {Object} [settings={}]
50
84
  * @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
51
85
  * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
52
86
  * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
87
+ * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
88
+ * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
53
89
  * @returns {() => void} Function that removes all installed event listeners.
90
+ * @throws {TypeError} If any provided setting is invalid.
54
91
  */
55
- export function installWindowHiddenScript({ element, hiddenClass, visibleClass, }?: {
92
+ export function installWindowHiddenScript({ element, hiddenClass, visibleClass, onVisible, onHidden, }?: {
56
93
  element?: HTMLElement | undefined;
57
94
  hiddenClass?: string | undefined;
58
95
  visibleClass?: string | undefined;
96
+ onVisible?: (() => void) | undefined;
97
+ onHidden?: (() => void) | undefined;
59
98
  }): () => void;
60
99
  export function getHtmlElBordersWidth(el: Element): HtmlElBoxSides;
61
100
  export function getHtmlElBorders(el: Element): HtmlElBoxSides;
@@ -15,31 +15,101 @@ export function areHtmlElsColliding(elem1, elem2) {
15
15
  rect1.top > rect2.bottom);
16
16
  }
17
17
  /**
18
- * Reads and parses a JSON data using FileReader.
19
- * Throws an error if the content is not valid JSON.
20
- * @param {File} file
21
- * @returns {Promise<any>}
18
+ * Reads the contents of a file using the specified FileReader method.
19
+ *
20
+ * @param {File} file - The file to be read.
21
+ * @param {'readAsArrayBuffer'|'readAsDataURL'|'readAsText'|'readAsBinaryString'} method -
22
+ * The FileReader method to use for reading the file.
23
+ * @returns {Promise<any>} - A promise that resolves with the file content, according to the chosen method.
24
+ * @throws {Error} - If an unexpected error occurs while handling the result.
25
+ * @throws {DOMException} - If the FileReader encounters an error while reading the file.
22
26
  */
23
- export function readJsonBlob(file) {
27
+ export function readFileBlob(file, method) {
24
28
  return new Promise((resolve, reject) => {
25
29
  const reader = new FileReader();
26
30
  reader.onload = () => {
27
31
  try {
28
- // @ts-ignore
29
- const result = JSON.parse(reader.result);
30
- resolve(result);
32
+ resolve(reader.result);
31
33
  }
32
34
  catch (error) {
33
- // @ts-ignore
34
- reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
35
+ reject(error);
35
36
  }
36
37
  };
37
38
  reader.onerror = () => {
38
- reject(new Error(`Error reading file: ${file.name}`));
39
+ reject(reader.error);
39
40
  };
40
- reader.readAsText(file);
41
+ reader[method](file);
41
42
  });
42
43
  }
44
+ /**
45
+ * Reads a file as a Base64 string using FileReader, and optionally formats it as a full data URL.
46
+ *
47
+ * Performs strict validation to ensure the result is a valid Base64 string or a proper data URL.
48
+ *
49
+ * @param {File} file - The file to be read.
50
+ * @param {boolean|string} [isDataUrl=false] - If true, returns a full data URL; if false, returns only the Base64 string;
51
+ * if a string is passed, it is used as the MIME type in the data URL.
52
+ * @returns {Promise<string>} - A promise that resolves with the Base64 string or data URL.
53
+ *
54
+ * @throws {TypeError} - If the result is not a string or if `isDataUrl` is not a valid type.
55
+ * @throws {Error} - If the result does not match the expected data URL format or Base64 structure.
56
+ * @throws {DOMException} - If the FileReader fails to read the file.
57
+ */
58
+ export function readBase64Blob(file, isDataUrl = false) {
59
+ return new Promise((resolve, reject) => {
60
+ if (typeof isDataUrl !== 'string' && typeof isDataUrl !== 'boolean')
61
+ reject(new TypeError('The isDataUrl parameter must be a boolean or a string.'));
62
+ readFileBlob(file, 'readAsDataURL')
63
+ .then(
64
+ /**
65
+ * Ensure that the URL format is correct in the required pattern
66
+ * @param {string} base64Data
67
+ */ (base64Data) => {
68
+ if (typeof base64Data !== 'string')
69
+ throw new TypeError('Expected file content to be a string.');
70
+ const match = base64Data.match(/^data:(.+);base64,(.*)$/);
71
+ if (!match || !match[2])
72
+ throw new Error('Invalid data URL format or missing Base64 content.');
73
+ const [, mimeType, base64] = match;
74
+ if (!/^[\w/+]+=*$/.test(base64))
75
+ throw new Error('Base64 content is malformed.');
76
+ if (typeof isDataUrl === 'boolean')
77
+ return resolve(isDataUrl ? base64Data : base64);
78
+ if (!/^[\w-]+\/[\w.+-]+$/.test(isDataUrl))
79
+ throw new Error(`Invalid MIME type string: ${isDataUrl}`);
80
+ return resolve(`data:${isDataUrl};base64,${base64}`);
81
+ })
82
+ .catch(reject);
83
+ });
84
+ }
85
+ /**
86
+ * Reads a file and strictly validates its content as proper JSON using FileReader.
87
+ *
88
+ * Performs several checks to ensure the file contains valid, parsable JSON data.
89
+ *
90
+ * @param {File} file - The file to be read. It must contain valid JSON as plain text.
91
+ * @returns {Promise<Record<string|number|symbol, any>|any[]>} - A promise that resolves with the parsed JSON object.
92
+ *
93
+ * @throws {SyntaxError} - If the file content is not valid JSON syntax.
94
+ * @throws {TypeError} - If the result is not a string or does not represent a JSON value.
95
+ * @throws {Error} - If the result is empty or structurally invalid as JSON.
96
+ * @throws {DOMException} - If the FileReader fails to read the file.
97
+ */
98
+ export function readJsonBlob(file) {
99
+ return new Promise((resolve, reject) => readFileBlob(file, 'readAsText')
100
+ .then((data) => {
101
+ if (typeof data !== 'string')
102
+ throw new TypeError('Expected file content to be a string.');
103
+ const trimmed = data.trim();
104
+ if (trimmed.length === 0)
105
+ throw new Error('File is empty or contains only whitespace.');
106
+ const parsed = JSON.parse(trimmed);
107
+ if (typeof parsed !== 'object' || parsed === null)
108
+ throw new Error('Parsed content is not a valid JSON object or array.');
109
+ resolve(parsed);
110
+ })
111
+ .catch(reject));
112
+ }
43
113
  /**
44
114
  * Saves a JSON object as a downloadable file.
45
115
  * @param {string} filename
@@ -119,7 +189,7 @@ export async function fetchJson(url, { method = 'GET', body, timeout = 0, retrie
119
189
  if (!contentType.includes('application/json'))
120
190
  throw new Error(`Unexpected content-type: ${contentType}`);
121
191
  const data = await response.json();
122
- if (!isJsonObject(data))
192
+ if (!Array.isArray(data) && !isJsonObject(data))
123
193
  throw new Error('Received invalid data instead of valid JSON.');
124
194
  return data;
125
195
  }
@@ -208,23 +278,35 @@ export const getHtmlElPadding = (el) => {
208
278
  };
209
279
  /**
210
280
  * Installs a script that toggles CSS classes on a given element
211
- * based on the page's visibility or focus state.
281
+ * based on the page's visibility or focus state, and optionally
282
+ * triggers callbacks on visibility changes.
212
283
  *
213
284
  * @param {Object} [settings={}]
214
285
  * @param {HTMLElement} [settings.element=document.body] - The element to receive visibility classes.
215
286
  * @param {string} [settings.hiddenClass='windowHidden'] - CSS class applied when the page is hidden.
216
287
  * @param {string} [settings.visibleClass='windowVisible'] - CSS class applied when the page is visible.
288
+ * @param {() => void} [settings.onVisible] - Callback called when page becomes visible.
289
+ * @param {() => void} [settings.onHidden] - Callback called when page becomes hidden.
217
290
  * @returns {() => void} Function that removes all installed event listeners.
291
+ * @throws {TypeError} If any provided setting is invalid.
218
292
  */
219
- export function installWindowHiddenScript({ element = document.body, hiddenClass = 'windowHidden', visibleClass = 'windowVisible', } = {}) {
293
+ export function installWindowHiddenScript({ element = document.body, hiddenClass = 'windowHidden', visibleClass = 'windowVisible', onVisible, onHidden, } = {}) {
294
+ if (!(element instanceof HTMLElement))
295
+ throw new TypeError(`"element" must be an instance of HTMLElement.`);
296
+ if (typeof hiddenClass !== 'string')
297
+ throw new TypeError(`"hiddenClass" must be a string.`);
298
+ if (typeof visibleClass !== 'string')
299
+ throw new TypeError(`"visibleClass" must be a string.`);
300
+ if (onVisible !== undefined && typeof onVisible !== 'function')
301
+ throw new TypeError(`"onVisible" must be a function if provided.`);
302
+ if (onHidden !== undefined && typeof onHidden !== 'function')
303
+ throw new TypeError(`"onHidden" must be a function if provided.`);
220
304
  const removeClass = () => {
221
305
  element.classList.remove(hiddenClass);
222
306
  element.classList.remove(visibleClass);
223
307
  };
224
308
  /** @type {string|null} */
225
309
  let hiddenProp = null;
226
- /** @type {(this: any, evt: Event) => void} */
227
- let handler;
228
310
  const visibilityEvents = [
229
311
  'visibilitychange',
230
312
  'mozvisibilitychange',
@@ -238,7 +320,8 @@ export function installWindowHiddenScript({ element = document.body, hiddenClass
238
320
  break;
239
321
  }
240
322
  }
241
- handler = function (evt) {
323
+ /** @type {(this: any, evt: Event) => void} */
324
+ const handler = function (evt) {
242
325
  removeClass();
243
326
  const type = evt?.type;
244
327
  // @ts-ignore
@@ -247,12 +330,21 @@ export function installWindowHiddenScript({ element = document.body, hiddenClass
247
330
  const hiddenEvents = ['blur', 'focusout', 'pagehide'];
248
331
  if (visibleEvents.includes(type)) {
249
332
  element.classList.add(visibleClass);
333
+ onVisible?.();
250
334
  }
251
335
  else if (hiddenEvents.includes(type)) {
252
336
  element.classList.add(hiddenClass);
337
+ onHidden?.();
253
338
  }
254
339
  else {
255
- element.classList.add(isHidden ? hiddenClass : visibleClass);
340
+ if (isHidden) {
341
+ element.classList.add(hiddenClass);
342
+ onHidden?.();
343
+ }
344
+ else {
345
+ element.classList.add(visibleClass);
346
+ onVisible?.();
347
+ }
256
348
  }
257
349
  };
258
350
  /** @type {() => void} */
@@ -287,6 +379,7 @@ export function installWindowHiddenScript({ element = document.body, hiddenClass
287
379
  removeClass();
288
380
  };
289
381
  }
382
+ // Trigger initial state
290
383
  // @ts-ignore
291
384
  const simulatedEvent = new Event(hiddenProp && document[hiddenProp] ? 'blur' : 'focus');
292
385
  handler(simulatedEvent);
@@ -26,6 +26,8 @@ exports.getHtmlElBordersWidth = html.getHtmlElBordersWidth;
26
26
  exports.getHtmlElMargin = html.getHtmlElMargin;
27
27
  exports.getHtmlElPadding = html.getHtmlElPadding;
28
28
  exports.installWindowHiddenScript = html.installWindowHiddenScript;
29
+ exports.readBase64Blob = html.readBase64Blob;
30
+ exports.readFileBlob = html.readFileBlob;
29
31
  exports.readJsonBlob = html.readJsonBlob;
30
32
  exports.saveJsonFile = html.saveJsonFile;
31
33
  exports.cloneObjTypeOrder = objFilter.cloneObjTypeOrder;
@@ -47,5 +49,6 @@ exports.getAge = simpleMath.getAge;
47
49
  exports.getSimplePerc = simpleMath.getSimplePerc;
48
50
  exports.ruleOfThree = simpleMath.ruleOfThree;
49
51
  exports.addAiMarkerShortcut = text.addAiMarkerShortcut;
52
+ exports.safeTextTrim = text.safeTextTrim;
50
53
  exports.toTitleCase = text.toTitleCase;
51
54
  exports.toTitleCaseLowerFirst = text.toTitleCaseLowerFirst;
@@ -1,3 +1,4 @@
1
+ import { safeTextTrim } from './text.mjs';
1
2
  import { installWindowHiddenScript } from './html.mjs';
2
3
  import { genFibonacciSeq } from './simpleMath.mjs';
3
4
  import { getHtmlElBorders } from './html.mjs';
@@ -6,6 +7,8 @@ import { getHtmlElMargin } from './html.mjs';
6
7
  import { getHtmlElPadding } from './html.mjs';
7
8
  import { fetchJson } from './html.mjs';
8
9
  import { readJsonBlob } from './html.mjs';
10
+ import { readFileBlob } from './html.mjs';
11
+ import { readBase64Blob } from './html.mjs';
9
12
  import { saveJsonFile } from './html.mjs';
10
13
  import { documentIsFullScreen } from './fullScreen.mjs';
11
14
  import { isScreenFilled } from './fullScreen.mjs';
@@ -35,5 +38,5 @@ import { getTimeDuration } from './clock.mjs';
35
38
  import { shuffleArray } from './array.mjs';
36
39
  import { toTitleCase } from './text.mjs';
37
40
  import { toTitleCaseLowerFirst } from './text.mjs';
38
- export { installWindowHiddenScript, genFibonacciSeq, getHtmlElBorders, getHtmlElBordersWidth, getHtmlElMargin, getHtmlElPadding, fetchJson, readJsonBlob, saveJsonFile, documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, areHtmlElsColliding, isJsonObject, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
41
+ export { safeTextTrim, installWindowHiddenScript, genFibonacciSeq, getHtmlElBorders, getHtmlElBordersWidth, getHtmlElMargin, getHtmlElPadding, fetchJson, readJsonBlob, readFileBlob, readBase64Blob, saveJsonFile, documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, areHtmlElsColliding, isJsonObject, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst };
39
42
  //# sourceMappingURL=index.d.mts.map
@@ -2,9 +2,9 @@ import arraySortPositions from '../../legacy/libs/arraySortPositions.mjs';
2
2
  import asyncReplace from '../../legacy/libs/replaceAsync.mjs';
3
3
  import { shuffleArray } from './array.mjs';
4
4
  import { formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration } from './clock.mjs';
5
- import { areHtmlElsColliding, readJsonBlob, saveJsonFile, fetchJson, getHtmlElBorders, getHtmlElBordersWidth, getHtmlElMargin, getHtmlElPadding, installWindowHiddenScript, } from './html.mjs';
5
+ import { areHtmlElsColliding, readJsonBlob, saveJsonFile, fetchJson, getHtmlElBorders, getHtmlElBordersWidth, getHtmlElMargin, getHtmlElPadding, installWindowHiddenScript, readFileBlob, readBase64Blob, } from './html.mjs';
6
6
  import { countObj, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, objType, isJsonObject, } from './objFilter.mjs';
7
7
  import { documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, } from './fullScreen.mjs';
8
8
  import { formatBytes, genFibonacciSeq, getAge, getSimplePerc, ruleOfThree } from './simpleMath.mjs';
9
- import { addAiMarkerShortcut, toTitleCase, toTitleCaseLowerFirst } from './text.mjs';
10
- export { installWindowHiddenScript, genFibonacciSeq, getHtmlElBorders, getHtmlElBordersWidth, getHtmlElMargin, getHtmlElPadding, fetchJson, readJsonBlob, saveJsonFile, documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, areHtmlElsColliding, isJsonObject, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst, };
9
+ import { addAiMarkerShortcut, safeTextTrim, toTitleCase, toTitleCaseLowerFirst } from './text.mjs';
10
+ export { safeTextTrim, installWindowHiddenScript, genFibonacciSeq, getHtmlElBorders, getHtmlElBordersWidth, getHtmlElMargin, getHtmlElPadding, fetchJson, readJsonBlob, readFileBlob, readBase64Blob, saveJsonFile, documentIsFullScreen, isScreenFilled, requestFullScreen, exitFullScreen, isFullScreenMode, onFullScreenChange, offFullScreenChange, areHtmlElsColliding, isJsonObject, arraySortPositions, formatBytes, addAiMarkerShortcut, extendObjType, reorderObjTypeOrder, cloneObjTypeOrder, countObj, objType, ruleOfThree, getSimplePerc, asyncReplace, getAge, formatCustomTimer, formatDayTimer, formatTimer, getTimeDuration, shuffleArray, toTitleCase, toTitleCaseLowerFirst, };
@@ -64,6 +64,48 @@ function addAiMarkerShortcut(key = 'a') {
64
64
  });
65
65
  }
66
66
 
67
+ /**
68
+ * Trims a text string to a specified character limit, attempting to avoid cutting words in half.
69
+ * If a space is found before the limit and it's not too far from the limit (at least 60%),
70
+ * the cut is made at that space; otherwise, the text is hard-cut at the limit.
71
+ * If the input is shorter than the limit, it is returned unchanged.
72
+ *
73
+ * @param {string} text - The input text to be trimmed.
74
+ * @param {number} limit - The maximum number of characters allowed.
75
+ * @param {number} [safeCutZone=0.6] - A decimal between 0 and 1 representing the minimal acceptable position
76
+ * (as a fraction of `limit`) to cut at a space. Defaults to 0.6.
77
+ * @returns {string} - The trimmed text, possibly ending with an ellipsis ("...").
78
+ * @throws {TypeError} - Throws if `text` is not a string.
79
+ * @throws {TypeError} - Throws if `limit` is not a positive integer.
80
+ * @throws {TypeError} - Throws if `safeCutZone` is not a number between 0 and 1 (inclusive).
81
+ */
82
+ function safeTextTrim(text, limit, safeCutZone = 0.6) {
83
+ if (typeof text !== 'string')
84
+ throw new TypeError(`Expected a string for 'text', but received ${typeof text}`);
85
+ if (!Number.isInteger(limit) || limit <= 0)
86
+ throw new TypeError(`Expected 'limit' to be a positive integer, but received ${limit}`);
87
+ if (typeof safeCutZone !== 'number' || safeCutZone < 0 || safeCutZone > 1)
88
+ throw new TypeError(
89
+ `Expected 'safeCutZone' to be a number between 0 and 1, but received ${safeCutZone}`,
90
+ );
91
+
92
+ let result = text.trim();
93
+ if (result.length > limit) {
94
+ // Try to cut the string into a space before the limit
95
+ const safeCut = result.lastIndexOf(' ', limit);
96
+
97
+ if (safeCut > 0 && safeCut >= limit * safeCutZone) {
98
+ // Only cuts where there is a space, and if the cut is not too early
99
+ return `${result.substring(0, safeCut).trim()}...`;
100
+ } else {
101
+ // Emergency: Cuts straight to the limit and adds "...".
102
+ return `${result.substring(0, limit).trim()}...`;
103
+ }
104
+ }
105
+
106
+ return result;
107
+ }
108
+
67
109
  /*
68
110
  import { useEffect } from "react";
69
111
 
@@ -89,5 +131,6 @@ export default KeyPressHandler;
89
131
  */
90
132
 
91
133
  exports.addAiMarkerShortcut = addAiMarkerShortcut;
134
+ exports.safeTextTrim = safeTextTrim;
92
135
  exports.toTitleCase = toTitleCase;
93
136
  exports.toTitleCaseLowerFirst = toTitleCaseLowerFirst;
@@ -32,4 +32,20 @@ export function toTitleCaseLowerFirst(str: string): string;
32
32
  * @param {string} [key='a'] - The lowercase character key to be used in combination with Ctrl and Alt.
33
33
  */
34
34
  export function addAiMarkerShortcut(key?: string): void;
35
+ /**
36
+ * Trims a text string to a specified character limit, attempting to avoid cutting words in half.
37
+ * If a space is found before the limit and it's not too far from the limit (at least 60%),
38
+ * the cut is made at that space; otherwise, the text is hard-cut at the limit.
39
+ * If the input is shorter than the limit, it is returned unchanged.
40
+ *
41
+ * @param {string} text - The input text to be trimmed.
42
+ * @param {number} limit - The maximum number of characters allowed.
43
+ * @param {number} [safeCutZone=0.6] - A decimal between 0 and 1 representing the minimal acceptable position
44
+ * (as a fraction of `limit`) to cut at a space. Defaults to 0.6.
45
+ * @returns {string} - The trimmed text, possibly ending with an ellipsis ("...").
46
+ * @throws {TypeError} - Throws if `text` is not a string.
47
+ * @throws {TypeError} - Throws if `limit` is not a positive integer.
48
+ * @throws {TypeError} - Throws if `safeCutZone` is not a number between 0 and 1 (inclusive).
49
+ */
50
+ export function safeTextTrim(text: string, limit: number, safeCutZone?: number): string;
35
51
  //# sourceMappingURL=text.d.mts.map