/** * @doeixd/dom * ========================================== * A production-grade, target-first, type-safe DOM library. * * ----------------------------------------------------------------------------- * 🧠 DESIGN PHILOSOPHY * ----------------------------------------------------------------------------- * 1. Target-First: `Action(Element)(Config)` pattern for intuitive chaining. * 2. Curried: Functions return closures for composition/piping. * 3. Null-Safe: All functions fail gracefully on `null`/`undefined` targets. * 4. Type-Safe: Full Generics for HTML Elements, Events, and Return types. * * ----------------------------------------------------------------------------- * 🌲 TREE-SHAKING / SIDE EFFECTS * ----------------------------------------------------------------------------- * This module is side-effect free: importing it must not touch the DOM, * register listeners/observers, or mutate globals. All work happens inside * exported functions. Top-level call-expression initializers (Proxies, `def` * wrappers, `createStorage`) are annotated `@__PURE__` so bundlers can drop * unused exports, and `package.json` declares `"sideEffects": false`. * Keep it that way: no module-level statements with observable effects. * * ----------------------------------------------------------------------------- * πŸ“š API DIRECTORY (27 MODULES) * ----------------------------------------------------------------------------- * * 🟒 DOM CORE * 1. Querying ......... find, findAll, closest * 2. Events ........... on, onDelegated, dispatch * 3. Manipulation ..... modify, css, tempStyle * 4. Structure ........ append, prepend, after, before, remove, wrap, mount * 5. Creation ......... el, html, htmlMany, clone * * πŸ”΅ STATE & ATTRIBUTES * 6. Classes .......... cls (add/remove/toggle), watchClass * 7. Attributes ....... Data (get/set/read/bind), watchAttr, watchText * 12. Objects ......... Obj (clone, isEqual, pick, omit) * 14. Refs ............ refs, groupRefs (data-ref handling) * 16. Cycling ......... cycleClass (State machines) * * 🟑 LIFECYCLE & OBSERVATION * 8. Lifecycle ........ onReady, onMount, waitFor * 17. Cleanup ......... stripListeners, instantiate, cloneMany * 20. Timing .......... debounce, throttle * 25. Groups .......... createListenerGroup (Batch cleanup) * 26. Signals ......... Signal (AbortController wrappers) * * 🟣 LAYOUT & NAVIGATION * 10. Navigation ...... Traverse (parent, children, siblings, next, prev, parents, nextAll, prevAll, closestAll) * 11. CSS Utils ....... CssVar, computed, injectStyles, waitTransition * 15. Color ........... toColorSpace (Color mix utils) * 18. Geometry ........ rect, offset, isVisible * 19. Scroll/Focus .... scrollInto, focus, blur * * 🟠 DATA & NETWORK * 9. URL/Form ......... Params, Form (serialize/populate) * 13. Collections ..... batch, groupBy * 21. Storage ......... Local, Session (Typed wrappers) * 22. Cookies ......... Cookie (get/set/remove) * 23. Network ......... Http (get/post/put/delete) * 24. PWA ............. SW (Service Worker reg/post) * 27. Pub/Sub ......... createBus (Typed Event Emitter) * * @module fdom * @author Patrick Glenn * @license MIT */ // ============================================================================= // 0. TYPES & HELPERS // ============================================================================= /** * Infers a DOM element type from a CSS selector string. * * Supports: * - Tag names: `'div'` β†’ `HTMLDivElement` * - SVG tags: `'svg'` β†’ `SVGSVGElement` * - ID selectors: `'#app'` β†’ `HTMLElement` * - Class selectors: `'.card'` β†’ `HTMLElement` * - Complex selectors: `'div.card'` β†’ `HTMLElement` * * @template S - The selector string literal type * * @example * ```typescript * type ButtonEl = ParseSelector<'button'>; // HTMLButtonElement * type AnchorEl = ParseSelector<'a'>; // HTMLAnchorElement * type SvgEl = ParseSelector<'svg'>; // SVGSVGElement * ``` */ export type ParseSelector = S extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[S] : S extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[S] : S extends `#${string}` ? HTMLElement : S extends `.${string}` ? HTMLElement : HTMLElement; /** * Input that can be an element, selector string, function returning element, or null. * Used by selector-enabled utilities to accept flexible input types. */ export type ElementInput = | ParseSelector // Direct element | S // String selector | (() => ParseSelector | null) // Function returning element | null; // Null /** * Function signature for selector-enabled utilities with dual-mode support. * Supports both immediate execution and curried application. */ export type SelectorFunction<_T extends HTMLElement, A extends any[], R> = { /** Immediate mode: all arguments provided at once */ (input: ElementInput, ...args: A): R; /** Curried mode: input provided first, returns function accepting remaining args */ (input: ElementInput): (...args: A) => R; }; /** * A cleanup/unsubscribe function returned by event listeners and subscriptions. * * Call this function to remove the listener and free resources. * * @example * ```typescript * const cleanup = on(button)('click', handler); * // Later... * cleanup(); // Removes the event listener * ``` */ export type Unsubscribe = () => void; /** * Event map for HTML elements, extensible for custom events. * * @template T - Additional custom event mappings * * @example * ```typescript * type MyEvents = EventMap<{ * 'custom:save': CustomEvent<{ id: number }>; * 'custom:delete': CustomEvent<{ id: number }>; * }>; * ``` */ export type EventMap = {}> = HTMLElementEventMap & T; /** * Extracts the detail type from a CustomEvent. * * @template T - The CustomEvent type * * @example * ```typescript * type SaveEvent = CustomEvent<{ id: number }>; * type Detail = ExtractEventDetail; // { id: number } * ``` */ export type ExtractEventDetail = T extends CustomEvent ? D : never; /** * Properties for creating/modifying elements. * * Supports declarative configuration of: * - Text content * - HTML content * - Inline styles * - Data attributes * - CSS classes * - HTML attributes * - Form element properties * * @example * ```typescript * const props: ElementProps = { * text: 'Click me', * class: { active: true, disabled: false }, * dataset: { userId: 123, role: 'admin' }, * style: { color: 'red', fontSize: '16px' }, * attr: { 'aria-label': 'Submit button' } * }; * ``` */ export interface ElementProps { /** Sets innerText (safer than html) */ text?: string; /** Sets innerHTML (use with caution - XSS risk) */ html?: string; /** Inline CSS styles */ style?: Partial; /** Data attributes (data-*) - auto-converts to kebab-case */ dataset?: Record; /** CSS classes with boolean toggles */ class?: Record; /** HTML attributes */ attr?: Record; /** Value for form inputs */ value?: string | number; /** Disabled state for form inputs */ disabled?: boolean; } /** * Strict element properties with element-specific validation. * * Provides better type safety by constraining properties based on element type. * * @template T - The HTML element type * * @example * ```typescript * const inputProps: StrictElementProps = { * value: 'test', * disabled: true, * attr: { type: 'text', placeholder: 'Enter name' } * }; * ``` */ export type StrictElementProps = ElementProps & { value?: T extends HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement ? string | number : never; disabled?: T extends HTMLInputElement | HTMLButtonElement | HTMLSelectElement | HTMLTextAreaElement ? boolean : never; }; /** * Makes all properties in T deeply readonly. * * @template T - The type to make readonly */ export type DeepReadonly = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; }; /** * Makes all properties in T deeply partial. * * @template T - The type to make partial */ export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; }; // ============================================================================= // NEW FEATURE TYPES // ============================================================================= /** * SVG element tag names that require special namespace handling in h() proxy. */ export type SVGElementTags = | 'svg' | 'g' | 'path' | 'circle' | 'rect' | 'line' | 'polygon' | 'polyline' | 'ellipse' | 'text' | 'tspan' | 'defs' | 'clipPath' | 'linearGradient' | 'radialGradient' | 'stop' | 'mask' | 'pattern' | 'marker' | 'symbol' | 'use' | 'image' | 'foreignObject'; /** * Extended ElementProps that includes dataRef support for h() proxy. * * @example * ```typescript * const props: HElementProps = { * class: { active: true }, * dataRef: 'myElement' // Will set data-ref="myElement" * }; * ``` */ export interface HElementProps extends ElementProps { /** Reference name for element extraction via refs() */ dataRef?: string; } /** * Options for configuring a List instance. * * @template T - The data item type * * @example * ```typescript * // Simple list (default blow-away mode) * const options: ListOptions = { * render: (item) => h.li({}, [item]) * }; * * // Keyed list (efficient diffing) * const options: ListOptions = { * key: user => user.id, * render: (user) => h.li({}, [user.name]), * update: (el, user) => { el.textContent = user.name; } * }; * ``` */ export interface ListOptions { /** Function to render each item to an element (required) */ render: (item: T, index: number) => HTMLElement; /** Optional key function - if provided, enables keyed reconciliation */ key?: (item: T) => string | number; /** Optional update function for efficient keyed updates */ update?: (element: HTMLElement, item: T, index: number) => void; /** Optional lifecycle hooks */ onRemove?: (element: HTMLElement, item: T) => void; onAdd?: (element: HTMLElement, item: T) => void; /** * Optional custom reconciliation function for full control. * When provided, this function is responsible for all DOM updates. * * @example * ```typescript * // Use morphdom for custom reconciliation * reconcile: (oldItems, newItems, container, renderFn) => { * const newHtml = newItems.map(renderFn).map(el => el.outerHTML).join(''); * morphdom(container, '
' + newHtml + '
'); * } * ``` */ reconcile?: ( oldItems: T[], newItems: T[], container: HTMLElement, renderFn: (item: T, index: number) => HTMLElement ) => void; } /** * Bound list instance with reactive update methods. * * @template T - The data item type */ export interface BoundList { /** Replace entire list with new items */ set(items: T[]): void; /** Append items to end of list */ append(items: T[]): void; /** Prepend items to start of list */ prepend(items: T[]): void; /** Insert items at specific index */ insert(index: number, items: T[]): void; /** Remove items matching predicate */ remove(predicate: (item: T) => boolean): void; /** Update items matching predicate */ update(predicate: (item: T) => boolean, updater: (item: T) => T): void; /** Clear all items */ clear(): void; /** Get current items array (readonly) */ items(): readonly T[]; /** Get current elements array (readonly) */ elements(): readonly HTMLElement[]; /** Destroy the list and cleanup */ destroy(): void; } /** * Options for configuring a viewRefs template instance. */ export interface ViewRefsOptions { /** Optional root element class names */ className?: string | string[]; /** Optional root element ID */ id?: string; /** Optional initial properties for root element */ props?: ElementProps; } /** * Context passed to viewRefs template factory. * * @template R - The refs shape */ export interface ViewRefsContext> { /** Extracted refs object (populated after template execution) */ refs: R; } /** * Instance returned by viewRefs factory. * * @template R - The refs shape */ export interface ViewRefsInstance> { /** The root element */ element: HTMLElement; /** Typed refs object */ refs: R; /** Update root element properties */ update(props: ElementProps): void; /** Update individual refs with smart value handling */ updateRefs(updates: Partial<{[K in keyof R]: any}>): void; /** Get a setter function for a specific ref */ bind(key: K): (value: any) => void; /** Destroy element and cleanup */ destroy(): void; } /** * Schema defining how refs map to setters. * * @template R - The refs shape */ export type BinderSchema> = { [K in keyof R]: Setter; }; /** * Infers the data shape from a binder schema. * * @template S - The binder schema type */ export type InferBinderData>> = { [K in keyof S]: S[K] extends Setter ? T : never; }; /** * Enhanced binder with batch updates and type-safe setters. * * @template R - The refs shape */ export interface EnhancedBinder> { /** Call with data object to update multiple refs */ (data: Partial>>): void; /** Individual setter functions */ set: BinderSchema; /** Batch multiple updates into single operation */ batch(fn: () => void): void; /** Get current refs object */ refs(): R; } /** * Primitive binding functions for common DOM operations. */ export interface BindPrimitives { /** Bind to textContent */ text(el: HTMLElement | null): Setter; /** Bind to innerHTML */ html(el: HTMLElement | null): Setter; /** Bind to attribute */ attr(name: string): (el: HTMLElement | null) => Setter; /** Bind to property */ prop(name: K): (el: HTMLElement | null) => Setter; /** Bind to CSS class toggle */ toggle(className: string): (el: HTMLElement | null) => Setter; /** Bind to multiple CSS classes */ classes(el: HTMLElement | null): Setter>; /** Bind to style properties */ style(el: HTMLElement | null): Setter>; /** Bind to form input value */ value(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null): Setter; /** Bind to element visibility */ show(el: HTMLElement | null): Setter; } /** * Internal: Normalizes content into an array of Nodes. * * Handles: * - Flattening nested arrays * - Filtering out null/undefined/false * - Converting strings to text nodes * - Preserving existing Node instances * * @internal */ /** * A renderable child for `el`/`h`: nodes and strings render; `null`, * `undefined`, and `false` are skipped (conditional rendering). */ export type HChild = string | Node | null | undefined | false; const _nodes = (args: any[]): Node[] => args.flat() .filter(x => x != null && x !== false) .map(x => x instanceof Node ? x : document.createTextNode(String(x))); /** * Hybrid Function Builder. * Creates a function that supports both Curried and Imperative usage. * * @template T - The Target type (Element, string, etc) * @template A - The Arguments tuple type * @template R - The Return type * * @example * const add = def((el: HTMLElement, cls: string) => el.classList.add(cls)); * * // Usage 1: Imperative (Cleaner DX) * add(div, 'active'); * * // Usage 2: Curried (Pipeline friendly) * pipe( * find('.btn'), * el => add(el)('active') * ); */ export const def = ( // The implementation function takes target + args all at once fn: (target: T | null, ...args: A) => R ) => { // Overload 1: Called with arguments -> Execute immediately function wrapper(target: T | null, ...args: A): R; // Overload 2: Called with just target -> Return curried function function wrapper(target: T | null): (...args: A) => R; // Implementation function wrapper(target: T | null, ...args: any[]) { // If we have extra args, run immediately if (args.length > 0) { // @ts-ignore - spread is safe here due to generics return fn(target, ...args); } // Otherwise return the closure return (...lateArgs: A) => fn(target, ...lateArgs); } return wrapper; }; // ============================================================================= // 1. QUERYING // ============================================================================= /** * Finds the first element matching the selector. * * Overloads allow calling in two ways: * 1. `find(root)(selector)` β€” search within a specific root (default: document) * 2. `find(selector)` β€” root is implicitly `document` * * @template S - CSS selector (literal string for best inference) * * @overload * @param selector - The selector to search for within `document` * @returns The matched element or `null`, inferred from selector * * @example * // String-first API * const btn = find("button"); // HTMLButtonElement | null * const app = find("#app"); // HTMLElement | null * * @overload * @param root - The root to search within (defaults to document) * @returns A function that accepts a selector and returns the matched element * * @example * // Curried API * const findIn = find(document.querySelector(".card")!); * const title = findIn("h1"); // HTMLHeadingElement | null */ export function find( selector: S ): ParseSelector | null; export function find(root?: ParentNode): (selector: S) => ParseSelector | null; export function find(arg: any) { // Case 1: string passed β€” treat as selector with implicit document root if (typeof arg === "string") { const selector = arg; return document.querySelector(selector) as any; } // Case 2: root passed β€” return curried selector function const root: ParentNode = arg ?? document; return (selector: S): ParseSelector | null => { return root.querySelector(selector) as ParseSelector | null; }; } /** * Finds an element or throws if not found. * * @template S - CSS selector * @param selector - The selector to search for * @param root - The root to search within (default: document) * @returns The matched element * @throws Error if element not found */ export function require(selector: S, root: ParentNode = document): ParseSelector { const el = root.querySelector(selector); if (!el) throw new Error(`Element not found: ${selector}`); return el as ParseSelector; } /** * Finds all elements matching the selector. * * Supports two call styles: * 1. `findAll(selector)` β€” searches `document` * 2. `findAll(root)(selector)` β€” searches a specific root * * @template S - CSS selector (literal string for best inference) * * @overload * @param selector - Selector to search for within `document` * @returns Array of matched elements (empty if none) * * @example * const items = findAll("li"); // HTMLLIElement[] * * @overload * @param root - The root to search within (defaults to document) * @returns Function that accepts a selector and returns an array of elements * * @example * const findInside = findAll(container); * const inputs = findInside("input"); // HTMLInputElement[] */ export function findAll( selector: S ): ParseSelector[]; export function findAll(root?: ParentNode): (selector: S) => ParseSelector[]; export function findAll(arg: any) { if (typeof arg === "string") { const selector = arg; return Array.from(document.querySelectorAll(selector)) as any[]; } const root: ParentNode = arg ?? document; return (selector: S): ParseSelector[] => { return Array.from(root.querySelectorAll(selector)) as ParseSelector[]; }; } /** * Finds the closest ancestor (including self) matching the selector. * * Supports: * 1. `closest(selector)` β€” uses `document.documentElement` as the starting point * 2. `closest(element)(selector)` β€” starts from a specific element * * Note: Using `closest(selector)` alone is rarely useful unless you * intentionally want to search from the root element. * * @template S - CSS selector (literal type for best inference) * * @overload * @param selector - Selector to match when starting at `document.documentElement` * @returns The matched ancestor or `null` * * @example * const htmlOrNull = closest("html"); * * @overload * @param element - Starting element (null-safe) * @returns Function accepting a selector that returns the matched ancestor * * @example * const card = closest(button)(".card"); // HTMLElement | null */ export function closest( selector: S ): ParseSelector | null; export function closest(element: Element | null): (selector: S) => ParseSelector | null; export function closest(arg: any) { if (typeof arg === "string") { const selector = arg; return document.documentElement.closest(selector) as any; } const element: Element | null = arg; return (selector: S): ParseSelector | null => { return element?.closest(selector) as ParseSelector | null; }; } /** * Checks whether an element matching the selector exists. * * Overloads: * 1. `exists(selector)` β€” searches `document` * 2. `exists(root)(selector)` β€” searches within a specific root * * @template S - CSS selector (literal for best inference) * * @overload * @param selector - Selector to test within `document` * @returns `true` if a matching element exists, otherwise `false` * * @example * exists("button"); // boolean * exists("#app"); // boolean */ export function exists( selector: S ): boolean; export function exists(root?: ParentNode): (selector: S) => boolean; export function exists(arg: any) { if (typeof arg === "string") { return document.querySelector(arg) !== null; } const root: ParentNode = arg ?? document; return (selector: S): boolean => { return root.querySelector(selector) !== null; }; } /** * Returns all siblings of an element (excluding the element itself). * * Overloads: * 1. `siblings(node)` β€” returns its siblings * 2. `siblings(root)(node)` β€” sibling list relative to a specific parent * * Null-safe: returns an empty array if `node` or parent is null. * * @example * const btn = document.querySelector("button"); * const sibs = siblings(btn); // Element[] * * @example * const list = document.querySelector("ul"); * const sibsOf = siblings(list); * sibsOf(list.querySelector("li")); */ export function siblings(node: Element | null): Element[]; export function siblings(root: ParentNode | null): (node: Element | null) => Element[]; export function siblings(arg: any): any { if (!(arg instanceof Element) && arg !== null) { const root: ParentNode | null = arg; return (node: Element | null): Element[] => { if (!root || !node) return []; return Array.from(root.children).filter(el => el !== node); }; } const node: Element | null = arg; if (!node || !node.parentElement) return []; return Array.from(node.parentElement.children).filter(el => el !== node); } /** * Checks whether a given element contains a descendant matching the selector. * * Overloads: * 1. `has(selector)` β€” checks within `document` * 2. `has(element)(selector)` β€” checks within a given element * * Null-safe: Passing `null` returns a function that always returns false. * * @template S - CSS selector string * * @overload * @param selector - Selector checked inside `document` * @returns `true` if a match exists, otherwise `false` * * @example * has(".card"); // boolean * * @overload * @param element - Element to test within (null-safe) * @returns Function testing a selector inside that element * * @example * const card = document.querySelector(".card"); * const result = has(card)("button"); */ export function has( selector: S ): boolean; export function has(element: ParentNode | null): (selector: S) => boolean; export function has(arg: any) { if (typeof arg === "string") { return document.querySelector(arg) !== null; } const root: ParentNode | null = arg; return (selector: S): boolean => { if (!root) return false; return root.querySelector(selector) !== null; }; } /** * Returns the index of a node among its siblings. * * Overloads: * 1. `index(node)` β€” returns the node's index or -1 * 2. `index(root)(node)` β€” curries the "list parent" (rare but consistent) * * Note: When called as `index(node)`, the parent is automatically the node's * actual parent element. * * @example * const item = document.querySelector("li"); * index(item); // 0, 1, 2, ... * * @example * // Curried * const list = document.querySelector("ul"); * index(list)(someLi); */ export function index(node: Element | null): number; export function index(root: ParentNode | null): (node: Element | null) => number; export function index(arg: any): any { // Case: Direct index(node) if (!(arg instanceof Element) && arg !== null) { // Treat as curried root const root: ParentNode | null = arg; return (node: Element | null): number => { if (!root || !node) return -1; const children = Array.from(root.children); return children.indexOf(node); }; } // Direct element case const node: Element | null = arg; if (!node || !node.parentElement) return -1; return Array.from(node.parentElement.children).indexOf(node); } // ============================================================================= // 2. EVENTS // ============================================================================= /** * Handler type for events */ type EventHandler = ( event: HTMLElementEventMap[K], target: T ) => void; /** * Event setup stage: attach listener with event type and handler */ type EventSetup = { ( eventType: K, handler: EventHandler, options?: boolean | AddEventListenerOptions ): Unsubscribe; }; /** * Attaches an event listener to the target element. * * Returns a cleanup function to remove the listener. Supports all standard * DOM events with full type inference. The handler receives both the event * and the target element for convenience. * * Supports multiple calling styles: * ```typescript * // Curried * on(button)('click', handler); * * // Imperative * on(button, 'click', handler); * ``` * * @example * ```typescript * const button = find('button'); * * // Curried - great for reuse * const onButton = on(button); * onButton('click', (e, target) => console.log('Clicked!', target)); * onButton('mouseenter', (e, target) => cls.add(target)('hovered')); * * // Imperative - concise one-offs * on(button, 'click', (e, target) => { * e.preventDefault(); * submit(); * }); * * // With options * on(window, 'scroll', handler, { passive: true }); * on(button)('click', handler, { once: true }); * * // Null-safe: returns no-op cleanup if target is null * const missing = find('.missing'); * on(missing, 'click', handler); // Safe, returns () => {} * * // Type inference works throughout: * on(input, 'input', (e) => { * console.log(e.data); // e is InputEvent * }); * * on(document, 'keydown', (e) => { * if (e.key === 'Escape') close(); // e is KeyboardEvent * }); * ``` */ // Overload: just target -> returns event setup export function on( target: T | null ): EventSetup; // Overload: all args -> returns Unsubscribe export function on( target: T | null, eventType: K, handler: EventHandler, options?: boolean | AddEventListenerOptions ): Unsubscribe; // Implementation export function on( target: T | null, eventType?: K, handler?: EventHandler, options?: boolean | AddEventListenerOptions ): EventSetup | Unsubscribe { // Core listener factory const createListener = ( evt: Evt, fn: EventHandler, opts: boolean | AddEventListenerOptions = false ): Unsubscribe => { if (!target) return () => { }; const listener = (e: Event) => fn(e as HTMLElementEventMap[Evt], target); target.addEventListener(evt, listener, opts); return () => target.removeEventListener(evt, listener, opts); }; // Event setup stage const eventSetup: EventSetup = ( evt: Evt, fn: EventHandler, opts?: boolean | AddEventListenerOptions ) => createListener(evt, fn, opts); // Route based on provided arguments if (eventType === undefined || handler === undefined) { // on(target) -> event setup return eventSetup; } // on(target, event, handler) -> Unsubscribe return createListener(eventType, handler, options); } /** * Handler type for delegated events */ type DelegatedHandler = ( event: HTMLElementEventMap[K], match: ParseSelector ) => void; /** * Final stage: attach event listener */ type DelegatedEventSetup = { ( eventType: K, handler: DelegatedHandler, options?: boolean | AddEventListenerOptions ): Unsubscribe; }; /** * Middle stage: select target elements * Supports both curried and imperative styles */ type DelegatedSelectorSetup = { // Curried: (selector) => (event, handler) => Unsubscribe (selector: S): DelegatedEventSetup; // Imperative: (selector, event, handler) => Unsubscribe ( selector: S, eventType: K, handler: DelegatedHandler, options?: boolean | AddEventListenerOptions ): Unsubscribe; }; /** * Attaches a **Delegated Event Listener** using event bubbling. * * Supports multiple calling styles: * ```typescript * // Fully curried * onDelegated(root)('li')('click', handler); * * // Partially curried * onDelegated(root, 'li')('click', handler); * onDelegated(root)('li', 'click', handler); * * // Fully imperative * onDelegated(root, 'li', 'click', handler); * ``` * * @example * ```typescript * const list = find('#user-list'); * * // Curried - great for reuse * const onListItem = onDelegated(list)('li'); * onListItem('click', (e, li) => console.log(li.textContent)); * onListItem('mouseenter', (e, li) => cls.add(li)('hovered')); * * // Imperative - concise one-offs * onDelegated(list, 'button.delete', 'click', (e, btn) => { * e.stopPropagation(); * deleteItem(btn.dataset.id); * }); * ``` */ // Overload: just root -> returns selector setup export function onDelegated( root: ParentNode | null ): DelegatedSelectorSetup; // Overload: root + selector -> returns event setup export function onDelegated( root: ParentNode | null, selector: S ): DelegatedEventSetup; // Overload: all args -> returns Unsubscribe export function onDelegated( root: ParentNode | null, selector: S, eventType: K, handler: DelegatedHandler, options?: boolean | AddEventListenerOptions ): Unsubscribe; // Implementation export function onDelegated( root: ParentNode | null, selector?: S, eventType?: K, handler?: DelegatedHandler, options?: boolean | AddEventListenerOptions ): DelegatedSelectorSetup | DelegatedEventSetup | Unsubscribe { // Core listener factory - the actual work happens here const createListener = ( sel: Sel, evt: Evt, fn: DelegatedHandler, opts: boolean | AddEventListenerOptions = false ): Unsubscribe => { if (!root) return () => { }; const listener = (e: Event) => { const target = e.target as Element; const match = target.closest?.(sel); if (match && root.contains(match)) { fn(e as HTMLElementEventMap[Evt], match as ParseSelector); } }; root.addEventListener(evt, listener, opts); return () => root.removeEventListener(evt, listener, opts); }; // Event setup stage: (eventType, handler, options?) => Unsubscribe const eventSetup = (sel: Sel): DelegatedEventSetup => { return (( evt: Evt, fn: DelegatedHandler, opts?: boolean | AddEventListenerOptions ) => createListener(sel, evt, fn, opts)) as DelegatedEventSetup; }; // Selector setup stage: supports both curried and imperative const selectorSetup = (( sel: Sel, evt?: Evt, fn?: DelegatedHandler, opts?: boolean | AddEventListenerOptions ) => { // Imperative: all args provided if (evt !== undefined && fn !== undefined) { return createListener(sel, evt, fn, opts); } // Curried: return event setup return eventSetup(sel); }) as DelegatedSelectorSetup; // Route based on provided arguments if (selector === undefined) { // onDelegated(root) -> selector setup return selectorSetup; } if (eventType === undefined || handler === undefined) { // onDelegated(root, selector) -> event setup return eventSetup(selector); } // onDelegated(root, selector, event, handler) -> Unsubscribe return createListener(selector, eventType, handler, options); } /** * Dispatches a CustomEvent from the target element. * * Creates and dispatches a CustomEvent with optional detail data. The detail * type is inferred from the provided data. By default, events bubble up the * DOM tree. * * @template T - The type of the detail data * @param target - The element to dispatch the event from (null-safe) * @returns A curried function that accepts event name, detail, and options * * @example * ```typescript * // Simple custom event * const button = document.querySelector('button'); * dispatch(button)('clicked', { timestamp: Date.now() }); * * // Typed custom events * interface SaveEvent { * id: number; * data: { name: string; email: string }; * } * dispatch(form)('save', { id: 123, data: formData } as SaveEvent); * * // Listen for custom events * on(button)('clicked' as any, (e: CustomEvent) => { * console.log('Detail:', e.detail); // { timestamp: ... } * }); * * // Non-bubbling event * dispatch(element)('custom', data, { bubbles: false }); * * // Cancelable event * dispatch(element)('beforeSave', data, { cancelable: true }); * * // Component communication pattern * const modal = document.querySelector('.modal'); * dispatch(modal)('modal:close', { reason: 'user-action' }); * * // Null-safe: does nothing if target is null * const missing = document.querySelector('.missing'); * dispatch(missing)('event', data); // Safe, no error * ``` */ export const dispatch = (target: EventTarget | null) => { return (eventName: string, detail?: T, options: EventInit = { bubbles: true }) => { if (target) { target.dispatchEvent(new CustomEvent(eventName, { detail, ...options })); } return target; }; }; // ============================================================================= // 3. MANIPULATION (MODIFY & STYLE) // ============================================================================= // ============================================================================= // 3. MANIPULATION (MODIFY & STYLE) // ============================================================================= /** * The core logic for applying properties to an element. Kept private. * @internal */ const _applyProps = (element: T | null, props: ElementProps): T | null => { if (!element) return null; if (props.text !== undefined) element.innerText = props.text; if (props.html !== undefined) element.innerHTML = props.html; if (props.value !== undefined) (element as any).value = props.value; if (props.disabled !== undefined) (element as any).disabled = props.disabled; if (props.style) Object.assign(element.style, props.style); if (props.dataset) { Object.entries(props.dataset).forEach(([k, v]) => { // Allow null/undefined to remove data attributes if (v === null || v === undefined) { delete element.dataset[k]; } else { element.dataset[k] = String(v); } }); } if (props.class) { Object.entries(props.class).forEach(([k, v]) => element.classList.toggle(k, !!v)); } if (props.attr) { Object.entries(props.attr).forEach(([k, v]) => { if (v === false || v === null || v === undefined) element.removeAttribute(k); else element.setAttribute(k, String(v)); }); } return element; }; /** * Declaratively modifies an element's properties with full type safety. * * Provides a unified API for setting text, styles, classes, attributes, and more. * Supports three calling styles for maximum flexibility. * * @template T - The HTML element type, which is preserved. * * @overload * **1. Config-First (Curried):** `modify(props)(element)` * Best for creating reusable modifiers in functional pipelines. * @param props - The properties to apply. * @returns A function that accepts an element and returns it. * @example * ```typescript * import { Fn, find } from '@doeixd/dom'; * * const makePrimary = modify({ * class: { 'btn-primary': true }, * attr: { 'aria-disabled': false } * }); * * Fn.pipe( * find('#submit-btn'), * makePrimary * ); * ``` * * @overload * **2. Element-First (Curried):** `modify(element)(props)` * Best for applying multiple, separate modifications to the same element. * @param element - The element to modify (null-safe). * @returns A function that accepts properties and returns the element. * @example * ```typescript * const btn = find('button'); * const modifyBtn = modify(btn); * * modifyBtn({ text: 'Step 1' }); * // ... later * modifyBtn({ class: { success: true }, text: 'Complete' }); * ``` * * @overload * **3. Imperative:** `modify(element, props)` * Best for simple, one-off modifications. Cleanest syntax. * @param element - The element to modify (null-safe). * @param props - The properties to apply. * @returns The modified element. * @example * ```typescript * const btn = find('button'); * modify(btn, { text: 'Click Me' }); * ``` */ export function modify(props: ElementProps): (element: T | null) => T | null; export function modify(element: T | null, props: ElementProps): T | null; export function modify(element: T | null): (props: ElementProps) => T | null; export function modify( arg1: any, arg2?: any ): any { const isElementFirst = arg1 instanceof HTMLElement || arg1 === null; // Case 1: Config-First -> modify(props) if (!isElementFirst) { const props = arg1; return (element: HTMLElement | null) => _applyProps(element, props); } // Case 2: Imperative -> modify(el, props) if (arg2 !== undefined) { const element = arg1; const props = arg2; return _applyProps(element, props); } // Case 3: Element-First Curried -> modify(el) const element = arg1; return (props: ElementProps) => _applyProps(element, props); } /** * Sets properties on an element. * @alias modify */ export const set = modify; /** * Applies inline CSS styles to an element. Supports multiple calling styles. * * @overload **1. Config-First:** `css(styles)(element)` * @param styles - The CSS styles to apply. * @returns A function that accepts an element. * * @overload **2. Element-First:** `css(element)(styles)` * @param element - The element to style. * @returns A function that accepts styles. * * @overload **3. Imperative:** `css(element, styles)` * @param element - The element to style. * @param styles - The CSS styles to apply. */ export function css( styles: Partial ): (element: T | null) => T | null; export function css( element: T | null, styles: Partial ): T | null; export function css( element: T | null ): (styles: Partial) => T | null; export function css( arg1: any, arg2?: any ): any { const isElementFirst = arg1 instanceof HTMLElement || arg1 === null; const applyCss = (element: T | null, styles: Partial) => { if (element) Object.assign(element.style, styles); return element; }; if (!isElementFirst) { const styles = arg1; return (element: HTMLElement | null) => applyCss(element, styles); } if (arg2 !== undefined) { const element = arg1; const styles = arg2; return applyCss(element, styles); } const element = arg1; return (styles: Partial) => applyCss(element, styles); } /** * Applies styles temporarily and returns a revert function. * * Saves the original style values and applies new ones. The returned function * restores the original values. Useful for temporary visual states like hover * effects or loading states. * * @param element - The element to style (null-safe) * @returns A curried function that accepts styles and returns a cleanup function * * @example * ```typescript * const div = document.querySelector('div'); * * // Temporarily change opacity * const revert = tempStyle(div)({ opacity: '0.5' }); * // Later... * revert(); // Restores original opacity * * // Loading state pattern * const showLoading = () => { * const revert = tempStyle(button)({ * opacity: '0.6', * pointerEvents: 'none', * cursor: 'wait' * }); * * fetchData().finally(() => { * revert(); // Restore original styles * }); * }; * * // Animation workflow * const element = document.querySelector('.box'); * const cleanup = tempStyle(element)({ * transform: 'scale(1.2)', * transition: 'transform 0.3s' * }); * setTimeout(cleanup, 300); // Revert after animation * * // Null-safe: returns no-op if element is null * const noop = tempStyle(null)({ color: 'red' }); // () => {} * ``` */ export const tempStyle = (element: HTMLElement | null) => { return (styles: Partial): Unsubscribe => { if (!element) return () => { }; const original: Record = {}; // Save original values Object.keys(styles).forEach((key) => { original[key] = element.style[key as keyof CSSStyleDeclaration] as string; }); Object.assign(element.style, styles); return () => Object.assign(element.style, original); }; }; // ============================================================================= // 4. STRUCTURE & TRAVERSAL // ============================================================================= /** * Appends content to the end of the target element. * * Accepts multiple arguments of mixed types (strings, Nodes, null, undefined). * Strings are automatically converted to text nodes. Null/undefined values are * filtered out. Returns the parent for chaining. * * @param parent - The parent element to append to (null-safe) * @returns A curried function that accepts content and returns the parent * * @example * ```typescript * const list = document.querySelector('ul'); * * // Imperative (cleaner DX) * append(list, item1, item2, item3); * * // Curried (pipeline friendly) * append(list)(item1, item2, item3); * * // Append a single element * const item = document.createElement('li'); * append(list)(item); * * // Mix elements and text * append(container)(heading, 'Some text', paragraph); * * // Append text nodes * append(div)('Hello', ' ', 'World'); * * // Null values are safely ignored * append(list)(item1, null, item2, undefined); // Only appends item1 and item2 * * // Chaining * const parent = append(container)(child1); * append(parent)(child2); * ``` */ export const append = /* @__PURE__ */ def((parent: HTMLElement | null, ...content: (string | Node | null | undefined)[]) => { parent?.append(..._nodes(content)); return parent; }); /** * Prepends content to the start of the target element. * * Inserts content at the beginning, before any existing children. Accepts * multiple arguments of mixed types. Returns the parent for chaining. * * @param parent - The parent element to prepend to (null-safe) * @returns A curried function that accepts content and returns the parent * * @example * ```typescript * const list = document.querySelector('ul'); * * // Imperative (cleaner DX) * prepend(list, firstItem); * * // Curried (pipeline friendly) * prepend(list)(firstItem); * * // Add header before content * const container = document.querySelector('.container'); * const header = document.createElement('h1'); * prepend(container)(header); * ``` */ export const prepend = /* @__PURE__ */ def((parent: HTMLElement | null, ...content: (string | Node | null | undefined)[]) => { parent?.prepend(..._nodes(content)); return parent; }); /** * Inserts content AFTER the target element as siblings. * * The content is inserted after the target in the DOM tree, at the same level. * Useful for inserting elements without modifying the target's children. * * @param target - The reference element (null-safe) * @returns A curried function that accepts content and returns the target * * @example * ```typescript * const header = document.querySelector('h1'); * const banner = document.createElement('div'); * * // Imperative (cleaner DX) * after(header, banner, notice, alert); * * // Curried (pipeline friendly) * after(header)(banner); * * // Insert multiple elements * after(header)(banner, notice, alert); * ``` */ export const after = /* @__PURE__ */ def((target: Element | null, ...content: (string | Node | null | undefined)[]) => { target?.after(..._nodes(content)); return target; }); /** * Inserts content BEFORE the target element as siblings. * * The content is inserted before the target in the DOM tree, at the same level. * * @param target - The reference element (null-safe) * @returns A curried function that accepts content and returns the target * * @example * ```typescript * const footer = document.querySelector('footer'); * const disclaimer = document.createElement('p'); * * // Imperative (cleaner DX) * before(footer, disclaimer); * * // Curried (pipeline friendly) * before(footer)(disclaimer); * ``` */ export const before = /* @__PURE__ */ def((target: Element | null, ...content: (string | Node | null | undefined)[]) => { target?.before(..._nodes(content)); return target; }); /** * Removes the target element from the DOM. * * Detaches the element from its parent. Event listeners attached via * addEventListener will be garbage collected. Always returns `null` for * type safety (prevents accidental reuse of removed elements). * * @param target - The element to remove (null-safe) * @returns Always returns `null` * * @example * ```typescript * const modal = document.querySelector('.modal'); * remove(modal); // Modal is removed from DOM * * // Conditional removal * if (shouldRemove) { * remove(element); * } * ``` */ export const remove = (target: Element | null) => { target?.remove(); return null; }; /** * Removes all children from the target element. * * More efficient than `innerHTML = ''` and safer (doesn't parse HTML). * Returns the target for chaining. * * @param target - The element to empty (null-safe) * @returns The target element * * @example * ```typescript * const container = document.querySelector('.container'); * * // Clear all content * empty(container); * * // Then add new content * append(empty(container))(newContent); * ``` */ export const empty = (target: Element | null) => { if (target) target.replaceChildren(); return target; }; /** * Wraps the target element with a wrapper element. * * Inserts the wrapper before the target in the DOM, then moves the target * inside the wrapper. Useful for adding container elements around existing * content. * * @param target - The element to wrap (null-safe) * @returns A curried function that accepts a wrapper and returns it * * @example * ```typescript * const img = document.querySelector('img'); * const figure = document.createElement('figure'); * * // Imperative (cleaner DX) * wrap(img, figure); * * // Curried (pipeline friendly) * wrap(img)(figure); * // DOM:
* * // Using el() helper * wrap(img)(el('figure')({})([])); * * // Add caption to wrapper * const wrapper = wrap(img)(figure); * append(wrapper)(el('figcaption')({})(['Image caption'])); * ``` */ export const wrap = /* @__PURE__ */ def((target: HTMLElement | null, wrapper: HTMLElement) => { if (target && wrapper && target.parentNode) { target.parentNode.insertBefore(wrapper, target); wrapper.appendChild(target); } return wrapper; }); /** * Mounts a child element into a parent container. * * Appends the child to the parent and returns a cleanup function to remove it. * Useful for dynamic DOM updates, modals, popovers, and temporary UI elements. * * Supports two call styles: * 1. `mount(parent, child)` β€” Imperative (cleaner DX) * 2. `mount(parent)(child)` β€” Curried (pipeline friendly) * * @overload * @param parent - Parent element or selector (null-safe) * @param child - Child element to mount (or null for curried) * @returns Cleanup function to unmount the child, or no-op if parent not found * * @example * ```typescript * // Imperative style * const modal = document.createElement('div'); * modal.textContent = 'Hello World'; * const cleanup = mount(document.body, modal); * * // Later: remove the element * cleanup(); * * // Using selector-first API * const popup = el('div')({ class: { popup: true } })(['Content']); * const remove = mount(".container")(popup); * * // Mounting multiple elements * const list = document.querySelector('ul'); * const items = [el('li')({})(['Item 1']), el('li')({})(['Item 2'])]; * const cleanups = items.map(item => mount(list)(item)); * * // With temporary modal * const showModal = (content: string) => { * const modal = el('div')({ * class: { modal: true }, * attr: { role: 'dialog' } * })([content]); * * const cleanup = mount(document.body)(modal); * * // Auto-cleanup on button click * modal.addEventListener('click', () => cleanup()); * return cleanup; * }; * ``` */ export const mount = /* @__PURE__ */ def((parent: Element | string | null, child: Element | null): Unsubscribe => { if (!child) return () => { }; const parentEl = typeof parent === 'string' ? document.querySelector(parent) : parent; if (!parentEl) return () => { }; parentEl.appendChild(child); // Return cleanup function return () => { if (child.parentNode === parentEl) { parentEl.removeChild(child); } }; }); // ============================================================================= // 5. CREATION & TEMPLATES // ============================================================================= export interface CreateWebComponentOptions { /** Optional custom element name (overrides class name). */ name?: string; /** Auto-define the component (default: true). */ define?: boolean; /** Options passed to `customElements.define`. */ defineOptions?: ElementDefinitionOptions; } /** * Registers a custom element with smart defaults and flexible call styles. * * Defaults to a kebab-case name derived from the class name. If the derived * name lacks a hyphen, `-el` is appended to make it valid. * * Supports both call styles: * - `createWebComponent(MyElement, options?)` * - `createWebComponent(options?)(MyElement)` * * @template T - Custom element constructor * @param ctor - Custom element class * @param options - Optional registration configuration * @returns Metadata about the registration * * @example * ```typescript * class MyButton extends HTMLElement {} * const { name } = createWebComponent(MyButton); * // name === 'my-button' * ``` * * @example * ```typescript * class FancyInput extends HTMLInputElement {} * createWebComponent(FancyInput, { * name: 'fancy-input', * defineOptions: { extends: 'input' } * }); * ``` * * @example * ```typescript * class ModalDialog extends HTMLElement {} * const register = createWebComponent({ define: false, name: 'modal-dialog' }); * const info = register(ModalDialog); * // info.defined === false * ``` */ export function createWebComponent( ctor: T, options?: CreateWebComponentOptions ): { name: string; ctor: T; defined: boolean }; export function createWebComponent( options?: CreateWebComponentOptions ): (ctor: T) => { name: string; ctor: T; defined: boolean }; export function createWebComponent( arg1?: CreateWebComponentOptions | CustomElementConstructor, arg2?: CreateWebComponentOptions ) { const toKebab = (value: string) => value .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') .replace(/[_\s]+/g, '-') .toLowerCase(); const build = ( ctor: T, options: CreateWebComponentOptions = {} ) => { const rawName = options.name ?? toKebab(ctor.name ?? ''); if (!rawName) { throw new Error('createWebComponent: name is required for anonymous classes.'); } const name = rawName.includes('-') ? rawName : `${rawName}-el`; const shouldDefine = options.define !== false; const registry = typeof customElements === 'undefined' ? undefined : customElements; const alreadyDefined = registry?.get(name); if (shouldDefine) { if (!registry) { throw new Error('createWebComponent: customElements is not available in this environment.'); } if (!alreadyDefined) { registry.define(name, ctor, options.defineOptions); } } return { name, ctor, defined: shouldDefine ? true : !!alreadyDefined }; }; if (typeof arg1 === 'function') { return build(arg1, arg2); } const options = arg1 ?? {}; return (ctor: CustomElementConstructor) => build(ctor, options); } /** * Creates a DOM element with full type inference. * * Supports two syntaxes: * 1. **Hyperscript-style**: `el(tag, props, children)` β€” cleaner, more readable * 2. **Curried**: `el(tag)(props)(children)` β€” composable, pipeline-friendly * * The return type is automatically inferred from the tag name. * * @template K - The HTML tag name (keyof HTMLElementTagNameMap) * @param tag - The HTML tag name (e.g., 'div', 'button', 'a') * @param props - Optional properties (text, classes, attributes, etc.) * @param children - Optional children (elements or text) * @returns The created element (Hyperscript) or curried function (Curried) * * @example * ```typescript * // Hyperscript-style (new, cleaner) * const btn = el('button', { class: { primary: true } }, ['Click me']); * // btn is typed as HTMLButtonElement * * // Nested elements (much more readable) * const card = el('div', { class: { card: true } }, [ * el('h2', {}, ['Title']), * el('p', {}, ['Description']) * ]); * * // Curried syntax (still supported for backward compatibility) * const link = el('a')({ * attr: { href: '/home' }, * class: { active: true }, * text: 'Home' * })([]); * // link is typed as HTMLAnchorElement * * // Form input with type inference * const input = el('input', { * attr: { type: 'text', placeholder: 'Enter name' }, * value: 'John' * }, []); * // input is typed as HTMLInputElement * * // Partial application for reuse (curried) * const createButton = el('button'); * const primaryBtn = createButton({ class: { primary: true } })(['Save']); * const secondaryBtn = createButton({ class: { secondary: true } })(['Cancel']); * ``` */ export function el( tag: K, props: ElementProps, children: HChild[] ): HTMLElementTagNameMap[K]; export function el( tag: K ): (props?: ElementProps) => (children?: HChild[]) => HTMLElementTagNameMap[K]; export function el( tag: K, props?: ElementProps, children?: HChild[] ): any { // Hyperscript-style: el(tag, props, children) if (props !== undefined && children !== undefined) { const node = document.createElement(tag); modify(node)(props); node.append(..._nodes(children)); return node; } // Curried syntax: el(tag)(props)(children) return (propsArg: ElementProps = {}) => { return (childrenArg: HChild[] = []): HTMLElementTagNameMap[K] => { const node = document.createElement(tag); modify(node)(propsArg); node.append(..._nodes(childrenArg)); return node; }; }; } /** * Creates an element from an HTML template string. * * Uses tagged template literals for convenient HTML creation. Interpolated * values are automatically escaped. Returns the first element in the template. * * ⚠️ **XSS Warning**: Only use with trusted content. Do not interpolate * user input directly without sanitization. * * @param strings - Template string parts * @param values - Interpolated values * @returns The created HTMLElement * @throws Error if template doesn't produce an element * * @example * ```typescript * // Basic usage * const div = html`
Hello
`; * * // With interpolation * const name = 'World'; * const greeting = html`

