chayns-components
Version:
A set of beautiful React components for developing chayns® applications.
388 lines (385 loc) • 14.3 kB
JavaScript
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,
inputDefaultValue,
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