import React, { useState, useEffect, useRef, useId, useMemo, useCallback, } from "react"; import { ChevronDownIcon, CheckIcon } from "@heroicons/react/24/outline"; import { getSizeStyles } from "../../data/getInputSizes"; import Textfield from "../Textfeild/Textfeild"; import { motion } from "framer-motion"; import { Tooltip } from "../Tooltip/Tooltip"; import { createPortal } from "react-dom"; import { checkIsMobile } from "../../utils/checkIsMobile"; interface DataItem { key: number | string; value: string; disabled?: boolean; isGroupHeader?: boolean; originalGroupKey?: string | number; } interface AutoCompleteProps { data: DataItem[]; multiselect?: boolean; inPage?: boolean; disabled?: boolean; selectedKeys: (number | string)[]; onChange: (keys: (number | string)[]) => void | []; width?: string; buttonHeight?: number | string; title?: string; error?: boolean; size?: "small" | "medium" | "large"; helperText?: string; onChangeValue?: (data: { value: string; key: number | string }) => void; onGroupHeaderClick?: (groupKey: string | number) => void; selectField?: boolean; } const AutoComplete: React.FC = ({ data, multiselect = false, selectedKeys, onChange, disabled = false, inPage = false, title = "", error = false, size = "medium", helperText, onChangeValue, onGroupHeaderClick, selectField = false, }) => { const [filteredData, setFilteredData] = useState(data); const [showOptions, setShowOptions] = useState(false); const [dropdownWidth, setDropdownWidth] = useState(0); const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number; }>({ top: 0, left: 0 }); const [maxHeight, setMaxHeight] = useState(240); const inputRef = useRef(null); const dropdownRef = useRef(null); const uniqueId = useId(); const selectedKeysRef = useRef<(number | string)[]>(selectedKeys); const isInternalChangeRef = useRef(false); useEffect(() => { const updateDropdownDimensions = () => { if (inputRef.current) { const rect = inputRef.current.getBoundingClientRect(); const defaultMaxHeight = 240; const spaceBelow = window.innerHeight - rect.bottom; const availableHeight = Math.max(100, spaceBelow - 10); const calculatedMaxHeight = spaceBelow < defaultMaxHeight ? availableHeight : defaultMaxHeight; setDropdownWidth(rect.width); setMaxHeight(calculatedMaxHeight); setDropdownPosition({ top: rect.bottom + window.scrollY, left: rect.left + window.scrollX, }); } }; updateDropdownDimensions(); const resizeObserver = new ResizeObserver(updateDropdownDimensions); if (inputRef.current) { resizeObserver.observe(inputRef.current); } window.addEventListener("resize", updateDropdownDimensions); window.addEventListener("scroll", updateDropdownDimensions); return () => { resizeObserver.disconnect(); window.removeEventListener("resize", updateDropdownDimensions); window.removeEventListener("scroll", updateDropdownDimensions); }; }, []); useEffect(() => { if (!showOptions) return; let animationFrameId: number; let isActive = true; let lastTop = 0; let lastLeft = 0; let lastWidth = 0; let lastMaxHeight = 0; const updatePosition = (force = false) => { if (!isActive || !inputRef.current) return; const rect = inputRef.current.getBoundingClientRect(); const defaultMaxHeight = 240; const viewportHeight = window.visualViewport?.height || window.innerHeight; const spaceBelow = viewportHeight - rect.bottom; const availableHeight = Math.max(100, spaceBelow - 10); const calculatedMaxHeight = spaceBelow < defaultMaxHeight ? availableHeight : defaultMaxHeight; const newTop = rect.bottom + window.scrollY; const newLeft = rect.left + window.scrollX; const newWidth = rect.width; if ( force || Math.abs(newTop - lastTop) > 0.5 || Math.abs(newLeft - lastLeft) > 0.5 || Math.abs(newWidth - lastWidth) > 0.5 || Math.abs(calculatedMaxHeight - lastMaxHeight) > 1 ) { setDropdownWidth(newWidth); setMaxHeight(calculatedMaxHeight - 30); setDropdownPosition({ top: newTop, left: newLeft, }); lastTop = newTop; lastLeft = newLeft; lastWidth = newWidth; lastMaxHeight = calculatedMaxHeight; } if (isActive && showOptions) { animationFrameId = requestAnimationFrame(() => updatePosition(false)); } }; updatePosition(); const handleResize = () => updatePosition(true); const handleScroll = () => updatePosition(); let lastViewportHeight = window.visualViewport?.height || window.innerHeight; const handleVisualViewportResize = () => { const currentHeight = window.visualViewport?.height || window.innerHeight; const heightDiff = Math.abs(currentHeight - lastViewportHeight); lastViewportHeight = currentHeight; if (heightDiff > 50) { setTimeout(() => { updatePosition(true); setTimeout(() => updatePosition(true), 200); setTimeout(() => updatePosition(true), 400); }, 50); } else { setTimeout(() => updatePosition(true), 50); } }; const handleVisualViewportScroll = () => updatePosition(); const handleFocus = () => { setTimeout(() => updatePosition(true), 300); }; const handleBlur = () => { setTimeout(() => { updatePosition(true); setTimeout(() => updatePosition(true), 200); setTimeout(() => updatePosition(true), 400); }, 100); }; window.addEventListener("resize", handleResize); window.addEventListener("scroll", handleScroll, true); if (checkIsMobile()) { if (window.visualViewport) { window.visualViewport.addEventListener( "resize", handleVisualViewportResize ); window.visualViewport.addEventListener( "scroll", handleVisualViewportScroll ); } const inputElement = inputRef.current; if (inputElement) { inputElement.addEventListener("focus", handleFocus); inputElement.addEventListener("blur", handleBlur); inputElement.addEventListener("touchstart", handleFocus); } } return () => { isActive = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); } window.removeEventListener("resize", handleResize); window.removeEventListener("scroll", handleScroll, true); if (checkIsMobile()) { if (window.visualViewport) { window.visualViewport.removeEventListener( "resize", handleVisualViewportResize ); window.visualViewport.removeEventListener( "scroll", handleVisualViewportScroll ); } const inputElement = inputRef.current; if (inputElement) { inputElement.removeEventListener("focus", handleFocus); inputElement.removeEventListener("blur", handleBlur); inputElement.removeEventListener("touchstart", handleFocus); } } }; }, [showOptions]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; const clickedInsideCurrent = target.closest(`.select-group-${uniqueId}`); const clickedOnAnotherAutocomplete = target.closest(".select-group") && !clickedInsideCurrent; const clickedOnPortalDropdown = target.closest( `[data-autocomplete-portal="${uniqueId}"]` ); if (clickedOnAnotherAutocomplete) { setShowOptions(false); return; } if (!clickedInsideCurrent && !clickedOnPortalDropdown) { setTimeout(() => { const isInputFocused = document.activeElement === inputRef.current; if (!isInputFocused) { setShowOptions(false); } }, 0); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [uniqueId]); useEffect(() => { setFilteredData(data); }, [data]); useEffect(() => { selectedKeysRef.current = selectedKeys; }, [selectedKeys]); useEffect(() => { if (isInternalChangeRef.current) { isInternalChangeRef.current = false; return; } if (selectedKeys?.length > 0 && onChangeValue) { const selectedItem = data.find((item) => item.key === selectedKeys[0]); if (selectedItem) { onChangeValue({ value: selectedItem.value.trim(), key: selectedItem.key, }); } } }, [selectedKeys, data]); useEffect(() => { if (!showOptions) { setIsTyping(false); } }, [showOptions]); useEffect(() => { if (!showOptions || !checkIsMobile()) return; const originalOverflow = window.getComputedStyle(document.body).overflow; const originalPosition = window.getComputedStyle(document.body).position; const originalTop = document.body.style.top; const scrollY = window.scrollY; document.body.style.overflow = "hidden"; document.body.style.position = "fixed"; document.body.style.top = `-${scrollY}px`; document.body.style.width = "100%"; const preventTouchMove = (e: TouchEvent) => { const target = e.target as HTMLElement; const dropdown = document.querySelector( `[data-autocomplete-portal="${uniqueId}"]` ); if (dropdown) { const touch = e.touches[0] || e.changedTouches[0]; if (touch) { const elementAtPoint = document.elementFromPoint( touch.clientX, touch.clientY ); if ( elementAtPoint && (dropdown.contains(elementAtPoint) || dropdown.contains(target)) ) { return; } } else if (dropdown.contains(target)) { return; } } e.preventDefault(); }; document.addEventListener("touchmove", preventTouchMove, { passive: false, }); return () => { document.body.style.overflow = originalOverflow; document.body.style.position = originalPosition; document.body.style.top = originalTop; document.body.style.width = ""; window.scrollTo(0, scrollY); document.removeEventListener("touchmove", preventTouchMove); }; }, [showOptions, uniqueId]); const inputValue = useMemo(() => { if (selectedKeys?.length > 0) { const selectedValues = data .filter((item) => selectedKeys?.includes(item.key)) .map((item) => item.value); return selectedValues?.join(", "); } return ""; }, [selectedKeys, data]); const [localInputValue, setLocalInputValue] = useState(""); const [isTyping, setIsTyping] = useState(false); const handleInputChange = useCallback( (event: React.ChangeEvent) => { const value = event.target.value; setLocalInputValue(value); setIsTyping(true); const filtered = data.filter((item) => item.value.toLowerCase().includes(value.toLowerCase()) ); setFilteredData(filtered); setShowOptions(true); }, [data] ); const handleChange = useCallback( (newSelectedKeys: (number | string)[]) => { isInternalChangeRef.current = true; onChange(newSelectedKeys); if (onChangeValue && newSelectedKeys.length > 0) { const selectedItem = data.find( (item) => item.key === newSelectedKeys[0] ); if (selectedItem) { onChangeValue({ value: selectedItem.value.trim(), key: selectedItem.key, }); } } }, [onChange, onChangeValue, data] ); const handleOptionClick = useCallback( (key: number | string) => { const currentSelectedKeys = selectedKeysRef.current; let newSelectedKeys: (number | string)[]; if (multiselect) { if (currentSelectedKeys.includes(key)) { newSelectedKeys = currentSelectedKeys.filter((item) => item !== key); } else { newSelectedKeys = [...currentSelectedKeys, key]; } } else { if (currentSelectedKeys.includes(key)) { newSelectedKeys = currentSelectedKeys.filter((item) => item !== key); } else { newSelectedKeys = [key]; } } handleChange(newSelectedKeys); setIsTyping(false); if (!multiselect) { setLocalInputValue(""); setShowOptions(false); } }, [multiselect, handleChange] ); const handleInputClick = useCallback(() => { document.querySelectorAll(".select-group").forEach((el) => { if (!el.classList.contains(`select-group-${uniqueId}`)) { const input = el.querySelector("input"); if (input) { input.blur(); } } }); setShowOptions(true); setFilteredData(data); setLocalInputValue(""); setIsTyping(false); }, [uniqueId, data]); const handleCloseInput = useCallback(() => { setShowOptions(false); setIsTyping(false); }, []); const selectedKeysSet = useMemo(() => new Set(selectedKeys), [selectedKeys]); const handleSelectAll = useCallback(() => { const enabledItems = filteredData.filter((item) => !item.disabled); const allEnabledKeys = enabledItems.map((item) => item.key); handleChange(allEnabledKeys); }, [filteredData, handleChange]); const handleDeselectAll = useCallback(() => { handleChange([]); }, [handleChange]); const areAllSelected = useMemo(() => { const enabledItems = filteredData.filter((item) => !item.disabled); return ( enabledItems.length > 0 && enabledItems.every((item) => selectedKeysSet.has(item.key)) ); }, [filteredData, selectedKeysSet]); const dropdownOptions = useMemo(() => { if (filteredData.length === 0) { return (
  • نتیجه‌ای یافت نشد
  • ); } const selectAllHeader = multiselect ? (
  • {areAllSelected ? "عدم انتخاب همه" : "انتخاب همه"} {areAllSelected && ( )}
  • ) : null; const options = filteredData.map((item) => { const isSelected = selectedKeysSet.has(item.key); const isGroupHeader = item.isGroupHeader; const handleClick = () => { if (isGroupHeader && onGroupHeaderClick) { const groupKey = item.originalGroupKey !== undefined ? item.originalGroupKey : String(item.key).startsWith("__group__") ? String(item.key).slice(11) : item.key; onGroupHeaderClick(groupKey); } else if (!item.disabled) { handleOptionClick(item.key); } }; return (
  • {checkIsMobile() ? ( 55 ? "text-xs" : "text-sm" }`} > {item.value} ) : ( )} {isSelected && ( )}
  • ); }); return selectAllHeader ? [selectAllHeader, ...options] : options; }, [ filteredData, selectedKeysSet, handleOptionClick, multiselect, areAllSelected, handleSelectAll, handleDeselectAll, onGroupHeaderClick, ]); const dropdownPortalContent = useMemo(() => { if (!showOptions) return null; return createPortal( {dropdownOptions} , document.body ); }, [ showOptions, dropdownPosition, dropdownWidth, uniqueId, dropdownOptions, maxHeight, ]); return (
    {dropdownPortalContent}
    ); }; export default AutoComplete;