UNPKG

highlight-dropdown

Version:

A user-friendly dropdown menu created using Material-UI's Autocomplete, providing easy selection and highlighting of search results in real-time. Ideal for parent-child relationship selection.

350 lines (324 loc) 11 kB
/* eslint-disable react/prop-types */ import { useEffect, useState } from "react"; import { Autocomplete, Checkbox, Chip, TextField } from "@mui/material"; import parser from "html-react-parser"; import ArrowDown from "@mui/icons-material/KeyboardArrowDown"; import ArrowUp from "@mui/icons-material/KeyboardArrowUp"; import BrowsePopup from "./BrowsePopup"; export default function HighlightDropdown({ dropdownData, selectedValues, setSelectedValues, isReadOnly = false, open, size = "medium", placeholder = "Select items", className = "", highlightColor = "text-[#DE6A00]", browsePopupTitle, }) { const [search, setSearch] = useState(""); const [data, setData] = useState([]); const [broswePopupOpen, setBroswePopupOpen] = useState(false); const customOptionLabel = (label) => { const startIndex = label.toLowerCase().indexOf(search.toLowerCase()); const endIndex = startIndex > -1 ? startIndex + search.length : 0; const highlightedLabel = `<span>${label.substring( 0, startIndex )}<span className='font-semibold ${highlightColor}'>${label.substring( startIndex, endIndex )}</span>${label.substring(endIndex)}</span>`; return parser(highlightedLabel); }; const checkGroup = (group) => { const groupLength = data.filter((c) => c.groupName === group).length; const selectedGroupLength = selectedValues.filter( (c) => c.groupName === group ).length; return groupLength === selectedGroupLength; }; const checkGroupIndeterminate = (group) => { const groupLength = data.filter((c) => c.groupName === group).length; const selectedGroupLength = selectedValues.filter( (c) => c.groupName === group ).length; if (selectedGroupLength === 0) { return false; } return groupLength !== selectedGroupLength; }; const checkOptions = (data) => { let dataIndex = -1; selectedValues.forEach((value, index) => { if (value.name === data.name) { dataIndex = index; } }); if (dataIndex === -1) { selectedValues.push(data); setSelectedValues([...selectedValues]); return; } selectedValues.splice(dataIndex, 1); setSelectedValues([...selectedValues]); }; const selectGroup = (group) => { const groupData = data.filter((c) => c.groupName === group); const selectedGroupFilms = selectedValues.filter( (c) => c.groupName === group ); if (selectedGroupFilms.length > 0) { setSelectedValues((prevState) => [ ...prevState.filter((c) => c.groupName !== group), ]); } else { setSelectedValues((prevState) => [ ...prevState, ...groupData.filter( (c) => c.groupName.toLowerCase().includes(search.toLowerCase()) || c.name.toLowerCase().includes(search.toLowerCase()) ), ]); } }; const onAutocompleteChange = (situation, option) => { switch (situation) { case "removeOption": setSelectedValues( selectedValues.filter((value) => option?.name !== value.name) ); break; case "clear": setSelectedValues([]); break; default: break; } }; const onChipDelete = (label) => { if (label.isGroup) { setSelectedValues(() => [ ...selectedValues.filter((value) => label.value !== value.groupName), ]); return; } setSelectedValues(() => [ ...selectedValues.filter( (value) => label.value !== `${value.groupName} - ${value.name}` ), ]); }; const processLabels = (tagValues) => { const labels = []; // Create a Set to store unique years const uniqueGroups = new Set(); // Iterate through the data and add unique years to the Set tagValues.forEach((item) => { uniqueGroups.add(item.groupName); }); uniqueGroups.forEach((value) => { if (checkGroup(value.toString())) { labels.push({ isGroup: true, value: value.toString(), }); } else { tagValues.forEach((option) => { if (value === option.groupName) { labels.push({ isGroup: false, value: `${option.groupName} - ${option.name}`, }); } }); } }); return labels.map((option, index) => ( <Chip key={index} style={{ backgroundColor: "#EEF4FF", color: "#3538CD", fontSize: "0.875rem", fontWeight: "500", }} label={option.value} onDelete={() => onChipDelete(option)} className="m-1" disabled={isReadOnly} /> )); }; const onGroupExpandClick = (groupName) => { data.forEach((d) => { if (d.groupName === groupName) { d.isExpanded = !d.isExpanded; } }); setData(() => [...data]); }; const handleBrowseClick = (e) => { e.stopPropagation(); setBroswePopupOpen(true); }; const handleBrowsePopupClose = () => { setBroswePopupOpen(false); }; useEffect(() => { dropdownData?.forEach((dropdownValue) => { dropdownValue.isExpanded = true; }); dropdownData.sort((a, b) => a.groupName.localeCompare(b.groupName)); setData([...dropdownData]); }, [dropdownData]); return ( <> {data?.length > 0 && ( <div className={`w-full flex items-start ${className}`}> <Autocomplete multiple isOptionEqualToValue={(option, value) => option.id === value.id} disableCloseOnSelect open={open === undefined ? search.length > 0 : open} size={size} className="w-full" inputValue={search} options={data} value={selectedValues} limitTags={1} onBlur={() => setSearch(() => "")} readOnly={isReadOnly} onChange={(_, __, situation, option) => { onAutocompleteChange(situation, option?.option); }} renderTags={(tagValues) => processLabels(tagValues)} groupBy={(option) => option.groupName} getOptionLabel={(value) => value.name} filterOptions={(options, state) => { if (state.inputValue?.length > 0) { return options.filter( (option) => option.groupName .toLowerCase() .includes(state.inputValue.toLowerCase()) || option.name .toLowerCase() .includes(state.inputValue.toLowerCase()) ); } return options; }} renderGroup={(params) => { const groupName = params.group.split(",")[0]; const isExpanded = data.find( (d) => d.groupName === groupName )?.isExpanded; return ( <div className="cursor-pointer" key={params.key} onClick={() => { onGroupExpandClick(groupName); }} > <div className="flex justify-between align-middle"> <div className="self-center" onClick={(e) => { e.stopPropagation(); selectGroup(groupName); }} > <Checkbox key={params.key} checked={checkGroup(groupName)} indeterminate={checkGroupIndeterminate(groupName)} /> <strong>{customOptionLabel(groupName)}</strong> </div> <div> {!isExpanded && ( <button type="button" className="bg-transparent border-0" > <ArrowDown /> {/* <img src={ArrowDown} alt="arrow down" /> */} </button> )} {isExpanded && ( <button type="button" className="bg-transparent border-0" > <ArrowUp /> {/* <img src={ArrowUp} alt="arrow up" /> */} </button> )} </div> </div> {isExpanded && params.children} </div> ); }} renderOption={(props, option, { index }) => ( <li {...props} onClick={(e) => { e.stopPropagation(); checkOptions(option); }} > <Checkbox key={index} id={option.id} checked={ !!selectedValues.find((value) => value.name === option.name) } /> <label className="hover:cursor-pointer" htmlFor={option.id} onClick={(e) => e.preventDefault()} > {customOptionLabel(option.name)} </label> </li> )} renderInput={(params) => ( <TextField {...params} variant="outlined" placeholder={placeholder} onChange={(e) => setSearch(e.target.value)} fullWidth /> )} /> <button className="ml-[-2px] rounded-s-none bg-[#20276F] border-[#20276F] text-white shadow-[0_0.063rem_0.125rem_0rem_rgba(16,24,40,0.05)] hover:cursor-pointer hover:bg-[#191F57] rounded-[0.5rem] py-[0.5rem] px-[0.875rem] justify-center items-center border-solid" disabled={isReadOnly} type="button" onClick={handleBrowseClick} > Browse </button> </div> )} {broswePopupOpen && ( <BrowsePopup open={broswePopupOpen} handleClose={handleBrowsePopupClose} dropdownData={data} selectedValues={selectedValues} setSelectedValues={setSelectedValues} browsePopupTitle={browsePopupTitle} /> )} </> ); }