@chayns-components/core
Version:
A set of beautiful React components for developing your own applications with chayns.
658 lines (643 loc) • 20 kB
JavaScript
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