fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -0,0 +1,270 @@
/**
* Modal Manager for LiveComponents
*
* Manages modal stack using native <dialog> element with showModal().
*
* Native <dialog> Features:
* - Automatic backdrop/overlay (::backdrop pseudo-element)
* - Automatic focus management (focus trapping)
* - Automatic ESC handling (cancel event)
* - Native modal semantics (blocks background interaction)
* - Better accessibility (ARIA attributes)
*
* Browser Support:
* - Chrome 37+ (2014)
* - Firefox 98+ (2022)
* - Safari 15.4+ (2022)
*/
export class ModalManager {
constructor() {
this.modalStack = []; // Array of { componentId, dialogElement, zIndex, closeOnEscape, closeOnBackdrop }
this.baseZIndex = 1050; // Bootstrap modal default
this.zIndexIncrement = 10;
}
/**
* Open modal using native <dialog> element
* @param {string} componentId - Component ID
* @param {Object} options - Modal options
* @returns {Object} Modal instance with dialog element
*/
open(componentId, options = {}) {
const {
title = '',
content = '',
size = 'medium',
buttons = [],
closeOnBackdrop = true,
closeOnEscape = true
} = options;
// Calculate z-index for stacking
const zIndex = this.baseZIndex + (this.modalStack.length * this.zIndexIncrement);
// Create native <dialog> element
const dialog = document.createElement('dialog');
dialog.className = `livecomponent-modal livecomponent-modal--${size}`;
dialog.id = `modal-${componentId}`;
dialog.style.zIndex = zIndex.toString();
// Set content
dialog.innerHTML = this.createModalContent(title, content, buttons);
// Add to DOM
document.body.appendChild(dialog);
// Setup event handlers BEFORE showing modal
this.setupDialogHandlers(dialog, componentId, closeOnBackdrop, closeOnEscape, buttons);
// Show modal using native showModal() - this blocks background interaction
dialog.showModal();
// Track in stack
const stackItem = {
componentId,
dialogElement: dialog,
zIndex,
closeOnEscape,
closeOnBackdrop
};
this.modalStack.push(stackItem);
// Focus management (native <dialog> handles focus trapping, but we set initial focus)
this.focusModal(dialog);
// Return wrapper object for compatibility
return {
dialog: dialog,
close: () => this.close(componentId),
isOpen: () => dialog.open
};
}
/**
* Setup event handlers for dialog element
* @param {HTMLDialogElement} dialog - Dialog element
* @param {string} componentId - Component ID
* @param {boolean} closeOnBackdrop - Whether to close on backdrop click
* @param {boolean} closeOnEscape - Whether to close on ESC key
* @param {Array} buttons - Button configurations
*/
setupDialogHandlers(dialog, componentId, closeOnBackdrop, closeOnEscape, buttons) {
// Native cancel event (triggered by ESC key)
dialog.addEventListener('cancel', (e) => {
e.preventDefault();
if (closeOnEscape) {
this.close(componentId);
}
});
// Backdrop click handling
// Note: Native <dialog> doesn't have built-in backdrop click handling
// We need to check if click target is the dialog element itself (backdrop)
if (closeOnBackdrop) {
dialog.addEventListener('click', (e) => {
// If click target is the dialog element itself (not its children), it's a backdrop click
if (e.target === dialog) {
this.close(componentId);
}
});
}
// Close button
const closeBtn = dialog.querySelector('.modal-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
this.close(componentId);
});
}
// Button actions
dialog.querySelectorAll('[data-modal-action]').forEach((btn, index) => {
btn.addEventListener('click', () => {
const buttonConfig = buttons[index];
if (buttonConfig && buttonConfig.action) {
// Trigger action if specified
if (buttonConfig.action === 'close') {
this.close(componentId);
}
}
});
});
}
/**
* Close modal and remove from stack
* @param {string} componentId - Component ID
*/
close(componentId) {
const index = this.modalStack.findIndex(item => item.componentId === componentId);
if (index === -1) {
return;
}
const { dialogElement } = this.modalStack[index];
// Close dialog using native close() method
if (dialogElement && dialogElement.open) {
dialogElement.close();
}
// Remove from DOM
if (dialogElement.parentNode) {
dialogElement.parentNode.removeChild(dialogElement);
}
// Remove from stack
this.modalStack.splice(index, 1);
// Focus previous modal or return focus to body
if (this.modalStack.length > 0) {
const previousModal = this.modalStack[this.modalStack.length - 1];
this.focusModal(previousModal.dialogElement);
} else {
// Return focus to previously focused element or body
// Native <dialog> handles this automatically, but we ensure it
const activeElement = document.activeElement;
if (activeElement && activeElement !== document.body) {
activeElement.blur();
}
document.body.focus();
}
}
/**
* Close all modals
*/
closeAll() {
while (this.modalStack.length > 0) {
const { componentId } = this.modalStack[this.modalStack.length - 1];
this.close(componentId);
}
}
/**
* Get topmost modal
* @returns {Object|null}
*/
getTopModal() {
if (this.modalStack.length === 0) {
return null;
}
return this.modalStack[this.modalStack.length - 1];
}
/**
* Check if modal is open
* @param {string} componentId - Component ID
* @returns {boolean}
*/
isOpen(componentId) {
return this.modalStack.some(item => item.componentId === componentId);
}
/**
* Focus modal element
* Native <dialog> handles focus trapping automatically, but we set initial focus
* @param {HTMLDialogElement} dialog - Dialog element
*/
focusModal(dialog) {
if (!dialog) {
return;
}
// Find first focusable element
const focusableSelectors = [
'button:not([disabled])',
'[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
const focusableElement = dialog.querySelector(focusableSelectors);
if (focusableElement) {
// Small delay to ensure dialog is fully rendered
requestAnimationFrame(() => {
focusableElement.focus();
});
} else {
// Fallback: focus dialog itself
requestAnimationFrame(() => {
dialog.focus();
});
}
}
/**
* Create modal content HTML
* @param {string} title - Modal title
* @param {string} content - Modal content
* @param {Array} buttons - Button configurations
* @returns {string}
*/
createModalContent(title, content, buttons) {
const buttonsHtml = buttons.map((btn, index) => {
const btnClass = btn.class || btn.className || 'btn-secondary';
return `<button class="btn ${btnClass}" data-modal-action="${index}">${btn.text || 'Button'}</button>`;
}).join('');
return `
<div class="livecomponent-modal-content">
${title ? `<div class="modal-header">
<h3 class="modal-title">${title}</h3>
<button class="modal-close" aria-label="Close">×</button>
</div>` : ''}
<div class="modal-body">${content}</div>
${buttons.length > 0 ? `<div class="modal-footer">${buttonsHtml}</div>` : ''}
</div>
`;
}
/**
* Cleanup all modals
*/
destroy() {
this.closeAll();
}
}