rsuite
Version:
A suite of react components
522 lines (458 loc) • 19.2 kB
JavaScript
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;