basecoat-css
Version:
Tailwind CSS for Basecoat components
181 lines (155 loc) • 6.66 kB
JavaScript
(() => {
let toaster;
const toasts = new WeakMap();
let isPaused = false;
const ICONS = {
success: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
error: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
info: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
warning: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
};
function initToaster(toasterElement) {
if (toasterElement.dataset.toasterInitialized) return;
toaster = toasterElement;
toaster.addEventListener('mouseenter', pauseAllTimeouts);
toaster.addEventListener('mouseleave', resumeAllTimeouts);
toaster.addEventListener('click', (event) => {
const actionLink = event.target.closest('.toast footer a');
const actionButton = event.target.closest('.toast footer button');
if (actionLink || actionButton) {
closeToast(event.target.closest('.toast'));
}
});
toaster.querySelectorAll('.toast:not([data-toast-initialized])').forEach(initToast);
toaster.dataset.toasterInitialized = 'true';
toaster.dispatchEvent(new CustomEvent('basecoat:initialized'));
}
function initToast(element) {
if (element.dataset.toastInitialized) return;
const duration = parseInt(element.dataset.duration);
const timeoutDuration = duration !== -1
? duration || (element.dataset.category === 'error' ? 5000 : 3000)
: -1;
const state = {
remainingTime: timeoutDuration,
timeoutId: null,
startTime: null,
};
if (timeoutDuration !== -1) {
if (isPaused) {
state.timeoutId = null;
} else {
state.startTime = Date.now();
state.timeoutId = setTimeout(() => closeToast(element), timeoutDuration);
}
}
toasts.set(element, state);
element.dataset.toastInitialized = 'true';
}
function pauseAllTimeouts() {
if (isPaused) return;
isPaused = true;
toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
if (!toasts.has(element)) return;
const state = toasts.get(element);
if (state.timeoutId) {
clearTimeout(state.timeoutId);
state.timeoutId = null;
state.remainingTime -= Date.now() - state.startTime;
}
});
}
function resumeAllTimeouts() {
if (!isPaused) return;
isPaused = false;
toaster.querySelectorAll('.toast:not([aria-hidden="true"])').forEach(element => {
if (!toasts.has(element)) return;
const state = toasts.get(element);
if (state.remainingTime !== -1 && !state.timeoutId) {
if (state.remainingTime > 0) {
state.startTime = Date.now();
state.timeoutId = setTimeout(() => closeToast(element), state.remainingTime);
} else {
closeToast(element);
}
}
});
}
function closeToast(element) {
if (!toasts.has(element)) return;
const state = toasts.get(element);
clearTimeout(state.timeoutId);
toasts.delete(element);
if (element.contains(document.activeElement)) document.activeElement.blur();
element.setAttribute('aria-hidden', 'true');
element.addEventListener('transitionend', () => element.remove(), { once: true });
}
function executeAction(button, toast) {
const actionString = button.dataset.toastAction;
if (!actionString) return;
try {
const func = new Function('close', actionString);
func(() => closeToast(toast));
} catch (event) {
console.error('Error executing toast action:', event);
}
}
function createToast(config) {
const {
category = 'info',
title,
description,
action,
cancel,
duration,
icon,
} = config;
const iconHtml = icon || (category && ICONS[category]) || '';
const titleHtml = title ? `<h2>${title}</h2>` : '';
const descriptionHtml = description ? `<p>${description}</p>` : '';
const actionHtml = action?.href
? `<a href="${action.href}" class="btn" data-toast-action>${action.label}</a>`
: action?.onclick
? `<button type="button" class="btn" data-toast-action onclick="${action.onclick}">${action.label}</button>`
: '';
const cancelHtml = cancel
? `<button type="button" class="btn-outline h-6 text-xs px-2.5 rounded-sm" data-toast-cancel onclick="${cancel?.onclick}">${cancel.label}</button>`
: '';
const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
const html = `
<div
class="toast"
role="${category === 'error' ? 'alert' : 'status'}"
aria-atomic="true"
${category ? `data-category="${category}"` : ''}
${duration !== undefined ? `data-duration="${duration}"` : ''}
>
<div class="toast-content">
${iconHtml}
<section>
${titleHtml}
${descriptionHtml}
</section>
${footerHtml}
</div>
</div>
</div>
`;
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
document.addEventListener('basecoat:toast', (event) => {
if (!toaster) {
console.error('Cannot create toast: toaster container not found on page.');
return;
}
const config = event.detail?.config || {};
const toastElement = createToast(config);
toaster.appendChild(toastElement);
});
if (window.basecoat) {
window.basecoat.register('toaster', '#toaster:not([data-toaster-initialized])', initToaster);
window.basecoat.register('toast', '.toast:not([data-toast-initialized])', initToast);
}
})();