UNPKG

watch-selector

Version:

Runs a function when a selector is added to dom

1,665 lines (1,567 loc) 128 kB
/** * DOM API Implementation with Sync Generators and Branded Types * * This module implements DOM manipulation functions that support multiple API patterns: * 1. Direct element manipulation: text(element, 'content') * 2. CSS selector manipulation: text('#id', 'content') * 3. Generator with yield*: yield* text('content') * * All functions use sync generators and the yield* pattern for better type safety. */ import type { Workflow, WatchContext, Operation } from "../types"; import { getCurrentContext } from "../core/context"; import { isCSSSelector, type CSSSelector, type ClassName, } from "../utils/selector-types"; // ============================================================================ // Type Definitions // ============================================================================ // More specific style value types with literal types for common CSS values type CSSLengthUnit = | "px" | "em" | "rem" | "%" | "vh" | "vw" | "vmin" | "vmax" | "ch" | "ex" | "cm" | "mm" | "in" | "pt" | "pc"; type CSSLength = | `${number}${CSSLengthUnit}` | number | "0" | "auto" | "inherit" | "initial" | "unset"; type CSSColor = | `#${string}` | `rgb(${string})` | `rgba(${string})` | `hsl(${string})` | `hsla(${string})` | "transparent" | "currentColor" | "inherit"; type StyleValue = string | number | null | undefined; type StyleObject< K extends keyof CSSStyleDeclaration = keyof CSSStyleDeclaration, > = { [P in K]?: CSSStyleDeclaration[P] | StyleValue; }; type AttributeObject = Record< string, string | number | boolean | null | undefined >; type DataObject<T = any> = Record<string, T>; // Type-safe style properties with autocomplete type CSSStyleProperties = Partial<CSSStyleDeclaration>; // Better display value types type DisplayValue = | "none" | "block" | "inline" | "inline-block" | "flex" | "inline-flex" | "grid" | "inline-grid" | "table" | "table-row" | "table-cell" | "contents" | "list-item" | "run-in"; // Position types type PositionValue = "static" | "relative" | "absolute" | "fixed" | "sticky"; // More specific element types for better inference type FormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; type FocusableElement = HTMLElement & { focus(): void; blur(): void }; type ValueElement = | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLOutputElement; // Generic constraint for element queries with better type narrowing type ElementConstraint = Element | HTMLElement | SVGElement; type HTMLElementConstraint = | HTMLElement | HTMLDivElement | HTMLSpanElement | HTMLButtonElement | HTMLInputElement; type QueryConstraint<T = Element> = T extends Element ? T : Element; // Strict element type mapping for better inference type StrictElementMap<K extends keyof HTMLElementTagNameMap> = HTMLElementTagNameMap[K]; type StrictSVGElementMap<K extends keyof SVGElementTagNameMap> = SVGElementTagNameMap[K]; // ============================================================================ // Helper Functions // ============================================================================ /** * Type guard: Check if a value is an HTML element with generic constraint */ function isHTMLElement<T extends HTMLElement = HTMLElement>( value: unknown, tagName?: keyof HTMLElementTagNameMap, ): value is T { if (!(value instanceof HTMLElement)) return false; if ( tagName && (value as HTMLElement).tagName.toLowerCase() !== tagName.toLowerCase() ) { return false; } return true; } /** * Type guard for specific HTML element types */ // Removed unused isSpecificHTMLElement function /** * Type guard: Check if a value is an Element with generic constraint */ function isElement<T extends Element = Element>( value: unknown, tagName?: keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap, ): value is T { if (!(value instanceof Element)) return false; if (tagName && value.tagName.toLowerCase() !== tagName.toLowerCase()) { return false; } return true; } // Removed unused isNode and isDocument functions /** * Type guard: Check if element can receive focus */ function isFocusable(element: unknown): element is FocusableElement { return ( element instanceof HTMLElement && typeof (element as any).focus === "function" && typeof (element as any).blur === "function" ); } /** * Type guard: Check if element has a value property */ function hasValue(element: unknown): element is ValueElement { return ( element instanceof HTMLElement && "value" in element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element instanceof HTMLOutputElement) ); } // Removed unused isFormControl function /** * Check if we're in a generator context */ function isInGeneratorContext(): boolean { return getCurrentContext() !== null; } /** * Resolve elements from a selector string with generic type support */ function resolveElements<T extends Element = HTMLElement>( selector: string, root: Element | Document = document, ): T[] { const elements: T[] = []; root.querySelectorAll(selector).forEach((el) => { elements.push(el as T); }); return elements; } // Removed unused resolveElementsStrict function /** * Resolve single element with generic type support */ function resolveElement<T extends Element = HTMLElement>( selector: string, root: Element | Document = document, ): T | null { const element = root.querySelector(selector); return element as T | null; } // Removed unused resolveElementSafe function // Removed unused queryElement and queryElements functions /** * Enhanced selector detection with better heuristics */ function looksLikeSelector(value: unknown): boolean { if (typeof value !== "string") return false; // Use our improved heuristic from selector-types return isCSSSelector(value); } // ============================================================================ // Text Function // ============================================================================ /** * Sets or gets text content of elements with full type safety and multiple usage patterns. * * This function provides a unified API for text manipulation that works in three distinct patterns: * 1. Direct element manipulation - Pass an element directly * 2. CSS selector targeting - Use a selector string to find and modify elements * 3. Generator context - Use within watch() generators with yield * * @example Direct element manipulation * ```typescript * const button = document.querySelector('button'); * // Set text * text(button, 'Click me'); * // Get text * const content = text(button); // returns string * ``` * * @example CSS selector pattern * ```typescript * // Set text for all matching elements * text('.button', 'Click me'); * // Get text from first matching element * const content = text('.button'); // returns string | null * ``` * * @example Generator context with yield * ```typescript * import { watch, text } from 'watch-selector'; * * watch('.dynamic-content', function* () { * // Set text * yield text('Loading...'); * * // Get current text * const current = yield text(); * console.log('Current text:', current); * * // Update with dynamic content * yield text(`Loaded at ${new Date().toLocaleTimeString()}`); * }); * ``` * * @example With template literals and variables * ```typescript * text('button', 'Click me'); * ``` * * @example Generator pattern * ```typescript * watch('button', function* () { * yield* text('Click me'); * const content = yield* text(); * }); * ``` */ export function text(element: HTMLElement, content: string | number): void; export function text(selector: string, content: string | number): void; export function text(selector: CSSSelector, content: string | number): void; export function text<T extends HTMLElement = HTMLElement>( element: T, content: string | number, ): void; export function text<T extends HTMLElement = HTMLElement>(element: T): string; export function text( selector: string | CSSSelector, content: string | number, ): void; export function text(selector: string | CSSSelector): string | null; export function text(content: string | number): Workflow<void>; export function text(): Workflow<string>; export function text<T extends HTMLElement = HTMLElement>(...args: any[]): any { // Direct element manipulation setter if (args.length === 2 && isHTMLElement(args[0])) { const [element, content] = args as [T, string | number]; element.textContent = String(content); return; } // Direct element manipulation getter if (args.length === 1 && isHTMLElement(args[0])) { const [element] = args as [T]; return element.textContent || ""; } // CSS selector manipulation setter if (args.length === 2 && typeof args[0] === "string") { const [selector, content] = args as [string, string | number]; const elements = resolveElements<HTMLElement>(String(selector)); elements.forEach((el) => { el.textContent = String(content); }); return; } // CSS selector manipulation getter if ( args.length === 1 && typeof args[0] === "string" && !isInGeneratorContext() ) { const [selector] = args as [string]; const element = resolveElement<HTMLElement>(String(selector)); return element ? element.textContent || "" : null; } // Generator setter pattern - returns sync Workflow if (args.length === 1) { const [content] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { context.element.textContent = String(content); }) as Operation<void>; })(); } // Generator getter pattern - returns sync Workflow if (args.length === 0) { return (function* (): Generator<Operation<string>, string, any> { const result = yield ((context: WatchContext) => { return context.element.textContent || ""; }) as Operation<string>; return result; })(); } throw new Error( `Invalid arguments for text(): ${args.length} arguments provided`, ); } // ============================================================================ // HTML Function // ============================================================================ /** * Sets or gets HTML content of elements with full type safety. * * ⚠️ WARNING: Setting HTML content can expose your application to XSS attacks. * Always sanitize user input before using it as HTML content. * * @example Direct element manipulation * ```typescript * const container = document.querySelector('.content'); * // Set HTML (be careful with user input!) * html(container, '<strong>Bold text</strong>'); * // Get HTML * const markup = html(container); // returns string * ``` * * @example CSS selector pattern * ```typescript * // Set HTML for all matching elements * html('.card-body', '<p>Card content</p>'); * // Get HTML from first matching element * const markup = html('.card-body'); // returns string | null * ``` * * @example Generator context * ```typescript * import { watch, html } from 'watch-selector'; * * watch('.markdown-output', function* () { * yield html('<p>Rendering...</p>'); * * // Fetch and render markdown * const response = await fetch('/api/content'); * const rendered = await response.text(); * * // ⚠️ Only use with trusted content! * yield html(rendered); * }); * ``` * * @param element - The element to manipulate (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param content - HTML content to set (optional, if not provided, gets content) * @returns void when setting, string when getting, or Workflow in generator context */ export function html<T extends HTMLElement = HTMLElement>( element: T, content: string, ): void; export function html<T extends HTMLElement = HTMLElement>(element: T): string; export function html(selector: string | CSSSelector, content: string): void; export function html(selector: string | CSSSelector): string | null; export function html(content: string): Workflow<void>; export function html(): Workflow<string>; export function html<T extends HTMLElement = HTMLElement>(...args: any[]): any { // Direct element manipulation setter if (args.length === 2 && isHTMLElement(args[0])) { const [element, content] = args as [T, string]; element.innerHTML = String(content); return; } // Direct element manipulation getter if (args.length === 1 && isHTMLElement(args[0])) { const [element] = args as [T]; return element.innerHTML; } // CSS selector manipulation setter if (args.length === 2 && typeof args[0] === "string") { const [selector, content] = args as [string, string]; const elements = resolveElements<HTMLElement>(String(selector)); elements.forEach((el) => { el.innerHTML = String(content); }); return; } // CSS selector manipulation getter if ( args.length === 1 && typeof args[0] === "string" && !isInGeneratorContext() ) { const [selector] = args as [string]; const element = resolveElement<HTMLElement>(String(selector)); return element ? element.innerHTML : null; } // Generator setter pattern if (args.length === 1) { const [content] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { context.element.innerHTML = String(content); }) as Operation<void>; })(); } // Generator getter pattern if (args.length === 0) { return (function* (): Generator<Operation<string>, string, any> { const result = yield ((context: WatchContext) => { return context.element.innerHTML; }) as Operation<string>; return result; })(); } throw new Error( `Invalid arguments for html(): ${args.length} arguments provided`, ); } // ============================================================================ // Class Functions // ============================================================================ /** * Adds one or more CSS classes to elements with intelligent deduplication. * * Supports space-separated class names and automatically handles duplicates. * Classes are only added if they don't already exist on the element. * * @param element - HTMLElement to add classes to (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param className - Single class or space-separated classes to add * @returns void for direct/selector patterns, Workflow for generator pattern * * @example Adding classes in generators with yield* * ```typescript * import { watch, addClass, click } from 'watch-selector'; * * watch('.card', function* () { * // Add single class * yield* addClass('interactive'); * * // Add multiple classes at once * yield* addClass('shadow-lg rounded bordered'); * * yield* click(function* () { * // Add state classes reactively * yield* addClass('selected highlighted'); * }); * }); * ``` * * @example Conditional class addition * ```typescript * watch('.notification', function* () { * const type = yield* getState('type', 'info'); * * // Add classes based on state * yield* addClass('notification'); * yield* addClass(`notification-${type}`); * * if (type === 'error') { * yield* addClass('urgent shake-animation'); * } * }); * ``` * * @example Animation and transition classes * ```typescript * watch('.modal', function* () { * // Prepare for animation * yield* addClass('modal-base'); * * // Trigger animation after a frame * yield* onMount(function* () { * requestAnimationFrame(() => { * yield* addClass('fade-in slide-up'); * }); * }); * }); * ``` */ // Class manipulation with generics export function addClass<T extends Element = HTMLElement>( element: T, className: string | ClassName, ): void; export function addClass<T extends Element = HTMLElement>( selector: string | CSSSelector, className: string | ClassName, ): void; export function addClass(className: string | ClassName): Workflow<void>; export function addClass<T extends Element = HTMLElement>(...args: any[]): any { // Direct element manipulation if (args.length === 2 && isElement(args[0])) { const [element, className] = args as [T, string | ClassName]; const classes = String(className).split(/\s+/).filter(Boolean); element.classList.add(...classes); return; } // CSS selector manipulation if (args.length === 2 && looksLikeSelector(args[0])) { const [selector, className] = args as [ string | CSSSelector, string | ClassName, ]; const classes = String(className).split(/\s+/).filter(Boolean); const elements = resolveElements<T>(String(selector)); elements.forEach((el) => { el.classList.add(...classes); }); return; } // Generator pattern if (args.length === 1) { const [className] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { const classes = String(className).split(/\s+/).filter(Boolean); context.element.classList.add(...classes); }) as Operation<void>; })(); } throw new Error( `Invalid arguments for addClass(): ${args.length} arguments provided`, ); } /** * Removes one or more CSS classes from elements. * * Supports space-separated class names and safely handles non-existent classes. * * @example Direct element manipulation * ```typescript * const modal = document.querySelector('.modal'); * // Remove single class * removeClass(modal, 'hidden'); * // Remove multiple classes * removeClass(modal, 'hidden fade-out disabled'); * ``` * * @example CSS selector pattern * ```typescript * // Remove classes from all matching elements * removeClass('.error-field', 'error highlighted'); * // Clear loading states * removeClass('.loading', 'loading spinner'); * ``` * * @example Generator context * ```typescript * import { watch, removeClass, addClass, delay } from 'watch-selector'; * * watch('.notification', function* () { * yield addClass('visible slide-in'); * yield delay(3000); * yield removeClass('visible'); * yield addClass('slide-out'); * }); * ``` * * @param element - The element to remove classes from (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param className - Space-separated class names to remove * @returns void when used directly, Workflow in generator context */ /** * Removes one or more CSS classes from elements. * * Supports space-separated class names and safely handles non-existent classes. * No error is thrown if a class doesn't exist on the element. * * @param element - HTMLElement to remove classes from (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param className - Single class or space-separated classes to remove * @returns void for direct/selector patterns, Workflow for generator pattern * * @example Removing classes with yield* in generators * ```typescript * import { watch, removeClass, addClass, click } from 'watch-selector'; * * watch('.toggle-button', function* () { * yield* click(function* () { * // Remove multiple classes at once * yield* removeClass('inactive disabled'); * yield* addClass('active enabled'); * }); * }); * ``` * * @example State transitions with class swapping * ```typescript * watch('.status-indicator', function* () { * const status = yield* getState('status', 'pending'); * * // Clear all possible status classes * yield* removeClass('status-pending status-loading status-success status-error'); * * // Add the current status class * yield* addClass(`status-${status}`); * }); * ``` * * @example Animation cleanup * ```typescript * watch('.animated-element', function* () { * yield* click(function* () { * // Trigger animation * yield* addClass('animating bounce'); * * // Clean up after animation completes * setTimeout(() => { * yield* removeClass('animating bounce'); * yield* addClass('animation-complete'); * }, 1000); * }); * }); * ``` */ export function removeClass<T extends Element = HTMLElement>( element: T, className: string | ClassName, ): void; export function removeClass<T extends Element = HTMLElement>( selector: string | CSSSelector, className: string | ClassName, ): void; export function removeClass(className: string | ClassName): Workflow<void>; export function removeClass<T extends Element = HTMLElement>( ...args: any[] ): any { // Direct element manipulation if (args.length === 2 && isElement(args[0])) { const [element, className] = args as [T, string | ClassName]; const classes = String(className).split(/\s+/).filter(Boolean); element.classList.remove(...classes); return; } // CSS selector manipulation if (args.length === 2 && looksLikeSelector(args[0])) { const [selector, className] = args as [ string | CSSSelector, string | ClassName, ]; const classes = String(className).split(/\s+/).filter(Boolean); const elements = resolveElements<T>(String(selector)); elements.forEach((el) => { el.classList.remove(...classes); }); return; } // Generator pattern if (args.length === 1) { const [className] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { const classes = String(className).split(/\s+/).filter(Boolean); context.element.classList.remove(...classes); }) as Operation<void>; })(); } throw new Error( `Invalid arguments for removeClass(): ${args.length} arguments provided`, ); } /** * Sets or gets inline style properties on elements. * * Supports multiple usage patterns: * - Set single property: style(element, 'color', 'red') * - Set multiple properties: style(element, { color: 'red', fontSize: '16px' }) * - Get property value: style(element, 'color') * * @example Direct element - setting styles * ```typescript * const box = document.querySelector('.box'); * // Set single property * style(box, 'backgroundColor', '#ff0000'); * style(box, 'padding', '20px'); * // Set with number (adds 'px' for applicable properties) * style(box, 'width', 200); // becomes '200px' * // Set multiple properties at once *al force parameter. * * @example Direct element manipulation * ```typescript * const panel = document.querySelector('.panel'); * // Toggle class * toggleClass(panel, 'expanded'); * // Force add (true) or remove (false) * toggleClass(panel, 'active', isActive); * ``` * * @example CSS selector pattern * ```typescript * // Toggle on all matching elements * toggleClass('.accordion-item', 'open'); * // Force state based on condition * toggleClass('.menu', 'visible', window.innerWidth > 768); * ``` * * @example Generator context * ```typescript * import { watch, toggleClass, click } from 'watch-selector'; * * watch('.toggle-switch', function* () { * yield click(function* () { * yield toggleClass('on'); * const isOn = yield hasClass('on'); * console.log('Switch is:', isOn ? 'ON' : 'OFF'); * }); * }); * ``` * * @param element - The element to toggle classes on (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param className - Space-separated class names to toggle * @param force - Optional: true to add, false to remove, undefined to toggle * @returns void when used directly, Workflow in generator context */ /** * Toggles CSS classes on elements with optional force flag. * * Intelligently adds or removes classes based on their current presence. * The optional force parameter allows explicit control over the operation. * * @param element - HTMLElement to toggle classes on (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param className - Single class or space-separated classes to toggle * @param force - If true, adds class; if false, removes class; if undefined, toggles * @returns void for direct/selector patterns, Workflow for generator pattern * * @example Basic toggle with yield* in generators * ```typescript * import { watch, toggleClass, click } from 'watch-selector'; * * watch('.expandable', function* () { * yield* click(function* () { * // Toggle expanded state * yield* toggleClass('expanded'); * * // Toggle multiple classes * yield* toggleClass('open active highlighted'); * }); * }); * ``` * * @example Forced toggle based on conditions * ```typescript * watch('.theme-toggle', function* () { * yield* click(function* () { * const isDark = yield* hasClass('dark-mode'); * * // Force toggle based on current state * yield* toggleClass('dark-mode', !isDark); * yield* toggleClass('light-mode', isDark); * }); * }); * ``` * * @example Accordion behavior with toggles * ```typescript * watch('.accordion-item', function* () { * yield* click(function* () { * // Close all other items * const siblings = yield* siblings(); * for (const sibling of siblings) { * toggleClass(sibling, 'expanded', false); * } * * // Toggle current item * yield* toggleClass('expanded'); * }); * }); * ``` */ export function toggleClass<T extends Element = HTMLElement>( element: T, className: string | ClassName, force?: boolean, ): void; export function toggleClass( selector: string | CSSSelector, className: string | ClassName, force?: boolean, ): void; export function toggleClass( className: string | ClassName, force?: boolean, ): Workflow<void>; export function toggleClass(...args: any[]): any { // Direct element manipulation if ((args.length === 2 || args.length === 3) && isHTMLElement(args[0])) { const [element, className, force] = args; const classes = String(className).split(/\s+/).filter(Boolean); classes.forEach((cls) => { element.classList.toggle(cls, force); }); return; } // CSS selector manipulation if ((args.length === 2 || args.length === 3) && looksLikeSelector(args[0])) { const [selector, className, force] = args; const classes = String(className).split(/\s+/).filter(Boolean); const elements = resolveElements(String(selector)); elements.forEach((el) => { classes.forEach((cls) => { el.classList.toggle(cls, force); }); }); return; } // Generator pattern if (args.length === 1 || args.length === 2) { const [className, force] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { const classes = String(className).split(/\s+/).filter(Boolean); classes.forEach((cls) => { context.element.classList.toggle(cls, force); }); }) as Operation<void>; })(); } throw new Error( `Invalid arguments for toggleClass(): ${args.length} arguments provided`, ); } /** * Checks if an element has ALL specified CSS classes. * * Returns true only if the element contains every specified class. * For checking if element has ANY of the classes, use multiple calls. * * @example Direct element manipulation * ```typescript * const button = document.querySelector('button'); * // Check single class * if (hasClass(button, 'active')) { * console.log('Button is active'); * } * // Check multiple classes (ALL must be present) * if (hasClass(button, 'primary large')) { * console.log('Button is primary AND large'); * } * ``` * * @example CSS selector pattern * ```typescript * // Check first matching element * const isVisible = hasClass('.modal', 'visible'); * // Check multiple classes * const isReady = hasClass('.component', 'initialized loaded'); * ``` * * @example Generator context * ```typescript * import { watch, hasClass, addClass, removeClass } from 'watch-selector'; * * watch('.toggle-element', function* () { * const wasActive = yield hasClass('active'); * * if (wasActive) { * yield removeClass('active'); * yield addClass('inactive'); * } else { * yield removeClass('inactive'); * yield addClass('active'); * } * }); * ``` * * @param element - The element to check (direct pattern) * @param selector - CSS selector to find element (selector pattern) * @param className - Space-separated class names to check for * @returns boolean indicating if ALL classes are present, or Workflow<boolean> in generator context */ export function hasClass( element: HTMLElement, className: string | ClassName, ): boolean; /** * Checks if an element has a specific attribute. * * @example Direct element * ```typescript * const input = document.querySelector('input'); * if (hasAttr(input, 'required')) { * console.log('Field is required'); * } * ``` * * @example CSS selector pattern * ```typescript * const hasPlaceholder = hasAttr('input.search', 'placeholder'); * const isDisabled = hasAttr('button.submit', 'disabled'); * ``` * * @example Generator context * ```typescript * import { watch, hasAttr, attr, addClass } from 'watch-selector'; * * watch('input', function* () { * if (yield hasAttr('required')) { * yield addClass('required-field'); * yield attr('aria-required', 'true'); * } * }); * ``` * * @param element - The element to check (direct pattern) * @param selector - CSS selector to find element (selector pattern) * @param name - Attribute name to check for * @returns boolean indicating if attribute exists, Workflow<boolean> in generator context */ export function hasClass( selector: string | CSSSelector, className: string | ClassName, ): boolean; export function hasClass(className: string | ClassName): Workflow<boolean>; export function hasClass(...args: any[]): any { // Direct element check if (args.length === 2 && isHTMLElement(args[0])) { const [element, className] = args; const classes = String(className).split(/\s+/).filter(Boolean); return classes.every((cls) => element.classList.contains(cls)); } // CSS selector check (checks first element) if (args.length === 2 && looksLikeSelector(args[0])) { const [selector, className] = args; const element = document.querySelector(String(selector)); if (!element || !(element instanceof HTMLElement)) return false; const classes = String(className).split(/\s+/).filter(Boolean); return classes.every((cls) => element.classList.contains(cls)); } // Generator pattern if (args.length === 1) { const [className] = args; return (function* (): Generator<Operation<boolean>, boolean, any> { const result = yield ((context: WatchContext) => { const classes = String(className).split(/\s+/).filter(Boolean); return classes.every((cls) => context.element.classList.contains(cls)); }) as Operation<boolean>; return result; })(); } throw new Error( `Invalid arguments for hasClass(): ${args.length} arguments provided`, ); } // ============================================================================ // Style Function // ============================================================================ /** * Sets or gets CSS style properties on elements with type safety. * * Supports setting individual properties, multiple properties via object, * or getting computed style values. Automatically handles vendor prefixes * and unit conversion for numeric values. * * @example Direct element - single property * ```typescript * const div = document.querySelector('.box'); * // Set single style * style(div, 'backgroundColor', 'red'); * style(div, 'width', 100); // Automatically adds 'px' * style(div, 'opacity', 0.5); * * // Get computed style * const width = style(div, 'width'); // returns "100px" * ``` * * @example Direct element - multiple properties * ```typescript * const panel = document.querySelector('.panel'); * style(panel, { * backgroundColor: '#f0f0f0', * padding: 20, // becomes '20px' * borderRadius: '8px', * opacity: 0.9, * display: 'flex' * }); * ``` * * @example CSS selector pattern * ```typescript * // Style all matching elements * style('.card', 'boxShadow', '0 2px 4px rgba(0,0,0,0.1)'); * * // Apply multiple styles * style('.highlighted', { * backgroundColor: 'yellow', * fontWeight: 'bold', * padding: 10 * }); * * // Get style from first match * const bgColor = style('.card', 'backgroundColor'); * ``` * * @example Generator context with animations * ```typescript * import { watch, style, delay } from 'watch-selector'; * * watch('.animate-box', function* () { * // Fade in animation * yield style('opacity', 0); * yield style('transform', 'translateY(20px)'); * * yield delay(100); * * yield style({ * opacity: 1, * transform: 'translateY(0)', * transition: 'all 0.3s ease' * }); * }); * ``` * * @example Responsive styling * ```typescript * import { watch, style, onResize } from 'watch-selector'; * * watch('.responsive-element', function* () { * yield onResize(function* (entry) { * const width = entry.contentRect.width; * * if (width < 600) { * yield style({ fontSize: '14px', padding: '10px' }); * } else { * yield style({ fontSize: '18px', padding: '20px' }); * } * }); * }); * ``` * * @param element - The element to style (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param prop - CSS property name (camelCase or kebab-case) * @param value - Style value (numbers auto-convert to px for applicable properties) * @param styles - Object of property-value pairs for multiple styles * @returns void when setting, string when getting, Workflow in generator context */ /** * Manipulates inline styles on elements with support for objects and individual properties. * * Handles CSS property names in both camelCase and kebab-case formats. * Automatically adds 'px' units to numeric values for applicable properties. * Setting a value to null or empty string removes the style property. * * @param element - HTMLElement to style (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param prop - CSS property name or object of property-value pairs * @param value - CSS value (string, number, or null to remove) * @returns void when setting, string when getting single property, Workflow for generators * * @example Setting styles with yield* in generators * ```typescript * import { watch, style, click } from 'watch-selector'; * * watch('.animated-box', function* () { * // Set single style property * yield* style('background-color', '#3498db'); * yield* style('padding', 20); // Auto-adds 'px' * * // Set multiple styles with object * yield* style({ * width: 200, // Becomes '200px' * height: 100, // Becomes '100px' * backgroundColor: '#2ecc71', * borderRadius: '8px', * transition: 'all 0.3s ease' * }); * }); * ``` * * @example Dynamic styling based on state * ```typescript * watch('.progress-bar', function* () { * const progress = yield* getState('progress', 0); * * yield* style({ * width: `${progress}%`, * backgroundColor: progress === 100 ? '#27ae60' : '#3498db', * transition: 'width 0.5s ease' * }); * * // Get computed style * const currentWidth = yield* style('width'); * console.log('Current width:', currentWidth); * }); * ``` * * @example Animations with dynamic styles * ```typescript * watch('.floating-element', function* () { * let position = 0; * * yield* onMount(function* () { * const animate = () => { * position += 1; * yield* style('transform', `translateY(${Math.sin(position * 0.1) * 10}px)`); * requestAnimationFrame(animate); * }; * animate(); * }); * }); * ``` * * @example Removing styles * ```typescript * watch('.resettable', function* () { * yield* click(function* () { * // Remove specific styles by setting to null * yield* style('backgroundColor', null); * yield* style('border', ''); * * // Or remove multiple at once * yield* style({ * width: null, * height: null, * position: null * }); * }); * }); * ``` */ // Style manipulation with better type inference export function style<T extends HTMLElement = HTMLElement>( element: T, prop: keyof CSSStyleDeclaration | string, value: StyleValue, ): void; export function style<T extends HTMLElement = HTMLElement>( element: T, styles: Partial<CSSStyleDeclaration> | StyleObject, ): void; export function style<T extends HTMLElement = HTMLElement>( element: T, prop: keyof CSSStyleDeclaration | string, ): string; export function style( selector: string | CSSSelector, prop: keyof CSSStyleDeclaration | string, value: StyleValue, ): void; export function style( selector: string | CSSSelector, styles: Partial<CSSStyleDeclaration> | StyleObject, ): void; export function style( selector: string | CSSSelector, prop: keyof CSSStyleDeclaration | string, ): string | null; export function style( prop: keyof CSSStyleDeclaration | string, value: StyleValue, ): Workflow<void>; export function style( styles: Partial<CSSStyleDeclaration> | StyleObject, ): Workflow<void>; export function style( prop: keyof CSSStyleDeclaration | string, ): Workflow<string>; export function style(...args: any[]): any { const applyStyles = <E extends HTMLElement>( element: E, propOrStyles: StyleObject | string, value?: StyleValue, ) => { if (typeof propOrStyles === "object") { Object.entries(propOrStyles).forEach(([prop, val]) => { if (val !== null && val !== undefined) { (element.style as any)[prop] = String(val); } else { (element.style as any)[prop] = ""; } }); } else { if (value !== null && value !== undefined) { (element.style as any)[propOrStyles] = String(value); } else { (element.style as any)[propOrStyles] = ""; } } }; // Direct element manipulation if (isHTMLElement(args[0])) { const [element, propOrStyles, value] = args; applyStyles(element, propOrStyles, value); return; } // CSS selector manipulation with 3 args (selector, prop, value) if (args.length === 3 && looksLikeSelector(args[0])) { const [selector, prop, value] = args; const elements = resolveElements(String(selector)); elements.forEach((el) => { applyStyles(el, prop, value); }); return; } // CSS selector manipulation with 2 args (selector, styles object) if ( args.length === 2 && looksLikeSelector(args[0]) && typeof args[1] === "object" ) { const [selector, styles] = args; const elements = resolveElements(String(selector)); elements.forEach((el) => { applyStyles(el, styles); }); return; } // Generator setter with prop/value if ( args.length === 2 && typeof args[0] === "string" && typeof args[1] !== "object" ) { const [prop, value] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { applyStyles(context.element, prop, value); }) as Operation<void>; })(); } // Generator setter with object if (args.length === 1 && typeof args[0] === "object") { const [styles] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { applyStyles(context.element, styles); }) as Operation<void>; })(); } // Generator getter if (args.length === 1 && typeof args[0] === "string") { const [prop] = args; return (function* (): Generator<Operation<string>, string, any> { const result = yield ((context: WatchContext) => { return getComputedStyle(context.element).getPropertyValue(prop); }) as Operation<string>; return result; })(); } throw new Error( `Invalid arguments for style(): ${args.length} arguments provided`, ); } // ============================================================================ // Attribute Functions // ============================================================================ export function attr<T extends Element = HTMLElement>( element: T, name: string, value: string | number | boolean, ): void; export function attr<T extends Element = HTMLElement>( element: T, attrs: AttributeObject, ): void; export function attr<T extends Element = HTMLElement>( element: T, name: string, ): string | null; export function attr( selector: string | CSSSelector, name: string, value: string | number | boolean, ): void; export function attr( selector: string | CSSSelector, attrs: AttributeObject, ): void; export function attr( selector: string | CSSSelector, name: string, ): string | null; export function attr( name: string, value: string | number | boolean, ): Workflow<void>; export function attr(attrs: AttributeObject): Workflow<void>; export function attr(name: string): Workflow<string | null>; export function attr<T extends Element = HTMLElement>(...args: any[]): any { const applyAttrs = <E extends Element>( element: E, nameOrAttrs: AttributeObject | string, value?: string | number | boolean, ) => { if (typeof nameOrAttrs === "object") { Object.entries(nameOrAttrs).forEach(([name, val]) => { if (val !== null && val !== undefined) { element.setAttribute(name, String(val)); } else { element.removeAttribute(name); } }); } else { if (value !== null && value !== undefined) { element.setAttribute(nameOrAttrs, String(value)); } else { element.removeAttribute(nameOrAttrs); } } }; // Direct element manipulation setter if (args.length >= 2 && isElement(args[0])) { const [element, nameOrAttrs, value] = args as [ T, string | AttributeObject, string | number | boolean | undefined, ]; if (typeof nameOrAttrs === "string" && value === undefined) { // Getter pattern return element.getAttribute(nameOrAttrs); } applyAttrs(element, nameOrAttrs, value as string | number | boolean); return; } // CSS selector manipulation with 3 args (selector, name, value) if (args.length === 3 && looksLikeSelector(args[0])) { const [selector, name, value] = args; const elements = resolveElements(String(selector)); elements.forEach((el) => { applyAttrs(el, name, value); }); return; } // CSS selector manipulation with 2 args (selector, attrs object) if ( args.length === 2 && looksLikeSelector(args[0]) && typeof args[1] === "object" ) { const [selector, attrs] = args; const elements = resolveElements(String(selector)); elements.forEach((el) => { applyAttrs(el, attrs); }); return; } // Generator setter with name/value if ( args.length === 2 && typeof args[0] === "string" && typeof args[1] !== "object" ) { const [name, value] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { if (typeof value === "boolean") { if (value) { context.element.setAttribute(name, ""); } else { context.element.removeAttribute(name); } } else { context.element.setAttribute(name, String(value)); } }) as Operation<void>; })(); } // Generator setter with object if (args.length === 1 && typeof args[0] === "object") { const [attrs] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { applyAttrs(context.element, attrs); }) as Operation<void>; })(); } // Generator getter if (args.length === 1 && typeof args[0] === "string") { const [name] = args; return (function* (): Generator< Operation<string | null>, string | null, any > { const result = yield ((context: WatchContext) => { return context.element.getAttribute(name); }) as Operation<string | null>; return result; })(); } throw new Error( `Invalid arguments for attr(): ${args.length} arguments provided`, ); } export function removeAttr<T extends HTMLElement = HTMLElement>( element: T, name: string | string[], ): void; export function removeAttr( selector: string | CSSSelector, name: string | string[], ): void; export function removeAttr(name: string | string[]): Workflow<void>; export function removeAttr(...args: any[]): any { // Direct element manipulation if (args.length === 2 && isHTMLElement(args[0])) { const [element, name] = args; const names = Array.isArray(name) ? name : [name]; names.forEach((n) => element.removeAttribute(n)); return; } // CSS selector manipulation if (args.length === 2 && looksLikeSelector(args[0])) { const [selector, name] = args; const elements = resolveElements(String(selector)); elements.forEach((el) => { el.removeAttribute(name); }); return; } // Generator pattern if (args.length === 1) { const [name] = args; return (function* (): Generator<Operation<void>, void, any> { yield ((context: WatchContext) => { const names = Array.isArray(name) ? name : [name]; names.forEach((n) => context.element.removeAttribute(n)); }) as Operation<void>; })(); } throw new Error( `Invalid arguments for removeAttr(): ${args.length} arguments provided`, ); } export function hasAttr<T extends HTMLElement = HTMLElement>( element: T, name: string, ): boolean; export function hasAttr(selector: string | CSSSelector, name: string): boolean; export function hasAttr(name: string): Workflow<boolean>; export function hasAttr(...args: any[]): any { // Direct element check if (args.length === 2 && isHTMLElement(args[0])) { const [element, name] = args; return element.hasAttribute(name); } // CSS selector check if (args.length === 2 && looksLikeSelector(args[0])) { const [selector, name] = args; const element = document.querySelector(String(selector)); if (!element) return false; return element.hasAttribute(name); } // Generator pattern if (args.length === 1) { const [name] = args; return (function* (): Generator<Operation<boolean>, boolean, any> { const result = yield ((context: WatchContext) => { return context.element.hasAttribute(name); }) as Operation<boolean>; return result; })(); } throw new Error( `Invalid arguments for hasAttr(): ${args.length} arguments provided`, ); } // ============================================================================ // Property Functions // ============================================================================ /** * Manipulates JavaScript properties on DOM elements. * * Properties are part of the DOM object and can be any JavaScript type (boolean, number, object, etc). * Use this for properties like 'checked', 'disabled', 'value', 'selectedIndex', custom properties, etc. * Properties reflect the current state, while attributes represent the initial HTML markup. * * @param element - HTMLElement to manipulate properties on (direct pattern) * @param selector - CSS selector to find elements (selector pattern) * @param name - Property name to get or set * @param value - Property value of any type * @returns void when setting, T when getting, Workflow for generators * * @example Boolean properties with yield* * ```typescript * import { watch, prop, click } from 'watch-selector'; * * watch('.checkbox-wrapper', function* () { * // Set boolean properties * yield* prop('checked', true); * yield* prop('disabled', false); * yield* prop('required', true); * * yield* click(function* () { * // Toggle checked state * const isChecked = yield* prop('checked'); * yield* prop('checked', !isChecked); * * // Update related elements * if (!isChecked) { * yield* addClass('selected'); * } else { * yield* removeClass('selected'); * } * }); * }); * ``` * * @example Form element properties * ```typescript * watch('.form-select', function* () { * // Set selected index * yield* prop('selectedIndex', 2); * * // Get current selection * const index = yield* prop('selectedIndex'); * const selectedOption = yield* prop('selectedOptions'); * * yield* change(function* () { * const value = yield* prop('value'); * console.log('Selected:', value); * * // Enable submit button when something is selected * const submitBtn = yield* query('.submit-btn'); * if (submitBtn) { * prop(submitBtn, 'disabled', !value); * } * }); * }); * ``` * * @example Custom properties and objects * ```typescript * watch('.data-container', function* () { * // Store complex data as properties * yield* prop('customData', { * id: 123, * name: 'Test Item', * metadata: { created: new Date() } * }); * * // Store functions as properties * yield* prop('validator', (value: string) => { * return value.length > 0 && value.length < 100; * }); * * // Retrieve and use custom propert