stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
866 lines (789 loc) • 29.1 kB
text/typescript
// ============================================================================
// 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<string, RegistryEntry>([
// ========================================================================
// 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<HTMLElement>(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<HTMLElement>(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<HTMLElement>(
"[data-ss-menu-toggle]",
);
const menu = el.querySelector<HTMLElement>(
"[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());
}