UNPKG

ksk-core

Version:

Core design system components and styles for Kickstart projects

1,796 lines (1,514 loc) 257 kB
/** * 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