react-select-async-paginate
Version:
Wrapper above react-select that supports pagination on menu scroll
542 lines (524 loc) • 17.1 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
AsyncPaginate: () => AsyncPaginate,
checkIsResponse: () => checkIsResponse,
reduceGroupedOptions: () => reduceGroupedOptions,
useAsyncPaginate: () => useAsyncPaginate,
useAsyncPaginateBase: () => useAsyncPaginateBase,
useComponents: () => useComponents,
validateResponse: () => validateResponse,
withAsyncPaginate: () => withAsyncPaginate,
wrapMenuList: () => wrapMenuList
});
module.exports = __toCommonJS(index_exports);
var import_react_select2 = __toESM(require("react-select"));
// src/components/useComponents.ts
var import_react2 = require("react");
var import_react_select = require("react-select");
// src/components/wrapMenuList.tsx
var import_compose_react_refs = __toESM(require("@seznam/compose-react-refs"));
var import_react = require("react");
var import_jsx_runtime = require("react/jsx-runtime");
var CHECK_TIMEOUT = 300;
function wrapMenuList(MenuList2) {
function WrappedMenuList(props) {
const { selectProps, innerRef } = props;
const { handleScrolledToBottom, shouldLoadMore } = selectProps;
const checkTimeoutRef = (0, import_react.useRef)(null);
const menuListRef = (0, import_react.useRef)(null);
const shouldHandle = (0, import_react.useCallback)(() => {
const el = menuListRef.current;
if (!el) {
return false;
}
const { scrollTop, scrollHeight, clientHeight } = el;
return shouldLoadMore(scrollHeight, clientHeight, scrollTop);
}, [shouldLoadMore]);
const checkAndHandle = (0, import_react.useCallback)(() => {
if (shouldHandle()) {
if (handleScrolledToBottom) {
handleScrolledToBottom();
}
}
}, [shouldHandle, handleScrolledToBottom]);
const setCheckAndHandleTimeout = (0, import_react.useMemo)(() => {
const res = () => {
checkAndHandle();
checkTimeoutRef.current = setTimeout(
res,
CHECK_TIMEOUT
);
};
return res;
}, [checkAndHandle]);
(0, import_react.useEffect)(() => {
setCheckAndHandleTimeout();
return () => {
if (checkTimeoutRef.current) {
clearTimeout(checkTimeoutRef.current);
}
};
}, []);
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
MenuList2,
{
...props,
innerRef: (0, import_compose_react_refs.default)(innerRef, menuListRef)
}
);
}
return WrappedMenuList;
}
// src/components/useComponents.ts
var MenuList = wrapMenuList(
// biome-ignore lint/suspicious/noExplicitAny: fix types
import_react_select.components.MenuList
);
var useComponents = (components) => (0, import_react2.useMemo)(
() => ({
MenuList,
...components
}),
[components]
);
// src/useAsyncPaginate.ts
var import_react4 = require("react");
// src/useAsyncPaginateBase.ts
var import_use_lazy_ref = require("@vtaits/use-lazy-ref");
var import_react3 = require("react");
var import_use_is_mounted_ref = __toESM(require("use-is-mounted-ref"));
var import_use_latest = __toESM(require("use-latest"));
// src/defaultReduceOptions.ts
var defaultReduceOptions = (prevOptions, loadedOptions) => [...prevOptions, ...loadedOptions];
// src/defaultShouldLoadMore.ts
var AVAILABLE_DELTA = 10;
var defaultShouldLoadMore = (scrollHeight, clientHeight, scrollTop) => {
const bottomBorder = scrollHeight - clientHeight - AVAILABLE_DELTA;
return bottomBorder < scrollTop;
};
// src/getInitialCache.ts
var getInitialCache = (params) => ({
isFirstLoad: true,
options: [],
hasMore: true,
isLoading: false,
lockedUntil: 0,
additional: params.additional
});
// src/getInitialOptionsCache.ts
var getInitialOptionsCache = ({
options,
defaultOptions,
additional,
defaultAdditional
}) => {
const initialOptions = defaultOptions === true ? null : Array.isArray(defaultOptions) ? defaultOptions : options;
if (initialOptions) {
return {
"": {
isFirstLoad: false,
isLoading: false,
options: initialOptions,
hasMore: true,
lockedUntil: 0,
additional: defaultAdditional || additional
}
};
}
return {};
};
// src/requestOptions.ts
var import_krustykrab = require("krustykrab");
var import_sleep_promise = __toESM(require("sleep-promise"));
// src/validateResponse.ts
var errorText = '[react-select-async-paginate] response of "loadOptions" should be an object with "options" prop, which contains array of options.';
var checkIsResponse = (response) => {
if (!response) {
return false;
}
const { options, hasMore } = response;
if (!Array.isArray(options)) {
return false;
}
if (typeof hasMore !== "boolean" && typeof hasMore !== "undefined") {
return false;
}
return true;
};
var validateResponse = (response) => {
if (!checkIsResponse(response)) {
console.error(errorText, "Received:", response);
throw new Error(errorText);
}
return true;
};
// src/requestOptions.ts
var requestOptions = async (caller, paramsRef, optionsCacheRef, debounceTimeout, setOptionsCache, reduceOptions, isMountedRef, clearCacheOnSearchChange) => {
const currentInputValue = paramsRef.current.inputValue;
const isCacheEmpty = !optionsCacheRef.current[currentInputValue];
const currentOptions = isCacheEmpty ? getInitialCache(paramsRef.current) : optionsCacheRef.current[currentInputValue];
if (currentOptions.isLoading || !currentOptions.hasMore || currentOptions.lockedUntil > Date.now()) {
return;
}
setOptionsCache(
(prevOptionsCache) => {
if (clearCacheOnSearchChange && caller === "input-change") {
return {
[currentInputValue]: {
...currentOptions,
isLoading: true
}
};
}
return {
...prevOptionsCache,
[currentInputValue]: {
...currentOptions,
isLoading: true
}
};
}
);
if (debounceTimeout > 0 && caller === "input-change") {
await (0, import_sleep_promise.default)(debounceTimeout);
const newInputValue = paramsRef.current.inputValue;
if (currentInputValue !== newInputValue) {
setOptionsCache((prevOptionsCache) => {
if (isCacheEmpty) {
const { [currentInputValue]: _itemForDelete, ...restCache } = prevOptionsCache;
return restCache;
}
return {
...prevOptionsCache,
[currentInputValue]: {
...currentOptions,
isLoading: false
}
};
});
return;
}
}
const { loadOptions, reloadOnErrorTimeout = 0 } = paramsRef.current;
const result = await (0, import_krustykrab.getResult)(
Promise.resolve().then(
() => loadOptions(
currentInputValue,
currentOptions.options,
currentOptions.additional
)
)
);
if (!isMountedRef.current) {
return;
}
if (result.isErr()) {
setOptionsCache((prevOptionsCache) => ({
...prevOptionsCache,
[currentInputValue]: {
...currentOptions,
isLoading: false,
lockedUntil: Date.now() + reloadOnErrorTimeout
}
}));
return;
}
const response = result.unwrap();
if (validateResponse(response)) {
const { options, hasMore } = response;
const newAdditional = Object.hasOwn(response, "additional") ? response.additional : currentOptions.additional;
setOptionsCache((prevOptionsCache) => ({
...prevOptionsCache,
[currentInputValue]: {
...currentOptions,
options: reduceOptions(currentOptions.options, options, newAdditional),
hasMore: !!hasMore,
isLoading: false,
isFirstLoad: false,
additional: newAdditional
}
}));
}
};
// src/useAsyncPaginateBase.ts
var increaseStateId = (prevStateId) => prevStateId + 1;
var useAsyncPaginateBase = (params, deps = []) => {
const {
clearCacheOnSearchChange = false,
clearCacheOnMenuClose = false,
defaultOptions,
loadOptionsOnMenuOpen = true,
debounceTimeout = 0,
inputValue,
menuIsOpen,
filterOption = null,
reduceOptions = defaultReduceOptions,
shouldLoadMore = defaultShouldLoadMore,
mapOptionsForMenu = void 0
} = params;
const menuIsOpenRef = (0, import_use_latest.default)(menuIsOpen);
const isMountedRef = (0, import_use_is_mounted_ref.default)();
const reduceOptionsRef = (0, import_use_latest.default)(reduceOptions);
const loadOptionsOnMenuOpenRef = (0, import_use_latest.default)(loadOptionsOnMenuOpen);
const isInitRef = (0, import_react3.useRef)(true);
const paramsRef = (0, import_react3.useRef)(params);
paramsRef.current = params;
const [_stateId, setStateId] = (0, import_react3.useState)(0);
const optionsCacheRef = (0, import_use_lazy_ref.useLazyRef)(() => getInitialOptionsCache(params));
const callRequestOptionsRef = (0, import_use_latest.default)(
(caller) => {
requestOptions(
caller,
paramsRef,
optionsCacheRef,
debounceTimeout,
(reduceState) => {
optionsCacheRef.current = reduceState(optionsCacheRef.current);
if (isMountedRef.current) {
setStateId(increaseStateId);
}
},
reduceOptionsRef.current,
isMountedRef,
clearCacheOnSearchChange
);
}
);
const handleScrolledToBottom = (0, import_react3.useCallback)(() => {
const currentInputValue = paramsRef.current.inputValue;
const currentOptions2 = optionsCacheRef.current[currentInputValue];
if (currentOptions2) {
callRequestOptionsRef.current("menu-scroll");
}
}, [callRequestOptionsRef, optionsCacheRef]);
(0, import_react3.useEffect)(() => {
if (isInitRef.current) {
isInitRef.current = false;
} else {
optionsCacheRef.current = {};
setStateId(increaseStateId);
}
if (defaultOptions === true) {
callRequestOptionsRef.current("autoload");
}
}, deps);
(0, import_react3.useEffect)(() => {
if (menuIsOpenRef.current && !optionsCacheRef.current[inputValue]) {
callRequestOptionsRef.current("input-change");
}
}, [callRequestOptionsRef, inputValue, menuIsOpenRef, optionsCacheRef]);
(0, import_react3.useEffect)(() => {
if (menuIsOpen) {
if (!optionsCacheRef.current[""] && loadOptionsOnMenuOpenRef.current) {
callRequestOptionsRef.current("menu-toggle");
return;
}
return;
}
if (clearCacheOnMenuClose) {
optionsCacheRef.current = {};
setStateId(increaseStateId);
}
}, [
callRequestOptionsRef,
loadOptionsOnMenuOpenRef,
menuIsOpen,
optionsCacheRef,
clearCacheOnMenuClose
]);
const currentOptions = optionsCacheRef.current[inputValue] || getInitialCache(params);
const options = (0, import_react3.useMemo)(() => {
if (!mapOptionsForMenu) {
return currentOptions.options;
}
return mapOptionsForMenu(currentOptions.options);
}, [currentOptions.options, mapOptionsForMenu]);
return {
handleScrolledToBottom,
shouldLoadMore,
filterOption,
isLoading: currentOptions.isLoading || currentOptions.lockedUntil > Date.now(),
isFirstLoad: currentOptions.isFirstLoad,
options
};
};
// src/useAsyncPaginate.ts
var useAsyncPaginate = (params, deps = []) => {
const {
inputValue: inputValueParam,
menuIsOpen: menuIsOpenParam,
defaultInputValue: defaultInputValueParam,
defaultMenuIsOpen: defaultMenuIsOpenParam,
onInputChange: onInputChangeParam,
onMenuClose: onMenuCloseParam,
onMenuOpen: onMenuOpenParam
} = params;
const [inputValueState, setInputValue] = (0, import_react4.useState)(
defaultInputValueParam || ""
);
const [menuIsOpenState, setMenuIsOpen] = (0, import_react4.useState)(!!defaultMenuIsOpenParam);
const inputValue = typeof inputValueParam === "string" ? inputValueParam : inputValueState;
const menuIsOpen = typeof menuIsOpenParam === "boolean" ? menuIsOpenParam : menuIsOpenState;
const onInputChange = (0, import_react4.useCallback)(
(nextInputValue, actionMeta) => {
if (onInputChangeParam) {
onInputChangeParam(nextInputValue, actionMeta);
}
setInputValue(nextInputValue);
},
[onInputChangeParam]
);
const onMenuClose = (0, import_react4.useCallback)(() => {
if (onMenuCloseParam) {
onMenuCloseParam();
}
setMenuIsOpen(false);
}, [onMenuCloseParam]);
const onMenuOpen = (0, import_react4.useCallback)(() => {
if (onMenuOpenParam) {
onMenuOpenParam();
}
setMenuIsOpen(true);
}, [onMenuOpenParam]);
const baseResult = useAsyncPaginateBase(
{
...params,
inputValue,
menuIsOpen
},
deps
);
return {
...baseResult,
inputValue,
menuIsOpen,
onInputChange,
onMenuClose,
onMenuOpen
};
};
// src/withAsyncPaginate.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
var defaultCacheUniqs = [];
var defaultComponents2 = {};
function withAsyncPaginate(SelectComponent) {
function WithAsyncPaginate(props) {
const {
components = defaultComponents2,
selectRef = void 0,
isLoading: isLoadingProp,
cacheUniqs = defaultCacheUniqs,
menuPlacement,
menuShouldScrollIntoView,
...rest
} = props;
const asyncPaginateProps = useAsyncPaginate(rest, cacheUniqs);
const processedComponents = useComponents(
components
);
const isLoading = typeof isLoadingProp === "boolean" ? isLoadingProp : asyncPaginateProps.isLoading;
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
SelectComponent,
{
...props,
...asyncPaginateProps,
menuPlacement,
menuShouldScrollIntoView: menuPlacement === "auto" ? isLoading ? false : menuShouldScrollIntoView : menuShouldScrollIntoView,
isLoading,
components: processedComponents,
ref: selectRef
}
);
}
return WithAsyncPaginate;
}
// src/reduceGroupedOptions.ts
var checkGroup = (group) => {
if (!group) {
return false;
}
const { label, options } = group;
if (typeof label !== "string" && typeof label !== "undefined") {
return false;
}
if (!Array.isArray(options)) {
return false;
}
return true;
};
var reduceGroupedOptions = (prevOptions, loadedOptions) => {
const res = prevOptions.slice();
const mapLabelToIndex = {};
let prevOptionsIndex = 0;
const prevOptionsLength = prevOptions.length;
for (const optionOrGroup of loadedOptions) {
const group = checkGroup(optionOrGroup) ? optionOrGroup : {
options: [optionOrGroup]
};
const { label = "" } = group;
let groupIndex = mapLabelToIndex[label];
if (typeof groupIndex !== "number") {
for (; prevOptionsIndex < prevOptionsLength && typeof mapLabelToIndex[label] !== "number"; ++prevOptionsIndex) {
const prevGroup = prevOptions[prevOptionsIndex];
if (checkGroup(prevGroup)) {
mapLabelToIndex[prevGroup.label || ""] = prevOptionsIndex;
}
}
groupIndex = mapLabelToIndex[label];
}
if (typeof groupIndex !== "number") {
mapLabelToIndex[label] = res.length;
res.push(group);
} else {
res[groupIndex] = {
...res[groupIndex],
options: [...res[groupIndex].options, ...group.options]
};
}
}
return res;
};
// src/index.ts
var AsyncPaginate = withAsyncPaginate(import_react_select2.default);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AsyncPaginate,
checkIsResponse,
reduceGroupedOptions,
useAsyncPaginate,
useAsyncPaginateBase,
useComponents,
validateResponse,
withAsyncPaginate,
wrapMenuList
});
//# sourceMappingURL=index.js.map