UNPKG

@coreui/react-pro

Version:

UI Components Library for React.js

245 lines (242 loc) 16.3 kB
import { __rest } from '../../node_modules/tslib/tslib.es6.js'; import React, { forwardRef, useRef, useId, useState, useMemo, useEffect } from 'react'; import classNames from '../../_virtual/index.js'; import PropTypes from 'prop-types'; import { CFormControlWrapper } from '../form/CFormControlWrapper.js'; import { CConditionalPortal } from '../conditional-portal/CConditionalPortal.js'; import { CMultiSelectNativeSelect } from './CMultiSelectNativeSelect.js'; import { CMultiSelectOptions } from './CMultiSelectOptions.js'; import { CMultiSelectSelection } from './CMultiSelectSelection.js'; import { useDropdownWithPopper } from '../../hooks/useDropdownWithPopper.js'; import '@popperjs/core'; import getNextActiveElement from '../../utils/getNextActiveElement.js'; import { flattenOptionsArray, isExternalSearch, filterOptionsList, createOption, selectOptions, isGlobalSearch, getOptionsList } from './utils.js'; const CMultiSelect = forwardRef((_a, ref) => { var { allowCreateOptions, ariaCleanerLabel = 'Clear all selections', ariaIndicatorLabel = 'Toggle visibility of options menu', className, cleaner = true, clearSearchOnSelect, container, disabled, feedback, feedbackInvalid, feedbackValid, id, invalid, label, loading, multiple = true, name, onChange, onFilterChange, onHide, onShow, options, optionsMaxHeight = 'auto', optionsStyle = 'checkbox', optionsTemplate, optionsGroupsTemplate, placeholder = 'Select...', portal = false, required, resetSelectionOnOptionsChange = false, search = true, searchNoResultsLabel = 'No results found', selectAll = true, selectAllLabel = 'Select all options', selectionType = 'tags', selectionTypeCounterText = 'item(s) selected', size, text, tooltipFeedback, valid, value, virtualScroller, visible = false, visibleItems = 10 } = _a, rest = __rest(_a, ["allowCreateOptions", "ariaCleanerLabel", "ariaIndicatorLabel", "className", "cleaner", "clearSearchOnSelect", "container", "disabled", "feedback", "feedbackInvalid", "feedbackValid", "id", "invalid", "label", "loading", "multiple", "name", "onChange", "onFilterChange", "onHide", "onShow", "options", "optionsMaxHeight", "optionsStyle", "optionsTemplate", "optionsGroupsTemplate", "placeholder", "portal", "required", "resetSelectionOnOptionsChange", "search", "searchNoResultsLabel", "selectAll", "selectAllLabel", "selectionType", "selectionTypeCounterText", "size", "text", "tooltipFeedback", "valid", "value", "virtualScroller", "visible", "visibleItems"]); const { dropdownMenuElement, dropdownRefElement, isOpen, closeDropdown, openDropdown, toggleDropdown, updatePopper, } = useDropdownWithPopper(); const nativeSelectRef = useRef(null); const searchRef = useRef(null); const isInitialMount = useRef(true); const uniqueId = useId(); const [searchValue, setSearchValue] = useState(''); const [selected, setSelected] = useState([]); const [userOptions, setUserOptions] = useState([]); const filteredOptions = useMemo(() => flattenOptionsArray(isExternalSearch(search) ? [...options, ...filterOptionsList(searchValue, userOptions)] : filterOptionsList(searchValue, [...options, ...userOptions]), true), [options, searchValue, userOptions]); const flattenedOptions = useMemo(() => flattenOptionsArray(options).map((option) => { if (value && Array.isArray(value)) { return Object.assign(Object.assign({}, option), { selected: value.includes(option.value) }); } if (value === option.value) { return Object.assign(Object.assign({}, option), { selected: true }); } return option; }), [options, value]); const userOption = useMemo(() => { if (allowCreateOptions && filteredOptions.some((option) => option.label && option.label.toLowerCase() === searchValue.toLowerCase())) { return false; } return searchRef.current && createOption(String(searchValue), flattenedOptions); }, [filteredOptions, searchValue]); useEffect(() => { if (!isInitialMount.current && resetSelectionOnOptionsChange) { return setSelected([]); } const _selected = flattenedOptions.filter((option) => option.selected === true); const deselected = flattenedOptions.filter((option) => option.selected === false); if (_selected.length > 0 || deselected.length > 0) { setSelected(selectOptions(multiple, _selected, selected, deselected)); } }, [flattenedOptions]); useEffect(() => { if (!isInitialMount.current && onFilterChange) { onFilterChange(searchValue); } }, [searchValue]); useEffect(() => { if (!isInitialMount.current && nativeSelectRef.current) { nativeSelectRef.current.dispatchEvent(new Event('change', { bubbles: true })); } updatePopper(); }, [JSON.stringify(selected)]); useEffect(() => { visible ? openDropdown() : closeDropdown(); }, [visible]); useEffect(() => { var _a; if (isOpen) { if (onShow) onShow(); if (portal && dropdownMenuElement.current && dropdownRefElement.current) { dropdownMenuElement.current.style.minWidth = `${dropdownRefElement.current.offsetWidth}px`; } (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus(); } return () => { if (onHide) onHide(); setSearchValue(''); if (searchRef.current) { searchRef.current.value = ''; } }; }, [isOpen]); useEffect(() => { isInitialMount.current = false; }, []); const handleSearchChange = (event) => { setSearchValue(event.target.value); }; const handleSearchKeyDown = (event) => { if ((!isOpen && event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'ArrowDown') { openDropdown(); } if (event.key === 'ArrowDown' && dropdownMenuElement.current && searchRef.current && searchRef.current.value.length === searchRef.current.selectionStart) { event.preventDefault(); const items = getOptionsList(dropdownMenuElement.current); const target = event.target; getNextActiveElement(items, target, event.key === 'ArrowDown', !items.includes(target)).focus(); return; } if (event.key === 'Enter' && searchValue && allowCreateOptions) { event.preventDefault(); if (userOption) { setSelected([...selected, ...userOption]); setUserOptions([...userOptions, ...userOption]); } if (!userOption) { setSelected([ ...selected, filteredOptions.find((option) => String(option.label).toLowerCase() === searchValue.toLowerCase()), ]); } setSearchValue(''); if (searchRef.current) { searchRef.current.value = ''; } return; } if (searchValue.length > 0) { return; } if (event.key === 'Backspace' || event.key === 'Delete') { const last = selected.filter((option) => !option.disabled).pop(); if (last) { setSelected(selected.filter((option) => option.value !== last.value)); } } }; const handleTogglerKeyDown = (event) => { if (!isOpen && (event.key === 'Enter' || event.key === 'ArrowDown')) { event.preventDefault(); openDropdown(); return; } if (isOpen && dropdownMenuElement.current && event.key === 'ArrowDown') { event.preventDefault(); const items = getOptionsList(dropdownMenuElement.current); const target = event.target; getNextActiveElement(items, target, event.key === 'ArrowDown', !items.includes(target)).focus(); } }; const handleGlobalSearch = (event) => { if (isGlobalSearch(search) && searchRef.current && (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete')) { searchRef.current.focus(); } }; const handleOnOptionClick = (option) => { if (!multiple) { setSelected([option]); closeDropdown(); setSearchValue(''); if (searchRef.current) { searchRef.current.value = ''; } return; } if (option.custom && !userOptions.some((_option) => _option.value === option.value)) { setUserOptions([...userOptions, option]); } if (clearSearchOnSelect || option.custom) { setSearchValue(''); if (searchRef.current) { searchRef.current.value = ''; searchRef.current.focus(); } } if (selected.some((_option) => _option.value === option.value)) { setSelected(selected.filter((_option) => _option.value !== option.value)); } else { setSelected([...selected, option]); } }; const handleSelectAll = () => { setSelected(selectOptions(multiple, [...flattenedOptions.filter((option) => !option.disabled), ...userOptions], selected)); }; const handleDeselectAll = () => { setSelected(selected.filter((option) => option.disabled)); }; return (React.createElement(CFormControlWrapper, { describedby: rest['aria-describedby'], feedback: feedback, feedbackInvalid: feedbackInvalid, feedbackValid: feedbackValid, id: id !== null && id !== void 0 ? id : uniqueId, invalid: invalid, label: label, text: text, tooltipFeedback: tooltipFeedback, valid: valid }, React.createElement(CMultiSelectNativeSelect, { id: id !== null && id !== void 0 ? id : uniqueId, multiple: multiple, name: name !== null && name !== void 0 ? name : uniqueId, options: selected, required: required, value: multiple ? selected.map((option) => option.value.toString()) : selected.map((option) => option.value)[0], onChange: () => onChange && onChange(selected), ref: nativeSelectRef }), React.createElement("div", { className: classNames('form-multi-select', { [`form-multi-select-${size}`]: size, disabled, 'is-invalid': invalid, 'is-valid': valid, show: isOpen, }, className), onKeyDown: handleGlobalSearch, "aria-expanded": isOpen, ref: ref }, React.createElement("div", Object.assign({ className: "form-multi-select-input-group" }, (!search && !disabled && { tabIndex: 0 }), { onClick: () => !disabled && openDropdown(), onKeyDown: handleTogglerKeyDown, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen }, (portal && { 'aria-owns': `multiselect-listbox-${uniqueId}` }), { ref: dropdownRefElement }), React.createElement(CMultiSelectSelection, { disabled: disabled, multiple: multiple, onRemove: (option) => !disabled && handleOnOptionClick(option), placeholder: placeholder, search: search, selected: selected, selectionType: selectionType, selectionTypeCounterText: selectionTypeCounterText }, search && (React.createElement("input", Object.assign({ type: "text", className: "form-multi-select-search", disabled: disabled, id: `search${id !== null && id !== void 0 ? id : uniqueId}`, name: `search${name !== null && name !== void 0 ? name : uniqueId}`, autoComplete: "off", onChange: handleSearchChange, onKeyDown: handleSearchKeyDown }, (selected.length === 0 && { placeholder: placeholder }), (selected.length > 0 && selectionType === 'counter' && { placeholder: `${selected.length} ${selectionTypeCounterText}`, }), (selected.length > 0 && !multiple && { placeholder: selected.map((option) => option.label)[0] }), (multiple && selected.length > 0 && selectionType !== 'counter' && { size: searchValue.length + 2 }), { ref: searchRef }))), !search && selected.length === 0 && (React.createElement("span", { className: "form-multi-select-placeholder" }, placeholder))), React.createElement("div", { className: "form-multi-select-buttons" }, !disabled && cleaner && selected.length > 0 && (React.createElement("button", { type: "button", className: "form-multi-select-cleaner", onClick: () => handleDeselectAll(), "aria-label": ariaCleanerLabel })), React.createElement("button", Object.assign({ type: "button", className: "form-multi-select-indicator", onClick: (event) => { event.preventDefault(); event.stopPropagation(); if (!disabled) { toggleDropdown(); } } }, (disabled && { tabIndex: -1 }), { "aria-label": ariaIndicatorLabel })))), React.createElement(CConditionalPortal, { container: container, portal: portal }, React.createElement("div", { className: classNames('form-multi-select-dropdown', { show: portal && isOpen, }), id: `multiselect-listbox-${uniqueId}`, onKeyDown: handleGlobalSearch, role: "listbox", "aria-multiselectable": multiple, ref: dropdownMenuElement }, multiple && selectAll && (React.createElement("button", { type: "button", className: "form-multi-select-all", onClick: () => handleSelectAll() }, selectAllLabel)), React.createElement(CMultiSelectOptions, { loading: loading, onOptionOnClick: (option) => !disabled && handleOnOptionClick(option), options: filteredOptions.length === 0 && allowCreateOptions ? userOption || [] : filteredOptions, optionsMaxHeight: optionsMaxHeight, optionsStyle: optionsStyle, optionsTemplate: optionsTemplate, optionsGroupsTemplate: optionsGroupsTemplate, searchNoResultsLabel: searchNoResultsLabel, selected: selected, virtualScroller: virtualScroller, visibleItems: visibleItems })))))); }); CMultiSelect.propTypes = Object.assign({ allowCreateOptions: PropTypes.bool, ariaCleanerLabel: PropTypes.string, ariaIndicatorLabel: PropTypes.string, className: PropTypes.string, cleaner: PropTypes.bool, clearSearchOnSelect: PropTypes.bool, container: PropTypes.any, disabled: PropTypes.bool, loading: PropTypes.bool, multiple: PropTypes.bool, name: PropTypes.string, onChange: PropTypes.func, onFilterChange: PropTypes.func, onHide: PropTypes.func, onShow: PropTypes.func, options: PropTypes.array.isRequired, optionsMaxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), optionsStyle: PropTypes.oneOf(['checkbox', 'text']), optionsTemplate: PropTypes.func, optionsGroupsTemplate: PropTypes.func, placeholder: PropTypes.string, portal: PropTypes.bool, required: PropTypes.bool, resetSelectionOnOptionsChange: PropTypes.bool, search: PropTypes.oneOfType([ PropTypes.bool, PropTypes.oneOf(['external', 'global']), PropTypes.shape({ external: PropTypes.bool.isRequired, global: PropTypes.bool.isRequired, }), ]), searchNoResultsLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), selectAll: PropTypes.bool, selectAllLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), selectionType: PropTypes.oneOf(['counter', 'tags', 'text']), selectionTypeCounterText: PropTypes.string, size: PropTypes.oneOf(['sm', 'lg']), value: PropTypes.oneOfType([ PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string), ]), virtualScroller: PropTypes.bool, visible: PropTypes.bool, visibleItems: PropTypes.number }, CFormControlWrapper.propTypes); CMultiSelect.displayName = 'CMultiSelect'; export { CMultiSelect }; //# sourceMappingURL=CMultiSelect.js.map