tiny-essentials 1.11.0 → 1.12.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 (72) hide show
  1. package/README.md +12 -3
  2. package/dist/node_modules/firebase-functions/lib/common/trace.cjs +82 -0
  3. package/dist/node_modules/firebase-functions/lib/logger/common.cjs +57 -0
  4. package/dist/node_modules/firebase-functions/lib/logger/index.cjs +158 -0
  5. package/dist/v1/TinyBasicsEs.js +1653 -1
  6. package/dist/v1/TinyBasicsEs.min.js +1 -1
  7. package/dist/v1/TinyDragDropDetector.js +307 -0
  8. package/dist/v1/TinyDragDropDetector.min.js +1 -0
  9. package/dist/v1/TinyDragger.js +3563 -0
  10. package/dist/v1/TinyDragger.min.js +2 -0
  11. package/dist/v1/TinyDragger.min.js.LICENSE.txt +8 -0
  12. package/dist/v1/TinyEssentials.js +2783 -2
  13. package/dist/v1/TinyEssentials.min.js +1 -1
  14. package/dist/v1/TinyNotifyCenter.js +32 -0
  15. package/dist/v1/TinyNotifyCenter.min.js +1 -1
  16. package/dist/v1/TinyToastNotify.js +24 -0
  17. package/dist/v1/TinyToastNotify.min.js +1 -1
  18. package/dist/v1/TinyUploadClicker.js +5235 -0
  19. package/dist/v1/TinyUploadClicker.min.js +2 -0
  20. package/dist/v1/TinyUploadClicker.min.js.LICENSE.txt +8 -0
  21. package/dist/v1/basics/fileManager.cjs +524 -0
  22. package/dist/v1/basics/fileManager.d.mts +209 -0
  23. package/dist/v1/basics/fileManager.mjs +443 -0
  24. package/dist/v1/basics/fullScreen.cjs +134 -0
  25. package/dist/v1/basics/fullScreen.d.mts +8 -0
  26. package/dist/v1/basics/fullScreen.mjs +121 -0
  27. package/dist/v1/basics/html.cjs +262 -0
  28. package/dist/v1/basics/html.d.mts +63 -0
  29. package/dist/v1/basics/html.mjs +208 -0
  30. package/dist/v1/basics/index.cjs +40 -0
  31. package/dist/v1/basics/index.d.mts +38 -1
  32. package/dist/v1/basics/index.mjs +4 -1
  33. package/dist/v1/build/TinyDragDropDetector.cjs +7 -0
  34. package/dist/v1/build/TinyDragDropDetector.d.mts +3 -0
  35. package/dist/v1/build/TinyDragDropDetector.mjs +2 -0
  36. package/dist/v1/build/TinyDragger.cjs +7 -0
  37. package/dist/v1/build/TinyDragger.d.mts +3 -0
  38. package/dist/v1/build/TinyDragger.mjs +2 -0
  39. package/dist/v1/build/TinyUploadClicker.cjs +7 -0
  40. package/dist/v1/build/TinyUploadClicker.d.mts +3 -0
  41. package/dist/v1/build/TinyUploadClicker.mjs +2 -0
  42. package/dist/v1/css/TinyDraggerExample.css +21 -0
  43. package/dist/v1/css/TinyDraggerExample.min.css +1 -0
  44. package/dist/v1/index.cjs +44 -0
  45. package/dist/v1/index.d.mts +40 -1
  46. package/dist/v1/index.mjs +6 -1
  47. package/dist/v1/libs/TinyDragDropDetector.cjs +268 -0
  48. package/dist/v1/libs/TinyDragDropDetector.d.mts +123 -0
  49. package/dist/v1/libs/TinyDragDropDetector.mjs +228 -0
  50. package/dist/v1/libs/TinyDragger.cjs +801 -0
  51. package/dist/v1/libs/TinyDragger.d.mts +300 -0
  52. package/dist/v1/libs/TinyDragger.mjs +689 -0
  53. package/dist/v1/libs/TinyNotifyCenter.cjs +32 -0
  54. package/dist/v1/libs/TinyNotifyCenter.d.mts +9 -0
  55. package/dist/v1/libs/TinyNotifyCenter.mjs +28 -0
  56. package/dist/v1/libs/TinyToastNotify.cjs +24 -0
  57. package/dist/v1/libs/TinyToastNotify.d.mts +8 -0
  58. package/dist/v1/libs/TinyToastNotify.mjs +20 -0
  59. package/dist/v1/libs/TinyUploadClicker.cjs +223 -0
  60. package/dist/v1/libs/TinyUploadClicker.d.mts +80 -0
  61. package/dist/v1/libs/TinyUploadClicker.mjs +182 -0
  62. package/docs/v1/README.md +8 -2
  63. package/docs/v1/basics/fileManager.md +222 -0
  64. package/docs/v1/basics/fullScreen.md +183 -0
  65. package/docs/v1/basics/html.md +243 -0
  66. package/docs/v1/libs/TinyDragDropDetector.md +143 -0
  67. package/docs/v1/libs/TinyDragger.md +415 -0
  68. package/docs/v1/libs/TinyLevelUp.md +1 -1
  69. package/docs/v1/libs/TinyNotifyCenter.md +20 -0
  70. package/docs/v1/libs/TinyToastNotify.md +19 -0
  71. package/docs/v1/libs/TinyUploadClicker.md +108 -0
  72. package/package.json +12 -2
