2026-05-25 00:28:10 +02:00
|
|
|
import { i18n } from '../core/i18n.js';
|
|
|
|
|
import { replaceIconsInElement } from '../core/icons.js';
|
2026-04-13 15:09:10 +02:00
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
/**
|
|
|
|
|
* Modal System for OxiCloud
|
|
|
|
|
* Provides modern, styled modals to replace browser prompts/alerts
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const Modal = {
|
|
|
|
|
// Modal element references
|
2026-04-24 23:37:14 +02:00
|
|
|
/** @private @type {HTMLElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
overlay: null,
|
2026-04-24 23:37:14 +02:00
|
|
|
/** @private @type {HTMLElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
icon: null,
|
2026-04-24 23:37:14 +02:00
|
|
|
/** @private @type {HTMLElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
title: null,
|
2026-04-24 23:37:14 +02:00
|
|
|
/** @private @type {HTMLElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
label: null,
|
2026-05-07 23:40:02 +02:00
|
|
|
/** @private @type {HTMLInputElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
input: null,
|
2026-05-07 23:40:02 +02:00
|
|
|
/** @private @type {HTMLButtonElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
cancelBtn: null,
|
2026-05-07 23:40:02 +02:00
|
|
|
/** @private @type {HTMLButtonElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
confirmBtn: null,
|
2026-05-07 23:40:02 +02:00
|
|
|
/** @private @type {HTMLButtonElement | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
closeBtn: null,
|
|
|
|
|
|
|
|
|
|
// Current callback
|
2026-05-07 23:40:02 +02:00
|
|
|
/** @private @type {Function | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
onConfirm: null,
|
2026-05-07 23:40:02 +02:00
|
|
|
/** @private @type {Function | null} */
|
2026-04-07 22:48:59 +02:00
|
|
|
onCancel: null,
|
|
|
|
|
|
2026-05-09 00:06:30 +02:00
|
|
|
/** @private @type {((value: string) => Promise<void>) | null} */
|
|
|
|
|
_action: null,
|
|
|
|
|
|
|
|
|
|
/** @private @type {HTMLElement | null} */
|
|
|
|
|
errorEl: null,
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
// Rename mode: select only name without extension
|
|
|
|
|
_selectNameOnly: false,
|
|
|
|
|
|
2026-05-25 16:54:50 +02:00
|
|
|
// Panel mode — openPanel() sets this; skips input-focus logic
|
|
|
|
|
/** @private */
|
|
|
|
|
_panelMode: false,
|
|
|
|
|
|
|
|
|
|
// Saved modal-body innerHTML to restore when a panel closes
|
|
|
|
|
/** @private */
|
|
|
|
|
_savedBodyHTML: '',
|
|
|
|
|
|
2026-06-15 00:17:17 +02:00
|
|
|
// Element focused before the modal opened — focus returns here on close.
|
|
|
|
|
/** @private @type {HTMLElement|null} */
|
|
|
|
|
_previousFocus: null,
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
/**
|
|
|
|
|
* Initialize modal system
|
|
|
|
|
*/
|
|
|
|
|
init() {
|
|
|
|
|
this.overlay = document.getElementById('input-modal');
|
|
|
|
|
if (!this.overlay) {
|
|
|
|
|
console.warn('Modal overlay not found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.icon = document.getElementById('modal-icon');
|
|
|
|
|
this.title = document.getElementById('modal-title');
|
|
|
|
|
this.label = document.getElementById('modal-label');
|
2026-05-07 23:40:02 +02:00
|
|
|
this.input = /** @type {HTMLInputElement} */ (document.getElementById('modal-input'));
|
|
|
|
|
this.cancelBtn = /** @type {HTMLButtonElement} */ (document.getElementById('modal-cancel-btn'));
|
|
|
|
|
this.confirmBtn = /** @type {HTMLButtonElement} */ (document.getElementById('modal-confirm-btn'));
|
|
|
|
|
this.closeBtn = /** @type {HTMLButtonElement} */ (document.getElementById('modal-close-btn'));
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
|
|
|
// Event listeners
|
2026-05-09 00:06:30 +02:00
|
|
|
this.errorEl = document.getElementById('modal-error');
|
|
|
|
|
|
2026-04-30 02:02:27 +02:00
|
|
|
this.cancelBtn?.addEventListener('click', () => this.close(false));
|
|
|
|
|
this.closeBtn?.addEventListener('click', () => this.close(false));
|
|
|
|
|
this.confirmBtn?.addEventListener('click', () => this.confirm());
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
|
|
|
// Close on overlay click
|
|
|
|
|
this.overlay.addEventListener('click', (e) => {
|
|
|
|
|
if (e.target === this.overlay) {
|
|
|
|
|
this.close(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-15 00:17:17 +02:00
|
|
|
// Trap Tab focus within the dialog while it is open.
|
|
|
|
|
this.overlay.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Tab') this._trapFocus(e);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-09 00:06:30 +02:00
|
|
|
// Clear inline error as soon as the user starts typing
|
|
|
|
|
this.input?.addEventListener('input', () => this.clearError());
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
// Handle Enter and Escape keys
|
2026-04-30 02:02:27 +02:00
|
|
|
this.input?.addEventListener('keydown', (e) => {
|
2026-04-07 22:48:59 +02:00
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.confirm();
|
|
|
|
|
} else if (e.key === 'Escape') {
|
|
|
|
|
this.close(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-05-25 16:54:50 +02:00
|
|
|
|
|
|
|
|
// Escape in panel mode (input isn't focused so the above handler won't fire)
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Escape' && this._panelMode && !this.overlay?.classList.contains('hidden')) {
|
|
|
|
|
this.close(false);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
},
|
|
|
|
|
|
2026-05-09 00:06:30 +02:00
|
|
|
/** @param {string} message */
|
|
|
|
|
showError(message) {
|
|
|
|
|
this.input?.classList.add('modal-input--error');
|
|
|
|
|
if (this.errorEl) {
|
|
|
|
|
this.errorEl.textContent = message;
|
|
|
|
|
this.errorEl.classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clearError() {
|
|
|
|
|
this.input?.classList.remove('modal-input--error');
|
|
|
|
|
if (this.errorEl) {
|
|
|
|
|
this.errorEl.textContent = '';
|
|
|
|
|
this.errorEl.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
/**
|
|
|
|
|
* Show input modal (replacement for prompt())
|
|
|
|
|
* @param {Object} options - Modal configuration
|
2026-05-07 23:40:02 +02:00
|
|
|
* @param {string} [options.title] - Modal title
|
|
|
|
|
* @param {string} [options.label] - Input label
|
|
|
|
|
* @param {string} [options.placeholder] - Input placeholder
|
|
|
|
|
* @param {string} [options.value] - Initial input value
|
|
|
|
|
* @param {string} [options.icon] - Font Awesome icon class (e.g., 'fa-folder-plus')
|
|
|
|
|
* @param {string} [options.confirmText] - Confirm button text
|
|
|
|
|
* @param {string} [options.cancelText] - Cancel button text
|
2026-05-09 00:06:30 +02:00
|
|
|
* @param {(value: string) => Promise<void>} [options.action] - Async action called on confirm.
|
|
|
|
|
* Throw an Error to keep the modal open and display the error message inline.
|
|
|
|
|
* When omitted the modal resolves immediately with the input value (legacy behaviour).
|
2026-04-07 22:48:59 +02:00
|
|
|
* @returns {Promise<string|null>} - Resolves with input value or null if cancelled
|
|
|
|
|
*/
|
|
|
|
|
prompt(options = {}) {
|
|
|
|
|
return new Promise((resolve) => {
|
2026-05-09 00:06:30 +02:00
|
|
|
const {
|
|
|
|
|
title = 'Input',
|
|
|
|
|
label = '',
|
|
|
|
|
placeholder = '',
|
|
|
|
|
value = '',
|
|
|
|
|
icon = 'fa-keyboard',
|
|
|
|
|
confirmText = null,
|
|
|
|
|
cancelText = null,
|
|
|
|
|
action = null
|
|
|
|
|
} = options;
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
|
|
|
// Set modal content - update the icon
|
|
|
|
|
const iconContainer = document.querySelector('.modal-icon');
|
|
|
|
|
if (iconContainer) {
|
|
|
|
|
// Replace contents with a fresh <i> that icons.js will convert
|
|
|
|
|
iconContainer.innerHTML = `<i id="modal-icon" class="fas ${icon}"></i>`;
|
|
|
|
|
this.icon = document.getElementById('modal-icon');
|
|
|
|
|
// Let icons.js convert it to SVG
|
2026-04-13 15:09:10 +02:00
|
|
|
if (replaceIconsInElement) {
|
|
|
|
|
replaceIconsInElement(iconContainer);
|
2026-04-07 22:48:59 +02:00
|
|
|
this.icon = document.getElementById('modal-icon');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.title.textContent = title;
|
|
|
|
|
this.label.textContent = label;
|
|
|
|
|
this.input.placeholder = placeholder;
|
|
|
|
|
this.input.value = value;
|
|
|
|
|
|
|
|
|
|
if (confirmText) {
|
|
|
|
|
this.confirmBtn.textContent = confirmText;
|
2026-04-25 22:55:42 +02:00
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
this.confirmBtn.textContent = i18n.t('actions.confirm');
|
2026-04-07 22:48:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cancelText) {
|
|
|
|
|
this.cancelBtn.textContent = cancelText;
|
2026-04-25 22:55:42 +02:00
|
|
|
} else {
|
2026-04-13 15:09:10 +02:00
|
|
|
this.cancelBtn.textContent = i18n.t('actions.cancel');
|
2026-04-07 22:48:59 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 00:06:30 +02:00
|
|
|
this._action = action;
|
|
|
|
|
this.clearError();
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
// Set callbacks
|
|
|
|
|
this.onConfirm = () => {
|
|
|
|
|
const inputValue = this.input.value.trim();
|
|
|
|
|
resolve(inputValue || null);
|
|
|
|
|
};
|
|
|
|
|
this.onCancel = () => resolve(null);
|
|
|
|
|
|
|
|
|
|
// Show modal
|
|
|
|
|
this.open();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Show modal for creating new folder
|
2026-05-09 00:06:30 +02:00
|
|
|
* @param {(value: string) => Promise<void>} [action]
|
2026-04-07 22:48:59 +02:00
|
|
|
* @returns {Promise<string|null>}
|
|
|
|
|
*/
|
2026-05-09 00:06:30 +02:00
|
|
|
promptNewFolder(action = null) {
|
2026-04-07 22:48:59 +02:00
|
|
|
return this.prompt({
|
2026-04-25 23:19:36 +02:00
|
|
|
title: i18n.t('dialogs.new_folder_title'),
|
|
|
|
|
label: i18n.t('dialogs.folder_name'),
|
|
|
|
|
placeholder: i18n.t('dialogs.folder_placeholder'),
|
2026-04-07 22:48:59 +02:00
|
|
|
icon: 'fa-folder-plus',
|
2026-05-09 00:06:30 +02:00
|
|
|
confirmText: i18n.t('actions.create'),
|
|
|
|
|
action
|
2026-04-07 22:48:59 +02:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Show modal for renaming
|
|
|
|
|
* @param {string} currentName - Current name of file/folder
|
|
|
|
|
* @param {boolean} isFolder - Whether it's a folder
|
2026-05-09 00:06:30 +02:00
|
|
|
* @param {(value: string) => Promise<void>} [action]
|
2026-04-07 22:48:59 +02:00
|
|
|
* @returns {Promise<string|null>}
|
|
|
|
|
*/
|
2026-05-09 00:06:30 +02:00
|
|
|
promptRename(currentName, isFolder = false, action = null) {
|
2026-04-07 22:48:59 +02:00
|
|
|
this._selectNameOnly = !isFolder;
|
|
|
|
|
|
|
|
|
|
return this.prompt({
|
2026-04-25 23:19:36 +02:00
|
|
|
title: i18n.t('dialogs.rename_title'),
|
|
|
|
|
label: i18n.t('dialogs.new_name'),
|
2026-04-07 22:48:59 +02:00
|
|
|
placeholder: '',
|
|
|
|
|
value: currentName,
|
|
|
|
|
icon: isFolder ? 'fa-folder' : 'fa-file',
|
2026-05-09 00:06:30 +02:00
|
|
|
confirmText: i18n.t('actions.rename'),
|
|
|
|
|
action
|
2026-04-07 22:48:59 +02:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-15 00:17:17 +02:00
|
|
|
/**
|
|
|
|
|
* Keep Tab focus cycling within the dialog container (focus trap).
|
|
|
|
|
* @private
|
|
|
|
|
* @param {KeyboardEvent} e
|
|
|
|
|
*/
|
|
|
|
|
_trapFocus(e) {
|
|
|
|
|
const container = this.overlay?.querySelector('.modal-container');
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const focusables = /** @type {NodeListOf<HTMLElement>} */ (
|
|
|
|
|
container.querySelectorAll(
|
|
|
|
|
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
if (!focusables.length) return;
|
|
|
|
|
const first = focusables[0];
|
|
|
|
|
const last = focusables[focusables.length - 1];
|
|
|
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
last.focus();
|
|
|
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
first.focus();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
/**
|
|
|
|
|
* Open the modal
|
|
|
|
|
*/
|
|
|
|
|
open() {
|
|
|
|
|
if (!this.overlay) return;
|
|
|
|
|
|
2026-06-15 00:17:17 +02:00
|
|
|
this._previousFocus = /** @type {HTMLElement|null} */ (document.activeElement);
|
2026-05-19 11:17:32 +02:00
|
|
|
this.confirmBtn.disabled = false;
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
// Show overlay
|
2026-04-24 23:37:14 +02:00
|
|
|
this.overlay.classList.remove('hidden');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
|
|
|
// Trigger animation
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.overlay.classList.add('active');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Focus input after animation
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.input.focus();
|
|
|
|
|
|
|
|
|
|
if (this._selectNameOnly) {
|
|
|
|
|
// Select only the filename, excluding the extension
|
|
|
|
|
const value = this.input.value;
|
|
|
|
|
const lastDot = value.lastIndexOf('.');
|
|
|
|
|
if (lastDot > 0) {
|
|
|
|
|
this.input.setSelectionRange(0, lastDot);
|
|
|
|
|
} else {
|
|
|
|
|
this.input.select();
|
|
|
|
|
}
|
|
|
|
|
this._selectNameOnly = false;
|
|
|
|
|
} else {
|
|
|
|
|
this.input.select();
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Close the modal
|
|
|
|
|
* @param {boolean} confirmed - Whether the action was confirmed
|
|
|
|
|
*/
|
|
|
|
|
close(confirmed = false) {
|
|
|
|
|
if (!this.overlay) return;
|
|
|
|
|
|
2026-05-09 00:06:30 +02:00
|
|
|
this.clearError();
|
|
|
|
|
this._action = null;
|
2026-04-07 22:48:59 +02:00
|
|
|
this.overlay.classList.remove('active');
|
|
|
|
|
|
2026-05-25 16:54:50 +02:00
|
|
|
const wasPanel = this._panelMode;
|
|
|
|
|
|
2026-04-07 22:48:59 +02:00
|
|
|
setTimeout(() => {
|
2026-04-24 23:37:14 +02:00
|
|
|
this.overlay.classList.add('hidden');
|
2026-04-07 22:48:59 +02:00
|
|
|
|
|
|
|
|
if (!confirmed && this.onCancel) {
|
|
|
|
|
this.onCancel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear callbacks
|
|
|
|
|
this.onConfirm = null;
|
|
|
|
|
this.onCancel = null;
|
2026-05-25 16:54:50 +02:00
|
|
|
|
|
|
|
|
// Restore original modal-body content after a panel closes
|
|
|
|
|
if (wasPanel) {
|
|
|
|
|
const bodyEl = this.overlay?.querySelector('.modal-body');
|
|
|
|
|
if (bodyEl) bodyEl.innerHTML = this._savedBodyHTML;
|
|
|
|
|
this.overlay?.querySelector('.modal-container')?.classList.remove('modal-container--panel');
|
|
|
|
|
this._panelMode = false;
|
|
|
|
|
this._savedBodyHTML = '';
|
|
|
|
|
}
|
2026-06-15 00:17:17 +02:00
|
|
|
|
|
|
|
|
// Return focus to whatever was focused before the modal opened.
|
|
|
|
|
this._previousFocus?.focus?.();
|
|
|
|
|
this._previousFocus = null;
|
2026-04-07 22:48:59 +02:00
|
|
|
}, 200);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-09 00:06:30 +02:00
|
|
|
* Confirm the action. When an async action is set, the modal stays open
|
|
|
|
|
* until it resolves — closing only on success, showing the error inline on failure.
|
2026-04-07 22:48:59 +02:00
|
|
|
*/
|
2026-05-09 00:06:30 +02:00
|
|
|
async confirm() {
|
2026-05-25 16:54:50 +02:00
|
|
|
// Panel mode: delegate entirely to the caller-supplied onConfirm
|
|
|
|
|
if (this._panelMode) {
|
|
|
|
|
if (this.onConfirm) this.onConfirm();
|
|
|
|
|
this.close(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 00:06:30 +02:00
|
|
|
if (!this._action) {
|
|
|
|
|
if (this.onConfirm) this.onConfirm();
|
|
|
|
|
this.close(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const inputValue = this.input.value.trim();
|
|
|
|
|
if (!inputValue) return;
|
|
|
|
|
|
|
|
|
|
this.clearError();
|
|
|
|
|
this.confirmBtn.disabled = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this._action(inputValue);
|
|
|
|
|
if (this.onConfirm) this.onConfirm();
|
|
|
|
|
this.close(true);
|
|
|
|
|
} catch (e) {
|
2026-05-07 23:40:02 +02:00
|
|
|
this.showError(/** @type {Error} */ (e).message || 'An error occurred');
|
2026-05-09 00:06:30 +02:00
|
|
|
this.confirmBtn.disabled = false;
|
|
|
|
|
this.input.focus();
|
2026-04-07 22:48:59 +02:00
|
|
|
}
|
2026-05-25 16:54:50 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Open the modal with fully custom body content (panel mode).
|
|
|
|
|
*
|
|
|
|
|
* The caller supplies a pre-built HTMLElement as `content`; it is injected
|
|
|
|
|
* into `.modal-body`, replacing the default label/input/error elements for
|
|
|
|
|
* the lifetime of this panel. The overlay, header, animation, footer
|
|
|
|
|
* buttons, click-outside, and Escape handling all come from Modal.
|
|
|
|
|
*
|
|
|
|
|
* Original `.modal-body` innerHTML is restored automatically when the
|
|
|
|
|
* panel closes.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} options
|
|
|
|
|
* @param {string} options.title
|
2026-05-28 19:56:31 +02:00
|
|
|
* @param {string} [options.icon] - Font Awesome class, default 'fa-share-alt'
|
|
|
|
|
* @param {HTMLElement} options.content - DOM node to inject into .modal-body
|
|
|
|
|
* @param {string} [options.confirmText] - Confirm button label
|
|
|
|
|
* @param {string} [options.cancelText] - Cancel button label
|
|
|
|
|
* @param {boolean} [options.confirmDisabled] - Initial disabled state of the confirm button
|
|
|
|
|
* @param {() => void} [options.onConfirm] - Called when Confirm is clicked
|
|
|
|
|
* @param {() => void} [options.onCancel] - Called when Cancel / close is triggered
|
2026-05-25 16:54:50 +02:00
|
|
|
*/
|
2026-05-28 19:56:31 +02:00
|
|
|
openPanel({ title, icon = 'fa-share-alt', content, confirmText = null, cancelText = null, confirmDisabled = false, onConfirm = null, onCancel = null }) {
|
2026-05-25 16:54:50 +02:00
|
|
|
if (!this.overlay) return;
|
|
|
|
|
|
|
|
|
|
this._panelMode = true;
|
|
|
|
|
|
|
|
|
|
// ── Header ──────────────────────────────────────────────────────────
|
|
|
|
|
const iconContainer = this.overlay.querySelector('.modal-icon');
|
|
|
|
|
if (iconContainer) {
|
|
|
|
|
iconContainer.innerHTML = `<i class="fas ${icon}"></i>`;
|
|
|
|
|
if (replaceIconsInElement) replaceIconsInElement(iconContainer);
|
|
|
|
|
}
|
|
|
|
|
if (this.title) this.title.textContent = title;
|
|
|
|
|
|
|
|
|
|
// ── Body swap ───────────────────────────────────────────────────────
|
|
|
|
|
const bodyEl = this.overlay.querySelector('.modal-body');
|
|
|
|
|
if (bodyEl) {
|
|
|
|
|
this._savedBodyHTML = bodyEl.innerHTML;
|
|
|
|
|
bodyEl.replaceChildren(content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Container size modifier ──────────────────────────────────────────
|
|
|
|
|
this.overlay.querySelector('.modal-container')?.classList.add('modal-container--panel');
|
|
|
|
|
|
|
|
|
|
// ── Footer buttons ──────────────────────────────────────────────────
|
|
|
|
|
if (this.confirmBtn) {
|
|
|
|
|
this.confirmBtn.textContent = confirmText ?? i18n.t('actions.apply', 'Apply');
|
2026-05-28 19:56:31 +02:00
|
|
|
this.confirmBtn.disabled = confirmDisabled;
|
2026-05-25 16:54:50 +02:00
|
|
|
}
|
|
|
|
|
if (this.cancelBtn) {
|
|
|
|
|
this.cancelBtn.textContent = cancelText ?? i18n.t('actions.cancel');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Callbacks ───────────────────────────────────────────────────────
|
|
|
|
|
this.onConfirm = onConfirm;
|
|
|
|
|
this.onCancel = onCancel;
|
|
|
|
|
this._action = null;
|
|
|
|
|
this.clearError();
|
|
|
|
|
|
|
|
|
|
// ── Show overlay (same animation as prompt, no input focus) ─────────
|
|
|
|
|
this.overlay.classList.remove('hidden');
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.overlay.classList.add('active');
|
|
|
|
|
});
|
2026-04-07 22:48:59 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Initialize when DOM is ready
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
Modal.init();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Export for use in other modules
|
2026-04-13 15:09:10 +02:00
|
|
|
export { Modal };
|