watch-selector
Version:
Runs a function when a selector is added to dom
1,433 lines • 67.7 kB
TypeScript
/**
* 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 } from "../types";
import { type CSSSelector, type ClassName } from "../utils/selector-types";
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 CSSStyleProperties = Partial<CSSStyleDeclaration>;
type DisplayValue = "none" | "block" | "inline" | "inline-block" | "flex" | "inline-flex" | "grid" | "inline-grid" | "table" | "table-row" | "table-cell" | "contents" | "list-item" | "run-in";
type PositionValue = "static" | "relative" | "absolute" | "fixed" | "sticky";
type FormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
type FocusableElement = HTMLElement & {
focus(): void;
blur(): void;
};
type ValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLOutputElement;
type ElementConstraint = Element | HTMLElement | SVGElement;
type HTMLElementConstraint = HTMLElement | HTMLDivElement | HTMLSpanElement | HTMLButtonElement | HTMLInputElement;
type QueryConstraint<T = Element> = T extends Element ? T : Element;
type StrictElementMap<K extends keyof HTMLElementTagNameMap> = HTMLElementTagNameMap[K];
type StrictSVGElementMap<K extends keyof SVGElementTagNameMap> = SVGElementTagNameMap[K];
/**
* Type guard: Check if a value is an HTML element with generic constraint
*/
declare function isHTMLElement<T extends HTMLElement = HTMLElement>(value: unknown, tagName?: keyof HTMLElementTagNameMap): value is T;
/**
* Type guard for specific HTML element types
*/
/**
* Type guard: Check if a value is an Element with generic constraint
*/
declare function isElement<T extends Element = Element>(value: unknown, tagName?: keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap): value is T;
/**
* Resolve single element with generic type support
*/
declare function resolveElement<T extends Element = HTMLElement>(selector: string, root?: Element | Document): T | null;
/**
* 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 declare function text(element: HTMLElement, content: string | number): void;
export declare function text(selector: string, content: string | number): void;
export declare function text(selector: CSSSelector, content: string | number): void;
export declare function text<T extends HTMLElement = HTMLElement>(element: T, content: string | number): void;
export declare function text<T extends HTMLElement = HTMLElement>(element: T): string;
export declare function text(selector: string | CSSSelector, content: string | number): void;
export declare function text(selector: string | CSSSelector): string | null;
export declare function text(content: string | number): Workflow<void>;
export declare function text(): Workflow<string>;
/**
* 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 declare function html<T extends HTMLElement = HTMLElement>(element: T, content: string): void;
export declare function html<T extends HTMLElement = HTMLElement>(element: T): string;
export declare function html(selector: string | CSSSelector, content: string): void;
export declare function html(selector: string | CSSSelector): string | null;
export declare function html(content: string): Workflow<void>;
export declare function html(): Workflow<string>;
/**
* 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');
* });
* });
* });
* ```
*/
export declare function addClass<T extends Element = HTMLElement>(element: T, className: string | ClassName): void;
export declare function addClass<T extends Element = HTMLElement>(selector: string | CSSSelector, className: string | ClassName): void;
export declare function addClass(className: string | ClassName): Workflow<void>;
/**
* 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 declare function removeClass<T extends Element = HTMLElement>(element: T, className: string | ClassName): void;
export declare function removeClass<T extends Element = HTMLElement>(selector: string | CSSSelector, className: string | ClassName): void;
export declare function removeClass(className: string | ClassName): Workflow<void>;
/**
* 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 declare function toggleClass<T extends Element = HTMLElement>(element: T, className: string | ClassName, force?: boolean): void;
export declare function toggleClass(selector: string | CSSSelector, className: string | ClassName, force?: boolean): void;
export declare function toggleClass(className: string | ClassName, force?: boolean): Workflow<void>;
/**
* 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 declare 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 declare function hasClass(selector: string | CSSSelector, className: string | ClassName): boolean;
export declare function hasClass(className: string | ClassName): Workflow<boolean>;
/**
* 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
* });
* });
* });
* ```
*/
export declare function style<T extends HTMLElement = HTMLElement>(element: T, prop: keyof CSSStyleDeclaration | string, value: StyleValue): void;
export declare function style<T extends HTMLElement = HTMLElement>(element: T, styles: Partial<CSSStyleDeclaration> | StyleObject): void;
export declare function style<T extends HTMLElement = HTMLElement>(element: T, prop: keyof CSSStyleDeclaration | string): string;
export declare function style(selector: string | CSSSelector, prop: keyof CSSStyleDeclaration | string, value: StyleValue): void;
export declare function style(selector: string | CSSSelector, styles: Partial<CSSStyleDeclaration> | StyleObject): void;
export declare function style(selector: string | CSSSelector, prop: keyof CSSStyleDeclaration | string): string | null;
export declare function style(prop: keyof CSSStyleDeclaration | string, value: StyleValue): Workflow<void>;
export declare function style(styles: Partial<CSSStyleDeclaration> | StyleObject): Workflow<void>;
export declare function style(prop: keyof CSSStyleDeclaration | string): Workflow<string>;
export declare function attr<T extends Element = HTMLElement>(element: T, name: string, value: string | number | boolean): void;
export declare function attr<T extends Element = HTMLElement>(element: T, attrs: AttributeObject): void;
export declare function attr<T extends Element = HTMLElement>(element: T, name: string): string | null;
export declare function attr(selector: string | CSSSelector, name: string, value: string | number | boolean): void;
export declare function attr(selector: string | CSSSelector, attrs: AttributeObject): void;
export declare function attr(selector: string | CSSSelector, name: string): string | null;
export declare function attr(name: string, value: string | number | boolean): Workflow<void>;
export declare function attr(attrs: AttributeObject): Workflow<void>;
export declare function attr(name: string): Workflow<string | null>;
export declare function removeAttr<T extends HTMLElement = HTMLElement>(element: T, name: string | string[]): void;
export declare function removeAttr(selector: string | CSSSelector, name: string | string[]): void;
export declare function removeAttr(name: string | string[]): Workflow<void>;
export declare function hasAttr<T extends HTMLElement = HTMLElement>(element: T, name: string): boolean;
export declare function hasAttr(selector: string | CSSSelector, name: string): boolean;
export declare function hasAttr(name: string): Workflow<boolean>;
/**
* 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 properties
* const data = yield* prop('customData');
* const validator = yield* prop('validator');
*
* if (validator && data) {
* const isValid = validator(data.name);
* yield* toggleClass('valid', isValid);
* }
* });
* ```
*
* @example Video/Audio element properties
* ```typescript
* watch('video', function* () {
* // Control playback properties
* yield* prop('volume', 0.5);
* yield* prop('muted', false);
* yield* prop('playbackRate', 1.25);
*
* // Monitor properties
* const duration = yield* prop('duration');
* const currentTime = yield* prop('currentTime');
*
* yield* text(`${currentTime}s / ${duration}s`);
* });
* ```
*/
export declare function prop<T = any>(element: HTMLElement, name: string, value: T): void;
export declare function prop<T = any>(element: HTMLElement, props: DataObject): void;
export declare function prop<T = any>(selector: string | CSSSelector, name: string, value: T): void;
export declare function prop<T = any>(selector: string | CSSSelector, props: DataObject): void;
export declare function prop<T = any>(name: string, value: T): Workflow<void>;
export declare function prop(props: DataObject): Workflow<void>;
export declare function prop<T = any>(name: string): Workflow<T>;
/**
* Manipulates HTML5 data-* attributes with automatic camelCase conversion.
*
* Provides a convenient API for working with data attributes. Automatically handles
* the conversion between camelCase property names and kebab-case attribute names.
* For example, 'userId' becomes 'data-user-id' in the HTML.
*
* @param element - HTMLElement to manipulate data attributes on (direct pattern)
* @param selector - CSS selector to find elements (selector pattern)
* @param key - Data key (without 'data-' prefix) or object of key-value pairs
* @param value - Data value (automatically serialized if not a string)
* @returns void when setting, T when getting single, object when getting all, Workflow for generators
*
* @example Setting and getting data attributes with yield*
* ```typescript
* import { watch, data, click } from 'watch-selector';
*
* watch('.product-card', function* () {
* // Set single data attribute (becomes data-product-id)
* yield* data('productId', '12345');
*
* // Set multiple data attributes with object
* yield* data({
* productId: '12345',
* categoryName: 'Electronics', // becomes data-category-name
* inStock: true, // becomes data-in-stock="true"
* price: 99.99 // becomes data-price="99.99"
* });
*
* // Get single data attribute
* const productId = yield* data('productId');
* console.log('Product ID:', productId);
* });
* ```
*
* @example Complex data serialization
* ```typescript
* watch('.user-profile', function* () {
* // Store complex data (automatically JSON stringified)
* yield* data('userPreferences', {
* theme: 'dark',
* language: 'en',
* notifications: true
* });
*
* // Retrieve and parse complex data
* const prefs = yield* data('userPreferences');
* const parsed = typeof prefs === 'string' ? JSON.parse(prefs) : prefs;
*
* yield* addClass(`theme-${parsed.theme}`);
* });
* ```
*
* @example Dynamic data attributes for tracking
* ```typescript
* watch('.trackable-element', function* () {
* // Set tracking data
* yield* data({
* trackCategory: 'engagement',
* trackAction: 'view',
* trackLabel: 'hero-banner',
* trackValue: Date.now()
* });
*
* yield* click(function* () {
* // Update tracking on interaction
* yield* data('trackAction', 'click');
* yield* data('trackValue', Date.now());
*
* // Send to analytics
* const trackingData = {
* category: yield* data('trackCategory'),
* action: yield* data('trackAction'),
* label: yield* data('trackLabel')
* };
* console.log('Track event:', trackingData);
* });
* });
* ```
*
* @example Component state in data attributes
* ```typescript
* watch('.accordion', function* () {
* // Initialize component state
* yield* data({
* expanded: false,
* animating: false,
* height: 'auto'
* });
*
* yield* click(function* () {
* const isExpanded = yield* data('expanded') === 'true';
*
* // Toggle state
* yield* data('expanded', !isExpanded);
* yield* data('animating', true);
*
* // Update UI based on state
* yield* toggleClass('expanded', !isExpanded);
*
* setTimeout(() => {
* yield* data('animating', false);
* }, 300);
* });
* });
* ```
*/
export declare function data<T = any>(element: HTMLElement, data: DataObject): void;
export declare function data<T = any>(element: HTMLElement, key: string, value: T): void;
export declare function data<T = any>(element: HTMLElement, data: DataObject): void;
export declare function data<T = any>(selector: string | CSSSelector, key: string, value: T): void;
export declare function data<T = any>(selector: string | CSSSelector, data: DataObject): void;
export declare function data<T = any>(key: string, value: T): Workflow<void>;
export declare function data(data: DataObject): Workflow<void>;
export declare function data<T = any>(key: string): Workflow<T | undefined>;
/**
* Removes focus from an element.
*
* @example Direct element
* ```typescript
* const input = document.querySelector('input');
* blur(input);
* ```
*
* @example CSS selector pattern
* ```typescript
* blur('input:focus'); // Blur currently focused input
* ```
*
* @example Generator context
* ```typescript
* import { watch, blur, keydown } from 'watch-selector';
*
* watch('input.auto-blur', function* () {
* yield keydown(function* (e) {
* if (e.key === 'Enter') {
* yield blur(); // Remove focus on Enter
* }
* });
* });
* ```
*
* @param element - The element to blur
* @param selector - CSS selector to find element to blur
* @returns void when used directly, Workflow<void> in generator context
*/
/**
* Hides an element by setting 'display: none'.
*
* @example Direct element
* ```typescript
* const popup = document.querySelector('.popup');
* hide(popup);
* ```
*
* @example CSS selector pattern
* ```typescript
* hide('.loading-spinner');
* hide('[data-temporary]');
* ```
*
* @example Generator context with timing
* ```typescript
* import { watch, hide, show, delay } from 'watch-selector';
*
* watch('.flash-message', function* () {
* yield show();
* yield delay(3000); // Show for 3 seconds
* yield hide();
* });
* ```
*
* @param element - The element to hide
* @param selector - CSS selector to find elements to hide
* @returns void when used directly, Workflow<void> in generator context
**/
export declare function value<T extends ValueElement>(element: T, val: string | number): void;
export declare function value<T extends ValueElement>(element: T): string;
export declare function value(selector: string | CSSSelector, val: string | number): void;
export declare function value(selector: string | CSSSelector): string | null;
export declare function value(val: string | number): Workflow<void>;
export declare function value(): Workflow<string>;
export declare function checked<T extends HTMLInputElement = HTMLInputElement>(element: T, val: boolean): void;
export declare function checked<T extends HTMLInputElement = HTMLInputElement>(element: T): boolean;
export declare function checked(selector: string | CSSSelector, val: boolean): void;
export declare function checked(selector: string | CSSSelector): boolean | null;
export declare function checked(val: boolean): Workflow<void>;
export declare function checked(): Workflow<boolean>;
/**
* Sets focus on an element, making it the active element for keyboard input.
*
* Triggers focus events and updates the document's activeElement. Useful for
* improving accessibility, managing form navigation, and creating keyboard-driven
* interfaces. The element must be focusable (inputs, buttons, links, or elements
* with tabindex).
*
* @param element - HTMLElement to focus (direct pattern)
* @param selector - CSS selector to find element to focus
* @returns void for direct/selector patterns, Workflow for generators
*
* @example Auto-focus search input with yield*
* ```typescript
* import { watch, focus, onMount } from 'watch-selector';
*
* watch('.search-modal', function* () {
* yield* onMount(function* () {
* // Focus search input when modal opens
* const searchInput = yield* query('.search-input');
* if (searchInput) {
* // Small delay to ensure modal animation completes
* setTimeout(() => {
* focus(searchInput);
* }, 100);
* }
* });
* });
* ```
*
* @example Form field navigation
* ```typescript
* watch('.form-field', function* () {
* yield* on('keydown', function* (event) {
* if (event.key === 'Enter') {
* event.preventDefault();
*
* // Move to next field on Enter
* const fields = yield* queryAll('.form-field');
* const currentIndex = fields.indexOf(event.target as HTMLElement);
*
* if (currentIndex < fields.length - 1) {
* focus(fields[currentIndex + 1]);
* } else {
* // Focus submit button at the end
* yield* focus('.submit-btn');
* }
* }
* });
* });
* ```
*
* @example Focus management in dropdown
* ```typescript
* watch('.dropdown', function* () {
* let focusIndex = -1;
*
* yield* click('.dropdown-trigger', function* () {
* yield* toggleClass('open');
*
* if (yield* hasClass('open')) {
* // Focus first item when opening
* const firstItem = yield* query('.dropdown-item');
* if (firstItem) {
* focus(firstItem);
* focusIndex = 0;
* }
* }
* });
*
* // Keyboard navigation
* yield* on('keydown', function* (event) {
* const items = yield* queryAll('.dropdown-item');
*
* if (event.key === 'ArrowDown') {
* event.preventDefault();
* focusIndex = Math.min(focusIndex + 1, items.length - 1);
* focus(items[focusIndex]);
* } else if (event.key === 'ArrowUp') {
* event.preventDefault();
* focusIndex = Math.max(focusIndex - 1, 0);
* focus(items[focusIndex]);
* }
* });
* });
* ```
*
* @example Focus trap for modal accessibility
* ```typescript
* watch('.modal', function* () {
* yield* onMount(function* () {
* // Store previously focused element
* const previousFocus = document.activeElement as HTMLElement;
*
* // Focus first focusable element in modal
* const focusableElements = yield* queryAll(
* 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
* );
*
* if (focusableElements.length > 0) {
* focus(focusableElements[0]);
* }
*
* // Restore focus on close
* yield* onUnmount(() => {
* if (previousFocus) {
* focus(previousFocus);
* }
* });
* });
* });
* ```
*/
export declare function focus<T extends FocusableElement = FocusableElement>(element: T): void;
export declare function focus<T extends FocusableElement = FocusableElement>(element: T): void;
export declare function focus(selector: string | CSSSelector): void;
export declare function focus(): Workflow<void>;
/**
* Removes focus from an element, triggering blur events.
*
* The blur method causes an element to lose focus, making it no longer the
* activeElement. This triggers blur events and can be used for validation,
* saving data, or hiding UI elements. After blur, focus typically returns
* to the document body unless another element is explicitly focused.
*
* @param element - HTMLElement to blur (direct pattern)
* @param selector - CSS selector to find element to blur
* @returns void for direct/selector patterns, Workflow for generators
*
* @example Input validation on blur with yield*
* ```typescript
* import { watch, blur, addClass, removeClass } from 'watch-selector';
*
* watch('.validate-on-blur', function* () {
* yield* on('input', function* () {
* // Remove validation classes while typing
* yield* removeClass('valid invalid');
* });
*
* yield* on('blur', function* () {
* const value = yield* value();
*
* // Validate when user leaves field
* if (value.length < 3) {
* yield* addClass('invalid');
* yield* attr('aria-invalid', 'true');
* } else {
* yield* addClass('valid');
* yield* attr('aria-invalid', 'false');
* }
*
* // Save draft
* yield* setState('draft', value);
* });
* });
* ```
*
* @example Auto-save on blur
* ```typescript
* watch('.auto-save-field', function* () {
* let isDirty = false;
*
* yield* input(function* () {
* isDirty = true;
* yield* addClass('unsaved');
* });
*
* yield* on('blur', function* () {
* if (isDirty) {
* const content = yield* value();
*
* // Show saving indicator
* yield* addClass('saving');
* yield* removeClass('unsaved');
*
* // Save data
* try {
* await fetch('/api/save', {
* method: 'POST',
* body: JSON.stringify({ content })
* });
*
* yield* removeClass('saving');
* yield* addClass('saved');
* isDirty = false;
*
* // Clear saved indicator after delay
* setTimeout(() => {
* yield* removeClass('saved');
* }, 2000);
* } catch (error) {
* yield* removeClass('saving');
* yield* addClass('error');
* }
* }
* });
* });
* ```
*
* @example Dropdown close on blur
* ```typescript
* watch('.searchable-dropdown', function* () {
* yield* on('focus', '.dropdown-input', function* () {
* yield* addClass('open');
* yield* show('.dropdown-menu');
* });
*
* yield* on('blur', '.dropdown-input', function* (event) {
* // Delay to allow clicking on dropdown items
* setTimeout(() => {
* // Check if focus moved to a dropdown item
* const focusedElement = document.activeElement;
* const isInDropdown = yield* query('.dropdown-menu')?.contains(focusedElement);
*
* if (!isInDropdown) {
* yield* removeClass('open');
* yield* hide('.dropdown-menu');
* }
* }, 200);
* });
* });
* ```
*
* @example Programmatic blur for closing popups
* ```typescript
* watch('.popup-trigger', function* () {
* yield* click(function* () {
* const popup = yield* query('.popup');
*
* if (popup) {
* yield* toggleClass('active', popup);
*
* if (yield* hasClass('active', popup)) {
* // Focus popup for keyboard navigation
* focus(popup);
* } else {
* // Blur to remove focus from popup
* blur(popup);
* // Return focus to trigger
* yield* focus();
* }
* }
* });
*
* // Close on Escape key
* yield* on('keydown', function* (event) {
* if (event.key === 'Escape') {
* const popup = yield* query('.popup');
* if (popup && yield* hasClass('active', popup)) {
* blur(popup);
* yield* removeClass('active', popup);
* yield* focus(); // Return focus to trigger
* }
* }
* });
* });
* ```
*/
export declare function blur<T extends FocusableElement = FocusableElement>(element: T): void;
export declare function blur<T extends FocusableElement = FocusableElement>(element: T): void;
export declare function blur(selector: string | CSSSelector): void;
export declare function blur(): Workflow<void>;
/**
* Shows a hidden element by removing 'display: none' style.
*
* Removes any inline display: none style, allowing the element to return to its
* default or CSS-defined display value. This is more reliable than setting a
* specific display value as it respects the element's natural display type.
*
* @param element - HTMLElement to show (direct pattern)
* @param selector - CSS selector to find elements to show
* @returns void for direct/selector patterns, Workflow for generators
*
* @example Basic show/hide toggle with yield*
* ```typescript
* import { watch, show, hide, click } from 'watch-selector';
*
* watch('.collapsible', function* () {
* // Initially hide content
* yield* hide('.content');
*
* yield* click('.toggle-btn', function* () {
* const content = yield* query('.content');
* const isHidden = content?.style.display === 'none';
*
* if (isHidden) {
* yield* show('.content');
* yield* text('.toggle-btn', 'Hide Content');
* } else {
* yield* hide('.content');
* yield* text('.toggle-btn', 'Show Content');
* }
* });
* });
* ```
*
* @example Conditional visibility based on state
* ```typescript
* watch('.notification-area', function* () {
* const hasNotifications = yield* getState('notificationCount', 0) > 0;
*
* if (hasNotifications) {
* yield* show('.notification-badge');
* yield* show('.notification-list');
* } else {
* yield* hide('.notification-badge');
* yield* hide('.notification-list');
* yield* show('.empty-state');
* }
* });
* ```
*
* @example Progressive disclosure pattern
* ```typescript
* watch('.progressive-form', function* () {
* // Hide advanced sections initially
* yield* hide('.advanced-options');
* yield* hide('.expert-settings');
*
* yield* change('#user-level', function* () {
* const level = yield* value();
*
* switch(level) {
* case 'beginner':
* yield* hide('.advanced-options');
* yield* hide('.expert-settings');
* break;
* case 'intermediate':
* yield* show('.advanced-options');
* yield* hide('.expert-settings');
* break;
* case 'expert':
* yield* show('.advanced-options');
* yield* show('.expert-settings');
* break;
* }
* });
* });
* ```
*
* @example Loading states with visibility
* ```typescript
* watch('.data-container', function* () {
* yield* click('.load-btn', async function* () {
* // Show loading, hide content and error
* yield* show('.loading-spinner');
* yield* hide('.content');
* yield* hide('.error-message');
*
* try {
* const data = await fetch('/api/data').then(r => r.json());
*
* // Show content, hide loading
* yield* hide('.loading-spinner');
* yield* show('.content');
* yield* text('.content', data.message);
* } catch (error) {
* // Show error, hide loading
* yield* hide('.loading-spinner');
* yield* show('.error-message');
* yield* text('.error-message', 'Failed to load data');
* }
* });
* });
* ```
*/
export declare function show<T extends HTMLElement = HTMLElement>(element: T): void;
export declare function show(selector: string | CSSSelector): void;
export declare function show(): Workflow<void>;
/**
* Hides an element by setting 'display: none' inline style.
*
* Applies an inline style of display: none, which takes precedence over
* CSS rules. The element remains in the DOM but is completely removed from
* the document flow and is not visible or accessible to screen readers.
*
* @param element - HTMLElement to hide (direct pattern)
* @param selector - CSS selector to find elements to hide
* @returns void for direct/selector patterns, Workflow for generators
*
* @example Dismissible alerts with yield*
* ```typescript
* import { watch, hide, addClass } from 'watch-selector';
*
* watch('.alert', function* () {
* yield* click('.dismiss-btn', function* () {
* // Add fade out animation
* yield* addClass('fade-out');
*
* // Hide after animation completes
* setTimeout(() => {
* yield* hide();
* }, 300);
* });
*
* // Auto-dismiss after 5 seconds
* yield* onMount(function* () {
* setTimeout(() => {
* yield* addClass('fade-out');
* setTimeout(() => {
* yield* hide();
* }, 300);
* }, 5000);
* });
* });
* ```
*
* @example Filter/search with hiding
* ```typescript
* watch('.filter-container', function* () {
* yield* input('.search-input', function* () {
* const searchTerm = (yield* value()).toLowerCase();
* const items = yield* queryAll('.item');
*
* for (const item of items) {
* const text = it