Hello ${name}!

`; * * // Complex structure * const card = html` *
*

${title}

*

${description}

*
* `; * * // ⚠️ UNSAFE - Don't do this with user input! * // const unsafe = html`
${userInput}
`; * * // βœ… SAFE - Sanitize user input first * const safe = html`
${sanitize(userInput)}
`; * ``` */ export const html = (strings: TemplateStringsArray, ...values: any[]): HTMLElement => { const str = strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), ''); const tpl = document.createElement('template'); tpl.innerHTML = str.trim(); const el = tpl.content.firstElementChild; if (!el) throw new Error('html: Template did not result in an element'); return el as HTMLElement; }; /** * Creates a DocumentFragment from an HTML template string. * * Like `html()` but returns a DocumentFragment containing all elements * from the template. Useful for creating multiple sibling elements at once. * * ⚠️ **XSS Warning**: Only use with trusted content. * * @param strings - Template string parts * @param values - Interpolated values * @returns A DocumentFragment containing the created elements * * @example * ```typescript * // Create multiple list items * const items = htmlMany` *
  • Item 1
  • *
  • Item 2
  • *
  • Item 3
  • * `; * * const list = document.querySelector('ul'); * list.appendChild(items); * * // With interpolation * const rows = htmlMany` * ${col1}${col2} * ${col3}${col4} * `; * ``` */ export const htmlMany = (strings: TemplateStringsArray, ...values: any[]): DocumentFragment => { const str = strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), ''); const tpl = document.createElement('template'); tpl.innerHTML = str.trim(); return tpl.content; }; /** * Clones a node, preserving its exact type. * * Creates a copy of the node and optionally its descendants. The cloned node * has no parent and is not part of the document. Event listeners are NOT * copied. * * @template T - The node type (preserved in return type) * @param node - The node to clone (null-safe) * @returns A curried function that accepts deep flag and returns the clone * * @example * ```typescript * const button = document.querySelector('button'); * * // Deep clone (includes children) * const btnCopy = clone(button)(true); * // btnCopy is typed as HTMLButtonElement * * // Shallow clone (no children) * const btnShallow = clone(button)(false); * * // Template pattern * const template = el('div')({ class: { card: true } })([ * el('h2')({})(['Title']), * el('p')({})(['Description']) * ]); * * // Create multiple cards from template * const card1 = clone(template)(true); * const card2 = clone(template)(true); * const card3 = clone(template)(true); * * // Null-safe * const missing = document.querySelector('.missing'); * const result = clone(missing)(true); // null * ``` */ export const clone = (node: T | null) => { return (deep: boolean = true): T | null => { return node ? (node.cloneNode(deep) as T) : null; }; }; /** * SVG element tags that require special namespace handling. * All tags are stored in lowercase for case-insensitive matching. * @internal */ const svgElementTags = new Set([ 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline', 'ellipse', 'text', 'tspan', 'defs', 'clippath', 'lineargradient', 'radialgradient', 'stop', 'mask', 'pattern', 'marker', 'symbol', 'use', 'image', 'foreignobject' ]); /** * VanJS-style hyperscript proxy for element creation. * * Provides a Proxy-based API where property access creates element factories. * Supports both HTML and SVG elements with automatic namespace detection. * * **Performance Note**: The Proxy has minimal overhead (~5% vs direct el() calls). * For performance-critical loops with 1000s of elements, prefer el() directly. * * **Type Safety**: Returns `HTMLElement` rather than specific element types due to * Proxy limitations. Use type assertions if you need specific element types. * * @example * ```typescript * import { h } from '@doeixd/dom'; * * // Basic HTML elements * const card = h.div({ class: { card: true } }, [ * h.h2({}, ['Title']), * h.p({ text: 'Content' }), * h.button({ dataRef: 'submit' }, ['Submit']) * ]); * * // SVG elements (automatically use SVG namespace) * const icon = h.svg({ attr: { viewBox: '0 0 24 24', width: '24', height: '24' } }, [ * h.path({ attr: { d: 'M12 2L2 7v10c0 5.5 3.8 10.7 10 12 6.2-1.3 10-6.5 10-12V7l-10-5z' } }) * ]); * * // Nested structures * const list = h.ul({ class: { 'todo-list': true } }, [ * h.li({}, ['Item 1']), * h.li({}, ['Item 2']), * h.li({}, ['Item 3']) * ]); * * // With refs for later access * const form = h.form({}, [ * h.input({ dataRef: 'name', attr: { type: 'text', placeholder: 'Name' } }), * h.input({ dataRef: 'email', attr: { type: 'email', placeholder: 'Email' } }), * h.button({ dataRef: 'submit', attr: { type: 'submit' } }, ['Submit']) * ]); * * // Extract refs * const formRefs = refs(form); * console.log(formRefs.name, formRefs.email, formRefs.submit); * ``` */ export const h = /* @__PURE__ */ new Proxy({} as Record HTMLElement>, { get(_target, tag: string) { if (typeof tag !== 'string') return undefined; // Validate tag name (alphanumeric, starting with letter) if (!/^[a-z][a-z0-9]*$/i.test(tag)) { throw new Error(`h: Invalid tag name "${tag}". Tag names must start with a letter and contain only letters and numbers.`); } return (props: HElementProps = {}, children: (string | Node)[] = []) => { // Extract dataRef prop separately to avoid passing it to modify() const { dataRef, ...restProps } = props; // Create element with appropriate namespace const isSVG = svgElementTags.has(tag.toLowerCase()); const element = isSVG ? document.createElementNS('http://www.w3.org/2000/svg', tag) : document.createElement(tag); // Apply properties if (Object.keys(restProps).length > 0) { // For SVG elements, handle common props directly to ensure proper namespace handling if (isSVG) { // Handle attributes if (restProps.attr) { Object.entries(restProps.attr).forEach(([key, value]) => { if (value === false || value === null || value === undefined) { element.removeAttribute(key); } else { element.setAttribute(key, String(value)); } }); } // Handle classes if (restProps.class) { Object.entries(restProps.class).forEach(([className, isActive]) => { if (isActive) { element.classList.add(className); } else { element.classList.remove(className); } }); } // Handle style if (restProps.style) { Object.assign((element as HTMLElement).style, restProps.style); } // Handle text and html if (restProps.text !== undefined) { element.textContent = restProps.text; } if (restProps.html !== undefined) { element.innerHTML = restProps.html; } } else { // For HTML elements, use modify for everything modify(element as HTMLElement, restProps); } } // Set data-ref attribute if provided if (dataRef) { element.setAttribute('data-ref', dataRef); } // Append children if any if (children.length > 0) { element.append(..._nodes(children)); } return element as HTMLElement; }; } }); /** * Alias for `h` proxy. Provides alternative naming for hyperscript-style element creation. * * Some developers prefer `tags` as it's more explicit about creating HTML tags. * Functionally identical to `h`. * * @example * ```typescript * import { tags } from '@doeixd/dom'; * * const page = tags.div({ class: { container: true } }, [ * tags.header({}, [tags.h1({}, ['My App'])]), * tags.main({}, [tags.p({}, ['Content'])]), * tags.footer({}, [tags.small({}, ['Β© 2024'])]) * ]); * ``` */ export const tags = h; // ============================================================================= // 6. CLASS MANIPULATION // ============================================================================= /** * Utilities for manipulating CSS classes on elements. * * All methods are null-safe and return the element for chaining (except `has`). * Provides a functional API for common classList operations. * * @example * ```typescript * const button = document.querySelector('button'); * * // Add multiple classes * cls.add(button)('btn', 'btn-primary', 'active'); * * // Remove classes * cls.remove(button)('disabled', 'loading'); * * // Toggle with optional force * cls.toggle(button)('active'); // Toggles * cls.toggle(button)('active', true); // Forces add * cls.toggle(button)('active', false); // Forces remove * * // Replace a class * cls.replace(button)('btn-primary', 'btn-secondary'); * * // Check for class * if (cls.has(button)('active')) { * console.log('Button is active'); * } * * // Chaining * cls.add(button)('btn'); * cls.toggle(button)('active'); * ``` */ export const cls = { /** * Adds one or more CSS classes to the element. * * @param el - The element to add classes to (null-safe) * @returns A curried function that accepts class names and returns the element * * @example * ```typescript * // Imperative (cleaner DX) * cls.add(btn, 'active', 'shadow'); * * // Curried (pipeline friendly) * cls.add(btn)('active', 'shadow'); * * // Add single class * cls.add(div)('active'); * * // Add multiple classes * cls.add(div)('card', 'shadow', 'rounded'); * * // Null-safe * cls.add(null)('active'); // Returns null * ``` */ add: def((el: Element | null, ...classes: string[]) => { el?.classList.add(...classes); return el; }), /** * Removes one or more CSS classes from the element. * * @param el - The element to remove classes from (null-safe) * @returns A curried function that accepts class names and returns the element * * @example * ```typescript * // Imperative (cleaner DX) * cls.remove(btn, 'active', 'shadow'); * * // Curried (pipeline friendly) * cls.remove(btn)('active', 'shadow'); * * // Remove single class * cls.remove(div)('active'); * * // Remove multiple classes * cls.remove(div)('loading', 'disabled', 'error'); * * // Safe if class doesn't exist * cls.remove(div)('nonexistent'); // No error * ``` */ remove: def((el: Element | null, ...classes: string[]) => { el?.classList.remove(...classes); return el; }), /** * Toggles a CSS class on the element. * * @param el - The element to toggle the class on (null-safe) * @returns A curried function that accepts class name and optional force flag * * @example * ```typescript * // Imperative (cleaner DX) * cls.toggle(btn, 'active'); * cls.toggle(btn, 'active', true); // Force add * * // Curried (pipeline friendly) * cls.toggle(btn)('active'); // Adds if absent, removes if present * cls.toggle(btn)('active', true); // Always adds * cls.toggle(btn)('active', false); // Always removes * * // Conditional toggle * cls.toggle(button)('disabled', isLoading); * ``` */ toggle: def((el: Element | null, className: string, force?: boolean) => { el?.classList.toggle(className, force); return el; }), /** * Replaces an old class with a new class. * * @param el - The element to modify (null-safe) * @returns A curried function that accepts old and new class names * * @example * ```typescript * // Imperative (cleaner DX) * cls.replace(btn, 'btn-primary', 'btn-secondary'); * * // Curried (pipeline friendly) * cls.replace(btn)('btn-primary', 'btn-secondary'); * * // Replace theme class * cls.replace(div)('theme-light', 'theme-dark'); * * // No effect if old class doesn't exist * cls.replace(div)('nonexistent', 'new'); // No change * ``` */ replace: def((el: Element | null, oldClass: string, newClass: string) => { el?.classList.replace(oldClass, newClass); return el; }), /** * Checks if the element has a specific class. * * @param el - The element to check (null-safe) * @returns A curried function that accepts a class name and returns boolean * * @example * ```typescript * const button = document.querySelector('button'); * * // Check for class * if (cls.has(button)('active')) { * console.log('Button is active'); * } * * // Conditional logic * const isDisabled = cls.has(button)('disabled'); * * // Null-safe: returns false if element is null * cls.has(null)('active'); // false * ``` */ has: (el: Element | null) => (className: string) => { return !!el && el.classList.contains(className); } }; /** * Observes changes to a specific class on an element. * * Uses MutationObserver to watch for class attribute changes. The callback * fires only when the specified class is added or removed (not on other class * changes). Returns a cleanup function to stop observing. * * **Performance**: Uses attribute filtering for efficiency. Consider debouncing * the callback if rapid changes are expected. * * @param target - The element to observe (null-safe) * @returns A curried function that accepts class name and callback, returns cleanup function * * @example * ```typescript * const modal = document.querySelector('.modal'); * * // Imperative (cleaner DX) * const cleanup = watchClass(modal, 'open', (isPresent, el) => { * if (isPresent) { * console.log('Modal opened'); * document.body.style.overflow = 'hidden'; * } else { * console.log('Modal closed'); * document.body.style.overflow = ''; * } * }); * * // Curried (pipeline friendly) * const cleanup = watchClass(modal)('open', (isPresent, el) => { * if (isPresent) { * console.log('Modal opened'); * document.body.style.overflow = 'hidden'; * } else { * console.log('Modal closed'); * document.body.style.overflow = ''; * } * }); * * // Later: stop watching * cleanup(); * * // Watch loading state * watchClass(button)('loading', (isLoading) => { * button.disabled = isLoading; * }); * * // Sync state between elements * watchClass(sidebar)('collapsed', (isCollapsed) => { * cls.toggle(mainContent)('expanded', isCollapsed); * }); * * // Null-safe: returns no-op cleanup * const noop = watchClass(null)('active', callback); // () => {} * ``` */ export const watchClass = /* @__PURE__ */ def((target: Element | null, className: string, callback: (isPresent: boolean, el: Element) => void): Unsubscribe => { if (!target) return () => { }; let was = target.classList.contains(className); const obs = new MutationObserver(() => { const is = target.classList.contains(className); if (is !== was) { was = is; callback(is, target); } }); obs.observe(target, { attributes: true, attributeFilter: ['class'] }); return () => obs.disconnect(); }); // ============================================================================= // 7. DATASET & ATTRIBUTES // ============================================================================= const toDataAttr = (str: string) => 'data-' + str.replace(/[A-Z]/g, m => "-" + m.toLowerCase()); /** * Utilities for working with data attributes (data-*). * * Provides a functional API for getting, setting, and observing data attributes. * Automatically handles type conversion (numbers, booleans, JSON) and camelCase * to kebab-case conversion. * * @example * ```typescript * const div = document.querySelector('div'); * * // Set data attributes * Data.set(div)('userId', 123); // data-user-id="123" * Data.set(div)('isActive', true); // data-is-active="true" * Data.set(div)('config', { a: 1 }); // data-config='{"a":1}' * * // Get raw string value * const userId = Data.get(div)('userId'); // "123" * * // Read with type inference * const id = Data.read(div)('userId'); // 123 (number) * const active = Data.read(div)('isActive'); // true (boolean) * const config = Data.read(div)('config'); // { a: 1 } (object) * * // React to changes * Data.bind(div)('count', (value) => { * console.log('Count changed:', value); * }); * ``` */ export const Data = { /** * Gets the raw string value of a data attribute. * * Returns the value as-is from the dataset. For type conversion, use `read()`. * * @template T - The element type (inferred) * @param el - The element to get data from (null-safe) * @returns A curried function that accepts a key and returns the value or undefined * * @example * ```typescript * const div = document.querySelector('div'); * div.dataset.userId = '123'; * * // Get raw value * const userId = Data.get(div)('userId'); // "123" (string) * * // CamelCase key * const userName = Data.get(div)('userName'); // Accesses data-user-name * * // Missing attribute * const missing = Data.get(div)('missing'); // undefined * * // Null-safe * Data.get(null)('userId'); // undefined * ``` */ get: (el: HTMLElement | null) => (key: string) => el?.dataset[key], /** * Sets a data attribute value. * * Automatically converts objects to JSON strings and handles null/undefined * by removing the attribute. CamelCase keys are converted to kebab-case. * * @template T - The element type (inferred) * @param el - The element to set data on (null-safe) * @returns A curried function that accepts key and value, returns the element * * @example * ```typescript * const div = document.querySelector('div'); * * // Imperative (cleaner DX) * Data.set(div, 'userId', '123'); * * // Curried (pipeline friendly) * Data.set(div)('userId', '123'); * * // Set number (converted to string) * Data.set(div)('count', 42); // data-count="42" * * // Set boolean * Data.set(div)('isActive', true); // data-is-active="true" * * // Set object (JSON stringified) * Data.set(div)('config', { theme: 'dark', size: 'lg' }); * // data-config='{"theme":"dark","size":"lg"}' * * // Remove attribute (null or undefined) * Data.set(div)('userId', null); // Removes data-user-id * * // CamelCase to kebab-case * Data.set(div)('userName', 'John'); // Sets data-user-name="John" * * // Chaining * Data.set(div)('id', 1); * Data.set(div)('name', 'Item'); * ``` */ set: def((el: HTMLElement | null, key: string, val: any) => { if (!el) return el; if (val == null) delete el.dataset[key]; else el.dataset[key] = typeof val === 'object' ? JSON.stringify(val) : String(val); return el; }), /** * Reads a data attribute with automatic type inference. * * Intelligently parses the value: * - `"true"` β†’ `true` (boolean) * - `"false"` β†’ `false` (boolean) * - `"null"` β†’ `null` * - `"123"` β†’ `123` (number) * - `'{"a":1}'` β†’ `{a:1}` (parsed JSON) * - Other β†’ string * * @template T - The expected return type * @param el - The element to read from (null-safe) * @returns A curried function that accepts a key and returns the parsed value * * @example * ```typescript * const div = document.querySelector('div'); * * // Boolean parsing * div.dataset.isActive = 'true'; * Data.read(div)('isActive'); // true (boolean) * * // Number parsing * div.dataset.count = '42'; * Data.read(div)('count'); // 42 (number) * * // JSON parsing * div.dataset.config = '{"theme":"dark"}'; * Data.read(div)('config'); // { theme: 'dark' } * * // String fallback * div.dataset.name = 'John'; * Data.read(div)('name'); // "John" (string) * * // Missing attribute * Data.read(div)('missing'); // undefined * * // Type-safe usage * interface Config { theme: string; size: string; } * const config = Data.read(div)('config'); * ``` */ read: (el: HTMLElement | null) => (key: string): any => { if (!el || !(key in (el.dataset || {}))) return undefined; const val = el.dataset[key]!; if (val === 'true') return true; if (val === 'false') return false; if (val === 'null') return null; if (!isNaN(Number(val)) && val.trim() !== '') return Number(val); if (val.startsWith('{') || val.startsWith('[')) { try { return JSON.parse(val); } catch { return val; } } return val; }, /** * Observes changes to a data attribute and fires a callback. * * Uses MutationObserver to watch for attribute changes. The callback fires * immediately with the current value, then on every change. Values are * automatically parsed using `Data.read()`. * * @template T - The expected value type * @param el - The element to observe (null-safe) * @returns A curried function that accepts key and callback, returns cleanup function * * @example * ```typescript * const div = document.querySelector('div'); * * // Imperative (cleaner DX) * const cleanup = Data.bind(div, 'count', (value, el) => { * console.log('Count is now:', value); * }); * * // Curried (pipeline friendly) * const cleanup = Data.bind(div)('count', (value, el) => { * console.log('Count is now:', value); * // Fires immediately with current value * // Then fires on every change * }); * * // Later: stop watching * cleanup(); * * // Form validation example * Data.bind(input)('validationError', (error) => { * if (error) { * errorDisplay.textContent = error; * errorDisplay.style.display = 'block'; * } else { * errorDisplay.style.display = 'none'; * } * }); * * // Sync state between components * Data.bind(slider)('value', (value) => { * valueDisplay.textContent = String(value); * }); * * // Null-safe: returns no-op cleanup * const noop = Data.bind(null)('key', callback); // () => {} * ``` */ bind: def((el: HTMLElement | null, key: string, callback: (val: any, el: HTMLElement) => void): Unsubscribe => { if (!el) return () => { }; const attr = toDataAttr(key); const update = () => callback(Data.read(el)(key), el); update(); // Initial const obs = new MutationObserver((m) => { if (m.some(x => x.attributeName === attr)) update(); }); obs.observe(el, { attributes: true, attributeFilter: [attr] }); return () => obs.disconnect(); }) }; /** * Observes changes to one or more attributes on an element. * * Uses MutationObserver to watch for attribute changes. The callback fires * whenever any of the specified attributes change, receiving the new value * and attribute name. Returns a cleanup function to stop observing. * * **Performance**: Uses attribute filtering for efficiency. The observer only * watches the specified attributes, not all attribute changes. * * @param target - The element to observe (null-safe) * @returns A curried function that accepts attributes and callback, returns cleanup function * * @example * ```typescript * const input = document.querySelector('input'); * * // Imperative (cleaner DX) * const cleanup = watchAttr(input, 'disabled', (value, attrName) => { * console.log(`${attrName} changed to:`, value); * }); * * // Curried (pipeline friendly) * const cleanup = watchAttr(input)('disabled', (value, attrName) => { * console.log(`${attrName} changed to:`, value); * // value is the new attribute value (string | null) * }); * * // Watch multiple attributes * watchAttr(input)(['value', 'placeholder', 'type'], (value, attrName) => { * console.log(`${attrName} = ${value}`); * }); * * // Form validation * watchAttr(input)('aria-invalid', (value) => { * if (value === 'true') { * input.classList.add('error'); * } else { * input.classList.remove('error'); * } * }); * * // Sync attributes between elements * watchAttr(sourceElement)('title', (value) => { * if (value) targetElement.setAttribute('title', value); * }); * * // Later: stop watching * cleanup(); * * // Null-safe: returns no-op cleanup * const noop = watchAttr(null)('disabled', callback); // () => {} * ``` */ export const watchAttr = /* @__PURE__ */ def((target: Element | null, attrs: string | string[], callback: (val: string | null, attr: string) => void): Unsubscribe => { if (!target) return () => { }; const obs = new MutationObserver((muts) => muts.forEach(m => { if (m.attributeName) callback(target.getAttribute(m.attributeName), m.attributeName); })); obs.observe(target, { attributes: true, attributeFilter: Array.isArray(attrs) ? attrs : [attrs] }); return () => obs.disconnect(); }); /** * Observes changes to the text content of an element. * * Uses MutationObserver to watch for text content changes. The callback fires * whenever the element's textContent changes, receiving the new text value. * Returns a cleanup function to stop observing. * * **Performance**: Watches both characterData (direct text node changes) and * childList (when text nodes are added/removed) with subtree enabled. * * @param target - The element to observe (null-safe) * @returns A curried function that accepts a callback, returns cleanup function * * @example * ```typescript * const div = document.querySelector('div'); * * // Imperative (cleaner DX) * const cleanup = watchText(div, (newText) => { * console.log('Text changed to:', newText); * }); * * // Curried (pipeline friendly) * const cleanup = watchText(div)((newText) => { * console.log('Text changed to:', newText); * }); * * // Later: stop watching * cleanup(); * * // Use in reactive UI * const counter = document.querySelector('#counter'); * watchText(counter)((text) => { * const count = parseInt(text); * if (count > 100) alert('Limit exceeded!'); * }); * * // Null-safe: returns no-op cleanup * const noop = watchText(null)(callback); // () => {} * ``` */ export const watchText = /* @__PURE__ */ def((target: Element | null, callback: (text: string, el: Element) => void): Unsubscribe => { if (!target) return () => { }; const obs = new MutationObserver(() => { callback(target.textContent || '', target); }); obs.observe(target, { characterData: true, childList: true, subtree: true }); return () => obs.disconnect(); }); const resolveWatchElement = (target: ElementInput | null) => { if (typeof target === 'string') return find(document)(target) as T | null; if (typeof target === 'function') return target() as T | null; return target as T | null; }; /** * Unified watch helpers for classes, attributes, text, and mutations. * * Supports both direct calls (`watch.class(el, 'active', cb)`) and * fluent calls (`watch(el).class('active', cb)`). * * @example * ```typescript * watch.class(el, 'open', (isOpen) => console.log(isOpen)); * watch(el).attr('disabled', (value) => console.log(value)); * watch.mutations(el, { subtree: true }, (records) => console.log(records.length)); * ``` */ export const watch = Object.assign( (target: ElementInput | null) => { return { class: (className: string, callback: (isPresent: boolean, el: T) => void) => watch.class(target, className, callback), attr: (attrName: string, callback: (value: string | null, el: T) => void) => watch.attr(target, attrName, callback), text: (callback: (text: string, el: T) => void) => watch.text(target, callback), mutations: ( optionsOrCallback?: MutationObserverInit | ((records: MutationRecord[], observer: MutationObserver, el: T) => void), maybeCallback?: (records: MutationRecord[], observer: MutationObserver, el: T) => void ) => watch.mutations(target, optionsOrCallback as any, maybeCallback) }; }, { /** * Observe class changes for a specific class name. * * @param target - Element, selector, or resolver * @param className - Class to watch * @param callback - Called when class toggles * @returns Cleanup function * * @example * ```typescript * const stop = watch.class(btn, 'active', (isActive) => { * console.log(isActive); * }); * ``` */ class: ( target: ElementInput | null, className: string, callback: (isPresent: boolean, el: T) => void ): Unsubscribe => { const el = resolveWatchElement(target); return watchClass(el, className, (isPresent, element) => callback(isPresent, element as T)); }, /** * Observe attribute changes for a specific attribute. * * @param target - Element, selector, or resolver * @param attrName - Attribute to watch * @param callback - Called when attribute changes * @returns Cleanup function * * @example * ```typescript * watch.attr(input, 'aria-invalid', (value) => { * console.log(value); * }); * ``` */ attr: ( target: ElementInput | null, attrName: string, callback: (value: string | null, el: T) => void ): Unsubscribe => { const el = resolveWatchElement(target); return watchAttr(el, attrName, (value, element) => callback(value, element as unknown as T)); }, /** * Observe text content changes. * * @param target - Element, selector, or resolver * @param callback - Called when text changes * @returns Cleanup function * * @example * ```typescript * watch.text(status, (text) => console.log(text)); * ``` */ text: ( target: ElementInput | null, callback: (text: string, el: T) => void ): Unsubscribe => { const el = resolveWatchElement(target); return watchText(el, (text, element) => callback(text, element as T)); }, /** * Observe DOM mutations for a target element. * * Defaults to `{ attributes: true, childList: true, subtree: false }`. * * @param target - Element, selector, or resolver * @param options - Mutation observer options or callback * @param callback - Mutation observer callback * @returns Cleanup function * * @example * ```typescript * const stop = watch.mutations(el, (records) => { * console.log(records.length); * }); * * watch.mutations(el, { subtree: true }, (records) => { * console.log(records); * }); * ``` */ mutations: ( target: ElementInput | null, optionsOrCallback?: MutationObserverInit | ((records: MutationRecord[], observer: MutationObserver, el: T) => void), maybeCallback?: (records: MutationRecord[], observer: MutationObserver, el: T) => void ): Unsubscribe => { const el = resolveWatchElement(target); if (!el) return () => { }; const defaultOptions: MutationObserverInit = { attributes: true, childList: true, subtree: false }; const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback; if (!callback) return () => { }; const options = typeof optionsOrCallback === 'function' ? defaultOptions : { ...defaultOptions, ...optionsOrCallback }; const observer = new MutationObserver((records) => callback(records, observer, el)); observer.observe(el, options); return () => observer.disconnect(); } } ); /** * Gets or sets an attribute on an element. * * Overloads: * 1. attr("data-id") β€” gets attribute from documentElement * 2. attr(el)("data-id") β€” gets attribute from the element * 3. attr(el)("data-id", "123") β€” sets attribute * * Getter returns `string | null` * Setter returns `void` */ export function attr(attribute: string): string | null; export function attr(el: Element | null): (attribute: string) => string | null; export function attr(el: Element | null): (attribute: string, value: string) => void; export function attr(a: any): any { // Case 1: attr("data-id") if (typeof a === "string") { const attribute = a; return document.documentElement.getAttribute(attribute); } // Curried case: attr(el) const el: Element | null = a; return (attribute: string, value?: string): any => { if (!el) return value === undefined ? null : undefined; if (value === undefined) { return el.getAttribute(attribute); } el.setAttribute(attribute, value); }; } /** * Gets or sets a DOM property. * * Works like `attr` but for real JS properties. * * Overloads: * 1. prop("value") β€” gets from document.documentElement * 2. prop(el)("value") β€” getter * 3. prop(el)("value", newValue) β€” setter * * Getter: returns the property type of the element if known. * Setter: void */ export function prop(prop: K): HTMLElement[K]; export function prop( el: T | null ): (prop: K) => T[K]; export function prop( el: T | null ): (prop: K, value: T[K]) => void; export function prop(a: any): any { if (typeof a === "string") { const key = a; const el = document.documentElement as any; return el[key]; } const el = a as HTMLElement | null; return (key: any, value?: any): any => { if (!el) return undefined; if (value === undefined) return (el as any)[key]; (el as any)[key] = value; }; } // ============================================================================= // 8. LIFECYCLE // ============================================================================= /** * Executes a callback when the DOM is fully loaded and parsed. * * If the DOM is already ready, the callback executes immediately (synchronously). * Otherwise, it waits for the DOMContentLoaded event. This is safer than placing * scripts at the end of the body, as it guarantees DOM availability. * * **Timing Guarantee**: The callback will execute exactly once, either immediately * or when DOMContentLoaded fires. External resources (images, stylesheets) may * still be loading. * * **SSR Considerations**: In server-side rendering contexts, ensure this code * only runs in the browser (check for `typeof document !== 'undefined'`). * * @param fn - The callback to execute when the DOM is ready * @returns void * * @example * ```typescript * // Basic usage * onReady(() => { * console.log('DOM is ready!'); * const app = document.querySelector('#app'); * // Safe to manipulate DOM here * }); * * // Initialize app * onReady(() => { * const form = document.querySelector('form'); * on(form)('submit', handleSubmit); * * const buttons = findAll(document)('button'); * buttons.forEach(btn => { * on(btn)('click', handleClick); * }); * }); * * // Multiple callbacks (each executes independently) * onReady(() => console.log('First')); * onReady(() => console.log('Second')); * * // SSR-safe usage * if (typeof document !== 'undefined') { * onReady(() => { * // Client-side only code * }); * } * * // Difference from window.onload: * // - onReady: Fires when DOM is parsed (faster) * // - window.onload: Fires when all resources loaded (slower) * ``` */ export const onReady = (fn: () => void): void => { if (typeof document === 'undefined') return; if (document.readyState === 'complete' || document.readyState === 'interactive') { fn(); } else { document.addEventListener('DOMContentLoaded', fn, { once: true }); } }; /** * Promise-based DOM lifecycle utilities for different timing needs. * * Type-safe, composable methods for waiting on specific lifecycle phases: * - `dom()` β€” waits until DOM is parsed (DOMContentLoaded) * - `micro()` β€” waits until microtask queue is empty * - `raf()` β€” waits until next requestAnimationFrame * * @example * ```typescript * // Wait for DOM to be parsed * await ready.dom(); * const app = document.querySelector('#app'); * * // Wait for microtasks to flush * await ready.micro(); * * // Wait for next paint * await ready.raf(); * * // Chain multiple lifecycle waits * await ready.dom(); * await ready.micro(); * await ready.raf(); * // Now safe to interact with layout * ``` */ export const ready = { /** * Waits until the DOM is parsed and interactive (DOMContentLoaded). * Resolves immediately if DOM is already loaded. * * @returns Promise that resolves when DOM is ready */ dom: () => new Promise(resolve => { if (typeof document === 'undefined') return resolve(); if (document.readyState !== "loading") resolve(); else document.addEventListener("DOMContentLoaded", () => resolve(), { once: true }); }), /** * Waits until the microtask queue is flushed (after current JS execution). * Useful for ensuring Promise chains and MutationObserver callbacks have run. * * @returns Promise that resolves on next microtask */ micro: () => new Promise(resolve => queueMicrotask(resolve)), /** * Waits until the next requestAnimationFrame (next paint cycle). * Useful for deferring layout-dependent code. * * @returns Promise that resolves on next frame */ raf: () => new Promise(resolve => requestAnimationFrame(() => resolve())) }; /** * Observes when elements matching a selector are added to the DOM. * * Uses MutationObserver to watch for new elements. The handler fires for: * 1. Elements already in the DOM (initial check) * 2. Elements added dynamically after setup * * Each element is tracked using WeakSet to prevent duplicate handler calls. * Returns a cleanup function to stop observing. * * **Performance**: Uses WeakSet for O(1) duplicate checking without memory leaks. * The observer watches the entire subtree by default. * * **SPA Navigation**: Perfect for handling dynamically loaded content in single-page * applications where elements appear/disappear without full page reloads. * * @template S - The CSS selector string * @param selector - CSS selector to match elements * @returns A curried function that accepts handler, root, and once flag * * @example * ```typescript * // Imperative (cleaner DX) * const cleanup = onMount('.modal', (modal) => { * console.log('Modal added:', modal); * modal.classList.add('initialized'); * }); * * // Curried (pipeline friendly) * const cleanup = onMount('.modal')((modal) => { * console.log('Modal added:', modal); * modal.classList.add('initialized'); * * // Setup modal-specific behavior * const closeBtn = modal.querySelector('.close'); * on(closeBtn)('click', () => modal.remove()); * }); * * // Later: stop observing * cleanup(); * * // Watch within a specific container * const container = document.querySelector('#app'); * onMount('.dynamic-card')((card) => { * console.log('Card added'); * }, container); * * // One-time handler (stops after first match) * onMount('#splash-screen')((splash) => { * setTimeout(() => splash.remove(), 3000); * }, document, true); // once = true * * // SPA route handling * onMount('[data-page]')((page) => { * const pageName = page.getAttribute('data-page'); * console.log('Page loaded:', pageName); * * // Initialize page-specific features * initializeAnalytics(pageName); * loadPageData(pageName); * }); * * // Lazy-load images as they're added * onMount('img[data-src]')((img) => { * const src = img.getAttribute('data-src'); * if (src) { * img.setAttribute('src', src); * img.removeAttribute('data-src'); * } * }); * * // Component initialization pattern * onMount('[data-component="tooltip"]')((el) => { * new Tooltip(el); // Initialize tooltip component * }); * ``` */ export const onMount = /* @__PURE__ */ def((selector: string | null, handler: (el: Element) => void, root: ParentNode = document, once = false): Unsubscribe => { if (!selector) return () => { }; const seen = new WeakSet(); let foundAny = false; let obs: MutationObserver | null = null; const check = (node: Element) => { if (seen.has(node)) return; if (node.matches(selector)) { seen.add(node); handler(node); foundAny = true; } node.querySelectorAll(selector).forEach(c => { if (!seen.has(c)) { seen.add(c); handler(c); foundAny = true; } }); }; const stopIfOnceSatisfied = () => { if (once && foundAny && obs) { obs.disconnect(); obs = null; } }; // Initial check root.querySelectorAll(selector).forEach(check); if (once && foundAny) return () => { }; obs = new MutationObserver(muts => { muts.forEach(m => { m.addedNodes.forEach(n => { if (n.nodeType === 1) check(n as Element); }); }); stopIfOnceSatisfied(); }); obs.observe(root, { childList: true, subtree: true }); return () => { obs?.disconnect(); obs = null; }; }); /** * Waits for a condition to become true on an element. * * Returns a Promise that resolves when the predicate returns true. Uses * MutationObserver to watch for changes. The predicate is checked immediately, * then on every mutation until it returns true. * * **Timeout Recommendation**: Consider adding a timeout wrapper to prevent * infinite waiting: * ```typescript * Promise.race([ * waitFor(el, predicate), * wait(5000).then(() => { throw new Error('Timeout'); }) * ]); * ``` * * **Memory Leak Prevention**: The observer automatically disconnects when the * condition is met. If the element is null, the promise rejects (consider * null-checking before calling). * * @param target - The element to observe (null-unsafe: promise rejects if null) * @returns A curried function that accepts a predicate and returns a Promise * * @example * ```typescript * const button = document.querySelector('button'); * * // Imperative (cleaner DX) * await waitFor(button, (el) => el.classList.contains('ready')); * console.log('Button is ready!'); * * // Curried (pipeline friendly) * await waitFor(button)((el) => el.classList.contains('ready')); * console.log('Button is ready!'); * * // Wait for specific attribute value * await waitFor(input)((el) => el.getAttribute('data-loaded') === 'true'); * console.log('Data loaded!'); * * // Wait for child count * const list = document.querySelector('ul'); * await waitFor(list)((el) => el.children.length >= 10); * console.log('List has at least 10 items'); * * // Wait for text content * await waitFor(status)((el) => el.textContent?.includes('Complete')); * console.log('Status is complete'); * * // With timeout (recommended) * try { * await Promise.race([ * waitFor(element)((el) => el.classList.contains('loaded')), * wait(5000).then(() => { throw new Error('Timeout waiting for element'); }) * ]); * console.log('Element loaded in time'); * } catch (e) { * console.error('Timed out:', e); * } * * // Animation completion * element.classList.add('animating'); * await waitFor(element)((el) => !el.classList.contains('animating')); * console.log('Animation complete'); * * // Form validation * await waitFor(form)((el) => { * const inputs = el.querySelectorAll('input[required]'); * return Array.from(inputs).every(input => input.value.length > 0); * }); * console.log('All required fields filled'); * ``` */ export const waitFor = /* @__PURE__ */ def((target: Element | null, predicate: (el: Element) => boolean): Promise => { return new Promise((resolve, reject) => { if (!target) { reject(new Error('waitFor: target is null')); return; } if (predicate(target)) return resolve(target); const obs = new MutationObserver(() => { if (predicate(target)) { obs.disconnect(); resolve(target); } }); obs.observe(target, { attributes: true, childList: true, subtree: true, characterData: true }); }); }); // ============================================================================= // 9. UTILS (URL, FORM, ETC) // ============================================================================= /** * Utilities for working with URL query parameters. * * Provides a functional API for reading and modifying URL search parameters. * Supports both 'soft' navigation (using pushState) and 'hard' navigation * (full page reload). * * **URL Encoding**: Values are automatically URL-encoded when set. * * @example * ```typescript * // URL: https://example.com?page=1&sort=name * * // Get single parameter * const page = Params.get('page'); // "1" * const missing = Params.get('missing'); // null * * // Get multiple values (for array parameters) * // URL: ?tags=js&tags=ts&tags=react * const tags = Params.getAll('tags'); // ["js", "ts", "react"] * * // Set parameter (soft navigation - no reload) * Params.set('page')('2')(); // Updates URL without reload * // URL becomes: ?page=2&sort=name * * // Set parameter (hard navigation - full reload) * Params.set('page')('2')('hard'); // Reloads page with new URL * * // Chaining for multiple updates * Params.set('page')('1')(); * Params.set('sort')('date')(); * Params.set('filter')('active')(); * ``` */ export const Params = { /** * Gets a single query parameter value. * * @param key - The parameter name * @returns The parameter value or null if not found * * @example * ```typescript * // URL: ?id=123&name=John * const id = Params.get('id'); // "123" * const name = Params.get('name'); // "John" * const missing = Params.get('missing'); // null * ``` */ get: (key: string) => new URLSearchParams(window.location.search).get(key), /** * Gets all values for a query parameter (for array-like parameters). * * @param key - The parameter name * @returns Array of all values for that parameter * * @example * ```typescript * // URL: ?tags=js&tags=ts&tags=react * const tags = Params.getAll('tags'); // ["js", "ts", "react"] * * // URL: ?filter=active * const filters = Params.getAll('filter'); // ["active"] * * // Missing parameter * const missing = Params.getAll('missing'); // [] * ``` */ getAll: (key: string) => new URLSearchParams(window.location.search).getAll(key), /** * Sets a query parameter value. * * @param key - The parameter name * @returns A curried function that accepts value and navigation type * * @example * ```typescript * // Soft navigation (no page reload) * Params.set('page')('2')(); // Default: soft * Params.set('page')('2')('soft'); * * // Hard navigation (full page reload) * Params.set('page')('2')('hard'); * * // Pagination example * const nextPage = () => { * const current = parseInt(Params.get('page') || '1'); * Params.set('page')(String(current + 1))(); * }; * * // Filter example * const applyFilter = (filter: string) => { * Params.set('filter')(filter)(); * Params.set('page')('1')(); // Reset to page 1 * loadData(); // Fetch filtered data * }; * ``` */ set: (key: string) => (val: string) => (type: 'soft' | 'hard' = 'soft') => { const u = new URL(window.location.href); u.searchParams.set(key, val); if (type === 'hard') window.location.href = u.href; else window.history.pushState(null, '', u.href); } }; /** * Utilities for working with form data. * * Provides serialization and population of form fields. Automatically handles * different input types (text, checkbox, radio, number, select, textarea). * * **FormData Compatibility**: For native FormData support, use `new FormData(form)`. * This utility provides a plain object representation. * * @example * ```typescript * const form = document.querySelector('form'); * * // Serialize form to object * const data = Form.serialize(form); * // { username: "john", email: "john@example.com", age: 25, subscribe: true } * * // Populate form from object * Form.populate(form)({ * username: "jane", * email: "jane@example.com", * age: 30, * subscribe: false * }); * * // Save/load form state * const saveForm = () => { * const data = Form.serialize(form); * Local.set('formDraft')(data); * }; * * const loadForm = () => { * const data = Local.get('formDraft'); * if (data) Form.populate(form)(data); * }; * ``` */ export interface FormSerializeOptions { /** Use dot-notation to create nested objects. */ nested?: boolean; /** Include FileList values from file inputs. */ includeFiles?: boolean; /** Include disabled fields (default: false). */ includeDisabled?: boolean; } export interface Register { (cleanup: T): T; } export type AutoCleanup = (fn: (r: Register) => T | Promise) => T | Promise; export const Form = { /** * Serializes form inputs into a plain object. * * Handles: * - Text inputs β†’ string * - Number inputs β†’ number * - Checkboxes β†’ boolean * - Radio buttons β†’ string (only checked value) * - Select β†’ string * - Textarea β†’ string * * Options: * - `nested`: Supports dot-notation for nested objects * - `includeFiles`: Includes FileList values * - `includeDisabled`: Includes disabled inputs * * Only includes inputs with a `name` attribute. * * @param root - The form or container element * @param options - Serialization options * @returns Object with field names as keys and values * * @example * ```typescript * const form = document.querySelector('form'); * const data = Form.serialize(form); * * // Submit to API * await Http.post('/api/submit')(data); * * // Validate before submit * on(form)('submit', (e) => { * e.preventDefault(); * const data = Form.serialize(form); * if (validate(data)) { * submitForm(data); * } * }); * * // Auto-save draft * const inputs = form.querySelectorAll('input, textarea'); * inputs.forEach(input => { * on(input)('input', debounce(() => { * const data = Form.serialize(form); * Local.set('draft')(data); * }, 500)); * }); * ``` */ serialize: (root: HTMLElement | null, options: FormSerializeOptions = {}) => { const data: Record = {}; if (!root) return data; const { nested = false, includeFiles = false, includeDisabled = false } = options; const isNumeric = (value: string) => /^\d+$/.test(value); const setNested = (target: Record, path: string[], value: any) => { let current: any = target; path.forEach((segment, index) => { const isLast = index === path.length - 1; if (isLast) { current[segment] = value; return; } const nextSegment = path[index + 1]; const shouldBeArray = typeof nextSegment === 'string' && isNumeric(nextSegment); if (current[segment] === undefined || typeof current[segment] !== 'object') { current[segment] = shouldBeArray ? [] : {}; } current = current[segment]; }); }; const setValue = (name: string, value: any) => { if (nested) { setNested(data, name.split('.').filter(Boolean), value); } else { data[name] = value; } }; root.querySelectorAll('input, select, textarea').forEach(el => { if (!el.name) return; if (!includeDisabled && el.disabled) return; if (el instanceof HTMLInputElement && el.type === 'file') { if (includeFiles) setValue(el.name, el.files); return; } if (el instanceof HTMLInputElement && el.type === 'checkbox') { setValue(el.name, el.checked); } else if (el instanceof HTMLInputElement && el.type === 'radio') { if (el.checked) setValue(el.name, el.value); } else if (el instanceof HTMLInputElement && el.type === 'number') { setValue(el.name, Number(el.value)); } else { setValue(el.name, el.value); } }); return data; }, /** * Populates form inputs from a plain object. * * Matches object keys to input `name` attributes and sets values accordingly. * * @param root - The form or container element * @returns A curried function that accepts data object and returns the root * * @example * ```typescript * const form = document.querySelector('form'); * * // Imperative (cleaner DX) * Form.populate(form, { * username: 'john', * email: 'john@example.com', * notifications: true * }); * * // Curried (pipeline friendly) * Form.populate(form)({ * username: user.username, * email: user.email, * bio: user.bio, * notifications: user.preferences.notifications * }); * * // Load saved data * const savedData = Local.get('formData'); * if (savedData) Form.populate(form)(savedData); * * // Reset form to defaults * Form.populate(form)({ * theme: 'light', * language: 'en', * notifications: true * }); * ``` */ populate: def((root: HTMLElement | null, data: Record) => { if (!root) return root; Object.entries(data).forEach(([k, v]) => { const el = root.querySelector(`[name="${k}"]`) as HTMLInputElement; if (!el) return; if (el.type === 'checkbox' || el.type === 'radio') el.checked = !!v; else el.value = String(v); }); return root; }) }; /** * Waits for a specified number of milliseconds. * * Returns a Promise that resolves after the delay. Useful for adding delays * in async functions or creating timeouts. * * @param ms - The number of milliseconds to wait * @returns A Promise that resolves after the delay * * @example * ```typescript * // Simple delay * await wait(1000); // Wait 1 second * console.log('Done waiting'); * * // Delay in async function * async function showMessage() { * console.log('Loading...'); * await wait(2000); * console.log('Done!'); * } * * // Animation timing * async function animate() { * element.classList.add('fade-in'); * await wait(300); * element.classList.remove('fade-in'); * } * * // Retry with delay * async function retryFetch(url: string, retries = 3) { * for (let i = 0; i < retries; i++) { * try { * return await fetch(url); * } catch (e) { * if (i < retries - 1) await wait(1000 * (i + 1)); * } * } * } * ``` */ export const wait = (ms: number) => new Promise(r => setTimeout(r, ms)); /** * Waits for the next animation frame. * * Returns a Promise that resolves on the next requestAnimationFrame callback. * Useful for ensuring DOM updates are rendered before proceeding. * * @returns A Promise that resolves on the next frame * * @example * ```typescript * // Ensure DOM update is rendered * element.style.opacity = '0'; * await nextFrame(); * element.style.transition = 'opacity 0.3s'; * element.style.opacity = '1'; * * // Batch DOM reads after writes * element.style.width = '100px'; * await nextFrame(); * const width = element.offsetWidth; // Avoids layout thrashing * * // Smooth animation sequence * async function animateSequence() { * element.classList.add('step-1'); * await nextFrame(); * element.classList.add('step-2'); * await nextFrame(); * element.classList.add('step-3'); * } * ``` */ export const nextFrame = () => new Promise(r => requestAnimationFrame(r)); /** CSS Template Literal for highlighting */ export const cssTemplate = (strings: TemplateStringsArray, ...values: any[]) => strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), ''); // ============================================================================= // 10. NAVIGATION (TRAVERSAL) // ============================================================================= /** * Flexible, type-aware DOM traversal utilities. * * Each method supports two invocation styles: * * 1. **Element-first** (immediate): * Traverse.next(el) β†’ Element | null * Traverse.next(el, "span.highlight") β†’ Element | null * * 2. **Selector-first**: * Traverse.next(".item") β†’ Element | null * * All operations are: * - **Null-safe**: All functions gracefully return `null` or `[]`. * - **Type-preserving**: Passing `HTMLDivElement` returns `HTMLDivElement | null`. * - **Selector-aware**: Passing a selector filters the returned element(s). */ export const Traverse = { /** * Get the parent element, optionally filtered by a selector. * * @example * // Element-first * const parent = Traverse.parent(el); //
    | null * * // Selector-first * const parent = Traverse.parent("#child"); // parent of #child * * // Curried * const specific = Traverse.parent(el)(".box"); */ parent(elOrSelector?: Element | string | null, selector?: string) { if (typeof elOrSelector === "string") { const el = document.querySelector(elOrSelector); const parent = el?.parentElement || null; if (!parent) return null; return !selector || parent.matches(selector) ? parent : null; } const el = elOrSelector ?? null; const parent = el?.parentElement ?? null; if (!parent) return null; return !selector || parent.matches(selector) ? parent : null; }, /** * Get the next sibling element. * * @example * Traverse.next(el); //
  • | null * Traverse.next(".active"); // next of .active * Traverse.next(el)("button"); // next button sibling */ next(elOrSelector?: Element | string | null, selector?: string) { if (typeof elOrSelector === "string") { const el = document.querySelector(elOrSelector); const next = el?.nextElementSibling || null; if (!next) return null; return !selector || next.matches(selector) ? next : null; } const el = elOrSelector ?? null; const next = el?.nextElementSibling ?? null; if (!next) return null; return !selector || next.matches(selector) ? next : null; }, /** * Get the previous sibling element. * * @example * Traverse.prev(el); * Traverse.prev(".selected"); * Traverse.prev(el)(".item"); */ prev(elOrSelector?: Element | string | null, selector?: string) { if (typeof elOrSelector === "string") { const el = document.querySelector(elOrSelector); const prev = el?.previousElementSibling || null; if (!prev) return null; return !selector || prev.matches(selector) ? prev : null; } const el = elOrSelector ?? null; const prev = el?.previousElementSibling ?? null; if (!prev) return null; return !selector || prev.matches(selector) ? prev : null; }, /** * Get child elements, with optional selector filtering. * * @example * Traverse.children(el); // Element[] * Traverse.children(".list"); // children of element matching .list * Traverse.children(el)("li"); // only
  • children */ children(elOrSelector?: Element | string | null, selector?: string) { if (typeof elOrSelector === "string") { const el = document.querySelector(elOrSelector); if (!el) return []; const kids = Array.from(el.children); return selector ? kids.filter(c => c.matches(selector)) : kids; } const el = elOrSelector ?? null; if (!el) return []; const kids = Array.from(el.children); return selector ? kids.filter(c => c.matches(selector)) : kids; }, /** * Get sibling elements (excluding the original element). * * @example * Traverse.siblings(el); * Traverse.siblings("#active"); * Traverse.siblings(el)(".item"); */ siblings(elOrSelector?: Element | string | null, selector?: string) { if (typeof elOrSelector === "string") { const el = document.querySelector(elOrSelector); if (!el?.parentElement) return []; const sibs = Array.from(el.parentElement.children).filter(c => c !== el); return selector ? sibs.filter(s => s.matches(selector)) : sibs; } const el = elOrSelector ?? null; if (!el?.parentElement) return []; const sibs = Array.from(el.parentElement.children).filter(s => s !== el); return selector ? sibs.filter(s => s.matches(selector)) : sibs; }, /** * Get all ancestor elements up to the document root. * * Optionally stops at an element matching a selector. * * @example * Traverse.parents(el); // All ancestors * Traverse.parents("#child"); // Ancestors of #child * Traverse.parents(el, ".section"); // Ancestors until .section match * Traverse.parents(el)(".container"); // Curried: ancestors matching .container */ parents(elOrSelector?: Element | string | null, until?: string | ((el: Element) => boolean)): Element[] { const el = typeof elOrSelector === "string" ? document.querySelector(elOrSelector) : elOrSelector ?? null; const result: Element[] = []; let current = el?.parentElement ?? null; while (current) { // If until is a string selector if (typeof until === "string" && current.matches(until)) { break; } // If until is a function predicate if (typeof until === "function" && until(current)) { break; } result.push(current); current = current.parentElement; } return result; }, /** * Get all following sibling elements. * * Optionally filtered by a selector. * * @example * Traverse.nextAll(el); // All following siblings * Traverse.nextAll(".selected"); // Following siblings of .selected * Traverse.nextAll(el)(".item"); // Following siblings matching .item */ nextAll(elOrSelector?: Element | string | null, selector?: string): Element[] { const el = typeof elOrSelector === "string" ? document.querySelector(elOrSelector) : elOrSelector ?? null; const result: Element[] = []; let current = el?.nextElementSibling ?? null; while (current) { if (!selector || current.matches(selector)) { result.push(current); } current = current.nextElementSibling; } return result; }, /** * Get all preceding sibling elements. * * Optionally filtered by a selector. * * @example * Traverse.prevAll(el); // All preceding siblings * Traverse.prevAll(".selected"); // Preceding siblings of .selected * Traverse.prevAll(el)(".item"); // Preceding siblings matching .item */ prevAll(elOrSelector?: Element | string | null, selector?: string): Element[] { const el = typeof elOrSelector === "string" ? document.querySelector(elOrSelector) : elOrSelector ?? null; const result: Element[] = []; let current = el?.previousElementSibling ?? null; while (current) { if (!selector || current.matches(selector)) { result.push(current); } current = current.previousElementSibling; } return result; }, /** * Get all ancestors including the element itself, up to document root. * * Optionally filtered by a selector. * * @example * Traverse.closestAll(el); // Element + all ancestors * Traverse.closestAll("#child"); // Self + ancestors of #child * Traverse.closestAll(el)(".box"); // Self + ancestors matching .box */ closestAll(elOrSelector?: Element | string | null, selector?: string): Element[] { const el = typeof elOrSelector === "string" ? document.querySelector(elOrSelector) : elOrSelector ?? null; const result: Element[] = []; let current: Element | null = el; while (current) { if (!selector || current.matches(selector)) { result.push(current); } current = current.parentElement; } return result; } }; // ============================================================================= // 11. CSS VARIABLES & ANIMATION // ============================================================================= /** * Utilities for working with CSS custom properties (variables). * * Provides a functional API for getting and setting CSS variables on elements. * Variables can be set at any level (element, :root, etc.) and will cascade * according to CSS specificity rules. * * **Fallback Values**: When getting variables, you can provide fallback values * using standard CSS syntax: `var(--color, blue)`. * * @example * ```typescript * const element = document.querySelector('.card'); * * // Set CSS variable * CssVar.set(element)('--primary-color', '#007bff'); * CssVar.set(element)('--spacing', '1rem'); * * // Get CSS variable * const color = CssVar.get(element)('--primary-color'); // "#007bff" * * // Set on :root for global theme * CssVar.set(document.documentElement)('--theme', 'dark'); * * // Dynamic theming * const setTheme = (theme: 'light' | 'dark') => { * const root = document.documentElement; * if (theme === 'dark') { * CssVar.set(root)('--bg', '#1a1a1a'); * CssVar.set(root)('--text', '#ffffff'); * } else { * CssVar.set(root)('--bg', '#ffffff'); * CssVar.set(root)('--text', '#000000'); * } * }; * ``` */ export const CssVar = { /** * Sets a CSS custom property (variable) on an element. * * @param el - The element to set the variable on (null-safe) * @returns A curried function that accepts name and value, returns the element * * @example * ```typescript * const div = document.querySelector('div'); * * // Set single variable * CssVar.set(div)('--color', 'red'); * * // Set multiple variables * CssVar.set(div)('--width', '100px'); * CssVar.set(div)('--height', '100px'); * * // Global theme variables * const root = document.documentElement; * CssVar.set(root)('--primary', '#007bff'); * CssVar.set(root)('--secondary', '#6c757d'); * * // Dynamic values * CssVar.set(element)('--progress', `${percentage}%`); * * // Null-safe * CssVar.set(null)('--color', 'red'); // Returns null * ``` */ set: (el: HTMLElement | null) => (name: string, value: string) => { el?.style.setProperty(name, value); return el; }, /** * Gets the computed value of a CSS custom property. * * Returns the computed value, which may be inherited from a parent element * or :root. The value is trimmed of whitespace. * * @param el - The element to get the variable from (null-safe) * @returns A curried function that accepts name and returns the value * * @example * ```typescript * const div = document.querySelector('div'); * * // Get variable value * const color = CssVar.get(div)('--primary-color'); * * // Get inherited variable * const spacing = CssVar.get(div)('--spacing'); // May come from parent * * // Use in calculations * const width = parseInt(CssVar.get(element)('--width')); * * // Check if variable is set * const hasTheme = CssVar.get(document.documentElement)('--theme') !== ''; * * // Null-safe * CssVar.get(null)('--color'); // "" * ``` */ get: (el: HTMLElement | null) => (name: string) => { return el ? getComputedStyle(el).getPropertyValue(name).trim() : ''; } }; /** * Reads a computed CSS property value from an element. * * Gets the final computed value of any CSS property, including inherited values, * cascaded values, and browser defaults. Useful for reading actual rendered values * rather than inline styles. * * **Unit Parsing**: The returned value includes units (e.g., "16px", "1.5em"). * Parse with `parseInt()` or `parseFloat()` if you need numeric values. * * **Computed vs Inline**: This reads computed styles (what's actually rendered), * not inline styles. Use `element.style.property` for inline styles only. * * @param el - The element to read from (null-safe) * @returns A curried function that accepts a property name and returns the value * * @example * ```typescript * const div = document.querySelector('div'); * * // Get computed width (includes padding, border if box-sizing) * const width = computed(div)('width'); // "200px" * * // Get font size * const fontSize = computed(div)('fontSize'); // "16px" * * // Get color (returns rgb/rgba) * const color = computed(div)('color'); // "rgb(0, 0, 0)" * * // Parse numeric values * const widthNum = parseInt(computed(div)('width')); // 200 * * // Check display state * const isHidden = computed(div)('display') === 'none'; * * // Get inherited values * const lineHeight = computed(div)('lineHeight'); * * // Null-safe * computed(null)('width'); // "" * ``` */ export const computed = (el: HTMLElement | null) => (prop: keyof CSSStyleDeclaration): string => { if (!el) return ''; const value = getComputedStyle(el)[prop]; return typeof value === 'string' ? value : String(value); }; /** * Injects CSS styles into the document. * * Creates a