@@ -417,6 +417,38 @@ class TinyNotifyCenter {
417
417
  get count() {
418
418
  return this.#count;
419
419
  }
420
+
421
+ /**
422
+ * Destroys the notification center instance, removing all event listeners,
423
+ * clearing notifications, and optionally removing DOM elements.
424
+ *
425
+ * Call this when the notification center is no longer needed to prevent memory leaks.
426
+ *
427
+ * @returns {void}
428
+ */
429
+ destroy() {
430
+ // Remove event listeners
431
+ this.#button?.removeEventListener('click', this.toggle);
432
+ this.#center?.querySelector('.close')?.removeEventListener('click', this.close);
433
+ this.#center?.querySelector('.clear-all')?.removeEventListener('click', this.clear);
434
+ this.#overlay?.removeEventListener('click', this.close);
435
+
436
+ // Clear all notifications
437
+ this.clear();
438
+
439
+ this.#center?.remove();
440
+ this.#overlay?.remove();
441
+ this.#button?.remove();
442
+
443
+ // Clean internal references
444
+ // this.#center = null;
445
+ // this.#list = null;
446
+ // this.#badge = null;
447
+ // this.#button = null;
448
+ // this.#overlay = null;
449
+ this.#count = 0;
450
+ this.#modes = new WeakMap();
451
+ }
420
452
  }
421
453
 
422
454
  module.exports = TinyNotifyCenter;
@@ -161,6 +161,15 @@ declare class TinyNotifyCenter {
161
161
  * @returns {number}
162
162
  */
163
163
  get count(): number;
164
+ /**
165
+ * Destroys the notification center instance, removing all event listeners,
166
+ * clearing notifications, and optionally removing DOM elements.
167
+ *
168
+ * Call this when the notification center is no longer needed to prevent memory leaks.
169
+ *
170
+ * @returns {void}
171
+ */
172
+ destroy(): void;
164
173
  #private;
165
174
  }
166
175
  //# sourceMappingURL=TinyNotifyCenter.d.mts.map
@@ -381,5 +381,33 @@ class TinyNotifyCenter {
381
381
  get count() {
382
382
  return this.#count;
383
383
  }
384
+ /**
385
+ * Destroys the notification center instance, removing all event listeners,
386
+ * clearing notifications, and optionally removing DOM elements.
387
+ *
388
+ * Call this when the notification center is no longer needed to prevent memory leaks.
389
+ *
390
+ * @returns {void}
391
+ */
392
+ destroy() {
393
+ // Remove event listeners
394
+ this.#button?.removeEventListener('click', this.toggle);
395
+ this.#center?.querySelector('.close')?.removeEventListener('click', this.close);
396
+ this.#center?.querySelector('.clear-all')?.removeEventListener('click', this.clear);
397
+ this.#overlay?.removeEventListener('click', this.close);
398
+ // Clear all notifications
399
+ this.clear();
400
+ this.#center?.remove();
401
+ this.#overlay?.remove();
402
+ this.#button?.remove();
403
+ // Clean internal references
404
+ // this.#center = null;
405
+ // this.#list = null;
406
+ // this.#badge = null;
407
+ // this.#button = null;
408
+ // this.#overlay = null;
409
+ this.#count = 0;
410
+ this.#modes = new WeakMap();
411
+ }
384
412
  }
