@coreui/react-pro
Version:
UI Components Library for React.js
300 lines (297 loc) • 16 kB
JavaScript
import { __rest } from '../../node_modules/tslib/tslib.es6.js';
import React, { forwardRef, useRef, useId, useState, useMemo, useEffect, useCallback } from 'react';
import classNames from '../../_virtual/index.js';
import PropTypes from 'prop-types';
import { CConditionalPortal } from '../conditional-portal/CConditionalPortal.js';
import { CFormControlWrapper } from '../form/CFormControlWrapper.js';
import { CAutocompleteOptions } from './CAutocompleteOptions.js';
import { usePopper } from '../../hooks/usePopper.js';
import isRTL from '../../utils/isRTL.js';
import { useForkedRef } from '../../hooks/useForkedRef.js';
import { filterOptions, isExternalSearch, getFirstOptionByValue, flattenOptionsArray, getOptionLabel, isGlobalSearch, getFirstOptionByLabel } from './utils.js';
const CAutocomplete = forwardRef((_a, ref) => {
var { allowOnlyDefinedOptions = false, className, cleaner = false, clearSearchOnSelect = true, container, disabled, feedback, feedbackInvalid, feedbackValid, highlightOptionsOnSearch = false, id, indicator, invalid, label, loading, name, onChange, onHide, onInput, onShow, options, optionsMaxHeight = 'auto', optionsTemplate, optionsGroupsTemplate, placeholder, portal = false, readOnly, required, resetSelectionOnOptionsChange = false, search, searchNoResultsLabel = false, showHints = false, size, text, tooltipFeedback, valid, value, virtualScroller, visible, visibleItems = 10 } = _a, rest = __rest(_a, ["allowOnlyDefinedOptions", "className", "cleaner", "clearSearchOnSelect", "container", "disabled", "feedback", "feedbackInvalid", "feedbackValid", "highlightOptionsOnSearch", "id", "indicator", "invalid", "label", "loading", "name", "onChange", "onHide", "onInput", "onShow", "options", "optionsMaxHeight", "optionsTemplate", "optionsGroupsTemplate", "placeholder", "portal", "readOnly", "required", "resetSelectionOnOptionsChange", "search", "searchNoResultsLabel", "showHints", "size", "text", "tooltipFeedback", "valid", "value", "virtualScroller", "visible", "visibleItems"]);
const autoCompleteRef = useRef(null);
const autoCompleteForkedRef = useForkedRef(ref, autoCompleteRef);
const dropdownRef = useRef(null);
const togglerRef = useRef(null);
const inputRef = useRef(null);
const inputHintRef = useRef(null);
const uniqueId = useId();
const { initPopper, destroyPopper } = usePopper();
const [_visible, setVisible] = useState(visible);
const [hint, setHint] = useState();
const [searchValue, setSearchValue] = useState('');
const [selected, setSelected] = useState(null);
const filteredOptions = useMemo(() => (isExternalSearch(search) ? options : filterOptions(options, searchValue)), [options, searchValue, search]);
const popperConfig = useMemo(() => ({
placement: (isRTL(autoCompleteRef.current) ? 'bottom-end' : 'bottom-start'),
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'clippingParents',
},
},
{
name: 'offset',
options: {
offset: [0, 2],
},
},
],
}), [autoCompleteRef.current]);
useEffect(() => {
if (resetSelectionOnOptionsChange) {
handleClear();
}
}, [options]);
useEffect(() => {
if (value && typeof value === 'string') {
handleSelect(value);
return;
}
if (value && typeof value === 'number') {
const foundOption = getFirstOptionByValue(value, options);
if (foundOption) {
handleSelect(foundOption);
}
return;
}
const _selected = flattenOptionsArray(filteredOptions).find((option) => typeof option !== 'string' && option.selected === true);
if (_selected) {
handleSelect(_selected);
}
}, [options, value]);
useEffect(() => {
if (!showHints) {
return;
}
const findOption = searchValue.length > 0
? filteredOptions.find((option) => getOptionLabel(option).toLowerCase().startsWith(searchValue.toLowerCase()))
: undefined;
setHint(findOption);
}, [filteredOptions, searchValue, showHints]);
useEffect(() => {
if (!searchNoResultsLabel &&
searchValue.length > 0 &&
filteredOptions.length === 0 &&
_visible) {
handleDropdownHide();
return;
}
if (searchValue.length > 0 && filteredOptions.length > 0 && !_visible) {
handleDropdownShow();
}
}, [filteredOptions]);
useEffect(() => {
if (visible === true) {
handleDropdownShow();
}
else if (visible === false) {
handleDropdownHide();
}
}, [visible]);
const handleClear = () => {
if (inputRef.current) {
inputRef.current.value = '';
}
setSearchValue('');
setSelected(null);
onChange === null || onChange === void 0 ? void 0 : onChange(null);
};
const handleGlobalSearch = (event) => {
if (isGlobalSearch(search) &&
inputRef.current &&
(event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete')) {
inputRef.current.focus();
}
};
const handleInputChange = (event) => {
const value = event.target.value;
handleSearch(value);
if (selected !== null) {
onChange === null || onChange === void 0 ? void 0 : onChange(null);
setSelected(null);
}
};
const handleInputKeyDown = (event) => {
var _a, _b, _c, _d;
if (event.key === 'Escape') {
handleDropdownHide();
return;
}
if ((event.key === 'Down' || event.key === 'ArrowDown') &&
((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.value.length) === ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.selectionStart)) {
event.preventDefault();
handleDropdownShow();
const target = event.target;
const firstOption = (_d = (_c = target.parentElement) === null || _c === void 0 ? void 0 : _c.parentElement) === null || _d === void 0 ? void 0 : _d.querySelectorAll('.autocomplete-option')[0];
if (firstOption) {
firstOption.focus();
}
return;
}
if (showHints && hint && event.key === 'Tab') {
event.preventDefault();
handleSelect(hint);
handleDropdownHide();
return;
}
if (event.key === 'Enter') {
const input = event.target;
const foundOptions = getFirstOptionByLabel(input.value, filteredOptions);
if (foundOptions) {
handleSelect(foundOptions);
}
else {
if (!allowOnlyDefinedOptions) {
handleSelect(input.value);
}
}
handleDropdownHide();
return;
}
if (event.key === 'Backspace' || event.key === 'Delete') {
if (selected !== null) {
setSelected(null);
onChange === null || onChange === void 0 ? void 0 : onChange(null);
}
return;
}
};
const handleKeyUp = useCallback((event) => {
if (event.key === 'Escape') {
handleDropdownHide();
}
if (autoCompleteRef.current &&
!autoCompleteRef.current.contains(event.target)) {
handleDropdownHide();
}
}, []);
const handleMouseUp = useCallback((event) => {
if (autoCompleteRef.current &&
autoCompleteRef.current.contains(event.target)) {
return;
}
handleDropdownHide();
}, []);
const handleOptionClick = (option) => {
handleSelect(option);
handleDropdownHide();
};
const handleSearch = (search) => {
onInput === null || onInput === void 0 ? void 0 : onInput(search);
setSearchValue(search);
};
const handleSelect = (option) => {
if (option && typeof option === 'object' && option.disabled) {
return;
}
if (inputRef.current) {
inputRef.current.value = option ? getOptionLabel(option) : '';
}
if (clearSearchOnSelect) {
handleSearch('');
}
else {
setHint('');
}
setSelected(option !== null && option !== void 0 ? option : null);
onChange === null || onChange === void 0 ? void 0 : onChange(option !== null && option !== void 0 ? option : null);
};
const handleDropdownShow = useCallback(() => {
var _a;
if (disabled || readOnly || _visible) {
return;
}
if (!isExternalSearch(search) &&
filteredOptions.length === 0 &&
searchNoResultsLabel === false) {
return;
}
if (portal && dropdownRef.current && togglerRef.current) {
dropdownRef.current.style.minWidth = `${togglerRef.current.offsetWidth}px`;
}
setVisible(true);
onShow === null || onShow === void 0 ? void 0 : onShow();
window.addEventListener('mouseup', handleMouseUp);
window.addEventListener('keyup', handleKeyUp);
if (togglerRef.current && dropdownRef.current) {
setTimeout(() => {
initPopper(togglerRef.current, dropdownRef.current, popperConfig);
}, 1); // Allow DOM updates to complete before initializing Popper
}
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
}, [
disabled,
readOnly,
_visible,
filteredOptions.length,
searchNoResultsLabel,
allowOnlyDefinedOptions,
onShow,
handleMouseUp,
handleKeyUp,
initPopper,
popperConfig,
]);
const handleDropdownHide = useCallback(() => {
var _a;
setVisible(false);
onHide === null || onHide === void 0 ? void 0 : onHide();
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('keyup', handleKeyUp);
destroyPopper();
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
}, [onHide, handleMouseUp, handleKeyUp, destroyPopper]);
return (React.createElement(CFormControlWrapper, { describedby: rest['aria-describedby'], feedback: feedback, feedbackInvalid: feedbackInvalid, feedbackValid: feedbackValid, id: id || `autocomplete-${uniqueId}`, invalid: invalid, label: label, text: text, tooltipFeedback: tooltipFeedback, valid: valid },
React.createElement("div", { className: classNames('autocomplete', {
[`autocomplete-${size}`]: size,
disabled,
'is-invalid': invalid,
'is-valid': valid,
show: _visible,
}, className), onKeyDown: handleGlobalSearch, ref: autoCompleteForkedRef },
React.createElement("div", { className: "autocomplete-input-group", onClick: () => handleDropdownShow(), ref: togglerRef },
showHints && searchValue !== '' && (React.createElement("input", { className: "autocomplete-input autocomplete-input-hint", id: id || `autocomplete-hint-${uniqueId}`, autoComplete: "off", readOnly: true, tabIndex: -1, "aria-hidden": "true", value: hint ? `${searchValue}${getOptionLabel(hint).slice(searchValue.length)}` : '', ref: inputHintRef })),
React.createElement("input", Object.assign({ type: "text", className: "autocomplete-input", disabled: disabled, id: id || `autocomplete-${uniqueId}`, name: name || `autocomplete-${uniqueId}`, onBlur: (event) => {
event.preventDefault();
event.stopPropagation();
if (allowOnlyDefinedOptions && selected === null && filteredOptions.length === 0) {
handleClear();
}
}, onChange: handleInputChange, onKeyDown: handleInputKeyDown, placeholder: placeholder, autoComplete: "off", required: required, "aria-autocomplete": "list", "aria-expanded": _visible, "aria-haspopup": "listbox" }, (portal && { 'aria-owns': `autocomplete-listbox-${uniqueId}` }), { readOnly: readOnly, role: "combobox", ref: inputRef })),
(cleaner || indicator) && (React.createElement("div", { className: "autocomplete-buttons" },
!disabled && !readOnly && cleaner && selected && (React.createElement("button", { type: "button", className: "autocomplete-cleaner", onClick: (event) => {
event.preventDefault();
event.stopPropagation();
handleClear();
} })),
indicator && (React.createElement("button", { type: "button", className: "autocomplete-indicator", disabled: !(searchNoResultsLabel || filteredOptions.length > 0) &&
isExternalSearch(search), onClick: (event) => {
event.preventDefault();
event.stopPropagation();
if (_visible) {
handleDropdownHide();
}
else {
handleDropdownShow();
}
} }))))),
React.createElement(CConditionalPortal, { container: container, portal: portal },
React.createElement("div", { className: classNames('autocomplete-dropdown', {
show: portal && _visible,
}), id: `autocomplete-listbox-${uniqueId}`, role: "listbox", "aria-labelledby": id || `autocomplete-${uniqueId}`, ref: dropdownRef },
React.createElement(CAutocompleteOptions, { highlightOptionsOnSearch: highlightOptionsOnSearch, loading: loading, onOptionClick: (option) => !disabled && !readOnly && handleOptionClick(option), options: filteredOptions, optionsMaxHeight: optionsMaxHeight, optionsTemplate: optionsTemplate, optionsGroupsTemplate: optionsGroupsTemplate, searchNoResultsLabel: searchNoResultsLabel, searchValue: searchValue, selected: selected, virtualScroller: virtualScroller, visibleItems: visibleItems }))))));
});
CAutocomplete.propTypes = Object.assign({ allowOnlyDefinedOptions: PropTypes.bool, className: PropTypes.string, clearSearchOnSelect: PropTypes.bool, cleaner: PropTypes.bool, container: PropTypes.any, disabled: PropTypes.bool, highlightOptionsOnSearch: PropTypes.bool, indicator: PropTypes.bool, loading: PropTypes.bool, name: PropTypes.string, onChange: PropTypes.func, onHide: PropTypes.func, onInput: PropTypes.func, onShow: PropTypes.func, options: PropTypes.array.isRequired, optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), optionsTemplate: PropTypes.func, optionsGroupsTemplate: PropTypes.func, placeholder: PropTypes.string, portal: PropTypes.bool, required: PropTypes.bool, resetSelectionOnOptionsChange: PropTypes.bool, search: PropTypes.oneOfType([
PropTypes.oneOf(['external', 'global']),
PropTypes.shape({
external: PropTypes.bool.isRequired,
global: PropTypes.bool.isRequired,
}),
]), searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), showHints: PropTypes.bool, size: PropTypes.oneOf(['sm', 'lg']), value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), virtualScroller: PropTypes.bool, visible: PropTypes.bool, visibleItems: PropTypes.number }, CFormControlWrapper.propTypes);
CAutocomplete.displayName = 'CAutocomplete';
export { CAutocomplete };
//# sourceMappingURL=CAutocomplete.js.map