UNPKG

@chayns-components/core

Version:

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

658 lines (643 loc) • 20 kB
import { getDevice } from 'chayns-api'; import { AnimatePresence } from 'motion/react'; import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTheme } from 'styled-components'; import { calculateContentHeight } from '../../utils/calculate'; import { searchList } 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, StyledSearchBoxIcon, StyledSearchBoxLeftWrapper } from './SearchBox.styles'; import { useUuid } from '../../hooks/uuid'; const SearchBox = /*#__PURE__*/forwardRef((_ref, ref) => { let { isInvalid = false, placeholder, leftIcons, lists, onChange, onBlur, onSelect, onKeyDown, selectedId, container, shouldHideFilterButtons = false, shouldShowRoundImage, shouldShowContentOnEmptyInput = true, shouldAddInputToList = true, shouldShowToggleIcon = false, customFilter, presetValue } = _ref; const [matchingListsItems, setMatchingListsItems] = useState(lists); const [selectedImage, setSelectedImage] = useState(); const [value, setValue] = useState(typeof presetValue === 'string' && presetValue !== '' ? presetValue : ''); const [isAnimating, setIsAnimating] = useState(false); const [height, setHeight] = useState(0); const [width, setWidth] = 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 [portal, setPortal] = useState(); const [internalCoordinates, setInternalCoordinates] = useState({ x: 0, y: 0 }); const [newContainer, setNewContainer] = useState(container ?? null); const uuid = useUuid(); const boxRef = useRef(null); const contentRef = useRef(null); const inputRef = useRef(null); const hasFocusRef = useRef(false); const isAnimatingRef = useRef(false); const shouldShowPresetValue = useRef(typeof presetValue === 'string' && presetValue !== ''); const theme = useTheme(); const { browser } = getDevice(); useEffect(() => { if (boxRef.current && !container) { const el = boxRef.current; const element = el.closest('.dialog-inner') || el.closest('body'); setNewContainer(element); } }, [container]); useEffect(() => { if (container instanceof Element) { setNewContainer(container); } }, [container]); useEffect(() => { if (boxRef.current) { const { x, y } = boxRef.current.getBoundingClientRect(); setInternalCoordinates({ x, y }); } }, []); /** * Checks if Lists are smaller then 1 */ useEffect(() => { setHasMultipleGroups(lists.length > 1); }, [lists]); const filterButtons = useMemo(() => { const items = []; if (lists.length <= 1) { return items; } lists.forEach(_ref2 => { let { groupName } = _ref2; 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(_ref3 => { let { list, groupName } = _ref3; const newList = searchList({ items: list, searchString: value }); if (newList.length > 0) { newMatchingItems.push({ groupName, list: newList }); } }); if (newMatchingItems.length === 0 && shouldAddInputToList) { newMatchingItems.push({ groupName: undefined, list: [] }); } const filteredMatchingListItems = newMatchingItems.map(_ref4 => { let { list, groupName } = _ref4; return { groupName, list: list.filter(item => { if (typeof customFilter === 'function') { return customFilter(item); } return !(newMatchingItems.length === 1 && item.text === value); }) }; }); setMatchingListsItems(filteredMatchingListItems); return newLists; }, [groups, lists, customFilter, shouldAddInputToList, value]); const handleOpen = useCallback(() => { if (boxRef.current && newContainer) { const { left: comboBoxLeft, top: comboBoxTop, height: bodyHeight } = boxRef.current.getBoundingClientRect(); const { left, top } = newContainer.getBoundingClientRect(); const x = comboBoxLeft - left + newContainer.scrollLeft; const y = comboBoxTop - top + newContainer.scrollTop; setInternalCoordinates({ x, y: y + bodyHeight }); setIsAnimating(true); } }, [newContainer]); const handleClose = useCallback(() => { setIsAnimating(false); }, []); const handleFilterButtonsGroupSelect = keys => { setGroups(keys.length === 0 ? ['all'] : keys); }; /** * This function closes the list of items */ const handleOutsideClick = useCallback(event => { if (boxRef.current && !boxRef.current.contains(event.target) && contentRef.current && !contentRef.current.contains(event.target)) { handleClose(); } }, [handleClose]); /** * This hook listens for clicks */ useEffect(() => { document.addEventListener('click', handleOutsideClick); window.addEventListener('blur', () => handleClose()); return () => { document.removeEventListener('click', handleOutsideClick); window.addEventListener('blur', () => handleClose()); }; }, [handleOutsideClick, boxRef, handleClose]); /** * This hook calculates the height */ useEffect(() => { const textArray = []; activeList.forEach(_ref5 => { let { list, groupName } = _ref5; list.forEach(_ref6 => { let { text } = _ref6; return textArray.push(text); }); if (!groupName) { return; } textArray.push(groupName); }); if (shouldAddInputToList && inputToListValue !== '') { textArray.push(inputToListValue); } setHeight(calculateContentHeight(textArray)); }, [inputToListValue, activeList, placeholder, shouldAddInputToList]); /** * This hook calculates the width */ useEffect(() => { const input = document.getElementById(`search_box_input${uuid}`); const getInputWidth = () => { if (input) { setWidth(input.offsetWidth); } }; if (input) { new ResizeObserver(getInputWidth).observe(input); } }, [uuid]); useEffect(() => { if (selectedId) { activeList.forEach(_ref7 => { let { list } = _ref7; const selectedItem = list.find(_ref8 => { let { id } = _ref8; return 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 = isAnimating; }, [isAnimating]); useEffect(() => { if (matchingListsItems.length !== 0 && !isAnimatingRef.current && hasFocusRef.current) { handleOpen(); } }, [handleOpen, matchingListsItems.length]); /** * This function sets the items on focus if shouldShowContentOnEmptyInput */ const handleFocus = useCallback(() => { hasFocusRef.current = true; if (shouldShowContentOnEmptyInput) { const newMatchingItems = []; activeList.forEach(_ref9 => { let { list, groupName } = _ref9; const newList = searchList({ items: list, searchString: value }); if (newList.length > 0) { newMatchingItems.push({ groupName, list: newList }); } }); if (newMatchingItems.length === 0 && shouldAddInputToList) { newMatchingItems.push({ groupName: undefined, list: [] }); } const filteredMatchingListItems = newMatchingItems.map(_ref0 => { let { list, groupName } = _ref0; return { groupName, list: list.filter(item => { if (typeof customFilter === 'function') { return customFilter(item); } return !(newMatchingItems.length === 1 && item.text === value); }) }; }); setMatchingListsItems(filteredMatchingListItems); if (filteredMatchingListItems.length !== 0) { handleOpen(); } } }, [activeList, handleOpen, customFilter, shouldAddInputToList, shouldShowContentOnEmptyInput, value]); /** * This function filters the lists by input */ useEffect(() => { const newMatchingItems = []; activeList.forEach(_ref1 => { let { list, groupName } = _ref1; const newList = searchList({ items: list, searchString: value }); if (newList.length > 0) { newMatchingItems.push({ groupName, list: newList }); } }); if (newMatchingItems.length === 0 && shouldAddInputToList) { newMatchingItems.push({ groupName: undefined, list: [] }); } if (shouldAddInputToList && inputToListValue !== '') { newMatchingItems.forEach(_ref10 => { let { list } = _ref10; list.forEach(_ref11 => { let { text } = _ref11; if (text.toLowerCase() === inputToListValue.toLowerCase()) { setInputToListValue(''); } }); }); } }, [inputToListValue, activeList, shouldAddInputToList, shouldShowContentOnEmptyInput, value]); const handleClick = useCallback(() => { if (isAnimating) { handleClose(); } else { handleOpen(); } }, [handleClose, handleOpen, isAnimating]); 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(() => /*#__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(_ref12 => { let { list, groupName } = _ref12; const newList = searchList({ items: list, searchString: event.target.value }); 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); } }, [activeList, handleOpen, onChange, shouldAddInputToList, shouldShowContentOnEmptyInput]); /** * This function handles the blur event of the input */ const handleBlur = useCallback(event => { hasFocusRef.current = false; if (typeof onBlur === 'function') { onBlur(event); } }, [onBlur]); /** * This function handles the item selection */ const handleSelect = useCallback(item => { const newItem = { ...item, text: item.text.replace('<b>', '').replace('</b>', '').replace('</b', '') }; 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]); const content = useMemo(() => { const items = []; matchingListsItems.forEach((_ref13, index) => { let { groupName, list } = _ref13; if (hasMultipleGroups) { if (list.length <= 0) { return; } if (index !== 0) { items.push(/*#__PURE__*/React.createElement(GroupName, { key: groupName, name: groupName ?? '' })); } } list.forEach(_ref14 => { let { id, text, imageUrl } = _ref14; items.push(/*#__PURE__*/React.createElement(SearchBoxItem, { key: `${id}_${groupName ?? ''}`, id: id, text: text, imageUrl: imageUrl, shouldShowRoundImage: shouldShowRoundImage, onSelect: handleSelect, groupName: groupName })); }); }); if (shouldAddInputToList && inputToListValue !== '') { items.push(/*#__PURE__*/React.createElement(SearchBoxItem, { id: "input-value", onSelect: handleSelect, text: `<b>${inputToListValue}</b` })); } return items; }, [matchingListsItems, shouldAddInputToList, inputToListValue, hasMultipleGroups, shouldShowRoundImage, handleSelect]); useEffect(() => { const handleKeyDown = e => { if (!isAnimating || matchingListsItems.length === 0) { return; } if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); const children = contentRef.current?.children; if (!children) { return; } const childrenArray = Array.from(children); const newChildren = childrenArray.find(child => child.id.startsWith('searchbox-content__'))?.children; if (newChildren && newChildren.length > 0) { const filteredChildren = Array.from(newChildren).filter(child => child.dataset.isgroupname !== 'true'); setFilteredChildrenArray(filteredChildren); 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; newElement.focus(); } } else if (e.key === 'Enter' && focusedIndex !== null) { if (filteredChildrenArray) { const element = filteredChildrenArray[focusedIndex]; 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; } handleSelect({ id: id.replace('search-box-item__', ''), text: textContent ?? '', imageUrl }); } } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [filteredChildrenArray, focusedIndex, handleSelect, isAnimating, matchingListsItems.length]); const handleKeyPress = useCallback(event => { if (event.keyCode === 27) { setMatchingListsItems([]); } }, []); useImperativeHandle(ref, () => ({ clear: () => 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]); useEffect(() => { if (!newContainer) { return; } setPortal(() => /*#__PURE__*/createPortal(/*#__PURE__*/React.createElement(AnimatePresence, { initial: false, key: `search-box-body-animation-wrapper-${uuid}` }, isAnimating && matchingListsItems.length !== 0 && (value.trim() !== '' || shouldShowContentOnEmptyInput) && /*#__PURE__*/React.createElement(SearchBoxBody, { key: `search-box-body-${uuid}`, filterButtons: filterButtons, selectedGroups: groups, width: width, coordinates: internalCoordinates, browser: browser?.name, height: height, ref: contentRef, onGroupSelect: handleFilterButtonsGroupSelect, shouldHideFilterButtons: shouldHideFilterButtons }, content)), newContainer)); }, [browser?.name, newContainer, content, filterButtons, groups, height, internalCoordinates, isAnimating, width, shouldHideFilterButtons, matchingListsItems.length, value, shouldShowContentOnEmptyInput, uuid]); return useMemo(() => /*#__PURE__*/React.createElement(StyledSearchBox, { ref: boxRef, key: `search-box-${uuid}` }, /*#__PURE__*/React.createElement("div", { id: `search_box_input${uuid}` }, /*#__PURE__*/React.createElement(Input, { isInvalid: isInvalid, ref: inputRef, onChange: handleChange, onBlur: handleBlur, onFocus: handleFocus, placeholder: placeholder, onKeyDown: onKeyDown, leftElement: leftElement, rightElement: rightElement, value: value })), portal), [handleBlur, handleChange, handleFocus, isInvalid, leftElement, onKeyDown, placeholder, portal, rightElement, uuid, value]); }); SearchBox.displayName = 'SearchBox'; export default SearchBox; //# sourceMappingURL=SearchBox.js.map