Something went wrong. Try again.
a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt
hypermedia frontend signals
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181/** * Modifier utilities for event and input bindings * * Provides parsing and application of modifiers like .prevent, .stop, .debounce, etc. */
import type { Optional, Timer } from "$types/helpers";import type { Modifier, ParsedAttribute, TimedFunction } from "$types/volt";
/** * Parse attribute name to extract base name and modifiers. * * Modifiers are separated by dashes only when the entire string uses dash-case (e.g., from dataset). * This allows attribute names to contain dashes (like aria-label) while still supporting modifiers. * * Examples: * - "click-prevent-stop" -> {baseName: "click", modifiers: [{name: "prevent"}, {name: "stop"}]} * - "aria-label" -> {baseName: "aria-label", modifiers: []} (no modifiers detected) * - "input-debounce500" -> {baseName: "input", modifiers: [{name: "debounce", value: 500}]} * * @param attrName - The attribute name with potential modifiers * @returns Parsed attribute with base name and modifiers array */export function parseModifiers(attrName: string): ParsedAttribute { const parts = attrName.split("-");
if (parts.length === 1) { return { baseName: attrName, modifiers: [] }; }
const baseName = parts[0]; const modifiers: Modifier[] = []; const KNOWN_MODIFIERS = new Set([ "prevent", "stop", "self", "window", "document", "once", "debounce", "throttle", "passive", "number", "trim", "lazy", "replace", "prefetch", "notransition", ]);
let i = 1; while (i < parts.length) { const part = parts[i];
const numMatch = /^([a-zA-Z]+)(\d+)$/.exec(part); if (numMatch && KNOWN_MODIFIERS.has(numMatch[1])) { modifiers.push({ name: numMatch[1], value: Number(numMatch[2]) }); i++; } else if (KNOWN_MODIFIERS.has(part)) { if (i + 1 < parts.length) { const numValue = Number(parts[i + 1]); if (!Number.isNaN(numValue)) { modifiers.push({ name: part, value: numValue }); i += 2; continue; } } modifiers.push({ name: part }); i++; } else { break; } }
if (modifiers.length === 0) { return { baseName: attrName, modifiers: [] }; }
return { baseName, modifiers };}
/** * Check if a modifier is present in the modifiers array */export function hasModifier(modifiers: Modifier[], name: string): boolean { return modifiers.some((m) => m.name === name);}
/** * Get a modifier's value or return a default */export function getModifierValue(modifiers: Modifier[], name: string, defaultValue: number): number { const modifier = modifiers.find((m) => m.name === name); return modifier?.value ?? defaultValue;}
/** * Create a debounced version of a function. * Delays execution until after the specified wait time has elapsed since the last call. * * @param fn - Function to debounce * @param wait - Milliseconds to wait before executing * @returns Debounced function with cleanup method */export function debounce<T extends unknown[], R>(fn: (...args: T) => R, wait: number): TimedFunction<T> { let timeoutId: Optional<Timer>;
const debounced = function(this: unknown, ...args: T) { if (timeoutId !== undefined) { clearTimeout(timeoutId); }
timeoutId = setTimeout(() => { timeoutId = undefined; fn.apply(this, args); }, wait); };
debounced.cancel = () => { if (timeoutId !== undefined) { clearTimeout(timeoutId); timeoutId = undefined; } };
return debounced;}
/** * Create a throttled version of a function. * Limits execution to at most once per specified wait time. * * @param fn - Function to throttle * @param wait - Milliseconds to wait between executions * @returns Throttled function with cleanup method */export function throttle<T extends unknown[], R>(fn: (...args: T) => R, wait: number): TimedFunction<T> { let timeoutId: Optional<Timer>; let lastExecutionTime = 0; let pendingArgs: Optional<T>; let pendingThis: unknown;
const throttled = function(this: unknown, ...args: T) { const now = Date.now(); const timeSinceLastExecution = now - lastExecutionTime;
pendingArgs = args; // eslint-disable-next-line unicorn/no-this-assignment pendingThis = this;
if (timeSinceLastExecution >= wait) { lastExecutionTime = now; fn.apply(this, args); pendingArgs = undefined; pendingThis = undefined; } else if (timeoutId === undefined) { const remainingTime = wait - timeSinceLastExecution; timeoutId = setTimeout(() => { timeoutId = undefined; lastExecutionTime = Date.now(); if (pendingArgs !== undefined) { fn.apply(pendingThis, pendingArgs); pendingArgs = undefined; pendingThis = undefined; } }, remainingTime); } };
throttled.cancel = () => { if (timeoutId !== undefined) { clearTimeout(timeoutId); timeoutId = undefined; } pendingArgs = undefined; pendingThis = undefined; };
return throttled;}