tiny-essentials 1.10.2 → 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 (106) hide show
  1. package/README.md +13 -4
  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/{TinyEssentials.js → v1/TinyBasicsEs.js} +1726 -1362
  6. package/dist/v1/TinyBasicsEs.min.js +2 -0
  7. package/dist/v1/TinyDragDropDetector.js +307 -0
  8. package/dist/v1/TinyDragDropDetector.min.js +1 -0
  9. package/dist/{TinyBasicsEs.js → v1/TinyDragger.js} +1004 -506
  10. package/dist/v1/TinyDragger.min.js +2 -0
  11. package/dist/v1/TinyEssentials.js +7938 -0
  12. package/dist/v1/TinyEssentials.min.js +2 -0
  13. package/dist/v1/TinyEssentials.min.js.LICENSE.txt +8 -0
  14. package/dist/v1/TinyNotifyCenter.js +493 -0
  15. package/dist/v1/TinyNotifyCenter.min.js +1 -0
  16. package/dist/v1/TinyToastNotify.js +441 -0
  17. package/dist/v1/TinyToastNotify.min.js +1 -0
  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/TinyNotifyCenter.cjs +7 -0
  40. package/dist/v1/build/TinyNotifyCenter.d.mts +3 -0
  41. package/dist/v1/build/TinyNotifyCenter.mjs +2 -0
  42. package/dist/v1/build/TinyToastNotify.cjs +7 -0
  43. package/dist/v1/build/TinyToastNotify.d.mts +3 -0
  44. package/dist/v1/build/TinyToastNotify.mjs +2 -0
  45. package/dist/v1/build/TinyUploadClicker.cjs +7 -0
  46. package/dist/v1/build/TinyUploadClicker.d.mts +3 -0
  47. package/dist/v1/build/TinyUploadClicker.mjs +2 -0
  48. package/dist/v1/css/TinyDraggerExample.css +21 -0
  49. package/dist/v1/css/TinyDraggerExample.min.css +1 -0
  50. package/dist/v1/css/TinyNotify.css +350 -0
  51. package/dist/v1/css/TinyNotify.min.css +1 -0
  52. package/dist/v1/index.cjs +48 -0
  53. package/dist/v1/index.d.mts +42 -1
  54. package/dist/v1/index.mjs +8 -1
  55. package/dist/v1/libs/TinyDragDropDetector.cjs +268 -0
  56. package/dist/v1/libs/TinyDragDropDetector.d.mts +123 -0
  57. package/dist/v1/libs/TinyDragDropDetector.mjs +228 -0
  58. package/dist/v1/libs/TinyDragger.cjs +801 -0
  59. package/dist/v1/libs/TinyDragger.d.mts +300 -0
  60. package/dist/v1/libs/TinyDragger.mjs +689 -0
  61. package/dist/v1/libs/TinyNotifyCenter.cjs +454 -0
  62. package/dist/v1/libs/TinyNotifyCenter.d.mts +175 -0
  63. package/dist/v1/libs/TinyNotifyCenter.mjs +413 -0
  64. package/dist/v1/libs/TinyToastNotify.cjs +402 -0
  65. package/dist/v1/libs/TinyToastNotify.d.mts +165 -0
  66. package/dist/v1/libs/TinyToastNotify.mjs +352 -0
  67. package/dist/v1/libs/TinyUploadClicker.cjs +223 -0
  68. package/dist/v1/libs/TinyUploadClicker.d.mts +80 -0
  69. package/dist/v1/libs/TinyUploadClicker.mjs +182 -0
  70. package/docs/{README.md → v1/README.md} +8 -0
  71. package/docs/v1/basics/fileManager.md +222 -0
  72. package/docs/v1/basics/fullScreen.md +183 -0
  73. package/docs/v1/basics/html.md +243 -0
  74. package/docs/{basics → v1/basics}/text.md +2 -2
  75. package/docs/v1/libs/TinyDragDropDetector.md +143 -0
  76. package/docs/v1/libs/TinyDragger.md +415 -0
  77. package/docs/{libs → v1/libs}/TinyLevelUp.md +1 -1
  78. package/docs/v1/libs/TinyNotifyCenter.md +311 -0
  79. package/docs/v1/libs/TinyToastNotify.md +309 -0
  80. package/docs/v1/libs/TinyUploadClicker.md +108 -0
  81. package/package.json +14 -2
  82. package/dist/TinyBasicsEs.min.js +0 -2
  83. package/dist/TinyEssentials.min.js +0 -2
  84. package/dist/v1/libs/TinyRateLimit.cjs +0 -196
  85. package/dist/v1/libs/TinyRateLimit.d.mts +0 -86
  86. package/dist/v1/libs/TinyRateLimit.mjs +0 -171
  87. /package/dist/{ColorSafeStringify.js → v1/ColorSafeStringify.js} +0 -0
  88. /package/dist/{ColorSafeStringify.min.js → v1/ColorSafeStringify.min.js} +0 -0
  89. /package/dist/{TinyBasicsEs.min.js.LICENSE.txt → v1/TinyBasicsEs.min.js.LICENSE.txt} +0 -0
  90. /package/dist/{TinyEssentials.min.js.LICENSE.txt → v1/TinyDragger.min.js.LICENSE.txt} +0 -0
  91. /package/dist/{TinyLevelUp.js → v1/TinyLevelUp.js} +0 -0
  92. /package/dist/{TinyLevelUp.min.js → v1/TinyLevelUp.min.js} +0 -0
  93. /package/dist/{TinyPromiseQueue.js → v1/TinyPromiseQueue.js} +0 -0
  94. /package/dist/{TinyPromiseQueue.min.js → v1/TinyPromiseQueue.min.js} +0 -0
  95. /package/dist/{TinyRateLimiter.js → v1/TinyRateLimiter.js} +0 -0
  96. /package/dist/{TinyRateLimiter.min.js → v1/TinyRateLimiter.min.js} +0 -0
  97. /package/dist/{aiMarker.css → v1/css/aiMarker.css} +0 -0
  98. /package/dist/{aiMarker.min.css → v1/css/aiMarker.min.css} +0 -0
  99. /package/docs/{basics → v1/basics}/array.md +0 -0
  100. /package/docs/{basics → v1/basics}/asyncReplace.md +0 -0
  101. /package/docs/{basics → v1/basics}/clock.md +0 -0
  102. /package/docs/{basics → v1/basics}/objFilter.md +0 -0
  103. /package/docs/{basics → v1/basics}/simpleMath.md +0 -0
  104. /package/docs/{libs → v1/libs}/ColorSafeStringify.md +0 -0
  105. /package/docs/{libs → v1/libs}/TinyPromiseQueue.md +0 -0
  106. /package/docs/{libs → v1/libs}/TinyRateLimiter.md +0 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * A callback function used to manually close a notification.
