rsuite
Version:
A suite of react components
519 lines (464 loc) • 19.8 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
import React, { useState, useRef, useCallback } from 'react';
import PropTypes from 'prop-types';
import pick from 'lodash/pick';
import omit from 'lodash/omit';
import isFunction from 'lodash/isFunction';
import isNil from 'lodash/isNil';
import DropdownMenu from './DropdownMenu';
import Checkbox from '../Checkbox';
import { useCascadeValue, useColumnData, useFlattenData, isSomeChildChecked } from './utils';
import { getNodeParents, findNodeOfTree } from '../utils/treeUtils';
import { getColumnsAndPaths } from '../Cascader/utils';
import { createChainedFunction, mergeRefs, getSafeRegExpString, useClassNames, useCustom, useUpdateEffect } from '../utils';
import { PickerToggle, PickerOverlay, SearchBar, SelectedElement, PickerToggleTrigger, usePickerClassName, usePublicMethods, useToggleKeyDownEvent, useFocusItemValue, pickTriggerPropKeys, omitTriggerPropKeys, listPickerPropTypes } from '../Picker';
var emptyArray = [];
var MultiCascader = /*#__PURE__*/React.forwardRef(function (props, ref) {
var _selectedPaths;
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,
defaultValue = props.defaultValue,
valueProp = props.value,
_props$valueKey = props.valueKey,
valueKey = _props$valueKey === void 0 ? 'value' : _props$valueKey,
_props$labelKey = props.labelKey,
labelKey = _props$labelKey === void 0 ? 'label' : _props$labelKey,
_props$childrenKey = props.childrenKey,
childrenKey = _props$childrenKey === void 0 ? 'children' : _props$childrenKey,
disabled = props.disabled,
_props$disabledItemVa = props.disabledItemValues,
disabledItemValues = _props$disabledItemVa === void 0 ? emptyArray : _props$disabledItemVa,
_props$cleanable = props.cleanable,
cleanable = _props$cleanable === void 0 ? true : _props$cleanable,
overrideLocale = props.locale,
toggleAs = props.toggleAs,
style = props.style,
_props$countable = props.countable,
countable = _props$countable === void 0 ? true : _props$countable,
_props$cascade = props.cascade,
cascade = _props$cascade === void 0 ? true : _props$cascade,
inline = props.inline,
placeholder = props.placeholder,
_props$placement = props.placement,
placement = _props$placement === void 0 ? 'bottomStart' : _props$placement,
_props$appearance = props.appearance,
appearance = _props$appearance === void 0 ? 'default' : _props$appearance,
menuWidth = props.menuWidth,
menuHeight = props.menuHeight,
menuClassName = props.menuClassName,
menuStyle = props.menuStyle,
_props$searchable = props.searchable,
searchable = _props$searchable === void 0 ? true : _props$searchable,
_props$uncheckableIte = props.uncheckableItemValues,
uncheckableItemValues = _props$uncheckableIte === void 0 ? emptyArray : _props$uncheckableIte,
id = props.id,
getChildren = props.getChildren,
renderValue = props.renderValue,
renderMenu = props.renderMenu,
renderMenuItem = props.renderMenuItem,
renderExtraFooter = props.renderExtraFooter,
onEnter = props.onEnter,
onExit = props.onExit,
onExited = props.onExited,
onClean = props.onClean,
onSearch = props.onSearch,
onSelect = props.onSelect,
onChange = props.onChange,
onOpen = props.onOpen,
onClose = props.onClose,
onCheck = props.onCheck,
rest = _objectWithoutPropertiesLoose(props, ["as", "data", "classPrefix", "defaultValue", "value", "valueKey", "labelKey", "childrenKey", "disabled", "disabledItemValues", "cleanable", "locale", "toggleAs", "style", "countable", "cascade", "inline", "placeholder", "placement", "appearance", "menuWidth", "menuHeight", "menuClassName", "menuStyle", "searchable", "uncheckableItemValues", "id", "getChildren", "renderValue", "renderMenu", "renderMenuItem", "renderExtraFooter", "onEnter", "onExit", "onExited", "onClean", "onSearch", "onSelect", "onChange", "onOpen", "onClose", "onCheck"]);
var itemKeys = {
childrenKey: childrenKey,
labelKey: labelKey,
valueKey: valueKey
};
var _useState = useState(false),
active = _useState[0],
setActive = _useState[1];
var _useFlattenData = useFlattenData(data, itemKeys),
flattenData = _useFlattenData.flattenData,
addFlattenData = _useFlattenData.addFlattenData;
var _useCascadeValue = useCascadeValue(_extends({}, itemKeys, {
uncheckableItemValues: uncheckableItemValues,
cascade: cascade,
value: valueProp || defaultValue
}), flattenData),
value = _useCascadeValue.value,
setValue = _useCascadeValue.setValue,
splitValue = _useCascadeValue.splitValue; // The columns displayed in the cascading panel.
var _useColumnData = useColumnData(flattenData),
columnData = _useColumnData.columnData,
setColumnData = _useColumnData.setColumnData,
addColumn = _useColumnData.addColumn,
enforceUpdateColumnData = _useColumnData.enforceUpdateColumnData;
useUpdateEffect(function () {
enforceUpdateColumnData(data);
}, [data]); // The path after cascading data selection.
var _useState2 = useState(),
selectedPaths = _useState2[0],
setSelectedPaths = _useState2[1];
var triggerRef = useRef(null);
var overlayRef = useRef(null);
var targetRef = useRef(null);
var searchInputRef = useRef(null);
usePublicMethods(ref, {
triggerRef: triggerRef,
overlayRef: overlayRef,
targetRef: targetRef
});
var _useCustom = useCustom('Picker', overrideLocale),
locale = _useCustom.locale,
rtl = _useCustom.rtl;
var selectedItems = flattenData.filter(function (item) {
return value.some(function (v) {
return v === item[valueKey];
});
}) || []; // Used to hover the focuse item when trigger `onKeydown`
var _useFocusItemValue = useFocusItemValue(selectedPaths === null || selectedPaths === void 0 ? void 0 : (_selectedPaths = selectedPaths[selectedPaths.length - 1]) === null || _selectedPaths === void 0 ? void 0 : _selectedPaths[valueKey], {
rtl: rtl,
data: flattenData,
valueKey: valueKey,
defaultLayer: selectedPaths !== null && selectedPaths !== void 0 && selectedPaths.length ? selectedPaths.length - 1 : 0,
target: function target() {
return overlayRef.current;
},
callback: useCallback(function (value) {
var _getColumnsAndPaths = getColumnsAndPaths(data, value, {
valueKey: valueKey,
childrenKey: childrenKey,
isAttachChildren: true
}),
columns = _getColumnsAndPaths.columns,
paths = _getColumnsAndPaths.paths;
setColumnData(columns);
setSelectedPaths(paths);
}, [childrenKey, data, setColumnData, valueKey])
}),
focusItemValue = _useFocusItemValue.focusItemValue,
setLayer = _useFocusItemValue.setLayer,
setKeys = _useFocusItemValue.setKeys,
onFocusItem = _useFocusItemValue.onKeyDown;
/**
* 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 = selectedItems.length > 0 || Number(valueProp === null || valueProp === void 0 ? void 0 : valueProp.length) > 0 && isFunction(renderValue);
var _useClassNames = useClassNames(classPrefix),
prefix = _useClassNames.prefix,
merge = _useClassNames.merge;
var _useState3 = useState(''),
searchKeyword = _useState3[0],
setSearchKeyword = _useState3[1];
var handleEntered = useCallback(function () {
onOpen === null || onOpen === void 0 ? void 0 : onOpen();
setActive(true);
}, [onOpen]);
var handleExited = useCallback(function () {
setActive(false);
setSearchKeyword('');
}, []);
var handleSelect = useCallback(function (node, cascadePaths, event) {
var _node$childrenKey, _node$childrenKey2, _triggerRef$current, _triggerRef$current$u;
setSelectedPaths(cascadePaths);
onSelect === null || onSelect === void 0 ? void 0 : onSelect(node, cascadePaths, event); // 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) {
addFlattenData(data, node);
addColumn(data, cascadePaths.length);
}
});
} else {
node.loading = false;
node[childrenKey] = children;
addFlattenData(children, node);
addColumn(children, cascadePaths.length);
}
} else if ((_node$childrenKey2 = node[childrenKey]) !== null && _node$childrenKey2 !== void 0 && _node$childrenKey2.length) {
addColumn(node[childrenKey], cascadePaths.length);
}
(_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 ? void 0 : (_triggerRef$current$u = _triggerRef$current.updatePosition) === null || _triggerRef$current$u === void 0 ? void 0 : _triggerRef$current$u.call(_triggerRef$current);
}, [onSelect, getChildren, childrenKey, addColumn, addFlattenData]);
var handleCheck = useCallback(function (node, event, checked) {
var nodeValue = node[valueKey];
var nextValue = [];
if (cascade) {
nextValue = splitValue(node, checked, value).value;
} else {
nextValue = [].concat(value);
if (checked) {
nextValue.push(nodeValue);
} else {
nextValue = nextValue.filter(function (n) {
return n !== nodeValue;
});
}
}
setValue(nextValue);
onChange === null || onChange === void 0 ? void 0 : onChange(nextValue, event);
onCheck === null || onCheck === void 0 ? void 0 : onCheck(nextValue, node, checked, event);
}, [cascade, onChange, onCheck, setValue, splitValue, value, valueKey]);
var handleClean = useCallback(function (event) {
if (disabled || !targetRef.current) {
return;
}
setSelectedPaths([]);
setValue([]);
setColumnData([data]);
onChange === null || onChange === void 0 ? void 0 : onChange([], event);
}, [data, disabled, onChange, setColumnData, setValue]);
var handleMenuPressEnter = useCallback(function (event) {
var _overlayRef$current;
var focusItem = findNodeOfTree(data, function (item) {
return item[valueKey] === focusItemValue;
});
var checkbox = (_overlayRef$current = overlayRef.current) === null || _overlayRef$current === void 0 ? void 0 : _overlayRef$current.querySelector("[data-key=\"" + focusItemValue + "\"] [type=\"checkbox\"]");
if (checkbox) {
handleCheck(focusItem, event, (checkbox === null || checkbox === void 0 ? void 0 : checkbox.getAttribute('aria-checked')) !== 'true');
}
}, [data, focusItemValue, handleCheck, 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 handleSearch = useCallback(function (value, event) {
setSearchKeyword(value);
onSearch === null || onSearch === void 0 ? void 0 : onSearch(value, event);
if (value) {
setLayer(0);
} else if (selectedPaths !== null && selectedPaths !== void 0 && selectedPaths.length) {
setLayer(selectedPaths.length - 1);
}
setKeys([]);
}, [onSearch, selectedPaths, setKeys, setLayer]);
var getSearchResult = useCallback(function () {
var items = [];
var result = flattenData.filter(function (item) {
if (uncheckableItemValues.some(function (value) {
return item[valueKey] === value;
})) {
return false;
}
if (item[labelKey].match(new RegExp(getSafeRegExpString(searchKeyword), 'i'))) {
return true;
}
return false;
});
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;
}, [flattenData, labelKey, searchKeyword, uncheckableItemValues, valueKey]);
var renderSearchRow = function renderSearchRow(item, key) {
var _extends2;
var nodes = getNodeParents(item);
var regx = new RegExp(getSafeRegExpString(searchKeyword), 'ig');
var labelElements = [];
var a = item[labelKey].split(regx);
var b = item[labelKey].match(regx);
for (var i = 0; i < a.length; i++) {
labelElements.push(a[i]);
if (b[i]) {
labelElements.push( /*#__PURE__*/React.createElement("span", {
key: i,
className: prefix('cascader-search-match')
}, b[i]));
}
}
nodes.push(_extends({}, item, (_extends2 = {}, _extends2[labelKey] = labelElements, _extends2)));
var active = value.some(function (value) {
if (cascade) {
return nodes.some(function (node) {
return node[valueKey] === value;
});
}
return item[valueKey] === value;
});
var disabled = disabledItemValues.some(function (value) {
return nodes.some(function (node) {
return node[valueKey] === value;
});
});
var itemClasses = prefix('cascader-row', {
'cascader-row-disabled': disabled,
'cascader-row-focus': item[valueKey] === focusItemValue
});
return /*#__PURE__*/React.createElement("div", {
key: key,
className: itemClasses,
"aria-disabled": disabled,
"data-key": item[valueKey]
}, /*#__PURE__*/React.createElement(Checkbox, {
disabled: disabled,
checked: active,
value: item[valueKey],
indeterminate: cascade && !active && isSomeChildChecked(item, value, {
valueKey: valueKey,
childrenKey: childrenKey
}),
onChange: function onChange(_value, checked, event) {
handleCheck(item, event, checked);
}
}, /*#__PURE__*/React.createElement("span", {
className: prefix('cascader-cols')
}, nodes.map(function (node, index) {
return /*#__PURE__*/React.createElement("span", {
key: "col-" + index,
className: prefix('cascader-col')
}, node[labelKey]);
}))));
};
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 _ref = positionProps || {},
left = _ref.left,
top = _ref.top,
className = _ref.className;
var styles = _extends({}, menuStyle, {
left: left,
top: top
});
var classes = merge(className, menuClassName, prefix('cascader-menu', 'multi-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,
cascade: cascade,
menuWidth: menuWidth,
menuHeight: menuHeight,
uncheckableItemValues: uncheckableItemValues,
disabledItemValues: disabledItemValues,
valueKey: valueKey,
labelKey: labelKey,
childrenKey: childrenKey,
classPrefix: 'picker-cascader-menu',
cascadeData: columnData,
cascadePaths: selectedPaths,
value: value,
onSelect: handleSelect,
onCheck: handleCheck,
renderMenu: renderMenu,
renderMenuItem: renderMenuItem
}), renderExtraFooter === null || renderExtraFooter === void 0 ? void 0 : renderExtraFooter());
};
var selectedElement = placeholder;
if (selectedItems.length > 0) {
selectedElement = /*#__PURE__*/React.createElement(SelectedElement, {
selectedItems: selectedItems,
countable: countable,
valueKey: valueKey,
labelKey: labelKey,
childrenKey: childrenKey,
prefix: prefix,
cascade: cascade,
locale: locale
});
}
if (hasValue && isFunction(renderValue)) {
selectedElement = renderValue(value.length ? value : valueProp !== null && valueProp !== void 0 ? valueProp : [], selectedItems, selectedElement); // If renderValue returns null or undefined, hasValue is false.
if (isNil(selectedElement)) {
hasValue = false;
}
}
var _usePickerClassName = usePickerClassName(_extends({}, props, {
classPrefix: classPrefix,
hasValue: hasValue,
countable: countable,
name: 'cascader',
appearance: appearance,
cleanable: cleanable
})),
classes = _usePickerClassName[0],
usedClassNamePropKeys = _usePickerClassName[1];
if (inline) {
return renderDropdownMenu();
}
return /*#__PURE__*/React.createElement(PickerToggleTrigger, {
pickerProps: pick(props, pickTriggerPropKeys),
ref: triggerRef,
placement: placement,
onEnter: createChainedFunction(handleEntered, onEnter),
onExited: createChainedFunction(handleExited, onExited),
onExit: createChainedFunction(onClose, onExit),
speaker: renderDropdownMenu
}, /*#__PURE__*/React.createElement(Component, {
className: classes,
style: style
}, /*#__PURE__*/React.createElement(PickerToggle, _extends({}, omit(rest, [].concat(omitTriggerPropKeys, usedClassNamePropKeys)), {
id: id,
as: toggleAs,
appearance: appearance,
disabled: disabled,
ref: targetRef,
onClean: createChainedFunction(handleClean, onClean),
onKeyDown: onPickerKeyDown,
cleanable: cleanable && !disabled,
hasValue: hasValue,
active: active,
placement: placement,
inputValue: value
}), selectedElement || locale.placeholder)));
});
MultiCascader.displayName = 'MultiCascader';
MultiCascader.propTypes = _extends({}, listPickerPropTypes, {
value: PropTypes.array,
disabledItemValues: PropTypes.array,
locale: PropTypes.any,
appearance: PropTypes.oneOf(['default', 'subtle']),
cascade: PropTypes.bool,
inline: PropTypes.bool,
countable: PropTypes.bool,
menuWidth: PropTypes.number,
menuHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
uncheckableItemValues: PropTypes.array,
searchable: PropTypes.bool,
renderMenuItem: PropTypes.func,
renderMenu: PropTypes.func,
onSearch: PropTypes.func,
onSelect: PropTypes.func,
onCheck: PropTypes.func
});
export default MultiCascader;