// ============================================================================ // Stylescape | Aside Handler // ============================================================================ // Manages sidebar/aside menu visibility with localStorage persistence. // Supports data-ss-aside attributes for declarative configuration. // ============================================================================ import { StateManager } from "../state/StateManager.js"; import { LocalStorageManager } from "../storage/LocalStorageManager.js"; /** * Configuration options for AsideHandler */ export interface AsideHandlerOptions { /** CSS class for visible state (default `active`) */ visibleClass?: string; /** * Visibility on a first visit, before anything is stored, and always * when `persist` is off (default `hide`). */ initialState?: "show" | "hide"; /** * Remember the state in localStorage and restore it on the next visit * (default `true`). Off, the menu starts in `initialState` every time * and nothing is read from or written to storage. */ persist?: boolean; /** Callback when menu opens */ onOpen?: (menu: HTMLElement) => void; /** Callback when menu closes */ onClose?: (menu: HTMLElement) => void; } /** * Aside/sidebar menu handler with persistence support. * Manages the toggle state of sidebar menus with localStorage. * * @example JavaScript * ```typescript * const aside = new AsideHandler("sideMenu", "menuToggle", { * initialState: "show", * }) * * // Programmatic control * aside.showMenu() * aside.hideMenu() * aside.toggleMenu() * ``` * * @example HTML with data-ss * ```html * * * ``` */ export class AsideHandler { /** CSS class applied when menu is visible */ private static readonly VISIBLE_CLASS = "active"; /** LocalStorage key suffix for visibility state */ private static readonly VISIBLE_SUFFIX = "_visibility"; /** State value for visible menu */ private static readonly VISIBLE_STATE = "show"; /** State value for hidden menu */ private static readonly HIDDEN_STATE = "hide"; /** LocalStorage manager instance */ localStorageManager: LocalStorageManager; /** State manager instance */ stateManager: StateManager; /** ID of the aside menu element */ menuId: string; /** ID of the toggle switch element */ switchId: string; /** Resolved options */ options: Required> & AsideHandlerOptions; /** Reference to the aside menu element */ asideMenu: HTMLElement | null = null; /** Reference to the toggle switch element */ asideSwitch: HTMLElement | null = null; /** Current visibility state */ asideMenuActive: string; /** * Creates a new AsideHandler instance. * * @param menuId - ID of the aside menu element * @param switchId - ID of the toggle button element * @param options - Initial state, persistence and callbacks */ constructor( menuId: string, switchId: string, options: AsideHandlerOptions = {}, ) { this.localStorageManager = LocalStorageManager.getInstance(); this.stateManager = new StateManager(); this.menuId = menuId; this.switchId = switchId; this.options = { ...options, visibleClass: options.visibleClass || AsideHandler.VISIBLE_CLASS, persist: options.persist !== false, }; this.asideMenuActive = options.initialState === AsideHandler.VISIBLE_STATE ? AsideHandler.VISIBLE_STATE : AsideHandler.HIDDEN_STATE; this.assertMenu(); this.setupToggleListener(); this.updateStateMenu(); } /** * Queries and caches DOM references for menu and switch elements. */ private assertMenu(): void { this.asideMenu = document.getElementById(this.menuId); this.asideSwitch = document.getElementById(this.switchId); } /** * Sets up click event listener on the toggle switch. */ private setupToggleListener(): void { if (this.asideSwitch) { this.asideSwitch.addEventListener("click", () => this.toggleMenu(), ); } } /** * Toggles the menu between visible and hidden states. */ public toggleMenu(): void { this.assertMenu(); if (this.asideMenu?.classList.contains(this.options.visibleClass)) { this.hideMenu(); } else { this.showMenu(); } } /** * Shows the menu and, when persisting, stores the state. */ public showMenu(): void { this.setState(AsideHandler.VISIBLE_STATE); } /** * Hides the menu and, when persisting, stores the state. */ public hideMenu(): void { this.setState(AsideHandler.HIDDEN_STATE); } /** * Records a new state, stores it when persisting, and applies it. */ private setState(state: string): void { this.assertMenu(); this.asideMenuActive = state; if (this.options.persist) { this.localStorageManager.setValue(this.storageKey(), state); } this.updateStateMenu(); } /** LocalStorage key of this menu's state. */ private storageKey(): string { return this.menuId + AsideHandler.VISIBLE_SUFFIX; } /** * Updates the visual state of the menu. When persisting, a stored state * wins over the current one; otherwise the current state applies. * Applies or removes the visible class on both menu and switch and * fires `onOpen` / `onClose` on a change. */ public updateStateMenu(): void { this.assertMenu(); if (!this.asideMenu) return; if (this.options.persist) { this.asideMenuActive = this.localStorageManager.getValue(this.storageKey()) || this.asideMenuActive; } const { visibleClass } = this.options; const wasVisible = this.asideMenu.classList.contains(visibleClass); const isVisible = this.asideMenuActive === AsideHandler.VISIBLE_STATE; this.asideMenu.classList.toggle(visibleClass, isVisible); this.asideSwitch?.classList.toggle(visibleClass, isVisible); this.asideSwitch?.setAttribute("aria-expanded", String(isVisible)); if (isVisible !== wasVisible) { (isVisible ? this.options.onOpen : this.options.onClose)?.( this.asideMenu, ); } } }