@activecollab/components
Version:
ActiveCollab Components
525 lines • 17.8 kB
JavaScript
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 {
...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 {
...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 => {
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, {
...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({
...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(() => {
var _inputEl$current2;
const listenerTarget = (_inputEl$current2 = inputEl == null ? void 0 : inputEl.current) != null ? _inputEl$current2 : document;
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({
...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