UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

293 lines (292 loc) 10.2 kB
"use client"; import { useAppElement } from "../../ThemeProvider/AppElementContext.mjs"; import { countVirtualItems, getOptionSearchText, isGroupOption, isValueEmpty, normalizeValueFor, splitBySeparators } from "./helpers.mjs"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { jsx } from "react/jsx-runtime"; //#region src/base-ui/Select/hooks.tsx function useSelectValue({ defaultValue, extraOptions, isMultiple, onChange, onSelect, options, setExtraOptions, value }) { const [uncontrolledValue, setUncontrolledValue] = useState(() => { if (defaultValue !== void 0) return defaultValue; return isMultiple ? [] : null; }); const normalizeValue = useMemo(() => normalizeValueFor(isMultiple), [isMultiple]); const mergedValue = value !== void 0 ? value : uncontrolledValue; const normalizedValue = useMemo(() => normalizeValue(mergedValue), [mergedValue, normalizeValue]); const valueArray = useMemo(() => { if (isMultiple) return normalizedValue; return isValueEmpty(normalizedValue) ? [] : [normalizedValue]; }, [isMultiple, normalizedValue]); const { optionMap, resolvedOptions } = useMemo(() => { const baseOptions = options ?? []; const optionValueMap = /* @__PURE__ */ new Map(); const addOption = (item) => { if (!optionValueMap.has(item.value)) optionValueMap.set(item.value, item); }; baseOptions.forEach((item) => { if (isGroupOption(item)) item.options.forEach(addOption); else addOption(item); }); const filteredExtraOptions = extraOptions.filter((item) => !optionValueMap.has(item.value)); filteredExtraOptions.forEach(addOption); const mergedOptions = [...baseOptions, ...filteredExtraOptions]; const missingValueOptions = valueArray.filter((val) => !optionValueMap.has(val)).map((val) => ({ label: String(val), value: val })); missingValueOptions.forEach(addOption); return { optionMap: optionValueMap, resolvedOptions: missingValueOptions.length ? [...mergedOptions, ...missingValueOptions] : mergedOptions }; }, [ extraOptions, options, valueArray ]); const getOption = useCallback((optionValue) => { const matched = optionMap.get(optionValue); if (matched) return matched; if (optionValue && typeof optionValue === "object" && "label" in optionValue) return { label: optionValue.label, value: optionValue }; return { label: String(optionValue), value: optionValue }; }, [optionMap]); const previousValueRef = useRef(normalizedValue); useEffect(() => { previousValueRef.current = normalizedValue; }, [normalizedValue]); const handleValueChange = useCallback((nextValue) => { const normalizedNextValue = normalizeValue(nextValue); const previousValue = previousValueRef.current; if (isMultiple) { const prevValues = Array.isArray(previousValue) ? previousValue : []; const nextValues = Array.isArray(normalizedNextValue) ? normalizedNextValue : []; nextValues.filter((val) => !prevValues.some((prev) => Object.is(prev, val))).forEach((val) => onSelect?.(val, getOption(val))); if (value === void 0) setUncontrolledValue(nextValues); onChange?.(nextValues, nextValues.map((val) => getOption(val))); } else { if (!isValueEmpty(normalizedNextValue) && !Object.is(previousValue, normalizedNextValue)) onSelect?.(normalizedNextValue, getOption(normalizedNextValue)); if (value === void 0) setUncontrolledValue(normalizedNextValue); onChange?.(normalizedNextValue, isValueEmpty(normalizedNextValue) ? void 0 : getOption(normalizedNextValue)); } previousValueRef.current = normalizedNextValue; }, [ getOption, isMultiple, normalizeValue, onChange, onSelect, value ]); return { appendTagValues: useCallback((rawValues) => { const valuesToAdd = rawValues.map((val) => val.trim()).filter(Boolean); if (!valuesToAdd.length) return; const nextValues = [...valueArray]; const newOptionValues = valuesToAdd.filter((val) => !optionMap.has(val)); if (newOptionValues.length > 0) setExtraOptions((prev) => { const existingValues = new Set(prev.map((item) => item.value)); const merged = [...prev]; newOptionValues.forEach((val) => { if (!existingValues.has(val)) merged.push({ label: val, value: val }); }); return merged; }); valuesToAdd.forEach((val) => { if (!nextValues.some((item) => Object.is(item, val))) nextValues.push(val); }); if (nextValues.length !== valueArray.length) handleValueChange(nextValues); }, [ handleValueChange, optionMap, setExtraOptions, valueArray ]), getOption, handleValueChange, normalizedValue, normalizeValue, optionMap, resolvedOptions, valueArray }; } function useSelectOpen({ defaultOpen, onOpenChange, open }) { const [uncontrolledOpen, setUncontrolledOpen] = useState(Boolean(defaultOpen)); useEffect(() => { if (open !== void 0) setUncontrolledOpen(open); }, [open]); const mergedOpen = open ?? uncontrolledOpen; return { handleOpenChange: useCallback((nextOpen, eventDetails) => { onOpenChange?.(nextOpen, eventDetails); if (open === void 0) setUncontrolledOpen(nextOpen); }, [onOpenChange, open]), mergedOpen }; } function useSelectSearch({ appendTagValues, handleOpenChange, mergedOpen, mode, resolvedOptions, showSearch, tokenSeparators }) { const [searchValue, setSearchValue] = useState(""); const shouldShowSearch = Boolean(showSearch || mode === "tags"); useEffect(() => { if (!mergedOpen) setSearchValue(""); }, [mergedOpen]); const handleSearchChange = useCallback((event) => { const nextValue = event.target.value; if (mode === "tags") { const parts = splitBySeparators(nextValue, tokenSeparators); if (parts.length > 1) { const pending = parts.pop() ?? ""; appendTagValues(parts.filter(Boolean)); setSearchValue(pending); return; } } setSearchValue(nextValue); }, [ appendTagValues, mode, tokenSeparators ]); const handleSearchKeyDown = useCallback((event) => { event.stopPropagation(); if (event.key === "Escape") { handleOpenChange(false); return; } if (mode !== "tags") return; const isSeparator = tokenSeparators?.includes(event.key); if (event.key === "Enter" || isSeparator) { event.preventDefault(); event.stopPropagation(); appendTagValues([searchValue]); setSearchValue(""); } }, [ appendTagValues, handleOpenChange, mode, searchValue, tokenSeparators ]); const stopSearchPropagation = useCallback((event) => { event.stopPropagation(); }, []); return { filteredOptions: useMemo(() => { if (!shouldShowSearch || !searchValue.trim()) return resolvedOptions; const query = searchValue.trim().toLowerCase(); return resolvedOptions.map((item) => { if (isGroupOption(item)) { const groupItems = item.options.filter((option) => getOptionSearchText(option).toLowerCase().includes(query)); if (!groupItems.length) return null; return { ...item, options: groupItems }; } return getOptionSearchText(item).toLowerCase().includes(query) ? item : null; }).filter(Boolean); }, [ resolvedOptions, searchValue, shouldShowSearch ]), handleSearchChange, handleSearchKeyDown, searchValue, shouldShowSearch, stopSearchPropagation }; } function useSelectVirtual({ filteredOptions, listItemHeight, size, valueArray, virtual }) { const listRef = useRef(null); const pointerScrollRef = useRef(false); const pointerScrollTimeoutRef = useRef(null); const renderVirtualItem = useCallback((props) => { const { ref, ...rest } = props; return /* @__PURE__ */ jsx("div", { ...rest, ref: (node) => { if (node) node.scrollIntoView = (...args) => { if (!pointerScrollRef.current) HTMLElement.prototype.scrollIntoView.call(node, ...args); }; if (typeof ref === "function") ref(node); else if (ref && "current" in ref) ref.current = node; } }); }, []); const markPointerScroll = useCallback(() => { pointerScrollRef.current = true; if (pointerScrollTimeoutRef.current) clearTimeout(pointerScrollTimeoutRef.current); pointerScrollTimeoutRef.current = setTimeout(() => { pointerScrollRef.current = false; }, 120); }, []); const handleListScroll = useCallback(() => { if (!virtual || !pointerScrollRef.current) return; const listElement = listRef.current; const activeElement = document.activeElement; if (listElement && activeElement && listElement.contains(activeElement)) listElement.focus({ preventScroll: true }); }, [virtual]); useEffect(() => { return () => { if (pointerScrollTimeoutRef.current) clearTimeout(pointerScrollTimeoutRef.current); }; }, []); const virtualListStyle = useMemo(() => { if (!virtual) return void 0; const rowCount = countVirtualItems(filteredOptions); return { height: `min(${Math.min(Math.max(rowCount, 1), 6) * (listItemHeight ?? (size === "large" ? 40 : size === "small" ? 28 : 32)) + 8}px, var(--lobe-select-available-height, var(--available-height)))` }; }, [ filteredOptions, listItemHeight, size, virtual ]); return { handleListScroll, keepMountedIndices: useMemo(() => { if (!virtual || valueArray.length === 0) return void 0; const selectedSet = new Set(valueArray); const indices = []; let index = 0; filteredOptions.forEach((item) => { if (isGroupOption(item)) { if (item.options.some((option) => selectedSet.has(option.value))) indices.push(index); index += 1; return; } if (selectedSet.has(item.value)) indices.push(index); index += 1; }); return indices.length ? indices : void 0; }, [ filteredOptions, valueArray, virtual ]), listRef, markPointerScroll, renderVirtualItem, virtualListStyle }; } function usePortalContainer() { const appElement = useAppElement(); return useMemo(() => { if (typeof window === "undefined") return appElement; if (!(appElement instanceof HTMLElement)) return void 0; return window.getComputedStyle(appElement).display === "contents" ? document.body : appElement; }, [appElement]); } //#endregion export { usePortalContainer, useSelectOpen, useSelectSearch, useSelectValue, useSelectVirtual }; //# sourceMappingURL=hooks.mjs.map