UNPKG

@chayns-components/core

Version:

A set of beautiful React components for developing your own applications with chayns.

674 lines (661 loc) • 22.2 kB
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