UNPKG

watch-selector

Version:

Runs a function when a selector is added to dom

541 lines (504 loc) 17.2 kB
/** * @fileoverview Synchronous DOM operations for the generator submodule * * This module provides synchronous DOM manipulation operations that return SyncWorkflow<T> * directly, enabling the new `yield*` pattern in synchronous generators without forcing async. * Each function returns a sync generator that yields operations to be executed * by the watch runtime with full type safety and automatic cleanup. * * @example Basic Usage with Sync Generators * ```typescript * import { watch } from 'watch-selector'; * import { addClass, text, self } from 'watch-selector/generator-sync'; * * watch('button', function*() { // Note: sync function*, not async * // Direct yield* syntax in sync generator - no async needed! * yield* addClass('interactive'); * yield* text('Click me!'); * * // Get the element with perfect typing * const button = yield* self<HTMLButtonElement>(); * console.log(button.disabled); // TypeScript knows it's a button! * }); * ``` * * @example When to Use Sync vs Async * ```typescript * // Use SYNC generators when: * // - You don't need await/async operations * // - You want better performance * // - You're doing simple DOM manipulations * import { text, addClass } from 'watch-selector/generator-sync'; * * watch('.simple', function*() { * yield* text('Hello'); * yield* addClass('ready'); * }); * * // Use ASYNC generators when: * // - You need to await promises * // - You're fetching data * // - You're using setTimeout/delays * import { text, addClass } from 'watch-selector/generator'; * * watch('.complex', async function*() { * const data = await fetch('/api/data').then(r => r.json()); * yield* text(data.message); * yield* addClass('loaded'); * }); * ``` * * @module generator-sync/dom */ import type { SyncWorkflow, WatchContext, Operation } from "../types"; // ============================================================================ // TEXT CONTENT OPERATIONS // ============================================================================ /** * Sets the text content of an element using the sync generator API. * * This function returns a SyncWorkflow that can be used directly with `yield*` syntax * in synchronous generators, avoiding the overhead of async generators when not needed. * * @param content - The text content to set * @returns A SyncWorkflow<void> that sets the text content when yielded * * @example Basic sync text setting * ```typescript * import { watch } from 'watch-selector'; * import { text, addClass } from 'watch-selector/generator-sync'; * * watch('button', function* () { // Sync generator * yield* text('Click me!'); * yield* addClass('interactive'); * }); * ``` */ export function text(content: string): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.textContent = content; }) as Operation<void>; })(); } /** * Gets the text content of an element using the sync generator API. * * @returns A SyncWorkflow<string> that returns the current text content * * @example Reading text in sync generator * ```typescript * import { watch } from 'watch-selector'; * import { getText, setText } from 'watch-selector/generator-sync'; * * watch('.mirror', function* () { * const content = yield* getText(); * yield* setText(`Mirror: ${content}`); * }); * ``` */ export function getText(): SyncWorkflow<string> { return (function* () { const result = yield ((context: WatchContext) => { return context.element.textContent || ''; }) as Operation<string>; return result as string; })(); } // ============================================================================ // HTML CONTENT OPERATIONS // ============================================================================ /** * Sets the HTML content of an element using the sync generator API. * * @param content - The HTML content to set * @returns A SyncWorkflow<void> that sets the HTML content * * @warning Be careful with user-provided content to avoid XSS attacks */ export function html(content: string): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.innerHTML = content; }) as Operation<void>; })(); } /** * Gets the HTML content of an element using the sync generator API. * * @returns A SyncWorkflow<string> that returns the HTML content */ export function getHtml(): SyncWorkflow<string> { return (function* () { const result = yield ((context: WatchContext) => { return context.element.innerHTML; }) as Operation<string>; return result as string; })(); } // ============================================================================ // CLASS MANIPULATION OPERATIONS // ============================================================================ /** * Adds one or more CSS classes to an element using the sync generator API. * * @param className - Space-separated list of classes to add * @returns A SyncWorkflow<void> that adds the classes * * @example Adding classes in sync generator * ```typescript * import { watch } from 'watch-selector'; * import { addClass, removeClass } from 'watch-selector/generator-sync'; * * watch('.card', function* () { * yield* addClass('visible animated'); * yield* removeClass('hidden'); * }); * ``` */ export function addClass(className: string): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { const classes = className.split(' ').filter(c => c); context.element.classList.add(...classes); }) as Operation<void>; })(); } /** * Removes one or more CSS classes from an element using the sync generator API. * * @param className - Space-separated list of classes to remove * @returns A SyncWorkflow<void> that removes the classes */ export function removeClass(className: string): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { const classes = className.split(' ').filter(c => c); context.element.classList.remove(...classes); }) as Operation<void>; })(); } /** * Toggles a CSS class on an element using the sync generator API. * * @param className - The class to toggle * @param force - Optional force parameter (true to add, false to remove) * @returns A SyncWorkflow<boolean> that returns the new state */ export function toggleClass(className: string, force?: boolean): SyncWorkflow<boolean> { return (function* () { const result = yield ((context: WatchContext) => { return context.element.classList.toggle(className, force); }) as Operation<boolean>; return result as boolean; })(); } /** * Checks if an element has a CSS class using the sync generator API. * * @param className - The class to check for * @returns A SyncWorkflow<boolean> that returns true if the class exists */ export function hasClass(className: string): SyncWorkflow<boolean> { return (function* () { const result = yield ((context: WatchContext) => { return context.element.classList.contains(className); }) as Operation<boolean>; return result as boolean; })(); } // ============================================================================ // STYLE OPERATIONS // ============================================================================ /** * Sets inline styles on an element using the sync generator API. * * @param styles - Object with CSS property names and values * @returns A SyncWorkflow<void> that sets the styles * * @example Setting styles in sync generator * ```typescript * import { watch } from 'watch-selector'; * import { style } from 'watch-selector/generator-sync'; * * watch('.box', function* () { * yield* style({ * backgroundColor: 'blue', * padding: '10px', * borderRadius: '5px' * }); * }); * ``` */ export function style(styles: Partial<CSSStyleDeclaration>): SyncWorkflow<void>; export function style(property: string, value: string): SyncWorkflow<void>; export function style( propOrStyles: string | Partial<CSSStyleDeclaration>, value?: string ): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { if (typeof propOrStyles === 'string' && value !== undefined) { (context.element.style as any)[propOrStyles] = value; } else if (typeof propOrStyles === 'object') { Object.assign(context.element.style, propOrStyles); } }) as Operation<void>; })(); } // ============================================================================ // ATTRIBUTE OPERATIONS // ============================================================================ /** * Sets attributes on an element using the sync generator API. * * @param attributes - Object with attribute names and values, or single attribute name * @param value - Value when setting a single attribute * @returns A SyncWorkflow<void> that sets the attributes */ export function attr(attributes: Record<string, string>): SyncWorkflow<void>; export function attr(name: string, value: string): SyncWorkflow<void>; export function attr( nameOrAttrs: string | Record<string, string>, value?: string ): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { if (typeof nameOrAttrs === 'string' && value !== undefined) { context.element.setAttribute(nameOrAttrs, value); } else if (typeof nameOrAttrs === 'object') { for (const [key, val] of Object.entries(nameOrAttrs)) { context.element.setAttribute(key, val); } } }) as Operation<void>; })(); } /** * Gets an attribute value from an element using the sync generator API. * * @param name - The attribute name * @returns A SyncWorkflow<string | null> that returns the attribute value */ export function getAttr(name: string): SyncWorkflow<string | null> { return (function* () { const result = yield ((context: WatchContext) => { return context.element.getAttribute(name); }) as Operation<string | null>; return result as string | null; })(); } /** * Removes an attribute from an element using the sync generator API. * * @param name - The attribute name to remove * @returns A SyncWorkflow<void> that removes the attribute */ export function removeAttr(name: string): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.removeAttribute(name); }) as Operation<void>; })(); } // ============================================================================ // ELEMENT ACCESS OPERATIONS // ============================================================================ /** * Gets the current element being processed in the sync generator. * * @returns A SyncWorkflow that returns the typed element * * @example Getting typed element in sync generator * ```typescript * import { watch } from 'watch-selector'; * import { self } from 'watch-selector/generator-sync'; * * watch('button', function* () { * const button = yield* self<HTMLButtonElement>(); * console.log(button.disabled); // TypeScript knows it's a button * }); * ``` */ export function self<El extends HTMLElement = HTMLElement>(): SyncWorkflow<El> { return (function* () { const result = yield ((context: WatchContext) => { return context.element as El; }) as Operation<El>; return result as El; })(); } /** * Queries for a child element using the sync generator API. * * @param selector - CSS selector to query * @returns A SyncWorkflow that returns the element or null */ export function query<El extends HTMLElement = HTMLElement>( selector: string ): SyncWorkflow<El | null> { return (function* () { const result = yield ((context: WatchContext) => { return context.element.querySelector(selector) as El | null; }) as Operation<El | null>; return result as El | null; })(); } /** * Queries for all child elements using the sync generator API. * * @param selector - CSS selector to query * @returns A SyncWorkflow that returns an array of elements */ export function queryAll<El extends HTMLElement = HTMLElement>( selector: string ): SyncWorkflow<El[]> { return (function* () { const result = yield ((context: WatchContext) => { return Array.from(context.element.querySelectorAll(selector)) as El[]; }) as Operation<El[]>; return result as El[]; })(); } // ============================================================================ // VISIBILITY OPERATIONS // ============================================================================ /** * Shows an element by removing display: none using the sync generator API. * * @returns A SyncWorkflow<void> that shows the element */ export function show(): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.style.display = ''; }) as Operation<void>; })(); } /** * Hides an element by setting display: none using the sync generator API. * * @returns A SyncWorkflow<void> that hides the element */ export function hide(): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.style.display = 'none'; }) as Operation<void>; })(); } /** * Toggles element visibility using the sync generator API. * * @returns A SyncWorkflow<boolean> that returns true if now visible */ export function toggle(): SyncWorkflow<boolean> { return (function* () { const result = yield ((context: WatchContext) => { const isHidden = context.element.style.display === 'none' || window.getComputedStyle(context.element).display === 'none'; context.element.style.display = isHidden ? '' : 'none'; return !isHidden; }) as Operation<boolean>; return result as boolean; })(); } // ============================================================================ // FORM OPERATIONS // ============================================================================ /** * Sets the value of a form element using the sync generator API. * * @param val - The value to set * @returns A SyncWorkflow<void> that sets the value */ export function value(val: string): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { const el = context.element as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; if ('value' in el) { el.value = val; } }) as Operation<void>; })(); } /** * Gets the value of a form element using the sync generator API. * * @returns A SyncWorkflow<string> that returns the value */ export function getValue(): SyncWorkflow<string> { return (function* () { const result = yield ((context: WatchContext) => { const el = context.element as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; return 'value' in el ? el.value : ''; }) as Operation<string>; return result as string; })(); } /** * Sets the checked state of a checkbox or radio button using the sync generator API. * * @param state - The checked state to set * @returns A SyncWorkflow<void> that sets the checked state */ export function checked(state: boolean): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { const el = context.element as HTMLInputElement; if ('checked' in el) { el.checked = state; } }) as Operation<void>; })(); } /** * Gets the checked state of a checkbox or radio button using the sync generator API. * * @returns A SyncWorkflow<boolean> that returns the checked state */ export function isChecked(): SyncWorkflow<boolean> { return (function* () { const result = yield ((context: WatchContext) => { const el = context.element as HTMLInputElement; return 'checked' in el ? el.checked : false; }) as Operation<boolean>; return result as boolean; })(); } // ============================================================================ // FOCUS OPERATIONS // ============================================================================ /** * Focuses an element using the sync generator API. * * @returns A SyncWorkflow<void> that focuses the element */ export function focus(): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.focus(); }) as Operation<void>; })(); } /** * Blurs (removes focus from) an element using the sync generator API. * * @returns A SyncWorkflow<void> that blurs the element */ export function blur(): SyncWorkflow<void> { return (function* () { yield ((context: WatchContext) => { context.element.blur(); }) as Operation<void>; })(); } // ============================================================================ // CONVENIENCE ALIASES // ============================================================================ export { text as setText }; export { html as setHtml }; export { value as setValue }; export { checked as setChecked }; export { self as element }; export { self as el };