385
413
  export default TinyNotifyCenter;
@@ -46,6 +46,8 @@ class TinyToastNotify {
46
46
  #baseDuration;
47
47
  #extraPerChar;
48
48
  #fadeOutDuration;
49
+
50
+ /** @type {HTMLElement|null} */
49
51
  #container;
50
52
 
51
53
  /**
@@ -373,6 +375,28 @@ class TinyToastNotify {
373
375
 
374
376
  setTimeout(() => close(), totalTime);
375
377
  }
378
+
379
+ /**
380
+ * Destroys the notification container and removes all active notifications.
381
+ * This should be called when the notification system is no longer needed,
382
+ * such as when unloading a page or switching views in a single-page app.
383
+ *
384
+ * @returns {void}
385
+ */
386
+ destroy() {
387
+ if (!(this.#container instanceof HTMLElement)) return;
388
+
389
+ // Remove all child notifications
390
+ this.#container.querySelectorAll('.notify').forEach((el) => el.remove());
391
+
392
+ // Remove the container itself from the DOM
393
+ if (this.#container.parentNode) {
394
+ this.#container.parentNode.removeChild(this.#container);
395
+ }
396
+
397
+ // Optional: Clean internal references (safe practice)
398
+ this.#container = null;
399
+ }
376
400
  }
377
401
 
378
402
  module.exports = TinyToastNotify;
@@ -152,6 +152,14 @@ declare class TinyToastNotify {
152
152
  * @param {NotifyData} data
153
153
  */
154
154
  show(data: NotifyData): void;
155
+ /**
156
+ * Destroys the notification container and removes all active notifications.
157
+ * This should be called when the notification system is no longer needed,
158
+ * such as when unloading a page or switching views in a single-page app.
159
+ *
160
+ * @returns {void}
161
+ */
162
+ destroy(): void;
155
163
  #private;
156
164
  }
157
165
  //# sourceMappingURL=TinyToastNotify.d.mts.map
@@ -42,6 +42,7 @@ class TinyToastNotify {
42
42
  #baseDuration;
43
43
  #extraPerChar;
44
44
  #fadeOutDuration;
45
+ /** @type {HTMLElement|null} */
45
46
  #container;
46
47
  /**
47
48
  * @param {'top'|'bottom'} y - 'top' or 'bottom'
@@ -328,5 +329,24 @@ class TinyToastNotify {
328
329
  }, 1);
329
330
  setTimeout(() => close(), totalTime);
330
331
  }
332
+ /**
333
+ * Destroys the notification container and removes all active notifications.
334
+ * This should be called when the notification system is no longer needed,
335
+ * such as when unloading a page or switching views in a single-page app.
336
+ *
337
+ * @returns {void}
338
+ */
339
+ destroy() {
340
+ if (!(this.#container instanceof HTMLElement))
341
+ return;
342
+ // Remove all child notifications
343
+ this.#container.querySelectorAll('.notify').forEach((el) => el.remove());
344
+ // Remove the container itself from the DOM
345
+ if (this.#container.parentNode) {
346
+ this.#container.parentNode.removeChild(this.#container);
347
+ }
348
+ // Optional: Clean internal references (safe practice)
349
+ this.#container = null;
350
+ }
331
351
  }
332
352
  export default TinyToastNotify;
@@ -0,0 +1,223 @@
1
+ 'use strict';
2
+
3
+ var objFilter = require('../basics/objFilter.cjs');
4
+ require('fs');
5
+ require('path');
6
+
7
+ /**
8
+ * @typedef {Object} UploaderConfig
9
+ *
10
+ * @property {string | HTMLElement | Array<string | HTMLElement>} triggers -
11
+ * Single or multiple elements (or selectors) that will act as upload triggers.
12
+ *
13
+ * @property {boolean} [multiple=false] -
14
+ * Whether to allow selection of multiple files.
15
+ *
16
+ * @property {string|string[]} [accept=""] -
17
+ * A comma-separated list of accepted file types (e.g., ".png,.jpg" or "image/*").
18
+ *
19
+ * @property {Record<string, string>} [inputAttributes={}] -
20
+ * Additional attributes to apply to the created `<input type="file">`.
21
+ *
22
+ * @property {Partial<CSSStyleDeclaration>} [inputStyles={ display: 'none' }] -
23
+ * Inline CSS styles to apply to the hidden input. Default hides the element.
24
+ *
25
+ * @property {((triggerElement: HTMLElement) => void)|null} [onClick=null] -
26
+ * Callback executed when a trigger is clicked (before the file dialog opens).
27
+ *
28
+ * @property {((files: FileList, triggerElement: HTMLElement) => void)|null} [onFileLoad=null] -
29
+ * Callback executed when files are selected through the file input.
30
+ */
31
+
32
+
33
+ /**
34
+ * TinyUploadClicker is a lightweight utility class for attaching custom clickable elements
35
+ * (like buttons, divs, icons, etc.) that trigger hidden file input elements.
36
+ *
37
+ * It provides full control over the input file element without needing to display it,
38
+ * and allows per-trigger customization, style injection, and event handling.
39
+ *
40
+ * Each trigger element creates its own associated hidden file input, enabling multiple
41
+ * upload zones with different behaviors. You can handle `onClick` and `onFileLoad` callbacks,
42
+ * define accepted file types, allow multiple selections, and cleanly destroy everything via `destroy()`.
43
+ *
44
+ * Designed for maximum flexibility in pure JavaScript environments, including dynamic UI rendering.
45
+ *
46
+ * @example
47
+ * const uploader = new TinyUploadClicker({
48
+ * triggers: '#uploadBtn',
49
+ * accept: ['.png', '.jpg'],
50
+ * multiple: true,
51
+ * inputAttributes: { 'data-tracker': 'upload' },
52
+ * inputStyles: { display: 'none' },
53
+ * onClick: (el) => console.log('Trigger clicked:', el),
54
+ * onFileLoad: (files, el) => console.log('Files selected:', files)
55
+ * });
56
+ *
57
+ * // Later, if needed:
58
+ * uploader.destroy();
59
+ */
60
+ class TinyUploadClicker {
61
+ /** @type {UploaderConfig} */
62
+ #config;
63
+
64
+ /** @type {Array<HTMLElement|Element|null>} */
65
+ #triggerElements = [];
66
+
67
+ /** @type {WeakMap<HTMLElement, HTMLInputElement>} */
68
+ #inputs = new WeakMap();
69
+
70
+ /** @type {(ev: MouseEvent) => void} */
71
+ #boundClick;
72
+
73
+ /**
74
+ * Creates a new instance of TinyUploadClicker.
75
+ *
76
+ * @param {UploaderConfig} options - Configuration object for customizing the uploader behavior and appearance.
77
+ * @throws {TypeError} If the config is invalid or required options are missing.
78
+ */
79
+ constructor(options) {
80
+ if (!objFilter.isJsonObject(options))
81
+ throw new TypeError('TinyUploadClicker: "options" must be a valid object.');
82
+
83
+ this.#config = {
84
+ multiple: false,
85
+ accept: '',
86
+ inputAttributes: {},
87
+ inputStyles: { display: 'none' },
88
+ onClick: null,
89
+ onFileLoad: null,
90
+ ...options,
91
+ };
92
+
93
+ const { triggers } = options;
94
+
95
+ if (
96
+ !Array.isArray(triggers) &&
97
+ typeof triggers !== 'string' &&
98
+ !(triggers instanceof HTMLElement)
99
+ )
100
+ throw new TypeError(
101
+ 'TinyUploadClicker: "triggers" is required and must be a string, HTMLElement, or an array of them.',
102
+ );
103
+
104
+ const triggerList = Array.isArray(this.#config.triggers)
105
+ ? this.#config.triggers
106
+ : [this.#config.triggers];
107
+
108
+ const resolvedTriggers = triggerList.map((ref) => {
109
+ if (typeof ref === 'string') {
110
+ const el = document.querySelector(ref);
111
+ if (!el) {
112
+ throw new Error(`TinyUploadClicker: No element found for selector "${ref}".`);
113
+ }
114
+ return el;
115
+ }
116
+
117
+ if (!(ref instanceof HTMLElement))
118
+ throw new TypeError(
119
+ 'TinyUploadClicker: All triggers must be either strings (selectors) or HTMLElements.',
120
+ );
121
+ return ref;
122
+ });
123
+
124
+ // Validate optional values
125
+ if (
126
+ options.accept !== undefined &&
127
+ typeof options.accept !== 'string' &&
128
+ !Array.isArray(options.accept)
129
+ )
130
+ throw new TypeError('TinyUploadClicker: "accept" must be a string or an array of strings.');
131
+
132
+ if (
133
+ options.onClick !== undefined &&
134
+ typeof options.onClick !== 'function' &&
135
+ options.onClick !== null
136
+ )
137
+ throw new TypeError('TinyUploadClicker: "onClick" must be a function or null.');
138
+
139
+ if (
140
+ options.onFileLoad !== undefined &&
141
+ typeof options.onFileLoad !== 'function' &&
142
+ options.onFileLoad !== null
143
+ )
144
+ throw new TypeError('TinyUploadClicker: "onFileLoad" must be a function or null.');
145
+
146
+ if (options.inputAttributes !== undefined && !objFilter.isJsonObject(options.inputAttributes))
147
+ throw new TypeError('TinyUploadClicker: "inputAttributes" must be an object.');
148
+
149
+ if (options.inputStyles !== undefined && !objFilter.isJsonObject(options.inputStyles))
150
+ throw new TypeError('TinyUploadClicker: "inputStyles" must be an object.');
151
+
152
+ this.#boundClick = this.#handleClick.bind(this);
153
+ this.#triggerElements = resolvedTriggers;
154
+
155
+ this.#triggerElements.forEach((trigger) => {
156
+ if (!(trigger instanceof HTMLElement)) return;
157
+ const input = document.createElement('input');
158
+ input.type = 'file';
159
+ if (this.#config.multiple) input.multiple = true;
160
+ if (typeof this.#config.accept === 'string') input.accept = this.#config.accept;
161
+ else if (Array.isArray(this.#config.accept)) input.accept = this.#config.accept.join(',');
162
+
163
+ // Apply attributes
164
+ const attr = this.#config.inputAttributes;
165
+ if (attr && typeof attr === 'object') {
166
+ for (const [key, value] of Object.entries(attr)) {
167
+ input.setAttribute(key, value);
168
+ }
169
+ }
170
+
171
+ // Apply styles
172
+ Object.assign(input.style, this.#config.inputStyles || { display: 'none' });
173
+
174
+ // Listen for file changes
175
+ input.addEventListener('change', () => {
176
+ if (input.files && typeof this.#config.onFileLoad === 'function')
177
+ this.#config.onFileLoad(input.files, trigger);
178
+
179
+ input.value = ''; // reset so same file can be reselected
180
+ });
181
+
182
+ document.body.appendChild(input);
183
+ this.#inputs.set(trigger, input);
184
+
185
+ trigger.addEventListener('click', this.#boundClick);
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Handles the click on a trigger element and forwards it to the associated file input.
191
+ *
192
+ * @param {MouseEvent} event - The click event object.
193
+ */
194
+ #handleClick(event) {
195
+ const trigger = event.currentTarget;
196
+ if (!(trigger instanceof HTMLElement)) return;
197
+ const input = this.#inputs.get(trigger);
198
+ if (!input) return;
199
+
200
+ if (typeof this.#config.onClick === 'function') this.#config.onClick(trigger);
201
+
202
+ input.click();
203
+ }
204
+
205
+ /**
206
+ * Cleans up all internal elements and event listeners, removing created inputs and breaking references.
207
+ */
208
+ destroy() {
209
+ for (const trigger of this.#triggerElements) {
210
+ if (!(trigger instanceof HTMLElement)) return;
211
+ trigger.removeEventListener('click', this.#boundClick);
212
+
213
+ const input = this.#inputs.get(trigger);
214
+ if (input && input.parentNode) input.remove();
215
+
216
+ this.#inputs.delete(trigger);
217
+ }
218
+
219
+ this.#triggerElements = [];
220
+ }
221
+ }
222
+
223
+ module.exports = TinyUploadClicker;
@@ -0,0 +1,80 @@
1
+ export default TinyUploadClicker;
2
+ export type UploaderConfig = {
3
+ /**
4
+ * -
5
+ * Single or multiple elements (or selectors) that will act as upload triggers.
6
+ */
7
+ triggers: string | HTMLElement | Array<string | HTMLElement>;
8
+ /**
9
+ * -
10
+ * Whether to allow selection of multiple files.
11
+ */
12
+ multiple?: boolean | undefined;
13
+ /**
14
+ * -
15
+ * A comma-separated list of accepted file types (e.g., ".png,.jpg" or "image/*").
16
+ */
17
+ accept?: string | string[] | undefined;
18
+ /**
19
+ * -
20
+ * Additional attributes to apply to the created `<input type="file">`.
21
+ */
22
+ inputAttributes?: Record<string, string> | undefined;
23
+ /**
24
+ * -
25
+ * Inline CSS styles to apply to the hidden input. Default hides the element.
26
+ */
27
+ inputStyles?: Partial<CSSStyleDeclaration> | undefined;
28
+ /**
29
+ * -
30
+ * Callback executed when a trigger is clicked (before the file dialog opens).
31
+ */
32
+ onClick?: ((triggerElement: HTMLElement) => void) | null | undefined;
33
+ /**
34
+ * -
35
+ * Callback executed when files are selected through the file input.
36
+ */
37
+ onFileLoad?: ((files: FileList, triggerElement: HTMLElement) => void) | null | undefined;
38
+ };
39
+ /**
40
+ * TinyUploadClicker is a lightweight utility class for attaching custom clickable elements
41
+ * (like buttons, divs, icons, etc.) that trigger hidden file input elements.
42
+ *
43
+ * It provides full control over the input file element without needing to display it,
44
+ * and allows per-trigger customization, style injection, and event handling.
45
+ *
46
+ * Each trigger element creates its own associated hidden file input, enabling multiple
47
+ * upload zones with different behaviors. You can handle `onClick` and `onFileLoad` callbacks,
48
+ * define accepted file types, allow multiple selections, and cleanly destroy everything via `destroy()`.
49
+ *
50
+ * Designed for maximum flexibility in pure JavaScript environments, including dynamic UI rendering.
51
+ *
52
+ * @example
53
+ * const uploader = new TinyUploadClicker({
54
+ * triggers: '#uploadBtn',
55
+ * accept: ['.png', '.jpg'],
56
+ * multiple: true,
57
+ * inputAttributes: { 'data-tracker': 'upload' },
58
+ * inputStyles: { display: 'none' },
59
+ * onClick: (el) => console.log('Trigger clicked:', el),
60
+ * onFileLoad: (files, el) => console.log('Files selected:', files)
61
+ * });
62
+ *
63
+ * // Later, if needed:
64
+ * uploader.destroy();
65
+ */
66
+ declare class TinyUploadClicker {
67
+ /**
68
+ * Creates a new instance of TinyUploadClicker.
69
+ *
70
+ * @param {UploaderConfig} options - Configuration object for customizing the uploader behavior and appearance.
71
+ * @throws {TypeError} If the config is invalid or required options are missing.
72
+ */
73
+ constructor(options: UploaderConfig);
74
+ /**
75
+ * Cleans up all internal elements and event listeners, removing created inputs and breaking references.
76
+ */
77
+ destroy(): void;
78
+ #private;
79
+ }
80
+ //# sourceMappingURL=TinyUploadClicker.d.mts.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @typedef {Object} UploaderConfig
3
+ *
4
+ * @property {string | HTMLElement | Array<string | HTMLElement>} triggers -
5
+ * Single or multiple elements (or selectors) that will act as upload triggers.
6
+ *
7
+ * @property {boolean} [multiple=false] -
8
+ * Whether to allow selection of multiple files.
9
+ *
10
+ * @property {string|string[]} [accept=""] -
11
+ * A comma-separated list of accepted file types (e.g., ".png,.jpg" or "image/*").
12
+ *
13
+ * @property {Record<string, string>} [inputAttributes={}] -
14
+ * Additional attributes to apply to the created `<input type="file">`.
15
+ *
16
+ * @property {Partial<CSSStyleDeclaration>} [inputStyles={ display: 'none' }] -
17
+ * Inline CSS styles to apply to the hidden input. Default hides the element.
18
+ *
19
+ * @property {((triggerElement: HTMLElement) => void)|null} [onClick=null] -
20
+ * Callback executed when a trigger is clicked (before the file dialog opens).
21
+ *
22
+ * @property {((files: FileList, triggerElement: HTMLElement) => void)|null} [onFileLoad=null] -
23
+ * Callback executed when files are selected through the file input.
24
+ */
25
+ import { isJsonObject } from '../index.mjs';
26
+ /**
27
+ * TinyUploadClicker is a lightweight utility class for attaching custom clickable elements
28
+ * (like buttons, divs, icons, etc.) that trigger hidden file input elements.
29
+ *
30
+ * It provides full control over the input file element without needing to display it,
31
+ * and allows per-trigger customization, style injection, and event handling.
32
+ *
33
+ * Each trigger element creates its own associated hidden file input, enabling multiple
34
+ * upload zones with different behaviors. You can handle `onClick` and `onFileLoad` callbacks,
35
+ * define accepted file types, allow multiple selections, and cleanly destroy everything via `destroy()`.
36
+ *
37
+ * Designed for maximum flexibility in pure JavaScript environments, including dynamic UI rendering.
38
+ *
39
+ * @example
40
+ * const uploader = new TinyUploadClicker({
41
+ * triggers: '#uploadBtn',
42
+ * accept: ['.png', '.jpg'],
43
+ * multiple: true,
44
+ * inputAttributes: { 'data-tracker': 'upload' },
45
+ * inputStyles: { display: 'none' },
46
+ * onClick: (el) => console.log('Trigger clicked:', el),
47
+ * onFileLoad: (files, el) => console.log('Files selected:', files)
48
+ * });
49
+ *
50
+ * // Later, if needed:
51
+ * uploader.destroy();
52
+ */
53
+ class TinyUploadClicker {
54
+ /** @type {UploaderConfig} */
55
+ #config;
56
+ /** @type {Array<HTMLElement|Element|null>} */
57
+ #triggerElements = [];
58
+ /** @type {WeakMap<HTMLElement, HTMLInputElement>} */
59
+ #inputs = new WeakMap();
60
+ /** @type {(ev: MouseEvent) => void} */
61
+ #boundClick;
62
+ /**
63
+ * Creates a new instance of TinyUploadClicker.
64
+ *
65
+ * @param {UploaderConfig} options - Configuration object for customizing the uploader behavior and appearance.
66
+ * @throws {TypeError} If the config is invalid or required options are missing.
67
+ */
68
+ constructor(options) {
69
+ if (!isJsonObject(options))
70
+ throw new TypeError('TinyUploadClicker: "options" must be a valid object.');
71
+ this.#config = {
72
+ multiple: false,
73
+ accept: '',
74
+ inputAttributes: {},
75
+ inputStyles: { display: 'none' },
76
+ onClick: null,
77
+ onFileLoad: null,
78
+ ...options,
79
+ };
80
+ const { triggers } = options;
81
+ if (!Array.isArray(triggers) &&
82
+ typeof triggers !== 'string' &&
83
+ !(triggers instanceof HTMLElement))
84
+ throw new TypeError('TinyUploadClicker: "triggers" is required and must be a string, HTMLElement, or an array of them.');
85
+ const triggerList = Array.isArray(this.#config.triggers)
86
+ ? this.#config.triggers
87
+ : [this.#config.triggers];
88
+ const resolvedTriggers = triggerList.map((ref) => {
89
+ if (typeof ref === 'string') {
90
+ const el = document.querySelector(ref);
91
+ if (!el) {
92
+ throw new Error(`TinyUploadClicker: No element found for selector "${ref}".`);
93
+ }
94
+ return el;
95
+ }
96
+ if (!(ref instanceof HTMLElement))
97
+ throw new TypeError('TinyUploadClicker: All triggers must be either strings (selectors) or HTMLElements.');
98
+ return ref;
99
+ });
100
+ // Validate optional values
101
+ if (options.accept !== undefined &&
102
+ typeof options.accept !== 'string' &&
103
+ !Array.isArray(options.accept))
104
+ throw new TypeError('TinyUploadClicker: "accept" must be a string or an array of strings.');
105
+ if (options.onClick !== undefined &&
106
+ typeof options.onClick !== 'function' &&
107
+ options.onClick !== null)
108
+ throw new TypeError('TinyUploadClicker: "onClick" must be a function or null.');
109
+ if (options.onFileLoad !== undefined &&
110
+ typeof options.onFileLoad !== 'function' &&
111
+ options.onFileLoad !== null)
112
+ throw new TypeError('TinyUploadClicker: "onFileLoad" must be a function or null.');
113
+ if (options.inputAttributes !== undefined && !isJsonObject(options.inputAttributes))
114
+ throw new TypeError('TinyUploadClicker: "inputAttributes" must be an object.');
115
+ if (options.inputStyles !== undefined && !isJsonObject(options.inputStyles))
116
+ throw new TypeError('TinyUploadClicker: "inputStyles" must be an object.');
117
+ this.#boundClick = this.#handleClick.bind(this);
118
+ this.#triggerElements = resolvedTriggers;
119
+ this.#triggerElements.forEach((trigger) => {
120
+ if (!(trigger instanceof HTMLElement))
121
+ return;
122
+ const input = document.createElement('input');
123
+ input.type = 'file';
124
+ if (this.#config.multiple)
125
+ input.multiple = true;
126
+ if (typeof this.#config.accept === 'string')
127
+ input.accept = this.#config.accept;
128
+ else if (Array.isArray(this.#config.accept))
129
+ input.accept = this.#config.accept.join(',');
130
+ // Apply attributes
131
+ const attr = this.#config.inputAttributes;
132
+ if (attr && typeof attr === 'object') {
133
+ for (const [key, value] of Object.entries(attr)) {
134
+ input.setAttribute(key, value);
135
+ }
136
+ }
137
+ // Apply styles
138
+ Object.assign(input.style, this.#config.inputStyles || { display: 'none' });
139
+ // Listen for file changes
140
+ input.addEventListener('change', () => {
141
+ if (input.files && typeof this.#config.onFileLoad === 'function')
142
+ this.#config.onFileLoad(input.files, trigger);
143
+ input.value = ''; // reset so same file can be reselected
144
+ });
145
+ document.body.appendChild(input);
146
+ this.#inputs.set(trigger, input);
147
+ trigger.addEventListener('click', this.#boundClick);
148
+ });
149
+ }
150
+ /**
151
+ * Handles the click on a trigger element and forwards it to the associated file input.
152
+ *
153
+ * @param {MouseEvent} event - The click event object.
154
+ */
155
+ #handleClick(event) {
156
+ const trigger = event.currentTarget;
157
+ if (!(trigger instanceof HTMLElement))
158
+ return;
159
+ const input = this.#inputs.get(trigger);
160
+ if (!input)
161
+ return;
162
+ if (typeof this.#config.onClick === 'function')
163
+ this.#config.onClick(trigger);
164
+ input.click();
165
+ }
166
+ /**
167
+ * Cleans up all internal elements and event listeners, removing created inputs and breaking references.
168
+ */
169
+ destroy() {
170
+ for (const trigger of this.#triggerElements) {
171
+ if (!(trigger instanceof HTMLElement))
172
+ return;
173
+ trigger.removeEventListener('click', this.#boundClick);
174
+ const input = this.#inputs.get(trigger);
175
+ if (input && input.parentNode)
176
+ input.remove();
177
+ this.#inputs.delete(trigger);
178
+ }
179
+ this.#triggerElements = [];
180
+ }
181
+ }
182
+ export default TinyUploadClicker;