tiny-essentials 1.22.15 → 1.23.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.
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Represents the possible states of the loading screen.
3
+ * - `'none'` → Not visible
4
+ * - `'fadeIn'` → Appearing with fade-in animation
5
+ * - `'active'` → Fully visible and active
6
+ * - `'fadeOut'` → Disappearing with fade-out animation
7
+ * @typedef {'none'|'active'|'fadeIn'|'fadeOut'} LoadingStatus
8
+ */
9
+ /**
10
+ * Configuration options for the loading screen.
11
+ * @typedef {Object} LoadingSettings
12
+ * @property {number|null} fadeIn - Duration of fade-in animation in milliseconds, or `null` to disable.
13
+ * @property {number|null} fadeOut - Duration of fade-out animation in milliseconds, or `null` to disable.
14
+ * @property {number} zIndex - CSS z-index of the overlay element.
15
+ */
16
+ /**
17
+ * TinyLoadingScreen
18
+ *
19
+ * A lightweight, fully-configurable loading overlay component that can be appended to any HTMLElement.
20
+ *
21
+ * Key features:
22
+ * - Configurable fadeIn/fadeOut durations (milliseconds) and zIndex.
23
+ * - Accepts string or HTMLElement messages.
24
+ * - Optionally allows HTML inside string messages when `allowHtmlText` is enabled.
25
+ * - Exposes `overlay`, `messageElement`, `status`, `options` and helpers for testing and integration.
26
+ *
27
+ * @class
28
+ */
29
+ class TinyLoadingScreen {
30
+ /** @type {HTMLDivElement|null} Overlay container element */
31
+ #overlay = null;
32
+ /** @returns {HTMLDivElement|null} The overlay element if active, otherwise `null`. */
33
+ get overlay() {
34
+ return this.#overlay;
35
+ }
36
+ /** @type {HTMLDivElement|null} Element containing the loading message */
37
+ #messageElement = null;
38
+ /** @returns {HTMLDivElement|null} The element used to render the message, or `null` if inactive. */
39
+ get messageElement() {
40
+ return this.#messageElement;
41
+ }
42
+ /** @type {HTMLElement} Container where the overlay will be attached */
43
+ #container;
44
+ /** @returns {HTMLElement} The container element that holds the overlay. */
45
+ get container() {
46
+ return this.#container;
47
+ }
48
+ /** @type {LoadingSettings} Internal configuration */
49
+ #options = { fadeIn: null, fadeOut: null, zIndex: 9999 };
50
+ /** @returns {LoadingSettings} A copy of the current configuration options. */
51
+ get options() {
52
+ return { ...this.#options };
53
+ }
54
+ /**
55
+ * Updates the loading screen options.
56
+ * @param {Partial<LoadingSettings>} value - New configuration values.
57
+ * @throws {TypeError} If any option has an invalid type or value.
58
+ */
59
+ set options(value) {
60
+ if (typeof value !== 'object' || value === null)
61
+ throw new TypeError('options must be an object');
62
+ if (typeof value.fadeIn !== 'undefined' &&
63
+ value.fadeIn !== null &&
64
+ (typeof value.fadeIn !== 'number' || value.fadeIn < 0))
65
+ throw new TypeError('fadeIn must be a non-negative number or null');
66
+ if (typeof value.fadeOut !== 'undefined' &&
67
+ value.fadeOut !== null &&
68
+ (typeof value.fadeOut !== 'number' || value.fadeOut < 0))
69
+ throw new TypeError('fadeOut must be a non-negative number or null');
70
+ if (typeof value.zIndex !== 'undefined' &&
71
+ (typeof value.zIndex !== 'number' || !Number.isInteger(value.zIndex)))
72
+ throw new TypeError('zIndex must be an integer number');
73
+ this.#options = {
74
+ fadeIn: value.fadeIn ?? null,
75
+ fadeOut: value.fadeOut ?? null,
76
+ zIndex: value.zIndex ?? 9999,
77
+ };
78
+ }
79
+ /** @type {LoadingStatus} Current status of the loading screen */
80
+ #status = 'none';
81
+ /** @returns {LoadingStatus} The current loading screen status. */
82
+ get status() {
83
+ return this.#status;
84
+ }
85
+ /** @type {string|HTMLElement} Default message shown when no custom message is provided */
86
+ #defaultMessage = '';
87
+ /** @returns {string|HTMLElement} The default message. */
88
+ get defaultMessage() {
89
+ return this.#defaultMessage;
90
+ }
91
+ /**
92
+ * @param {string|HTMLElement} value - New default message.
93
+ * @throws {TypeError} If the value is neither a string nor an HTMLElement.
94
+ */
95
+ set defaultMessage(value) {
96
+ if (typeof value !== 'string' && !(value instanceof HTMLElement))
97
+ throw new TypeError('defaultMessage must be a string or an HTMLElement');
98
+ this.#defaultMessage = value;
99
+ }
100
+ /** @type {string|HTMLElement|null} Current active message */
101
+ #message = null;
102
+ /** @returns {string|HTMLElement|null} The currently displayed message. */
103
+ get message() {
104
+ return this.#message;
105
+ }
106
+ /** @type {boolean} Whether HTML is allowed in string messages */
107
+ #allowHtmlText = false;
108
+ /** @returns {boolean} True if HTML is allowed inside string messages. */
109
+ get allowHtmlText() {
110
+ return this.#allowHtmlText;
111
+ }
112
+ /**
113
+ * Enables or disables HTML rendering in string messages.
114
+ * @param {boolean} value - Whether to allow HTML.
115
+ * @throws {TypeError} If value is not a boolean.
116
+ */
117
+ set allowHtmlText(value) {
118
+ if (typeof value !== 'boolean')
119
+ throw new TypeError('allowHtmlText must be a boolean');
120
+ this.#allowHtmlText = value;
121
+ }
122
+ /** @type {NodeJS.Timeout|null} Timeout handler for fadeIn */
123
+ #fadeInTimeout = null;
124
+ /** @returns {boolean} Whether a fadeIn timeout is currently pending. */
125
+ get fadeInTimeout() {
126
+ return this.#fadeInTimeout !== null;
127
+ }
128
+ /** @type {NodeJS.Timeout|null} Timeout handler for fadeOut */
129
+ #fadeOutTimeout = null;
130
+ /** @returns {boolean} Whether a fadeOut timeout is currently pending. */
131
+ get fadeOutTimeout() {
132
+ return this.#fadeOutTimeout !== null;
133
+ }
134
+ /** @returns {boolean} True if the overlay is currently visible. */
135
+ get visible() {
136
+ return !!this.#overlay;
137
+ }
138
+ /**
139
+ * Optional callback fired whenever the loading screen status changes.
140
+ * @type {((status: LoadingStatus) => void) | null}
141
+ */
142
+ #onChange = null;
143
+ /**
144
+ * Returns the current status-change callback.
145
+ * @returns {((status: LoadingStatus) => void) | null}
146
+ */
147
+ get onChange() {
148
+ return this.#onChange;
149
+ }
150
+ /**
151
+ * Sets the status-change callback.
152
+ * @param {((status: LoadingStatus) => void) | null} value
153
+ * @throws {TypeError} If value is neither a function nor null.
154
+ */
155
+ set onChange(value) {
156
+ if (value !== null && typeof value !== 'function') {
157
+ throw new TypeError('onChange must be a function or null');
158
+ }
159
+ this.#onChange = value;
160
+ }
161
+ /**
162
+ * Internal helper to emit the onChange callback.
163
+ * @private
164
+ */
165
+ _emitChange() {
166
+ if (typeof this.#onChange === 'function')
167
+ this.#onChange(this.#status);
168
+ }
169
+ /**
170
+ * Creates a new TinyLoadingScreen instance.
171
+ * @param {HTMLElement} [container=document.body] - The container element where the overlay should be appended.
172
+ * @throws {TypeError} If container is not an HTMLElement.
173
+ */
174
+ constructor(container = document.body) {
175
+ if (!(container instanceof HTMLElement))
176
+ throw new TypeError('container must be an HTMLElement');
177
+ this.#container = container;
178
+ }
179
+ /**
180
+ * Internal helper to update the displayed message.
181
+ * @param {string|HTMLElement} [message=this.#defaultMessage] - The new message.
182
+ * @throws {TypeError} If the message is not a string or HTMLElement.
183
+ * @throws {Error} If trying to use HTMLElement without allowHtmlText enabled.
184
+ * @private
185
+ */
186
+ _updateMessage(message = this.#defaultMessage) {
187
+ if (!this.#messageElement)
188
+ throw new Error('messageElement is not initialized');
189
+ if (typeof message !== 'string' && !(message instanceof HTMLElement))
190
+ throw new TypeError('message must be a string or an HTMLElement');
191
+ this.#message = message;
192
+ if (typeof message === 'string') {
193
+ if (!this.#allowHtmlText)
194
+ this.#messageElement.textContent = message;
195
+ else
196
+ this.#messageElement.innerHTML = message;
197
+ }
198
+ else {
199
+ if (!this.#allowHtmlText)
200
+ throw new Error('HTMLElement messages require allowHtmlText = true');
201
+ this.#messageElement.textContent = '';
202
+ this.#messageElement.appendChild(message);
203
+ }
204
+ }
205
+ /**
206
+ * Removes all status-related CSS classes (`active`, `fadeIn`, `fadeOut`)
207
+ * from the overlay element, if it exists.
208
+ *
209
+ * @private
210
+ * @returns {void}
211
+ */
212
+ _removeOldClasses() {
213
+ this.#overlay?.classList.remove('active');
214
+ this.#overlay?.classList.remove('fadeIn');
215
+ this.#overlay?.classList.remove('fadeOut');
216
+ }
217
+ /**
218
+ * Starts the loading screen or updates its message if already active.
219
+ * @param {string|HTMLElement} [message=this.#defaultMessage] - Message to display.
220
+ * @returns {boolean} `true` if the overlay was created, `false` if only the message was updated.
221
+ * @throws {TypeError} If message is not a string or HTMLElement.
222
+ */
223
+ start(message = this.#defaultMessage) {
224
+ if (typeof message !== 'string' && !(message instanceof HTMLElement))
225
+ throw new TypeError('message must be a string or an HTMLElement');
226
+ if (!this.#overlay) {
227
+ this.#overlay = document.createElement('div');
228
+ this.#overlay.classList.add('loading-overlay');
229
+ this.#overlay.style.zIndex = String(this.#options.zIndex);
230
+ const content = document.createElement('div');
231
+ content.classList.add('loading-content');
232
+ const spinner = document.createElement('div');
233
+ spinner.classList.add('loading-spinner');
234
+ this.#messageElement = document.createElement('div');
235
+ this.#messageElement.classList.add('loading-message');
236
+ content.appendChild(spinner);
237
+ content.appendChild(this.#messageElement);
238
+ this.#overlay.appendChild(content);
239
+ this.#container.appendChild(this.#overlay);
240
+ // trigger fade in
241
+ this._removeOldClasses();
242
+ this.#status = 'fadeIn';
243
+ this.#overlay.classList.add('fadeIn');
244
+ this._emitChange();
245
+ const fadeIn = () => {
246
+ this._removeOldClasses();
247
+ this.#fadeInTimeout = null;
248
+ this.#status = 'active';
249
+ this.#overlay?.classList.add('active');
250
+ this._emitChange();
251
+ };
252
+ if (typeof this.#options.fadeIn === 'number') {
253
+ if (this.#fadeInTimeout)
254
+ clearTimeout(this.#fadeInTimeout);
255
+ this.#fadeInTimeout = setTimeout(fadeIn, this.#options.fadeIn);
256
+ }
257
+ else
258
+ fadeIn();
259
+ this._updateMessage(message);
260
+ return true;
261
+ }
262
+ if (this.#messageElement)
263
+ this._updateMessage(message);
264
+ return false;
265
+ }
266
+ /**
267
+ * Updates the loading screen with a new message.
268
+ * @param {string|HTMLElement} [message=this.#defaultMessage] - The new message.
269
+ * @returns {boolean} `true` if the message was updated, `false` if overlay is not active.
270
+ * @throws {TypeError} If message is not a string or HTMLElement.
271
+ */
272
+ update(message = this.#defaultMessage) {
273
+ if (typeof message !== 'string' && !(message instanceof HTMLElement))
274
+ throw new TypeError('message must be a string or an HTMLElement');
275
+ if (this.#messageElement) {
276
+ this._updateMessage(message);
277
+ return true;
278
+ }
279
+ return false;
280
+ }
281
+ /**
282
+ * Stops and removes the loading screen.
283
+ * @returns {boolean} `true` if the overlay was removed, `false` if not active.
284
+ */
285
+ stop() {
286
+ if (this.#overlay) {
287
+ this._removeOldClasses();
288
+ this.#status = 'fadeOut';
289
+ this.#overlay.classList.add('fadeOut');
290
+ this._emitChange();
291
+ // trigger fade out
292
+ const fadeOut = () => {
293
+ this._removeOldClasses();
294
+ this.#fadeOutTimeout = null;
295
+ this.#status = 'none';
296
+ this.#overlay?.remove();
297
+ this.#overlay = null;
298
+ this.#messageElement = null;
299
+ this.#message = null;
300
+ this._emitChange();
301
+ };
302
+ if (typeof this.#options.fadeOut === 'number') {
303
+ if (this.#fadeOutTimeout)
304
+ clearTimeout(this.#fadeOutTimeout);
305
+ this.#fadeOutTimeout = setTimeout(fadeOut, this.#options.fadeOut);
306
+ }
307
+ else
308
+ fadeOut();
309
+ return true;
310
+ }
311
+ return false;
312
+ }
313
+ }
314
+ export default TinyLoadingScreen;
package/docs/v1/README.md CHANGED
@@ -58,6 +58,7 @@ This folder contains the core scripts we have worked on so far. Each file is a m
58
58
  - 🎮 **[TinyNeedBar](./libs/TinyNeedBar.md)** — A versatile "need bar" system for simulating decay over time with multiple configurable factors, serialization, cloning, and full control over clamped and infinite values.
59
59
  - 🎲 **[TinySimpleDice](./libs/TinySimpleDice.md)** — A lightweight and flexible dice rolling utility with configurable maximum values, zero allowance, and array/Set index rolling support.
60
60
  - 👀 **[TinyElementObserver](./libs/TinyElementObserver.md)** — A DOM mutation tracking utility built on MutationObserver, with customizable detectors for handling changes, event dispatching, and lifecycle management.
61
+ - ⏳ **[TinyLoadingScreen](./libs/TinyLoadingScreen.md)** — A lightweight, fully-configurable loading overlay with fade-in/out animations, custom messages (string or HTMLElement), HTML rendering option, and status-change callbacks.
61
62
 
62
63
  ### 3. **`fileManager/`**
63
64
  * 📁 **[Main](./fileManager/main.md)** — A Node.js file/directory utility module with support for JSON, backups, renaming, size analysis, and more.
@@ -0,0 +1,186 @@
1
+ # TinyLoadingScreen 📦✨
2
+
3
+ A lightweight, fully-configurable **loading overlay component** that can be appended to any HTML element.
4
+
5
+ It allows you to display a customizable overlay with a spinner and message while your app is loading content.
6
+
7
+ ---
8
+
9
+ ## Constructor 🏗️
10
+
11
+ ```ts
12
+ new TinyLoadingScreen(container?: HTMLElement)
13
+ ```
14
+
15
+ * `container` — The HTML element where the overlay will be appended. Defaults to `document.body`.
16
+ * Throws `TypeError` if the container is not an `HTMLElement`.
17
+
18
+ ---
19
+
20
+ ## Properties 🔑
21
+
22
+ | Property | Type | Description |
23
+ | ---------------- | ----------------------------------------- | -------------------------------------------------------------- |
24
+ | `overlay` | `HTMLDivElement \| null` | The overlay element if active, otherwise `null`. |
25
+ | `messageElement` | `HTMLDivElement \| null` | The element used to render the message, or `null` if inactive. |
26
+ | `container` | `HTMLElement` | The container element that holds the overlay. |
27
+ | `status` | `'none'\|'fadeIn'\|'active'\|'fadeOut'` | Current state of the loading screen. |
28
+ | `defaultMessage` | `string \| HTMLElement` | Default message to display when no custom message is provided. |
29
+ | `message` | `string \| HTMLElement \| null` | Currently displayed message. |
30
+ | `allowHtmlText` | `boolean` | Whether HTML is allowed inside string messages. |
31
+ | `visible` | `boolean` | `true` if the overlay is currently visible, `false` otherwise. |
32
+ | `onChange` | `(status: LoadingStatus) => void \| null` | Optional callback fired whenever the status changes. |
33
+
34
+ ---
35
+
36
+ ## Options ⚙️
37
+
38
+ You can configure the loading screen using the `options` property:
39
+
40
+ ```ts
41
+ loader.options = {
42
+ fadeIn: 300, // milliseconds, null = no animation
43
+ fadeOut: 300, // milliseconds, null = no animation
44
+ zIndex: 9999 // overlay z-index
45
+ };
46
+ ```
47
+
48
+ * Throws `TypeError` if invalid values are provided.
49
+
50
+ ---
51
+
52
+ ## Methods 🛠️
53
+
54
+ ### `start(message?: string | HTMLElement) → boolean` ✅
55
+
56
+ * Starts the loading screen, or updates the message if already active.
57
+ * `message` — The message to display. Defaults to `defaultMessage`.
58
+ * Returns `true` if overlay was created, `false` if only the message was updated.
59
+ * Throws `TypeError` if `message` is not a string or HTMLElement.
60
+
61
+ ---
62
+
63
+ ### `update(message?: string | HTMLElement) → boolean` ✏️
64
+
65
+ * Updates the loading screen message.
66
+ * Returns `true` if the message was updated, `false` if overlay is not active.
67
+ * Throws `TypeError` if `message` is not a string or HTMLElement.
68
+
69
+ ---
70
+
71
+ ### `stop() → boolean` ❌
72
+
73
+ * Stops and removes the loading screen.
74
+ * Returns `true` if overlay was removed, `false` if overlay was not active.
75
+
76
+ ---
77
+
78
+ ### Internal / Private Methods 🔒
79
+
80
+ | Method | Description |
81
+ | ------------------------- | ----------------------------------------------------------------------------------------------------- |
82
+ | `_updateMessage(message)` | Updates the displayed message. Throws errors if invalid types or HTMLElement without `allowHtmlText`. |
83
+ | `_removeOldClasses()` | Removes all status-related CSS classes (`active`, `fadeIn`, `fadeOut`) from the overlay. |
84
+ | `_emitChange()` | Emits the `onChange` callback with the current `status`. |
85
+
86
+ > ⚠️ Private methods are for internal use only.
87
+
88
+ ---
89
+
90
+ ## Status States 🟢⚪🔴
91
+
92
+ The loading screen has four states:
93
+
94
+ | Status | Meaning |
95
+ | ----------- | ------------------------------------ |
96
+ | `'none'` | Not visible |
97
+ | `'fadeIn'` | Appearing with fade-in animation |
98
+ | `'active'` | Fully visible and active |
99
+ | `'fadeOut'` | Disappearing with fade-out animation |
100
+
101
+ * `onChange` callback is fired whenever the status changes.
102
+
103
+ ---
104
+
105
+ ## Callbacks 🔔
106
+
107
+ ### `onChange: (status: LoadingStatus) => void`
108
+
109
+ ```ts
110
+ loader.onChange = (status) => {
111
+ console.log('Loading screen status:', status);
112
+ };
113
+ ```
114
+
115
+ * Fired on every status change: `fadeIn`, `active`, `fadeOut`, `none`.
116
+
117
+ ---
118
+
119
+ ## Usage Examples 💡
120
+
121
+ ### Basic Example
122
+
123
+ ```js
124
+ import TinyLoadingScreen from './TinyLoadingScreen.mjs';
125
+
126
+ const loader = new TinyLoadingScreen();
127
+ loader.defaultMessage = 'Loading...';
128
+ loader.start();
129
+
130
+ // Stop after 2 seconds
131
+ setTimeout(() => loader.stop(), 2000);
132
+ ```
133
+
134
+ ---
135
+
136
+ ### Custom Container
137
+
138
+ ```js
139
+ const container = document.getElementById('custom-container');
140
+ const loader2 = new TinyLoadingScreen(container);
141
+ loader2.defaultMessage = 'Loading content...';
142
+ loader2.start();
143
+
144
+ // Update message dynamically
145
+ loader2.update('Almost done...');
146
+ ```
147
+
148
+ ---
149
+
150
+ ### With onChange Callback
151
+
152
+ ```js
153
+ loader.onChange = (status) => {
154
+ console.log('Status changed:', status);
155
+ };
156
+ ```
157
+
158
+ ---
159
+
160
+ ### Custom Animations ⏳
161
+
162
+ ```js
163
+ loader.options = {
164
+ fadeIn: 500, // fade-in duration in ms
165
+ fadeOut: 500, // fade-out duration in ms
166
+ zIndex: 10000
167
+ };
168
+ ```
169
+
170
+ ---
171
+
172
+ ### Allow HTML Messages 🖌️
173
+
174
+ ```js
175
+ loader.allowHtmlText = true;
176
+ loader.start('<b>Loading <i>data</i>...</b>');
177
+ ```
178
+
179
+ ---
180
+
181
+ ### Notes 📝
182
+
183
+ * Fade durations can be `null` to disable animations.
184
+ * HTML messages require `allowHtmlText = true`.
185
+ * `_removeOldClasses()` ensures that multiple animations don't conflict.
186
+ * Use `onChange` to hook into status changes for debugging or UI updates.