UNPKG

watch-selector

Version:

Runs a function when a selector is added to dom

622 lines 27 kB
/** * @module watch-selector/events * * # The Unified Event System * * This module provides a powerful, unified API for handling DOM and lifecycle * events. It seamlessly supports both standalone usage (on a specific DOM * element) and usage within a `watch()` generator context. * * ## Key Features * * - **Full Type Safety:** Event objects and their `detail` payloads are * correctly and automatically typed. * - **Generator-Powered Handlers:** Event handlers can be generators, allowing * you to `yield` other library functions for complex, asynchronous flows. * - **Robust Feature Set:** Includes event delegation, debouncing, throttling, * event filtering, and async generator queuing. * - **Automatic Cleanup:** All listeners are automatically cleaned up when their * associated element is removed from the DOM. * - **Standalone & Generator Compatibility:** The same functions work identically * everywhere, with or without a `watch()` context. */ import type { ElementFn, CleanupFunction, HybridEventOptions, HybridEventHandler, HybridCustomEventHandler, AttributeChange, TextChange, VisibilityChange, ResizeChange, Workflow } from "../types"; export declare function on<El extends Element, K extends keyof HTMLElementEventMap>(element: El, event: K, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): CleanupFunction; export declare function on<K extends keyof HTMLElementEventMap>(selector: string, event: K, handler: HybridEventHandler<HTMLElement, K>, options?: HybridEventOptions): CleanupFunction | null; export declare function on<El extends Element, K extends keyof HTMLElementEventMap>(event: K, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>; export declare function on<El extends Element, K extends keyof HTMLElementEventMap>(event: K, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): Workflow<CleanupFunction>; export declare function on<El extends Element, T>(element: El, event: CustomEvent<T>, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): CleanupFunction; export declare function on<T>(selector: string, event: CustomEvent<T>, handler: HybridCustomEventHandler<HTMLElement, T>, options?: HybridEventOptions): CleanupFunction | null; export declare function on<El extends Element, T>(event: CustomEvent<T>, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>; export declare function on<El extends Element, T>(event: CustomEvent<T>, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): Workflow<CleanupFunction>; export declare function on<T = any, El extends Element = HTMLElement>(element: El, eventType: string, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): CleanupFunction; export declare function on<T = any>(selector: string, eventType: string, handler: HybridCustomEventHandler<HTMLElement, T>, options?: HybridEventOptions): CleanupFunction | null; export declare function on<T = any, El extends Element = HTMLElement>(eventType: string, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>; export declare function on<T = any, El extends Element = HTMLElement>(eventType: string, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): Workflow<CleanupFunction>; /** * Interface for event shortcut functions that includes the gen property */ interface EventShortcutFunction<K extends keyof HTMLElementEventMap> { <El extends Element>(element: El, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): CleanupFunction; (selector: string, handler: HybridEventHandler<HTMLElement, K>, options?: HybridEventOptions): CleanupFunction | null; <El extends Element>(handler: HybridEventHandler<El, K>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>; gen<El extends Element>(handler: HybridEventHandler<El, K>, options?: HybridEventOptions): Workflow<CleanupFunction>; } /** * Attaches a click event listener using the full dual API pattern. * * This is a convenient shortcut for `on('click', ...)` that provides the complete * dual API functionality. It works with direct elements, CSS selectors, and within * watch generators, supporting advanced features like debouncing, delegation, and * queue management. * * @param element - HTMLElement to attach listener to (direct API) * @param selector - CSS selector to find element (selector API) * @param handler - Event handler function (can be a generator) * @param options - Advanced event options * @returns CleanupFunction when used directly, ElementFn when in generator mode * * @example Pattern 1: Direct element manipulation * ```typescript * import { click } from 'watch-selector'; * * const button = document.getElementById('my-button'); * const cleanup = click(button, (event) => { * console.log('Button clicked!', event.target); * }); * * // Later, clean up the listener * cleanup(); * ``` * * @example Pattern 2: CSS selector manipulation * ```typescript * import { click } from 'watch-selector'; * * // Attach to element found by selector * click('#submit-button', () => { * console.log('Submit button clicked!'); * }); * * // With event options * click('.once-button', (event) => { * console.log('This only fires once'); * }, { once: true }); * ``` * * @example Pattern 3: Traditional generator usage * ```typescript * import { watch, click, addClass } from 'watch-selector'; * * watch('.interactive-button', function* () { * yield click(function* (event) { * yield addClass('clicked'); * console.log('Button clicked at:', event.clientX, event.clientY); * }); * }); * ``` * * @example Pattern 4: Unified yield* pattern with $ wrapper * ```typescript * import { watch, $, click, addClass } from 'watch-selector'; * * watch('.button', async function* () { * yield* $(click(async function* (event) { * yield* $(addClass('processing')); * await processClick(event); * yield* $(removeClass('processing')); * })); * }); * ``` * * @example Pattern 5: Pure generator submodule * ```typescript * import { watch } from 'watch-selector'; * import { click, addClass } from 'watch-selector/generator'; * * watch('.button', async function* () { * yield* click(async function* (event) { * yield* addClass('active'); * // Handle click... * }); * }); * ``` * * @example Advanced options with debouncing * ```typescript * import { watch, click, addClass, removeClass } from 'watch-selector'; * * watch('.rapid-click-btn', function* () { * yield click(function* (event) { * yield addClass('processing'); * // Process click * yield removeClass('processing'); * }, { * debounce: { wait: 300, leading: true, trailing: false }, * queue: 'latest' // Cancel previous processing * }); * }); * ``` * * @example Event delegation for dynamic content * ```typescript * import { click } from 'watch-selector'; * * const container = document.getElementById('dynamic-list'); * click(container, (event, delegatedElement) => { * console.log('Clicked item:', delegatedElement.textContent); * }, { * delegate: '.list-item' // Handle clicks on any .list-item inside container * }); * ``` * * watch('.button', function* () { * yield click(function* (event) { * yield addClass('clicked'); * yield delay(150); * yield removeClass('clicked'); * }, { * debounce: { wait: 300 }, * queue: 'latest' * }); * }); * ``` */ export declare const click: EventShortcutFunction<"click">; /** * Generator version of click event handler for use with yield*. * * @example Generator usage with yield* * ```typescript * import { watch, click } from 'watch-selector'; * * watch('button', function* () { * yield* click.gen(function* (event) { * yield* addClass('clicked'); * console.log('Button clicked!'); * }); * }); * ``` */ /** * Attaches an input event listener using the dual API pattern. * * This is a convenient shortcut for `on('input', ...)` that's particularly useful * for handling real-time input changes in forms. It supports advanced features * like debouncing, which is commonly needed for search inputs or live validation. * * @param element - HTMLElement to attach listener to (direct API) * @param handler - Event handler function (can be a generator) * @param options - Advanced event options including debouncing * @returns CleanupFunction when used directly, ElementFn when in generator mode * * @example Real-time search with debouncing * ```typescript * import { watch, input, text } from 'watch-selector'; * * watch('#search-input', function* () { * yield input(function* (event) { * const query = (event.target as HTMLInputElement).value; * if (query.length > 2) { * const results = yield* searchAPI(query); * yield* updateResults(results); * } * }, { * debounce: { wait: 300, trailing: true } * }); * }); * ``` * * @example Form validation * ```typescript * import { watch, input, addClass, removeClass, attr } from 'watch-selector'; * * watch('input[required]', function* () { * yield input(function* (event) { * const input = event.target as HTMLInputElement; * const isValid = input.checkValidity(); * * if (isValid) { * yield removeClass('invalid'); * yield addClass('valid'); * yield attr('aria-invalid', 'false'); * } else { * yield removeClass('valid'); * yield addClass('invalid'); * yield attr('aria-invalid', 'true'); * } * }); * }); * ``` * * @example Character counter * ```typescript * import { watch, input, text } from 'watch-selector'; * * watch('.search-input', function* () { * yield input(function* (event) { * const query = (event.target as HTMLInputElement).value; * const results = await searchAPI(query); * yield text(`.results`, `Found ${results.length} results`); * }, { * debounce: { wait: 300 } * }); * }); * ``` * * @example Form validation * ```typescript * import { watch, input, addClass, removeClass } from 'watch-selector'; * * watch('.email-input', function* () { * yield input(function* (event) { * const email = (event.target as HTMLInputElement).value; * const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); * * if (isValid) { * yield addClass('valid'); * yield removeClass('invalid'); * } else { * yield addClass('invalid'); * yield removeClass('valid'); * } * }); * }); * ``` */ export declare const input: EventShortcutFunction<"input">; /** * Attaches a change event listener using the dual API pattern. * * This is a convenient shortcut for `on('change', ...)` that's ideal for handling * discrete changes in form elements like selects, checkboxes, and radio buttons. * Unlike input events, change events fire when the user finishes changing a value. * * @param element - HTMLElement to attach listener to (direct API) * @param handler - Event handler function (can be a generator) * @param options - Advanced event options * @returns CleanupFunction when used directly, ElementFn when in generator mode * * @example Select dropdown handler * ```typescript * import { watch, change, text, addClass, removeClass } from 'watch-selector'; * * watch('#theme-selector', function* () { * yield change(function* (event) { * const select = event.target as HTMLSelectElement; * const theme = select.value; * * // Update UI based on selection * yield removeClass('theme-light theme-dark theme-auto'); * yield addClass(`theme-${theme}`); * yield text('#current-theme', `Current theme: ${theme}`); * }); * }); * ``` * * @example Checkbox toggle * ```typescript * import { watch, change, toggleClass, attr } from 'watch-selector'; * * watch('#newsletter-checkbox', function* () { * yield change(function* (event) { * const checkbox = event.target as HTMLInputElement; * const isChecked = checkbox.checked; * * yield toggleClass('subscribed', isChecked); * yield attr('aria-pressed', isChecked.toString()); * * // Update related UI * const statusText = isChecked ? 'Subscribed' : 'Not subscribed'; * yield text('#subscription-status', statusText); * }); * }); * ``` * * @example Radio button group * ```typescript * import { watch, change, text, removeClass, addClass } from 'watch-selector'; * * watch('.category-select', function* () { * yield change(function* (event) { * const select = event.target as HTMLSelectElement; * const category = select.value; * yield text('.selected-category', `Selected: ${category}`); * }); * }); * ``` * * @example Checkbox handling * ```typescript * import { watch, change, addClass, removeClass } from 'watch-selector'; * * watch('.feature-toggle', function* () { * yield change(function* (event) { * const checkbox = event.target as HTMLInputElement; * const container = self(); * * if (checkbox.checked) { * yield addClass('feature-enabled'); * yield removeClass('feature-disabled'); * } else { * yield addClass('feature-disabled'); * yield removeClass('feature-enabled'); * } * }); * }); * ``` */ export declare const change: EventShortcutFunction<"change">; /** * Attaches a submit event listener using the dual API pattern. * * This is a convenient shortcut for `on('submit', ...)` that's specifically designed * for handling form submissions. It automatically provides access to the form element * and is commonly used with preventDefault() to handle submissions via JavaScript. * * @param element - HTMLFormElement to attach listener to (direct API) * @param handler - Event handler function (can be a generator) * @param options - Advanced event options * @returns CleanupFunction when used directly, ElementFn when in generator mode * * @example Form submission with validation * ```typescript * import { watch, submit, addClass, removeClass, text } from 'watch-selector'; * * watch('#contact-form', function* () { * yield submit(function* (event) { * event.preventDefault(); * * const form = event.target as HTMLFormElement; * const formData = new FormData(form); * * // Show loading state * yield addClass('submitting'); * yield text('#submit-btn', 'Sending...'); * * try { * const response = yield* submitForm(formData); * * if (response.ok) { * yield addClass('success'); * yield text('#message', 'Form submitted successfully!'); * form.reset(); * } else { * yield addClass('error'); * yield text('#message', 'Submission failed. Please try again.'); * } * } catch (error) { * yield addClass('error'); * yield text('#message', 'Network error. Please check your connection.'); * } finally { * yield removeClass('submitting'); * yield text('#submit-btn', 'Submit'); * } * }); * }); * ``` * * @example Form validation before submission * ```typescript * import { watch, submit, hasClass, text } from 'watch-selector'; * * watch('form', function* () { * yield submit(function* (event) { * const form = event.target as HTMLFormElement; * const hasErrors = yield hasClass('has-validation-errors'); * * if (hasErrors) { * event.preventDefault(); * yield text('.error-message', 'Please fix validation errors before submitting'); * return; * } * * // Allow form to submit normally or handle with AJAX * yield text('.status', 'Submitting form...'); * }); * }); * ``` * * @example Multi-step form submission * ```typescript * import { watch, submit, getState, setState, addClass, removeClass } from 'watch-selector'; * * watch('.contact-form', function* () { * yield submit(function* (event) { * event.preventDefault(); * * const form = event.target as HTMLFormElement; * const formData = new FormData(form); * * // Show loading state * yield addClass('loading'); * yield text('.submit-btn', 'Submitting...'); * * try { * const response = await fetch('/api/contact', { * method: 'POST', * body: formData * }); * * if (response.ok) { * yield text('.message', 'Form submitted successfully!'); * form.reset(); * } else { * yield text('.message', 'Submission failed. Please try again.'); * } * } catch (error) { * yield text('.message', 'Network error. Please try again.'); * } finally { * yield removeClass('loading'); * yield text('.submit-btn', 'Submit'); * } * }); * }); * ``` * * @example Multi-step form * ```typescript * import { watch, submit, getState, setState } from 'watch-selector'; * * watch('.multi-step-form', function* () { * setState('currentStep', 1); * * yield submit(function* (event) { * event.preventDefault(); * * const currentStep = getState<number>('currentStep'); * const form = event.target as HTMLFormElement; * * if (currentStep < 3) { * // Validate current step and advance * setState('currentStep', currentStep + 1); * yield showStep(currentStep + 1); * } else { * // Final submission * yield submitForm(form); * } * }); * }); * ``` */ export declare const submit: EventShortcutFunction<"submit">; /** * Creates a reusable event behavior that can be yielded within a `watch` generator. * This is useful for encapsulating complex or repeated event logic. * * @example * const rippleEffect = createEventBehavior('click', function*() { * yield addClass('ripple'); * yield delay(500); * yield removeClass('ripple'); * }); * * watch('.material-button', function*() { * yield* rippleEffect(); * }); */ export declare function createEventBehavior<K extends keyof HTMLElementEventMap, T = any>(eventType: K | string, behavior: HybridEventHandler<Element, K> | HybridCustomEventHandler<Element, T>, options?: HybridEventOptions): () => Generator<ElementFn<Element, CleanupFunction>, void, unknown>; /** * Composes multiple event handlers into a single handler. The handlers are executed * in the order they are provided. This is useful for layering multiple pieces of * logic onto a single event. * * @example * const logClick = (event) => console.log('Clicked!'); * const trackClick = (event) => analytics.track('click'); * const composedHandler = composeEventHandlers(logClick, trackClick); * yield click(composedHandler); */ export declare function composeEventHandlers<K extends keyof HTMLElementEventMap>(...handlers: HybridEventHandler<Element, K>[]): HybridEventHandler<Element, K>; /** * A helper for creating a delegated event listener. This is a convenient alternative * to using the `delegate` option in `on()`. * * @example * // These two are equivalent: * yield delegate('.list-item', 'click', handler); * yield on('click', handler, { delegate: '.list-item' }); * * @param selector The CSS selector for child elements to target. * @param eventType The name of the event to listen for. * @param handler The function to call when the event occurs on a matching child. * @param options Additional event listener options. */ export declare function delegate<K extends keyof HTMLElementEventMap, T = any>(selector: string, eventType: K | string, handler: HybridEventHandler<Element, K> | HybridCustomEventHandler<Element, T>, options?: Omit<HybridEventOptions, "delegate">): ElementFn<Element, CleanupFunction>; /** * Creates a new `CustomEvent` with full type safety for the `detail` payload. * * @param type The name of the custom event. * @param detail The data payload to include with the event. * @param options Standard `EventInit` options. * @returns A new, typed `CustomEvent` instance. */ export declare function createCustomEvent<T = any>(type: string, detail: T, options?: EventInit): CustomEvent<T>; /** * Dispatches a `CustomEvent` from an element. * * @example * // Inside a generator: * yield emit('user:action', { action: 'save' }); * * @example * // Standalone: * emit(document.body, 'app:ready'); */ export declare function emit<El extends Element>(element: El, eventName: string, detail?: any, options?: EventInit): void; export declare function emit<El extends Element = HTMLElement>(eventName: string, detail?: any, options?: EventInit): ElementFn<El>; export declare namespace emit { var gen: <T = any>(eventTypeOrEvent: string | CustomEvent<T>, detail?: T, options?: EventInit) => Workflow<void>; } /** Listens for changes to an element's attributes. */ export declare function onAttr(element: Element, handler: (change: AttributeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction; export declare function onAttr(selector: string, handler: (change: AttributeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction | null; export declare function onAttr(handler: (change: AttributeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): ElementFn<Element, CleanupFunction>; export declare function onAttr(handler: (change: AttributeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): Workflow<CleanupFunction>; /** Listens for changes to an element's `textContent`. */ export declare function onText(element: Element, handler: (change: TextChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction; export declare function onText(selector: string, handler: (change: TextChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction | null; export declare function onText(handler: (change: TextChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): ElementFn<Element, CleanupFunction>; export declare function onText(handler: (change: TextChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): Workflow<CleanupFunction>; /** Listens for when an element becomes visible or hidden in the viewport. */ export declare function onVisible(element: Element, handler: (change: VisibilityChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): CleanupFunction; export declare function onVisible(selector: string, handler: (change: VisibilityChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): CleanupFunction | null; export declare function onVisible(handler: (change: VisibilityChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): ElementFn<Element, CleanupFunction>; export declare function onVisible(handler: (change: VisibilityChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): Workflow<CleanupFunction>; /** Listens for changes to an element's size. */ export declare function onResize(element: Element, handler: (change: ResizeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): CleanupFunction; export declare function onResize(selector: string, handler: (change: ResizeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): CleanupFunction | null; export declare function onResize(handler: (change: ResizeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): ElementFn<Element, CleanupFunction>; export declare function onResize(handler: (change: ResizeChange & { element: Element; }) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): Workflow<CleanupFunction>; /** * Triggers a handler immediately when an element is first processed by `watch`. * This is effectively a "setup" or "initialization" hook. */ export declare function onMount<El extends Element>(element: El, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction; export declare function onMount(selector: string, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction | null; export declare function onMount<El extends Element>(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): ElementFn<El, CleanupFunction>; export declare function onMount(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): Workflow<CleanupFunction>; /** @internal Triggers all registered unmount and general cleanup handlers for an element. */ export declare function triggerUnmountHandlers(element: Element): void; /** * Registers a handler to be called when an element is removed from the DOM. * This is the primary "cleanup" hook for releasing resources or finalizing state. */ export declare function onUnmount<El extends Element>(element: El, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction; export declare function onUnmount(selector: string, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction | null; export declare function onUnmount<El extends Element>(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): ElementFn<El, CleanupFunction>; export declare function onUnmount(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): Workflow<CleanupFunction>; export {}; //# sourceMappingURL=events.d.ts.map