3
+ * Passed as a second argument to `onClick` handlers, allowing programmatic dismissal of the toast.
4
+ *
5
+ * @typedef {() => void} CloseToastFunc
6
+ */
7
+ /**
8
+ * Represents the data used to display a notification.
9
+ * Can be a plain string (used as the message), or an object with more customization options.
10
+ *
11
+ * @typedef {string | {
12
+ * message: string, // The main message to display
13
+ * title?: string, // Optional title to appear above the message
14
+ * onClick?: function(MouseEvent, CloseToastFunc): void, // Optional click handler for the notification
15
+ * html?: boolean, // Whether the message should be interpreted as raw HTML
16
+ * avatar?: string // Optional URL to an avatar image shown on the left
17
+ * }} NotifyData
18
+ */
19
+ /**
20
+ * A lightweight notification system designed to display timed messages inside a container.
21
+ * Supports positioning, timing customization, click actions, HTML content, and optional avatars.
22
+ *
23
+ * ## Features:
24
+ * - Positioning via `x` (`left`, `center`, `right`) and `y` (`top`, `bottom`).
25
+ * - Dynamic display time based on message length.
26
+ * - Optional `title`, `avatar`, `onClick`, and `html` message rendering.
27
+ * - Fade-out animation with customizable duration.
28
+ * - Rigid validation of inputs and internal state.
29
+ *
30
+ * ## Customization via setters:
31
+ * - `setX(position)` — horizontal alignment.
32
+ * - `setY(position)` — vertical alignment.
33
+ * - `setBaseDuration(ms)` — base visible time in milliseconds.
34
+ * - `setExtraPerChar(ms)` — extra time added per character.
35
+ * - `setFadeOutDuration(ms)` — fade-out animation duration in milliseconds.
36
+ *
37
+ * @class
38
+ */
39
+ class TinyToastNotify {
40
+ #y;
41
+ #x;
42
+ #baseDuration;
43
+ #extraPerChar;
44
+ #fadeOutDuration;
45
+ /** @type {HTMLElement|null} */
46
+ #container;
47
+ /**
48
+ * @param {'top'|'bottom'} y - 'top' or 'bottom'
49
+ * @param {'right'|'left'|'center'} x - 'right', 'left', or 'center'
50
+ * @param {number} baseDuration - Base display time in ms
51
+ * @param {number} extraPerChar - Extra ms per character
52
+ * @param {number} fadeOutDuration - Time in ms for fade-out effect
53
+ * @param {string} [selector='.notify-container'] - Base selector for container
54
+ */
55
+ constructor(y = 'top', x = 'right', baseDuration = 3000, extraPerChar = 50, fadeOutDuration = 300, selector = '.notify-container') {
56
+ this.#validateY(y);
57
+ this.#validateX(x);
58
+ this.#validateTiming(baseDuration, 'baseDuration');
59
+ this.#validateTiming(extraPerChar, 'extraPerChar');
60
+ this.#validateTiming(fadeOutDuration, 'fadeOutDuration');
61
+ this.#y = y;
62
+ this.#x = x;
63
+ this.#baseDuration = baseDuration;
64
+ this.#extraPerChar = extraPerChar;
65
+ this.#fadeOutDuration = fadeOutDuration;
66
+ const container = document.querySelector(`${selector}.${y}.${x}`);
67
+ if (!(container instanceof HTMLElement)) {
68
+ this.#container = document.createElement('div');
69
+ this.#container.className = `notify-container ${y} ${x}`;
70
+ document.body.appendChild(this.#container);
71
+ }
72
+ else
73
+ this.#container = container;
74
+ }
75
+ /**
76
+ * Returns the notification container element.
77
+ * Ensures that the container is a valid HTMLElement.
78
+ *
79
+ * @returns {HTMLElement} The notification container.
80
+ * @throws {Error} If the container is not a valid HTMLElement.
81
+ */
82
+ getContainer() {
83
+ if (!(this.#container instanceof HTMLElement))
84
+ throw new Error('Container is not a valid HTMLElement.');
85
+ return this.#container;
86
+ }
87
+ /**
88
+ * Validates the vertical position value.
89
+ * Must be either 'top' or 'bottom'.
90
+ *
91
+ * @param {string} value - The vertical position to validate.
92
+ * @throws {Error} If the value is not 'top' or 'bottom'.
93
+ */
94
+ #validateY(value) {
95
+ if (!['top', 'bottom'].includes(value)) {
96
+ throw new Error(`Invalid vertical direction "${value}". Expected "top" or "bottom".`);
97
+ }
98
+ }
99
+ /**
100
+ * Validates the horizontal position value.
101
+ * Must be 'left', 'right', or 'center'.
102
+ *
103
+ * @param {string} value - The horizontal position to validate.
104
+ * @throws {Error} If the value is not one of the accepted directions.
105
+ */
106
+ #validateX(value) {
107
+ if (!['left', 'right', 'center'].includes(value)) {
108
+ throw new Error(`Invalid horizontal position "${value}". Expected "left", "right" or "center".`);
109
+ }
110
+ }
111
+ /**
112
+ * Validates a numeric timing value.
113
+ * Must be a non-negative finite number.
114
+ *
115
+ * @param {number} value - The number to validate.
116
+ * @param {string} name - The name of the parameter (used for error messaging).
117
+ * @throws {Error} If the number is invalid.
118
+ */
119
+ #validateTiming(value, name) {
120
+ if (typeof value !== 'number' || value < 0 || !Number.isFinite(value)) {
121
+ throw new Error(`Invalid value for "${name}": ${value}. Must be a non-negative finite number.`);
122
+ }
123
+ }
124
+ /**
125
+ * Returns the current vertical position.
126
+ *
127
+ * @returns {'top'|'bottom'} The vertical direction of the notification container.
128
+ */
129
+ getY() {
130
+ return this.#y;
131
+ }
132
+ /**
133
+ * Sets the vertical position of the notification container.
134
+ * Updates the container's class to reflect the new position.
135
+ *
136
+ * @param {'top'|'bottom'} value - The vertical direction to set.
137
+ * @throws {Error} If the value is invalid.
138
+ */
139
+ setY(value) {
140
+ this.#validateY(value);
141
+ const container = this.getContainer();
142
+ container.classList.remove(this.#y);
143
+ container.classList.add(value);
144
+ this.#y = value;
145
+ }
146
+ /**
147
+ * Returns the current horizontal position.
148
+ *
149
+ * @returns {'left'|'right'|'center'} The horizontal direction of the notification container.
150
+ */
151
+ getX() {
152
+ return this.#x;
153
+ }
154
+ /**
155
+ * Sets the horizontal position of the notification container.
156
+ * Updates the container's class to reflect the new position.
157
+ *
158
+ * @param {'left'|'right'|'center'} value - The horizontal direction to set.
159
+ * @throws {Error} If the value is invalid.
160
+ */
161
+ setX(value) {
162
+ this.#validateX(value);
163
+ const container = this.getContainer();
164
+ container.classList.remove(this.#x);
165
+ container.classList.add(value);
166
+ this.#x = value;
167
+ }
168
+ /**
169
+ * Returns the base duration for displaying the notification.
170
+ *
171
+ * @returns {number} Base time (in milliseconds) that a notification stays on screen.
172
+ */
173
+ getBaseDuration() {
174
+ return this.#baseDuration;
175
+ }
176
+ /**
177
+ * Sets the base duration for the notification display time.
178
+ *
179
+ * @param {number} value - Base display time in milliseconds.
180
+ * @throws {Error} If the value is not a valid non-negative finite number.
181
+ */
182
+ setBaseDuration(value) {
183
+ this.#validateTiming(value, 'baseDuration');
184
+ this.#baseDuration = value;
185
+ }
186
+ /**
187
+ * Returns the extra display time added per character.
188
+ *
189
+ * @returns {number} Extra time (in milliseconds) per character in the notification.
190
+ */
191
+ getExtraPerChar() {
192
+ return this.#extraPerChar;
193
+ }
194
+ /**
195
+ * Sets the additional display time per character.
196
+ *
197
+ * @param {number} value - Extra time in milliseconds per character.
198
+ * @throws {Error} If the value is not a valid non-negative finite number.
199
+ */
200
+ setExtraPerChar(value) {
201
+ this.#validateTiming(value, 'extraPerChar');
202
+ this.#extraPerChar = value;
203
+ }
204
+ /**
205
+ * Returns the fade-out duration.
206
+ *
207
+ * @returns {number} Time (in milliseconds) used for fade-out transition.
208
+ */
209
+ getFadeOutDuration() {
210
+ return this.#fadeOutDuration;
211
+ }
212
+ /**
213
+ * Sets the fade-out transition time for notifications.
214
+ *
215
+ * @param {number} value - Fade-out duration in milliseconds.
216
+ * @throws {Error} If the value is not a valid non-negative finite number.
217
+ */
218
+ setFadeOutDuration(value) {
219
+ this.#validateTiming(value, 'fadeOutDuration');
220
+ this.#fadeOutDuration = value;
221
+ }
222
+ /**
223
+ * Displays a notification for a time based on message length.
224
+ * Accepts a string or an object with:
225
+ * {
226
+ * message: string,
227
+ * title?: string,
228
+ * onClick?: function(MouseEvent, CloseToastFunc): void,
229
+ * html?: boolean,
230
+ * avatar?: string // Optional avatar image URL
231
+ * }
232
+ *
233
+ * @param {NotifyData} data
234
+ */
235
+ show(data) {
236
+ let message = '';
237
+ let title = '';
238
+ let onClick = null;
239
+ let useHTML = false;
240
+ let avatarUrl = null;
241
+ const notify = document.createElement('div');
242
+ notify.className = 'notify enter';
243
+ if (typeof data === 'string') {
244
+ message = data;
245
+ }
246
+ else if (typeof data === 'object' && data !== null && typeof data.message === 'string') {
247
+ message = data.message;
248
+ title = typeof data.title === 'string' ? data.title : '';
249
+ useHTML = data.html === true;
250
+ avatarUrl = typeof data.avatar === 'string' ? data.avatar : null;
251
+ if (data.onClick !== undefined) {
252
+ if (typeof data.onClick !== 'function') {
253
+ throw new Error('onClick must be a function if defined');
254
+ }
255
+ onClick = data.onClick;
256
+ notify.classList.add('clickable');
257
+ }
258
+ }
259
+ else {
260
+ throw new Error(`Invalid argument for show(): expected string or { message: string, title?: string, onClick?: function, html?: boolean, avatar?: string }`);
261
+ }
262
+ // Add close button
263
+ const closeBtn = document.createElement('button');
264
+ closeBtn.innerHTML = '&times;';
265
+ closeBtn.className = 'close';
266
+ closeBtn.setAttribute('aria-label', 'Close');
267
+ // Optional hover effect
268
+ closeBtn.addEventListener('mouseenter', () => {
269
+ closeBtn.style.color = 'var(--notif-close-color-hover)';
270
+ });
271
+ closeBtn.addEventListener('mouseleave', () => {
272
+ closeBtn.style.color = 'var(--notif-close-color)';
273
+ });
274
+ // Avatar
275
+ if (avatarUrl) {
276
+ const avatar = document.createElement('img');
277
+ avatar.src = avatarUrl;
278
+ avatar.alt = 'avatar';
279
+ avatar.className = 'avatar';
280
+ notify.appendChild(avatar);
281
+ }
282
+ // Title
283
+ if (title) {
284
+ const titleElem = document.createElement('strong');
285
+ titleElem.textContent = title;
286
+ titleElem.style.display = 'block';
287
+ notify.appendChild(titleElem);
288
+ }
289
+ // Message
290
+ if (useHTML) {
291
+ const msgWrapper = document.createElement('div');
292
+ msgWrapper.innerHTML = message;
293
+ notify.appendChild(msgWrapper);
294
+ }
295
+ else {
296
+ notify.appendChild(document.createTextNode(message));
297
+ }
298
+ notify.appendChild(closeBtn);
299
+ this.getContainer().appendChild(notify);
300
+ const visibleTime = this.#baseDuration + message.length * this.#extraPerChar;
301
+ const totalTime = visibleTime + this.#fadeOutDuration;
302
+ // Close logic
303
+ let removed = false;
304
+ const close = () => {
305
+ if (removed)
306
+ return;
307
+ removed = true;
308
+ notify.classList.remove('enter', 'show');
309
+ notify.classList.add('exit');
310
+ setTimeout(() => notify.remove(), this.#fadeOutDuration);
311
+ };
312
+ // Click handler
313
+ if (typeof onClick === 'function') {
314
+ notify.addEventListener('click', (e) => {
315
+ if (e.target === closeBtn)
316
+ return;
317
+ onClick(e, close);
318
+ });
319
+ }
320
+ // Close button click
321
+ closeBtn.addEventListener('click', (e) => {
322
+ e.stopPropagation();
323
+ close();
324
+ });
325
+ // Transition activation force soon after the element is added
326
+ setTimeout(() => {
327
+ notify.classList.remove('enter');
328
+ notify.classList.add('show');
329
+ }, 1);
330
+ setTimeout(() => close(), totalTime);
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
+ }
351
+ }
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