@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
JavaScript
/******/ (() => { // 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