UNPKG

@activecollab/components

Version:

ActiveCollab Components

524 lines • 17.9 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _styled from "styled-components"; import React, { useCallback, useState, useMemo, useEffect, useRef } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { handleKeyboardMovement } from "./HandleKeyboard"; import { StyledAutocompleteBody, StyledAutocompleteNewItem, StyledAutocompleteScrollShadow } from "./Styles"; import highlightText from "../../hooks/useHighlightText"; import { Option } from "../Select/Option"; import { StyledOption } from "../Select/Option/Styles"; import { OptionGroup } from "../Select/OptionGroup"; export function isOptionGroup(item) { return item.options !== undefined; } export const Autocomplete = _ref => { let { type, options = [], inputEl, selected = [], emptyValue, noResultText, renderOption = option => option == null ? void 0 : option.name, defaultValue, sortDirection = "asc", handleChange, optionClassName, handleEmptyAction, disabledInternalSort, AutocompleteClassName, handleDefaultOptionChange, preselectDefaultValue, keepSameOptionsOrder = false, autoHeightMax = 340, clearInputOnSelect, mixedOptions = [], filterCriteria, disableVirtualization } = _ref; const listContainerRef = useRef(null); const itemRef = useRef(null); const selectedOptions = useMemo(() => { if (Array.isArray(selected)) { return selected; } return [selected]; }, [selected]); const handleSort = useCallback(opts => { var _opts$; if (keepSameOptionsOrder) { return opts; } const isGrouped = Array.isArray((_opts$ = opts[0]) == null ? void 0 : _opts$.options); const sortOptions = (a, b) => { const aSelected = selectedOptions.includes(a.id); const bSelected = selectedOptions.includes(b.id); const aMixed = mixedOptions.includes(a.id); const bMixed = mixedOptions.includes(b.id); if (aSelected && !bSelected) { return -1; } if (!aSelected && bSelected) { return 1; } if (aMixed && !bMixed) { return -1; } if (!aMixed && bMixed) { return 1; } return 0; }; if (isGrouped) { return opts.map(group => { const sortedGroupOptions = group.options.sort(sortOptions); return _extends({}, group, { options: sortedGroupOptions }); }); } else { return opts.sort(sortOptions); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [mixedOptions, selectedOptions]); const sortList = useCallback(options => { var _options$; if (disabledInternalSort) { return handleSort([...options]); } const isGrouped = Array.isArray((_options$ = options[0]) == null ? void 0 : _options$.options); if (isGrouped) { const sortedOptions = options.map(group => { const sortedGroupOptions = group.options.sort((a, b) => sortDirection === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)); return _extends({}, group, { options: handleSort(sortedGroupOptions) }); }); return handleSort(sortedOptions); } else { const sortedOptions = options.sort((a, b) => sortDirection === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)); return handleSort(sortedOptions); } }, [sortDirection, disabledInternalSort, handleSort]); const [sortedList, setSortedList] = useState(() => sortList(options)); useEffect(() => { setSortedList(sortList(options)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options]); const [hover, setHover] = useState({ item: undefined, by: undefined }); const [filter, setFilter] = useState(""); const handleEmpty = useCallback(e => { if (e && e.button !== 0) { return; } if (handleEmptyAction) { var _inputEl$current; inputEl == null || (_inputEl$current = inputEl.current) == null || _inputEl$current.focus(); setFilter(""); handleEmptyAction(filter); } }, [filter, handleEmptyAction, inputEl]); const onAddNewMouseEnter = useCallback(() => setHover({ item: "addNew", by: "mouse" }), []); const showAddNew = useMemo(() => { return !!(emptyValue && filter.trim() && options.every(option => { if (isOptionGroup(option)) { return option.options.every(v => v.name.toLowerCase() !== filter.trim().toLowerCase()); } return option.name.toLowerCase() !== filter.trim().toLowerCase(); })); }, [emptyValue, filter, options]); const filterOptions = useCallback((options, filter) => { const trimmedFilter = filter.trim(); const isGrouped = options[0] && isOptionGroup(options[0]); const matchesFilter = option => { if (option.hidden) { return false; } const nameMatch = option.name.toLowerCase().includes(trimmedFilter.toLowerCase()); if (filterCriteria && trimmedFilter) { return nameMatch || filterCriteria(option, trimmedFilter); } return nameMatch; }; if (isGrouped) { let hovered = false; return options.reduce((acc, groupedOption) => { const filteredOptions = groupedOption.options.filter(matchesFilter); if (filteredOptions.length > 0) { if (!hovered && filter) { setHover({ item: filteredOptions[0].id, by: "keyboard" }); } hovered = true; return [...acc, _extends({}, groupedOption, { options: filteredOptions })]; } return [...acc]; }, []); } else { const filteredOptions = options.filter(matchesFilter); if (filter && filteredOptions.length > 0) { setHover({ item: filteredOptions[0].id, by: "keyboard" }); } if (filteredOptions.length === 0 && emptyValue) { setHover({ item: "addNew", by: "keyboard" }); } return filteredOptions; } }, [emptyValue, filterCriteria]); const list = useMemo(() => filterOptions(sortedList, filter), [filter, filterOptions, sortedList]); const showDefaultOption = useMemo(() => !!defaultValue && !filter, [defaultValue, filter]); const flatOptions = useMemo(() => { const options = list.reduce((acc, option) => { if (!isOptionGroup(option)) { return [...acc, option]; } return [...acc, ...option.options]; }, []); return filterOptions(options, filter); }, [filter, filterOptions, list]); const showNoResultCondition = useMemo(() => noResultText && !showAddNew && list.length < 1 && (defaultValue && filter || !defaultValue), [noResultText, showAddNew, list.length, defaultValue, filter]); const virtualItems = useMemo(() => { const items = []; if (showDefaultOption) { items.push({ type: "default", id: "default" }); } list.forEach((item, index) => { if (isOptionGroup(item)) { items.push({ type: "group", id: item.id, item }); item.options.forEach(option => { items.push({ type: "option", id: option.id, item: option, index }); }); } else { items.push({ type: "option", id: item.id, item, index }); } }); if (showNoResultCondition) { items.push({ type: "noResult", id: "noResult" }); } if (showAddNew) { items.push({ type: "addNew", id: "addNew" }); } return items; }, [list, showDefaultOption, showNoResultCondition, showAddNew]); const shouldUseVirtualization = !disableVirtualization && virtualItems.length > 40; const rowVirtualizer = useVirtualizer({ count: virtualItems.length, getScrollElement: () => listContainerRef.current, estimateSize: () => 28, overscan: 5, gap: 4 }); const handleInputChange = useCallback(e => { if (e.target && !(e.key === "ArrowDown") && !(e.key === "ArrowUp") && !(e.key === "Enter")) { setFilter(e.target.value); } }, []); const handleHoverCallback = useCallback(e => { setHover({ item: e, by: "mouse" }); }, []); const toggleSelected = useCallback(id => { let result; if (id !== null) { if (type === "multiple") { if (selectedOptions.includes(id)) { result = selectedOptions.filter(_id => _id !== id); } else { result = [...selectedOptions, id]; } } else { result = id; } if (clearInputOnSelect && inputEl != null && inputEl.current) { inputEl.current.value = ""; inputEl.current.dispatchEvent(new Event("change", { bubbles: true })); } setFilter(""); } else { if (typeof handleDefaultOptionChange === "function") { handleDefaultOptionChange(); return; } } if (typeof handleChange === "function") { handleChange(result); } }, [clearInputOnSelect, handleChange, handleDefaultOptionChange, inputEl, selectedOptions, type]); const handleMouseEnter = useCallback(e => { if (e === undefined || e === null) { return setHover({ item: null, by: "mouse" }); } setHover({ item: e, by: "mouse" }); }, []); const handleClick = useCallback(e => { e.preventDefault(); toggleSelected(hover.item); }, [toggleSelected, hover]); const handleRenderOption = useCallback((item, index) => { if (isOptionGroup(item)) { return /*#__PURE__*/React.createElement(OptionGroup, { checked: selectedOptions, name: item.name, tooltip: item.tooltip, key: item.id, setHover: handleHoverCallback, id: item.id, hover: hover.item, options: item.options, renderOptions: handleRenderOption, type: type, onChange: handleChange, filter: filter, mixedOptions: mixedOptions }); } return /*#__PURE__*/React.createElement(Option, { name: item.name, ref: hover.item === item.id ? itemRef : null, key: index, tooltip: item.tooltip, onMouseEnter: handleMouseEnter, onClick: handleClick, id: item.id, hover: item.id === hover.item, className: optionClassName, renderOption: renderOption(_extends({}, item, { name: highlightText(item.name, filter) }), { id: "option_" + item.id, checked: selectedOptions && selectedOptions.includes(item.id), hover: hover.item === item.id, onChange: () => null }) }); }, [handleClick, handleMouseEnter, hover.item, optionClassName, renderOption, filter, selectedOptions, handleHoverCallback, type, handleChange, mixedOptions]); const handleOnMouseLeave = useCallback(() => { setHover({ item: undefined, by: "mouse" }); }, []); useEffect(() => { if (hover.by === "keyboard" && hover.item !== undefined) { const index = virtualItems.findIndex(item => item.id === hover.item); if (index !== -1) { rowVirtualizer.scrollToIndex(index, { align: "center" }); } } }, [hover, rowVirtualizer, virtualItems]); const handleOnKeyDown = useCallback(e => { if (e.key === "Enter") { e.preventDefault(); if (hover.item === undefined && filter === "") { return; } if (hover.item === "addNew") { handleEmpty(null); return; } if (hover.item === null && handleDefaultOptionChange) { handleDefaultOptionChange(); return; } if (typeof hover.item !== "undefined" || hover.item !== null) { toggleSelected(hover.item); setFilter(""); } return; } setHover({ item: handleKeyboardMovement(e, hover.item, flatOptions, showAddNew, showDefaultOption), by: "keyboard" }); }, [filter, flatOptions, toggleSelected, handleDefaultOptionChange, handleEmpty, hover, showAddNew, showDefaultOption]); useEffect(() => { const listenerTarget = inputEl == null ? void 0 : inputEl.current; if (!listenerTarget) return; listenerTarget.addEventListener("input", handleInputChange); listenerTarget.addEventListener("keydown", handleOnKeyDown); return () => { listenerTarget.removeEventListener("input", handleInputChange); listenerTarget.removeEventListener("keydown", handleOnKeyDown); }; }, [handleInputChange, handleOnKeyDown, inputEl]); const isDefaultOptionSelected = useCallback(() => { if (preselectDefaultValue) { return selectedOptions.length < 1 || selectedOptions[0] === ""; } else { return selectedOptions[0] === null || selectedOptions.length === flatOptions.length; } }, [flatOptions.length, preselectDefaultValue, selectedOptions]); const renderVirtualItem = useCallback((virtualItem, index) => { switch (virtualItem.type) { case "default": return /*#__PURE__*/React.createElement(Option, { name: String(defaultValue), ref: hover.item === null ? itemRef : null, hover: hover.item === null, onMouseEnter: handleMouseEnter, onClick: e => { e.preventDefault(); toggleSelected(null); }, renderOption: renderOption({ name: defaultValue, id: null }, { checked: isDefaultOptionSelected(), hover: hover.item === null, onChange: () => null }) }); case "group": if (!virtualItem.item || !isOptionGroup(virtualItem.item)) return null; return /*#__PURE__*/React.createElement(OptionGroup, { checked: selectedOptions, name: virtualItem.item.name, tooltip: virtualItem.item.tooltip, key: virtualItem.item.id, setHover: handleHoverCallback, id: virtualItem.item.id, hover: hover.item, options: virtualItem.item.options, renderOptions: handleRenderOption, type: type, onChange: handleChange, filter: filter, mixedOptions: mixedOptions }); case "option": if (!virtualItem.item || isOptionGroup(virtualItem.item)) return null; return /*#__PURE__*/React.createElement(Option, { name: virtualItem.item.name, ref: hover.item === virtualItem.id ? itemRef : null, key: index, tooltip: virtualItem.item.tooltip, onMouseEnter: () => handleMouseEnter(virtualItem.id), onClick: handleClick, id: virtualItem.id, hover: virtualItem.id === hover.item, className: optionClassName, renderOption: renderOption(_extends({}, virtualItem.item, { name: highlightText(virtualItem.item.name, filter) }), { id: "option_" + virtualItem.id, checked: selectedOptions && selectedOptions.includes(virtualItem.id), hover: hover.item === virtualItem.id, onChange: () => null }) }); case "noResult": return /*#__PURE__*/React.createElement(_StyledStyledOption, null, noResultText); case "addNew": return /*#__PURE__*/React.createElement(StyledAutocompleteNewItem, { ref: hover.item === "addNew" ? itemRef : null, key: "emptyValue", hover: hover.item === "addNew", onMouseDown: handleEmpty, onMouseEnter: onAddNewMouseEnter }, emptyValue); default: return null; } }, [defaultValue, hover.item, handleMouseEnter, renderOption, isDefaultOptionSelected, selectedOptions, handleHoverCallback, handleRenderOption, type, handleChange, filter, mixedOptions, handleClick, optionClassName, noResultText, handleEmpty, onAddNewMouseEnter, emptyValue, toggleSelected]); return /*#__PURE__*/React.createElement(StyledAutocompleteScrollShadow, { className: AutocompleteClassName, $isHidden: !defaultValue && !emptyValue && !noResultText && list.length < 1 }, _ref2 => { let { onScroll } = _ref2; return /*#__PURE__*/React.createElement(StyledAutocompleteBody, { key: "body", onMouseLeave: handleOnMouseLeave }, /*#__PURE__*/React.createElement("div", { ref: listContainerRef, style: { height: Math.min(rowVirtualizer.getTotalSize() + 12, autoHeightMax) + "px", overflow: "auto" }, onScroll: e => { if (onScroll) onScroll(e); } }, /*#__PURE__*/React.createElement("div", { style: { height: rowVirtualizer.getTotalSize() + "px", width: "100%", position: "relative" } }, shouldUseVirtualization ? rowVirtualizer.getVirtualItems().map(virtualRow => { const item = virtualItems[virtualRow.index]; return /*#__PURE__*/React.createElement("div", { key: virtualRow.index, "data-index": virtualRow.index, style: { position: "absolute", top: 0, left: 0, width: "100%", transform: "translateY(" + virtualRow.start + "px)" } }, renderVirtualItem(item, virtualRow.index)); }) : virtualItems.map((item, index) => /*#__PURE__*/React.createElement(React.Fragment, { key: index }, renderVirtualItem(item, index)))))); }); }; Autocomplete.displayName = "Autocomplete"; var _StyledStyledOption = _styled(StyledOption).withConfig({ displayName: "Autocomplete___StyledStyledOption", componentId: "sc-9x4q7e-0" })(["cursor:auto"]); //# sourceMappingURL=Autocomplete.js.map