UNPKG

react-select-async-paginate

Version:
499 lines (482 loc) 14.5 kB
// src/index.ts import Select from "react-select"; // src/components/useComponents.ts import { useMemo as useMemo2 } from "react"; import { components as defaultComponents } from "react-select"; // src/components/wrapMenuList.tsx import composeRefs from "@seznam/compose-react-refs"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { jsx } from "react/jsx-runtime"; var CHECK_TIMEOUT = 300; function wrapMenuList(MenuList2) { function WrappedMenuList(props) { const { selectProps, innerRef } = props; const { handleScrolledToBottom, shouldLoadMore } = selectProps; const checkTimeoutRef = useRef(null); const menuListRef = useRef(null); const shouldHandle = useCallback(() => { const el = menuListRef.current; if (!el) { return false; } const { scrollTop, scrollHeight, clientHeight } = el; return shouldLoadMore(scrollHeight, clientHeight, scrollTop); }, [shouldLoadMore]); const checkAndHandle = useCallback(() => { if (shouldHandle()) { if (handleScrolledToBottom) { handleScrolledToBottom(); } } }, [shouldHandle, handleScrolledToBottom]); const setCheckAndHandleTimeout = useMemo(() => { const res = () => { checkAndHandle(); checkTimeoutRef.current = setTimeout( res, CHECK_TIMEOUT ); }; return res; }, [checkAndHandle]); useEffect(() => { setCheckAndHandleTimeout(); return () => { if (checkTimeoutRef.current) { clearTimeout(checkTimeoutRef.current); } }; }, []); return /* @__PURE__ */ jsx( MenuList2, { ...props, innerRef: composeRefs(innerRef, menuListRef) } ); } return WrappedMenuList; } // src/components/useComponents.ts var MenuList = wrapMenuList( // biome-ignore lint/suspicious/noExplicitAny: fix types defaultComponents.MenuList ); var useComponents = (components) => useMemo2( () => ({ MenuList, ...components }), [components] ); // src/useAsyncPaginate.ts import { useCallback as useCallback3, useState as useState2 } from "react"; // src/useAsyncPaginateBase.ts import { useLazyRef } from "@vtaits/use-lazy-ref"; import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState } from "react"; import useIsMountedRef from "use-is-mounted-ref"; import useLatest from "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 import { getResult } from "krustykrab"; import sleep from "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 sleep(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 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 = useLatest(menuIsOpen); const isMountedRef = useIsMountedRef(); const reduceOptionsRef = useLatest(reduceOptions); const loadOptionsOnMenuOpenRef = useLatest(loadOptionsOnMenuOpen); const isInitRef = useRef2(true); const paramsRef = useRef2(params); paramsRef.current = params; const [_stateId, setStateId] = useState(0); const optionsCacheRef = useLazyRef(() => getInitialOptionsCache(params)); const callRequestOptionsRef = useLatest( (caller) => { requestOptions( caller, paramsRef, optionsCacheRef, debounceTimeout, (reduceState) => { optionsCacheRef.current = reduceState(optionsCacheRef.current); if (isMountedRef.current) { setStateId(increaseStateId); } }, reduceOptionsRef.current, isMountedRef, clearCacheOnSearchChange ); } ); const handleScrolledToBottom = useCallback2(() => { const currentInputValue = paramsRef.current.inputValue; const currentOptions2 = optionsCacheRef.current[currentInputValue]; if (currentOptions2) { callRequestOptionsRef.current("menu-scroll"); } }, [callRequestOptionsRef, optionsCacheRef]); useEffect2(() => { if (isInitRef.current) { isInitRef.current = false; } else { optionsCacheRef.current = {}; setStateId(increaseStateId); } if (defaultOptions === true) { callRequestOptionsRef.current("autoload"); } }, deps); useEffect2(() => { if (menuIsOpenRef.current && !optionsCacheRef.current[inputValue]) { callRequestOptionsRef.current("input-change"); } }, [callRequestOptionsRef, inputValue, menuIsOpenRef, optionsCacheRef]); useEffect2(() => { 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 = useMemo3(() => { 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] = useState2( defaultInputValueParam || "" ); const [menuIsOpenState, setMenuIsOpen] = useState2(!!defaultMenuIsOpenParam); const inputValue = typeof inputValueParam === "string" ? inputValueParam : inputValueState; const menuIsOpen = typeof menuIsOpenParam === "boolean" ? menuIsOpenParam : menuIsOpenState; const onInputChange = useCallback3( (nextInputValue, actionMeta) => { if (onInputChangeParam) { onInputChangeParam(nextInputValue, actionMeta); } setInputValue(nextInputValue); }, [onInputChangeParam] ); const onMenuClose = useCallback3(() => { if (onMenuCloseParam) { onMenuCloseParam(); } setMenuIsOpen(false); }, [onMenuCloseParam]); const onMenuOpen = useCallback3(() => { if (onMenuOpenParam) { onMenuOpenParam(); } setMenuIsOpen(true); }, [onMenuOpenParam]); const baseResult = useAsyncPaginateBase( { ...params, inputValue, menuIsOpen }, deps ); return { ...baseResult, inputValue, menuIsOpen, onInputChange, onMenuClose, onMenuOpen }; }; // src/withAsyncPaginate.tsx import { jsx as jsx2 } from "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__ */ jsx2( 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(Select); export { AsyncPaginate, checkIsResponse, reduceGroupedOptions, useAsyncPaginate, useAsyncPaginateBase, useComponents, validateResponse, withAsyncPaginate, wrapMenuList }; //# sourceMappingURL=index.js.map