fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
315
resources/js/modules/livecomponent/DrawerManager.js
Normal file
315
resources/js/modules/livecomponent/DrawerManager.js
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Drawer Manager for LiveComponents
|
||||
*
|
||||
* Manages drawer/sidebar components with:
|
||||
* - Slide-in animation (CSS Transitions)
|
||||
* - Overlay backdrop management
|
||||
* - ESC key handling
|
||||
* - Focus management (focus trapping)
|
||||
* - Stack management for multiple drawers
|
||||
*/
|
||||
|
||||
export class DrawerManager {
|
||||
constructor() {
|
||||
this.drawerStack = []; // Array of { componentId, drawerElement, overlayElement, zIndex, closeOnEscape, closeOnOverlay }
|
||||
this.baseZIndex = 1040;
|
||||
this.zIndexIncrement = 10;
|
||||
this.escapeHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open drawer and add to stack
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {Object} options - Drawer options
|
||||
* @returns {Object} Drawer instance
|
||||
*/
|
||||
open(componentId, options = {}) {
|
||||
const {
|
||||
title = '',
|
||||
content = '',
|
||||
position = 'left',
|
||||
width = '400px',
|
||||
showOverlay = true,
|
||||
closeOnOverlay = true,
|
||||
closeOnEscape = true,
|
||||
animation = 'slide'
|
||||
} = options;
|
||||
|
||||
// Calculate z-index
|
||||
const zIndex = this.baseZIndex + (this.drawerStack.length * this.zIndexIncrement);
|
||||
|
||||
// Create overlay if needed
|
||||
let overlay = null;
|
||||
if (showOverlay) {
|
||||
overlay = this.createOverlay(zIndex, closeOnOverlay, componentId);
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
// Create drawer element
|
||||
const drawer = this.createDrawerElement(componentId, title, content, position, width, animation, zIndex + 1);
|
||||
document.body.appendChild(drawer);
|
||||
|
||||
// Track in stack
|
||||
const stackItem = {
|
||||
componentId,
|
||||
drawerElement: drawer,
|
||||
overlayElement: overlay,
|
||||
zIndex: zIndex + 1,
|
||||
closeOnEscape,
|
||||
closeOnOverlay
|
||||
};
|
||||
this.drawerStack.push(stackItem);
|
||||
|
||||
// Setup event handlers
|
||||
this.setupDrawerHandlers(drawer, overlay, componentId, closeOnOverlay, closeOnEscape);
|
||||
|
||||
// Update ESC handler
|
||||
this.updateEscapeHandler();
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
if (overlay) {
|
||||
overlay.classList.add('drawer-overlay--show');
|
||||
}
|
||||
drawer.classList.add('drawer--show');
|
||||
});
|
||||
|
||||
// Focus management
|
||||
this.focusDrawer(drawer);
|
||||
|
||||
return {
|
||||
drawer: drawer,
|
||||
overlay: overlay,
|
||||
close: () => this.close(componentId),
|
||||
isOpen: () => drawer.classList.contains('drawer--show')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Close drawer and remove from stack
|
||||
* @param {string} componentId - Component ID
|
||||
*/
|
||||
close(componentId) {
|
||||
const index = this.drawerStack.findIndex(item => item.componentId === componentId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { drawerElement, overlayElement } = this.drawerStack[index];
|
||||
|
||||
// Animate out
|
||||
drawerElement.classList.remove('drawer--show');
|
||||
if (overlayElement) {
|
||||
overlayElement.classList.remove('drawer-overlay--show');
|
||||
}
|
||||
|
||||
// Remove from DOM after animation
|
||||
setTimeout(() => {
|
||||
if (drawerElement.parentNode) {
|
||||
drawerElement.parentNode.removeChild(drawerElement);
|
||||
}
|
||||
if (overlayElement && overlayElement.parentNode) {
|
||||
overlayElement.parentNode.removeChild(overlayElement);
|
||||
}
|
||||
}, 300); // Match CSS transition duration
|
||||
|
||||
// Remove from stack
|
||||
this.drawerStack.splice(index, 1);
|
||||
|
||||
// Update ESC handler
|
||||
this.updateEscapeHandler();
|
||||
|
||||
// Focus previous drawer or return focus to body
|
||||
if (this.drawerStack.length > 0) {
|
||||
const previousDrawer = this.drawerStack[this.drawerStack.length - 1];
|
||||
this.focusDrawer(previousDrawer.drawerElement);
|
||||
} else {
|
||||
// Return focus to previously focused element
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && activeElement !== document.body) {
|
||||
activeElement.blur();
|
||||
}
|
||||
document.body.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all drawers
|
||||
*/
|
||||
closeAll() {
|
||||
while (this.drawerStack.length > 0) {
|
||||
const { componentId } = this.drawerStack[this.drawerStack.length - 1];
|
||||
this.close(componentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get topmost drawer
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
getTopDrawer() {
|
||||
if (this.drawerStack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.drawerStack[this.drawerStack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if drawer is open
|
||||
* @param {string} componentId - Component ID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isOpen(componentId) {
|
||||
return this.drawerStack.some(item => item.componentId === componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create overlay element
|
||||
* @param {number} zIndex - Z-index value
|
||||
* @param {boolean} closeOnOverlay - Whether to close on overlay click
|
||||
* @param {string} componentId - Component ID
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
createOverlay(zIndex, closeOnOverlay, componentId) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'drawer-overlay';
|
||||
overlay.style.zIndex = zIndex.toString();
|
||||
overlay.setAttribute('role', 'presentation');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (closeOnOverlay) {
|
||||
overlay.addEventListener('click', () => {
|
||||
this.close(componentId);
|
||||
});
|
||||
}
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create drawer element
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {string} title - Drawer title
|
||||
* @param {string} content - Drawer content
|
||||
* @param {string} position - Position (left/right)
|
||||
* @param {string} width - Drawer width
|
||||
* @param {string} animation - Animation type
|
||||
* @param {number} zIndex - Z-index value
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
createDrawerElement(componentId, title, content, position, width, animation, zIndex) {
|
||||
const drawer = document.createElement('div');
|
||||
drawer.className = `drawer drawer--${position} drawer-animation-${animation}`;
|
||||
drawer.id = `drawer-${componentId}`;
|
||||
drawer.style.zIndex = zIndex.toString();
|
||||
drawer.style.width = width;
|
||||
drawer.setAttribute('role', 'dialog');
|
||||
drawer.setAttribute('aria-modal', 'true');
|
||||
drawer.setAttribute('aria-labelledby', title ? `drawer-title-${componentId}` : '');
|
||||
|
||||
drawer.innerHTML = `
|
||||
<div class="drawer-content">
|
||||
${title ? `
|
||||
<div class="drawer-header">
|
||||
<h3 class="drawer-title" id="drawer-title-${componentId}">${title}</h3>
|
||||
<button class="drawer-close" aria-label="Close drawer">×</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="drawer-body">${content}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return drawer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers for drawer
|
||||
* @param {HTMLElement} drawer - Drawer element
|
||||
* @param {HTMLElement|null} overlay - Overlay element
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {boolean} closeOnOverlay - Whether to close on overlay click
|
||||
* @param {boolean} closeOnEscape - Whether to close on ESC key
|
||||
*/
|
||||
setupDrawerHandlers(drawer, overlay, componentId, closeOnOverlay, closeOnEscape) {
|
||||
// Close button
|
||||
const closeBtn = drawer.querySelector('.drawer-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
this.close(componentId);
|
||||
});
|
||||
}
|
||||
|
||||
// Overlay click (already handled in createOverlay, but ensure it's set)
|
||||
if (overlay && closeOnOverlay) {
|
||||
// Already set in createOverlay
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ESC key handler
|
||||
*/
|
||||
updateEscapeHandler() {
|
||||
// Remove existing handler
|
||||
if (this.escapeHandler) {
|
||||
document.removeEventListener('keydown', this.escapeHandler);
|
||||
this.escapeHandler = null;
|
||||
}
|
||||
|
||||
// Add handler if there are drawers
|
||||
if (this.drawerStack.length > 0) {
|
||||
const topDrawer = this.getTopDrawer();
|
||||
if (topDrawer && topDrawer.closeOnEscape) {
|
||||
this.escapeHandler = (e) => {
|
||||
if (e.key === 'Escape' && !e.defaultPrevented) {
|
||||
e.preventDefault();
|
||||
this.close(topDrawer.componentId);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this.escapeHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus drawer element
|
||||
* @param {HTMLElement} drawer - Drawer element
|
||||
*/
|
||||
focusDrawer(drawer) {
|
||||
if (!drawer) {
|
||||
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 = drawer.querySelector(focusableSelectors);
|
||||
if (focusableElement) {
|
||||
requestAnimationFrame(() => {
|
||||
focusableElement.focus();
|
||||
});
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
drawer.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all drawers
|
||||
*/
|
||||
destroy() {
|
||||
this.closeAll();
|
||||
if (this.escapeHandler) {
|
||||
document.removeEventListener('keydown', this.escapeHandler);
|
||||
this.escapeHandler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user