refactor: Modularize UI components and utilities

- Extract UI components into separate JS files
- Centralize configuration values in config.js
- Introduce a dedicated logger module
- Improve file tree drag-and-drop and undo functionality
- Refactor modal handling to a single manager
- Add URL routing support for SPA navigation
- Implement view mode for read-only access
This commit is contained in:
Mahmoud-Emad
2025-10-26 15:42:15 +03:00
parent 23a24d42e2
commit 0ed6bcf1f2
34 changed files with 4136 additions and 940 deletions

View File

@@ -1,68 +1,169 @@
/**
* Confirmation Modal Manager
* Unified Modal Manager
* Handles showing and hiding a Bootstrap modal for confirmations and prompts.
* Uses a single reusable modal element to prevent double-opening issues.
*/
class Confirmation {
class ModalManager {
constructor(modalId) {
this.modalElement = document.getElementById(modalId);
this.modal = new bootstrap.Modal(this.modalElement);
if (!this.modalElement) {
console.error(`Modal element with id "${modalId}" not found`);
return;
}
this.modal = new bootstrap.Modal(this.modalElement, {
backdrop: 'static',
keyboard: true
});
this.messageElement = this.modalElement.querySelector('#confirmationMessage');
this.inputElement = this.modalElement.querySelector('#confirmationInput');
this.confirmButton = this.modalElement.querySelector('#confirmButton');
this.cancelButton = this.modalElement.querySelector('[data-bs-dismiss="modal"]');
this.titleElement = this.modalElement.querySelector('.modal-title');
this.currentResolver = null;
this.isShowing = false;
}
_show(message, title, showInput = false, defaultValue = '') {
/**
* Show a confirmation dialog
* @param {string} message - The message to display
* @param {string} title - The dialog title
* @param {boolean} isDangerous - Whether this is a dangerous action (shows red button)
* @returns {Promise<boolean>} - Resolves to true if confirmed, false/null if cancelled
*/
confirm(message, title = 'Confirmation', isDangerous = false) {
return new Promise((resolve) => {
// Prevent double-opening
if (this.isShowing) {
console.warn('Modal is already showing, ignoring duplicate request');
resolve(null);
return;
}
this.isShowing = true;
this.currentResolver = resolve;
this.titleElement.textContent = title;
this.messageElement.textContent = message;
this.inputElement.style.display = 'none';
if (showInput) {
this.inputElement.style.display = 'block';
this.inputElement.value = defaultValue;
this.inputElement.focus();
// Update button styling based on danger level
if (isDangerous) {
this.confirmButton.className = 'btn btn-danger';
this.confirmButton.textContent = 'Delete';
} else {
this.inputElement.style.display = 'none';
this.confirmButton.className = 'btn btn-primary';
this.confirmButton.textContent = 'OK';
}
this.confirmButton.onclick = () => this._handleConfirm(showInput);
this.modalElement.addEventListener('hidden.bs.modal', () => this._handleCancel(), { once: true });
// Set up event handlers
this.confirmButton.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
this._handleConfirm(false);
};
// Handle modal hidden event for cleanup
this.modalElement.addEventListener('hidden.bs.modal', () => {
if (this.currentResolver) {
this._handleCancel();
}
}, { once: true });
this.modal.show();
// Focus confirm button after modal is shown
this.modalElement.addEventListener('shown.bs.modal', () => {
this.confirmButton.focus();
}, { once: true });
});
}
/**
* Show a prompt dialog (input dialog)
* @param {string} message - The message/label to display
* @param {string} defaultValue - The default input value
* @param {string} title - The dialog title
* @returns {Promise<string|null>} - Resolves to input value if confirmed, null if cancelled
*/
prompt(message, defaultValue = '', title = 'Input') {
return new Promise((resolve) => {
// Prevent double-opening
if (this.isShowing) {
console.warn('Modal is already showing, ignoring duplicate request');
resolve(null);
return;
}
this.isShowing = true;
this.currentResolver = resolve;
this.titleElement.textContent = title;
this.messageElement.textContent = message;
this.inputElement.style.display = 'block';
this.inputElement.value = defaultValue;
// Reset button to primary style for prompts
this.confirmButton.className = 'btn btn-primary';
this.confirmButton.textContent = 'OK';
// Set up event handlers
this.confirmButton.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
this._handleConfirm(true);
};
// Handle Enter key in input
this.inputElement.onkeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this._handleConfirm(true);
}
};
// Handle modal hidden event for cleanup
this.modalElement.addEventListener('hidden.bs.modal', () => {
if (this.currentResolver) {
this._handleCancel();
}
}, { once: true });
this.modal.show();
// Focus and select input after modal is shown
this.modalElement.addEventListener('shown.bs.modal', () => {
this.inputElement.focus();
this.inputElement.select();
}, { once: true });
});
}
_handleConfirm(isPrompt) {
if (this.currentResolver) {
const value = isPrompt ? this.inputElement.value : true;
this.currentResolver(value);
const value = isPrompt ? this.inputElement.value.trim() : true;
const resolver = this.currentResolver;
this._cleanup();
resolver(value);
}
}
_handleCancel() {
if (this.currentResolver) {
this.currentResolver(null); // Resolve with null for cancellation
const resolver = this.currentResolver;
this._cleanup();
resolver(null);
}
}
_cleanup() {
this.confirmButton.onclick = null;
this.modal.hide();
this.inputElement.onkeydown = null;
this.currentResolver = null;
}
confirm(message, title = 'Confirmation') {
return this._show(message, title, false);
}
prompt(message, defaultValue = '', title = 'Prompt') {
return this._show(message, title, true, defaultValue);
this.isShowing = false;
this.modal.hide();
}
}
// Make it globally available
window.ConfirmationManager = new Confirmation('confirmationModal');
window.ConfirmationManager = new ModalManager('confirmationModal');
window.ModalManager = window.ConfirmationManager; // Alias for clarity