UNPKG

@react-hive/honey-utils

Version:

A lightweight TypeScript utility library providing a collection of helper functions for common programming tasks

1,388 lines (1,360 loc) 75.4 kB
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ "./src/array.ts": /*!**********************!*\ !*** ./src/array.ts ***! \**********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ chunk: () => (/* binding */ chunk), /* harmony export */ compact: () => (/* binding */ compact), /* harmony export */ compose: () => (/* binding */ compose), /* harmony export */ difference: () => (/* binding */ difference), /* harmony export */ intersection: () => (/* binding */ intersection), /* harmony export */ isArray: () => (/* binding */ isArray), /* harmony export */ isEmptyArray: () => (/* binding */ isEmptyArray), /* harmony export */ pipe: () => (/* binding */ pipe), /* harmony export */ unique: () => (/* binding */ unique) /* harmony export */ }); /* harmony import */ var _guards__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./guards */ "./src/guards.ts"); /** * Checks if a value is an array. * * @param value - The value to check. * * @returns `true` if the value is an array; otherwise, `false`. */ const isArray = (value) => Array.isArray(value); /** * Checks if a value is an empty array. * * @param value - The value to check. * * @returns `true` if the value is an empty array; otherwise, `false`. */ const isEmptyArray = (value) => isArray(value) && value.length === 0; /** * Removes all falsy values from an array. * * Falsy values include: `false`, `0`, `''` (empty string), `null`, `undefined`, and `NaN`. * * Useful for cleaning up arrays with optional, nullable, or conditionally included items. * * @template T - The type of the truthy items. * * @param array - An array possibly containing falsy values. * * @returns A new array containing only truthy values of type `T`. * * @example * ```ts * compact([0, 1, false, 2, '', 3, null, undefined, NaN]); // [1, 2, 3] * ``` */ const compact = (array) => array.filter(Boolean); /** * Returns a new array with duplicate values removed. * * Uses Set for efficient duplicate removal while preserving the original order. * * @template T - The type of the items in the array. * * @param array - The input array that may contain duplicate values. * * @returns A new array with only unique values, maintaining the original order. * * @example * ```ts * unique([1, 2, 2, 3, 1, 4]); // [1, 2, 3, 4] * unique(['a', 'b', 'a', 'c']); // ['a', 'b', 'c'] * ``` */ const unique = (array) => [...new Set(array)]; /** * Splits an array into chunks of the specified size. * * Useful for pagination, batch processing, or creating grid layouts. * * @template T - The type of the items in the array. * * @param array - The input array to be chunked. * @param size - The size of each chunk. Must be greater than 0. * * @returns An array of chunks, where each chunk is an array of the specified size * (except possibly the last chunk, which may be smaller). * * @example * ```ts * chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]] * chunk(['a', 'b', 'c', 'd'], 3); // [['a', 'b', 'c'], ['d']] * ``` */ const chunk = (array, size) => { (0,_guards__WEBPACK_IMPORTED_MODULE_0__.assert)(size > 0, 'Chunk size must be greater than 0'); return Array.from({ length: Math.ceil(array.length / size) }, (_, index) => array.slice(index * size, (index + 1) * size)); }; /** * Returns an array containing elements that exist in all provided arrays. * * @template T - The type of the items in the arrays. * * @param arrays - Two or more arrays to find common elements from. * * @returns A new array containing only the elements that exist in all input arrays. * * @example * ```ts * intersection([1, 2, 3], [2, 3, 4]); // [2, 3] * intersection(['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'e']); // ['b'] * ``` */ const intersection = (...arrays) => { if (arrays.length === 0) { return []; } if (arrays.length === 1) { return [...arrays[0]]; } const [first, ...rest] = arrays; const uniqueFirst = unique(first); return uniqueFirst.filter(item => rest.every(array => array.includes(item))); }; /** * Returns elements from the first array that don't exist in the second array. * * @template T - The type of the items in the arrays. * * @param array - The source array. * @param exclude - The array containing elements to exclude. * * @returns A new array with elements from the first array that don't exist in the second array. * * @example * ```ts * difference([1, 2, 3, 4], [2, 4]); // [1, 3] * difference(['a', 'b', 'c'], ['b']); // ['a', 'c'] * ``` */ const difference = (array, exclude) => array.filter(item => !exclude.includes(item)); /** * Composes multiple unary functions into a single function, applying them from left to right. * * Useful for building a data processing pipeline where the output of one function becomes the input of the next. * * Types are inferred up to 5 chained functions for full type safety. Beyond that, it falls back to the unknown. * * @param fns - A list of unary functions to compose. * * @returns A new function that applies all functions from left to right. * * @example * ```ts * const add = (x: number) => x + 1; * const double = (x: number) => x * 2; * const toStr = (x: number) => `Result: ${x}`; * * const result = pipe(add, double, toStr)(2); * // => 'Result: 6' * ``` */ const pipe = (...fns) => (arg) => fns.reduce((prev, fn) => fn(prev), arg); /** * Composes multiple unary functions into a single function, applying them from **right to left**. * * Often used for building functional pipelines where the innermost function runs first. * Types are inferred up to 5 chained functions for full type safety. * * @param fns - A list of unary functions to compose. * * @returns A new function that applies all functions from right to left. * * @example * ```ts * const add = (x: number) => x + 1; * const double = (x: number) => x * 2; * const toStr = (x: number) => `Result: ${x}`; * * const result = compose(toStr, double, add)(2); * // => 'Result: 6' * ``` */ const compose = (...fns) => (arg) => fns.reduceRight((prev, fn) => fn(prev), arg); /***/ }), /***/ "./src/async.ts": /*!**********************!*\ !*** ./src/async.ts ***! \**********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ everyAsync: () => (/* binding */ everyAsync), /* harmony export */ filterParallel: () => (/* binding */ filterParallel), /* harmony export */ filterSequential: () => (/* binding */ filterSequential), /* harmony export */ findAsync: () => (/* binding */ findAsync), /* harmony export */ isPromise: () => (/* binding */ isPromise), /* harmony export */ reduceAsync: () => (/* binding */ reduceAsync), /* harmony export */ runParallel: () => (/* binding */ runParallel), /* harmony export */ runSequential: () => (/* binding */ runSequential), /* harmony export */ someAsync: () => (/* binding */ someAsync) /* harmony export */ }); /* harmony import */ var _array__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./array */ "./src/array.ts"); /* harmony import */ var _function__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./function */ "./src/function.ts"); /** * Checks if a value is a Promise. * * @template T - The type of the value that the Promise resolves to. * * @param value - The value to check. * * @returns `true` if the value is a Promise; otherwise, `false`. */ const isPromise = (value) => (0,_function__WEBPACK_IMPORTED_MODULE_1__.isFunction)(value?.then); /** * Asynchronously iterates over an array and executes an async function on each item sequentially, * collecting the results. * * Useful when order or timing matters (e.g., rate limits, UI updates, animations). * * @param array - The array of items to iterate over. * @param fn - An async function to execute for each item. Must return a value. * * @returns A promise that resolves with an array of results from each function call. * * @example * ```ts * const results = await runSequential([1, 2, 3], async (item) => { * await delay(100); * * return item * 2; * }); * * console.log(results); // [2, 4, 6] * ``` */ const runSequential = async (array, fn) => { const results = []; for (let i = 0; i < array.length; i++) { results.push(await fn(array[i], i, array)); } return results; }; /** * Executes an asynchronous operation on each element of an array and waits for all promises to resolve. * * @param array - The array of items to operate on. * @param fn - The asynchronous operation to perform on each item. * * @returns A promise that resolves with an array of results after all operations are completed. * * @example * ```ts * const results = await runParallel([1, 2, 3], async (item) => { * await delay(100); * * return item * 2; * }); * * console.log(results); // [2, 4, 6] * ``` */ const runParallel = async (array, fn) => Promise.all(array.map(fn)); /** * Asynchronously filters an array using a predicate function, executing **sequentially**. * * Useful for rate-limited or stateful async operations where execution order matters. * * @template Item - The type of the items in the input array. * * @param array - The array of items to filter. * @param predicate - An async function that returns a `boolean` indicating whether to keep each item. * * @returns A promise that resolves to a new array containing only the items for which the predicate returned `true`. * * @example * ```ts * // Sequentially filter even numbers with delay * const result = await filterSequential([1, 2, 3, 4], async (num) => { * await delay(100); * * return num % 2 === 0; * }); * * console.log(result); // [2, 4] * ``` */ const filterSequential = async (array, predicate) => { const results = []; for (let i = 0; i < array.length; i++) { const item = array[i]; if (await predicate(item, i, array)) { results.push(item); } } return results; }; /** * Asynchronously filters an array based on a provided async predicate function. * * Each item is passed to the `predicate` function in parallel, and only the items * for which the predicate resolves to `true` are included in the final result. * * Useful for filtering based on asynchronous conditions such as API calls, * file system access, or any other delayed operations. * * @template Item - The type of the items in the input array. * * @param array - The array of items to filter. * @param predicate - An async function that returns a boolean indicating whether to keep each item. * * @returns A promise that resolves to a new array containing only the items for which the predicate returned `true`. * * @example * ```ts * // Filter numbers that are even after a simulated delay * const result = await filterParallel([1, 2, 3, 4], async (num) => { * await delay(100); * * return num % 2 === 0; * }); * * console.log(result); // [2, 4] * ``` */ const filterParallel = async (array, predicate) => { const results = await runParallel(array, async (item, index, array) => (await predicate(item, index, array)) ? item : false); return (0,_array__WEBPACK_IMPORTED_MODULE_0__.compact)(results); }; /** * Asynchronously checks if at least one element in the array satisfies the async condition. * * @param array - The array of items to check. * @param predicate - An async function that returns a boolean. * * @returns A promise that resolves to true if any item passes the condition. */ const someAsync = async (array, predicate) => { for (let i = 0; i < array.length; i++) { if (await predicate(array[i], i, array)) { return true; } } return false; }; /** * Asynchronously checks if all elements in the array satisfy the async condition. * * @param array - The array of items to check. * @param predicate - An async function that returns a boolean. * * @returns A promise that resolves to true if all items pass the condition. */ const everyAsync = async (array, predicate) => { for (let i = 0; i < array.length; i++) { if (!(await predicate(array[i], i, array))) { return false; } } return true; }; /** * Asynchronously reduces an array to a single accumulated value. * * @template Item - The type of items in the array. * @template Accumulator - The type of the accumulated result. * * @param array - The array to reduce. * @param fn - The async reducer function that processes each item and returns the updated accumulator. * @param initialValue - The initial accumulator value. * * @returns A promise that resolves to the final accumulated result. */ const reduceAsync = async (array, fn, initialValue) => { let accumulator = initialValue; for (let i = 0; i < array.length; i++) { accumulator = await fn(accumulator, array[i], i, array); } return accumulator; }; /** * Asynchronously finds the first element that satisfies the async condition. * * @param array - The array of items to search. * @param predicate - An async function that returns a boolean. * * @returns A promise that resolves to the found item or null if none match. */ const findAsync = async (array, predicate) => { for (let i = 0; i < array.length; i++) { if (await predicate(array[i], i, array)) { return array[i]; } } return null; }; /***/ }), /***/ "./src/dom.ts": /*!********************!*\ !*** ./src/dom.ts ***! \********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ FOCUSABLE_HTML_TAGS: () => (/* binding */ FOCUSABLE_HTML_TAGS), /* harmony export */ calculateCenterOffset: () => (/* binding */ calculateCenterOffset), /* harmony export */ centerElementInContainer: () => (/* binding */ centerElementInContainer), /* harmony export */ cloneBlob: () => (/* binding */ cloneBlob), /* harmony export */ downloadFile: () => (/* binding */ downloadFile), /* harmony export */ getDOMRectIntersectionRatio: () => (/* binding */ getDOMRectIntersectionRatio), /* harmony export */ getElementOffsetRect: () => (/* binding */ getElementOffsetRect), /* harmony export */ getFocusableHtmlElements: () => (/* binding */ getFocusableHtmlElements), /* harmony export */ getLocalStorageCapabilities: () => (/* binding */ getLocalStorageCapabilities), /* harmony export */ getXOverflowWidth: () => (/* binding */ getXOverflowWidth), /* harmony export */ getYOverflowHeight: () => (/* binding */ getYOverflowHeight), /* harmony export */ hasXOverflow: () => (/* binding */ hasXOverflow), /* harmony export */ hasYOverflow: () => (/* binding */ hasYOverflow), /* harmony export */ isAnchorHtmlElement: () => (/* binding */ isAnchorHtmlElement), /* harmony export */ isContentEditableHtmlElement: () => (/* binding */ isContentEditableHtmlElement), /* harmony export */ isHtmlElementFocusable: () => (/* binding */ isHtmlElementFocusable), /* harmony export */ isLocalStorageReadable: () => (/* binding */ isLocalStorageReadable), /* harmony export */ moveFocusWithinContainer: () => (/* binding */ moveFocusWithinContainer), /* harmony export */ parse2DMatrix: () => (/* binding */ parse2DMatrix) /* harmony export */ }); /* harmony import */ var _guards__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./guards */ "./src/guards.ts"); /* harmony import */ var _string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./string */ "./src/string.ts"); const FOCUSABLE_HTML_TAGS = ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'A']; /** * Extracts transformation values (translate, scale, skew) from the 2D transformation matrix of a given HTML element. * * Only works with 2D transforms (i.e., `matrix(a, b, c, d, e, f)`). * * @param element - The element with a CSS transform applied. * @returns An object with parsed transformation values. * * @example * ```ts * const values = parse2DMatrix(myElement); * console.log(values.translateX); * console.log(values.scaleX); * ``` */ const parse2DMatrix = (element) => { const computedStyles = window.getComputedStyle(element); const transformValue = computedStyles.getPropertyValue('transform'); const matrixMatch = transformValue.match(/^matrix\((.+)\)$/); if (!matrixMatch) { return { translateX: 0, translateY: 0, scaleX: 1, scaleY: 1, skewX: 0, skewY: 0, }; } const [scaleX, skewY, skewX, scaleY, translateX, translateY] = matrixMatch[1] .split(', ') .map(parseFloat); return { translateX, translateY, scaleX, scaleY, skewX, skewY, }; }; /** * Creates a clone of a Blob object. * * @param blob - The Blob object to clone. * * @returns A new Blob with the same content and type as the original. */ const cloneBlob = (blob) => new Blob([blob], { type: blob.type }); /** * Calculates the intersection ratio between two DOM rectangles. * * The ratio represents the proportion of the `targetRect` that is covered by `sourceRect`. * A value of `1` means `sourceRect` completely covers `targetRect`, and `0` means no overlap. * * @param sourceRect - The rectangle used to measure overlap against the target. * @param targetRect - The rectangle whose covered area is measured. * * @returns A number between `0` and `1` representing the intersection ratio. */ const getDOMRectIntersectionRatio = (sourceRect, targetRect) => { const xOverlap = Math.max(0, Math.min(sourceRect.right, targetRect.right) - Math.max(sourceRect.left, targetRect.left)); const yOverlap = Math.max(0, Math.min(sourceRect.bottom, targetRect.bottom) - Math.max(sourceRect.top, targetRect.top)); const intersectionArea = xOverlap * yOverlap; const targetArea = targetRect.width * targetRect.height; return intersectionArea / targetArea; }; /** * Returns the bounding DOMRect of an element based on offset and client dimensions. * * This utility is useful when you need a stable, layout-based rect * without triggering a reflow via `getBoundingClientRect()`. * * @param element - The target HTML element. * @returns A `DOMRect` representing the element’s offset position and size. */ const getElementOffsetRect = (element) => new DOMRect(element.offsetLeft, element.offsetTop, element.clientWidth, element.clientHeight); /** * Determines whether the given HTMLElement is an HTMLAnchorElement. * * Acts as a type guard so that TypeScript narrows `element` to * `HTMLAnchorElement` when the function returns `true`. * * An element qualifies as an anchor by having a tag name of `"A"`. * * @param element - The element to test. * * @returns Whether the element is an anchor element. */ const isAnchorHtmlElement = (element) => element.tagName === 'A'; /** * Checks whether an element is explicitly marked as contenteditable. * * Browsers treat elements with `contenteditable="true"` as focusable, * even if they are not normally keyboard-focusable. * * @param element - The element to inspect. * * @returns True if `contenteditable="true"` is set. */ const isContentEditableHtmlElement = (element) => element.getAttribute('contenteditable') === 'true'; /** * Determines whether an HTMLElement is focusable under standard browser rules. * * The function checks a combination of factors: * - The element must be rendered (not `display: none` or `visibility: hidden`). * - Disabled form controls are never focusable. * - Elements with `tabindex="-1"` are intentionally removed from the focus order. * - Certain native HTML elements are inherently focusable (e.g. inputs, buttons, anchors with `href`). * - Elements with `contenteditable="true"` are treated as focusable. * - Any element with a valid `tabindex` (not null) is considered focusable. * * This logic approximates how browsers and the accessibility tree * determine real-world focusability—not just tabindex presence. * * @param element - The element to test. `null` or `undefined` will return `false`. * * @returns Whether the element is focusable. */ const isHtmlElementFocusable = (element) => { if (!element) { return false; } // Hidden or not rendered const style = window.getComputedStyle(element); if (style.visibility === 'hidden' || style.display === 'none') { return false; } if ('disabled' in element && element.disabled) { return false; } // Explicitly removed from tab order const tabIndex = element.getAttribute('tabindex'); if (tabIndex === '-1') { return false; } if (FOCUSABLE_HTML_TAGS.includes(element.tagName)) { if (isAnchorHtmlElement(element)) { return element.href !== ''; } return true; } if (isContentEditableHtmlElement(element)) { return true; } return tabIndex !== null; }; /** * Collects all focusable descendant elements within a container. * * The function queries *all* elements under the container and filters them * using `isHtmlElementFocusable`, producing a reliable list of elements * that can receive keyboard focus in real-world browser conditions. * * @param container - The root container whose focusable children will be found. * * @returns An array of focusable HTMLElements in DOM order. */ const getFocusableHtmlElements = (container) => Array.from(container.querySelectorAll('*')).filter(isHtmlElementFocusable); /** * Moves focus to the next or previous focusable element within a container. * * This utility is commonly used to implement accessible keyboard navigation patterns such as: * - roving tabindex * - custom dropdowns * - tablists * - menus * - horizontal or vertical navigation groups * * Focus movement is scoped to a container and operates on the list of * focusable descendants returned by `getFocusableHtmlElements`. * * @param direction - Direction in which focus should move (`'next'` or `'previous'`). * @param container - Optional container that defines the focus scope. * If omitted, the parent element of the currently focused element is used. * @param options - Optional configuration controlling wrapping behavior and custom index resolution. * * @remarks * - This function reads from and mutates the document's focus state. * - If no active element exists, no container can be resolved, * or the active element is not part of the focusable set, no action is taken. * - When `getNextIndex` is provided, it fully overrides the default wrapping and directional logic. */ const moveFocusWithinContainer = (direction, container = null, { wrap = true, getNextIndex } = {}) => { const activeElement = document.activeElement; const scope = container ?? activeElement?.parentElement; if (!activeElement || !scope) { return; } const focusableElements = getFocusableHtmlElements(scope); if (focusableElements.length === 0) { return; } const currentIndex = focusableElements.indexOf(activeElement); if (currentIndex === -1) { return; } let nextIndex; if (getNextIndex) { nextIndex = getNextIndex(currentIndex, direction, focusableElements); } else { if (direction === 'next') { nextIndex = currentIndex + 1; if (nextIndex >= focusableElements.length) { nextIndex = wrap ? 0 : null; } } else { nextIndex = currentIndex - 1; if (nextIndex < 0) { nextIndex = wrap ? focusableElements.length - 1 : null; } } } if (nextIndex === null) { return; } focusableElements[nextIndex]?.focus(); }; /** * Checks whether an element has horizontal overflow. * * @param element - The element to check. * * @returns `true` if the content overflows horizontally. */ const hasXOverflow = (element) => element.scrollWidth > element.clientWidth; /** * Calculates the horizontal overflow width of an element. * * The overflow width represents how much wider the content is compared * to the visible container area. * * @param element - The scrollable container element. * * @returns The overflow width in pixels. Returns `0` when the content does not overflow horizontally. */ const getXOverflowWidth = (element) => Math.max(0, element.scrollWidth - element.clientWidth); /** * Checks whether an element has vertical overflow. * * @param element - The element to check. * * @returns `true` if the content overflows vertically. */ const hasYOverflow = (element) => element.scrollHeight > element.clientHeight; /** * Calculates the vertical overflow height of an element. * * The overflow height represents how much taller the content is compared * to the visible container area. * * @param element - The scrollable container element. * * @returns The overflow height in pixels. Returns `0` when the content does not overflow vertically. */ const getYOverflowHeight = (element) => Math.max(0, element.scrollHeight - element.clientHeight); /** * Calculates the offset required to center an element within a container along a single axis. * * The returned value is clamped so that the resulting translation does not * exceed the container's scrollable bounds. * * This function performs pure math only and does not access the DOM. * * @returns A negative offset value suitable for use in a CSS `translate` * transform, or `0` when no overflow exists on the axis. */ const calculateCenterOffset = ({ overflowSize, containerSize, elementOffset, elementSize, }) => { if (overflowSize <= 0) { return 0; } const containerCenter = containerSize / 2; const elementCenter = elementOffset + elementSize / 2; const targetOffset = elementCenter - containerCenter; return -Math.max(0, Math.min(targetOffset, overflowSize)); }; /** * Translates a container so that a target element is visually centered within its visible bounds. * * Centering is achieved by applying a CSS `transform: translate(...)` to the * container element rather than using native scrolling. * * ### Behavior * - Centering is calculated independently for each enabled axis. * - Translation is applied only when the container content overflows on that axis. * - When no overflow exists, the container remains untransformed for that axis. * * ### Notes * - This function performs immediate DOM reads and writes. * - The resulting transform is clamped to valid scrollable bounds. * * @param containerElement - The container whose content is translated. * @param elementToCenter - The descendant element to align to the container’s center. * @param options - Optional configuration controlling which axis or axes are centered. */ const centerElementInContainer = (containerElement, elementToCenter, { axis = 'both' } = {}) => { let translateX = 0; let translateY = 0; if (axis === 'x' || axis === 'both') { translateX = calculateCenterOffset({ overflowSize: getXOverflowWidth(containerElement), containerSize: containerElement.clientWidth, elementOffset: elementToCenter.offsetLeft, elementSize: elementToCenter.clientWidth, }); } if (axis === 'y' || axis === 'both') { translateY = calculateCenterOffset({ overflowSize: getYOverflowHeight(containerElement), containerSize: containerElement.clientHeight, elementOffset: elementToCenter.offsetTop, elementSize: elementToCenter.clientHeight, }); } containerElement.style.transform = `translate(${translateX}px, ${translateY}px)`; }; /** * Determines whether the browser environment allows safe read access to * `localStorage`. Some platforms (e.g., Safari Private Mode, sandboxed iframes) * expose `localStorage` but still throw when accessed. * * This function **only tests read access**, making it safe even when write * operations would fail due to `QuotaExceededError` or storage restrictions. * * @returns `true` if `localStorage` exists and calling `getItem()` does not * throw; otherwise `false`. */ const isLocalStorageReadable = () => { if (typeof window === 'undefined' || !window.localStorage) { return false; } try { window.localStorage.getItem('__non_existing_key__'); return true; } catch { return false; } }; /** * Determines whether the browser's `localStorage` supports safe read and write operations. * This function performs two independent checks: * * **1. Readability** * - Verified by calling `localStorage.getItem()` inside a `try` block. * - Fails in environments where storage access throws immediately (e.g., disabled storage, * sandboxed iframes, strict privacy modes, SSR). * * **2. Writeability** * - Verified by attempting to `setItem()` and then `removeItem()` using a temporary key. * - Can fail due to: * - `QuotaExceededError` when storage is full. * - Disabled write access (e.g., Safari Private Mode). * - Security-restricted contexts (third-party frames, hardened privacy settings) * * @returns An object describing the detected `localStorage` capabilities. */ const getLocalStorageCapabilities = () => { const readable = isLocalStorageReadable(); if (!readable) { return { readable: false, writable: false, }; } try { const key = '__test_write__'; window.localStorage.setItem(key, '1'); window.localStorage.removeItem(key); return { readable: true, writable: true, }; } catch { // Readable but not writable (QuotaExceededError, private mode, security restrictions) } return { readable: true, writable: false, }; }; /** * Initiates a file download in a browser environment. * * This utility supports downloading from: * - a URL string * - a `Blob` * - a `MediaSource` * * For non-string inputs, an object URL is created temporarily and * automatically revoked after the download is triggered. * * @remarks * - This function performs direct DOM manipulation and must be executed in a browser environment. * - In non-DOM contexts (e.g. SSR), the function exits without side effects. * - Object URLs are revoked asynchronously to avoid Safari-related issues. * * @param file - The file source to download (URL string or binary object). * @param options - Optional configuration controlling filename and link target. */ const downloadFile = (file, { fileName, target } = {}) => { // Browser guard (SSR / non-DOM environments) if ((0,_guards__WEBPACK_IMPORTED_MODULE_0__.isUndefined)(document)) { return; } const link = document.createElement('a'); let objectUrl = null; try { const href = (0,_string__WEBPACK_IMPORTED_MODULE_1__.isString)(file) ? file : (objectUrl = URL.createObjectURL(file)); link.href = href; if (fileName) { link.download = fileName; } if (target) { link.target = target; } // Required for Firefox / Safari document.body.appendChild(link); link.click(); } finally { link.remove(); if (objectUrl) { // Delay revocation to avoid Safari issues setTimeout(() => { (0,_guards__WEBPACK_IMPORTED_MODULE_0__.assert)(objectUrl, 'Object URL should not be null'); URL.revokeObjectURL(objectUrl); }, 0); } } }; /***/ }), /***/ "./src/file.ts": /*!*********************!*\ !*** ./src/file.ts ***! \*********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ blobToFile: () => (/* binding */ blobToFile), /* harmony export */ fileListToFiles: () => (/* binding */ fileListToFiles), /* harmony export */ isFile: () => (/* binding */ isFile), /* harmony export */ parseFileName: () => (/* binding */ parseFileName), /* harmony export */ readFilesFromDataTransfer: () => (/* binding */ readFilesFromDataTransfer), /* harmony export */ traverseFileSystemDirectory: () => (/* binding */ traverseFileSystemDirectory) /* harmony export */ }); /* harmony import */ var _async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./async */ "./src/async.ts"); /** * Checks if a value is a `File` object. * * @param value - The value to check. * * @returns `true` if the value is a `File` object; otherwise, `false`. */ const isFile = (value) => value instanceof File; /** * Splits a file name into its base name and extension. * * Special cases: * - Files without a dot return `[fileName, ""]` * - Hidden files like `.gitignore` return `[".gitignore", ""]` * - Names ending with a trailing dot (e.g., `"file."`) return `["file.", ""]` * - Multi-dot names (e.g., `"archive.tar.gz"`) split on the last dot * * @param fileName - The full file name to parse. * * @returns A tuple where: * - index 0 is the base name * - index 1 is the file extension (lowercased), or an empty string if none exists */ const parseFileName = (fileName) => { const lastDotIndex = fileName.lastIndexOf('.'); // No dot or leading dot with no extension (e.g., ".gitignore") if (lastDotIndex <= 0 || lastDotIndex === fileName.length - 1) { return [fileName, '']; } return [fileName.slice(0, lastDotIndex), fileName.slice(lastDotIndex + 1).toLowerCase()]; }; /** * Converts a `FileList` object to an array of `File` objects. * * @param fileList - The `FileList` object to convert. * * @returns An array of `File` objects. */ const fileListToFiles = (fileList) => { if (!fileList) { return []; } const files = []; for (let i = 0; i < fileList.length; i++) { files.push(fileList[i]); } return files; }; /** * Converts a `Blob` object into a `File` object with the specified name. * * This is useful when you receive a `Blob` (e.g., from canvas, fetch, or file manipulation) * and need to convert it into a `File` to upload via `FormData` or file inputs. * * @param blob - The `Blob` to convert. * @param fileName - The desired name for the resulting file (including extension). * * @returns A `File` instance with the same content and MIME type as the input `Blob`. * * @example * ```ts * const blob = new Blob(['Hello world'], { type: 'text/plain' }); * const file = blobToFile(blob, 'hello.txt'); * * console.log(file instanceof File); // true * ``` */ const blobToFile = (blob, fileName) => new File([blob], fileName, { type: blob.type, }); /** * Reads all entries from a file system directory asynchronously. * * @param directoryEntry - The directory entry to read. * * @returns A promise that resolves to all `FileSystemEntry` items in the directory. */ const readFileSystemDirectoryEntries = async (directoryEntry) => { const directoryReader = directoryEntry.createReader(); const readAll = async () => new Promise((resolve, reject) => { directoryReader.readEntries(async (entries) => { if (!entries.length) { resolve([]); return; } try { const restEntries = await readAll(); resolve([...entries, ...restEntries]); } catch (e) { reject(e); } }, reject); }); return readAll(); }; /** * Recursively scans a directory using the File System API and collects all nested files. * * This function walks through all subdirectories, resolving each file into a `File` object. * Directories themselves are not returned. To avoid unnecessary noise, certain system or * OS-generated files can be excluded via the `skipFiles` option. * * @param directoryEntry - The starting directory entry to traverse. * @param options - Optional settings that control traversal behavior. * * @returns A promise resolving to a flat array of all collected `File` objects. */ const traverseFileSystemDirectory = async (directoryEntry, { skipFiles = [ '.DS_Store', 'Thumbs.db', 'desktop.ini', 'ehthumbs.db', '.Spotlight-V100', '.Trashes', '.fseventsd', '__MACOSX', ], } = {}) => { const skipFilesSet = new Set(skipFiles); const entries = await readFileSystemDirectoryEntries(directoryEntry); const filePromises = await (0,_async__WEBPACK_IMPORTED_MODULE_0__.runParallel)(entries, async (entry) => { if (entry.isDirectory) { return traverseFileSystemDirectory(entry, { skipFiles, }); } else if (!skipFilesSet.has(entry.name)) { const file = await new Promise((resolve, reject) => { entry.file(resolve, reject); }); return [file]; } return []; }); return filePromises.flat(); }; /** * Reads files from a `DataTransfer` object, supporting both individual files * and entire directories (when available through the non-standard `webkitGetAsEntry` API). * * This function is typically used in drag-and-drop or paste handlers to obtain * all `File` objects contained in the user's action. When directories are dropped, * they are traversed recursively using `traverseFileSystemDirectory`, returning a * fully flattened list of nested files. * * @param dataTransfer - The `DataTransfer` instance from a drop or paste event. * If `null` or missing items, an empty array is returned. * @param traverseOptions - Optional settings passed to directory traversal. * * @returns A promise that resolves to a flat array of all extracted `File` objects. * This includes: * - direct files from the drag event, * - files extracted from directory entries via `webkitGetAsEntry`, * - and files found recursively within nested subdirectories. */ const readFilesFromDataTransfer = async (dataTransfer, traverseOptions = {}) => { const items = dataTransfer?.items; if (!items) { return []; } const tasks = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const item = items[itemIndex]; // Prefer using webkitGetAsEntry when available (directory support) // https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry#browser_compatibility if ('webkitGetAsEntry' in item) { // ?.() -> avoids throwing on Safari weirdness const entry = item.webkitGetAsEntry?.(); if (entry?.isDirectory) { tasks.push(traverseFileSystemDirectory(entry, traverseOptions)); continue; } if (entry?.isFile) { tasks.push(new Promise((resolve, reject) => entry.file(file => resolve([file]), reject))); continue; } } // Fallback to standard API const file = item.getAsFile(); if (file) { tasks.push(Promise.resolve([file])); } } return (await Promise.all(tasks)).flat(); }; /***/ }), /***/ "./src/function.ts": /*!*************************!*\ !*** ./src/function.ts ***! \*************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ delay: () => (/* binding */ delay), /* harmony export */ invokeIfFunction: () => (/* binding */ invokeIfFunction), /* harmony export */ isFunction: () => (/* binding */ isFunction), /* harmony export */ noop: () => (/* binding */ noop), /* harmony export */ not: () => (/* binding */ not), /* harmony export */ once: () => (/* binding */ once), /* harmony export */ retry: () => (/* binding */ retry), /* harmony export */ timeout: () => (/* binding */ timeout) /* harmony export */ }); const noop = () => { }; /** * Checks if a value is a function. * * @param value - The value to check. * * @returns `true` if the value is a function; otherwise, `false`. */ const isFunction = (value) => typeof value === 'function'; /** * Creates a function that negates the result of the given predicate function. * * @template Args - Argument types of the predicate function. * * @param fn - A function that returns any value. * * @returns A new function that returns the negated result of the original function. * * @example * ```ts * const isEven = (n: number) => n % 2 === 0; * const isOdd = not(isEven); * * console.log(isOdd(2)); // false * console.log(isOdd(3)); // true * ``` */ const not = (fn) => (...args) => !fn(...args); /** * Invokes the given input if it is a function, passing the provided arguments. * Otherwise, returns the input as-is. * * @template Args - Tuple of argument types to pass to the function. * @template Result - Return type of the function or the value. * * @param input - A function to invoke with `args`, or a direct value of type `Result`. * @param args - Arguments to pass if `input` is a function. * * @returns The result of invoking the function, or the original value if it's not a function. */ const invokeIfFunction = (input, ...args) => (typeof input === 'function' ? input(...args) : input); /** * Creates a promise that resolves after the specified delay. * * Useful for creating artificial delays, implementing timeouts, or spacing operations. * * @param delayMs - The delay in milliseconds. * * @returns A promise that resolves after the specified delay. * * @example * ```ts * // Wait for 1 second * await delay(1000); * console.log('This logs after 1 second'); * * // Use with other async operations * const fetchWithTimeout = async () => { * const timeoutPromise = delay(5000).then(() => { * throw new Error('Request timed out'); * }); * * return Promise.race([fetchData(), timeoutPromise]); * } * ``` */ const delay = (delayMs) => new Promise(resolve => setTimeout(resolve, delayMs)); /** * Wraps a promise with a timeout. If the promise does not settle within the specified time, * it will reject with a timeout error. * * @template T - The type of the promise result. * * @param promise - The promise to wrap. * @param timeoutMs - Timeout duration in milliseconds. * @param errorMessage - Optional custom error message. * * @returns A promise that resolves or rejects with the original promise, * or rejects with a timeout error if the duration is exceeded. * * @example * ```ts * // Rejects if fetch takes longer than 3 seconds * const response = await timeout(fetch('/api/data'), 3000); * * // With custom message * await timeout(fetchData(), 2000, 'Too long'); * ``` */ const timeout = async (promise, timeoutMs, errorMessage = 'Operation timed out') => { const timeoutId = null; try { return await Promise.race([ promise, delay(timeoutMs).then(() => Promise.reject(new Error(errorMessage))), ]); } finally { if (timeoutId) { clearTimeout(timeoutId); } } }; /** * Wraps an asynchronous function with retry logic. * * The returned function will attempt to call the original function up to `maxAttempts` times, * with a delay between retries. If all attempts fail, the last encountered error is thrown. * * Useful for operations that may fail intermittently, such as network requests. * * @template Task - The type of the async function to wrap. * @template TaskResult - The result type of the async function. * * @param task - The async function to wrap with retry logic. * @param options - Configuration options for retry behavior. * * @returns A function that wraps the original function with retry support. * * @example * ```ts * async function fetchData() { * const response = await fetch('/api/data'); * * if (!response.ok) { * throw new Error('Network error'); * } * * return await response.json(); * } * * const fetchWithRetry = retry(fetchData, { * maxAttempts: 5, * delayMs: 500, * onRetry: (attempt, error) => { * console.warn(`Attempt ${attempt} failed:`, error); * } * }); * * fetchWithRetry() * .then(data => console.log('Success:', data)) * .catch(error => console.error('Failed after retries:', error)); * ``` */ const retry = (task, { maxAttempts = 3, delayMs = 300, backoff = true, onRetry } = {}) => { return async (...args) => { let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await task(...args); } catch (e) { lastError = e; if (attempt < maxAttempts) { onRetry?.(attempt, e); const delayTime = backoff ? delayMs * 2 ** (attempt - 1) : delayMs; await delay(delayTime); } } } throw lastError; }; }; /** * Wraps a function so that it can only be executed once. * The wrapped function remembers (caches) the result of the first invocation * and returns that same result for all subsequent calls, regardless of the arguments provided. * * Common use cases include: * - initializing singletons * - running setup logic only once * - avoiding repeated expensive computations * * @template T - A function type whose return value should be cached. * * @param fn - The function to execute at most once. * * @returns A new function with the same signature as `fn`, but guaranteed to * execute `fn` only on the first call and return the cached result * thereafter. */ const once = (fn) => { let called = false; let result; return function (...args) { if (!called) { called = true; result = fn.apply(this, args); } return result; }; }; /***/ }), /***/ "./src/guards.ts": /*!***********************!*\ !*** ./src/guards.ts ***! \***********************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ assert: () => (/* binding */ assert), /* harmony export */ isBlob: () => (/* binding */ isBlob), /* harmony export */ isBool: () => (/* binding */ isBool), /* harmony export */ isDate: () => (/* binding */ isDate), /* harmony export */ isDecimal: () => (/* binding */ isDecimal), /* harmony export */ isDefined: () => (/* binding */ isDefined), /* harmony export */ isEmptyObject: () => (/* binding */ isEmptyObject), /* harmony export */ isError: () => (/* binding */ isError), /* harmony export */ isFiniteNumber: () => (/* binding */ isFiniteNumber), /* harmony export */ isInteger: () => (/* binding */ isInteger), /* harmony export */ isMap: () => (/* binding */ isMap), /* harmony export */ isNil: () => (/* binding */ isNil), /* harmony export */ isNull: () => (/* binding */ isNull), /* harmony export */ isNumber: () => (/* binding */ isNumber), /* harmony export */ isObject: () => (/* binding */ isObject), /* harmony export */ isRegExp: () => (/* binding */ isRegExp), /* harmony export */ isSet: () => (/* binding */ isSet), /* harmony export */ isSymbol: () => (/* binding */ isSymbol), /* harmony export */ isUndefined: () => (/* binding */ isUndefined), /* harmony export */ isValidDate: () => (/* binding */ isValidDate) /* harmony export */ }); function assert(condition, message) { if (!condition) { throw new Error(message); } } /** * Checks if a value is null. * * @param value - The value to check. * * @returns `true` if the value is null; otherwise, `false`. */ const isNull = (value) => value === null; /** * Checks if a value is null or undefined. * * @param value - The value to check. * * @returns `true` if the value is `null` or `undefined`, otherwise `false`. */ const isNil = (value) => value === undefined || value === null; /** * Checks if a value is neither `null` nor `undefined`. * * @param value - The value to check. * * @returns `true` if the value is defined (not `null` or `undefined`); otherwise, `false`. */ const isDefined = (value) => value !== null && value !== undefined; /** * Checks if a value is undefined. * * @param value - The value to check. * * @returns `true` if the value is undefined; otherwise, `false`. */ const isUndefined = (value) => value === undefined; /** * Checks if a value is a number. * * @param value - The value to check. * * @returns `true` if the value is a number; otherwise, `false`. */ const isNumber = (value) => typeof value === 'number'; /** * Checks if a value is a boolean. * * @param value - The value to check. * * @returns `true` if the value is a boolean; otherwise, `false`. */ const isBool = (value) => typeof value === 'boolean'; /** * Checks if a value is an object. * * @param value - The value to check. * * @returns `true` if the value is an object; otherwise, `false`. */ const isObject = (value) => typeof va