225 lines
5.4 KiB
JavaScript
225 lines
5.4 KiB
JavaScript
/**
|
||
* Notification System
|
||
* Accessible toast notifications
|
||
*/
|
||
|
||
(function () {
|
||
"use strict";
|
||
|
||
class NotificationManager {
|
||
constructor() {
|
||
this.container = null;
|
||
this.notifications = new Map();
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", () =>
|
||
this.createContainer()
|
||
);
|
||
} else {
|
||
this.createContainer();
|
||
}
|
||
}
|
||
|
||
createContainer() {
|
||
if (!document.body || this.container) return;
|
||
|
||
this.container = document.createElement("div");
|
||
this.container.id = "notification-container";
|
||
this.container.setAttribute("aria-live", "polite");
|
||
this.container.setAttribute("aria-atomic", "true");
|
||
this.container.className = "notification-container";
|
||
|
||
const style = document.createElement("style");
|
||
style.textContent = `
|
||
.notification-container {
|
||
position: fixed;
|
||
top: 80px;
|
||
right: 20px;
|
||
z-index: 10000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
max-width: 400px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.notification {
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
pointer-events: auto;
|
||
animation: slideInRight 0.3s ease;
|
||
min-width: 250px;
|
||
}
|
||
|
||
.notification.removing {
|
||
animation: slideOutRight 0.3s ease;
|
||
}
|
||
|
||
.notification-success {
|
||
background: #10b981;
|
||
}
|
||
|
||
.notification-error {
|
||
background: #ef4444;
|
||
}
|
||
|
||
.notification-info {
|
||
background: #3b82f6;
|
||
}
|
||
|
||
.notification-warning {
|
||
background: #f59e0b;
|
||
}
|
||
|
||
.notification-icon {
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.notification-message {
|
||
flex: 1;
|
||
}
|
||
|
||
.notification-close {
|
||
background: transparent;
|
||
border: none;
|
||
color: white;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
opacity: 0.8;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.notification-close:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
@keyframes slideInRight {
|
||
from {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slideOutRight {
|
||
from {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
to {
|
||
transform: translateX(400px);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.notification-container {
|
||
right: 10px;
|
||
left: 10px;
|
||
max-width: none;
|
||
}
|
||
|
||
.notification {
|
||
min-width: auto;
|
||
}
|
||
}
|
||
`;
|
||
|
||
document.head.appendChild(style);
|
||
document.body.appendChild(this.container);
|
||
}
|
||
|
||
show(message, type = "info", duration = 3000) {
|
||
if (!this.container) this.createContainer();
|
||
if (!this.container) return;
|
||
|
||
const id = Date.now() + Math.random();
|
||
const notification = document.createElement("div");
|
||
notification.className = `notification notification-${type}`;
|
||
notification.setAttribute("role", "alert");
|
||
|
||
const icons = {
|
||
success: "✓",
|
||
error: "✕",
|
||
info: "ℹ",
|
||
warning: "⚠",
|
||
};
|
||
|
||
notification.innerHTML = `
|
||
<span class="notification-icon">${icons[type] || icons.info}</span>
|
||
<span class="notification-message">${this.escapeHtml(message)}</span>
|
||
<button class="notification-close" aria-label="Close notification">×</button>
|
||
`;
|
||
|
||
const closeBtn = notification.querySelector(".notification-close");
|
||
closeBtn.addEventListener("click", () => this.remove(id));
|
||
|
||
this.container.appendChild(notification);
|
||
this.notifications.set(id, notification);
|
||
|
||
if (duration > 0) {
|
||
setTimeout(() => this.remove(id), duration);
|
||
}
|
||
|
||
return id;
|
||
}
|
||
|
||
remove(id) {
|
||
const notification = this.notifications.get(id);
|
||
if (!notification) return;
|
||
|
||
notification.classList.add("removing");
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
notification.parentNode.removeChild(notification);
|
||
}
|
||
this.notifications.delete(id);
|
||
}, 300);
|
||
}
|
||
|
||
escapeHtml(text) {
|
||
const div = document.createElement("div");
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
success(message, duration) {
|
||
return this.show(message, "success", duration);
|
||
}
|
||
|
||
error(message, duration) {
|
||
return this.show(message, "error", duration);
|
||
}
|
||
|
||
info(message, duration) {
|
||
return this.show(message, "info", duration);
|
||
}
|
||
|
||
warning(message, duration) {
|
||
return this.show(message, "warning", duration);
|
||
}
|
||
}
|
||
|
||
// Create global instance
|
||
window.Notifications = window.Notifications || new NotificationManager();
|
||
|
||
// Legacy compatibility
|
||
window.showNotification = function (message, type = "info") {
|
||
window.Notifications.show(message, type);
|
||
};
|
||
})();
|