ksk-core
Version:
Core design system components and styles for Kickstart projects
1,796 lines (1,514 loc) • 257 kB
JavaScript
/**
* Global Image Handler
* Handles image loading states, lazy loading, and error handling for all images on the site
*/
class GlobalImageHandler {
constructor() {
this.images = new Set();
this.init();
}
init() {
this.setupGlobalImageHandling();
this.setupIntersectionObserver();
this.setupMutationObserver();
}
setupGlobalImageHandling() {
// Handle all images with data-enhanced attribute
const images = document.querySelectorAll('img[data-enhanced]');
images.forEach(img => this.enhanceImage(img));
// Handle Astro's image placeholders (for broken images)
const placeholders = document.querySelectorAll('.ast-img-placeholder');
placeholders.forEach(placeholder => this.handleImagePlaceholder(placeholder));
}
enhanceImage(img) {
if (this.images.has(img)) return;
this.images.add(img);
// Add loading class initially
img.classList.add('ast-img--loading');
// Handle successful load
img.addEventListener('load', () => {
img.classList.remove('ast-img--loading');
img.classList.add('ast-img--loaded');
img.removeAttribute('data-loading');
});
// Handle load error
img.addEventListener('error', () => {
img.classList.remove('ast-img--loading');
img.classList.add('ast-img--error');
img.removeAttribute('data-loading');
this.handleImageError(img);
});
// Check if image is already loaded (cached)
if (img.complete && img.naturalHeight !== 0) {
img.classList.remove('ast-img--loading');
img.classList.add('ast-img--loaded');
} else if (img.complete) {
// Image failed to load
img.classList.remove('ast-img--loading');
img.classList.add('ast-img--error');
this.handleImageError(img);
} else {
img.setAttribute('data-loading', 'true');
}
}
handleImageError(img) {
const fallbackSrc = img.dataset.fallback;
const showPlaceholder = img.dataset.placeholder !== 'false';
if (fallbackSrc && img.src !== fallbackSrc) {
img.src = fallbackSrc;
img.classList.remove('ast-img--error');
img.classList.add('ast-img--loading');
} else if (showPlaceholder) {
this.createPlaceholder(img);
}
console.warn('Failed to load image:', img.src);
}
createPlaceholder(img) {
const placeholder = document.createElement('div');
placeholder.className = 'ast-img-placeholder';
placeholder.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<circle cx="8.5" cy="8.5" r="1.5" stroke="currentColor" stroke-width="2"/>
<polyline points="21,15 16,10 5,21" stroke="currentColor" stroke-width="2"/>
</svg>
<span>Image not available</span>
`;
// Copy relevant styles from the original image
if (img.style.width) placeholder.style.width = img.style.width;
if (img.style.height) placeholder.style.height = img.style.height;
if (img.className) placeholder.className += ' ' + img.className.replace(/ast-img--\w+/g, '');
img.parentNode.replaceChild(placeholder, img);
}
handleImagePlaceholder(placeholder) {
// Apply error styling to Astro's image placeholders
placeholder.classList.add('ast-img--error');
console.warn('Image placeholder detected for failed image');
}
setupIntersectionObserver() {
if (!('IntersectionObserver' in window)) return;
const lazyImages = document.querySelectorAll('img[data-lazy]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-lazy');
img.setAttribute('data-enhanced', '');
this.enhanceImage(img);
observer.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
lazyImages.forEach(img => imageObserver.observe(img));
}
// Method to manually enhance new images added to the DOM
enhanceNewImage(img) {
if (img.tagName !== 'IMG') return;
img.setAttribute('data-enhanced', '');
this.enhanceImage(img);
}
// Method to enhance all images in a container
enhanceImagesInContainer(container) {
const images = container.querySelectorAll('img:not([data-enhanced])');
images.forEach(img => {
img.setAttribute('data-enhanced', '');
this.enhanceImage(img);
});
}
setupMutationObserver() {
// Watch for dynamically added image placeholders
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a placeholder
if (node.classList && node.classList.contains('ast-img-placeholder')) {
this.handleImagePlaceholder(node);
}
// Check for placeholders within added nodes
const placeholders = node.querySelectorAll && node.querySelectorAll('.ast-img-placeholder');
if (placeholders) {
placeholders.forEach(placeholder => this.handleImagePlaceholder(placeholder));
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// Auto-initialize on DOM ready
let globalImageHandler;
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
globalImageHandler = new GlobalImageHandler();
// Make it available globally for other components
window.globalImageHandler = globalImageHandler;
});
}
/**
* Button JavaScript functionality
* Handles click events, loading states, and form submissions
*/
class ButtonComponent {
constructor(element) {
this.element = element;
this.isLoading = false;
this.originalContent = element.innerHTML;
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
// Handle loading state for form submissions
if (this.element.type === 'submit') {
const form = this.element.closest('form');
if (form) {
form.addEventListener('submit', () => {
this.setLoading(true);
});
}
}
// Handle click events for confirmation dialogs
this.element.addEventListener('click', (event) => {
if (this.isLoading || this.element.disabled) {
event.preventDefault();
return;
}
// Handle confirmation dialogs
if (this.element.getAttribute('data-confirm')) {
const message = this.element.getAttribute('data-confirm');
if (!confirm(message)) {
event.preventDefault();
return false;
}
}
});
}
setLoading(loading) {
this.isLoading = loading;
if (loading) {
this.element.disabled = true;
this.element.innerHTML = this.element.getAttribute('data-loading-text') || 'Loading...';
this.element.setAttribute('aria-busy', 'true');
} else {
this.element.disabled = false;
this.element.innerHTML = this.originalContent;
this.element.setAttribute('aria-busy', 'false');
}
}
// Public API methods
enable() {
this.element.disabled = false;
this.setLoading(false);
}
disable() {
this.element.disabled = true;
}
}
// Auto-init function
const initButton = () => {
const buttons = document.querySelectorAll('.ast-btn');
buttons.forEach((el) => {
if (!el.buttonComponent) {
el.buttonComponent = new ButtonComponent(el);
}
});
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initButton);
} else {
initButton();
}
/**
* Accordion Component JavaScript
* Handles expand/collapse functionality with keyboard navigation
*/
class AccordionComponent {
constructor(accordion) {
this.accordion = accordion;
this.allowMultiple = accordion.dataset.allowMultiple === "true";
this.triggers = accordion.querySelectorAll("[data-accordion-trigger]");
this.contents = accordion.querySelectorAll("[data-accordion-content]");
if (this.triggers.length === 0 || this.contents.length === 0) {
return;
}
// Store instance reference
this.accordion.accordionComponent = this;
this.init();
}
init() {
this.triggers.forEach((trigger, index) => {
trigger.addEventListener("click", () => this.handleClick(index));
trigger.addEventListener("keydown", (e) => this.handleKeydown(e, index));
});
}
handleClick(index) {
const trigger = this.triggers[index];
const content = this.contents[index];
if (!trigger || !content) return;
const isExpanded = trigger.getAttribute("aria-expanded") === "true";
if (!this.allowMultiple) {
// Close all other items
this.triggers.forEach((otherTrigger, otherIndex) => {
if (otherIndex !== index) {
this.collapseItem(otherIndex);
}
});
}
if (isExpanded) {
this.collapseItem(index);
} else {
this.expandItem(index);
}
}
handleKeydown(event, index) {
const { key } = event;
switch (key) {
case "ArrowDown":
event.preventDefault();
this.focusNextTrigger(index);
break;
case "ArrowUp":
event.preventDefault();
this.focusPreviousTrigger(index);
break;
case "Home":
event.preventDefault();
this.triggers[0].focus();
break;
case "End":
event.preventDefault();
this.triggers[this.triggers.length - 1].focus();
break;
case "Enter":
case " ":
event.preventDefault();
this.handleClick(index);
break;
}
}
expandItem(index) {
const trigger = this.triggers[index];
const content = this.contents[index];
if (!trigger || !content) return;
trigger.setAttribute("aria-expanded", "true");
content.setAttribute("data-expanded", "true");
// Remove inert attribute to allow tabbing to content
const body = content.querySelector('.ast-accordion__body');
if (body) {
body.removeAttribute('inert');
}
// Set max-height for smooth animation
const scrollHeight = content.scrollHeight;
content.style.maxHeight = `${scrollHeight}px`;
// Custom event
this.accordion.dispatchEvent(new CustomEvent('accordion:expand', {
detail: { index },
bubbles: true
}));
}
collapseItem(index) {
const trigger = this.triggers[index];
const content = this.contents[index];
if (!trigger || !content) return;
trigger.setAttribute("aria-expanded", "false");
content.setAttribute("data-expanded", "false");
content.style.maxHeight = "0";
// Add inert attribute to prevent tabbing to content
const body = content.querySelector('.ast-accordion__body');
if (body) {
body.setAttribute('inert', '');
}
// Custom event
this.accordion.dispatchEvent(new CustomEvent('accordion:collapse', {
detail: { index },
bubbles: true
}));
}
focusNextTrigger(currentIndex) {
const nextIndex = currentIndex === this.triggers.length - 1 ? 0 : currentIndex + 1;
this.triggers[nextIndex].focus();
}
focusPreviousTrigger(currentIndex) {
const previousIndex = currentIndex === 0 ? this.triggers.length - 1 : currentIndex - 1;
this.triggers[previousIndex].focus();
}
// Public API methods
collapseAll() {
this.triggers.forEach((trigger, index) => {
this.collapseItem(index);
});
}
expandAll() {
if (this.allowMultiple) {
this.triggers.forEach((trigger, index) => {
this.expandItem(index);
});
}
}
}
// Auto-init function for manual initialization
const initAccordion = () => {
const accordions = document.querySelectorAll('.ast-accordion');
accordions.forEach(accordion => {
if (!accordion.accordionComponent) {
new AccordionComponent(accordion);
}
});
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAccordion);
} else {
initAccordion();
}
/**
* Alert JavaScript functionality
* Handles dismissible alerts, animations, and accessibility
*/
class AlertComponent {
constructor(element) {
this.element = element;
this.closeButton = element.querySelector('.ast-alert__close');
this.isDismissed = false;
this.init();
}
init() {
this.setupEventListeners();
this.animateIn();
}
setupEventListeners() {
if (this.closeButton) {
this.closeButton.addEventListener('click', (e) => {
e.preventDefault();
this.dismiss();
});
// Keyboard support for close button
this.closeButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.dismiss();
}
});
}
// Auto-dismiss after timeout (if specified)
const autoDismiss = this.element.getAttribute('data-auto-dismiss');
if (autoDismiss) {
const timeout = parseInt(autoDismiss) || 5000;
setTimeout(() => {
if (!this.isDismissed) {
this.dismiss();
}
}, timeout);
}
}
animateIn() {
// Add entering class for animation
this.element.classList.add('ast-alert--entering');
// Use requestAnimationFrame to ensure the class is applied
requestAnimationFrame(() => {
this.element.classList.remove('ast-alert--entering');
this.element.classList.add('ast-alert--entered');
});
}
dismiss() {
if (this.isDismissed) return;
this.isDismissed = true;
// Custom event for dismiss
const dismissEvent = new CustomEvent('alert:dismiss', {
detail: { type: this.getAlertType(), element: this.element },
bubbles: true
});
this.element.dispatchEvent(dismissEvent);
// Animate out
this.element.classList.add('ast-alert--exiting');
// Remove from DOM after animation
setTimeout(() => {
if (this.element.parentNode) {
this.element.remove();
}
}, 300);
}
getAlertType() {
const classes = this.element.className.split(' ');
const typeClass = classes.find(cls => cls.startsWith('ast-alert--') && cls !== 'ast-alert--entering' && cls !== 'ast-alert--entered' && cls !== 'ast-alert--exiting');
return typeClass ? typeClass.replace('ast-alert--', '') : 'info';
}
// Public API methods
show() {
this.element.style.display = '';
this.animateIn();
}
hide() {
this.dismiss();
}
updateContent(content) {
const contentElement = this.element.querySelector('.ast-alert__content') || this.element;
if (this.closeButton) {
// Update content while preserving close button
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
contentElement.innerHTML = '';
while (tempDiv.firstChild) {
contentElement.appendChild(tempDiv.firstChild);
}
contentElement.appendChild(this.closeButton);
} else {
contentElement.innerHTML = content;
}
}
}
// Auto-initialize existing alerts
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
const alerts = document.querySelectorAll('.ast-alert');
alerts.forEach(alert => {
if (!alert.alertComponent) {
alert.alertComponent = new AlertComponent(alert);
}
});
});
}
// Auto-init function for manual initialization
const initAlert = () => {
const alerts = document.querySelectorAll('.ast-alert');
alerts.forEach(alert => {
if (!alert.alertComponent) {
alert.alertComponent = new AlertComponent(alert);
}
});
};
/**
* Card JavaScript functionality
* Handles interactive cards with minimal overhead
*/
class CardComponent {
constructor(element) {
this.element = element;
this.isInteractive = element.classList.contains('ast-card--interactive');
this.isClickable = element.classList.contains('ast-card--clickable');
this.init();
}
init() {
this.setupEventListeners();
}
setupEventListeners() {
if (this.isInteractive || this.isClickable) {
// Handle keyboard navigation
this.element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleActivation(e);
}
});
// Handle clicks for analytics and custom behavior
this.element.addEventListener('click', (e) => {
this.handleActivation(e);
});
}
// Handle focus states
if (this.isInteractive && !this.isClickable) {
this.element.addEventListener('focus', () => {
this.element.classList.add('ast-card--focused');
});
this.element.addEventListener('blur', () => {
this.element.classList.remove('ast-card--focused');
});
}
}
handleActivation(event) {
// Custom event for card activation
const activationEvent = new CustomEvent('ast-card:activate', {
detail: {
element: this.element,
originalEvent: event
},
bubbles: true
});
this.element.dispatchEvent(activationEvent);
}
}
// Auto-init function for manual initialization
const initCard = () => {
const cards = document.querySelectorAll('.ast-card');
cards.forEach(card => {
if (!card.cardComponent) {
card.cardComponent = new CardComponent(card);
}
});
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCard);
} else {
initCard();
}
/**
* Checkbox JavaScript functionality
* Handles validation, indeterminate states, and basic functionality
*/
class CheckboxComponent {
constructor(element) {
this.element = element;
this.input = element.querySelector('.ast-checkbox-field__input');
this.errorElement = element.querySelector('.ast-checkbox-field__error');
this.init();
}
init() {
this.setupEventListeners();
this.setupValidation();
this.handleInitialState();
}
setupEventListeners() {
// Change event for state tracking
this.input.addEventListener('change', (e) => {
this.handleChange(e);
});
// Focus events for custom styling
this.input.addEventListener('focus', () => {
this.element.classList.add('ast-checkbox-field--focused');
});
this.input.addEventListener('blur', () => {
this.element.classList.remove('ast-checkbox-field--focused');
this.validateInput();
});
}
setupValidation() {
// Simple required validation
this.isRequired = this.input.hasAttribute('required');
}
handleInitialState() {
// Handle indeterminate state from data attribute
const indeterminate = this.input.getAttribute('data-indeterminate');
if (indeterminate === 'true') {
this.setIndeterminate(true);
}
// Initial validation
if (this.input.value && !this.input.checkValidity()) {
this.validateInput();
}
}
handleChange(event) {
// Clear indeterminate state when user interacts
if (this.input.indeterminate) {
this.setIndeterminate(false);
}
// Validate on change
this.validateInput();
// Custom change event with additional data
const changeEvent = new CustomEvent('checkbox:change', {
detail: {
checked: this.input.checked,
value: this.input.value,
name: this.input.name,
element: this.element,
valid: this.isValid(),
originalEvent: event
},
bubbles: true
});
this.element.dispatchEvent(changeEvent);
// Handle group interactions
this.handleGroupInteraction();
}
handleGroupInteraction() {
const group = this.element.closest('.ast-checkbox-group');
if (!group) return;
const checkboxes = group.querySelectorAll('.ast-checkbox-field__input');
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
const totalCount = checkboxes.length;
// Update group state
const groupEvent = new CustomEvent('checkbox:group:change', {
detail: {
checkedCount,
totalCount,
allChecked: checkedCount === totalCount,
noneChecked: checkedCount === 0,
someChecked: checkedCount > 0 && checkedCount < totalCount,
group: group
},
bubbles: true
});
group.dispatchEvent(groupEvent);
}
validateInput() {
let isValid = true;
let errorMessage = '';
// Required validation
if (this.isRequired && !this.input.checked) {
isValid = false;
errorMessage = 'This field is required';
}
// Update UI
this.setError(isValid ? '' : errorMessage);
this.input.setAttribute('aria-invalid', isValid ? 'false' : 'true');
return isValid;
}
// Public API methods
check() {
this.input.checked = true;
this.handleChange(new Event('change'));
}
uncheck() {
this.input.checked = false;
this.handleChange(new Event('change'));
}
toggle() {
this.input.checked = !this.input.checked;
this.handleChange(new Event('change'));
}
setIndeterminate(indeterminate) {
this.input.indeterminate = indeterminate;
this.input.setAttribute('data-indeterminate', indeterminate.toString());
if (indeterminate) {
this.element.classList.add('ast-checkbox-field--indeterminate');
} else {
this.element.classList.remove('ast-checkbox-field--indeterminate');
}
}
setError(message) {
if (this.errorElement) {
if (message) {
this.errorElement.textContent = message;
this.errorElement.style.display = 'block';
this.element.classList.add('ast-checkbox-field--error');
} else {
this.errorElement.style.display = 'none';
this.element.classList.remove('ast-checkbox-field--error');
}
}
}
setDisabled(disabled) {
this.input.disabled = disabled;
if (disabled) {
this.element.classList.add('ast-checkbox-field--disabled');
} else {
this.element.classList.remove('ast-checkbox-field--disabled');
}
}
isValid() {
return this.validateInput();
}
getValue() {
return {
name: this.input.name,
value: this.input.value,
checked: this.input.checked,
valid: this.isValid()
};
}
}
// Checkbox Group Manager
class CheckboxGroupManager {
constructor(groupElement) {
this.group = groupElement;
this.checkboxes = [];
this.init();
}
init() {
// Find all checkboxes in the group
const checkboxElements = this.group.querySelectorAll('.ast-checkbox-field');
this.checkboxes = Array.from(checkboxElements).map(element => {
return element.checkboxComponent || new CheckboxComponent(element);
});
this.setupGroupEvents();
}
setupGroupEvents() {
// Listen for individual checkbox changes
this.group.addEventListener('checkbox:change', (event) => {
this.handleGroupChange(event);
});
}
handleGroupChange(event) {
const checkedCount = this.getCheckedCount();
const totalCount = this.checkboxes.length;
// Update "select all" checkbox if present
const selectAllCheckbox = this.group.querySelector('[data-select-all]');
if (selectAllCheckbox) {
const selectAllComponent = selectAllCheckbox.closest('.ast-checkbox-field').checkboxComponent;
if (checkedCount === 0) {
selectAllComponent.uncheck();
selectAllComponent.setIndeterminate(false);
} else if (checkedCount === totalCount) {
selectAllComponent.check();
selectAllComponent.setIndeterminate(false);
} else {
selectAllComponent.setIndeterminate(true);
}
}
}
// Public API methods
checkAll() {
this.checkboxes.forEach(checkbox => {
if (!checkbox.input.hasAttribute('data-select-all')) {
checkbox.check();
}
});
}
uncheckAll() {
this.checkboxes.forEach(checkbox => {
if (!checkbox.input.hasAttribute('data-select-all')) {
checkbox.uncheck();
}
});
}
getCheckedCount() {
return this.checkboxes.filter(checkbox =>
checkbox.input.checked && !checkbox.input.hasAttribute('data-select-all')
).length;
}
getCheckedValues() {
return this.checkboxes
.filter(checkbox => checkbox.input.checked && !checkbox.input.hasAttribute('data-select-all'))
.map(checkbox => checkbox.getValue());
}
validate() {
return this.checkboxes.every(checkbox => checkbox.isValid());
}
}
// Auto-init function for manual initialization
const initCheckbox = () => {
// Initialize individual checkboxes
const checkboxes = document.querySelectorAll('.ast-checkbox-field');
checkboxes.forEach(checkbox => {
if (!checkbox.checkboxComponent) {
checkbox.checkboxComponent = new CheckboxComponent(checkbox);
}
});
// Initialize checkbox groups
const groups = document.querySelectorAll('.ast-checkbox-group');
groups.forEach(group => {
if (!group.checkboxGroupManager) {
group.checkboxGroupManager = new CheckboxGroupManager(group);
}
});
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initCheckbox();
// Handle "select all" functionality
document.addEventListener('checkbox:change', (event) => {
const checkbox = event.target.closest('.ast-checkbox-field');
const input = checkbox.querySelector('.ast-checkbox-field__input');
if (input.hasAttribute('data-select-all')) {
const group = checkbox.closest('.ast-checkbox-group');
if (group && group.checkboxGroupManager) {
if (input.checked) {
group.checkboxGroupManager.checkAll();
} else {
group.checkboxGroupManager.uncheckAll();
}
}
}
});
});
} else {
initCheckbox();
}
/**
* Dialog Component JavaScript
* Handles accessibility, focus management, and user interactions
*/
class DialogManager {
constructor() {
this.activeDialog = null;
this.previousFocus = null;
this.focusableElements = [
'button',
'[href]',
'input',
'select',
'textarea',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable="true"]'
].join(',');
this.init();
}
init() {
if (typeof document !== 'undefined') {
// Initialize all dialogs immediately if DOM is ready, or wait for DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setupDialogs();
});
} else {
// DOM is already loaded, initialize immediately
this.setupDialogs();
}
// Handle dynamic content
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('[data-dialog]')) {
this.setupDialog(node);
}
// Check for dialogs in added subtree
const dialogs = node.querySelectorAll('[data-dialog]');
dialogs.forEach(dialog => this.setupDialog(dialog));
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
setupDialogs() {
const dialogs = document.querySelectorAll('[data-dialog]');
dialogs.forEach(dialog => this.setupDialog(dialog));
}
setupDialog(dialog) {
const dialogId = dialog.dataset.dialog;
const backdrop = document.querySelector(`[data-dialog-backdrop="${dialogId}"]`);
if (!backdrop) return;
// Set up event listeners
this.setupTriggers(dialogId);
this.setupCloseButtons(dialogId);
this.setupBackdropClick(backdrop, dialog);
this.setupKeyboardHandlers(dialog);
}
setupTriggers(dialogId) {
// Find all elements that trigger this dialog
const triggers = document.querySelectorAll(`[data-dialog-trigger="${dialogId}"]`);
triggers.forEach(trigger => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
this.openDialog(dialogId);
});
});
}
setupCloseButtons(dialogId) {
const closeButtons = document.querySelectorAll(`[data-dialog-close="${dialogId}"]`);
closeButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
this.closeDialog(dialogId);
});
});
}
setupBackdropClick(backdrop, dialog) {
const closeOnOutside = dialog.dataset.closeOutside !== 'false';
if (closeOnOutside) {
backdrop.addEventListener('click', (e) => {
// Only close if clicking the backdrop, not the dialog itself
if (e.target === backdrop) {
this.closeDialog(dialog.dataset.dialog);
}
});
}
}
setupKeyboardHandlers(dialog) {
const dialogId = dialog.dataset.dialog;
const closeOnEscape = dialog.dataset.closeEscape !== 'false';
dialog.addEventListener('keydown', (e) => {
switch (e.key) {
case 'Escape':
if (closeOnEscape) {
e.preventDefault();
this.closeDialog(dialogId);
}
break;
case 'Tab':
this.handleTabKey(e, dialog);
break;
}
});
}
openDialog(dialogId) {
const dialog = document.querySelector(`[data-dialog="${dialogId}"]`);
const backdrop = document.querySelector(`[data-dialog-backdrop="${dialogId}"]`);
if (!dialog || !backdrop) return;
// Store the currently focused element
this.previousFocus = document.activeElement;
// Prevent body scroll
this.lockDialog();
// Show the dialog
backdrop.setAttribute('aria-hidden', 'false');
// Set active dialog
this.activeDialog = dialog;
// Focus management
this.setInitialFocus(dialog);
// Announce to screen readers
this.announceDialog(dialog);
// Trigger custom event
dialog.dispatchEvent(new CustomEvent('dialog:open', {
detail: { dialogId },
bubbles: true
}));
}
closeDialog(dialogId) {
const dialog = document.querySelector(`[data-dialog="${dialogId}"]`);
const backdrop = document.querySelector(`[data-dialog-backdrop="${dialogId}"]`);
if (!dialog || !backdrop) return;
// Hide the dialog
backdrop.setAttribute('aria-hidden', 'true');
// Restore body scroll
this.unlockDialog();
// Restore focus
this.restoreFocus();
// Clear active dialog
this.activeDialog = null;
// Trigger custom event
dialog.dispatchEvent(new CustomEvent('dialog:close', {
detail: { dialogId },
bubbles: true
}));
}
handleTabKey(e, dialog) {
const focusableElements = this.getFocusableElements(dialog);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
getFocusableElements(container) {
const elements = container.querySelectorAll(this.focusableElements);
return Array.from(elements).filter(el => {
return !el.disabled &&
!el.getAttribute('aria-hidden') &&
el.offsetParent !== null;
});
}
setInitialFocus(dialog) {
// Priority order for initial focus:
// 1. Element with autofocus
// 2. First input/textarea
// 3. First focusable element
// 4. Dialog itself
const autofocusElement = dialog.querySelector('[autofocus]');
if (autofocusElement) {
autofocusElement.focus();
return;
}
const firstInput = dialog.querySelector('input, textarea');
if (firstInput && !firstInput.disabled) {
firstInput.focus();
return;
}
const focusableElements = this.getFocusableElements(dialog);
if (focusableElements.length > 0) {
focusableElements[0].focus();
return;
}
// Fallback to dialog itself
dialog.focus();
}
restoreFocus() {
if (this.previousFocus && typeof this.previousFocus.focus === 'function') {
// Small delay to ensure dialog is fully closed
setTimeout(() => {
try {
this.previousFocus.focus();
} catch (e) {
// Element might not be focusable anymore, fallback to body
document.body.focus();
}
}, 100);
}
this.previousFocus = null;
}
lockDialog() {
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.documentElement.classList.add('ast-dialog-open');
document.documentElement.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`);
document.documentElement.style.paddingRight = `${scrollbarWidth}px`;
document.body.style.overflow = 'hidden';
}
unlockDialog() {
document.documentElement.classList.remove('ast-dialog-open');
document.documentElement.style.removeProperty('--scrollbar-width');
document.documentElement.style.paddingRight = '';
document.body.style.overflow = '';
}
announceDialog(dialog) {
// Create a live region announcement for screen readers
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
const title = dialog.querySelector('.ast-dialog__title');
const announcementText = title
? `Dialog opened: ${title.textContent}`
: 'Dialog opened';
announcement.textContent = announcementText;
document.body.appendChild(announcement);
// Remove after announcement
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
// Public API methods
open(dialogId) {
this.openDialog(dialogId);
}
close(dialogId) {
this.closeDialog(dialogId);
}
closeAll() {
const openBackdrops = document.querySelectorAll('[data-dialog-backdrop][aria-hidden="false"]');
openBackdrops.forEach(backdrop => {
const dialogId = backdrop.dataset.dialogBackdrop;
this.closeDialog(dialogId);
});
}
isOpen(dialogId) {
const backdrop = document.querySelector(`[data-dialog-backdrop="${dialogId}"]`);
return backdrop && backdrop.getAttribute('aria-hidden') === 'false';
}
}
// Auto-init function for manual initialization
const initDialog = () => {
// Create a single global dialog manager instance
if (!window.dialogManager) {
window.dialogManager = new DialogManager();
}
};
/**
* FormField Component JavaScript
* Currently handles basic form field functionality
* Can be extended for validation, formatting, etc.
*/
class FormFieldManager {
constructor() {
this.init();
}
init() {
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
this.setupFormFields();
});
}
}
setupFormFields() {
const formFields = document.querySelectorAll('.ast-form-field');
formFields.forEach(field => {
this.setupField(field);
});
}
setupField(fieldContainer) {
const input = fieldContainer.querySelector('.ast-form-field__input');
if (!input) return;
// Add future enhancements here:
// - Real-time validation
// - Input formatting (phone numbers, etc.)
// - Character counting
// - Custom validation messages
}
// Public API for future enhancements
validateField(fieldName) {
// Future: Add validation logic
}
clearErrors(fieldName) {
// Future: Clear error states
}
setError(fieldName, errorMessage) {
// Future: Set error states dynamically
}
}
// Auto-initialize form fields
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
const formFields = document.querySelectorAll('.ast-form-field');
formFields.forEach(field => {
if (!field.formFieldManager) {
field.formFieldManager = new FormFieldManager();
}
});
});
}
// Auto-init function for manual initialization
const initFormField = () => {
const formFields = document.querySelectorAll('.ast-form-field');
formFields.forEach(field => {
if (!field.formFieldManager) {
field.formFieldManager = new FormFieldManager();
}
});
};
/**
* Navigation Component JavaScript
* Handles mobile menu, dropdown interactions, and keyboard accessibility
* For standard dropdown navigation (not mega menus)
*/
let Navigation$1 = class Navigation {
constructor(element) {
// Prevent duplicate initialization
if (element.dataset.navigationInitialized === 'true') {
return;
}
// Skip initialization if this is a mega menu (handled by MegaNavigation)
if (element.classList.contains('ast-navigation--mega')) {
return;
}
this.nav = element;
this.navigationId = this.nav.dataset.navigationId;
this.mobileAnimation = this.nav.dataset.mobileAnimation || 'left';
// Mark as initialized
this.nav.dataset.navigationInitialized = 'true';
// Elements
this.mobileToggle = this.nav.querySelector('[data-nav-toggle]');
this.mobileClose = this.nav.querySelector('[data-nav-close]');
this.menu = this.nav.querySelector('[data-nav-menu]');
this.overlay = this.nav.querySelector('[data-nav-overlay]');
this.dropdowns = this.nav.querySelectorAll('[data-dropdown]');
this.submenus = this.nav.querySelectorAll('[data-submenu]');
// State
this.isOpen = false;
this.activeDropdown = null;
this.focusedElement = null;
this.init();
}
init() {
this.bindEvents();
this.setupKeyboardNavigation();
}
bindEvents() {
// Mobile toggle
if (this.mobileToggle) {
this.mobileToggle.addEventListener('click', () => this.toggleMobile());
}
// Mobile close
if (this.mobileClose) {
this.mobileClose.addEventListener('click', () => this.closeMobile());
}
// Overlay click
if (this.overlay) {
this.overlay.addEventListener('click', () => this.closeMobile());
}
// Dropdown triggers
this.dropdowns.forEach((dropdown, index) => {
const trigger = dropdown.querySelector('[data-dropdown-trigger]');
const content = dropdown.querySelector('[data-dropdown-content]');
if (trigger && content) {
// Desktop hover
dropdown.addEventListener('mouseenter', () => {
if (window.innerWidth >= 768) {
this.openDropdown(dropdown);
}
});
dropdown.addEventListener('mouseleave', () => {
if (window.innerWidth >= 768) {
this.closeDropdown(dropdown);
}
});
// Click toggle (mobile and desktop)
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleDropdown(dropdown);
});
// Focus management for accessibility
trigger.addEventListener('focusout', (e) => {
setTimeout(() => {
if (!dropdown.contains(document.activeElement)) {
this.closeDropdown(dropdown);
}
}, 0);
});
content.addEventListener('focusout', (e) => {
setTimeout(() => {
if (!dropdown.contains(document.activeElement)) {
this.closeDropdown(dropdown);
}
}, 0);
});
}
});
// Submenu triggers
this.submenus.forEach(submenu => {
const trigger = submenu.querySelector('[data-submenu-trigger]');
if (trigger) {
trigger.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleSubmenu(submenu);
});
}
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.nav.contains(e.target)) {
this.closeAllDropdowns();
}
});
// Handle window resize
window.addEventListener('resize', () => {
if (window.innerWidth >= 768 && this.isOpen) {
this.closeMobile();
}
});
// Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (this.isOpen) {
this.closeMobile();
} else {
this.closeAllDropdowns();
}
}
});
}
setupKeyboardNavigation() {
const menuItems = this.nav.querySelectorAll('[role="menuitem"]');
menuItems.forEach((item, index) => {
item.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.focusNextItem(menuItems, index);
break;
case 'ArrowUp':
e.preventDefault();
this.focusPreviousItem(menuItems, index);
break;
case 'ArrowRight':
e.preventDefault();
this.handleRightArrow(item);
break;
case 'ArrowLeft':
e.preventDefault();
this.handleLeftArrow(item);
break;
case 'Enter':
case ' ':
e.preventDefault();
this.handleEnterSpace(item);
break;
case 'Home':
e.preventDefault();
menuItems[0].focus();
break;
case 'End':
e.preventDefault();
menuItems[menuItems.length - 1].focus();
break;
}
});
});
}
// Mobile menu methods
toggleMobile() {
if (this.isOpen) {
this.closeMobile();
} else {
this.openMobile();
}
}
openMobile() {
this.isOpen = true;
this.nav.classList.add('ast-navigation--open');
this.menu.classList.add('ast-navigation__menu--open');
this.overlay.classList.add('ast-navigation__overlay--visible');
// Update ARIA
this.mobileToggle.setAttribute('aria-expanded', 'true');
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Focus first menu item
const firstMenuItem = this.menu.querySelector('[role="menuitem"]');
if (firstMenuItem) {
firstMenuItem.focus();
}
}
closeMobile() {
this.isOpen = false;
this.nav.classList.remove('ast-navigation--open');
this.menu.classList.remove('ast-navigation__menu--open');
this.overlay.classList.remove('ast-navigation__overlay--visible');
// Update ARIA
this.mobileToggle.setAttribute('aria-expanded', 'false');
// Restore body scroll
document.body.style.overflow = '';
// Close all dropdowns
this.closeAllDropdowns();
// Return focus to toggle
this.mobileToggle.focus();
}
// Dropdown methods
toggleDropdown(dropdown) {
const isOpen = dropdown.classList.contains('ast-navigation__dropdown--open');
if (isOpen) {
this.closeDropdown(dropdown);
} else {
this.closeAllDropdowns();
this.openDropdown(dropdown);
}
}
openDropdown(dropdown) {
const trigger = dropdown.querySelector('[data-dropdown-trigger]');
const content = dropdown.querySelector('[data-dropdown-content]');
dropdown.classList.add('ast-navigation__dropdown--open');
content.classList.add('ast-navigation__dropdown-content--open');
trigger.setAttribute('aria-expanded', 'true');
this.activeDropdown = dropdown;
}
closeDropdown(dropdown) {
const trigger = dropdown.querySelector('[data-dropdown-trigger]');
const content = dropdown.querySelector('[data-dropdown-content]');
dropdown.classList.remove('ast-navigation__dropdown--open');
content.classList.remove('ast-navigation__dropdown-content--open');
trigger.setAttribute('aria-expanded', 'false');
// Close all submenus within this dropdown
const submenus = dropdown.querySelectorAll('[data-submenu]');
submenus.forEach(submenu => this.closeSubmenu(submenu));
if (this.activeDropdown === dropdown) {
this.activeDropdown = null;
}
}
closeAllDropdowns() {
this.dropdowns.forEach(dropdown => this.closeDropdown(dropdown));
}
// Submenu methods
toggleSubmenu(submenu) {
const isOpen = submenu.classList.contains('ast-navigation__submenu--open');
if (isOpen) {
this.closeSubmenu(submenu);
} else {
this.openSubmenu(submenu);
}
}
openSubmenu(submenu) {
const trigger = submenu.querySelector('[data-submenu-trigger]');
const content = submenu.querySelector('[data-submenu-content]');
submenu.classList.add('ast-navigation__submenu--open');
content.classList.add('ast-navigation__submenu-list--open');
trigger.setAttribute('aria-expanded', 'true');
}
closeSubmenu(submenu) {
const trigger = submenu.querySelector('[data-submenu-trigger]');
const content = submenu.querySelector('[data-submenu-content]');
submenu.classList.remove('ast-navigation__submenu--open');
content.classList.remove('ast-navigation__submenu-list--open');
trigger.setAttribute('aria-expanded', 'false');
}
// Keyboard navigation helpers
focusNextItem(items, currentIndex) {
const nextIndex = (currentIndex + 1) % items.length;
items[nextIndex].focus();
}
focusPreviousItem(items, currentIndex) {
const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
items[prevIndex].focus();
}
handleRightArrow(item) {
// Open submenu or dropdown if available
const dropdown = item.closest('[data-dropdown]');
const submenu = item.closest('[data-submenu]');
if (dropdown && !dropdown.classList.contains('ast-navigation__dropdown--open')) {
this.openDropdown(dropdown);
} else if (submenu && !submenu.classList.contains('ast-navigation__submenu--open')) {
this.openSubmenu(submenu);
}
}
handleLeftArrow(item) {
// Close current submenu or return to parent
const submenu = item.closest('[data-submenu]');
const dropdown = item.closest('[data-dropdown]');
if (submenu && submenu.classList.contains('ast-navigation__submenu--open')) {
this.closeSubmenu(submenu);
} else if (dropdown && dropdown.classList.contains('ast-navigation__dropdown--open')) {
this.closeDropdown(dropdown);
}
}
handleEnterSpace(item) {
// Trigger click for buttons, follow links
if (item.tagName === 'BUTTON') {
item.click();
} else if (item.tagName === 'A') {
window.location.href = item.href;
}
}
};
// Initialize all navigation components (excluding mega menus)
function initNavigation() {
const navigationElements = document.querySelectorAll('.ast-navigation:not(.ast-navigation--mega)');
navigationElements.forEach(nav => {
new Navigation$1(nav);
});
}
// Auto-initialize when DOM is ready (for standalone usage)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNavigation);
} else {
initNavigation();
}
/**
* Mega Navigation Component JavaScript
* Handles mega menu interactions with left column navigation and content section visibility
*/
class MegaNavigation {
constructor(element) {
// Prevent duplicate initialization
if (element.dataset.megaNavigationInitialized === 'true') {
return;
}
this.nav = element;
this.navigationId = this.nav.dataset.navigationId;
this.mobileAnimation = this.nav.dataset.mobileAnimation || 'left';
// Mark as initialized
this.nav.dataset.megaNavigationInitialized = 'true';
// Elements
this.mobileToggle = this.nav.querySelector('[data-nav-toggle]');
this.mobileClose = this.nav.querySelector('[data-nav-close]');
this.menu = this.nav.querySelector('[data-nav-menu]');
this.overlay = this.nav.querySelector('[data-nav-overlay]');
this.mainNavButtons = this.nav.querySelectorAll('[data-main-nav-button]');
this.megaMenu = this.nav.querySelector('[data-mega-menu]');
this.leftNavButtons = this.nav.querySelectorAll('[data-mega-section]');
this.contentSections = this.nav.querySelectorAll('[data-mega-content]');
// State
this.isMobileOpen = false;
this.isMegaMenuOpen = false;
this.activeSection = null;
this.init();
}
init() {
this.bindEvents();
this.setupKeyboardNavigation();
this.hideAllSections();
}
bindEvents() {
// Mobile toggle
if (this.mobileToggle) {
this.mobileToggle.addEventListener('click', () => this.toggleMobile());
}
// Mobile close
if (this.mobileClose) {
this.mobileClose.addEventListener('click', () => this.closeMobile());
}
// Overlay close
if (this.overlay) {
this.overlay.addEventListener('click', () => this.closeMobile());
}
// Main navigation buttons (top level)
this.mainNavButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const sectionName = button.dataset.megaTrigger.toLowerCase();
thi