@chayns-components/core
Version:
A set of beautiful React components for developing your own applications with chayns.
674 lines (661 loc) • 22.2 kB
JavaScript
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import { useDevice } from 'chayns-api';
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { useTheme } from 'styled-components';
import { calculateContentHeight } from '../../utils/calculate';
import { searchList, sortSearchBoxItems } from '../../utils/searchBox';
import Icon from '../icon/Icon';
import Input from '../input/Input';
import GroupName from './group-name/GroupName';
import SearchBoxBody from './search-box-body/SearchBoxBody';
import SearchBoxItem from './search-box-item/SearchBoxItem';
import { StyledSearchBoxItemImage } from './search-box-item/SearchBoxItem.styles';
import { StyledSearchBox, StyledSearchBoxHintText, StyledSearchBoxIcon, StyledSearchBoxLeftWrapper } from './SearchBox.styles';
import { useUuid } from '../../hooks/uuid';
import DropdownBodyWrapper from '../dropdown-body-wrapper/DropdownBodyWrapper';
import TagInput from '../tag-input/TagInput';
const filterSearchBoxItems = ({
customFilter,
items,
searchString,
shouldUseCustomFilterOnly
}) => {
if (typeof customFilter !== 'function') {
return searchList({
items,
searchString
});
}
if (shouldUseCustomFilterOnly) {
const filteredItems = items.filter(customFilter);
return sortSearchBoxItems({
items: filteredItems,
searchString
});
}
return searchList({
items,
searchString
}).filter(customFilter);
};
const getDropdownSearchString = ({
selectedId,
shouldKeepSelectedItemPosition,
value
}) => shouldKeepSelectedItemPosition && selectedId ? '' : value;
const SearchBox = /*#__PURE__*/forwardRef(({
container,
customFilter,
dropdownDirection,
inputProps,
isInvalid = false,
leftIcons,
lists,
maxHeight = 300,
onBlur,
onChange,
onKeyDown,
onSelect,
placeholder,
presetValue,
hintText,
selectedId,
shouldAddInputToList = true,
shouldKeepSelectedItemPosition = false,
shouldUseCustomFilterOnly = false,
shouldHideFilterButtons = false,
shouldShowContentOnEmptyInput = true,
shouldShowSmallItems = false,
shouldShowRoundImage,
shouldShowToggleIcon = false,
tagInputSettings,
shouldEnableKeyboardHighlighting
}, ref) => {
const [matchingListsItems, setMatchingListsItems] = useState(lists);
const [selectedImage, setSelectedImage] = useState();
const [internalValue, setInternalValue] = useState(typeof presetValue === 'string' && presetValue !== '' ? presetValue : '');
const inputValue = inputProps?.value;
const value = inputValue ?? internalValue;
const setValue = useCallback(nextValue => {
if (typeof inputValue !== 'string') {
setInternalValue(nextValue);
}
}, [inputValue]);
const [height, setHeight] = useState(0);
const [focusedIndex, setFocusedIndex] = useState(null);
const [hasMultipleGroups, setHasMultipleGroups] = useState(lists.length > 1);
const [filteredChildrenArray, setFilteredChildrenArray] = useState();
const [inputToListValue, setInputToListValue] = useState('');
const [groups, setGroups] = useState(['all']);
const [shouldShowBody, setShouldShowBody] = useState(false);
const uuid = useUuid();
const boxRef = useRef(null);
const contentRef = useRef(null);
const inputRef = useRef(null);
const tagInputRef = useRef(null);
const hasFocusRef = useRef(false);
const isAnimatingRef = useRef(false);
const shouldShowPresetValue = useRef(typeof presetValue === 'string' && presetValue !== '');
const theme = useTheme();
const dropdownSearchString = getDropdownSearchString({
selectedId,
shouldKeepSelectedItemPosition,
value
});
const {
isTouch
} = useDevice();
/**
* Checks if there are multiple groups in the lists
*/
useEffect(() => {
setHasMultipleGroups(lists.length > 1);
}, [lists]);
const filterButtons = useMemo(() => {
const items = [];
if (lists.length <= 1) {
return items;
}
lists.forEach(({
groupName
}) => {
if (groupName) {
items.push({
id: groupName,
text: groupName
});
}
});
return items;
}, [lists]);
/**
* Filters the lists by the FilterButtons
*/
const activeList = useMemo(() => {
let newLists = [];
if (groups[0] === 'all') {
newLists = lists;
} else {
lists.forEach(list => {
if (list.groupName && groups.includes(list.groupName)) {
newLists.push(list);
}
});
}
const newMatchingItems = [];
newLists.forEach(({
list,
groupName
}) => {
const newList = filterSearchBoxItems({
customFilter,
items: list,
searchString: dropdownSearchString,
shouldUseCustomFilterOnly
});
if (newList.length > 0) {
newMatchingItems.push({
groupName,
list: newList
});
}
});
if (newMatchingItems.length === 0 && shouldAddInputToList) {
newMatchingItems.push({
groupName: undefined,
list: []
});
}
const filteredMatchingListItems = newMatchingItems.map(({
list,
groupName
}) => ({
groupName,
list: list.filter(item => {
if (typeof customFilter === 'function' && shouldUseCustomFilterOnly) {
return true;
}
return !(newMatchingItems.length === 1 && item.text === dropdownSearchString);
})
}));
setMatchingListsItems(filteredMatchingListItems);
return newLists;
}, [groups, lists, customFilter, dropdownSearchString, shouldAddInputToList, shouldUseCustomFilterOnly]);
const handleOpen = useCallback(() => {
setShouldShowBody(true);
}, []);
const handleClose = useCallback(() => {
setShouldShowBody(false);
}, []);
const handleFilterButtonsGroupSelect = keys => {
setGroups(keys.length === 0 ? ['all'] : keys);
};
/**
* This hook calculates the height
*/
useEffect(() => {
const textArray = [];
activeList.forEach(({
list,
groupName
}) => {
list.forEach(({
text
}) => textArray.push(text));
if (!groupName) {
return;
}
textArray.push(groupName);
});
if (shouldAddInputToList && inputToListValue !== '') {
textArray.push(inputToListValue);
}
setHeight(calculateContentHeight(textArray));
}, [inputToListValue, activeList, placeholder, shouldAddInputToList]);
useEffect(() => {
if (selectedId) {
activeList.forEach(({
list
}) => {
const selectedItem = list.find(({
id
}) => id === selectedId);
if (selectedItem) {
setValue(selectedItem.text);
if (selectedItem.imageUrl) {
setSelectedImage(/*#__PURE__*/React.createElement(StyledSearchBoxItemImage, {
src: selectedItem.imageUrl,
$shouldShowRoundImage: shouldShowRoundImage
}));
}
}
});
}
}, [activeList, selectedId, shouldShowRoundImage]);
/**
* This hook resets the value if the selectedId changes to undefined. This is an own useEffect because the value
* should not be reset if the list changes and the selectedId is still undefined.
*/
useEffect(() => {
if (!selectedId && !shouldShowPresetValue.current) {
setValue('');
}
}, [selectedId]);
useEffect(() => {
isAnimatingRef.current = shouldShowBody;
}, [shouldShowBody]);
useEffect(() => {
if ((matchingListsItems.length !== 0 || hintText) && !isAnimatingRef.current && hasFocusRef.current) {
handleOpen();
}
}, [handleOpen, hintText, matchingListsItems.length]);
/**
* This function handles the focus event of the input and opens the dropdown if the input
* should show content on an empty input
*/
const handleFocus = useCallback(event => {
hasFocusRef.current = true;
if (typeof inputProps?.onFocus === 'function') {
inputProps.onFocus(event);
}
if (shouldShowContentOnEmptyInput) {
const newMatchingItems = [];
activeList.forEach(({
list,
groupName
}) => {
const newList = filterSearchBoxItems({
customFilter,
items: list,
searchString: dropdownSearchString,
shouldUseCustomFilterOnly
});
if (newList.length > 0) {
newMatchingItems.push({
groupName,
list: newList
});
}
});
if (newMatchingItems.length === 0 && shouldAddInputToList) {
newMatchingItems.push({
groupName: undefined,
list: []
});
}
const filteredMatchingListItems = newMatchingItems.map(({
list,
groupName
}) => ({
groupName,
list: list.filter(item => {
if (typeof customFilter === 'function' && shouldUseCustomFilterOnly) {
return true;
}
return !(newMatchingItems.length === 1 && item.text === dropdownSearchString);
})
}));
setMatchingListsItems(filteredMatchingListItems);
if (filteredMatchingListItems.length !== 0 || hintText) {
handleOpen();
}
}
}, [shouldShowContentOnEmptyInput, activeList, shouldAddInputToList, hintText, dropdownSearchString, customFilter, shouldUseCustomFilterOnly, handleOpen, inputProps]);
const handleKeyDown = useCallback(event => {
if (typeof onKeyDown === 'function') {
onKeyDown(event);
}
if (typeof inputProps?.onKeyDown === 'function') {
inputProps.onKeyDown(event);
}
}, [inputProps, onKeyDown]);
/**
* This function filters the lists by input
*/
useEffect(() => {
const newMatchingItems = [];
activeList.forEach(({
list,
groupName
}) => {
const newList = filterSearchBoxItems({
customFilter,
items: list,
searchString: dropdownSearchString,
shouldUseCustomFilterOnly
});
if (newList.length > 0) {
newMatchingItems.push({
groupName,
list: newList
});
}
});
if (newMatchingItems.length === 0 && shouldAddInputToList) {
newMatchingItems.push({
groupName: undefined,
list: []
});
}
if (shouldAddInputToList && inputToListValue !== '') {
newMatchingItems.forEach(({
list
}) => {
list.forEach(({
text
}) => {
if (text.toLowerCase() === inputToListValue.toLowerCase()) {
setInputToListValue('');
}
});
});
}
}, [inputToListValue, activeList, shouldAddInputToList, shouldShowContentOnEmptyInput, dropdownSearchString, customFilter, shouldUseCustomFilterOnly]);
const handleClick = useCallback(() => {
if (shouldShowBody) {
handleClose();
} else {
handleOpen();
}
}, [handleClose, handleOpen, shouldShowBody]);
const rightElement = useMemo(() => {
if (!shouldShowToggleIcon) {
return undefined;
}
return /*#__PURE__*/React.createElement(StyledSearchBoxIcon, {
onClick: handleClick
}, /*#__PURE__*/React.createElement(Icon, {
icons: ['fa fa-chevron-down'],
color: theme['006']
}));
}, [handleClick, shouldShowToggleIcon, theme]);
const leftElement = useMemo(() => (leftIcons || selectedImage) && /*#__PURE__*/React.createElement(StyledSearchBoxLeftWrapper, null, leftIcons && /*#__PURE__*/React.createElement(Icon, {
icons: leftIcons
}), selectedImage && selectedImage), [leftIcons, selectedImage]);
/**
* This function handles changes of the input
*/
const handleChange = useCallback(event => {
const filteredLists = [];
shouldShowPresetValue.current = false;
activeList.forEach(({
list,
groupName
}) => {
const newList = filterSearchBoxItems({
customFilter,
items: list,
searchString: event.target.value,
shouldUseCustomFilterOnly
});
if (newList.length > 0) {
filteredLists.push({
groupName,
list: newList
});
}
});
if (filteredLists.length === 0 && shouldAddInputToList) {
filteredLists.push({
groupName: undefined,
list: []
});
}
setSelectedImage(undefined);
if (!shouldShowContentOnEmptyInput && !event.target.value) {
setMatchingListsItems([]);
} else {
setMatchingListsItems(filteredLists);
}
if (filteredLists.length !== 0) {
handleOpen();
}
setValue(event.target.value);
setInputToListValue(event.target.value);
if (typeof onChange === 'function') {
onChange(event);
}
if (typeof inputProps?.onChange === 'function') {
inputProps.onChange(event);
}
}, [activeList, customFilter, handleOpen, inputProps, onChange, shouldAddInputToList, shouldShowContentOnEmptyInput, shouldUseCustomFilterOnly]);
/**
* This function handles the blur event of the input
*/
const handleBlur = useCallback(event => {
hasFocusRef.current = false;
if (typeof onBlur === 'function') {
onBlur(event);
}
if (typeof inputProps?.onBlur === 'function') {
inputProps.onBlur(event);
}
}, [inputProps, onBlur]);
const handleDropdownOutsideClick = useCallback(() => {
tagInputRef.current?.blur();
return hasFocusRef.current && isTouch;
}, [isTouch]);
const handleContainerBlur = useCallback(event => {
const nextFocusedElement = event.relatedTarget;
const currentContainer = event.currentTarget;
// Check if focus is moving outside the SearchBox container
// Also check the dropdown content (contentRef) since it's rendered in a portal
if (!nextFocusedElement || !currentContainer.contains(nextFocusedElement) && !contentRef.current?.contains(nextFocusedElement)) {
handleClose();
}
}, [handleClose]);
/**
* This function handles the item selection
*/
const handleSelect = useCallback(item => {
const newItem = {
...item,
text: item.text.replace('<b>', '').replace('</b>', '').replace('</b', '')
};
if (tagInputSettings) {
setValue('');
setInputToListValue('');
tagInputRef.current?.resetValue();
} else {
setValue(newItem.text);
}
handleClose();
setSelectedImage(newItem.imageUrl ? /*#__PURE__*/React.createElement(StyledSearchBoxItemImage, {
src: newItem.imageUrl,
$shouldShowRoundImage: shouldShowRoundImage
}) : undefined);
setMatchingListsItems([]);
if (typeof onSelect === 'function') {
onSelect(newItem);
}
}, [handleClose, onSelect, shouldShowRoundImage, tagInputSettings]);
const content = useMemo(() => {
if (hintText && matchingListsItems.length === 0) {
return /*#__PURE__*/React.createElement(StyledSearchBoxHintText, null, hintText.replace('##value##', value));
}
const items = [];
matchingListsItems.forEach(({
groupName,
list
}, index) => {
if (hasMultipleGroups) {
if (list.length <= 0) {
return;
}
if (index !== 0) {
items.push(/*#__PURE__*/React.createElement(GroupName, {
key: groupName,
name: groupName ?? ''
}));
}
}
list.forEach(({
id,
text,
imageUrl
}, listIndex) => {
items.push(/*#__PURE__*/React.createElement(SearchBoxItem, {
key: `${id}_${groupName ?? ''}`,
id: id,
text: text,
imageUrl: imageUrl,
shouldShowSmallItems: shouldShowSmallItems,
shouldShowRoundImage: shouldShowRoundImage,
onSelect: handleSelect,
groupName: groupName,
tabIndex: index === 0 && listIndex === 0 ? 0 : -1
}));
});
});
if (shouldAddInputToList && inputToListValue !== '') {
items.push(/*#__PURE__*/React.createElement(SearchBoxItem, {
id: "input-value",
onSelect: handleSelect,
shouldShowSmallItems: shouldShowSmallItems,
text: `<b>${inputToListValue}</b`,
tabIndex: items.length === 0 ? 0 : -1
}));
}
return items;
}, [hintText, matchingListsItems, shouldAddInputToList, inputToListValue, value, hasMultipleGroups, shouldShowSmallItems, shouldShowRoundImage, handleSelect]);
useEffect(() => {
const handleKeyDown = e => {
if (!shouldShowBody || matchingListsItems.length === 0) {
return;
}
const children = contentRef.current?.children;
if (!children) {
return;
}
const childrenArray = Array.from(children);
const newChildren = childrenArray.find(child => child.id.startsWith('searchBoxContent__'))?.children;
if (!(newChildren && newChildren.length > 0)) {
return;
}
const filteredChildren = Array.from(newChildren).filter(child => child.dataset.isgroupname !== 'true');
setFilteredChildrenArray(filteredChildren);
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
if (newChildren && newChildren.length > 0) {
const newIndex = focusedIndex !== null ? (focusedIndex + (e.key === 'ArrowUp' ? -1 : 1) + filteredChildren.length) % filteredChildren.length : 0;
if (focusedIndex !== null) {
const prevElement = filteredChildren[focusedIndex];
prevElement.tabIndex = -1;
}
setFocusedIndex(newIndex);
const newElement = filteredChildren[newIndex];
newElement.tabIndex = 0;
}
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
if (filteredChildren) {
const element = filteredChildren[focusedIndex ?? 0];
if (!element) {
return;
}
const {
id,
textContent
} = element;
let imageUrl;
// Just Ignore, it works
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (element.children[0]?.attributes.src) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
imageUrl = element.children[0]?.attributes.src.nodeValue;
}
const newId = id.replace('search-box-item__', '');
handleSelect({
id: newId === 'input-value' ? textContent : newId,
text: textContent ?? '',
imageUrl
});
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [filteredChildrenArray, focusedIndex, handleSelect, matchingListsItems.length, shouldShowBody]);
const handleKeyPress = useCallback(event => {
if (event.keyCode === 27) {
setMatchingListsItems([]);
}
}, []);
useImperativeHandle(ref, () => ({
clear: () => setValue('')
}), [setValue]);
useEffect(() => {
document.addEventListener('keydown', handleKeyPress);
return () => {
document.addEventListener('keydown', handleKeyPress);
};
}, [handleKeyPress]);
/**
* Update the value if preset value changes
*/
useEffect(() => {
if (presetValue) {
setValue(presetValue);
}
}, [presetValue, setValue]);
const shouldShowDropdown = shouldShowBody && (matchingListsItems.length !== 0 || !!hintText) && (value.trim() !== '' || shouldShowContentOnEmptyInput);
return useMemo(() => /*#__PURE__*/React.createElement(StyledSearchBox, {
ref: boxRef,
key: `search-box-${uuid}`,
onBlur: handleContainerBlur
}, /*#__PURE__*/React.createElement("div", {
id: `search_box_input${uuid}`
}, tagInputSettings ? /*#__PURE__*/React.createElement(TagInput, {
leftElement: leftElement,
onAdd: tagInputSettings.onAdd,
onBlur: handleBlur,
onChange: handleChange,
onFocus: handleFocus,
onRemove: tagInputSettings.onRemove,
placeholder: placeholder,
ref: tagInputRef,
shouldAllowMultiple: tagInputSettings.shouldAllowMultiple,
shouldEnableKeyboardHighlighting: shouldEnableKeyboardHighlighting,
shouldPreventEnter: true,
tags: tagInputSettings.tags
}) : /*#__PURE__*/React.createElement(Input
/* eslint-disable-next-line react/jsx-props-no-spreading */, _extends({}, inputProps, {
isInvalid: isInvalid,
leftElement: leftElement,
onBlur: handleBlur,
onChange: handleChange,
onFocus: handleFocus,
onKeyDown: handleKeyDown,
placeholder: placeholder,
ref: inputRef,
rightElement: rightElement,
shouldEnableKeyboardHighlighting: inputProps?.shouldEnableKeyboardHighlighting ?? shouldEnableKeyboardHighlighting,
value: value
}))), boxRef.current && /*#__PURE__*/React.createElement(DropdownBodyWrapper, {
anchorElement: boxRef.current,
container: container,
direction: dropdownDirection,
onClose: handleClose,
onOutsideClick: handleDropdownOutsideClick,
shouldShowDropdown: shouldShowDropdown
}, /*#__PURE__*/React.createElement(SearchBoxBody, {
filterButtons: filterButtons,
height: height,
maxHeight: maxHeight,
shouldShow: shouldShowDropdown,
key: `search-box-body-${uuid}`,
onGroupSelect: handleFilterButtonsGroupSelect,
ref: contentRef,
selectedGroups: groups,
shouldHideFilterButtons: shouldHideFilterButtons
}, content))), [container, content, dropdownDirection, filterButtons, groups, handleBlur, handleChange, handleClose, handleDropdownOutsideClick, handleFocus, handleKeyDown, height, inputProps, isInvalid, leftElement, maxHeight, onKeyDown, placeholder, rightElement, shouldHideFilterButtons, shouldShowDropdown, shouldEnableKeyboardHighlighting, tagInputSettings, uuid, value]);
});
SearchBox.displayName = 'SearchBox';
export default SearchBox;
//# sourceMappingURL=SearchBox.js.map