181 lines
5.0 KiB
JavaScript
181 lines
5.0 KiB
JavaScript
const popupData = {
|
|
// Error modal (blocking, full-screen) - for 500/503 errors
|
|
errorModal: {
|
|
show: false,
|
|
code: 0,
|
|
title: "",
|
|
message: "",
|
|
details: "",
|
|
},
|
|
|
|
// Toast notifications (non-blocking, stacked) - for success/warning/info
|
|
toasts: [],
|
|
toastIdCounter: 0,
|
|
|
|
// Handle HTMX beforeSwap event - intercept both errors and successes
|
|
handleBeforeSwap(event) {
|
|
const xhr = event.detail.xhr;
|
|
if (!xhr) return;
|
|
|
|
const status = xhr.status;
|
|
const trigger = xhr.getResponseHeader("HX-Trigger");
|
|
if (!trigger) return;
|
|
|
|
try {
|
|
const data = JSON.parse(trigger);
|
|
|
|
// Handle 500/503 error modals
|
|
if ((status === 500 || status === 503) && data.showErrorModal) {
|
|
this.errorModal = {
|
|
show: true,
|
|
code: data.showErrorModal.code,
|
|
title: data.showErrorModal.title,
|
|
message: data.showErrorModal.message,
|
|
details: data.showErrorModal.details,
|
|
};
|
|
// Prevent swap but allow error logging
|
|
event.detail.shouldSwap = false;
|
|
return;
|
|
}
|
|
|
|
// Handle success/warning/info toasts (200-299)
|
|
if (status >= 200 && status < 300 && data.showNotification) {
|
|
this.addToast(data.showNotification);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to parse HX-Trigger:", e);
|
|
}
|
|
},
|
|
|
|
// Add toast to stack
|
|
addToast(notification) {
|
|
const toast = {
|
|
id: ++this.toastIdCounter,
|
|
type: notification.type,
|
|
title: notification.title,
|
|
message: notification.message,
|
|
paused: false,
|
|
progress: 0,
|
|
};
|
|
|
|
this.toasts.push(toast);
|
|
|
|
// Determine timeout based on type
|
|
const timeout = notification.type === "warning" ? 5000 : 3000;
|
|
|
|
// Start progress animation
|
|
this.animateToastProgress(toast.id, timeout);
|
|
},
|
|
|
|
// Animate toast progress bar and auto-dismiss
|
|
animateToastProgress(toastId, duration) {
|
|
const toast = this.toasts.find((t) => t.id === toastId);
|
|
if (!toast) return;
|
|
|
|
const startTime = performance.now();
|
|
let totalPausedTime = 0;
|
|
let pauseStartTime = null;
|
|
|
|
const animate = (currentTime) => {
|
|
const toast = this.toasts.find((t) => t.id === toastId);
|
|
if (!toast) return; // Toast was manually removed
|
|
|
|
if (toast.paused) {
|
|
// Track when pause started
|
|
if (pauseStartTime === null) {
|
|
pauseStartTime = currentTime;
|
|
}
|
|
// Keep animating while paused
|
|
requestAnimationFrame(animate);
|
|
return;
|
|
} else {
|
|
// If we were paused, accumulate the paused time
|
|
if (pauseStartTime !== null) {
|
|
totalPausedTime += currentTime - pauseStartTime;
|
|
pauseStartTime = null;
|
|
}
|
|
}
|
|
|
|
// Calculate actual elapsed time (excluding paused time)
|
|
const elapsed = currentTime - startTime - totalPausedTime;
|
|
toast.progress = Math.min((elapsed / duration) * 100, 100);
|
|
|
|
if (elapsed >= duration) {
|
|
this.removeToast(toastId);
|
|
} else {
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
},
|
|
|
|
// Remove toast from stack
|
|
removeToast(id) {
|
|
this.toasts = this.toasts.filter((t) => t.id !== id);
|
|
},
|
|
|
|
// Close error modal
|
|
closeErrorModal() {
|
|
this.errorModal.show = false;
|
|
},
|
|
};
|
|
|
|
// Global function for WebSocket toasts to animate progress and auto-dismiss
|
|
// Pure vanilla JavaScript - no Alpine.js dependency
|
|
// Called via inline script in ToastNotificationWS template
|
|
function animateToastProgress(toastId, duration) {
|
|
const toastEl = document.getElementById(toastId);
|
|
const progressEl = document.getElementById(toastId + "-progress");
|
|
|
|
if (!toastEl || !progressEl) {
|
|
console.error("animateToastProgress: elements not found", toastId);
|
|
return;
|
|
}
|
|
|
|
const startTime = performance.now();
|
|
let totalPausedTime = 0;
|
|
let pauseStartTime = null;
|
|
|
|
const animate = (currentTime) => {
|
|
const el = document.getElementById(toastId);
|
|
const progEl = document.getElementById(toastId + "-progress");
|
|
|
|
if (!el || !progEl) return; // Toast was manually removed
|
|
|
|
// Check if paused (set via onmouseenter/onmouseleave)
|
|
const isPaused = el.dataset.paused === "true";
|
|
|
|
if (isPaused) {
|
|
// Track when pause started
|
|
if (pauseStartTime === null) {
|
|
pauseStartTime = currentTime;
|
|
}
|
|
// Keep animating while paused
|
|
requestAnimationFrame(animate);
|
|
return;
|
|
} else {
|
|
// If we were paused, accumulate the paused time
|
|
if (pauseStartTime !== null) {
|
|
totalPausedTime += currentTime - pauseStartTime;
|
|
pauseStartTime = null;
|
|
}
|
|
}
|
|
|
|
// Calculate actual elapsed time (excluding paused time)
|
|
const elapsed = currentTime - startTime - totalPausedTime;
|
|
// Progress bar goes from 100% to 0%
|
|
const progress = Math.max(100 - (elapsed / duration) * 100, 0);
|
|
progEl.style.width = progress + "%";
|
|
|
|
if (elapsed >= duration) {
|
|
// Remove the toast element
|
|
el.remove();
|
|
} else {
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|