stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
224 lines • 9.19 kB
JavaScript
export class NotificationManager {
constructor(options = {}) {
this.container = null;
this.notifications = new Map();
this.options = {
position: options.position ?? "top-right",
maxNotifications: options.maxNotifications ?? 5,
cssClass: options.cssClass ?? "ss-notification",
animationDuration: options.animationDuration ?? 300,
defaultDuration: options.defaultDuration ?? 4000,
newestOnTop: options.newestOnTop ?? true,
pauseOnHover: options.pauseOnHover ?? true,
};
this.createContainer();
if (!NotificationManager.instance) {
NotificationManager.instance = this;
}
}
success(message, options) {
return this.show({ ...options, message, type: "success" });
}
error(message, options) {
return this.show({
...options,
message,
type: "error",
duration: options?.duration ?? 0,
});
}
warning(message, options) {
return this.show({ ...options, message, type: "warning" });
}
info(message, options) {
return this.show({ ...options, message, type: "info" });
}
show(options) {
if (this.notifications.size >= this.options.maxNotifications) {
const oldest = this.options.newestOnTop
? Array.from(this.notifications.keys()).pop()
: Array.from(this.notifications.keys()).shift();
if (oldest)
this.dismiss(oldest);
}
const id = `notification-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const element = this.createNotification(id, options);
const duration = options.duration ?? this.options.defaultDuration;
const instance = {
id,
element,
options,
timeout: null,
startTime: Date.now(),
remainingTime: duration,
};
this.notifications.set(id, instance);
if (this.options.newestOnTop) {
this.container?.prepend(element);
}
else {
this.container?.appendChild(element);
}
requestAnimationFrame(() => {
element.classList.add(`${this.options.cssClass}--visible`);
});
if (duration > 0) {
instance.timeout = setTimeout(() => this.dismiss(id), duration);
}
return id;
}
dismiss(id) {
const instance = this.notifications.get(id);
if (!instance)
return;
if (instance.timeout) {
clearTimeout(instance.timeout);
}
instance.element.classList.remove(`${this.options.cssClass}--visible`);
instance.element.classList.add(`${this.options.cssClass}--removing`);
setTimeout(() => {
instance.element.remove();
this.notifications.delete(id);
instance.options.onClose?.();
}, this.options.animationDuration);
}
dismissAll() {
this.notifications.forEach((_, id) => this.dismiss(id));
}
get count() {
return this.notifications.size;
}
destroy() {
this.dismissAll();
this.container?.remove();
this.container = null;
if (NotificationManager.instance === this) {
NotificationManager.instance = null;
}
}
static getInstance() {
if (!NotificationManager.instance) {
NotificationManager.instance = new NotificationManager();
}
return NotificationManager.instance;
}
static init() {
const container = document.querySelector('[data-ss="notification-container"]');
return new NotificationManager({
position: container?.dataset
.ssNotificationPosition,
maxNotifications: container?.dataset.ssNotificationMax
? parseInt(container.dataset.ssNotificationMax, 10)
: undefined,
});
}
createContainer() {
this.container = document.querySelector(`[data-ss="notification-container"], .${this.options.cssClass}-container`);
if (!this.container) {
this.container = document.createElement("div");
this.container.className = `${this.options.cssClass}-container`;
document.body.appendChild(this.container);
}
this.container.classList.add(`${this.options.cssClass}-container--${this.options.position}`);
this.container.setAttribute("role", "region");
this.container.setAttribute("aria-label", "Notifications");
this.container.setAttribute("aria-live", "polite");
}
createNotification(id, options) {
const el = document.createElement("div");
el.className = `${this.options.cssClass} ${this.options.cssClass}--${options.type || "info"}`;
if (options.className) {
el.classList.add(options.className);
}
el.setAttribute("role", "alert");
el.setAttribute("aria-live", "assertive");
el.setAttribute("data-notification-id", id);
const iconHtml = options.icon
? `<span class="${this.options.cssClass}__icon">${options.icon}</span>`
: this.getDefaultIcon(options.type || "info");
const titleHtml = options.title
? `<div class="${this.options.cssClass}__title">${options.title}</div>`
: "";
const actionHtml = options.actionText
? `<button type="button" class="${this.options.cssClass}__action">${options.actionText}</button>`
: "";
const closeHtml = options.closable !== false
? `<button type="button" class="${this.options.cssClass}__close" aria-label="Close">×</button>`
: "";
const progressHtml = options.showProgress &&
(options.duration ?? this.options.defaultDuration) > 0
? `<div class="${this.options.cssClass}__progress"><div class="${this.options.cssClass}__progress-bar"></div></div>`
: "";
el.innerHTML = `
${iconHtml}
<div class="${this.options.cssClass}__content">
${titleHtml}
<div class="${this.options.cssClass}__message">${options.message}</div>
${actionHtml}
</div>
${closeHtml}
${progressHtml}
`;
const closeBtn = el.querySelector(`.${this.options.cssClass}__close`);
closeBtn?.addEventListener("click", () => this.dismiss(id));
const actionBtn = el.querySelector(`.${this.options.cssClass}__action`);
actionBtn?.addEventListener("click", () => {
options.onAction?.();
this.dismiss(id);
});
if (this.options.pauseOnHover) {
el.addEventListener("mouseenter", () => this.pauseTimeout(id));
el.addEventListener("mouseleave", () => this.resumeTimeout(id));
}
if (options.showProgress) {
const duration = options.duration ?? this.options.defaultDuration;
const progressBar = el.querySelector(`.${this.options.cssClass}__progress-bar`);
if (progressBar && duration > 0) {
progressBar.style.transition = `width ${duration}ms linear`;
requestAnimationFrame(() => {
progressBar.style.width = "0%";
});
}
}
return el;
}
getDefaultIcon(type) {
const icons = {
success: `<span class="${this.options.cssClass}__icon">✓</span>`,
error: `<span class="${this.options.cssClass}__icon">✕</span>`,
warning: `<span class="${this.options.cssClass}__icon">⚠</span>`,
info: `<span class="${this.options.cssClass}__icon">ℹ</span>`,
};
return icons[type];
}
pauseTimeout(id) {
const instance = this.notifications.get(id);
if (!instance || !instance.timeout)
return;
clearTimeout(instance.timeout);
instance.remainingTime -= Date.now() - instance.startTime;
const progressBar = instance.element.querySelector(`.${this.options.cssClass}__progress-bar`);
if (progressBar) {
const computed = getComputedStyle(progressBar);
progressBar.style.width = computed.width;
progressBar.style.transition = "none";
}
}
resumeTimeout(id) {
const instance = this.notifications.get(id);
if (!instance || instance.remainingTime <= 0)
return;
instance.startTime = Date.now();
instance.timeout = setTimeout(() => this.dismiss(id), instance.remainingTime);
const progressBar = instance.element.querySelector(`.${this.options.cssClass}__progress-bar`);
if (progressBar) {
progressBar.style.transition = `width ${instance.remainingTime}ms linear`;
requestAnimationFrame(() => {
progressBar.style.width = "0%";
});
}
}
}
NotificationManager.instance = null;
export default NotificationManager;
//# sourceMappingURL=NotificationManager.js.map