watch-selector
Version:
Runs a function when a selector is added to dom
662 lines (578 loc) • 27.8 kB
text/typescript
// Core types for Watch v5
// Element type inference from selectors
export type ElementFromSelector<S extends string> =
S extends `input[type="text"]${string}`
? HTMLInputElement
: S extends `input[type="email"]${string}`
? HTMLInputElement
: S extends `input[type="password"]${string}`
? HTMLInputElement
: S extends `input[type="search"]${string}`
? HTMLInputElement
: S extends `input[type="tel"]${string}`
? HTMLInputElement
: S extends `input[type="url"]${string}`
? HTMLInputElement
: S extends `input[type="number"]${string}`
? HTMLInputElement
: S extends `input[type="range"]${string}`
? HTMLInputElement
: S extends `input[type="date"]${string}`
? HTMLInputElement
: S extends `input[type="time"]${string}`
? HTMLInputElement
: S extends `input[type="datetime-local"]${string}`
? HTMLInputElement
: S extends `input[type="month"]${string}`
? HTMLInputElement
: S extends `input[type="week"]${string}`
? HTMLInputElement
: S extends `input[type="color"]${string}`
? HTMLInputElement
: S extends `input[type="file"]${string}`
? HTMLInputElement
: S extends `input[type="hidden"]${string}`
? HTMLInputElement
: S extends `input[type="checkbox"]${string}`
? HTMLInputElement
: S extends `input[type="radio"]${string}`
? HTMLInputElement
: S extends `input[type="submit"]${string}`
? HTMLInputElement
: S extends `input[type="reset"]${string}`
? HTMLInputElement
: S extends `input[type="button"]${string}`
? HTMLInputElement
: S extends `input[type="image"]${string}`
? HTMLInputElement
: S extends `input${string}`
? HTMLInputElement
: S extends `button${string}`
? HTMLButtonElement
: S extends `form${string}`
? HTMLFormElement
: S extends `a${string}`
? HTMLAnchorElement
: S extends `img${string}`
? HTMLImageElement
: S extends `select${string}`
? HTMLSelectElement
: S extends `textarea${string}`
? HTMLTextAreaElement
: S extends `div${string}`
? HTMLDivElement
: S extends `span${string}`
? HTMLSpanElement
: S extends `p${string}`
? HTMLParagraphElement
: S extends `h1${string}`
? HTMLHeadingElement
: S extends `h2${string}`
? HTMLHeadingElement
: S extends `h3${string}`
? HTMLHeadingElement
: S extends `h4${string}`
? HTMLHeadingElement
: S extends `h5${string}`
? HTMLHeadingElement
: S extends `h6${string}`
? HTMLHeadingElement
: S extends `ul${string}`
? HTMLUListElement
: S extends `ol${string}`
? HTMLOListElement
: S extends `li${string}`
? HTMLLIElement
: S extends `table${string}`
? HTMLTableElement
: S extends `tr${string}`
? HTMLTableRowElement
: S extends `td${string}`
? HTMLTableCellElement
: S extends `th${string}`
? HTMLTableCellElement
: S extends `thead${string}`
? HTMLTableSectionElement
: S extends `tbody${string}`
? HTMLTableSectionElement
: S extends `tfoot${string}`
? HTMLTableSectionElement
: S extends `canvas${string}`
? HTMLCanvasElement
: S extends `video${string}`
? HTMLVideoElement
: S extends `audio${string}`
? HTMLAudioElement
: S extends `iframe${string}`
? HTMLIFrameElement
: S extends `script${string}`
? HTMLScriptElement
: S extends `link${string}`
? HTMLLinkElement
: S extends `style${string}`
? HTMLStyleElement
: S extends `meta${string}`
? HTMLMetaElement
: S extends `title${string}`
? HTMLTitleElement
: S extends `head${string}`
? HTMLHeadElement
: S extends `body${string}`
? HTMLBodyElement
: S extends `html${string}`
? HTMLHtmlElement
: HTMLElement;
// Handler types
export type ElementHandler<El extends HTMLElement = HTMLElement> = (
element: El,
) => void;
export type ElementFn<El extends Element = HTMLElement, T = void> = (
element: El,
) => T;
// Selector type
export type Selector = string;
// Context for generator execution - this will hold the current element
export interface GeneratorContext<El extends HTMLElement = HTMLElement> {
readonly element: El;
readonly selector: string;
readonly index: number;
readonly array: readonly El[];
}
// Current element proxy type - combines function and proxy behaviors
export type ElementProxy<El extends HTMLElement = HTMLElement> = El & {
<T extends HTMLElement = HTMLElement>(selector: string): T | null;
all<T extends HTMLElement = HTMLElement>(selector: string): T[];
};
// Self function type
export type SelfFunction<El extends HTMLElement = HTMLElement> = () => El;
// Generator function type - element type is inferred and maintained throughout
export type GeneratorFunction<El extends HTMLElement = HTMLElement, T = void> =
| (() => Generator<ElementFn<El, any>, T, unknown>)
| (() => AsyncGenerator<ElementFn<El, any>, T, unknown>);
// Generator yield types - expanded to support more patterns
export type GeneratorYield<El extends HTMLElement = HTMLElement> =
| ElementFn<El, any>
| Promise<ElementFn<El, any>>
| Generator<ElementFn<El, any>, void, unknown>
| AsyncGenerator<ElementFn<El, any>, void, unknown>
| Promise<any>;
// Parent context type for child components
export interface ParentContext<
ParentEl extends HTMLElement = HTMLElement,
ParentApi = any,
> {
element: ParentEl;
api: ParentApi;
}
// Context function type - returns typed context
export type ContextFunction<El extends HTMLElement = HTMLElement> =
() => WatchContext<El>;
// Type-safe generator context that maintains element type through inference
export interface TypedGeneratorContext<El extends HTMLElement = HTMLElement> {
// These functions are properly typed for the current element
self(): El;
el<T extends HTMLElement = HTMLElement>(selector: string): T | null;
all<T extends HTMLElement = HTMLElement>(selector: string): T[];
cleanup(fn: CleanupFunction): void;
// Context access
ctx(): WatchContext<El>;
// Element info
readonly element: El;
readonly selector: string;
readonly index: number;
readonly array: readonly El[];
}
// Pre-defined watch context for enhanced type safety
export interface PreDefinedWatchContext<
S extends string = string,
El extends HTMLElement = ElementFromSelector<S>,
Options extends Record<string, unknown> = Record<string, unknown>,
> {
readonly selector: S;
readonly elementType: El;
readonly options: Options;
readonly __brand: "PreDefinedWatchContext";
}
// Options for creating watch contexts
export interface WatchContextOptions extends Record<string, unknown> {
// Debounce setup function calls
debounce?: number;
// Throttle setup function calls
throttle?: number;
// Only run once
once?: boolean;
// Custom data to attach to context
data?: Record<string, unknown>;
// Custom element filter function
filter?: (element: HTMLElement) => boolean;
// Priority for execution order
priority?: number;
}
// Overloaded function patterns for dual API
export type DualAPI<
DirectArgs extends readonly unknown[],
GeneratorArgs extends readonly unknown[],
El extends HTMLElement = HTMLElement,
ReturnType = void,
> = {
(...args: [...DirectArgs, El]): ReturnType;
(...args: GeneratorArgs): ElementFn<El, ReturnType>;
};
// Event handler type that receives element as second parameter
export type ElementEventHandler<
El extends HTMLElement = HTMLElement,
K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap,
> = (event: HTMLElementEventMap[K], element: El) => void;
// Custom event handler type for CustomEvent support
export type CustomEventHandler<
El extends HTMLElement = HTMLElement,
T = any,
> = (event: CustomEvent<T>, element: El) => void;
// Union type for all possible event handlers
export type EventHandler<
El extends HTMLElement = HTMLElement,
K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap,
T = any,
> = ElementEventHandler<El, K> | CustomEventHandler<El, T>;
// Enhanced event listener options with delegation support
export interface WatchEventListenerOptions extends AddEventListenerOptions {
/** Enable delegation - listen on parent and match against selector */
delegate?: string;
/** Debounce the event handler (milliseconds) */
debounce?: number;
/** Throttle the event handler (milliseconds) */
throttle?: number;
/** Only handle events from specific elements */
filter?: (event: Event, element: HTMLElement) => boolean;
}
// In your types.ts file or at the top of the context module
/**
* The possible return types for an event handler, which can be a regular
* function, an async function, or a synchronous/asynchronous generator.
*/
export type EventHandlerResult =
| void
| Promise<void>
| Generator<any, void, any>
| AsyncGenerator<any, void, any>;
/**
* A generic event handler function type.
* @template E - The specific Event type (e.g., MouseEvent, KeyboardEvent).
*/
export type EventHandler<E extends Event = Event> = (event: E) => EventHandlerResult;
// Hybrid event handler types that support both regular functions and generators
export type HybridEventHandler<
El extends Element = HTMLElement,
K extends keyof HTMLElementEventMap = keyof HTMLElementEventMap,
> =
| ((event: HTMLElementEventMap[K], element?: El) => void)
| ((event: HTMLElementEventMap[K], element?: El) => Promise<void>)
| ((
event: HTMLElementEventMap[K],
element?: El,
) => Generator<ElementFn<El>, void, unknown>)
| ((
event: HTMLElementEventMap[K],
element?: El,
) => AsyncGenerator<ElementFn<El>, void, unknown>)
| ((event: HTMLElementEventMap[K]) => void)
| ((event: HTMLElementEventMap[K]) => Promise<void>)
| ((event: HTMLElementEventMap[K]) => Generator<ElementFn<El>, void, unknown>)
| ((
event: HTMLElementEventMap[K],
) => AsyncGenerator<ElementFn<El>, void, unknown>);
export type HybridCustomEventHandler<
El extends Element = HTMLElement,
T = any,
> =
| ((event: CustomEvent<T>, element?: El) => void)
| ((event: CustomEvent<T>, element?: El) => Promise<void>)
| ((
event: CustomEvent<T>,
element?: El,
) => Generator<ElementFn<El>, void, unknown>)
| ((
event: CustomEvent<T>,
element?: El,
) => AsyncGenerator<ElementFn<El>, void, unknown>)
| ((event: CustomEvent<T>) => void)
| ((event: CustomEvent<T>) => Promise<void>)
| ((event: CustomEvent<T>) => Generator<ElementFn<El>, void, unknown>)
| ((event: CustomEvent<T>) => AsyncGenerator<ElementFn<El>, void, unknown>);
// Debounce configuration options
export interface DebounceOptions {
/** Wait time in milliseconds */
wait: number;
/** Execute on leading edge */
leading?: boolean;
/** Execute on trailing edge (default: true) */
trailing?: boolean;
}
// Throttle configuration options
export interface ThrottleOptions {
/** Limit in milliseconds */
limit: number;
/** Execute on leading edge (default: true) */
leading?: boolean;
/** Execute on trailing edge */
trailing?: boolean;
}
// Enhanced options for hybrid event handling
export interface HybridEventOptions
extends Omit<AddEventListenerOptions, "signal"> {
/** Enable delegation - listen on parent and match against selector */
delegate?: string;
/** Delegation phase - bubble (default) or capture */
delegatePhase?: "bubble" | "capture";
/** Debounce configuration */
debounce?: number | DebounceOptions;
/** Throttle configuration */
throttle?: number | ThrottleOptions;
/** Filter function to conditionally handle events */
filter?: (event: Event, element: HTMLElement) => boolean;
/** AbortSignal for cleanup */
signal?: AbortSignal;
/** Queue concurrent async generators: 'latest' | 'all' | 'none' (default: 'all') */
queue?: "latest" | "all" | "none";
}
// Form element types
export type FormElement =
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement;
// Utility types
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
// CSS property name type
export type CSSPropertyName = keyof CSSStyleDeclaration;
// Attribute name type
export type AttributeName = string;
// Data attribute key type
export type DataAttributeKey = string;
// Event name type
export type EventName<_El extends HTMLElement = HTMLElement> =
keyof HTMLElementEventMap;
// Matcher function type
export type ElementMatcher<El extends HTMLElement = HTMLElement> = (
element: HTMLElement,
) => element is El;
// TypedGeneratorContext - the context object passed to generator functions
export interface TypedGeneratorContext<El extends HTMLElement = HTMLElement> {
// Core element access
self: () => El;
el: <T extends HTMLElement = HTMLElement>(selector: string) => T | null;
all: <T extends HTMLElement = HTMLElement>(selector: string) => T[];
// Cleanup registration
cleanup: (fn: CleanupFunction) => void;
// Context information
ctx: () => WatchContext<El>;
// Direct properties for convenience
readonly element: El;
readonly selector: string;
readonly index: number;
readonly array: readonly El[];
}
// Enhanced matcher function with more control
export type AdvancedMatcher = (
element: HTMLElement,
) => "skip" | "observe" | "queue";
// Watch target types - now with advanced matcher and special objects
export type WatchTarget<El extends HTMLElement = HTMLElement> =
| string
| El
| El[]
| NodeListOf<El>
| ElementMatcher<El>
| AdvancedMatcher
| { parent: HTMLElement; childSelector: string }
| { parent: HTMLElement; selector: string }
| { parent: HTMLElement; matcher: ElementMatcher<any> };
// Observer event types
export interface AttributeChange {
attributeName: string;
oldValue: string | null;
newValue: string | null;
}
export interface TextChange {
oldText: string;
newText: string;
}
export interface VisibilityChange {
isVisible: boolean;
intersectionRatio: number;
boundingClientRect: DOMRectReadOnly;
}
export interface ResizeChange {
contentRect: DOMRectReadOnly;
borderBoxSize: readonly ResizeObserverSize[];
contentBoxSize: readonly ResizeObserverSize[];
devicePixelContentBoxSize?: readonly ResizeObserverSize[];
}
// Lifecycle event types
export type MountHandler<El extends HTMLElement = HTMLElement> = (
element: El,
) => void;
export type UnmountHandler<El extends HTMLElement = HTMLElement> = (
element: El,
) => void;
// Cleanup function type
export type CleanupFunction = () => void;
// Registry types for global observer
export type SelectorRegistry = Map<string, Set<ElementHandler>>;
export type UnmountRegistry = WeakMap<HTMLElement, Set<UnmountHandler>>;
// Enhanced state management types
export type TypedState<T = any> = {
get(): T;
set(value: T): void;
update(fn: (current: T) => T): void;
init(value: T): void;
};
// ============================================================================
// NEW GENERATOR API TYPES - Phase 1: Foundational Types
// ============================================================================
// An "Operation" is a pure function that describes work to be done.
// It receives a WatchContext and returns a result (sync or async).
export type Operation<TReturn, El extends HTMLElement = HTMLElement> = (
context: WatchContext<El>,
) => TReturn | Promise<TReturn>;
// A "Workflow" is what users write with the new API - sync generator by default
// that yields Operations and returns a final value.
export type SyncWorkflow<TReturn = void> = Generator<
Operation<any, any>,
TReturn,
any
>;
export type AsyncWorkflow<TReturn = void> = AsyncGenerator<
Operation<any, any>,
TReturn,
any
>;
// Workflow is sync by default - better performance, no async overhead
export type Workflow<TReturn = void> = SyncWorkflow<TReturn>;
// Enhanced WatchContext for the new pattern - extends existing for compatibility
export interface EnhancedWatchContext<El extends HTMLElement = HTMLElement>
extends Omit<WatchContext<El>, "state"> {
// Core context properties (readonly for immutability)
readonly element: El;
readonly selector: string;
readonly index: number;
readonly array: readonly El[];
// State management interface (replaces Map<string, any> from base)
readonly state: {
get<T>(key: string, defaultValue?: T): T;
set<T>(key: string, value: T): void;
update<T>(key: string, updater: (current: T) => T): void;
has(key: string): boolean;
delete(key: string): boolean;
watch<T>(
key: string,
callback: (newValue: T, oldValue: T) => void,
): () => void;
};
// Parent context for hierarchical operations
readonly parentContext?: EnhancedWatchContext;
// Enhanced cleanup registry
readonly cleanup: (fn: () => void) => void;
// Observer registration
readonly addObserver: (
observer: MutationObserver | IntersectionObserver | ResizeObserver,
) => void;
}
// Type alias for the new context pattern
export type OperationContext<El extends HTMLElement = HTMLElement> =
EnhancedWatchContext<El>;
// Generator operation result - what gets yielded in workflows
export type OperationResult<T = any> = T;
// Enhanced generator function type for the new API
export type WorkflowFunction<El extends HTMLElement = HTMLElement, T = void> = (
context: OperationContext<El>,
) => SyncWorkflow<T>;
// Operation factory type - functions that return operations
export type OperationFactory<
TArgs extends readonly unknown[],
TReturn,
El extends HTMLElement = HTMLElement,
> = (...args: TArgs) => Operation<TReturn, El>;
// Workflow composer type - for composing multiple workflows
export type WorkflowComposer<El extends HTMLElement = HTMLElement> = (
...workflows: WorkflowFunction<El>[]
) => WorkflowFunction<El>;
/**
* Represents a managed instance of a watched element, providing access to
* its element and a snapshot of its state for introspection purposes.
*/
export interface ManagedInstance {
readonly element: HTMLElement;
getState: () => Readonly<Record<string, any>>;
}
/**
* The controller object returned by the watch() function. It provides a handle
* to the watch operation, enabling advanced control like behavior layering,
* instance introspection, and manual destruction.
*
* For backward compatibility, controllers are callable as cleanup functions.
*/
export interface WatchController<El extends HTMLElement = HTMLElement> {
/** The original subject (selector, element, etc.) that this controller manages. */
readonly subject: WatchTarget<El>;
/** Returns a read-only Map of the current elements being managed by this watcher. */
getInstances(): ReadonlyMap<El, ManagedInstance>;
/**
* Adds a new behavior "layer" to the watched elements. This generator will be
* executed on all current and future elements matched by this controller.
*/
layer(
generator: (
ctx: TypedGeneratorContext<El>,
) =>
| Generator<ElementFn<El, any>, any, unknown>
| AsyncGenerator<any, any, unknown>,
): void;
/**
* Destroys this watch operation entirely, cleaning up all its layered behaviors
* and removing all listeners and observers for all managed instances.
*/
destroy(): void;
/**
* Backward compatibility: Allow controller to be called as a cleanup function
*/
(): void;
}
// Individual element instance with its own state
export interface WatchedInstance<El extends HTMLElement = HTMLElement> {
element: El;
state: Record<string, any>;
observers: Set<MutationObserver | IntersectionObserver | ResizeObserver>;
cleanupFns: (() => void)[];
}
// Shared configuration for all instances of a watch subject
export interface WatchConfig<El extends HTMLElement = HTMLElement> {
subject: WatchTarget<El>;
parentScope: HTMLElement;
mountFns: ((instance: WatchedInstance<El>) => void)[];
unmountFns: ((instance: WatchedInstance<El>) => void)[];
instances: Map<HTMLElement, WatchedInstance<El>>;
globalObserver?: MutationObserver;
}
// Context state for generators - enhanced
export interface WatchContext<El extends HTMLElement = HTMLElement> {
element: El;
selector: string;
index: number;
array: readonly El[];
// Enhanced state management
state: Map<string, any>;
observers: Set<MutationObserver | IntersectionObserver | ResizeObserver>;
// Proxy element access
el: ElementProxy<El>;
// Self function
self: SelfFunction<El>;
// Enhanced cleanup registration
cleanup: (fn: CleanupFunction) => void;
addObserver: (
observer: MutationObserver | IntersectionObserver | ResizeObserver,
) => void;
// AbortSignal for cancellation
signal?: AbortSignal;
}