UNPKG

watch-selector

Version:

Runs a function when a selector is added to dom

1,674 lines (1,519 loc) 92.3 kB
// Comprehensive DOM manipulation functions with dual API support import type { ElementFn, ElementFromSelector, GeneratorFunction, TypedGeneratorContext, } from "../types"; import { cleanup, executeElementCleanup } from "../core/generator"; import { getCurrentContext, getCurrentElement, registerParentContext, unregisterParentContext, pushContext, popContext, executeGenerator, } from "../core/context"; import { getState, setState } from "../core/state"; import { detectContext, ApiContext } from "../core/detection"; // Type guards and utilities /** * Type guard to check if a value is an HTMLElement. * * This is a utility function that provides type-safe checking for HTMLElement instances. * It's particularly useful when working with dynamic values or when you need to ensure * type safety in your DOM manipulation code. * * @param value - The value to check * @returns True if the value is an HTMLElement, false otherwise * * @example * ```typescript * import { isElement } from 'watch-selector'; * * const maybeElement = document.querySelector('button'); * if (isElement(maybeElement)) { * // TypeScript now knows maybeElement is HTMLElement * maybeElement.click(); * } * * // Use in filtering arrays * const elements = [div, null, span, undefined].filter(isElement); * // elements is now HTMLElement[] * ``` */ export function isElement(value: any): value is HTMLElement { return value instanceof HTMLElement; } /** * Type guard to check if a value can be used as an element reference. * * This function checks if a value is either an HTMLElement or a string (CSS selector). * It's useful for functions that accept both direct element references and CSS selectors. * * @param value - The value to check * @returns True if the value is an HTMLElement or string, false otherwise * * @example Direct element validation * ```typescript * import { isElementLike } from 'watch-selector'; * * function processElement(target: unknown) { * if (isElementLike(target)) { * // TypeScript knows target is HTMLElement | string * const element = resolveElement(target); * if (element) { * element.focus(); * } * } * } * * processElement(document.getElementById('my-button')); // Valid * processElement('#my-button'); // Valid * processElement(123); // Invalid - won't pass type guard * ``` * * @example Array filtering * ```typescript * import { isElementLike } from 'watch-selector'; * * const mixed = [ * document.getElementById('btn1'), * '#btn2', * null, * '.btn3', * undefined, * document.querySelector('.btn4') * ]; * * const validTargets = mixed.filter(isElementLike); * // validTargets is now (HTMLElement | string)[] * ``` */ export function isElementLike(value: any): value is HTMLElement | string { return typeof value === "string" || value instanceof HTMLElement; } /** * Resolves an element-like value to an actual HTMLElement. * * This function takes either an HTMLElement or a CSS selector string and returns * the corresponding HTMLElement. If a string is provided, it uses querySelector * to find the element. This is the core utility function used throughout the * library for element resolution. * * @param elementLike - Either an HTMLElement or a CSS selector string * @returns The resolved HTMLElement, or null if not found or invalid * * @example Direct element reference * ```typescript * import { resolveElement } from 'watch-selector'; * * const button = document.getElementById('my-button'); * const resolved = resolveElement(button); // Returns the button element directly * * console.log(resolved === button); // true * ``` * * @example CSS selector resolution * ```typescript * import { resolveElement } from 'watch-selector'; * * const resolved1 = resolveElement('#my-button'); // Finds and returns the element * const resolved2 = resolveElement('.not-found'); // Returns null if not found * const resolved3 = resolveElement('button:first-child'); // Complex selectors work * * if (resolved1) { * resolved1.click(); // Safe to use after null check * } * ``` * * @example Safe usage pattern * ```typescript * import { resolveElement } from 'watch-selector'; * * function safeClick(target: HTMLElement | string) { * const element = resolveElement(target); * if (element) { * element.click(); // Type-safe usage * } else { * console.warn('Element not found:', target); * } * } * * // Works with both patterns * safeClick('#submit-btn'); // CSS selector * safeClick(buttonElement); // Direct element * ``` * * @example Error handling for invalid selectors * ```typescript * import { resolveElement } from 'watch-selector'; * * // Invalid selector syntax is safely handled * const result = resolveElement('>>invalid<<selector'); // Returns null * console.log(result); // null - no exception thrown * ``` */ export function resolveElement( elementLike: HTMLElement | string, ): HTMLElement | null { if (typeof elementLike === "string") { try { return document.querySelector(elementLike); } catch { return null; } } if (elementLike instanceof HTMLElement) { return elementLike; } return null; } // Internal implementations for text function function _impl_text_set(element: HTMLElement, content: string): void { element.textContent = String(content); } function _impl_text_get(element: HTMLElement): string { return element.textContent ?? ""; } // Predicates for text function overloads function _is_text_direct_set(args: any[]): boolean { return args.length === 2 && isElementLike(args[0]); } function _is_text_direct_get(args: any[]): boolean { // Only treat as direct get if it's an HTMLElement return args.length === 1 && args[0] instanceof HTMLElement; } function _is_text_selector_get(args: any[]): boolean { // Treat as selector get if it's a string that looks like a selector return ( args.length === 1 && typeof args[0] === "string" && _looksLikeSelector(args[0]) ); } function _is_text_generator(args: any[]): boolean { // Everything else is generator mode return ( args.length <= 1 && !(args[0] instanceof HTMLElement) && (args.length === 0 || !_looksLikeSelector(args[0])) ); } function _looksLikeSelector(str: string): boolean { if (typeof str !== "string") return false; // If it looks like HTML (contains < or starts with <), it's not a selector if (str.includes("<") || str.startsWith("<")) { return false; } // Common selector patterns (excluding plain tag names to avoid false positives) // Only spaces that are part of combinators (like "div > span") should be considered selectors return ( str.includes(".") || str.includes("#") || str.includes("[") || str.includes(":") || str.includes(">") || str.includes("+") || str.includes("~") || str.includes("*") || (str.includes(" ") && (str.includes(">") || str.includes("+") || str.includes("~") || str.includes("."))) ); } // TEXT CONTENT /** * Gets or sets the text content of an element using the dual API pattern. * * This function provides a versatile way to manipulate text content that works both * directly with elements and within watch generators. It supports multiple usage patterns: * direct element manipulation, CSS selector-based manipulation, and generator-based * manipulation for use within watch functions. * * The function automatically handles type safety and provides different return types * based on the usage pattern. When used in generator mode, it returns an ElementFn * that can be yielded within a watch generator. * * @param element - HTMLElement to manipulate (direct API) * @param content - Text content to set * @returns void when setting, string when getting, ElementFn when in generator mode * * @example Direct API - Setting text * ```typescript * import { text } from 'watch-selector'; * * const button = document.getElementById('my-button'); * text(button, 'Click me!'); // Sets text content directly * * // Using CSS selector * text('#my-button', 'Click me!'); // Finds element and sets text * ``` * * @example Direct API - Getting text * ```typescript * import { text } from 'watch-selector'; * * const button = document.getElementById('my-button'); * const content = text(button); // Returns current text content * * // Using CSS selector * const content2 = text('#my-button'); // Returns text or null if not found * ``` * * @example Generator API - Within watch functions * ```typescript * import { watch, text, click } from 'watch-selector'; * * watch('button', function* () { * // Set initial text * yield text('Ready'); * * let count = 0; * yield click(function* () { * count++; * yield text(`Clicked ${count} times`); * }); * }); * ``` * * @example Generator API - Reading text in generators * ```typescript * import { watch, text, self } from 'watch-selector'; * * watch('.status', function* () { * // Get current text content * const currentText = yield text(); * console.log('Current status:', currentText); * * // Update based on current content * if (currentText === 'idle') { * yield text('active'); * } * }); * ``` * * @example Advanced usage with form elements * ```typescript * import { watch, text, input } from 'watch-selector'; * * watch('.character-counter', function* () { * const input = self().querySelector('input'); * * yield input(function* (event) { * const length = (event.target as HTMLInputElement).value.length; * yield text(`${length}/100 characters`); * }); * }); * ``` */ // Generator overloads first (more specific) export function text<El extends HTMLElement = HTMLElement>( content: string, ): ElementFn<El>; export function text<El extends HTMLElement = HTMLElement>(): ElementFn< El, string >; // Direct element overloads export function text(element: HTMLElement, content: string): void; export function text(element: HTMLElement): string; // Selector overloads last (catch-all for strings) export function text(selector: string, content: string): void; export function text(selector: string): string | null; export function text(...args: any[]): any { const detection = detectContext(args, text); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: text(element, value) or text(selector, value) const [target, content] = args; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element && args.length === 2) { // Setter with selector that doesn't match - just return return; } else if (!element && args.length === 1) { // Getter with selector that doesn't match return null; } } else if (target instanceof HTMLElement) { element = target; } if (!element) { // Handle null/undefined gracefully return args.length === 2 ? undefined : null; } // Setter or getter? if (args.length === 2) { _impl_text_set(element, content); return; } else { return _impl_text_get(element); } } case ApiContext.SYNC_GENERATOR: { // Sync generator mode: yield text(value) or const val = yield text() const [content] = args; if (content === undefined) { // Getter mode return ((element: HTMLElement) => _impl_text_get(element)) as ElementFn< HTMLElement, string >; } else { // Setter mode return ((element: HTMLElement) => { _impl_text_set(element, content); }) as ElementFn<HTMLElement, void>; } } case ApiContext.ASYNC_GENERATOR: { // For now, handle async generators the same as sync // In the future, we'll return proper Workflow objects const [content] = args; if (content === undefined) { // Getter mode return ((element: HTMLElement) => _impl_text_get(element)) as ElementFn< HTMLElement, string >; } else { // Setter mode return ((element: HTMLElement) => { _impl_text_set(element, content); }) as ElementFn<HTMLElement, void>; } } default: { // Unknown context - try to determine based on arguments // This maintains backwards compatibility if (_is_text_direct_set(args)) { const [elementLike, content] = args; const element = resolveElement(elementLike); if (element) { _impl_text_set(element, content); } return; } if (_is_text_direct_get(args)) { const [element] = args; return _impl_text_get(element); } if (_is_text_selector_get(args)) { const [selector] = args; const element = resolveElement(selector); return element ? _impl_text_get(element) : null; } // Generator mode - use the proper check if (_is_text_generator(args)) { const [content] = args; if (content === undefined) { return ((element: HTMLElement) => _impl_text_get(element)) as ElementFn<HTMLElement, string>; } else { return ((element: HTMLElement) => { _impl_text_set(element, content); }) as ElementFn<HTMLElement, void>; } } // Handle null/undefined gracefully if (args[0] === null || args[0] === undefined) { return args.length === 2 ? undefined : null; } throw new Error("Invalid arguments for text function"); } } } // Internal implementations for html function function _impl_html_set(element: HTMLElement, content: string): void { // WARNING: Direct innerHTML assignment can introduce XSS vulnerabilities // Consider using a safer alternative for untrusted content console.warn( "[watch-selector] Direct innerHTML assignment detected. Use safeHtml() or text() for untrusted content to prevent XSS.", ); element.innerHTML = String(content); } function _impl_html_get(element: HTMLElement): string { return element.innerHTML; } // HTML CONTENT // Generator overloads first (more specific) export function html<El extends HTMLElement = HTMLElement>( content: string, ): ElementFn<El>; export function html<El extends HTMLElement = HTMLElement>(): ElementFn< El, string >; // Direct element overloads export function html(element: HTMLElement, content: string): void; export function html(element: HTMLElement): string; // Selector overloads last (catch-all for strings) export function html(selector: string, content: string): void; export function html(selector: string): string | null; export function html(...args: any[]): any { const detection = detectContext(args, html); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: html(element, value) or html(selector, value) const [target, content] = args; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element && args.length === 2) { // Setter with selector that doesn't match - just return return; } else if (!element && args.length === 1) { // Getter with selector that doesn't match return null; } } else if (target instanceof HTMLElement) { element = target; } if (!element) { throw new Error(`Invalid target for html function`); } // Setter or getter? if (args.length === 2) { _impl_html_set(element, content); return; } else { return _impl_html_get(element); } } case ApiContext.SYNC_GENERATOR: case ApiContext.ASYNC_GENERATOR: { // Generator mode: yield html(value) or const val = yield html() const [content] = args; if (content === undefined) { // Getter mode return ((element: HTMLElement) => _impl_html_get(element)) as ElementFn< HTMLElement, string >; } else { // Setter mode return ((element: HTMLElement) => { _impl_html_set(element, content); }) as ElementFn<HTMLElement, void>; } } default: { // Fallback to original logic for unknown context if (args.length === 2 && isElementLike(args[0])) { const [elementLike, content] = args; const element = resolveElement(elementLike); if (element) { _impl_html_set(element, content); } return; } if (args.length === 1 && args[0] instanceof HTMLElement) { const [element] = args; return _impl_html_get(element); } if ( args.length === 1 && typeof args[0] === "string" && _looksLikeSelector(args[0]) ) { const [selector] = args; const element = resolveElement(selector); return element ? _impl_html_get(element) : null; } if (args.length <= 1) { const [content] = args; if (content === undefined) { return ((element: HTMLElement) => _impl_html_get(element)) as ElementFn<HTMLElement, string>; } else { return ((element: HTMLElement) => { _impl_html_set(element, content); }) as ElementFn<HTMLElement, void>; } } return ((element: HTMLElement) => _impl_html_get(element)) as ElementFn< HTMLElement, string >; } } } /** * Sets sanitized HTML content on an element to prevent XSS attacks. * Removes dangerous elements like script, iframe, and event handlers. * * @param args - Overloaded parameters supporting multiple usage patterns * @returns void, ElementFn, or the element depending on usage * * @example Direct element usage * ```typescript * const div = document.getElementById('content'); * safeHtml(div, userGeneratedContent); * ``` * * @example Selector usage * ```typescript * safeHtml('#content', untrustedHtml); * ``` * * @example Generator usage * ```typescript * watch('.user-content', function* () { * yield safeHtml(userInput); * }); * ``` */ export function safeHtml<El extends HTMLElement = HTMLElement>( ...args: any[] ): any { // Helper function to sanitize HTML content function sanitizeHtml(content: string): string { // Create a temporary element to parse the HTML const temp = document.createElement("div"); temp.innerHTML = content; // Remove dangerous elements const dangerousElements = temp.querySelectorAll( "script, iframe, object, embed, link, style, meta, base", ); dangerousElements.forEach((elem) => elem.remove()); // Remove dangerous attributes const allElements = temp.querySelectorAll("*"); allElements.forEach((elem) => { // Remove event handlers and javascript: URLs for (const attr of Array.from(elem.attributes)) { if ( attr.name.startsWith("on") || (attr.name === "href" && attr.value.startsWith("javascript:")) || (attr.name === "src" && attr.value.startsWith("javascript:")) ) { elem.removeAttribute(attr.name); } } }); return temp.innerHTML; } // Direct element usage: safeHtml(element, content) if (args.length === 2 && isElementLike(args[0])) { const [elementLike, content] = args; const element = resolveElement(elementLike); if (element) { element.innerHTML = sanitizeHtml(String(content)); } return; } // Selector usage: safeHtml(selector, content) if (args.length === 2 && typeof args[0] === "string") { const [selector, content] = args; const element = resolveElement(selector); if (element) { element.innerHTML = sanitizeHtml(String(content)); } return; } // Generator usage: yield safeHtml(content) if (args.length === 1) { const [content] = args; return ((element: El) => { element.innerHTML = sanitizeHtml(String(content)); }) as ElementFn<El, void>; } throw new Error("Invalid arguments for safeHtml"); } // Internal implementations for addClass function function _impl_addClass(element: HTMLElement, ...classNames: string[]): void { const splitClassNames = classNames.flatMap((name) => name.split(/\s+/).filter(Boolean), ); element.classList.add(...splitClassNames); } // CLASS MANIPULATION // Generator overload first export function addClass<El extends HTMLElement = HTMLElement>( ...classNames: string[] ): ElementFn<El>; // Direct element and selector overloads export function addClass(element: HTMLElement, ...classNames: string[]): void; export function addClass(selector: string, ...classNames: string[]): void; export function addClass(...args: any[]): any { const detection = detectContext(args, addClass); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: addClass(element, ...classNames) or addClass(selector, ...classNames) const [target, ...classNames] = args; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string" && !_looksLikeSelector(target)) { // If it doesn't look like a selector, it might be a class name in generator mode // Fall through to generator handling break; } else if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element) { return; // Selector didn't match } } else if (target instanceof HTMLElement) { element = target; } else if (target === null || target === undefined) { // Handle null/undefined gracefully return; } if (element && classNames.length > 0) { _impl_addClass(element, ...classNames); } return; } case ApiContext.SYNC_GENERATOR: case ApiContext.ASYNC_GENERATOR: { // Generator mode: yield addClass(...classNames) const classNames = args; return ((element: HTMLElement) => { _impl_addClass(element, ...classNames); }) as ElementFn<HTMLElement, void>; } default: { // Fallback to original logic if (args.length >= 2 && isElementLike(args[0])) { const [elementLike, ...classNames] = args; const element = resolveElement(elementLike); if (element) { _impl_addClass(element, ...classNames); } return; } if (args.length >= 1 && args[0] instanceof HTMLElement) { const [element, ...classNames] = args; _impl_addClass(element, ...classNames); return; } if ( args.length >= 1 && typeof args[0] === "string" && args.length >= 2 && _looksLikeSelector(args[0]) ) { const [selector, ...classNames] = args; const element = resolveElement(selector); if (element) { _impl_addClass(element, ...classNames); } return; } // Generator mode const allClassNames = args; return ((element: HTMLElement) => { _impl_addClass(element, ...allClassNames); }) as ElementFn<HTMLElement, void>; } } } // Internal implementations for removeClass function function _impl_removeClass( element: HTMLElement, ...classNames: string[] ): void { const splitClassNames = classNames.flatMap((name) => name.split(/\s+/).filter(Boolean), ); element.classList.remove(...splitClassNames); } // Generator overload first export function removeClass<El extends HTMLElement = HTMLElement>( ...classNames: string[] ): ElementFn<El>; // Direct element and selector overloads export function removeClass( element: HTMLElement, ...classNames: string[] ): void; export function removeClass(selector: string, ...classNames: string[]): void; export function removeClass(...args: any[]): any { const detection = detectContext(args, removeClass); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: removeClass(element, ...classNames) or removeClass(selector, ...classNames) const [target, ...classNames] = args; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string" && !_looksLikeSelector(target)) { // If it doesn't look like a selector, it might be a class name in generator mode // Fall through to generator handling break; } else if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element) { return; // Selector didn't match } } else if (target instanceof HTMLElement) { element = target; } if (element && classNames.length > 0) { _impl_removeClass(element, ...classNames); } return; } case ApiContext.SYNC_GENERATOR: case ApiContext.ASYNC_GENERATOR: { // Generator mode: yield removeClass(...classNames) const classNames = args; return ((element: HTMLElement) => { _impl_removeClass(element, ...classNames); }) as ElementFn<HTMLElement, void>; } default: { // Fallback to original logic if (args.length >= 2 && isElementLike(args[0])) { const [elementLike, ...classNames] = args; const element = resolveElement(elementLike); if (element) { _impl_removeClass(element, ...classNames); } return; } if (args.length >= 1 && args[0] instanceof HTMLElement) { const [element, ...classNames] = args; _impl_removeClass(element, ...classNames); return; } if ( args.length >= 1 && typeof args[0] === "string" && args.length >= 2 && _looksLikeSelector(args[0]) ) { const [selector, ...classNames] = args; const element = resolveElement(selector); if (element) { _impl_removeClass(element, ...classNames); } return; } // Generator mode const allClassNames = args; return ((element: HTMLElement) => { _impl_removeClass(element, ...allClassNames); }) as ElementFn<HTMLElement, void>; } } } function _impl_toggleClass( element: HTMLElement, className: string, force?: boolean, ): boolean { return element.classList.toggle(className, force); } // Generator overload first export function toggleClass<El extends HTMLElement = HTMLElement>( className: string, force?: boolean, ): ElementFn<El, boolean>; // Direct element and selector overloads export function toggleClass( element: HTMLElement, className: string, force?: boolean, ): boolean; export function toggleClass( selector: string, className: string, force?: boolean, ): boolean; export function toggleClass(...args: any[]): any { const detection = detectContext(args, toggleClass); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: toggleClass(element, className, force) or toggleClass(selector, className, force) const [target, className, force] = args; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string" && !_looksLikeSelector(target)) { // If it doesn't look like a selector, it might be a class name in generator mode // Fall through to generator handling break; } else if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element) { return false; // Selector didn't match } } else if (target instanceof HTMLElement) { element = target; } if (element && className) { return _impl_toggleClass(element, className, force); } return false; } case ApiContext.SYNC_GENERATOR: case ApiContext.ASYNC_GENERATOR: { // Generator mode: yield toggleClass(className, force) const [className, force] = args; return ((element: HTMLElement) => { return _impl_toggleClass(element, className, force); }) as ElementFn<HTMLElement, boolean>; } default: { // Fallback to original logic // Direct call: toggleClass(element, 'class', true) if (args.length >= 2 && isElementLike(args[0])) { const [elementLike, className, force] = args; const element = resolveElement(elementLike); if (element) { return _impl_toggleClass(element, className, force); } return false; } // Generator mode: toggleClass('class', true) if (args.length <= 2 && typeof args[0] === "string") { const [className, force] = args; return (element: HTMLElement) => _impl_toggleClass(element, className, force); } // Fallback for safety, though should not be reached with correct usage return (element: HTMLElement) => _impl_toggleClass(element, args[0], args[1]); } } } // Internal implementations for hasClass function function _impl_hasClass(element: HTMLElement, className: string): boolean { return element.classList.contains(className); } // Generator overload first export function hasClass<El extends HTMLElement = HTMLElement>( className: string, ): ElementFn<El, boolean>; // Direct element and selector overloads export function hasClass(element: HTMLElement, className: string): boolean; export function hasClass(selector: string, className: string): boolean; export function hasClass(...args: any[]): any { if (args.length === 2 && isElementLike(args[0])) { const [elementLike, className] = args; const element = resolveElement(elementLike); return element ? _impl_hasClass(element, className) : false; } if (args.length === 2 && args[0] instanceof HTMLElement) { const [element, className] = args; return _impl_hasClass(element, className); } if ( args.length === 2 && typeof args[0] === "string" && _looksLikeSelector(args[0]) ) { const [selector, className] = args; const element = resolveElement(selector); return element ? _impl_hasClass(element, className) : false; } // Generator mode const [className] = args; return (element: HTMLElement) => _impl_hasClass(element, className); } // Internal implementations for style function function _impl_style_set_object( element: HTMLElement, styles: Partial<CSSStyleDeclaration>, ): void { for (const [property, value] of Object.entries(styles)) { if (property.startsWith("--")) { // CSS custom properties must be set using setProperty element.style.setProperty(property, value as string); } else { // Regular properties can use direct assignment (element.style as any)[property] = value; } } } function _impl_style_set_property( element: HTMLElement, property: string, value: string, ): void { element.style.setProperty(property, value); } function _impl_style_get_property( element: HTMLElement, property: string, ): string { return ( element.style.getPropertyValue(property) || (element.style as any)[property] || "" ); } // Predicates for style function overloads function _is_style_direct_set_object(args: any[]): boolean { return ( args.length === 2 && isElementLike(args[0]) && typeof args[1] === "object" && args[1] !== null ); } function _is_style_direct_set_property(args: any[]): boolean { return ( args.length === 3 && isElementLike(args[0]) && typeof args[1] === "string" && args[2] !== undefined ); } function _is_style_direct_get_property(args: any[]): boolean { return ( args.length === 2 && isElementLike(args[0]) && typeof args[1] === "string" ); } function _is_style_generator_object(args: any[]): boolean { return args.length === 1 && typeof args[0] === "object" && args[0] !== null; } function _is_style_generator_property(args: any[]): boolean { return ( args.length === 2 && typeof args[0] === "string" && args[1] !== undefined ); } // STYLE MANIPULATION // Generator overloads first export function style<El extends HTMLElement = HTMLElement>( styles: Partial<CSSStyleDeclaration> | Record<string, string>, ): ElementFn<El>; export function style<El extends HTMLElement = HTMLElement>( property: string, value: string, ): ElementFn<El>; // Direct element overloads export function style(element: HTMLElement, property: string): string; export function style( element: HTMLElement, styles: Partial<CSSStyleDeclaration> | Record<string, string>, ): void; export function style( element: HTMLElement, property: string, value: string, ): void; // Selector overloads last export function style(selector: string, property: string): string | null; export function style( selector: string, styles: Partial<CSSStyleDeclaration> | Record<string, string>, ): void; export function style(selector: string, property: string, value: string): void; export function style(...args: any[]): any { const detection = detectContext(args, style); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: style(element/selector, ...) const [target, propertyOrStyles, value] = args; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element) { // For getters, return null; for setters, just return return args.length === 2 && typeof propertyOrStyles === "string" ? null : undefined; } } else if (target instanceof HTMLElement) { element = target; } if (!element) { // Handle null/undefined gracefully for direct/selector calls if (target === null || target === undefined) { return args.length === 3 ? undefined : args.length === 2 && typeof propertyOrStyles === "string" ? null : undefined; } // Not a direct/selector call, fall through break; } // Handle different argument patterns if (args.length === 3) { // Set single property: style(element, 'width', '100px') _impl_style_set_property(element, propertyOrStyles, value); return; } else if (args.length === 2) { if (typeof propertyOrStyles === "object" && propertyOrStyles !== null) { // Set multiple styles: style(element, {width: '100px'}) _impl_style_set_object(element, propertyOrStyles); return; } else if (typeof propertyOrStyles === "string") { // Get property: style(element, 'width') return _impl_style_get_property(element, propertyOrStyles); } } return; } case ApiContext.SYNC_GENERATOR: case ApiContext.ASYNC_GENERATOR: { // Generator mode: yield style(...) if (args.length === 2 && typeof args[0] === "string") { // Set single property: yield style('width', '100px') const [property, value] = args; return ((element: HTMLElement) => _impl_style_set_property(element, property, value)) as ElementFn< HTMLElement, void >; } else if (args.length === 1) { if (typeof args[0] === "object" && args[0] !== null) { // Set multiple styles: yield style({width: '100px'}) const [styles] = args; return ((element: HTMLElement) => _impl_style_set_object(element, styles)) as ElementFn< HTMLElement, void >; } else if (typeof args[0] === "string") { // Get property: yield style('width') const [property] = args; return ((element: HTMLElement) => _impl_style_get_property(element, property)) as ElementFn< HTMLElement, string >; } } return ((element: HTMLElement) => _impl_style_get_property(element, "")) as ElementFn< HTMLElement, string >; } default: { // Fallback to original logic if (_is_style_direct_set_object(args)) { const [elementLike, styles] = args; const element = resolveElement(elementLike); if (element) { _impl_style_set_object(element, styles); } return; } if (_is_style_direct_set_property(args)) { const [elementLike, property, value] = args; const element = resolveElement(elementLike); if (element) { _impl_style_set_property(element, property, value); } return; } if (_is_style_direct_get_property(args)) { const [elementLike, property] = args; const element = resolveElement(elementLike); return element ? _impl_style_get_property(element, property) : ""; } if (_is_style_generator_object(args)) { const [styles] = args; return ((element: HTMLElement) => _impl_style_set_object(element, styles)) as ElementFn< HTMLElement, void >; } if (_is_style_generator_property(args)) { const [property, value] = args; return ((element: HTMLElement) => _impl_style_set_property(element, property, value)) as ElementFn< HTMLElement, void >; } // Fallback for generator mode with single property get if (args.length === 1 && typeof args[0] === "string") { const [property] = args; return ((element: HTMLElement) => _impl_style_get_property(element, property)) as ElementFn< HTMLElement, string >; } return ((element: HTMLElement) => _impl_style_get_property(element, "")) as ElementFn< HTMLElement, string >; } } } // Internal implementations for attr function function _impl_attr_set_object( element: HTMLElement, attrs: Record<string, any>, ): void { Object.entries(attrs).forEach(([key, val]) => { element.setAttribute(key, String(val)); }); } function _impl_attr_set_property( element: HTMLElement, name: string, value: any, ): void { element.setAttribute(name, String(value)); } function _impl_attr_get_property( element: HTMLElement, name: string, ): string | null { return element.getAttribute(name); } // Internal implementations for prop function function _impl_prop_set_object( element: HTMLElement, props: Record<string, any>, ): void { Object.entries(props).forEach(([key, val]) => { (element as any)[key] = val; }); } function _impl_prop_set_property( element: HTMLElement, name: string, value: any, ): void { (element as any)[name] = value; } function _impl_prop_get_property(element: HTMLElement, name: string): any { return (element as any)[name]; } // Internal implementations for data function function _impl_data_set_object( element: HTMLElement, data: Record<string, any>, ): void { Object.entries(data).forEach(([key, val]) => { element.dataset[key] = String(val); }); } function _impl_data_set_property( element: HTMLElement, name: string, value: any, ): void { element.dataset[name] = String(value); } function _impl_data_get_property( element: HTMLElement, name: string, ): string | undefined { return element.dataset[name]; } // Predicates for accessor functions function _is_accessor_direct_set_object(args: any[]): boolean { return ( args.length === 2 && isElementLike(args[0]) && typeof args[1] === "object" && args[1] !== null ); } function _is_accessor_direct_set_property(args: any[]): boolean { return ( args.length === 3 && isElementLike(args[0]) && typeof args[1] === "string" && args[2] !== undefined ); } function _is_accessor_direct_get_property(args: any[]): boolean { return ( args.length === 2 && args[0] instanceof HTMLElement && typeof args[1] === "string" ); } function _is_accessor_selector_set_object(args: any[]): boolean { return ( args.length === 2 && typeof args[0] === "string" && _looksLikeSelector(args[0]) && typeof args[1] === "object" && args[1] !== null ); } function _is_accessor_selector_set_property(args: any[]): boolean { return ( args.length === 3 && typeof args[0] === "string" && _looksLikeSelector(args[0]) && typeof args[1] === "string" && args[2] !== undefined ); } function _is_accessor_selector_get_property(args: any[]): boolean { return ( args.length === 2 && typeof args[0] === "string" && _looksLikeSelector(args[0]) && typeof args[1] === "string" ); } function _is_accessor_generator_object(args: any[]): boolean { return args.length === 1 && typeof args[0] === "object" && args[0] !== null; } function _is_accessor_generator_property(args: any[]): boolean { return ( args.length === 2 && typeof args[0] === "string" && args[1] !== undefined ); } function _is_accessor_generator_get(args: any[]): boolean { return args.length === 1 && typeof args[0] === "string"; } // ATTRIBUTE & PROPERTY MANIPULATION function createAccessor(type: "attr" | "prop" | "data") { const implSetObject = type === "attr" ? _impl_attr_set_object : type === "prop" ? _impl_prop_set_object : _impl_data_set_object; const implSetProperty = type === "attr" ? _impl_attr_set_property : type === "prop" ? _impl_prop_set_property : _impl_data_set_property; const implGetProperty = type === "attr" ? _impl_attr_get_property : type === "prop" ? _impl_prop_get_property : _impl_data_get_property; return function (...args: any[]): any { if (_is_accessor_direct_set_object(args)) { const [elementLike, obj] = args; const element = resolveElement(elementLike); if (element) { implSetObject(element, obj); } return; } if (_is_accessor_direct_set_property(args)) { const [elementLike, name, value] = args; const element = resolveElement(elementLike); if (element) { implSetProperty(element, name, value); } return; } if (_is_accessor_direct_get_property(args)) { const [elementLike, name] = args; const element = resolveElement(elementLike); return element ? implGetProperty(element, name) : null; } if (_is_accessor_generator_object(args)) { const [obj] = args; return ((element: HTMLElement) => implSetObject(element, obj)) as ElementFn<HTMLElement, void>; } if (_is_accessor_generator_property(args)) { const [name, value] = args; return ((element: HTMLElement) => implSetProperty(element, name, value)) as ElementFn<HTMLElement, void>; } if (_is_accessor_generator_get(args)) { const [name] = args; return ((element: HTMLElement) => implGetProperty(element, name)) as ElementFn<HTMLElement, any>; } if (_is_accessor_selector_set_object(args)) { const [selector, obj] = args; const element = resolveElement(selector); if (element) { implSetObject(element, obj); } return; } if (_is_accessor_selector_set_property(args)) { const [selector, name, value] = args; const element = resolveElement(selector); if (element) { implSetProperty(element, name, value); } return; } if (_is_accessor_selector_get_property(args)) { const [selector, name] = args; const element = resolveElement(selector); return element ? implGetProperty(element, name) : null; } return (_element: HTMLElement) => null; }; } export const attr = createAccessor("attr"); export const prop = createAccessor("prop"); export const data = createAccessor("data"); // Internal implementations for removeAttr function function _impl_removeAttr(element: HTMLElement, names: string[]): void { names.flat().forEach((name) => element.removeAttribute(name)); } // Generator overload first export function removeAttr<El extends HTMLElement = HTMLElement>( ...names: string[] ): ElementFn<El>; // Direct element and selector overloads export function removeAttr(element: HTMLElement, names: string[]): void; export function removeAttr(element: HTMLElement, ...names: string[]): void; export function removeAttr(selector: string, names: string[]): void; export function removeAttr(selector: string, ...names: string[]): void; export function removeAttr(...args: any[]): any { const detection = detectContext(args, removeAttr); // Handle based on detected context switch (detection.context) { case ApiContext.DIRECT: case ApiContext.SELECTOR: { // Direct mode: removeAttr(element, ...names) or removeAttr(selector, ...names) const [target, ...namesOrArray] = args; // Handle both array and spread arguments const names = Array.isArray(namesOrArray[0]) ? namesOrArray[0] : namesOrArray; // Resolve element from target let element: HTMLElement | null = null; if (typeof target === "string" && !_looksLikeSelector(target)) { // If it doesn't look like a selector, it might be an attribute name in generator mode // Fall through to generator handling break; } else if (typeof target === "string") { element = document.querySelector(target) as HTMLElement; if (!element) { return; // Selector didn't match } } else if (target instanceof HTMLElement) { element = target; } if (element && names.length > 0) { _impl_removeAttr(element, names); } return; } case ApiContext.SYNC_GENERATOR: case ApiContext.ASYNC_GENERATOR: { // Generator mode: yield removeAttr(...names) const names = args; return ((element: HTMLElement) => { _impl_removeAttr(element, names); }) as ElementFn<HTMLElement, void>; } default: { // Fallback to original logic if (isElementLike(args[0])) { const [elementLike, ...namesOrArray] = args; const names = Array.isArray(namesOrArray[0]) ? namesOrArray[0] : namesOrArray; const element = resolveElement(elementLike); if (element) { _impl_removeAttr(element, names); } return; } if (args.length >= 1 && args[0] instanceof HTMLElement) { const [element, ...namesOrArray] = args; const names = Array.isArray(namesOrArray[0]) ? namesOrArray[0] : namesOrArray; _impl_removeAttr(element, names); return; } if ( args.length >= 1 && typeof args[0] === "string" && args.length >= 2 && _looksLikeSelector(args[0]) ) { const [selector, ...namesOrArray] = args; const names = Array.isArray(namesOrArray[0]) ? namesOrArray[0] : namesOrArray; const element = resolveElement(selector); if (element) { _impl_removeAttr(element, names); } return; } // Generator mode return (element: HTMLElement) => { _impl_removeAttr(element, args); }; } } } // Internal implementations for hasAttr function function _impl_hasAttr(element: HTMLElement, name: string): boolean { return element.hasAttribute(name); } // Generator overload first export function hasAttr<El extends HTMLElement = HTMLElement>( name: string, ): ElementFn<El, boolean>; // Direct element and selector overloads export function hasAttr(element: HTMLElement, name: string): boolean; export function hasAttr(selector: string, name: string): boolean; export function hasAttr(...args: any[]): any { if (args.length === 2 && isElementLike(args[0])) { const [elementLike, name] = args; const element = resolveElement(elementLike); return element ? _impl_hasAttr(element, name) : false; } if (args.length === 2 && args[0] instanceof HTMLElement) { const [element, name] = args; return _impl_hasAttr(element, name); } if ( args.length === 2 && typeof args[0] === "string" && _looksLikeSelector(args[0]) ) { const [selector, name] = args; const element = resolveElement(selector); return element ? _impl_hasAttr(element, name) : false; } // Generator mode const [name] = args; return ((element: HTMLElement) => _impl_hasAttr(element, name)) as ElementFn< HTMLElement, boolean >; } // FORM VALUES // Internal implementations for value function function _impl_value_set( element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, val: string, ): void { element.value = val; } function _impl_value_get( element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, ): string { return element.value || ""; } // FORM VALUES // Generator overloads first export function value< El extends | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement = HTMLInputEl