basecoat-css
Version:
Tailwind CSS for Basecoat components
893 lines (751 loc) • 29.1 kB
JavaScript
(() => {
const initDropdownMenu = (dropdownMenuComponent) => {
const trigger = dropdownMenuComponent.querySelector(':scope > button');
const popover = dropdownMenuComponent.querySelector(':scope > [data-popover]');
const menu = popover.querySelector('[role="menu"]');
if (!trigger || !menu || !popover) {
const missing = [];
if (!trigger) missing.push('trigger');
if (!menu) missing.push('menu');
if (!popover) missing.push('popover');
console.error(`Dropdown menu initialisation failed. Missing element(s): ${missing.join(', ')}`, dropdownMenuComponent);
return;
}
let menuItems = [];
let activeIndex = -1;
const closePopover = (focusOnTrigger = true) => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('aria-activedescendant');
popover.setAttribute('aria-hidden', 'true');
if (focusOnTrigger) {
trigger.focus();
}
setActiveItem(-1);
};
const openPopover = () => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: dropdownMenuComponent }
}));
trigger.setAttribute('aria-expanded', 'true');
popover.setAttribute('aria-hidden', 'false');
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]')).filter(item =>
!item.hasAttribute('disabled') &&
item.getAttribute('aria-disabled') !== 'true'
);
if (menuItems.length > 0) {
setActiveItem(0);
}
};
const setActiveItem = (index) => {
if (activeIndex > -1 && menuItems[activeIndex]) {
menuItems[activeIndex].classList.remove('active');
}
activeIndex = index;
if (activeIndex > -1 && menuItems[activeIndex]) {
const activeItem = menuItems[activeIndex];
activeItem.classList.add('active');
trigger.setAttribute('aria-activedescendant', activeItem.id);
} else {
trigger.removeAttribute('aria-activedescendant');
}
};
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
openPopover();
}
});
dropdownMenuComponent.addEventListener('keydown', (event) => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (event.key === 'Escape') {
if (isExpanded) closePopover();
return;
}
if (!isExpanded) {
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
event.preventDefault();
openPopover();
}
return;
}
if (menuItems.length === 0) return;
let nextIndex = activeIndex;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
nextIndex = Math.min(activeIndex + 1, menuItems.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
nextIndex = Math.max(activeIndex - 1, 0);
break;
case 'Home':
event.preventDefault();
nextIndex = 0;
break;
case 'End':
event.preventDefault();
nextIndex = menuItems.length - 1;
break;
case 'Enter':
case ' ':
event.preventDefault();
menuItems[activeIndex]?.click();
closePopover();
return;
}
if (nextIndex !== activeIndex) {
setActiveItem(nextIndex);
}
});
menu.addEventListener('click', (event) => {
if (event.target.closest('[role^="menuitem"]')) {
closePopover();
}
});
document.addEventListener('click', (event) => {
if (!dropdownMenuComponent.contains(event.target)) {
closePopover();
}
});
document.addEventListener('basecoat:popover', (event) => {
if (event.detail.source !== dropdownMenuComponent) {
closePopover(false);
}
});
dropdownMenuComponent.dataset.dropdownMenuInitialized = true;
};
document.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
initDropdownMenu(node);
}
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
(() => {
const initPopover = (popoverComponent) => {
const trigger = popoverComponent.querySelector(':scope > button');
const content = popoverComponent.querySelector(':scope > [data-popover]');
if (!trigger || !content) {
const missing = [];
if (!trigger) missing.push('trigger');
if (!content) missing.push('content');
console.error(`Popover initialisation failed. Missing element(s): ${missing.join(', ')}`, popoverComponent);
return;
}
const closePopover = (focusOnTrigger = true) => {
if (trigger.getAttribute('aria-expanded') === 'false') return;
trigger.setAttribute('aria-expanded', 'false');
content.setAttribute('aria-hidden', 'true');
if (focusOnTrigger) {
trigger.focus();
}
};
const openPopover = () => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: popoverComponent }
}));
const elementToFocus = content.querySelector('[autofocus]');
if (elementToFocus) {
content.addEventListener('transitionend', () => {
elementToFocus.focus();
}, { once: true });
}
trigger.setAttribute('aria-expanded', 'true');
content.setAttribute('aria-hidden', 'false');
};
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
openPopover();
}
});
popoverComponent.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closePopover();
}
});
document.addEventListener('click', (event) => {
if (!popoverComponent.contains(event.target)) {
closePopover();
}
});
document.addEventListener('basecoat:popover', (event) => {
if (event.detail.source !== popoverComponent) {
closePopover(false);
}
});
popoverComponent.dataset.popoverInitialized = true;
};
document.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('.popover:not([data-popover-initialized])')) {
initPopover(node);
}
node.querySelectorAll('.popover:not([data-popover-initialized])').forEach(initPopover);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
(() => {
const initSelect = (selectComponent) => {
const trigger = selectComponent.querySelector(':scope > button');
const selectedValue = trigger.querySelector(':scope > span');
const popover = selectComponent.querySelector(':scope > [data-popover]');
const listbox = popover.querySelector('[role="listbox"]');
const input = selectComponent.querySelector(':scope > input[type="hidden"]');
const filter = selectComponent.querySelector('header input[type="text"]');
if (!trigger || !popover || !listbox || !input) {
const missing = [];
if (!trigger) missing.push('trigger');
if (!popover) missing.push('popover');
if (!listbox) missing.push('listbox');
if (!input) missing.push('input');
console.error(`Select component initialisation failed. Missing element(s): ${missing.join(', ')}`, selectComponent);
return;
}
const options = Array.from(listbox.querySelectorAll('[role="option"]'));
let visibleOptions = [...options];
let activeIndex = -1;
const hasTransition = () => {
const style = getComputedStyle(popover);
return parseFloat(style.transitionDuration) > 0 || parseFloat(style.transitionDelay) > 0;
};
const updateValue = (option) => {
if (option) {
selectedValue.innerHTML = option.dataset.label || option.innerHTML;
input.value = option.dataset.value;
listbox.querySelector('[role="option"][aria-selected="true"]')?.removeAttribute('aria-selected');
option.setAttribute('aria-selected', 'true');
}
};
const closePopover = (focusOnTrigger = true) => {
if (popover.getAttribute('aria-hidden') === 'true') return;
if (filter) {
const resetFilter = () => {
filter.value = '';
visibleOptions = [...options];
options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
};
if (hasTransition()) {
popover.addEventListener('transitionend', resetFilter, { once: true });
} else {
resetFilter();
}
}
if (focusOnTrigger) trigger.focus();
popover.setAttribute('aria-hidden', 'true');
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('aria-activedescendant');
if (activeIndex > -1) options[activeIndex]?.classList.remove('active');
activeIndex = -1;
}
const selectOption = (option) => {
if (!option) return;
if (option.dataset.value != null) {
updateValue(option);
}
closePopover();
const event = new CustomEvent('change', {
detail: { value: option.dataset.value },
bubbles: true
});
selectComponent.dispatchEvent(event);
};
if (filter) {
const filterOptions = () => {
const searchTerm = filter.value.trim().toLowerCase();
if (activeIndex > -1) {
options[activeIndex].classList.remove('active');
trigger.removeAttribute('aria-activedescendant');
activeIndex = -1;
}
visibleOptions = [];
options.forEach(option => {
const optionText = (option.dataset.label || option.textContent).trim().toLowerCase();
const matches = optionText.includes(searchTerm);
option.setAttribute('aria-hidden', String(!matches));
if (matches) {
visibleOptions.push(option);
}
});
};
filter.addEventListener('input', filterOptions);
}
let initialOption = options.find(opt => opt.dataset.value === input.value);
if (!initialOption && options.length > 0) initialOption = options[0];
updateValue(initialOption);
const handleKeyNavigation = (event) => {
const isPopoverOpen = popover.getAttribute('aria-hidden') === 'false';
if (!['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End', 'Escape'].includes(event.key)) {
return;
}
if (!isPopoverOpen) {
if (event.key !== 'Enter' && event.key !== 'Escape') {
event.preventDefault();
trigger.click();
}
return;
}
event.preventDefault();
if (event.key === 'Escape') {
closePopover();
return;
}
if (event.key === 'Enter') {
if (activeIndex > -1) {
selectOption(options[activeIndex]);
}
return;
}
if (visibleOptions.length === 0) return;
const currentVisibleIndex = activeIndex > -1 ? visibleOptions.indexOf(options[activeIndex]) : -1;
let nextVisibleIndex = currentVisibleIndex;
switch (event.key) {
case 'ArrowDown':
if (currentVisibleIndex < visibleOptions.length - 1) {
nextVisibleIndex = currentVisibleIndex + 1;
}
break;
case 'ArrowUp':
if (currentVisibleIndex > 0) {
nextVisibleIndex = currentVisibleIndex - 1;
} else if (currentVisibleIndex === -1) {
nextVisibleIndex = 0;
}
break;
case 'Home':
nextVisibleIndex = 0;
break;
case 'End':
nextVisibleIndex = visibleOptions.length - 1;
break;
}
if (nextVisibleIndex !== currentVisibleIndex) {
if (currentVisibleIndex > -1) {
visibleOptions[currentVisibleIndex].classList.remove('active');
}
const newActiveOption = visibleOptions[nextVisibleIndex];
newActiveOption.classList.add('active');
activeIndex = options.indexOf(newActiveOption);
if (newActiveOption.id) {
trigger.setAttribute('aria-activedescendant', newActiveOption.id);
}
newActiveOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
};
trigger.addEventListener('keydown', handleKeyNavigation);
if (filter) {
filter.addEventListener('keydown', handleKeyNavigation);
}
const openPopover = () => {
document.dispatchEvent(new CustomEvent('basecoat:popover', {
detail: { source: selectComponent }
}));
if (filter) {
if (hasTransition()) {
popover.addEventListener('transitionend', () => {
filter.focus();
}, { once: true });
} else {
filter.focus();
}
}
popover.setAttribute('aria-hidden', 'false');
trigger.setAttribute('aria-expanded', 'true');
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
if (selectedOption) {
if (activeIndex > -1) {
options[activeIndex]?.classList.remove('active');
}
activeIndex = options.indexOf(selectedOption);
selectedOption.classList.add('active');
if (selectedOption.id) {
trigger.setAttribute('aria-activedescendant', selectedOption.id);
}
selectedOption.scrollIntoView({ block: 'nearest' });
}
};
trigger.addEventListener('click', () => {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
closePopover();
} else {
openPopover();
}
});
listbox.addEventListener('click', (event) => {
const clickedOption = event.target.closest('[role="option"]');
if (clickedOption) {
selectOption(clickedOption);
}
});
document.addEventListener('click', (event) => {
if (!selectComponent.contains(event.target)) {
closePopover(false);
}
});
document.addEventListener('basecoat:popover', (event) => {
if (event.detail.source !== selectComponent) {
closePopover(false);
}
});
popover.setAttribute('aria-hidden', 'true');
selectComponent.dataset.selectInitialized = true;
};
document.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('div.select:not([data-select-initialized])')) {
initSelect(node);
}
node.querySelectorAll('div.select:not([data-select-initialized])').forEach(initSelect);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
(() => {
// Monkey patching the history API to detect client-side navigation
if (!window.history.__basecoatPatched) {
const originalPushState = window.history.pushState;
window.history.pushState = function(...args) {
originalPushState.apply(this, args);
window.dispatchEvent(new Event('basecoat:locationchange'));
};
const originalReplaceState = window.history.replaceState;
window.history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
window.dispatchEvent(new Event('basecoat:locationchange'));
};
window.history.__basecoatPatched = true;
}
const initSidebar = (sidebarComponent) => {
const initialOpen = sidebarComponent.dataset.initialOpen !== 'false';
const initialMobileOpen = sidebarComponent.dataset.initialMobileOpen === 'true';
const breakpoint = parseInt(sidebarComponent.dataset.breakpoint) || 768;
let open = breakpoint > 0
? (window.innerWidth >= breakpoint ? initialOpen : initialMobileOpen)
: initialOpen;
const updateCurrentPageLinks = () => {
const currentPath = window.location.pathname.replace(/\/$/, '');
sidebarComponent.querySelectorAll('a').forEach(link => {
if (link.hasAttribute('data-ignore-current')) return;
const linkPath = new URL(link.href).pathname.replace(/\/$/, '');
if (linkPath === currentPath) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
});
};
const updateState = () => {
sidebarComponent.setAttribute('aria-hidden', !open);
if (open) {
sidebarComponent.removeAttribute('inert');
} else {
sidebarComponent.setAttribute('inert', '');
}
};
const setState = (state) => {
open = state;
updateState();
};
const sidebarId = sidebarComponent.id;
document.addEventListener('basecoat:sidebar', (event) => {
if (event.detail?.id && event.detail.id !== sidebarId) return;
switch (event.detail?.action) {
case 'open':
setState(true);
break;
case 'close':
setState(false);
break;
default:
setState(!open);
break;
}
});
sidebarComponent.addEventListener('click', (event) => {
const target = event.target;
const nav = sidebarComponent.querySelector('nav');
const isMobile = window.innerWidth < breakpoint;
if (isMobile && (target.closest('a, button') && !target.closest('[data-keep-mobile-sidebar-open]'))) {
if (document.activeElement) document.activeElement.blur();
setState(false);
return;
}
if (target === sidebarComponent || (nav && !nav.contains(target))) {
if (document.activeElement) document.activeElement.blur();
setState(false);
}
});
window.addEventListener('popstate', updateCurrentPageLinks);
window.addEventListener('basecoat:locationchange', updateCurrentPageLinks);
updateState();
updateCurrentPageLinks();
sidebarComponent.dataset.sidebarInitialized = true;
};
document.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('.sidebar:not([data-sidebar-initialized])')) {
initSidebar(node);
}
node.querySelectorAll('.sidebar:not([data-sidebar-initialized])').forEach(initSidebar);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
(() => {
const initTabs = (tabsComponent) => {
const tablist = tabsComponent.querySelector('[role="tablist"]');
if (!tablist) return;
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls'))).filter(Boolean);
const selectTab = (tabToSelect) => {
tabs.forEach((tab, index) => {
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
if (panels[index]) panels[index].hidden = true;
});
tabToSelect.setAttribute('aria-selected', 'true');
tabToSelect.setAttribute('tabindex', '0');
const activePanel = document.getElementById(tabToSelect.getAttribute('aria-controls'));
if (activePanel) activePanel.hidden = false;
};
tablist.addEventListener('click', (event) => {
const clickedTab = event.target.closest('[role="tab"]');
if (clickedTab) selectTab(clickedTab);
});
tablist.addEventListener('keydown', (event) => {
const currentTab = event.target;
if (!tabs.includes(currentTab)) return;
let nextTab;
const currentIndex = tabs.indexOf(currentTab);
switch (event.key) {
case 'ArrowRight':
nextTab = tabs[(currentIndex + 1) % tabs.length];
break;
case 'ArrowLeft':
nextTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length];
break;
case 'Home':
nextTab = tabs[0];
break;
case 'End':
nextTab = tabs[tabs.length - 1];
break;
default:
return;
}
event.preventDefault();
selectTab(nextTab);
nextTab.focus();
});
tabsComponent.dataset.tabsInitialized = true;
};
document.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('.tabs:not([data-tabs-initialized])')) {
initTabs(node);
}
node.querySelectorAll('.tabs:not([data-tabs-initialized])').forEach(initTabs);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
(() => {
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';
}
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 (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;
}
const initialToaster = document.getElementById('toaster');
if (initialToaster) initToaster(initialToaster);
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);
});
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.matches('#toaster')) {
initToaster(node);
}
if (toaster && node.matches('.toast:not([data-toast-initialized])')) {
initToast(node);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();