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:
288
resources/js/modules/livecomponent/ToastQueue.js
Normal file
288
resources/js/modules/livecomponent/ToastQueue.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Toast Queue Manager
|
||||
*
|
||||
* Manages multiple toast notifications with:
|
||||
* - Queue system for multiple toasts
|
||||
* - Position management (stacking toasts)
|
||||
* - Auto-dismiss with queue management
|
||||
* - Maximum number of simultaneous toasts
|
||||
*/
|
||||
|
||||
export class ToastQueue {
|
||||
constructor(maxToasts = 5) {
|
||||
this.maxToasts = maxToasts;
|
||||
this.queue = [];
|
||||
this.activeToasts = new Map(); // componentId → toast element
|
||||
this.positionOffsets = new Map(); // position → current offset
|
||||
}
|
||||
|
||||
/**
|
||||
* Add toast to queue
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {Object} options - Toast options
|
||||
* @returns {HTMLElement|null} Toast element or null if queue full
|
||||
*/
|
||||
add(componentId, options = {}) {
|
||||
const {
|
||||
message = '',
|
||||
type = 'info',
|
||||
duration = 5000,
|
||||
position = 'top-right'
|
||||
} = options;
|
||||
|
||||
// Check if we already have a toast for this component
|
||||
if (this.activeToasts.has(componentId)) {
|
||||
// Replace existing toast
|
||||
this.remove(componentId);
|
||||
}
|
||||
|
||||
// Check if we've reached max toasts for this position
|
||||
const positionCount = this.getPositionCount(position);
|
||||
if (positionCount >= this.maxToasts) {
|
||||
// Remove oldest toast at this position
|
||||
this.removeOldestAtPosition(position);
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toast = this.createToastElement(message, type, position, componentId);
|
||||
|
||||
// Calculate offset for stacking
|
||||
const offset = this.getNextOffset(position);
|
||||
this.setToastPosition(toast, position, offset);
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateY(0)';
|
||||
});
|
||||
|
||||
// Store reference
|
||||
this.activeToasts.set(componentId, toast);
|
||||
this.updatePositionOffset(position, offset);
|
||||
|
||||
// Setup auto-dismiss
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
this.remove(componentId);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// Setup close button
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
this.remove(componentId);
|
||||
});
|
||||
}
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove toast
|
||||
* @param {string} componentId - Component ID
|
||||
*/
|
||||
remove(componentId) {
|
||||
const toast = this.activeToasts.get(componentId);
|
||||
if (!toast) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate out
|
||||
toast.style.opacity = '0';
|
||||
const position = this.getToastPosition(toast);
|
||||
toast.style.transform = `translateY(${position.includes('top') ? '-20px' : '20px'})`;
|
||||
|
||||
setTimeout(() => {
|
||||
if (toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
this.activeToasts.delete(componentId);
|
||||
this.recalculateOffsets(position);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all toasts
|
||||
*/
|
||||
clear() {
|
||||
for (const componentId of this.activeToasts.keys()) {
|
||||
this.remove(componentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove oldest toast at position
|
||||
* @param {string} position - Position
|
||||
*/
|
||||
removeOldestAtPosition(position) {
|
||||
let oldestToast = null;
|
||||
let oldestComponentId = null;
|
||||
let oldestTimestamp = Infinity;
|
||||
|
||||
for (const [componentId, toast] of this.activeToasts.entries()) {
|
||||
if (this.getToastPosition(toast) === position) {
|
||||
const timestamp = parseInt(toast.dataset.timestamp || '0');
|
||||
if (timestamp < oldestTimestamp) {
|
||||
oldestTimestamp = timestamp;
|
||||
oldestToast = toast;
|
||||
oldestComponentId = componentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestComponentId) {
|
||||
this.remove(oldestComponentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of toasts at position
|
||||
* @param {string} position - Position
|
||||
* @returns {number}
|
||||
*/
|
||||
getPositionCount(position) {
|
||||
let count = 0;
|
||||
for (const toast of this.activeToasts.values()) {
|
||||
if (this.getToastPosition(toast) === position) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next offset for position
|
||||
* @param {string} position - Position
|
||||
* @returns {number}
|
||||
*/
|
||||
getNextOffset(position) {
|
||||
const currentOffset = this.positionOffsets.get(position) || 0;
|
||||
return currentOffset + 80; // 80px spacing between toasts
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position offset
|
||||
* @param {string} position - Position
|
||||
* @param {number} offset - Offset value
|
||||
*/
|
||||
updatePositionOffset(position, offset) {
|
||||
this.positionOffsets.set(position, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate offsets for position after removal
|
||||
* @param {string} position - Position
|
||||
*/
|
||||
recalculateOffsets(position) {
|
||||
const toastsAtPosition = [];
|
||||
for (const [componentId, toast] of this.activeToasts.entries()) {
|
||||
if (this.getToastPosition(toast) === position) {
|
||||
toastsAtPosition.push({ componentId, toast });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by DOM order
|
||||
toastsAtPosition.sort((a, b) => {
|
||||
const positionA = a.toast.compareDocumentPosition(b.toast);
|
||||
return positionA & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
||||
});
|
||||
|
||||
// Recalculate offsets
|
||||
let offset = 0;
|
||||
for (const { toast } of toastsAtPosition) {
|
||||
this.setToastPosition(toast, position, offset);
|
||||
offset += 80;
|
||||
}
|
||||
|
||||
this.positionOffsets.set(position, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create toast element
|
||||
* @param {string} message - Toast message
|
||||
* @param {string} type - Toast type
|
||||
* @param {string} position - Position
|
||||
* @param {string} componentId - Component ID
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
createToastElement(message, type, position, componentId) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast-queue-toast toast-queue-toast--${type} toast-queue-toast--${position}`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'polite');
|
||||
toast.dataset.componentId = componentId;
|
||||
toast.dataset.timestamp = Date.now().toString();
|
||||
|
||||
const colors = {
|
||||
info: '#3b82f6',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-content">
|
||||
<span class="toast-message">${message}</span>
|
||||
<button class="toast-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
${position.includes('top') ? 'top' : 'bottom'}: 1rem;
|
||||
${position.includes('left') ? 'left' : 'right'}: 1rem;
|
||||
max-width: 400px;
|
||||
padding: 1rem;
|
||||
background: ${colors[type] || colors.info};
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transform: translateY(${position.includes('top') ? '-20px' : '20px'});
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
`;
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set toast position with offset
|
||||
* @param {HTMLElement} toast - Toast element
|
||||
* @param {string} position - Position
|
||||
* @param {number} offset - Offset in pixels
|
||||
*/
|
||||
setToastPosition(toast, position, offset) {
|
||||
if (position.includes('top')) {
|
||||
toast.style.top = `${1 + offset / 16}rem`; // Convert px to rem (assuming 16px base)
|
||||
} else {
|
||||
toast.style.bottom = `${1 + offset / 16}rem`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get toast position from element
|
||||
* @param {HTMLElement} toast - Toast element
|
||||
* @returns {string}
|
||||
*/
|
||||
getToastPosition(toast) {
|
||||
if (toast.classList.contains('toast-queue-toast--top-right')) {
|
||||
return 'top-right';
|
||||
}
|
||||
if (toast.classList.contains('toast-queue-toast--top-left')) {
|
||||
return 'top-left';
|
||||
}
|
||||
if (toast.classList.contains('toast-queue-toast--bottom-right')) {
|
||||
return 'bottom-right';
|
||||
}
|
||||
if (toast.classList.contains('toast-queue-toast--bottom-left')) {
|
||||
return 'bottom-left';
|
||||
}
|
||||
return 'top-right'; // default
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user