UNPKG

houser-js-utils

Version:

A comprehensive collection of TypeScript utility functions for common development tasks including array manipulation, string processing, date handling, random number generation, validation, and much more.

1 lines 589 kB
{"version":3,"file":"index.mjs","sources":["../src/AccessibilityUtils.ts","../src/AGGridUtils.ts","../src/AnimationUtils.ts","../src/ArrayUtils.ts","../src/ColorUtils.ts","../src/DateUtils.ts","../src/DeviceUtils.ts","../src/DOMUtils.ts","../src/ErrorUtils.ts","../src/FileUtils.ts","../src/FormatUtils.ts","../src/FunctionUtils.ts","../src/ImageUtils.ts","../src/InternationalizationUtils.ts","../src/JwtUtils.ts","../src/KeyboardUtils.ts","../src/LocationUtils.ts","../src/LoggingUtils.ts","../src/MathUtils.ts","../src/MediaUtils.ts","../src/NetworkUtils.ts","../src/NumberUtils.ts","../src/ObjectUtils.ts","../src/PerformanceUtils.ts","../src/RandomUtils.ts","../src/SecurityUtils.ts","../src/StorageUtils.ts","../src/StringUtils.ts","../src/TestUtils.ts","../src/TimeZoneUtils.ts","../src/ValidationUtils.ts"],"sourcesContent":["/**\n * @module AccessibilityUtils\n * @description A collection of utility functions for improving web accessibility, ARIA management, and inclusive design.\n * Provides methods for handling ARIA attributes, focus management, and keyboard navigation.\n *\n * @example\n * ```typescript\n * import { AccessibilityUtils } from 'houser-js-utils';\n *\n * // Set ARIA attributes\n * AccessibilityUtils.setAriaLabel(element, 'Close dialog');\n *\n * // Manage focus\n * AccessibilityUtils.trapFocus(modalElement);\n *\n * // Check accessibility\n * const isAccessible = AccessibilityUtils.hasValidAriaLabels(form);\n * ```\n */\n\nexport const AccessibilityUtils = {\n /**\n * Gets the ARIA described by attribute of an element\n * @param element - The DOM element to check\n * @returns The value of aria-describedby attribute or null if not set\n * @example\n * ```typescript\n * const element = document.querySelector('.input');\n * const describedBy = AccessibilityUtils.getAriaDescribedBy(element);\n * ```\n */\n getAriaDescribedBy(element: Element): string | null {\n return element.getAttribute(\"aria-describedby\");\n },\n\n /**\n * Sets the ARIA described by attribute of an element\n * @param element - The DOM element to modify\n * @param describedBy - The ID(s) of the element(s) that describe this element\n * @example\n * ```typescript\n * const input = document.querySelector('.input');\n * const helpText = document.querySelector('.help-text');\n * AccessibilityUtils.setAriaDescribedBy(input, helpText.id);\n * ```\n */\n setAriaDescribedBy(element: Element, describedBy: string): void {\n element.setAttribute(\"aria-describedby\", describedBy);\n },\n\n /**\n * Gets the ARIA expanded state of an element\n * @param element - Element to check\n * @returns ARIA expanded state\n */\n getAriaExpanded(element: Element): boolean | null {\n const expanded = element.getAttribute(\"aria-expanded\");\n return expanded === null ? null : expanded === \"true\";\n },\n\n /**\n * Sets the ARIA expanded state of an element\n * @param element - Element to modify\n * @param expanded - ARIA expanded state to set\n */\n setAriaExpanded(element: Element, expanded: boolean): void {\n element.setAttribute(\"aria-expanded\", expanded.toString());\n },\n\n /**\n * Gets the ARIA hidden state of an element\n * @param element - Element to check\n * @returns ARIA hidden state\n */\n getAriaHidden(element: Element): boolean | null {\n const hidden = element.getAttribute(\"aria-hidden\");\n return hidden === null ? null : hidden === \"true\";\n },\n\n /**\n * Sets the ARIA hidden state of an element\n * @param element - Element to modify\n * @param hidden - ARIA hidden state to set\n */\n setAriaHidden(element: Element, hidden: boolean): void {\n element.setAttribute(\"aria-hidden\", hidden.toString());\n },\n\n /**\n * Gets the ARIA invalid state of an element\n * @param element - Element to check\n * @returns ARIA invalid state\n */\n getAriaInvalid(element: Element): boolean | null {\n const invalid = element.getAttribute(\"aria-invalid\");\n return invalid === null ? null : invalid === \"true\";\n },\n\n /**\n * Sets the ARIA invalid state of an element\n * @param element - Element to modify\n * @param invalid - ARIA invalid state to set\n */\n setAriaInvalid(element: Element, invalid: boolean): void {\n element.setAttribute(\"aria-invalid\", invalid.toString());\n },\n\n /**\n * Gets the ARIA label of an element\n * @param element - Element to check\n * @returns ARIA label\n */\n getAriaLabel(element: Element): string | null {\n return element.getAttribute(\"aria-label\");\n },\n\n /**\n * Sets the ARIA label of an element\n * @param element - Element to modify\n * @param label - ARIA label to set\n */\n setAriaLabel(element: Element, label: string): void {\n element.setAttribute(\"aria-label\", label);\n },\n\n /**\n * Gets the ARIA required state of an element\n * @param element - Element to check\n * @returns ARIA required state\n */\n getAriaRequired(element: Element): boolean | null {\n const required = element.getAttribute(\"aria-required\");\n return required === null ? null : required === \"true\";\n },\n\n /**\n * Sets the ARIA required state of an element\n * @param element - Element to modify\n * @param required - ARIA required state to set\n */\n setAriaRequired(element: Element, required: boolean): void {\n element.setAttribute(\"aria-required\", required.toString());\n },\n\n /**\n * Gets the ARIA role of an element\n * @param element - Element to check\n * @returns ARIA role\n */\n getAriaRole(element: Element): string | null {\n return element.getAttribute(\"role\");\n },\n\n /**\n * Sets the ARIA role of an element\n * @param element - Element to modify\n * @param role - ARIA role to set\n */\n setAriaRole(element: Element, role: string): void {\n element.setAttribute(\"role\", role);\n },\n\n /**\n * Removes focus from an element\n * @param element - Element to blur\n */\n blurElement(element: Element): void {\n if (element instanceof HTMLElement) {\n element.blur();\n }\n },\n\n /**\n * Sets focus to an element\n * @param element - Element to focus\n */\n focusElement(element: Element): void {\n if (element instanceof HTMLElement) {\n element.focus();\n }\n },\n\n /**\n * Sets focus to the first focusable element in a container\n * @param container - Container element\n */\n focusFirstElement(container: Element): void {\n const focusableElements = this.getFocusableElements(container);\n if (focusableElements.length > 0) {\n (focusableElements[0] as HTMLElement).focus();\n }\n },\n\n /**\n * Sets focus to the last focusable element in a container\n * @param container - Container element\n */\n focusLastElement(container: Element): void {\n const focusableElements = this.getFocusableElements(container);\n if (focusableElements.length > 0) {\n (focusableElements[focusableElements.length - 1] as HTMLElement).focus();\n }\n },\n\n /**\n * Sets focus to the next focusable element\n * @param currentElement - Current element\n */\n focusNextElement(currentElement: Element): void {\n const focusableElements = this.getFocusableElements(document.body);\n const currentIndex = focusableElements.indexOf(currentElement);\n if (currentIndex < focusableElements.length - 1) {\n (focusableElements[currentIndex + 1] as HTMLElement).focus();\n }\n },\n\n /**\n * Sets focus to the previous focusable element\n * @param currentElement - Current element\n */\n focusPreviousElement(currentElement: Element): void {\n const focusableElements = this.getFocusableElements(document.body);\n const currentIndex = focusableElements.indexOf(currentElement);\n if (currentIndex > 0) {\n (focusableElements[currentIndex - 1] as HTMLElement).focus();\n }\n },\n\n /**\n * Gets all focusable elements within a container\n * @param container - Container element\n * @returns Array of focusable elements\n */\n getFocusableElements(container: Element): Element[] {\n const elements = container.querySelectorAll(\"*\");\n return Array.from(elements).filter((element) => this.isFocusable(element));\n },\n\n /**\n * Gets the current focus element\n * @returns Currently focused element\n */\n getFocusedElement(): Element | null {\n return document.activeElement;\n },\n\n /**\n * Checks if an element is focusable\n * @param element - Element to check\n * @returns True if element is focusable\n */\n isFocusable(element: Element): boolean {\n if (!(element instanceof HTMLElement)) {\n return false;\n }\n\n if (element.tabIndex < 0) {\n return false;\n }\n\n if (\"disabled\" in element && element.disabled) {\n return false;\n }\n\n switch (element.tagName.toLowerCase()) {\n case \"a\":\n case \"button\":\n case \"input\":\n case \"select\":\n case \"textarea\":\n return true;\n default:\n return false;\n }\n },\n\n /**\n * Traps focus within a container element, typically used for modals or dialogs\n * @param container - The container element to trap focus within\n * @returns A function that removes the focus trap when called\n * @example\n * ```typescript\n * const modal = document.querySelector('.modal');\n * const removeTrap = AccessibilityUtils.trapFocus(modal);\n *\n * // Later, when the modal is closed:\n * removeTrap();\n * ```\n */\n trapFocus(container: Element): () => void {\n const focusableElements = this.getFocusableElements(container);\n const firstFocusableElement = focusableElements[0];\n const lastFocusableElement =\n focusableElements[focusableElements.length - 1];\n\n const handleKeyDown = (event: Event) => {\n if (!(event instanceof KeyboardEvent) || event.key !== \"Tab\") {\n return;\n }\n\n if (event.shiftKey) {\n if (document.activeElement === firstFocusableElement) {\n event.preventDefault();\n (lastFocusableElement as HTMLElement).focus();\n }\n } else {\n if (document.activeElement === lastFocusableElement) {\n event.preventDefault();\n (firstFocusableElement as HTMLElement).focus();\n }\n }\n };\n\n container.addEventListener(\"keydown\", handleKeyDown as EventListener);\n return () =>\n container.removeEventListener(\"keydown\", handleKeyDown as EventListener);\n },\n};\n","/**\n * @module AGGridUtils\n * @description A collection of utility functions for AG Grid configuration, data manipulation, and grid management.\n * @example\n * ```typescript\n * import { AGGridUtils } from 'houser-js-utils';\n *\n * // Create column definitions\n * const columns = AGGridUtils.createColumnDefs(data);\n *\n * // Export grid data\n * AGGridUtils.exportToCsv(gridApi, 'data.csv');\n *\n * // Apply custom styling\n * AGGridUtils.applyCustomTheme(gridOptions);\n * ```\n */\n\nimport { ValueSetterParams } from \"@ag-grid-community/core\";\n\n/**\n * Collection of utility functions for AG Grid integration\n */\nexport const AGGridUtils = {\n /**\n * Collection of value parsers for AG Grid\n */\n parsers: {\n /**\n * Parses a string value to a number\n * @param params - Object containing the new value to parse\n * @returns Parsed number or null if invalid\n * @example\n * ```typescript\n * const value = AGGridUtils.parsers.number({ newValue: '123.45' });\n * // Returns: 123.45\n * ```\n */\n number: (params: { newValue: string }): number | null => {\n if (\n typeof params.newValue === \"string\" &&\n params.newValue.trim() === \"\"\n ) {\n return null;\n }\n const parsedValue = Number(params.newValue);\n return isNaN(parsedValue) ? null : parsedValue;\n },\n\n /**\n * Trims a string value\n * @param params - Object containing the new value to trim\n * @returns Trimmed string or null if empty\n * @example\n * ```typescript\n * const value = AGGridUtils.parsers.string({ newValue: ' hello ' });\n * // Returns: 'hello'\n * ```\n */\n string: (params: { newValue: string }): string | null =>\n String(params.newValue).trim(),\n },\n\n /**\n * Collection of value setters for AG Grid\n */\n setters: {\n /**\n * Creates a setter that rounds numbers up to the nearest integer\n * @param field - The field name to set the value on\n * @returns A value setter function\n * @example\n * ```typescript\n * const setter = AGGridUtils.setters.numberCeil('price');\n * setter({ data: {}, newValue: 123.45 });\n * // Sets data.price to 124\n * ```\n */\n numberCeil:\n (field: string) =>\n (params: ValueSetterParams): boolean => {\n params.data[field] = Math.ceil(params.newValue);\n return true;\n },\n\n /**\n * Creates a setter that rounds numbers down to the nearest integer\n * @param field - The field name to set the value on\n * @returns A value setter function\n * @example\n * ```typescript\n * const setter = AGGridUtils.setters.numberFloor('price');\n * setter({ data: {}, newValue: 123.45 });\n * // Sets data.price to 123\n * ```\n */\n numberFloor:\n (field: string) =>\n (params: ValueSetterParams): boolean => {\n params.data[field] = Math.floor(params.newValue);\n return true;\n },\n\n /**\n * Creates a setter that formats numbers to one decimal place if they're integers\n * @param field - The field name to set the value on\n * @returns A value setter function\n * @example\n * ```typescript\n * const setter = AGGridUtils.setters.numberFloat('price');\n * setter({ data: {}, newValue: 123 });\n * // Sets data.price to '123.0'\n * ```\n */\n numberFloat:\n (field: string) =>\n (params: ValueSetterParams): boolean => {\n const num = Number(params.newValue);\n params.data[field] = Number.isInteger(num) ? num.toFixed(1) : num;\n return true;\n },\n },\n};\n","/**\n * @module AnimationUtils\n * @description A collection of utility functions for handling animations in web applications.\n * Provides methods for CSS transitions, requestAnimationFrame animations, keyframe animations,\n * and spring physics-based animations.\n *\n * @example\n * ```typescript\n * import { AnimationUtils } from 'houser-js-utils';\n *\n * // Animate an element using CSS transitions\n * const element = document.querySelector('.box');\n * await AnimationUtils.animate(element, {\n * transform: 'translateX(100px)',\n * opacity: '0.5'\n * });\n *\n * // Create a spring animation\n * await AnimationUtils.createSpringAnimation(element, {\n * transform: 'translateY(200px)'\n * }, {\n * stiffness: 170,\n * damping: 26\n * });\n * ```\n */\n\n/**\n * Spring animation configuration options\n */\ninterface SpringOptions {\n /** Spring stiffness coefficient (default: 170) */\n stiffness?: number;\n /** Damping coefficient (default: 26) */\n damping?: number;\n /** Mass of the animated object (default: 1) */\n mass?: number;\n /** Maximum duration of the animation in milliseconds (default: 300) */\n duration?: number;\n}\n\nexport const AnimationUtils = {\n /**\n * Animates an element using CSS transitions\n * @param element - The HTML element to animate\n * @param properties - CSS properties to animate\n * @param duration - Animation duration in milliseconds (default: 300)\n * @param easing - CSS easing function (default: 'ease')\n * @returns Promise that resolves when animation completes\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * await AnimationUtils.animate(element, {\n * transform: 'translateX(100px)',\n * opacity: '0.5'\n * }, 500, 'ease-in-out');\n * ```\n */\n animate(\n element: HTMLElement,\n properties: Partial<CSSStyleDeclaration>,\n duration = 300,\n easing = \"ease\"\n ): Promise<void> {\n return new Promise((resolve) => {\n const startValues: { [key: string]: number } = {};\n const endValues: { [key: string]: number } = {};\n\n // Get start values\n Object.keys(properties).forEach((key) => {\n const value = properties[key as keyof CSSStyleDeclaration];\n if (value !== undefined) {\n const startValue = parseFloat(\n getComputedStyle(element)[\n key as keyof CSSStyleDeclaration\n ] as string\n );\n const endValue = parseFloat(value as string);\n if (!isNaN(startValue) && !isNaN(endValue)) {\n startValues[key] = startValue;\n endValues[key] = endValue;\n }\n }\n });\n\n // Set initial transition\n element.style.transition = `all ${duration}ms ${easing}`;\n\n // Set end values\n Object.keys(properties).forEach((key) => {\n const value = properties[key as keyof CSSStyleDeclaration];\n if (value !== undefined) {\n (element.style as any)[key] = value;\n }\n });\n\n // Handle animation end\n const handleTransitionEnd = () => {\n element.removeEventListener(\"transitionend\", handleTransitionEnd);\n element.style.transition = \"\";\n resolve();\n };\n\n element.addEventListener(\"transitionend\", handleTransitionEnd);\n\n // Fallback timeout\n setTimeout(handleTransitionEnd, duration);\n });\n },\n\n /**\n * Animates an element using requestAnimationFrame for smoother animations\n * @param element - The HTML element to animate\n * @param properties - CSS properties to animate\n * @param duration - Animation duration in milliseconds (default: 300)\n * @param easing - Custom easing function (default: linear)\n * @returns Promise that resolves when animation completes\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * await AnimationUtils.animateWithRAF(element, {\n * transform: 'translateX(100px)'\n * }, 500, (t) => t * t); // Quadratic easing\n * ```\n */\n animateWithRAF(\n element: HTMLElement,\n properties: Partial<CSSStyleDeclaration>,\n duration = 300,\n easing = (t: number) => t\n ): Promise<void> {\n return new Promise((resolve) => {\n const startTime = performance.now();\n const startValues: { [key: string]: number } = {};\n const endValues: { [key: string]: number } = {};\n\n // Get start values\n Object.keys(properties).forEach((key) => {\n const value = properties[key as keyof CSSStyleDeclaration];\n if (value !== undefined) {\n const startValue = parseFloat(\n getComputedStyle(element)[\n key as keyof CSSStyleDeclaration\n ] as string\n );\n const endValue = parseFloat(value as string);\n if (!isNaN(startValue) && !isNaN(endValue)) {\n startValues[key] = startValue;\n endValues[key] = endValue;\n }\n }\n });\n\n const animate = (currentTime: number) => {\n const elapsed = currentTime - startTime;\n const progress = Math.min(elapsed / duration, 1);\n const easedProgress = easing(progress);\n\n Object.keys(startValues).forEach((key) => {\n const startValue = startValues[key];\n const endValue = endValues[key];\n const currentValue =\n startValue + (endValue - startValue) * easedProgress;\n (element.style as any)[key] = `${currentValue}px`;\n });\n\n if (progress < 1) {\n requestAnimationFrame(animate);\n } else {\n resolve();\n }\n };\n\n requestAnimationFrame(animate);\n });\n },\n\n /**\n * Creates a keyframe animation using the Web Animations API\n * @param element - The HTML element to animate\n * @param keyframes - Array of keyframe objects\n * @param options - Animation options\n * @returns Animation object\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * const animation = AnimationUtils.createKeyframeAnimation(element, [\n * { transform: 'translateX(0)', opacity: 1 },\n * { transform: 'translateX(100px)', opacity: 0.5 }\n * ], {\n * duration: 500,\n * easing: 'ease-in-out'\n * });\n * ```\n */\n createKeyframeAnimation(\n element: HTMLElement,\n keyframes: Keyframe[],\n options: KeyframeAnimationOptions = {}\n ): Animation {\n return element.animate(keyframes, {\n duration: 300,\n easing: \"ease\",\n fill: \"forwards\",\n ...options,\n });\n },\n\n /**\n * Creates a spring physics-based animation\n * @param element - The HTML element to animate\n * @param properties - CSS properties to animate\n * @param options - Spring animation configuration\n * @returns Promise that resolves when animation completes\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * await AnimationUtils.createSpringAnimation(element, {\n * transform: 'translateY(200px)'\n * }, {\n * stiffness: 170,\n * damping: 26,\n * mass: 1\n * });\n * ```\n */\n createSpringAnimation(\n element: HTMLElement,\n properties: Partial<CSSStyleDeclaration>,\n options: SpringOptions = {}\n ): Promise<void> {\n const { stiffness = 170, damping = 26, mass = 1, duration = 300 } = options;\n\n return new Promise((resolve) => {\n const startTime = performance.now();\n const startValues: { [key: string]: number } = {};\n const endValues: { [key: string]: number } = {};\n const velocities: { [key: string]: number } = {};\n\n // Get start values\n Object.keys(properties).forEach((key) => {\n const value = properties[key as keyof CSSStyleDeclaration];\n if (value !== undefined) {\n const startValue = parseFloat(\n getComputedStyle(element)[\n key as keyof CSSStyleDeclaration\n ] as string\n );\n const endValue = parseFloat(value as string);\n if (!isNaN(startValue) && !isNaN(endValue)) {\n startValues[key] = startValue;\n endValues[key] = endValue;\n velocities[key] = 0;\n }\n }\n });\n\n const animate = (currentTime: number) => {\n const elapsed = currentTime - startTime;\n if (elapsed >= duration) {\n Object.keys(endValues).forEach((key) => {\n (element.style as any)[key] = `${endValues[key]}px`;\n });\n resolve();\n return;\n }\n\n Object.keys(startValues).forEach((key) => {\n const startValue = startValues[key];\n const endValue = endValues[key];\n const currentValue = parseFloat(\n (element.style as any)[key] || startValue\n );\n const velocity = velocities[key];\n\n const displacement = endValue - currentValue;\n const spring = stiffness * displacement;\n const damper = damping * velocity;\n const acceleration = (spring - damper) / mass;\n\n velocities[key] = velocity + acceleration * (elapsed / 1000);\n const newValue = currentValue + velocities[key] * (elapsed / 1000);\n (element.style as any)[key] = `${newValue}px`;\n });\n\n requestAnimationFrame(animate);\n };\n\n requestAnimationFrame(animate);\n });\n },\n\n /**\n * Gets all animations currently running on an element\n * @param element - The HTML element to check\n * @returns Array of Animation objects\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * const animations = AnimationUtils.getAnimations(element);\n * ```\n */\n getAnimations(element: HTMLElement): Animation[] {\n return element.getAnimations();\n },\n\n /**\n * Gets the current animation state of an element\n * @param element - The HTML element to check\n * @returns Current animation state: 'idle', 'running', or 'paused'\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * const state = AnimationUtils.getAnimationState(element);\n * if (state === 'running') {\n * console.log('Element is currently animating');\n * }\n * ```\n */\n getAnimationState(element: HTMLElement): \"idle\" | \"running\" | \"paused\" {\n const animations = element.getAnimations();\n if (animations.length === 0) {\n return \"idle\";\n }\n return animations.some((animation) => animation.playState === \"running\")\n ? \"running\"\n : \"paused\";\n },\n\n /**\n * Checks if an element has any active animations\n * @param element - The HTML element to check\n * @returns True if the element has any animations\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * if (AnimationUtils.hasAnimations(element)) {\n * console.log('Element has active animations');\n * }\n * ```\n */\n hasAnimations(element: HTMLElement): boolean {\n return element.getAnimations().length > 0;\n },\n\n /**\n * Pauses all animations on an element\n * @param element - The HTML element to pause animations on\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * AnimationUtils.pauseAnimations(element);\n * ```\n */\n pauseAnimations(element: HTMLElement): void {\n element.getAnimations().forEach((animation) => animation.pause());\n },\n\n /**\n * Resumes all paused animations on an element\n * @param element - The HTML element to resume animations on\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * AnimationUtils.resumeAnimations(element);\n * ```\n */\n resumeAnimations(element: HTMLElement): void {\n element.getAnimations().forEach((animation) => animation.play());\n },\n\n /**\n * Reverses the direction of all animations on an element\n * @param element - The HTML element to reverse animations on\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * AnimationUtils.reverseAnimations(element);\n * ```\n */\n reverseAnimations(element: HTMLElement): void {\n element.getAnimations().forEach((animation) => animation.reverse());\n },\n\n /**\n * Stops and removes all animations from an element\n * @param element - The HTML element to stop animations on\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * AnimationUtils.stopAnimations(element);\n * ```\n */\n stopAnimations(element: HTMLElement): void {\n element.getAnimations().forEach((animation) => animation.cancel());\n },\n\n /**\n * Waits for all animations on an element to complete\n * @param element - The HTML element to wait for\n * @returns Promise that resolves when all animations complete\n * @example\n * ```typescript\n * const element = document.querySelector('.box');\n * await AnimationUtils.waitForAnimations(element);\n * console.log('All animations completed');\n * ```\n */\n async waitForAnimations(element: HTMLElement): Promise<void> {\n const animations = element.getAnimations();\n await Promise.all(animations.map((animation) => animation.finished));\n },\n};\n","/**\n * @module ArrayUtils\n * @description A collection of utility functions for array manipulation and operations.\n * @example\n * ```typescript\n * import { ArrayUtils } from 'houser-js-utils';\n *\n * // Get unique values from an array\n * const unique = ArrayUtils.deduplicate([1, 2, 2, 3]);\n *\n * // Find maximum value\n * const max = ArrayUtils.findMax([1, 5, 3, 2]);\n * ```\n */\n\n/**\n * Options for array comparison operations\n */\ntype CompareOptions = {\n /** If true, elements must be in the same order. Defaults to false */\n ordered?: boolean;\n\n /** If true, performs deep equality comparison. Defaults to false */\n deep?: boolean;\n};\n\n/**\n * Represents an entity with an ID field\n */\ntype EmbeddedEntity = {\n /** Unique identifier for the entity */\n id: string;\n /** Additional properties of the entity */\n [key: string]: any;\n};\n\n/**\n * Types that can be compared using standard comparison operators\n */\ntype Comparable = string | number | boolean;\n\n/**\n * Collection of array utility functions\n */\nexport const ArrayUtils = {\n /**\n * Calculates the average of all numbers in an array\n * @param arr - Array of numbers to calculate average from\n * @returns The average of all numbers, or 0 if array is empty\n * @example\n * ```typescript\n * const avg = ArrayUtils.average([1, 2, 3, 4, 5]); // Returns 3\n * ```\n */\n average(arr: number[]): number {\n if (!Array.isArray(arr) || arr.length === 0) return 0;\n return this.sumArray(arr) / arr.length;\n },\n\n /**\n * Compares two arrays for equality by sorting and comparing elements\n * @param a1 - First array to compare\n * @param a2 - Second array to compare\n * @returns True if arrays contain the same elements in any order\n * @example\n * ```typescript\n * const equal = ArrayUtils.arrayEquals([1, 2, 3], [3, 2, 1]); // Returns true\n * ```\n */\n arrayEquals<T extends Comparable>(a1: T[], a2: T[]): boolean {\n if (!Array.isArray(a1) || !Array.isArray(a2)) return false;\n if (a1.length !== a2.length) return false;\n\n const sortedA1 = [...a1].sort(this.sortCompare);\n const sortedA2 = [...a2].sort(this.sortCompare);\n return sortedA1.every((v, i) => v === sortedA2[i]);\n },\n\n /**\n * Splits an array into chunks of specified size\n * @param arr - Array to split into chunks\n * @param size - Size of each chunk (must be positive)\n * @returns Array of arrays, each of size 'size'\n * @throws Error if size is not positive\n */\n chunks<T>(arr: T[], size: number): T[][] {\n if (!Number.isInteger(size) || size <= 0) {\n throw new Error(\"Chunk size must be a positive integer\");\n }\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n },\n\n /**\n * Compares two arrays for equality based on the given options\n * @param arr1 - First array to compare\n * @param arr2 - Second array to compare\n * @param options - Comparison options\n * @returns True if arrays are equal based on the options\n */\n compareArrays<T>(\n arr1: T[],\n arr2: T[],\n options: CompareOptions = {}\n ): boolean {\n if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false;\n const { ordered = false, deep = false } = options;\n\n if (arr1.length !== arr2.length) return false;\n\n if (ordered) {\n return arr1.every((item, index) =>\n deep ? this.deepEqual(item, arr2[index]) : item === arr2[index]\n );\n } else {\n const visited: boolean[] = new Array(arr2.length).fill(false);\n\n return arr1.every((item1) => {\n const index = arr2.findIndex(\n (item2, i) =>\n !visited[i] &&\n (deep ? this.deepEqual(item1, item2) : item1 === item2)\n );\n if (index === -1) return false;\n visited[index] = true;\n return true;\n });\n }\n },\n\n /**\n * Returns a new array with unique values\n * @param array - Array to make unique\n * @returns Array with duplicate values removed\n */\n deduplicate<T>(array: T[]): T[] {\n if (!Array.isArray(array)) return [];\n return Array.from(new Set(array));\n },\n\n /**\n * Deep equality comparison between two values\n * @param a - First value\n * @param b - Second value\n * @returns True if values are deeply equal\n */\n deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n\n if (typeof a !== typeof b) return false;\n\n if (typeof a === \"object\" && a !== null && b !== null) {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n\n if (Array.isArray(a)) {\n if (a.length !== (b as unknown[]).length) return false;\n return a.every((val, i) => this.deepEqual(val, (b as unknown[])[i]));\n }\n\n const keysA = Object.keys(a);\n const keysB = Object.keys(b as Record<string, unknown>);\n\n if (keysA.length !== keysB.length) return false;\n\n return keysA.every((key) =>\n this.deepEqual(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key]\n )\n );\n }\n\n return false;\n },\n\n /**\n * Returns elements from array a that are not in array b based on id property\n * @param a - First array\n * @param b - Second array\n * @param getId - Optional function to extract id from items\n * @returns Array of elements from a that are not in b\n */\n difference<T>(\n a: T[],\n b: T[],\n getId: (item: T) => string | number = (item: any) => item.id\n ): T[] {\n const bIds = new Set(b.map(getId));\n return a.filter((item) => !bIds.has(getId(item)));\n },\n\n /**\n * Returns the maximum value in an array\n * @param arr - Array of comparable values\n * @returns Maximum value or undefined if array is empty\n */\n findMax<T extends Comparable>(arr: T[]): T | undefined {\n if (!Array.isArray(arr) || arr.length === 0) return undefined;\n return arr.reduce((max, val) => (val > max ? val : max), arr[0]);\n },\n\n /**\n * Returns the minimum value in an array\n * @param arr - Array of comparable values\n * @returns Minimum value or undefined if array is empty\n */\n findMin<T extends Comparable>(arr: T[]): T | undefined {\n if (!Array.isArray(arr) || arr.length === 0) return undefined;\n return arr.reduce((min, val) => (val < min ? val : min), arr[0]);\n },\n\n /**\n * Finds and updates an item in a collection based on id\n * @param collection - Collection to update\n * @param item - Item to update\n * @returns Updated collection\n * @throws Error if item is not found\n */\n findAndUpdate(\n collection: EmbeddedEntity[],\n item: EmbeddedEntity\n ): EmbeddedEntity[] {\n const index = collection.findIndex((b) => b.id === item.id);\n if (index === -1) {\n throw new Error(`Item with id ${item.id} not found in collection`);\n }\n return collection.map((element, i) => (i === index ? item : element));\n },\n\n /**\n * Flattens a nested array to a specified depth\n * @param arr - Array to flatten\n * @param depth - Maximum depth to flatten (default: Infinity)\n * @returns Flattened array\n */\n flatten<T>(arr: T[], depth = Infinity): T[] {\n if (depth <= 0) return arr.slice();\n if (!Array.isArray(arr)) return [arr];\n\n return arr.reduce<T[]>((acc, val) => {\n if (Array.isArray(val)) {\n acc.push(...this.flatten(val, depth - 1));\n } else {\n acc.push(val);\n }\n return acc;\n }, []);\n },\n\n /**\n * Groups array elements by a key or function\n * @param arr - Array to group\n * @param keyOrFn - Key to group by or function that returns the group key\n * @returns Object with grouped arrays\n */\n groupBy<T>(\n arr: T[],\n keyOrFn: keyof T | ((item: T) => string | number)\n ): Record<string, T[]> {\n if (!Array.isArray(arr)) return {};\n\n return arr.reduce((groups, item) => {\n const key =\n typeof keyOrFn === \"function\" ? keyOrFn(item) : String(item[keyOrFn]);\n\n groups[key] = groups[key] || [];\n groups[key].push(item);\n return groups;\n }, {} as Record<string, T[]>);\n },\n\n /**\n * Checks if two arrays have any common elements\n * @param array1 - First array\n * @param array2 - Second array\n * @returns True if arrays share at least one common element\n */\n hasCommonElement<T>(array1: T[], array2: T[]): boolean {\n if (!array1?.length || !array2?.length) return false;\n\n const [smaller, larger] =\n array1.length < array2.length ? [array1, array2] : [array2, array1];\n\n const set = new Set(smaller);\n return larger.some((element) => set.has(element));\n },\n\n /**\n * Returns the intersection of two arrays\n * @param arr1 - First array\n * @param arr2 - Second array\n * @returns Array containing elements present in both arrays\n */\n intersection<T>(arr1: T[], arr2: T[]): T[] {\n if (!Array.isArray(arr1) || !Array.isArray(arr2)) return [];\n const set = new Set(arr2);\n return arr1.filter((item) => set.has(item));\n },\n\n /**\n * Moves an item from one position to another in an array\n * @param arr - Array to modify\n * @param from - Source index\n * @param to - Destination index\n * @returns New array with item moved\n * @throws Error if indices are out of bounds\n */\n moveItem<T>(arr: T[], from: number, to: number): T[] {\n if (!Array.isArray(arr)) {\n throw new Error(\"First argument must be an array\");\n }\n if (from < 0 || from >= arr.length) {\n throw new Error(`Source index ${from} is out of bounds`);\n }\n if (to < 0 || to >= arr.length) {\n throw new Error(`Destination index ${to} is out of bounds`);\n }\n\n const newArr = [...arr];\n const [item] = newArr.splice(from, 1);\n newArr.splice(to, 0, item);\n return newArr;\n },\n\n /**\n * Returns a random element from the array\n * @param arr - Array to get element from\n * @returns Random element or undefined if array is empty\n */\n random<T>(arr: T[]): T | undefined {\n if (!Array.isArray(arr) || arr.length === 0) return undefined;\n return arr[Math.floor(Math.random() * arr.length)];\n },\n\n /**\n * Removes elements from an array that match a predicate\n * @param arr - Array to remove elements from\n * @param predicate - Function that returns true for elements to remove\n * @returns New array with matching elements removed\n */\n remove<T>(arr: T[], predicate: (item: T) => boolean): T[] {\n if (!Array.isArray(arr)) return [];\n return arr.filter((item) => !predicate(item));\n },\n\n /**\n * Returns a new array with elements in random order\n * @param arr - Array to shuffle\n * @returns New array with elements in random order\n */\n shuffle<T>(arr: T[]): T[] {\n if (!Array.isArray(arr)) return [];\n const result = [...arr];\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [result[i], result[j]] = [result[j], result[i]];\n }\n return result;\n },\n\n /**\n * Compares two values for sorting\n * @param x - First value to compare\n * @param y - Second value to compare\n * @returns -1 if x < y, 0 if equal, 1 if x > y\n */\n sortCompare(x: Comparable, y: Comparable): number {\n const pre = [\"string\", \"number\", \"bool\"];\n\n if (typeof x !== typeof y) {\n return pre.indexOf(typeof y) - pre.indexOf(typeof x);\n }\n\n if (x === y) {\n return 0;\n }\n return x > y ? 1 : -1;\n },\n\n /**\n * Returns the sum of all numbers in an array\n * @param arr - Array of numbers\n * @returns Sum of all numbers\n */\n sumArray(arr: number[]): number {\n if (!Array.isArray(arr)) return 0;\n return arr.reduce((sum, num) => sum + (Number(num) || 0), 0);\n },\n\n /**\n * Returns the first n elements of an array\n * @param arr - Array to get elements from\n * @param n - Number of elements to get (default: 1)\n * @returns Array containing the first n elements\n */\n takeFirst<T>(arr: T[], n = 1): T[] {\n if (!Array.isArray(arr)) return [];\n return arr.slice(0, n);\n },\n\n /**\n * Returns the last n elements of an array\n * @param arr - Array to get elements from\n * @param n - Number of elements to get (default: 1)\n * @returns Array containing the last n elements\n */\n takeLast<T>(arr: T[], n = 1): T[] {\n if (!Array.isArray(arr)) return [];\n return arr.slice(-n);\n },\n\n /**\n * Returns the union of multiple arrays\n * @param arrays - Arrays to union\n * @returns Array containing unique elements from all arrays\n */\n union<T>(...arrays: T[][]): T[] {\n return this.deduplicate(arrays.flat());\n },\n};\n","/**\n * @module ColorUtils\n * @description A collection of utility functions for color manipulation and conversion.\n * Supports RGB, HEX, HSL, and CMYK color formats with conversion between them.\n *\n * @example\n * ```typescript\n * import { ColorUtils } from 'houser-js-utils';\n *\n * // Convert hex to RGB\n * const rgb = ColorUtils.hexToRgb('#FF0000');\n *\n * // Adjust color brightness\n * const darker = ColorUtils.adjustBrightness(rgb, -20);\n *\n * // Generate random color\n * const randomHex = ColorUtils.randomColor('hex');\n * ```\n */\n\n/**\n * Supported color format types\n */\ntype ColorType = \"rgb\" | \"hex\" | \"hsl\" | \"cmyk\";\n\n/**\n * RGB color representation\n */\ninterface RGBColor {\n /** Red component (0-255) */\n r: number;\n /** Green component (0-255) */\n g: number;\n /** Blue component (0-255) */\n b: number;\n}\n\n/**\n * HSL color representation\n */\ninterface HSLColor {\n /** Hue component (0-360) */\n h: number;\n /** Saturation component (0-100) */\n s: number;\n /** Lightness component (0-100) */\n l: number;\n}\n\n/**\n * CMYK color representation\n */\ninterface CMYKColor {\n /** Cyan component (0-100) */\n c: number;\n /** Magenta component (0-100) */\n m: number;\n /** Yellow component (0-100) */\n y: number;\n /** Key (black) component (0-100) */\n k: number;\n}\n\n/**\n * Validates RGB color values\n * @param color - RGB color to validate\n * @throws Error if color values are invalid\n */\nconst validateRGB = (color: RGBColor): void => {\n const { r, g, b } = color;\n if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {\n throw new Error(\"RGB values must be between 0 and 255\");\n }\n};\n\n/**\n * Validates HSL color values\n * @param color - HSL color to validate\n * @throws Error if color values are invalid\n */\nconst validateHSL = (color: HSLColor): void => {\n const { h, s, l } = color;\n if (h < 0 || h > 360 || s < 0 || s > 100 || l < 0 || l > 100) {\n throw new Error(\n \"HSL values must be between 0-360 for hue and 0-100 for saturation and lightness\"\n );\n }\n};\n\n/**\n * Validates CMYK color values\n * @param color - CMYK color to validate\n * @throws Error if color values are invalid\n */\nconst validateCMYK = (color: CMYKColor): void => {\n const { c, m, y, k } = color;\n if (\n c < 0 ||\n c > 100 ||\n m < 0 ||\n m > 100 ||\n y < 0 ||\n y > 100 ||\n k < 0 ||\n k > 100\n ) {\n throw new Error(\"CMYK values must be between 0 and 100\");\n }\n};\n\nexport const ColorUtils = {\n /**\n * Adjusts the brightness of a color\n * @param color - RGB color values\n * @param amount - Amount to adjust (-100 to 100)\n * @returns Adjusted RGB color values\n */\n adjustBrightness(color: RGBColor, amount: number): RGBColor {\n validateRGB(color);\n const adjust = (value: number) =>\n Math.max(0, Math.min(255, value + amount));\n return {\n r: adjust(color.r),\n g: adjust(color.g),\n b: adjust(color.b),\n };\n },\n\n /**\n * Adjusts the saturation of a color by a specified amount\n * @param color - RGB color values\n * @param amount - Amount to adjust (-100 to 100)\n * @returns Adjusted RGB color values\n */\n adjustSaturation(color: RGBColor, amount: number): RGBColor {\n validateRGB(color);\n const hsl = this.rgbToHsl(color);\n hsl.s = Math.max(0, Math.min(100, hsl.s + amount));\n return this.hslToRgb(hsl);\n },\n\n /**\n * Blends two colors together\n * @param color1 - First RGB color\n * @param color2 - Second RGB color\n * @param ratio - Blend ratio (0 to 1)\n * @returns Blended RGB color\n */\n blendColors(color1: RGBColor, color2: RGBColor, ratio: number): RGBColor {\n validateRGB(color1);\n validateRGB(color2);\n if (ratio < 0 || ratio > 1) {\n throw new Error(\"Blend ratio must be between 0 and 1\");\n }\n const blend = (a: number, b: number) =>\n Math.round(a * (1 - ratio) + b * ratio);\n return {\n r: blend(color1.r, color2.r),\n g: blend(color1.g, color2.g),\n b: blend(color1.b, color2.b),\n };\n },\n\n /**\n * Converts a CMYK color to RGB\n * @param cmyk - CMYK color values\n * @returns RGB color values\n */\n cmykToRgb(cmyk: CMYKColor): RGBColor {\n validateCMYK(cmyk);\n const { c, m, y, k } = cmyk;\n const r = 255 * (1 - c / 100) * (1 - k / 100);\n const g = 255 * (1 - m / 100) * (1 - k / 100);\n const b = 255 * (1 - y / 100) * (1 - k / 100);\n return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };\n },\n\n /**\n * Calculates the contrast ratio between two colors\n * @param color1 - First RGB color\n * @param color2 - Second RGB color\n * @returns Contrast ratio (1 to 21)\n */\n contrastRatio(color1: RGBColor, color2: RGBColor): number {\n validateRGB(color1);\n validateRGB(color2);\n const getLuminance = (color: RGBColor) => {\n const [r, g, b] = [color.r, color.g, color.b].map((c) => {\n c = c / 255;\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n });\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n };\n\n const l1 = getLuminance(color1);\n const l2 = getLuminance(color2);\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n return (lighter + 0.05) / (darker + 0.05);\n },\n\n /**\n * Converts a hex color to RGB\n * @param hex - Hex color string (e.g., \"#FF0000\")\n * @returns RGB color values\n */\n hexToRgb(hex: string): RGBColor {\n if (!this.isValidHexColor(hex)) {\n throw new Error(\"Invalid hex color format\");\n }\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (!result) throw new Error(\"Invalid hex color\");\n return {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16),\n };\n },\n\n /**\n * Converts an HSL color to RGB\n * @param hsl - HSL color values\n * @returns RGB color values\n */\n hslToRgb(hsl: HSLColor): RGBColor {\n validateHSL(hsl);\n const { h, s, l } = hsl;\n const s1 = s / 100;\n const l1 = l / 100;\n\n const c = (1 - Math.abs(2 * l1 - 1)) * s1;\n const x = c * (1 - Math.abs(((h / 60) % 2) - 1));\n const m = l1 - c / 2;\n\n let r = 0,\n g = 0,\n b = 0;\n\n if (h >= 0 && h < 60) {\n r = c;\n g = x;\n b = 0;\n } else if (h >= 60 && h < 120) {\n r = x;\n g = c;\n b = 0;\n } else if (h >= 120 && h < 180) {\n r = 0;\n g = c;\n b = x;\n } else if (h >= 180 && h < 240) {\n r = 0;\n g = x;\n b = c;\n } else if (h >= 240 && h < 300) {\n r = x;\n g = 0;\n b = c;\n } else if (h >= 300 && h < 360) {\n r = c;\n g = 0;\n b = x;\n }\n\n return {\n r: Math.round((r + m) * 255),\n g: Math.round((g + m) * 255),\n b: Math.round((b + m) * 255),\n };\n },\n\n /**\n * Inverts a color\n * @param color - RGB color values\n * @returns Inverted RGB color values\n */\n invertColor(color: RGBColor): RGBColor {\n validateRGB(color);\n return {\n r: 255 - color.r,\n g: 255 - color.g,\n b: 255 - color.b,\n };\n },\n\n /**\n * Validates if a string is a valid hex color\n * @param hex - Hex color string to validate\n * @returns True if the string is a valid hex color\n */\n isValidHexColor(hex: string): boolean {\n return /^#?([a-f\\d]{3}|[a-f\\d]{6})$/i.test(hex);\n },\n\n /**\n * Generates a random color in the specified format\n * @param type - Color format (rgb, hex, hsl, or cmyk)\n * @returns Random color string in the specified format\n */\n randomColor(type: ColorType = \"hex\"): string {\n const getRandomValue = (max: number): number =>\n Math.floor(Math.random() * max);\n\n switch (type) {\n case \"rgb\":\n const r = getRandomValue(256);\n const g = getRandomValue(256);\n const b = getRandomValue(256);\n return `rgb(${r}, ${g}, ${b})`;\n\n case \"hex\":\n const r1 = getRandomValue(256);\n const g1 = getRandomValue(256);\n const b1 = getRandomValue(256);\n return `#${[r1, g1, b1]\n .map((x) => x.toString(16).padStart(2, \"0\"))\n .join(\"\")}`;\n\n case \"hsl\":\n const h = getRandomValue(361);\n const s = getRandomValue(101);\n const l = getRandomValue(101);\n return `hsl(${h}, ${s}%, ${l}%)`;\n\n case \"cmyk\":\n const c = getRandomValue(101);\n const m = getRandomValue(101);\n const y = getRandomValue(101);\n const k = getRandomValue(101);\n return `cmyk(${c}%, ${m}%, ${y}%, ${k}%)`;\n\n default:\n const r2 = getRandomValue(256);\n const g2 = getRandomValue(256);\n const b2 = getRandomValue(256);\n return `#${[r2, g2, b2]\n .map((x) => x.toString(16).padStart(2, \"0\"))\n .join(\"\")}`;\n }\n },\n\n /**\n * Converts RGB color to CMYK\n * @param rgb - RGB color values\n * @returns CMYK color values\n */\n rgbToCmyk(rgb: RGBColor): CMYKColor {\n validateRGB(rgb);\n const { r, g, b } = rgb;\n const r1 = r / 255;\n const g1 = g / 255;\n const b1 = b / 255;\n\n const k = 1 - Math.max(r1, g1, b1);\n const c = (1 - r1 - k) / (1 - k) ||