// ============================================================================ // Stylescape | Component Registry // ============================================================================ // Central registry mapping data-ss component names to their handlers. // This enables automatic component initialization via data attributes. // ============================================================================ // Element Handlers import { ContentRevealer } from "../animations/ContentRevealer.js"; import { CountdownTimer } from "../animations/CountdownTimer.js"; import { Preloader } from "../animations/Preloader.js"; import { ProgressBarManager } from "../animations/ProgressBarManager.js"; import { ButtonHandler } from "../buttons/ButtonHandler.js"; import { ToggleSwitchManager } from "../buttons/ToggleSwitchManager.js"; import { FilterManager } from "../data/FilterManager.js"; import { RatingManager } from "../data/RatingManager.js"; import { AccordionManager } from "../elements/AccordionManager.js"; import { AsideHandler } from "../elements/AsideHandler.js"; import { CollapsibleSectionManager } from "../elements/CollapsibleSectionManager.js"; import { CollapsibleTableHandler } from "../elements/CollapsibleTableHandler.js"; import { DetailManager } from "../elements/DetailManager.js"; import { DropdownHandler } from "../elements/DropdownHandler.js"; import { ExclusiveDetails } from "../elements/ExclusiveDetails.js"; import { Modal } from "../elements/Modal.js"; import { NotificationManager } from "../elements/NotificationManager.js"; import { PasswordToggleManager } from "../elements/PasswordToggleManager.js"; import { ResponsiveMenuManager } from "../elements/ResponsiveMenuManager.js"; import { Tooltip } from "../elements/Tooltip.js"; // Form Components import { AutocompleteManager } from "../forms/AutocompleteManager.js"; import { FormValidator } from "../forms/FormValidator.js"; // Scroll Components import { ScrollToTopButton } from "../interface/scroll.js"; import { ImageCompareSlider } from "../media/ImageCompareSlider.js"; import { DragAndDropManager } from "../mouse/DragAndDropManager.js"; import { ScrollSpyManager } from "../scroll/ScrollSpyManager.js"; import { CookieConsentManager } from "../storage/CookieConsentManager.js"; import { ThemeToggler } from "../utilities/ThemeToggler.js"; // ============================================================================ // Types // ============================================================================ /** * Configuration options that can be passed to a component */ export interface ComponentConfig { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } /** * A component handler function that initializes a component on an element */ export type ComponentHandler = ( element: HTMLElement, config: ComponentConfig, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => any; /** * Registry entry containing the handler and optional default config */ export interface RegistryEntry { handler: ComponentHandler; defaults?: ComponentConfig; } // ============================================================================ // Component Registry // ============================================================================ /** * Central registry of all available Stylescape components. * * To register a new component: * 1. Import the component class * 2. Add an entry with a lowercase name as key * 3. Provide a handler function that instantiates the component * * @example * ```typescript * componentRegistry.set("mycomponent", { * handler: (el, config) => new MyComponent(el, config), * defaults: { option: "value" } * }) * ``` */ export const componentRegistry = new Map([ // ======================================================================== // Element Handlers // ======================================================================== [ "aside", { handler: (el, config) => { const menuId = config.menuId || el.dataset.ssAsideMenu || el.id; const switchId = config.switchId || el.dataset.ssAsideSwitch || `${menuId}_switch`; return new AsideHandler(menuId, switchId); }, defaults: {}, }, ], [ "dropdown", { handler: (el, config) => { const selector = config.selector || el.dataset.ssDropdownSelector || ".select_dropdown"; return new DropdownHandler(selector); }, defaults: {}, }, ], [ "collapsible-table", { handler: (_el, _config) => new CollapsibleTableHandler(), defaults: {}, }, ], [ "details", { handler: (_el, _config) => new DetailManager(), defaults: {}, }, ], [ "exclusive-details", { handler: (el, config) => { const selector = config.selector || el.dataset.ssExclusiveDetailsSelector || el.className; return new ExclusiveDetails(`.${selector}`); }, defaults: {}, }, ], [ "password-toggle", { handler: (_el, _config) => new PasswordToggleManager(), defaults: {}, }, ], // ======================================================================== // Media Components // ======================================================================== [ "image-compare", { handler: (_el, _config) => { // ImageCompareSlider has static initAll, so we use it for the element return ImageCompareSlider.initAll(); }, defaults: {}, }, ], // ======================================================================== // Utility Components // ======================================================================== [ "theme-toggle", { handler: (el, config) => { const toggleId = config.toggleId || el.dataset.ssThemeToggleId || el.id || "themeToggle"; return ThemeToggler.registerOnLoad(toggleId); }, defaults: {}, }, ], // ======================================================================== // Interactive Components (Placeholders for future implementation) // ======================================================================== [ "tooltip", { handler: (el, config) => { return new Tooltip(el, { content: config.content || el.dataset.ssTooltipContent || el.title, position: config.position || el.dataset.ssTooltipPosition, trigger: config.trigger?.split(",") || el.dataset.ssTooltipTrigger?.split(","), ...config, }); }, defaults: { position: "top" }, }, ], [ "modal", { handler: (el, config) => { return new Modal(el, { closeOnBackdrop: config.closeOnBackdrop ?? el.dataset.ssModalCloseBackdrop !== "false", closeOnEscape: config.closeOnEscape ?? el.dataset.ssModalCloseEscape !== "false", ...config, }); }, defaults: {}, }, ], [ "modal-trigger", { handler: (el, config) => { const targetSelector = config.target || el.dataset.ssModalTrigger; if (!targetSelector) return null; const modalEl = document.querySelector(targetSelector); if (!modalEl) return null; const modal = new Modal(modalEl); el.addEventListener("click", () => modal.open(el)); return modal; }, defaults: {}, }, ], [ "accordion", { handler: (el, config) => { return new AccordionManager(el, { allowMultiple: config.multiple ?? el.dataset.ssAccordionMultiple === "true", defaultOpen: config.defaultOpen ?? (el.dataset.ssAccordionDefaultOpen ? parseInt(el.dataset.ssAccordionDefaultOpen, 10) : -1), ...config, }); }, defaults: { multiple: false }, }, ], [ "tabs", { handler: (el, _config) => { const tabs = el.querySelectorAll("[data-ss-tab]"); const panels = el.querySelectorAll("[data-ss-tab-panel]"); const activate = (tabId: string) => { tabs.forEach((tab) => { const isActive = tab.getAttribute("data-ss-tab") === tabId; tab.classList.toggle("tab--active", isActive); tab.setAttribute("aria-selected", String(isActive)); }); panels.forEach((panel) => { const isActive = panel.getAttribute("data-ss-tab-panel") === tabId; panel.classList.toggle("tab-panel--active", isActive); panel.setAttribute("aria-hidden", String(!isActive)); }); }; tabs.forEach((tab) => { tab.addEventListener("click", () => { const tabId = tab.getAttribute("data-ss-tab"); if (tabId) activate(tabId); }); }); // Activate first tab by default const firstTab = tabs[0]?.getAttribute("data-ss-tab"); if (firstTab) activate(firstTab); return { activate }; }, defaults: {}, }, ], [ "carousel", { handler: (el, config) => { const slides = el.querySelectorAll("[data-ss-carousel-slide]"); const autoplay = config.autoplay !== false && el.dataset.ssCarouselAutoplay !== "false"; const interval = parseInt( config.interval || el.dataset.ssCarouselInterval || "5000", 10, ); let currentIndex = 0; let timer: number | null = null; const goTo = (index: number) => { currentIndex = (index + slides.length) % slides.length; slides.forEach((slide, i) => { slide.classList.toggle( "carousel-slide--active", i === currentIndex, ); }); }; const next = () => goTo(currentIndex + 1); const prev = () => goTo(currentIndex - 1); // Navigation buttons el.querySelector("[data-ss-carousel-prev]")?.addEventListener( "click", prev, ); el.querySelector("[data-ss-carousel-next]")?.addEventListener( "click", next, ); // Dots/indicators el.querySelectorAll("[data-ss-carousel-dot]").forEach( (dot, i) => { dot.addEventListener("click", () => goTo(i)); }, ); // Autoplay if (autoplay) { timer = window.setInterval(next, interval); el.addEventListener("mouseenter", () => { if (timer) clearInterval(timer); }); el.addEventListener("mouseleave", () => { timer = window.setInterval(next, interval); }); } goTo(0); return { goTo, next, prev, destroy: () => { if (timer) clearInterval(timer); }, }; }, defaults: { autoplay: true, interval: 5000 }, }, ], // ======================================================================== // Animation Components // ======================================================================== [ "preloader", { handler: (el, config) => { return new Preloader(el, { timeout: config.timeout ?? (el.dataset.ssPreloaderTimeout ? parseInt(el.dataset.ssPreloaderTimeout, 10) : undefined), minDisplayTime: config.minDisplayTime ?? (el.dataset.ssPreloaderMinDisplay ? parseInt(el.dataset.ssPreloaderMinDisplay, 10) : undefined), ...config, }); }, defaults: {}, }, ], [ "reveal", { handler: (el, config) => { return new ContentRevealer(el, { delay: config.delay ?? (el.dataset.ssRevealDelay ? parseInt(el.dataset.ssRevealDelay, 10) : undefined), onScroll: config.onScroll ?? el.dataset.ssRevealOnScroll === "true", threshold: config.threshold ?? (el.dataset.ssRevealThreshold ? parseFloat(el.dataset.ssRevealThreshold) : undefined), ...config, }); }, defaults: {}, }, ], [ "countdown", { handler: (el, config) => { return new CountdownTimer(el, { endTime: config.endTime ?? el.dataset.ssCountdownEndTime, format: config.format ?? el.dataset.ssCountdownFormat, endText: config.endText ?? el.dataset.ssCountdownEndText, ...config, }); }, defaults: {}, }, ], [ "progress", { handler: (el, config) => { return new ProgressBarManager(el, { value: config.value ?? (el.dataset.ssProgressValue ? parseInt(el.dataset.ssProgressValue, 10) : undefined), animate: config.animate ?? el.dataset.ssProgressAnimate !== "false", animationDuration: config.animationDuration ?? (el.dataset.ssProgressDuration ? parseInt(el.dataset.ssProgressDuration, 10) : undefined), ...config, }); }, defaults: {}, }, ], // ======================================================================== // Form Components // ======================================================================== [ "validate", { handler: (el, config) => { if (el.tagName === "FORM") { return new FormValidator(el as HTMLFormElement, config); } return null; }, defaults: {}, }, ], [ "autocomplete", { handler: (el, config) => { if (el.tagName === "INPUT") { return new AutocompleteManager(el as HTMLInputElement, { minChars: config.minChars ?? (el.dataset.ssAutocompleteMinChars ? parseInt( el.dataset.ssAutocompleteMinChars, 10, ) : undefined), suggestions: config.suggestions, ...config, }); } return null; }, defaults: {}, }, ], // ======================================================================== // Button/Input Components // ======================================================================== [ "button", { handler: (el, config) => { if (el.tagName === "BUTTON") { return new ButtonHandler(el as HTMLButtonElement, { disableOnLoading: config.disableOnLoading ?? el.dataset.ssButtonLoading === "true", ripple: config.ripple ?? el.dataset.ssButtonRipple !== "false", ...config, }); } return null; }, defaults: {}, }, ], [ "toggle", { handler: (el, config) => { if ( el.tagName === "INPUT" && (el as HTMLInputElement).type === "checkbox" ) { return new ToggleSwitchManager(el as HTMLInputElement, { persist: config.persist ?? el.dataset.ssTogglePersist === "true", storageKey: config.storageKey ?? el.dataset.ssToggleStorageKey, ...config, }); } return null; }, defaults: {}, }, ], // ======================================================================== // Mouse/Interaction Components // ======================================================================== [ "draggable", { handler: (el, config) => { return new DragAndDropManager(el, { handleSelector: config.handleSelector ?? el.dataset.ssDraggableHandle, dropZoneSelector: config.dropZoneSelector ?? el.dataset.ssDraggableDropzone, ...config, }); }, defaults: {}, }, ], // ======================================================================== // Data Components // ======================================================================== [ "filter", { handler: (el, config) => { if (el.tagName === "INPUT") { return new FilterManager(el as HTMLInputElement, { itemSelector: config.itemSelector ?? el.dataset.ssFilterItems, debounce: config.debounce ?? (el.dataset.ssFilterDebounce ? parseInt(el.dataset.ssFilterDebounce, 10) : undefined), minChars: config.minChars ?? (el.dataset.ssFilterMinChars ? parseInt(el.dataset.ssFilterMinChars, 10) : undefined), ...config, }); } return null; }, defaults: {}, }, ], [ "rating", { handler: (el, config) => { return new RatingManager(el, { max: config.max ?? (el.dataset.ssRatingMax ? parseInt(el.dataset.ssRatingMax, 10) : undefined), value: config.value ?? (el.dataset.ssRatingValue ? parseFloat(el.dataset.ssRatingValue) : undefined), half: config.half ?? el.dataset.ssRatingHalf === "true", readOnly: config.readOnly ?? el.dataset.ssRatingReadonly === "true", ...config, }); }, defaults: {}, }, ], // ======================================================================== // Storage Components // ======================================================================== [ "cookie-consent", { handler: (el, config) => { return new CookieConsentManager({ message: config.message ?? el.dataset.ssCookieMessage, position: config.position ?? el.dataset.ssCookiePosition, privacyPolicyUrl: config.privacyPolicyUrl ?? el.dataset.ssCookiePrivacyUrl, ...config, }); }, defaults: {}, }, ], // ======================================================================== // Scroll Components // ======================================================================== [ "scrollspy", { handler: (el, config) => { return new ScrollSpyManager({ navSelector: config.navSelector ?? el.dataset.ssScrollspyNav ?? `#${el.id} a`, threshold: config.threshold ?? (el.dataset.ssScrollspyThreshold ? parseFloat(el.dataset.ssScrollspyThreshold) : undefined), smoothScroll: config.smoothScroll ?? el.dataset.ssScrollspySmooth !== "false", offset: config.offset ?? (el.dataset.ssScrollspyOffset ? parseInt(el.dataset.ssScrollspyOffset, 10) : undefined), ...config, }); }, defaults: {}, }, ], [ "scroll-to-top", { handler: (el, config) => { return new ScrollToTopButton({ button: el, threshold: config.threshold ?? (el.dataset.ssScrollThreshold ? parseInt(el.dataset.ssScrollThreshold, 10) : undefined), ...config, }); }, defaults: {}, }, ], [ "scroll-to", { handler: (el, config) => { const target = config.target ?? el.dataset.ssScrollTarget ?? el.getAttribute("href"); const offset = config.offset ?? (el.dataset.ssScrollOffset ? parseInt(el.dataset.ssScrollOffset, 10) : 0); el.addEventListener("click", (e) => { e.preventDefault(); if (target) { const targetEl = document.querySelector(target); if (targetEl) { const top = targetEl.getBoundingClientRect().top + window.pageYOffset + offset; window.scrollTo({ top, behavior: "smooth" }); } } }); return { target, offset }; }, defaults: {}, }, ], // ======================================================================== // Notification Component // ======================================================================== [ "notification-container", { handler: (el, config) => { return new NotificationManager({ position: config.position ?? el.dataset.ssNotificationPosition, maxNotifications: config.maxNotifications ?? (el.dataset.ssNotificationMax ? parseInt(el.dataset.ssNotificationMax, 10) : undefined), ...config, }); }, defaults: {}, }, ], // ======================================================================== // Collapsible/Menu Components // ======================================================================== [ "collapsible", { handler: (el, config) => { return new CollapsibleSectionManager(el, { expanded: config.expanded ?? el.dataset.ssCollapsibleExpanded === "true", persist: config.persist ?? el.dataset.ssCollapsiblePersist === "true", ...config, }); }, defaults: {}, }, ], [ "responsive-menu", { handler: (el, config) => { const toggle = el.querySelector( "[data-ss-menu-toggle]", ); const menu = el.querySelector( "[data-ss-menu-content]", ); if (!toggle || !menu) return null; return new ResponsiveMenuManager(menu, toggle, { breakpoint: config.breakpoint ?? (el.dataset.ssMenuBreakpoint ? parseInt(el.dataset.ssMenuBreakpoint, 10) : undefined), ...config, }); }, defaults: {}, }, ], ]); // ============================================================================ // Registry Utilities // ============================================================================ /** * Register a new component in the registry * * @param name - Component name (used in data-ss attribute) * @param entry - Handler function and optional defaults */ export function registerComponent(name: string, entry: RegistryEntry): void { componentRegistry.set(name.toLowerCase(), entry); } /** * Check if a component is registered * * @param name - Component name to check */ export function hasComponent(name: string): boolean { return componentRegistry.has(name.toLowerCase()); } /** * Get a registered component entry * * @param name - Component name */ export function getComponent(name: string): RegistryEntry | undefined { return componentRegistry.get(name.toLowerCase()); } /** * Get all registered component names */ export function getComponentNames(): string[] { return Array.from(componentRegistry.keys()); }