UNPKG

rsuite

Version:

A suite of react components

522 lines (458 loc) 19.2 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; import React, { useRef, useState, useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; import isNil from 'lodash/isNil'; import isFunction from 'lodash/isFunction'; import shallowEqual from '../utils/shallowEqual'; import DropdownMenu from './DropdownMenu'; import { findNodeOfTree, flattenTree, getNodeParents } from '../utils/treeUtils'; import { usePaths } from './utils'; import { getSafeRegExpString, createChainedFunction, mergeRefs, useControlled, useCustom, useClassNames } from '../utils'; import { PickerToggle, PickerOverlay, SearchBar, PickerToggleTrigger, usePickerClassName, usePublicMethods, useToggleKeyDownEvent, useFocusItemValue, pickTriggerPropKeys, omitTriggerPropKeys, listPickerPropTypes } from '../Picker'; var emptyArray = []; var Cascader = /*#__PURE__*/React.forwardRef(function (props, ref) { var _props$as = props.as, Component = _props$as === void 0 ? 'div' : _props$as, _props$data = props.data, data = _props$data === void 0 ? emptyArray : _props$data, _props$classPrefix = props.classPrefix, classPrefix = _props$classPrefix === void 0 ? 'picker' : _props$classPrefix, _props$childrenKey = props.childrenKey, childrenKey = _props$childrenKey === void 0 ? 'children' : _props$childrenKey, _props$valueKey = props.valueKey, valueKey = _props$valueKey === void 0 ? 'value' : _props$valueKey, _props$labelKey = props.labelKey, labelKey = _props$labelKey === void 0 ? 'label' : _props$labelKey, defaultValue = props.defaultValue, placeholder = props.placeholder, disabled = props.disabled, _props$disabledItemVa = props.disabledItemValues, disabledItemValues = _props$disabledItemVa === void 0 ? emptyArray : _props$disabledItemVa, _props$appearance = props.appearance, appearance = _props$appearance === void 0 ? 'default' : _props$appearance, _props$cleanable = props.cleanable, cleanable = _props$cleanable === void 0 ? true : _props$cleanable, overrideLocale = props.locale, toggleAs = props.toggleAs, style = props.style, valueProp = props.value, inline = props.inline, menuClassName = props.menuClassName, menuStyle = props.menuStyle, menuWidth = props.menuWidth, menuHeight = props.menuHeight, _props$searchable = props.searchable, searchable = _props$searchable === void 0 ? true : _props$searchable, parentSelectable = props.parentSelectable, _props$placement = props.placement, placement = _props$placement === void 0 ? 'bottomStart' : _props$placement, id = props.id, renderMenuItem = props.renderMenuItem, renderSearchItem = props.renderSearchItem, renderValue = props.renderValue, renderMenu = props.renderMenu, renderExtraFooter = props.renderExtraFooter, onEnter = props.onEnter, onExited = props.onExited, onClean = props.onClean, onChange = props.onChange, onSelect = props.onSelect, onSearch = props.onSearch, onClose = props.onClose, onOpen = props.onOpen, getChildren = props.getChildren, rest = _objectWithoutPropertiesLoose(props, ["as", "data", "classPrefix", "childrenKey", "valueKey", "labelKey", "defaultValue", "placeholder", "disabled", "disabledItemValues", "appearance", "cleanable", "locale", "toggleAs", "style", "value", "inline", "menuClassName", "menuStyle", "menuWidth", "menuHeight", "searchable", "parentSelectable", "placement", "id", "renderMenuItem", "renderSearchItem", "renderValue", "renderMenu", "renderExtraFooter", "onEnter", "onExited", "onClean", "onChange", "onSelect", "onSearch", "onClose", "onOpen", "getChildren"]); // Use component active state to support keyboard events. var _useState = useState(false), active = _useState[0], setActive = _useState[1]; var _useState2 = useState(flattenTree(data, childrenKey)), flattenData = _useState2[0], setFlattenData = _useState2[1]; var triggerRef = useRef(null); var overlayRef = useRef(null); var targetRef = useRef(null); var searchInputRef = useRef(null); var _ref = useControlled(valueProp, defaultValue), value = _ref[0], setValue = _ref[1]; var _usePaths = usePaths({ data: data, valueKey: valueKey, childrenKey: childrenKey, value: value }), selectedPaths = _usePaths.selectedPaths, valueToPaths = _usePaths.valueToPaths, columnData = _usePaths.columnData, addColumn = _usePaths.addColumn, setValueToPaths = _usePaths.setValueToPaths, setColumnData = _usePaths.setColumnData, setSelectedPaths = _usePaths.setSelectedPaths, enforceUpdate = _usePaths.enforceUpdate; useEffect(function () { setFlattenData(flattenTree(data, childrenKey)); }, [data, childrenKey]); usePublicMethods(ref, { triggerRef: triggerRef, overlayRef: overlayRef, targetRef: targetRef }); var _useCustom = useCustom('Picker', overrideLocale), locale = _useCustom.locale, rtl = _useCustom.rtl; /** * 1.Have a value and the value is valid. * 2.Regardless of whether the value is valid, as long as renderValue is set, it is judged to have a value. */ var hasValue = valueToPaths.length > 0 || !isNil(value) && isFunction(renderValue); var _useClassNames = useClassNames(classPrefix), prefix = _useClassNames.prefix, merge = _useClassNames.merge; var _useState3 = useState(''), searchKeyword = _useState3[0], setSearchKeyword = _useState3[1]; var someKeyword = useCallback(function (item, keyword) { if (item[labelKey].match(new RegExp(getSafeRegExpString(keyword || searchKeyword), 'i'))) { return true; } if (item.parent && someKeyword(item.parent)) { return true; } return false; }, [labelKey, searchKeyword]); var getSearchResult = useCallback(function (keyword) { var items = []; var result = flattenData.filter(function (item) { if (!parentSelectable && item[childrenKey]) { return false; } return someKeyword(item, keyword); }); for (var i = 0; i < result.length; i++) { items.push(result[i]); // A maximum of 100 search results are returned. if (i === 99) { return items; } } return items; }, [childrenKey, flattenData, someKeyword, parentSelectable]); // Used to hover the focuse item when trigger `onKeydown` var _useFocusItemValue = useFocusItemValue(value, { rtl: rtl, data: flattenData, valueKey: valueKey, defaultLayer: valueToPaths !== null && valueToPaths !== void 0 && valueToPaths.length ? valueToPaths.length - 1 : 0, target: function target() { return overlayRef.current; }, callback: useCallback(function (value) { enforceUpdate(value, true); }, [enforceUpdate]) }), focusItemValue = _useFocusItemValue.focusItemValue, setFocusItemValue = _useFocusItemValue.setFocusItemValue, setLayer = _useFocusItemValue.setLayer, setKeys = _useFocusItemValue.setKeys, onFocusItem = _useFocusItemValue.onKeyDown; var handleSearch = useCallback(function (value, event) { var items = getSearchResult(value); setSearchKeyword(value); onSearch === null || onSearch === void 0 ? void 0 : onSearch(value, event); if (items.length > 0) { setFocusItemValue(items[0][valueKey]); setLayer(0); setKeys([]); } }, [getSearchResult, onSearch, setFocusItemValue, setKeys, setLayer, valueKey]); var handleEntered = useCallback(function () { if (!targetRef.current) { return; } onOpen === null || onOpen === void 0 ? void 0 : onOpen(); setActive(true); }, [onOpen]); var handleExited = useCallback(function () { if (!targetRef.current) { return; } onClose === null || onClose === void 0 ? void 0 : onClose(); setActive(false); setSearchKeyword(''); }, [onClose]); var handleClose = useCallback(function () { var _triggerRef$current; (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 ? void 0 : _triggerRef$current.close(); }, [triggerRef]); var handleClean = useCallback(function (event) { if (disabled || !targetRef.current) { return; } setColumnData([data]); setValue(null); setSelectedPaths([]); setValueToPaths([]); onChange === null || onChange === void 0 ? void 0 : onChange(null, event); }, [data, disabled, onChange, setSelectedPaths, setColumnData, setValueToPaths, setValue]); var handleMenuPressEnter = useCallback(function (event) { var focusItem = findNodeOfTree(data, function (item) { return item[valueKey] === focusItemValue; }); var isLeafNode = focusItem && !focusItem[childrenKey]; if (isLeafNode) { setValue(focusItemValue); setValueToPaths(selectedPaths); if (selectedPaths.length) { setLayer(selectedPaths.length - 1); } if (!shallowEqual(value, focusItemValue)) { onChange === null || onChange === void 0 ? void 0 : onChange(focusItemValue !== null && focusItemValue !== void 0 ? focusItemValue : null, event); } handleClose(); } }, [childrenKey, data, focusItemValue, handleClose, onChange, selectedPaths, setLayer, setValue, setValueToPaths, value, valueKey]); var onPickerKeyDown = useToggleKeyDownEvent(_extends({ toggle: !focusItemValue || !active, triggerRef: triggerRef, targetRef: targetRef, overlayRef: overlayRef, searchInputRef: searchInputRef, active: active, onExit: handleClean, onMenuKeyDown: onFocusItem, onMenuPressEnter: handleMenuPressEnter }, rest)); var handleSelect = function handleSelect(node, cascadePaths, isLeafNode, event) { var _node$childrenKey, _node$childrenKey2, _triggerRef$current2; var nextValue = node[valueKey]; onSelect === null || onSelect === void 0 ? void 0 : onSelect(node, cascadePaths, event); setSelectedPaths(cascadePaths); // Lazy load node's children if (typeof getChildren === 'function' && ((_node$childrenKey = node[childrenKey]) === null || _node$childrenKey === void 0 ? void 0 : _node$childrenKey.length) === 0) { node.loading = true; var children = getChildren(node); if (children instanceof Promise) { children.then(function (data) { node.loading = false; node[childrenKey] = data; if (targetRef.current) { addColumn(data, cascadePaths.length); } }); } else { node.loading = false; node[childrenKey] = children; addColumn(children, cascadePaths.length); } } else if ((_node$childrenKey2 = node[childrenKey]) !== null && _node$childrenKey2 !== void 0 && _node$childrenKey2.length) { addColumn(node[childrenKey], cascadePaths.length); } if (isLeafNode) { // Determines whether the option is a leaf node, and if so, closes the picker. handleClose(); // Update the selected path to the value path. // That is, the selected path will be displayed on the button after clicking the child node. setValueToPaths(cascadePaths); setValue(nextValue); if (!shallowEqual(value, nextValue)) { onChange === null || onChange === void 0 ? void 0 : onChange(nextValue, event); } return; } /** When the parent is optional, the value and the displayed path are updated. */ if (parentSelectable && !shallowEqual(value, nextValue)) { setValue(nextValue); onChange === null || onChange === void 0 ? void 0 : onChange(nextValue, event); setValueToPaths(cascadePaths); } // Update menu position (_triggerRef$current2 = triggerRef.current) === null || _triggerRef$current2 === void 0 ? void 0 : _triggerRef$current2.updatePosition(); }; /** * The search structure option is processed after being selected. */ var handleSearchRowSelect = function handleSearchRowSelect(node, nodes, event) { var nextValue = node[valueKey]; handleClose(); setSearchKeyword(''); setValue(nextValue); setValueToPaths(nodes); enforceUpdate(nextValue); onSelect === null || onSelect === void 0 ? void 0 : onSelect(node, nodes, event); onChange === null || onChange === void 0 ? void 0 : onChange(nextValue, event); }; var renderSearchRow = function renderSearchRow(item, key) { var regx = new RegExp(getSafeRegExpString(searchKeyword), 'ig'); var nodes = getNodeParents(item); nodes.push(item); var formattedNodes = nodes.map(function (node) { var _extends2; var labelElements = []; var a = node[labelKey].split(regx); var b = node[labelKey].match(regx); for (var i = 0; i < a.length; i++) { labelElements.push(a[i]); if (b && b[i]) { labelElements.push( /*#__PURE__*/React.createElement("span", { key: i, className: prefix('cascader-search-match') }, b[i])); } } return _extends({}, node, (_extends2 = {}, _extends2[labelKey] = labelElements, _extends2)); }); var disabled = disabledItemValues.some(function (value) { return formattedNodes.some(function (node) { return node[valueKey] === value; }); }); var itemClasses = prefix('cascader-row', { 'cascader-row-disabled': disabled, 'cascader-row-focus': item[valueKey] === focusItemValue }); var label = formattedNodes.map(function (node, index) { return /*#__PURE__*/React.createElement("span", { key: "col-" + index, className: prefix('cascader-col') }, node[labelKey]); }); return /*#__PURE__*/React.createElement("div", { key: key, "aria-disabled": disabled, "data-key": item[valueKey], className: itemClasses, onClick: function onClick(event) { if (!disabled) { handleSearchRowSelect(item, nodes, event); } } }, renderSearchItem ? renderSearchItem(label, nodes) : label); }; var renderSearchResultPanel = function renderSearchResultPanel() { if (searchKeyword === '') { return null; } var items = getSearchResult(); return /*#__PURE__*/React.createElement("div", { className: prefix('cascader-search-panel'), "data-layer": 0 }, items.length ? items.map(renderSearchRow) : /*#__PURE__*/React.createElement("div", { className: prefix('none') }, locale.noResultsText)); }; var renderDropdownMenu = function renderDropdownMenu(positionProps, speakerRef) { var _ref2 = positionProps || {}, left = _ref2.left, top = _ref2.top, className = _ref2.className; var styles = _extends({}, menuStyle, { left: left, top: top }); var classes = merge(className, menuClassName, prefix('cascader-menu', { inline: inline })); return /*#__PURE__*/React.createElement(PickerOverlay, { ref: mergeRefs(overlayRef, speakerRef), className: classes, style: styles, target: triggerRef, onKeyDown: onPickerKeyDown }, searchable && /*#__PURE__*/React.createElement(SearchBar, { placeholder: locale === null || locale === void 0 ? void 0 : locale.searchPlaceholder, onChange: handleSearch, value: searchKeyword, inputRef: searchInputRef }), renderSearchResultPanel(), searchKeyword === '' && /*#__PURE__*/React.createElement(DropdownMenu, { id: id ? id + "-listbox" : undefined, menuWidth: menuWidth, menuHeight: menuHeight, disabledItemValues: disabledItemValues, valueKey: valueKey, labelKey: labelKey, childrenKey: childrenKey, classPrefix: 'picker-cascader-menu', cascadeData: columnData, cascadePaths: selectedPaths, activeItemValue: value, onSelect: handleSelect, renderMenu: renderMenu, renderMenuItem: renderMenuItem }), renderExtraFooter === null || renderExtraFooter === void 0 ? void 0 : renderExtraFooter()); }; var selectedElement = placeholder; if (valueToPaths.length > 0) { selectedElement = []; valueToPaths.forEach(function (item, index) { var key = item[valueKey] || item[labelKey]; selectedElement.push( /*#__PURE__*/React.createElement("span", { key: key }, item[labelKey])); if (index < valueToPaths.length - 1) { selectedElement.push( /*#__PURE__*/React.createElement("span", { className: "separator", key: key + "-separator" }, ' / ')); } }); } if (!isNil(value) && isFunction(renderValue)) { selectedElement = renderValue(value, valueToPaths, selectedElement); // If renderValue returns null or undefined, hasValue is false. if (isNil(selectedElement)) { hasValue = false; } } var _usePickerClassName = usePickerClassName(_extends({}, props, { classPrefix: classPrefix, hasValue: hasValue, name: 'cascader', appearance: appearance, cleanable: cleanable })), classes = _usePickerClassName[0], usedClassNamePropKeys = _usePickerClassName[1]; // TODO: bad api design // consider an isolated Menu component if (inline) { return renderDropdownMenu(); } return /*#__PURE__*/React.createElement(PickerToggleTrigger, { pickerProps: pick(props, pickTriggerPropKeys), ref: triggerRef, placement: placement, onEntered: createChainedFunction(handleEntered, onEnter), onExited: createChainedFunction(handleExited, onExited), speaker: renderDropdownMenu }, /*#__PURE__*/React.createElement(Component, { className: classes, style: style }, /*#__PURE__*/React.createElement(PickerToggle, _extends({}, omit(rest, [].concat(omitTriggerPropKeys, usedClassNamePropKeys)), { id: id, ref: targetRef, as: toggleAs, appearance: appearance, disabled: disabled, onClean: createChainedFunction(handleClean, onClean), onKeyDown: onPickerKeyDown, cleanable: cleanable && !disabled, hasValue: hasValue, active: active, placement: placement, inputValue: value !== null && value !== void 0 ? value : '' }), selectedElement || (locale === null || locale === void 0 ? void 0 : locale.placeholder)))); }); Cascader.displayName = 'Cascader'; Cascader.propTypes = _extends({}, listPickerPropTypes, { disabledItemValues: PropTypes.array, locale: PropTypes.any, appearance: PropTypes.oneOf(['default', 'subtle']), renderMenu: PropTypes.func, onSelect: PropTypes.func, onSearch: PropTypes.func, cleanable: PropTypes.bool, renderMenuItem: PropTypes.func, renderSearchItem: PropTypes.func, menuWidth: PropTypes.number, menuHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), searchable: PropTypes.bool, inline: PropTypes.bool, parentSelectable: PropTypes.bool }); export default Cascader;