tiny-essentials 1.28.18 → 1.28.19
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.
- package/dist/v1/css/TinyCookieConsent.min.css +1 -0
- package/dist/v1/css/TinyCookieConsent.min.css.map +1 -0
- package/dist/v1/css/TinyDraggerExample.min.css +1 -0
- package/dist/v1/css/TinyDraggerExample.min.css.map +1 -0
- package/dist/v1/css/TinyLoadingScreen.min.css +1 -0
- package/dist/v1/css/TinyLoadingScreen.min.css.map +1 -0
- package/dist/v1/css/TinyNotify.min.css +1 -0
- package/dist/v1/css/TinyNotify.min.css.map +1 -0
- package/dist/v1/css/aiMarker.min.css +1 -0
- package/dist/v1/css/aiMarker.min.css.map +1 -0
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.cjs +699 -0
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.d.mts +290 -0
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.d.mts.map +1 -0
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.mjs +590 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/input/Placeholder-Colors-Fix.scss +64 -0
- package/dist/v1/webTemplates/bootstrap/5.3/scss/spacing/Spacing-Plus.scss +31 -0
- package/docs/v1/Exports.md +10 -0
- package/docs/v1/README.md +7 -1
- package/docs/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.md +162 -0
- package/package.json +13 -8
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('bootstrap').Modal} Modal
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {typeof import('bootstrap').Modal} ModalClass
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} CustomElementConfig
|
|
9
|
+
* @property {string | string[]} [className] - CSS classes to be added to the element (as a string or an array of strings).
|
|
10
|
+
* @property {Object<string, string|null>} [styles] - CSS properties and values (e.g., { 'background-color': '#000' }).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ModalOptions - Configuration options for the dialog content and buttons.
|
|
14
|
+
* @property {string} [title] - The text to be displayed in the modal header.
|
|
15
|
+
* @property {string} [confirmText] - The text to be displayed on the primary confirmation button.
|
|
16
|
+
* @property {string} [cancelText] - The text to be displayed on the secondary cancel button.
|
|
17
|
+
* @property {string} [defaultValue] - The initial value for the input field (used in prompts).
|
|
18
|
+
* @property {CustomElementConfig} [modalConfig] - Customization for the main modal container.
|
|
19
|
+
* @property {CustomElementConfig} [headerConfig] - Customization for the modal header.
|
|
20
|
+
* @property {CustomElementConfig} [titleConfig] - Customization for the modal title.
|
|
21
|
+
* @property {CustomElementConfig} [closeBtnConfig] - Customization for the close button.
|
|
22
|
+
* @property {CustomElementConfig} [bodyConfig] - Customization for the modal body.
|
|
23
|
+
* @property {CustomElementConfig} [footerConfig] - Customization for the modal footer.
|
|
24
|
+
* @property {CustomElementConfig} [confirmBtnConfig] - Customization for the confirm button.
|
|
25
|
+
* @property {CustomElementConfig} [cancelBtnConfig] - Customization for the cancel button.
|
|
26
|
+
* @property {CustomElementConfig} [contentConfig] - Customization for the modal content container.
|
|
27
|
+
* @property {CustomElementConfig} [dialogConfig] - Customization for the modal dialog wrapper.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Utility to replace native alert/confirm/prompt with Bootstrap 5 modals.
|
|
31
|
+
* Built with strict DOM manipulation to prevent XSS.
|
|
32
|
+
*/
|
|
33
|
+
class BootstrapDialogs {
|
|
34
|
+
/** @type {Modal|null} */
|
|
35
|
+
static _activeInstance = null;
|
|
36
|
+
/** @type {Modal|null} */
|
|
37
|
+
static _loadingInstance = null;
|
|
38
|
+
/** @type {HTMLElement|null} */
|
|
39
|
+
static _modalElement = null;
|
|
40
|
+
/** @type {HTMLElement|null} */
|
|
41
|
+
static _loadingElement = null;
|
|
42
|
+
/** @type {Function|null} */
|
|
43
|
+
static _activeResolve = null;
|
|
44
|
+
/** @type {'alert'|'confirm'|'prompt'|null} */
|
|
45
|
+
static _activeType = null;
|
|
46
|
+
/** @type {ModalClass|null} */
|
|
47
|
+
static #Modal = null;
|
|
48
|
+
/**
|
|
49
|
+
* Default configurations for all modal elements to ensure consistent look.
|
|
50
|
+
* @type {Object<string, CustomElementConfig>}
|
|
51
|
+
*/
|
|
52
|
+
static #defaultConfig = {
|
|
53
|
+
modalConfig: { className: ['modal', 'fade'], styles: { 'z-index': '1065' } },
|
|
54
|
+
dialogConfig: { className: ['modal-dialog', 'modal-dialog-centered'], styles: {} },
|
|
55
|
+
contentConfig: { className: ['modal-content', 'shadow-lg', 'border-0'], styles: {} },
|
|
56
|
+
headerConfig: { className: ['modal-header', 'bg-body-tertiary'], styles: {} },
|
|
57
|
+
titleConfig: { className: ['modal-title', 'fw-bold'], styles: {} },
|
|
58
|
+
closeBtnConfig: { className: ['btn-close'], styles: {} },
|
|
59
|
+
bodyConfig: {
|
|
60
|
+
className: ['modal-body'],
|
|
61
|
+
styles: { 'white-space': 'pre-wrap', 'overflow-wrap': 'anywhere' },
|
|
62
|
+
},
|
|
63
|
+
footerConfig: { className: ['modal-footer', 'border-top-0', 'pt-0'], styles: {} },
|
|
64
|
+
confirmBtnConfig: { className: ['btn', 'btn-primary', 'fw-bold', 'px-4'], styles: {} },
|
|
65
|
+
cancelBtnConfig: { className: ['btn', 'btn-secondary'], styles: {} },
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Gets a deep copy of the default configuration.
|
|
69
|
+
* @returns {Object<string, CustomElementConfig>}
|
|
70
|
+
*/
|
|
71
|
+
static get defaultConfig() {
|
|
72
|
+
return structuredClone(this.#defaultConfig);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Sets the entire default configuration.
|
|
76
|
+
* @param {Object<string, CustomElementConfig>} config - The new default configuration.
|
|
77
|
+
* @throws {TypeError} If the configuration is invalid.
|
|
78
|
+
*/
|
|
79
|
+
static set defaultConfig(config) {
|
|
80
|
+
this._validateOptions(config);
|
|
81
|
+
this.#defaultConfig = structuredClone(config);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sets the Modal class constructor (e.g., Bootstrap's Modal).
|
|
85
|
+
* @param {ModalClass} Modal - The Bootstrap Modal class constructor.
|
|
86
|
+
* @throws {Error} If a Modal class has already been set.
|
|
87
|
+
*/
|
|
88
|
+
static set Modal(Modal) {
|
|
89
|
+
if (BootstrapDialogs.#Modal !== null)
|
|
90
|
+
throw new Error('Modal class has already been initialized.');
|
|
91
|
+
BootstrapDialogs.#Modal = Modal;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Gets the currently set Modal class constructor.
|
|
95
|
+
* @returns {ModalClass} The currently configured Modal class.
|
|
96
|
+
* @throws {Error} If no Modal class has been set.
|
|
97
|
+
*/
|
|
98
|
+
static get Modal() {
|
|
99
|
+
if (BootstrapDialogs.#Modal === null)
|
|
100
|
+
throw new Error('Modal class has not been initialized.');
|
|
101
|
+
return BootstrapDialogs.#Modal;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Updates a specific configuration key by merging it with existing defaults.
|
|
105
|
+
* @param {string} key - The key within #defaultConfig to update.
|
|
106
|
+
* @param {CustomElementConfig} config - The partial or full configuration to merge.
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
static _updateConfigKey(key, config) {
|
|
110
|
+
if (!(key in this.#defaultConfig)) {
|
|
111
|
+
throw new Error(`Invalid configuration key: ${key}`);
|
|
112
|
+
}
|
|
113
|
+
// Merge existing defaults with the new partial configuration
|
|
114
|
+
const updatedConfig = {
|
|
115
|
+
...this.#defaultConfig[key],
|
|
116
|
+
...config,
|
|
117
|
+
// Deep merge for styles object
|
|
118
|
+
styles: {
|
|
119
|
+
...(this.#defaultConfig[key].styles || {}),
|
|
120
|
+
...(config.styles || {}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
// Validate the merged object
|
|
124
|
+
const validationWrapper = { [key]: updatedConfig };
|
|
125
|
+
this._validateOptions(validationWrapper);
|
|
126
|
+
this.#defaultConfig[key] = updatedConfig;
|
|
127
|
+
}
|
|
128
|
+
// --- Individual Configuration Setters ---
|
|
129
|
+
/** @param {CustomElementConfig} cfg */
|
|
130
|
+
static set modalConfig(cfg) {
|
|
131
|
+
this._updateConfigKey('modalConfig', cfg);
|
|
132
|
+
}
|
|
133
|
+
static get modalConfig() {
|
|
134
|
+
return structuredClone(this.#defaultConfig.modalConfig);
|
|
135
|
+
}
|
|
136
|
+
/** @param {CustomElementConfig} cfg */
|
|
137
|
+
static set headerConfig(cfg) {
|
|
138
|
+
this._updateConfigKey('headerConfig', cfg);
|
|
139
|
+
}
|
|
140
|
+
static get headerConfig() {
|
|
141
|
+
return structuredClone(this.#defaultConfig.headerConfig);
|
|
142
|
+
}
|
|
143
|
+
/** @param {CustomElementConfig} cfg */
|
|
144
|
+
static set titleConfig(cfg) {
|
|
145
|
+
this._updateConfigKey('titleConfig', cfg);
|
|
146
|
+
}
|
|
147
|
+
static get titleConfig() {
|
|
148
|
+
return structuredClone(this.#defaultConfig.titleConfig);
|
|
149
|
+
}
|
|
150
|
+
/** @param {CustomElementConfig} cfg */
|
|
151
|
+
static set closeBtnConfig(cfg) {
|
|
152
|
+
this._updateConfigKey('closeBtnConfig', cfg);
|
|
153
|
+
}
|
|
154
|
+
static get closeBtnConfig() {
|
|
155
|
+
return structuredClone(this.#defaultConfig.closeBtnConfig);
|
|
156
|
+
}
|
|
157
|
+
/** @param {CustomElementConfig} cfg */
|
|
158
|
+
static set bodyConfig(cfg) {
|
|
159
|
+
this._updateConfigKey('bodyConfig', cfg);
|
|
160
|
+
}
|
|
161
|
+
static get bodyConfig() {
|
|
162
|
+
return structuredClone(this.#defaultConfig.bodyConfig);
|
|
163
|
+
}
|
|
164
|
+
/** @param {CustomElementConfig} cfg */
|
|
165
|
+
static set footerConfig(cfg) {
|
|
166
|
+
this._updateConfigKey('footerConfig', cfg);
|
|
167
|
+
}
|
|
168
|
+
static get footerConfig() {
|
|
169
|
+
return structuredClone(this.#defaultConfig.footerConfig);
|
|
170
|
+
}
|
|
171
|
+
/** @param {CustomElementConfig} cfg */
|
|
172
|
+
static set confirmBtnConfig(cfg) {
|
|
173
|
+
this._updateConfigKey('confirmBtnConfig', cfg);
|
|
174
|
+
}
|
|
175
|
+
static get confirmBtnConfig() {
|
|
176
|
+
return structuredClone(this.#defaultConfig.confirmBtnConfig);
|
|
177
|
+
}
|
|
178
|
+
/** @param {CustomElementConfig} cfg */
|
|
179
|
+
static set cancelBtnConfig(cfg) {
|
|
180
|
+
this._updateConfigKey('cancelBtnConfig', cfg);
|
|
181
|
+
}
|
|
182
|
+
static get cancelBtnConfig() {
|
|
183
|
+
return structuredClone(this.#defaultConfig.cancelBtnConfig);
|
|
184
|
+
}
|
|
185
|
+
/** @param {CustomElementConfig} cfg */
|
|
186
|
+
static set contentConfig(cfg) {
|
|
187
|
+
this._updateConfigKey('contentConfig', cfg);
|
|
188
|
+
}
|
|
189
|
+
static get contentConfig() {
|
|
190
|
+
return structuredClone(this.#defaultConfig.contentConfig);
|
|
191
|
+
}
|
|
192
|
+
/** @param {CustomElementConfig} cfg */
|
|
193
|
+
static set dialogConfig(cfg) {
|
|
194
|
+
this._updateConfigKey('dialogConfig', cfg);
|
|
195
|
+
}
|
|
196
|
+
static get dialogConfig() {
|
|
197
|
+
return structuredClone(this.#defaultConfig.dialogConfig);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Helper to forcefully clean up body styles injected by Bootstrap JS.
|
|
201
|
+
* @returns {void}
|
|
202
|
+
*/
|
|
203
|
+
static _restoreBody() {
|
|
204
|
+
const hasDialog = this._modalElement;
|
|
205
|
+
const hasLoading = this._loadingElement;
|
|
206
|
+
// If no modal managed by this class is open, force body cleaning
|
|
207
|
+
if (!hasDialog && !hasLoading) {
|
|
208
|
+
document.body.classList.remove('modal-open');
|
|
209
|
+
document.body.style.overflow = '';
|
|
210
|
+
document.body.style.paddingRight = '';
|
|
211
|
+
document.querySelectorAll('.modal-backdrop').forEach((backdrop) => backdrop.remove());
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Applies custom classes and styles to a given element.
|
|
216
|
+
* Combines default configurations with user-provided overrides.
|
|
217
|
+
* @param {HTMLElement} element - The target element.
|
|
218
|
+
* @param {CustomElementConfig} [defaultConfig] - The default configuration.
|
|
219
|
+
* @param {CustomElementConfig} [userConfig] - The user customization.
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
static _applyCustomizations(element, defaultConfig, userConfig) {
|
|
223
|
+
// Helper to normalize className into a flat array of strings
|
|
224
|
+
const normalizeClasses = (/** @type {string | string[] | undefined} */ val) => {
|
|
225
|
+
if (!val)
|
|
226
|
+
return [];
|
|
227
|
+
if (Array.isArray(val))
|
|
228
|
+
return val.filter(Boolean);
|
|
229
|
+
return val.split(' ').filter(Boolean);
|
|
230
|
+
};
|
|
231
|
+
// 1. Apply Classes (Additive)
|
|
232
|
+
const defaultClasses = normalizeClasses(defaultConfig?.className);
|
|
233
|
+
const userClasses = normalizeClasses(userConfig?.className);
|
|
234
|
+
element.classList.add(...defaultClasses, ...userClasses);
|
|
235
|
+
// 2. Apply Styles (User overrides defaults)
|
|
236
|
+
const combinedStyles = {
|
|
237
|
+
...(defaultConfig?.styles || {}),
|
|
238
|
+
...(userConfig?.styles || {}),
|
|
239
|
+
};
|
|
240
|
+
for (const [property, value] of Object.entries(combinedStyles)) {
|
|
241
|
+
element.style.setProperty(property, value);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Validates the ModalOptions object deeply.
|
|
246
|
+
* @param {ModalOptions} options - The options to validate.
|
|
247
|
+
* @throws {TypeError} If validation fails.
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
static _validateOptions(options) {
|
|
251
|
+
if (typeof options !== 'object' || options === null) {
|
|
252
|
+
throw new TypeError('Options must be a non-null object.');
|
|
253
|
+
}
|
|
254
|
+
const configKeys = [
|
|
255
|
+
'modalConfig',
|
|
256
|
+
'headerConfig',
|
|
257
|
+
'titleConfig',
|
|
258
|
+
'bodyConfig',
|
|
259
|
+
'footerConfig',
|
|
260
|
+
'confirmBtnConfig',
|
|
261
|
+
'cancelBtnConfig',
|
|
262
|
+
'closeBtnConfig',
|
|
263
|
+
'contentConfig',
|
|
264
|
+
'dialogConfig',
|
|
265
|
+
];
|
|
266
|
+
for (const key of configKeys) {
|
|
267
|
+
// @ts-ignore
|
|
268
|
+
if (options[key] !== undefined) {
|
|
269
|
+
// @ts-ignore
|
|
270
|
+
const config = options[key];
|
|
271
|
+
if (typeof config !== 'object' || config === null) {
|
|
272
|
+
throw new TypeError(`The property "${key}" must be an object.`);
|
|
273
|
+
}
|
|
274
|
+
if (config.className !== undefined &&
|
|
275
|
+
typeof config.className !== 'string' &&
|
|
276
|
+
!Array.isArray(config.className)) {
|
|
277
|
+
throw new TypeError(`The "className" property in "${key}" must be a string or an array of strings.`);
|
|
278
|
+
}
|
|
279
|
+
if (config.styles !== undefined) {
|
|
280
|
+
if (typeof config.styles !== 'object' || config.styles === null) {
|
|
281
|
+
throw new TypeError(`The "styles" property in "${key}" must be an object.`);
|
|
282
|
+
}
|
|
283
|
+
for (const styleKey of Object.keys(config.styles)) {
|
|
284
|
+
if (typeof config.styles[styleKey] !== 'string') {
|
|
285
|
+
throw new TypeError(`The style property "${styleKey}" in "${key}" must have a string value.`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Displays a simple alert modal.
|
|
294
|
+
* @param {string} message - The text message to be displayed to the user.
|
|
295
|
+
* @param {ModalOptions} [options={}] - Configuration options for the alert modal.
|
|
296
|
+
* @returns {Promise<void>} A promise that resolves when the alert is closed.
|
|
297
|
+
*/
|
|
298
|
+
static async alert(message, options = {}) {
|
|
299
|
+
const title = options.title ?? 'Alert';
|
|
300
|
+
const confirmText = options.confirmText ?? 'OK';
|
|
301
|
+
await this._show(title, message, false, confirmText, null, 'alert', options);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Displays a confirmation modal.
|
|
305
|
+
* @param {string} message - The question or message to be displayed to the user.
|
|
306
|
+
* @param {ModalOptions} [options={}] - Configuration options for the confirmation modal.
|
|
307
|
+
* @returns {Promise<boolean>} A promise that resolves with true if confirmed, or false if cancelled.
|
|
308
|
+
*/
|
|
309
|
+
static async confirm(message, options = {}) {
|
|
310
|
+
const title = options.title ?? 'Confirm';
|
|
311
|
+
const confirmText = options.confirmText ?? 'Yes';
|
|
312
|
+
const cancelText = options.cancelText ?? 'No';
|
|
313
|
+
return await this._show(title, message, true, confirmText, cancelText, 'confirm', options);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Displays a prompt modal with an input field.
|
|
317
|
+
* @param {string} message - The instruction or question for the user.
|
|
318
|
+
* @param {string} [defaultValue=''] - The initial value for the input field.
|
|
319
|
+
* @param {ModalOptions} [options={}] - Configuration options for the prompt modal.
|
|
320
|
+
* @returns {Promise<string|null>} A promise that resolves with the input value, or null if cancelled.
|
|
321
|
+
*/
|
|
322
|
+
static async prompt(message, defaultValue = '', options = {}) {
|
|
323
|
+
const title = options.title ?? 'Prompt';
|
|
324
|
+
const confirmText = options.confirmText ?? 'Submit';
|
|
325
|
+
const cancelText = options.cancelText ?? 'Cancel';
|
|
326
|
+
// Build prompt body with an input element
|
|
327
|
+
const bodyContainer = document.createElement('div');
|
|
328
|
+
const textNode = document.createElement('div');
|
|
329
|
+
textNode.textContent = message;
|
|
330
|
+
const input = document.createElement('input');
|
|
331
|
+
input.type = 'text';
|
|
332
|
+
input.className = 'form-control mt-2';
|
|
333
|
+
input.id = 'bs-prompt-input';
|
|
334
|
+
input.value = defaultValue;
|
|
335
|
+
bodyContainer.append(textNode, input);
|
|
336
|
+
return await this._show(title, bodyContainer, true, confirmText, cancelText, 'prompt', options);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Displays a global, non-dismissible loading overlay for background/DB tasks.
|
|
340
|
+
* @param {string} [message='Processing...'] - The text to display while the loading state is active.
|
|
341
|
+
* @returns {void}
|
|
342
|
+
*/
|
|
343
|
+
static showLoading(message = 'Processing...') {
|
|
344
|
+
if (this._loadingInstance)
|
|
345
|
+
return; // Prevent multiple loading overlays
|
|
346
|
+
const modal = document.createElement('div');
|
|
347
|
+
modal.className = 'modal fade';
|
|
348
|
+
modal.id = 'bs-loading-modal';
|
|
349
|
+
modal.setAttribute('data-bs-backdrop', 'static');
|
|
350
|
+
modal.setAttribute('data-bs-keyboard', 'false');
|
|
351
|
+
modal.style.zIndex = '1070';
|
|
352
|
+
const dialog = document.createElement('div');
|
|
353
|
+
dialog.className = 'modal-dialog modal-dialog-centered modal-sm';
|
|
354
|
+
const content = document.createElement('div');
|
|
355
|
+
content.className = 'modal-content bg-transparent border-0 shadow-none';
|
|
356
|
+
const body = document.createElement('div');
|
|
357
|
+
body.className =
|
|
358
|
+
'modal-body text-center d-flex flex-column align-items-center justify-content-center';
|
|
359
|
+
const spinner = document.createElement('div');
|
|
360
|
+
spinner.className = 'spinner-border text-primary shadow-sm mb-3';
|
|
361
|
+
spinner.setAttribute('role', 'status');
|
|
362
|
+
spinner.style.width = '4rem';
|
|
363
|
+
spinner.style.height = '4rem';
|
|
364
|
+
const title = document.createElement('h5');
|
|
365
|
+
title.className = 'fw-bold text-white shadow-sm px-4 py-2 rounded bg-dark bg-opacity-75';
|
|
366
|
+
title.textContent = message;
|
|
367
|
+
body.append(spinner, title);
|
|
368
|
+
content.appendChild(body);
|
|
369
|
+
dialog.appendChild(content);
|
|
370
|
+
modal.appendChild(dialog);
|
|
371
|
+
document.body.appendChild(modal);
|
|
372
|
+
this._loadingElement = modal;
|
|
373
|
+
this._loadingInstance = new BootstrapDialogs.Modal(modal, {
|
|
374
|
+
backdrop: 'static',
|
|
375
|
+
keyboard: false,
|
|
376
|
+
});
|
|
377
|
+
this._loadingInstance.show();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Hides and destroys the active loading overlay.
|
|
381
|
+
* @returns {void}
|
|
382
|
+
*/
|
|
383
|
+
static hideLoading() {
|
|
384
|
+
if (this._loadingInstance) {
|
|
385
|
+
this._loadingInstance.hide();
|
|
386
|
+
this._loadingInstance.dispose();
|
|
387
|
+
this._loadingInstance = null;
|
|
388
|
+
}
|
|
389
|
+
if (this._loadingElement) {
|
|
390
|
+
this._loadingElement.remove();
|
|
391
|
+
this._loadingElement = null;
|
|
392
|
+
}
|
|
393
|
+
this._restoreBody();
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Core method to build the modal DOM structure safely.
|
|
397
|
+
* @param {string} titleText - The text to be placed in the modal header.
|
|
398
|
+
* @param {HTMLElement|string} bodyContent - The content to be placed in the modal body.
|
|
399
|
+
* @param {boolean} showCancel - Whether to display the cancel button.
|
|
400
|
+
* @param {string} confirmText - The text for the confirmation button.
|
|
401
|
+
* @param {string|null} [cancelText='Cancel'] - The text for the cancel button.
|
|
402
|
+
* @param {ModalOptions} [options={}] - Configuration options for customization.
|
|
403
|
+
* @returns {HTMLElement} The constructed modal element.
|
|
404
|
+
*/
|
|
405
|
+
static _buildModalElement(titleText, bodyContent, showCancel, confirmText, cancelText = 'Cancel', options = {}) {
|
|
406
|
+
const modal = document.createElement('div');
|
|
407
|
+
modal.setAttribute('tabindex', '-1');
|
|
408
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
409
|
+
this._applyCustomizations(modal, this.#defaultConfig.modalConfig, options.modalConfig);
|
|
410
|
+
const dialog = document.createElement('div');
|
|
411
|
+
this._applyCustomizations(dialog, this.#defaultConfig.dialogConfig, options.dialogConfig);
|
|
412
|
+
const content = document.createElement('div');
|
|
413
|
+
this._applyCustomizations(content, this.#defaultConfig.contentConfig, options.contentConfig);
|
|
414
|
+
// Header
|
|
415
|
+
const header = document.createElement('div');
|
|
416
|
+
this._applyCustomizations(header, this.#defaultConfig.headerConfig, options.headerConfig);
|
|
417
|
+
const title = document.createElement('h5');
|
|
418
|
+
title.textContent = titleText;
|
|
419
|
+
this._applyCustomizations(title, this.#defaultConfig.titleConfig, options.titleConfig);
|
|
420
|
+
const closeBtn = document.createElement('button');
|
|
421
|
+
closeBtn.type = 'button';
|
|
422
|
+
closeBtn.setAttribute('data-bs-dismiss', 'modal');
|
|
423
|
+
closeBtn.setAttribute('aria-label', 'Close');
|
|
424
|
+
this._applyCustomizations(closeBtn, this.#defaultConfig.closeBtnConfig, options.closeBtnConfig);
|
|
425
|
+
header.append(title, closeBtn);
|
|
426
|
+
// Body
|
|
427
|
+
const body = document.createElement('div');
|
|
428
|
+
this._applyCustomizations(body, this.#defaultConfig.bodyConfig, options.bodyConfig);
|
|
429
|
+
if (bodyContent instanceof Node) {
|
|
430
|
+
body.appendChild(bodyContent);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
body.textContent = bodyContent;
|
|
434
|
+
}
|
|
435
|
+
// Footer
|
|
436
|
+
const footer = document.createElement('div');
|
|
437
|
+
this._applyCustomizations(footer, this.#defaultConfig.footerConfig, options.footerConfig);
|
|
438
|
+
if (showCancel) {
|
|
439
|
+
const cancelBtn = document.createElement('button');
|
|
440
|
+
cancelBtn.type = 'button';
|
|
441
|
+
cancelBtn.setAttribute('data-bs-dismiss', 'modal');
|
|
442
|
+
cancelBtn.textContent = cancelText;
|
|
443
|
+
this._applyCustomizations(cancelBtn, this.#defaultConfig.cancelBtnConfig, options.cancelBtnConfig);
|
|
444
|
+
footer.appendChild(cancelBtn);
|
|
445
|
+
}
|
|
446
|
+
const confirmBtn = document.createElement('button');
|
|
447
|
+
confirmBtn.type = 'button';
|
|
448
|
+
confirmBtn.id = 'bs-modal-confirm';
|
|
449
|
+
confirmBtn.textContent = confirmText;
|
|
450
|
+
this._applyCustomizations(confirmBtn, this.#defaultConfig.confirmBtnConfig, options.confirmBtnConfig);
|
|
451
|
+
footer.appendChild(confirmBtn);
|
|
452
|
+
content.append(header, body, footer);
|
|
453
|
+
dialog.appendChild(content);
|
|
454
|
+
modal.appendChild(dialog);
|
|
455
|
+
return modal;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Clean up any existing modal and resolve its promise.
|
|
459
|
+
* @returns {void}
|
|
460
|
+
*/
|
|
461
|
+
static _cleanup() {
|
|
462
|
+
if (this._activeResolve) {
|
|
463
|
+
/** @type {any} */
|
|
464
|
+
let cancelValue;
|
|
465
|
+
if (this._activeType === 'prompt')
|
|
466
|
+
cancelValue = null;
|
|
467
|
+
else if (this._activeType === 'confirm')
|
|
468
|
+
cancelValue = false;
|
|
469
|
+
else
|
|
470
|
+
cancelValue = undefined;
|
|
471
|
+
this._activeResolve(cancelValue);
|
|
472
|
+
this._activeResolve = null;
|
|
473
|
+
}
|
|
474
|
+
if (this._activeInstance) {
|
|
475
|
+
this._activeInstance.dispose();
|
|
476
|
+
this._activeInstance = null;
|
|
477
|
+
}
|
|
478
|
+
if (this._modalElement) {
|
|
479
|
+
this._modalElement.remove();
|
|
480
|
+
this._modalElement = null;
|
|
481
|
+
}
|
|
482
|
+
this._restoreBody();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Internal method to initialize the modal instance and handle lifecycle.
|
|
486
|
+
* @param {string} title - The title text for the modal.
|
|
487
|
+
* @param {HTMLElement|string} bodyContent - The content to be placed in the modal body.
|
|
488
|
+
* @param {boolean} showCancel - Whether to include a cancel button.
|
|
489
|
+
* @param {string} confirmText - The text for the confirmation button.
|
|
490
|
+
* @param {string|null} [cancelText='Cancel'] - The text for the cancel button.
|
|
491
|
+
* @param {'alert'|'confirm'|'prompt'} [type='alert'] - The type of dialog being shown.
|
|
492
|
+
* @param {ModalOptions} [options={}] - Configuration options for customization.
|
|
493
|
+
* @returns {Promise<any>} A promise that resolves with the user's action.
|
|
494
|
+
*/
|
|
495
|
+
static _show(title, bodyContent, showCancel, confirmText, cancelText, type = 'alert', options = {}) {
|
|
496
|
+
this._validateOptions(options);
|
|
497
|
+
this._cleanup();
|
|
498
|
+
return new Promise((resolve) => {
|
|
499
|
+
this._activeResolve = resolve;
|
|
500
|
+
this._activeType = type;
|
|
501
|
+
const modalElement = this._buildModalElement(title, bodyContent, showCancel, confirmText, cancelText, options);
|
|
502
|
+
this._modalElement = modalElement;
|
|
503
|
+
document.body.appendChild(modalElement);
|
|
504
|
+
const confirmBtn = modalElement.querySelector('#bs-modal-confirm');
|
|
505
|
+
const inputField = modalElement.querySelector('#bs-prompt-input');
|
|
506
|
+
if (!confirmBtn) {
|
|
507
|
+
throw new Error('Critical Error: Confirm button not found in modal template.');
|
|
508
|
+
}
|
|
509
|
+
this._activeInstance = new BootstrapDialogs.Modal(modalElement);
|
|
510
|
+
/** @type {boolean} */
|
|
511
|
+
let isConfirmed = false;
|
|
512
|
+
/**
|
|
513
|
+
* Handles the confirmation action.
|
|
514
|
+
*/
|
|
515
|
+
const handleConfirm = () => {
|
|
516
|
+
isConfirmed = true;
|
|
517
|
+
/** @type {any} */
|
|
518
|
+
let value;
|
|
519
|
+
if (type === 'prompt')
|
|
520
|
+
value = inputField instanceof HTMLInputElement ? inputField.value : null;
|
|
521
|
+
else if (type === 'confirm')
|
|
522
|
+
value = true;
|
|
523
|
+
else
|
|
524
|
+
value = true;
|
|
525
|
+
this._activeInstance?.hide();
|
|
526
|
+
resolve(value);
|
|
527
|
+
};
|
|
528
|
+
confirmBtn.addEventListener('click', handleConfirm);
|
|
529
|
+
// Handle cancel/close
|
|
530
|
+
modalElement.addEventListener('hidden.bs.modal', () => {
|
|
531
|
+
if (this._activeResolve === resolve) {
|
|
532
|
+
if (!isConfirmed) {
|
|
533
|
+
/** @type {any} */
|
|
534
|
+
let value;
|
|
535
|
+
if (type === 'prompt')
|
|
536
|
+
value = null;
|
|
537
|
+
else if (type === 'confirm')
|
|
538
|
+
value = false;
|
|
539
|
+
else
|
|
540
|
+
value = undefined;
|
|
541
|
+
resolve(value);
|
|
542
|
+
}
|
|
543
|
+
this._activeResolve = null;
|
|
544
|
+
this._activeInstance = null;
|
|
545
|
+
}
|
|
546
|
+
modalElement.remove();
|
|
547
|
+
this._restoreBody();
|
|
548
|
+
});
|
|
549
|
+
this._activeInstance.show();
|
|
550
|
+
if (type === 'prompt' && inputField) {
|
|
551
|
+
modalElement.addEventListener('shown.bs.modal', () => {
|
|
552
|
+
if (!(inputField instanceof HTMLInputElement))
|
|
553
|
+
return;
|
|
554
|
+
inputField.focus();
|
|
555
|
+
inputField.select();
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
export { BootstrapDialogs };
|
|
562
|
+
/**
|
|
563
|
+
* @param {string} msg - The message to display.
|
|
564
|
+
* @param {ModalOptions} [options] - Configuration options for the alert.
|
|
565
|
+
* @returns {Promise<void>}
|
|
566
|
+
*/
|
|
567
|
+
export const alert = (msg, options) => BootstrapDialogs.alert(msg, options);
|
|
568
|
+
/**
|
|
569
|
+
* @param {string} msg - The message to display.
|
|
570
|
+
* @param {string} [def] - The default value for the input field.
|
|
571
|
+
* @param {ModalOptions} [options] - Configuration options for the prompt.
|
|
572
|
+
* @returns {Promise<string|null>}
|
|
573
|
+
*/
|
|
574
|
+
export const prompt = (msg, def, options) => BootstrapDialogs.prompt(msg, def, options);
|
|
575
|
+
/**
|
|
576
|
+
* @param {string} msg - The message to display.
|
|
577
|
+
* @param {ModalOptions} [options] - Configuration options for the confirmation.
|
|
578
|
+
* @returns {Promise<boolean>}
|
|
579
|
+
*/
|
|
580
|
+
export const confirm = (msg, options) => BootstrapDialogs.confirm(msg, options);
|
|
581
|
+
/**
|
|
582
|
+
* @param {string} msg - The message to display.
|
|
583
|
+
* @returns {void}
|
|
584
|
+
*/
|
|
585
|
+
export const showLoading = (msg) => BootstrapDialogs.showLoading(msg);
|
|
586
|
+
/**
|
|
587
|
+
* Hides and destroys the active loading overlay.
|
|
588
|
+
* @returns {void}
|
|
589
|
+
*/
|
|
590
|
+
export const hideLoading = () => BootstrapDialogs.hideLoading();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
@import 'bootstrap/scss/functions';
|
|
2
|
+
@import 'bootstrap/scss/variables';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fixes placeholder visibility conflicts
|
|
6
|
+
* when using dark background utility classes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// 1. Variables
|
|
10
|
+
// We define a light color with enough opacity to be visible on dark backgrounds.
|
|
11
|
+
$placeholder-light-color: rgba($white, 0.6);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @mixin fix-placeholder-contrast($color)
|
|
15
|
+
* A mixin to force placeholder visibility.
|
|
16
|
+
* @param {Color} $color - The color to apply to the placeholder.
|
|
17
|
+
*/
|
|
18
|
+
@mixin fix-placeholder-contrast($color) {
|
|
19
|
+
// Standard modern browsers
|
|
20
|
+
&::placeholder {
|
|
21
|
+
color: $color !important;
|
|
22
|
+
opacity: 1; // Crucial: Bootstrap sets a default opacity that makes it too dark.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Webkit browsers (Chrome, Safari, Edge)
|
|
26
|
+
&::-webkit-input-placeholder {
|
|
27
|
+
color: $color !important;
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Firefox
|
|
32
|
+
&::-moz-placeholder {
|
|
33
|
+
color: $color !important;
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Internet Explorer / Legacy Edge
|
|
38
|
+
&:-ms-input-placeholder {
|
|
39
|
+
color: $color !important;
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 2. Specific Fix for Bootstrap Utility Conflicts
|
|
46
|
+
* This targets inputs that have both the Bootstrap form class
|
|
47
|
+
* and the dark background utility class.
|
|
48
|
+
*/
|
|
49
|
+
.form-control.bg-dark {
|
|
50
|
+
// We use the mixin to ensure the placeholder is visible against the dark background.
|
|
51
|
+
@include fix-placeholder-contrast($placeholder-light-color);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fix for when using .text-white on inputs to ensure
|
|
55
|
+
// the placeholder doesn't look "muddy"
|
|
56
|
+
.form-control.text-white {
|
|
57
|
+
@include fix-placeholder-contrast(rgba($white, 0.5));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 3. Proactive usage (Generic Mixin Example)
|
|
62
|
+
* If you create a custom dark component later, you can simply use:
|
|
63
|
+
* .my-custom-input { @include fix-placeholder-contrast(white); }
|
|
64
|
+
*/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import 'bootstrap/scss/functions';
|
|
2
|
+
@import 'bootstrap/scss/variables';
|
|
3
|
+
@import 'bootstrap/scss/mixins';
|
|
4
|
+
|
|
5
|
+
// 1. Definition of the height values we want to generate
|
|
6
|
+
// We use a map to associate the suffix with the percentage value
|
|
7
|
+
$height-options: (
|
|
8
|
+
'25': 25%,
|
|
9
|
+
'50': 50%,
|
|
10
|
+
'75': 75%,
|
|
11
|
+
'100': 100%,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// 2. Iteration over Bootstrap breakpoints
|
|
15
|
+
// We use the $grid-breakpoints variable already existing in Bootstrap
|
|
16
|
+
@each $breakpoint, $width in $grid-breakpoints {
|
|
17
|
+
// Skip 'xs' (extra small) because Bootstrap responsive classes start from 'sm'.
|
|
18
|
+
// 'xs' is the default (mobile-first) without a media query.
|
|
19
|
+
@if $breakpoint != xs {
|
|
20
|
+
// Apply the Bootstrap breakpoint mixin
|
|
21
|
+
@include media-breakpoint-up($breakpoint) {
|
|
22
|
+
// 3. Iteration over the height values defined in step 1
|
|
23
|
+
@each $suffix, $percentage in $height-options {
|
|
24
|
+
// Generates the class, e.g.: .h-sm-25, .h-md-50, .h-lg-100, etc.
|
|
25
|
+
.h-#{$breakpoint}-#{$suffix} {
|
|
26
|
+
height: $percentage !important;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|