UNPKG

chayns-components

Version:

A set of beautiful React components for developing chayns® applications.

388 lines (385 loc) 14.3 kB
import _extends from "@babel/runtime/helpers/extends"; /** * @component */ import classNames from 'clsx'; import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import InputBox from '../../react-chayns-input_box/component/InputBox'; import ResultSelection from './result-selection/ResultSelection'; import "./SearchBox.css"; import { isNumber, isString } from '../../utils/is'; /** * An autocomplete input to search through a list of entries. */ const SearchBox = _ref => { let { list, disabled = false, listValue = 'value', listKey = 'key', sortKey, defaultValue, onSelect, value: valueProp, stopPropagation = false, showListWithoutInput = false, inputValue: inputValueProp = null, inputDefaultValue = null, onChange, className, autoSelectFirst = false, highlightInputInResult = true, addInputToList = false, hasOpenCloseIcon = false, emptyKey, onBlur, ...otherProps } = _ref; const getValue = useCallback(stringOrObjectOrNumber => { if (isString(stringOrObjectOrNumber) || isNumber(stringOrObjectOrNumber) || !stringOrObjectOrNumber) { return stringOrObjectOrNumber; } return stringOrObjectOrNumber[listValue]; }, [listValue]); const getSortValue = useCallback(stringOrObjectOrNumber => { if (isString(stringOrObjectOrNumber) || isNumber(stringOrObjectOrNumber) || !stringOrObjectOrNumber) { return stringOrObjectOrNumber; } return stringOrObjectOrNumber[sortKey !== null && sortKey !== void 0 ? sortKey : listValue]; }, [sortKey, listValue]); const getKey = useCallback(stringOrObjectOrNumber => { if (isString(stringOrObjectOrNumber) || isNumber(stringOrObjectOrNumber)) { return stringOrObjectOrNumber; } if (!listKey || !stringOrObjectOrNumber) { return null; } const key = stringOrObjectOrNumber[listKey]; if (!key && addInputToList) { return stringOrObjectOrNumber[listValue]; } return key; }, [listKey, addInputToList, listValue]); const getItemByKey = useCallback(key => { let defaultReturnValue = {}; if (addInputToList) { defaultReturnValue = { [listValue]: key }; } if (list.length > 0) { if (isString(list[0])) { if (addInputToList) { defaultReturnValue = key; } else { defaultReturnValue = ''; } } else if (isNumber(list[0])) { if (addInputToList) { defaultReturnValue = Number(key); } else { defaultReturnValue = 0; } } } if (key === null || key === undefined) { return defaultReturnValue; } const res = list.find(item => String(getKey(item)) === String(key) || item === key); return res === undefined ? defaultReturnValue : res; }, [addInputToList, list, listValue, getKey]); const isItemExisting = useCallback(value => { if (!value && value !== 0) { return false; } return !!list.find(item => String(getValue(item)) === String(value) || String(item) === String(value)); }, [getValue, list]); const [valueState, setValueState] = useState(defaultValue); const value = valueProp !== null ? valueProp : valueState; const [inputValueState, setInputValueState] = useState((inputDefaultValue !== null ? inputDefaultValue : getValue(getItemByKey(value))) || ''); useEffect(() => { setInputValueState((inputDefaultValue !== null ? inputDefaultValue : getValue(getItemByKey(value))) || ''); // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, list]); const inputValue = inputValueProp !== null ? inputValueProp : inputValueState; const [focusIndex, setFocusIndex] = useState(autoSelectFirst ? 0 : null); const inputBoxRef = useRef(null); const inputRef = useRef(null); const [filteredList, setFilteredList] = useState([]); const inputOnChange = useCallback(input => { if (onChange) onChange(input); setInputValueState(input); }, [setInputValueState, onChange]); const onItemClick = useCallback((e, item) => { var _getKey, _inputRef$current$ref; const selection = (_getKey = getKey(item)) !== null && _getKey !== void 0 ? _getKey : e === null || e === void 0 ? void 0 : e.target.id; setValueState(selection); const itemValue = getValue(getItemByKey(selection)); let newInputValueState; if (addInputToList && !itemValue) { if (list.length >= 0 && isNumber(list[0])) { newInputValueState = Number(selection); } else { newInputValueState = selection; } } else { newInputValueState = itemValue; } setInputValueState(newInputValueState); if (onSelect && list && list.length > 0 && selection !== null && selection !== undefined) { onSelect(getItemByKey(selection)); } if (stopPropagation) e === null || e === void 0 ? void 0 : e.stopPropagation(); if (inputBoxRef.current) inputBoxRef.current.blur(); if (inputRef.current) (_inputRef$current$ref = inputRef.current.ref) === null || _inputRef$current$ref === void 0 ? void 0 : _inputRef$current$ref.blur(); }, [getKey, getValue, getItemByKey, addInputToList, onSelect, list, stopPropagation]); const handleKeyDown = useCallback(ev => { if (!filteredList) return; switch (ev.keyCode) { case 40: // Arrow down ev.preventDefault(); if (focusIndex === null) { setFocusIndex(0); } else if (focusIndex >= filteredList.length - 1) { setFocusIndex(filteredList.length - 1); } else { setFocusIndex(focusIndex + 1); } break; case 38: // Arrow up ev.preventDefault(); if (focusIndex === null || focusIndex <= 0) { setFocusIndex(0); } else { setFocusIndex(focusIndex - 1); } break; case 13: // Enter if (focusIndex !== null && filteredList[focusIndex]) { onItemClick(ev, filteredList[focusIndex]); inputRef.current.ref.blur(); setFocusIndex(null); } else if (filteredList.length === 1) { onItemClick(ev, filteredList[0]); inputRef.current.ref.blur(); setFocusIndex(null); } break; case 9: // Tabulator if (filteredList.length === 1) { onItemClick(ev, filteredList[0]); inputRef.current.ref.blur(); setFocusIndex(null); } break; case 27: // Escape inputRef.current.ref.blur(); if (inputBoxRef.current) inputBoxRef.current.blur(); setFocusIndex(null); break; default: break; } }, [filteredList, focusIndex, onItemClick]); useEffect(() => { const inputValueString = Number.isNaN(inputValue) ? '' : String(inputValue); const returnList = list === null || list === void 0 ? void 0 : list.filter(item => String(getValue(item)).toLowerCase().indexOf(inputValueString.toLowerCase()) >= 0 && (showListWithoutInput || inputValue)).sort((a, b) => { let aValue = getSortValue(a); let bValue = getSortValue(b); aValue = isString(aValue) ? aValue.toLowerCase() : aValue; bValue = isString(bValue) ? bValue.toLowerCase() : bValue; const aStartsWith = String(aValue).startsWith(inputValueString.toLowerCase()); const bStartsWith = String(bValue).startsWith(inputValueString.toLowerCase()); if (aStartsWith && !bStartsWith) return -1; if (!aStartsWith && bStartsWith) return 1; if (isString(aValue) || isString(bValue)) return aValue.localeCompare(bValue); return aValue - bValue; }); if (addInputToList && !isItemExisting(inputValue) && list.length > 0 && inputValueString) { if (isString(list[0])) { returnList.push(inputValue); } else if (isNumber(list[0])) { returnList.push(Number(inputValue)); } else { returnList.push({ [listValue]: inputValue }); } } setFilteredList(returnList); }, [inputValue, addInputToList, list, isItemExisting, getValue, getSortValue, showListWithoutInput, listValue]); useEffect(() => { let index = filteredList.findIndex(item => !(!inputValue && emptyKey) && value === getKey(item) || !inputValue && emptyKey === getKey(item)); if (index < 0) { index = null; } setFocusIndex(index || (autoSelectFirst ? 0 : null)); }, [autoSelectFirst, emptyKey, filteredList, getKey, inputValue, value]); useEffect(() => { const item = filteredList[focusIndex]; const elem = document.getElementById(`${getKey(item)}`); if (elem) { if (typeof elem.scrollIntoViewIfNeeded === 'function') { elem.scrollIntoViewIfNeeded(false); } else if (typeof elem.scrollIntoView === 'function') { elem.scrollIntoView({ behavior: 'smooth' }); } } }, [filteredList, focusIndex, getKey]); return /*#__PURE__*/React.createElement(InputBox, _extends({ value: inputValue, defaultValue: !inputValue && inputDefaultValue ? inputDefaultValue : undefined, onChange: inputOnChange, customProps: { autoComplete: 'off' }, type: list.length >= 0 && isNumber(list[0]) ? 'number' : 'text', onBlur: () => { // return filtered list on onBlur event if (typeof onBlur === 'function') { onBlur(filteredList); } if (addInputToList) { onItemClick(null, inputValue); } else if (filteredList.length === 1) { // select only matching item onItemClick(null, filteredList[0]); } else { // select exact match (ignore case) const item = list.find(i => { var _i$listValue; return ((_i$listValue = i[listValue]) === null || _i$listValue === void 0 ? void 0 : _i$listValue.toLowerCase()) === (inputValue === null || inputValue === void 0 ? void 0 : inputValue.toLowerCase()); }); if (item) { onItemClick(null, item); } else { var _inputRef$current, _inputRef$current$ref2; (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : (_inputRef$current$ref2 = _inputRef$current.ref) === null || _inputRef$current$ref2 === void 0 ? void 0 : _inputRef$current$ref2.blur(); } } } }, otherProps, { hasOpenCloseIcon: hasOpenCloseIcon, ref: inputBoxRef, disabled: disabled, className: classNames(className, disabled && 'cc__search-box--disabled'), onKeyDown: handleKeyDown, inputRef: ref => { inputRef.current = ref; }, emptyValue: getValue(getItemByKey(emptyKey)) }), filteredList && filteredList.length > 0 && filteredList.map((item, index) => /*#__PURE__*/React.createElement("div", { key: getKey(item), id: getKey(item), className: 'cc__search-box__item ellipsis' + (!(!inputValue && emptyKey) && value === getKey(item) || index === focusIndex || !inputValue && emptyKey === getKey(item) ? " cc__search-box__item--selected" : ""), onClick: onItemClick }, highlightInputInResult && inputValue ? /*#__PURE__*/React.createElement(ResultSelection, { text: getValue(item), search: inputValue }) : getValue(item)))); }; SearchBox.propTypes = { /** * A callback that will be invoked when a value was selected. */ onSelect: PropTypes.func, /** * Disables any user interaction and renders the search box in a disabled * style. */ disabled: PropTypes.bool, /** * An array of items to select from. */ list: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]), /** * The property name of a unique identifier in the `list` items. */ listKey: PropTypes.string, /** * The property name of the name of the `list` items that will be shown in * the dropdown. */ listValue: PropTypes.string, /** * The property name to use for sorting the list. Default is listValue */ sortKey: PropTypes.string, /** * A classname string that will be set on the container component. */ className: PropTypes.string, /** * The default value of the search box as a key to one of the list items. */ defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Wether to stop propagation of click events to parent elements. */ stopPropagation: PropTypes.bool, /** * A DOM element into which the overlay will be rendered. */ parent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), /** * A React style object that will be applied to the outer-most container. */ style: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), /** * The current value of the search box as a key to one of the list items. */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * The current value of the text input. */ inputValue: PropTypes.string, /** * Wether the list should be shown if there is no user input. */ showListWithoutInput: PropTypes.bool, /** * The default value of the input field. Has no effect when used with the * `inputValue`-prop. */ inputDefaultValue: PropTypes.string, /** * The `onChange`-callback for the input element. */ onChange: PropTypes.func, /** * Wether the first list item should be automatically selected. */ autoSelectFirst: PropTypes.bool, /** * Whether the search term should be marked in the selection */ highlightInputInResult: PropTypes.bool, /** * Whether the input value should be added to the end of the result list. * Allows also values which are not in the list. */ addInputToList: PropTypes.bool, /** * The key of the default value if nothing is selected or typed into the input. */ emptyKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Whether the input should have a small icon to open and close the result list. */ hasOpenCloseIcon: PropTypes.bool, /** * A callback that will be invoked when the user leaves the input. */ onBlur: PropTypes.func }; SearchBox.displayName = 'SearchBox'; export default SearchBox; //# sourceMappingURL=SearchBox.js.map