@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
1,348 lines • 51.9 kB
JavaScript
import _pick from "lodash/pick";
import _isNull from "lodash/isNull";
import _isUndefined from "lodash/isUndefined";
import _isFunction from "lodash/isFunction";
import _get from "lodash/get";
import _noop from "lodash/noop";
import _isEmpty from "lodash/isEmpty";
import _isString from "lodash/isString";
import _isEqual from "lodash/isEqual";
var __rest = this && this.__rest || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import cls from 'classnames';
import PropTypes from 'prop-types';
import TreeSelectFoundation from '@douyinfe/semi-foundation/lib/es/treeSelect/foundation';
import { convertDataToEntities, flattenTreeData, calcExpandedKeysForValues, calcMotionKeys, findKeysForValues, calcCheckedKeys, calcExpandedKeys, getValueOrKey, normalizeKeyList, calcDisabledKeys, normalizeValue, updateKeys, filterTreeData } from '@douyinfe/semi-foundation/lib/es/tree/treeUtil';
import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/treeSelect/constants';
import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/lib/es/popover/constants';
import { FixedSizeList as VirtualList } from 'react-window';
import '@douyinfe/semi-foundation/lib/es/tree/tree.css';
import '@douyinfe/semi-foundation/lib/es/treeSelect/treeSelect.css';
import BaseComponent from '../_base/baseComponent';
import ConfigContext from '../configProvider/context';
import TagGroup from '../tag/group';
import Tag from '../tag/index';
import Input from '../input/index';
import AutoSizer from '../tree/autoSizer';
import TreeContext from '../tree/treeContext';
import TreeNode from '../tree/treeNode';
import NodeList from '../tree/nodeList';
import { cloneDeep } from '../tree/treeUtil';
import LocaleConsumer from '../locale/localeConsumer';
import Trigger from '../trigger';
import TagInput from '../tagInput';
import { isSemiIcon } from '../_utils';
import { IconChevronDown, IconClear, IconSearch } from '@douyinfe/semi-icons';
import CheckboxGroup from '../checkbox/checkboxGroup';
import Popover from '../popover/index';
import VirtualRow from '../select/virtualRow';
const prefixcls = cssClasses.PREFIX;
const prefixTree = cssClasses.PREFIX_TREE;
const key = 0;
class TreeSelect extends BaseComponent {
constructor(props) {
super(props);
this.renderSuffix = () => {
const {
suffix
} = this.props;
const suffixWrapperCls = cls({
[`${prefixcls}-suffix`]: true,
[`${prefixcls}-suffix-text`]: suffix && _isString(suffix),
[`${prefixcls}-suffix-icon`]: isSemiIcon(suffix)
});
return /*#__PURE__*/React.createElement("div", {
className: suffixWrapperCls,
"x-semi-prop": "suffix"
}, suffix);
};
this.renderPrefix = () => {
const {
prefix,
insetLabel,
insetLabelId
} = this.props;
const labelNode = prefix || insetLabel;
const prefixWrapperCls = cls({
[`${prefixcls}-prefix`]: true,
// to be doublechecked
[`${prefixcls}-inset-label`]: insetLabel,
[`${prefixcls}-prefix-text`]: labelNode && _isString(labelNode),
[`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode)
});
return /*#__PURE__*/React.createElement("div", {
className: prefixWrapperCls,
id: insetLabelId,
"x-semi-prop": "prefix,insetLabel"
}, labelNode);
};
this.renderContent = () => {
const {
dropdownMinWidth
} = this.state;
const {
dropdownStyle,
dropdownClassName
} = this.props;
const style = Object.assign({
minWidth: dropdownMinWidth
}, dropdownStyle);
const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
return /*#__PURE__*/React.createElement("div", {
className: popoverCls,
style: style,
onKeyDown: this.foundation.handleKeyDown
}, this.renderTree());
};
this.removeTag = removedKey => {
this.foundation.removeTag(removedKey);
};
this.handleClick = e => {
this.foundation.handleClick(e);
};
this.getDataForKeyNotInKeyEntities = key => {
return this.foundation.getDataForKeyNotInKeyEntities(key);
};
/* istanbul ignore next */
this.handleSelectionEnterPress = e => {
this.foundation.handleSelectionEnterPress(e);
};
this.hasValue = () => {
const {
multiple,
checkRelation
} = this.props;
const {
realCheckedKeys,
checkedKeys,
selectedKeys
} = this.state;
let hasValue = false;
if (multiple) {
if (checkRelation === 'related') {
hasValue = Boolean(checkedKeys.size);
} else if (checkRelation === 'unRelated') {
hasValue = Boolean(realCheckedKeys.size);
}
} else {
hasValue = Boolean(selectedKeys.length);
}
return hasValue;
};
this.showClearBtn = () => {
const {
showClear,
disabled,
searchPosition
} = this.props;
const {
inputValue,
isOpen,
isHovering
} = this.state;
const triggerSearchHasInputValue = searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue;
return showClear && (this.hasValue() || triggerSearchHasInputValue) && !disabled && (isOpen || isHovering);
};
this.renderTagList = triggerRenderKeys => {
const {
keyEntities,
disabledKeys
} = this.state;
const {
treeNodeLabelProp,
leafOnly,
disabled,
disableStrictly,
size,
renderSelectedItem: propRenderSelectedItem,
keyMaps
} = this.props;
const realLabelName = _get(keyMaps, 'label', treeNodeLabelProp);
const renderSelectedItem = _isFunction(propRenderSelectedItem) ? propRenderSelectedItem : item => ({
isRenderInTag: true,
content: _get(item, realLabelName, null)
});
const tagList = [];
triggerRenderKeys.forEach((key, index) => {
const item = keyEntities[key] && keyEntities[key].key === key ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
const onClose = (tagContent, e) => {
if (e && typeof e.preventDefault === 'function') {
// make sure that tag will not hidden immediately in controlled mode
e.preventDefault();
}
this.removeTag(key);
};
const {
content,
isRenderInTag
} = item ? renderSelectedItem(item, {
index,
onClose
}) : {};
if (_isNull(content) || _isUndefined(content)) {
return;
}
const isDisabled = disabled || item.disabled || disableStrictly && disabledKeys.has(item.key);
const tag = {
closable: !isDisabled,
color: 'white',
visible: true,
onClose,
key: `tag-${key}-${index}`,
size: size === 'small' ? 'small' : 'large'
};
if (isRenderInTag) {
// pass ReactNode list to tagList when using tagGroup custom mode
tagList.push(/*#__PURE__*/React.createElement(Tag, Object.assign({}, tag), content));
} else {
tagList.push(content);
}
});
return tagList;
};
/**
* When single selection and the search box is on trigger, the items displayed in the rendered search box
*/
this.renderSingleTriggerSearchItem = () => {
const {
placeholder,
disabled
} = this.props;
const {
inputTriggerFocus
} = this.state;
const renderText = this.foundation.getRenderTextInSingle();
const spanCls = cls(`${prefixcls}-selection-TriggerSearchItem`, {
[`${prefixcls}-selection-TriggerSearchItem-placeholder`]: (inputTriggerFocus || !renderText) && !disabled,
[`${prefixcls}-selection-TriggerSearchItem-disabled`]: disabled
});
return /*#__PURE__*/React.createElement("span", {
className: spanCls,
onClick: this.foundation.onClickSingleTriggerSearchItem
}, renderText ? renderText : placeholder);
};
/**
* Single selection and the search box content rendered when the search box is on trigger
*/
this.renderSingleTriggerSearch = () => {
const {
inputValue
} = this.state;
return /*#__PURE__*/React.createElement(React.Fragment, null, this.renderInput(), !inputValue && this.renderSingleTriggerSearchItem());
};
this.renderSelectContent = triggerRenderKeys => {
const {
multiple,
placeholder,
maxTagCount,
searchPosition,
filterTreeNode,
showRestTagsPopover,
restTagsPopoverProps
} = this.props;
const isTriggerPositionSearch = filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER;
// searchPosition = trigger
if (isTriggerPositionSearch) {
return multiple ? this.renderTagInput(triggerRenderKeys) : this.renderSingleTriggerSearch();
}
// searchPosition = dropdown and single seleciton
if (!multiple || !this.hasValue()) {
const renderText = this.foundation.getRenderTextInSingle();
const spanCls = cls(`${prefixcls}-selection-content`, {
[`${prefixcls}-selection-placeholder`]: !renderText
});
return /*#__PURE__*/React.createElement("span", {
className: spanCls
}, renderText ? renderText : placeholder);
}
// searchPosition = dropdown and multiple seleciton
const tagList = this.renderTagList(triggerRenderKeys);
// mode=custom to return tagList directly
return /*#__PURE__*/React.createElement(TagGroup, {
maxTagCount: maxTagCount,
tagList: tagList,
size: "large",
mode: "custom",
showPopover: showRestTagsPopover,
popoverProps: restTagsPopoverProps
});
};
this.handleClear = e => {
e && e.stopPropagation();
this.foundation.handleClear(e);
};
/* istanbul ignore next */
this.handleClearEnterPress = e => {
e && e.stopPropagation();
this.foundation.handleClearEnterPress(e);
};
this.handleMouseOver = e => {
this.foundation.toggleHoverState(true);
};
this.handleMouseLeave = e => {
this.foundation.toggleHoverState(false);
};
this.search = value => {
const {
isOpen
} = this.state;
if (!isOpen) {
this.foundation.open();
}
this.foundation.handleInputChange(value);
};
this.close = () => {
this.foundation.close(null);
};
this.renderArrow = () => {
const showClearBtn = this.showClearBtn();
const {
arrowIcon
} = this.props;
if (showClearBtn) {
return null;
}
return arrowIcon ? (/*#__PURE__*/React.createElement("div", {
className: cls(`${prefixcls}-arrow`),
"x-semi-prop": "arrowIcon"
}, arrowIcon)) : null;
};
this.renderClearBtn = () => {
const showClearBtn = this.showClearBtn();
const {
clearIcon
} = this.props;
const clearCls = cls(`${prefixcls}-clearbtn`);
if (showClearBtn) {
return /*#__PURE__*/React.createElement("div", {
role: 'button',
tabIndex: 0,
"aria-label": "Clear TreeSelect value",
className: clearCls,
onClick: this.handleClear,
onKeyPress: this.handleClearEnterPress
}, clearIcon ? clearIcon : /*#__PURE__*/React.createElement(IconClear, null));
}
return null;
};
this.renderSelection = () => {
const _a = this.props,
{
disabled,
multiple,
filterTreeNode,
validateStatus,
prefix,
suffix,
style,
size,
insetLabel,
className,
placeholder,
showClear,
leafOnly,
searchPosition,
triggerRender,
borderless,
autoMergeValue,
checkRelation
} = _a,
rest = __rest(_a, ["disabled", "multiple", "filterTreeNode", "validateStatus", "prefix", "suffix", "style", "size", "insetLabel", "className", "placeholder", "showClear", "leafOnly", "searchPosition", "triggerRender", "borderless", "autoMergeValue", "checkRelation"]);
const {
inputValue,
selectedKeys,
checkedKeys,
keyEntities,
isFocus,
realCheckedKeys
} = this.state;
const filterable = Boolean(filterTreeNode);
const useCustomTrigger = typeof triggerRender === 'function';
const mouseEvent = showClear ? {
onMouseEnter: e => this.handleMouseOver(e),
onMouseLeave: e => this.handleMouseLeave(e)
} : {};
const isTriggerPositionSearch = searchPosition === strings.SEARCH_POSITION_TRIGGER && filterable;
const isEmptyTriggerSearch = isTriggerPositionSearch && _isEmpty(checkedKeys);
const isValueTriggerSearch = isTriggerPositionSearch && !_isEmpty(checkedKeys);
const classNames = useCustomTrigger ? cls(className) : cls(prefixcls, {
[`${prefixcls}-borderless`]: borderless,
[`${prefixcls}-focus`]: isFocus,
[`${prefixcls}-disabled`]: disabled,
[`${prefixcls}-single`]: !multiple,
[`${prefixcls}-multiple`]: multiple,
[`${prefixcls}-multiple-tagInput-empty`]: multiple && isEmptyTriggerSearch,
[`${prefixcls}-multiple-tagInput-notEmpty`]: multiple && isValueTriggerSearch,
[`${prefixcls}-filterable`]: filterable,
[`${prefixcls}-error`]: validateStatus === 'error',
[`${prefixcls}-warning`]: validateStatus === 'warning',
[`${prefixcls}-small`]: size === 'small',
[`${prefixcls}-large`]: size === 'large',
[`${prefixcls}-with-prefix`]: prefix || insetLabel,
[`${prefixcls}-with-suffix`]: suffix,
[`${prefixcls}-with-suffix`]: suffix
}, className);
let inner;
let triggerRenderKeys = [];
if (multiple) {
if (!autoMergeValue) {
triggerRenderKeys = [...checkedKeys];
} else if (checkRelation === 'related') {
triggerRenderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly, true);
} else if (checkRelation === 'unRelated') {
triggerRenderKeys = [...realCheckedKeys];
}
} else {
triggerRenderKeys = selectedKeys;
}
if (useCustomTrigger) {
inner = /*#__PURE__*/React.createElement(Trigger, {
inputValue: inputValue,
value: triggerRenderKeys.map(key => _get(keyEntities, [key, 'data'])),
disabled: disabled,
placeholder: placeholder,
onClear: this.handleClear,
componentName: 'TreeSelect',
triggerRender: triggerRender,
componentProps: Object.assign({}, this.props),
onSearch: this.search,
onRemove: this.removeTag
});
} else {
inner = [/*#__PURE__*/React.createElement(Fragment, {
key: 'prefix'
}, prefix || insetLabel ? this.renderPrefix() : null), /*#__PURE__*/React.createElement(Fragment, {
key: 'selection'
}, /*#__PURE__*/React.createElement("div", {
className: `${prefixcls}-selection`
}, this.renderSelectContent(triggerRenderKeys))), /*#__PURE__*/React.createElement(Fragment, {
key: 'suffix'
}, suffix ? this.renderSuffix() : null), /*#__PURE__*/React.createElement(Fragment, {
key: 'clearBtn'
}, showClear || isTriggerPositionSearch && inputValue ? this.renderClearBtn() : null), /*#__PURE__*/React.createElement(Fragment, {
key: 'arrow'
}, this.renderArrow())];
}
const tabIndex = disabled ? null : 0;
/**
* Reasons for disabling the a11y eslint rule:
* The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here
*/
return /*#__PURE__*/React.createElement("div", Object.assign({
// eslint-disable-next-line jsx-a11y/role-has-required-aria-props
role: 'combobox',
"aria-disabled": disabled,
"aria-haspopup": "tree",
tabIndex: tabIndex,
className: classNames,
style: style,
ref: this.triggerRef,
onClick: this.handleClick,
onKeyPress: this.handleSelectionEnterPress,
onKeyDown: this.foundation.handleKeyDown,
"aria-invalid": this.props['aria-invalid'],
"aria-errormessage": this.props['aria-errormessage'],
"aria-label": this.props['aria-label'],
"aria-labelledby": this.props['aria-labelledby'],
"aria-describedby": this.props['aria-describedby'],
"aria-required": this.props['aria-required']
}, mouseEvent, this.getDataAttr(rest)), inner);
};
this.renderTagItem = (key, idx) => {
const {
keyEntities,
disabledKeys
} = this.state;
const {
size,
leafOnly,
disabled,
disableStrictly,
renderSelectedItem: propRenderSelectedItem,
treeNodeLabelProp,
keyMaps
} = this.props;
const realLabelName = _get(keyMaps, 'label', treeNodeLabelProp);
const keyList = normalizeKeyList([key], keyEntities, leafOnly, true);
const nodes = keyList.map(i => keyEntities[key] && keyEntities[key].key === key ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
const value = getValueOrKey(nodes, keyMaps);
const tagCls = cls(`${prefixcls}-selection-tag`, {
[`${prefixcls}-selection-tag-disabled`]: disabled
});
const nodeHaveData = !_isEmpty(nodes) && !_isEmpty(nodes[0]);
const isDisableStrictlyNode = disableStrictly && nodeHaveData && disabledKeys.has(nodes[0].key);
const closable = nodeHaveData && !nodes[0].disabled && !disabled && !isDisableStrictlyNode;
const onClose = (tagChildren, e) => {
// When value has not changed, prevent clicking tag closeBtn to close tag
e.preventDefault();
this.removeTag(key);
};
const tagProps = {
size: size === 'small' ? 'small' : 'large',
key: `tag-${value}-${idx}`,
color: 'white',
className: tagCls,
closable,
onClose
};
const item = nodes[0];
const renderSelectedItem = _isFunction(propRenderSelectedItem) ? propRenderSelectedItem : selectedItem => ({
isRenderInTag: true,
content: _get(selectedItem, realLabelName, null)
});
if (_isFunction(renderSelectedItem)) {
const {
content,
isRenderInTag
} = item ? renderSelectedItem(item, {
index: idx,
onClose
}) : {};
if (isRenderInTag) {
return /*#__PURE__*/React.createElement(Tag, Object.assign({}, tagProps), content);
} else {
return content;
}
}
return /*#__PURE__*/React.createElement(Tag, Object.assign({}, tagProps), value);
};
this.renderTagInput = triggerRenderKeys => {
const {
disabled,
size,
searchAutoFocus,
placeholder,
maxTagCount,
showRestTagsPopover,
restTagsPopoverProps,
searchPosition,
filterTreeNode,
preventScroll
} = this.props;
const {
inputValue
} = this.state;
// auto focus search input divide into two parts
// 1. filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER
// Implemented by passing autofocus to the underlying input's autofocus
// 2. filterTreeNode && searchPosition === strings.SEARCH_POSITION_DROPDOWN
// Due to the off-screen rendering in the tooltip implementation mechanism, if it is implemented through the
// autofocus of the input, when the option panel is opened, the page will scroll to top, so it is necessary
// to call the focus method through ref in the onVisibleChange callback of the Popover to achieve focus
const autoFocus = filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER ? searchAutoFocus : undefined;
return /*#__PURE__*/React.createElement(TagInput, {
maxTagCount: maxTagCount,
disabled: disabled,
onInputChange: v => this.search(v),
ref: this.tagInputRef,
placeholder: placeholder,
value: triggerRenderKeys,
inputValue: inputValue,
size: size,
showRestTagsPopover: showRestTagsPopover,
restTagsPopoverProps: restTagsPopoverProps,
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus: autoFocus,
renderTagItem: (itemKey, index) => this.renderTagItem(itemKey, index),
onRemove: itemKey => this.removeTag(itemKey),
expandRestTagsOnClick: false,
preventScroll: preventScroll
});
};
// render Tree
this.renderInput = () => {
const {
searchPlaceholder,
searchRender,
showSearchClear,
searchPosition,
searchAutoFocus,
multiple,
disabled,
preventScroll
} = this.props;
const {
inputValue,
inputTriggerFocus
} = this.state;
const isDropdownPositionSearch = searchPosition === strings.SEARCH_POSITION_DROPDOWN;
const inputcls = cls({
[`${prefixTree}-input`]: isDropdownPositionSearch,
[`${prefixcls}-inputTrigger`]: !isDropdownPositionSearch
});
const baseInputProps = {
value: inputValue,
className: inputcls,
preventScroll,
onChange: value => this.search(value)
};
const inputDropdownProps = {
showClear: showSearchClear,
prefix: /*#__PURE__*/React.createElement(IconSearch, null)
};
const inputTriggerProps = {
autofocus: searchAutoFocus,
onFocus: e => this.foundation.handleInputTriggerFocus(),
onBlur: e => this.foundation.handleInputTriggerBlur(),
disabled
};
const realInputProps = isDropdownPositionSearch ? inputDropdownProps : inputTriggerProps;
const wrapperCls = cls({
[`${prefixTree}-search-wrapper`]: isDropdownPositionSearch,
[`${prefixcls}-triggerSingleSearch-wrapper`]: !isDropdownPositionSearch && !multiple,
[`${prefixcls}-triggerSingleSearch-upper`]: !isDropdownPositionSearch && inputTriggerFocus
});
const useCusSearch = typeof searchRender === 'function' || typeof searchRender === 'boolean';
if (useCusSearch && !searchRender) {
return null;
}
return /*#__PURE__*/React.createElement("div", {
className: wrapperCls
}, /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "TreeSelect"
}, locale => {
const placeholder = isDropdownPositionSearch ? searchPlaceholder || locale.searchPlaceholder : '';
if (useCusSearch) {
return searchRender(Object.assign(Object.assign(Object.assign({}, realInputProps), baseInputProps), {
placeholder
}));
}
return /*#__PURE__*/React.createElement(Input, Object.assign({
"aria-label": 'Filter TreeSelect item',
ref: this.inputRef,
placeholder: placeholder
}, baseInputProps, realInputProps));
}));
};
this.renderEmpty = () => {
const {
emptyContent
} = this.props;
if (emptyContent === null) {
return null;
}
if (emptyContent) {
return /*#__PURE__*/React.createElement(TreeNode, {
empty: true,
emptyContent: this.props.emptyContent
});
} else {
return /*#__PURE__*/React.createElement(LocaleConsumer, {
componentName: "Tree"
}, locale => /*#__PURE__*/React.createElement(TreeNode, {
empty: true,
emptyContent: locale.emptyText
}));
}
};
this.onNodeLoad = data => new Promise(resolve => this.foundation.setLoadKeys(data, resolve));
this.onNodeSelect = (e, treeNode) => {
this.foundation.handleNodeSelect(e, treeNode);
};
this.onNodeCheck = (e, treeNode) => {
this.foundation.handleNodeSelect(e, treeNode);
};
this.onNodeExpand = (e, treeNode) => {
this.foundation.handleNodeExpand(e, treeNode);
};
this.getTreeNodeRequiredProps = () => {
const {
expandedKeys,
selectedKeys,
checkedKeys,
halfCheckedKeys,
keyEntities,
filteredKeys
} = this.state;
return {
expandedKeys: expandedKeys || new Set(),
selectedKeys: selectedKeys || [],
checkedKeys: checkedKeys || new Set(),
halfCheckedKeys: halfCheckedKeys || new Set(),
filteredKeys: filteredKeys || new Set(),
keyEntities
};
};
this.getTreeNodeKey = treeNode => {
const {
data
} = treeNode;
const {
key
} = data;
return key;
};
/* Event handler function after popover visible change */
this.handlePopoverVisibleChange = isVisible => {
this.foundation.handlePopoverVisibleChange(isVisible);
};
this.afterClose = () => {
this.foundation.handleAfterClose();
};
this.renderTreeNode = (treeNode, ind, style) => {
const {
data,
key
} = treeNode;
const treeNodeProps = this.foundation.getTreeNodeProps(key);
const {
showLine
} = this.props;
if (!treeNodeProps) {
return null;
}
const props = _pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
const {
keyMaps,
expandIcon
} = this.props;
const children = data[_get(keyMaps, 'children', 'children')];
!_isUndefined(children) && (props.children = children);
return /*#__PURE__*/React.createElement(TreeNode, Object.assign({}, treeNodeProps, data, props, {
data: data,
style: style,
showLine: showLine,
expandIcon: expandIcon
}));
};
this.itemKey = (index, data) => {
const {
visibleOptions
} = data;
// Find the item at the specified index.
const item = visibleOptions[index];
// Return a value that uniquely identifies this item.
return item.key;
};
this.renderNodeList = () => {
const {
flattenNodes,
cachedFlattenNodes,
motionKeys,
motionType,
filteredKeys
} = this.state;
const {
direction
} = this.context;
const {
virtualize,
motionExpand
} = this.props;
const isExpandControlled = 'expandedKeys' in this.props;
if (!virtualize || _isEmpty(virtualize)) {
return /*#__PURE__*/React.createElement(NodeList, {
flattenNodes: flattenNodes,
flattenList: cachedFlattenNodes,
motionKeys: motionExpand ? motionKeys : new Set([]),
motionType: motionType,
// When motionKeys is empty, but filteredKeys is not empty (that is, the search hits), this situation should be distinguished from ordinary motionKeys
searchTargetIsDeep: isExpandControlled && motionExpand && _isEmpty(motionKeys) && !_isEmpty(filteredKeys),
onMotionEnd: this.onMotionEnd,
renderTreeNode: this.renderTreeNode
});
}
const data = {
visibleOptions: flattenNodes,
renderOption: this.renderTreeNode
};
return /*#__PURE__*/React.createElement(AutoSizer, {
defaultHeight: virtualize.height,
defaultWidth: virtualize.width
}, _ref => {
let {
height,
width
} = _ref;
return /*#__PURE__*/React.createElement(VirtualList, {
itemCount: flattenNodes.length,
itemSize: virtualize.itemSize,
height: height,
width: width,
// @ts-ignore avoid strict check of itemKey
itemKey: this.itemKey,
itemData: data,
className: `${prefixTree}-virtual-list`,
style: {
direction
}
}, VirtualRow);
});
};
this.renderTree = () => {
const {
keyEntities,
motionKeys,
motionType,
inputValue,
filteredKeys,
flattenNodes,
checkedKeys,
realCheckedKeys
} = this.state;
const {
loadData,
filterTreeNode,
disabled,
multiple,
showFilteredOnly,
motionExpand,
outerBottomSlot,
outerTopSlot,
expandAction,
labelEllipsis,
virtualize,
optionListStyle,
searchPosition,
renderLabel,
renderFullLabel,
checkRelation,
emptyContent
} = this.props;
const wrapperCls = cls(`${prefixTree}-wrapper`);
const searchNoRes = Boolean(inputValue) && !filteredKeys.size;
const noData = _isEmpty(flattenNodes) || showFilteredOnly && searchNoRes;
const isDropdownPositionSearch = searchPosition === strings.SEARCH_POSITION_DROPDOWN;
const listCls = cls(`${prefixTree}-option-list ${prefixTree}-option-list-block`, {
[`${prefixTree}-option-list-hidden`]: emptyContent === null && noData
});
return /*#__PURE__*/React.createElement(TreeContext.Provider, {
value: {
loadData,
treeDisabled: disabled,
motion: motionExpand,
motionKeys,
motionType,
expandAction,
filterTreeNode,
keyEntities,
onNodeClick: this.onNodeClick,
onNodeDoubleClick: this.onNodeDoubleClick,
// tree node will call this function when treeNode is right clicked
onNodeRightClick: _noop,
onNodeExpand: this.onNodeExpand,
onNodeSelect: this.onNodeSelect,
onNodeCheck: this.onNodeCheck,
renderTreeNode: this.renderTreeNode,
multiple,
showFilteredOnly,
isSearching: Boolean(inputValue),
renderLabel,
renderFullLabel,
labelEllipsis: typeof labelEllipsis === 'undefined' ? virtualize : labelEllipsis
}
}, /*#__PURE__*/React.createElement("div", {
className: wrapperCls
}, outerTopSlot, !outerTopSlot && filterTreeNode && isDropdownPositionSearch && this.renderInput(), /*#__PURE__*/React.createElement("div", {
className: listCls,
role: "tree",
"aria-multiselectable": multiple ? true : false,
style: optionListStyle
}, noData ? this.renderEmpty() : multiple ? (/*#__PURE__*/React.createElement(CheckboxGroup, {
value: Array.from(checkRelation === 'related' ? checkedKeys : realCheckedKeys)
}, this.renderNodeList())) : this.renderNodeList()), outerBottomSlot));
};
this.state = {
inputTriggerFocus: false,
isOpen: false,
isFocus: false,
// isInput: false,
rePosKey: key,
dropdownMinWidth: null,
inputValue: '',
keyEntities: {},
treeData: [],
flattenNodes: [],
cachedFlattenNodes: undefined,
selectedKeys: [],
checkedKeys: new Set(),
halfCheckedKeys: new Set(),
realCheckedKeys: new Set([]),
disabledKeys: new Set(),
motionKeys: new Set([]),
motionType: 'hide',
expandedKeys: new Set(props.expandedKeys),
filteredKeys: new Set(),
filteredExpandedKeys: new Set(),
filteredShownKeys: new Set(),
prevProps: null,
isHovering: false,
cachedKeyValuePairs: {},
loadedKeys: new Set(),
loadingKeys: new Set()
};
this.inputRef = /*#__PURE__*/React.createRef();
this.tagInputRef = /*#__PURE__*/React.createRef();
this.triggerRef = /*#__PURE__*/React.createRef();
this.optionsRef = /*#__PURE__*/React.createRef();
this.clickOutsideHandler = null;
this.foundation = new TreeSelectFoundation(this.adapter);
this.treeSelectID = Math.random().toString(36).slice(2);
this.onMotionEnd = () => {
this.adapter.rePositionDropdown();
};
}
static getDerivedStateFromProps(props, prevState) {
const {
prevProps,
rePosKey
} = prevState;
const {
keyMaps
} = props;
const needUpdate = name => !prevProps && name in props || prevProps && !_isEqual(prevProps[name], props[name]);
let treeData;
const withObject = props.onChangeWithObject;
let keyEntities = prevState.keyEntities || {};
let valueEntities = prevState.cachedKeyValuePairs || {};
const newState = {
prevProps: props
};
const needUpdateTreeData = needUpdate('treeData');
const needUpdateExpandedKeys = needUpdate('expandedKeys');
const isExpandControlled = 'expandedKeys' in props;
const isSearching = Boolean(props.filterTreeNode && prevState.inputValue && prevState.inputValue.length);
// TreeNode
if (needUpdateTreeData) {
treeData = props.treeData;
newState.treeData = treeData;
const entitiesMap = convertDataToEntities(treeData, keyMaps);
newState.keyEntities = Object.assign({}, entitiesMap.keyEntities);
keyEntities = newState.keyEntities;
newState.cachedKeyValuePairs = Object.assign({}, entitiesMap.valueEntities);
valueEntities = newState.cachedKeyValuePairs;
}
// if treeData keys changes, we won't show animation
if (treeData && props.motion && !_isEqual(Object.keys(newState.keyEntities), Object.keys(prevState.keyEntities))) {
if (prevProps && props.motion) {
newState.motionKeys = new Set([]);
newState.motionType = null;
}
}
const expandAllWhenDataChange = needUpdateTreeData && props.expandAll;
if (!isSearching) {
// expandedKeys
if (needUpdateExpandedKeys || prevProps && needUpdate('autoExpandParent')) {
newState.expandedKeys = calcExpandedKeys(props.expandedKeys, keyEntities, props.autoExpandParent || !prevProps);
// only show animation when treeData does not change
if (prevProps && props.motion && !treeData) {
const {
motionKeys,
motionType
} = calcMotionKeys(prevState.expandedKeys, newState.expandedKeys, keyEntities);
newState.motionKeys = new Set(motionKeys);
newState.motionType = motionType;
if (motionType === 'hide') {
// cache flatten nodes: expandedKeys changed may not be triggered by interaction
newState.cachedFlattenNodes = cloneDeep(prevState.flattenNodes);
}
}
} else if (!prevProps && (props.defaultExpandAll || props.expandAll) || expandAllWhenDataChange) {
newState.expandedKeys = new Set(Object.keys(keyEntities));
} else if (!prevProps && props.defaultExpandedKeys) {
newState.expandedKeys = calcExpandedKeys(props.defaultExpandedKeys, keyEntities);
} else if (!prevProps && props.defaultValue) {
newState.expandedKeys = calcExpandedKeysForValues(normalizeValue(props.defaultValue, withObject, keyMaps), keyEntities, props.multiple, valueEntities);
} else if (!prevProps && props.value) {
newState.expandedKeys = calcExpandedKeysForValues(normalizeValue(props.value, withObject, keyMaps), keyEntities, props.multiple, valueEntities);
}
if (!newState.expandedKeys) {
delete newState.expandedKeys;
}
if (treeData || newState.expandedKeys) {
const flattenNodes = flattenTreeData(treeData || prevState.treeData, newState.expandedKeys || prevState.expandedKeys, keyMaps);
newState.flattenNodes = flattenNodes;
}
} else {
let filteredState;
// treeData changed while searching
if (treeData) {
// Get filter data
filteredState = filterTreeData({
treeData,
inputValue: prevState.inputValue,
filterTreeNode: props.filterTreeNode,
filterProps: props.treeNodeFilterProp,
showFilteredOnly: props.showFilteredOnly,
keyEntities: newState.keyEntities,
prevExpandedKeys: [...prevState.filteredExpandedKeys],
keyMaps: keyMaps
});
newState.flattenNodes = filteredState.flattenNodes;
newState.motionKeys = new Set([]);
newState.filteredKeys = filteredState.filteredKeys;
newState.filteredShownKeys = filteredState.filteredShownKeys;
newState.filteredExpandedKeys = filteredState.filteredExpandedKeys;
}
// expandedKeys changed while searching
if (props.expandedKeys) {
newState.filteredExpandedKeys = calcExpandedKeys(props.expandedKeys, keyEntities, props.autoExpandParent || !prevProps);
if (prevProps && props.motion) {
const prevKeys = prevState ? prevState.filteredExpandedKeys : new Set([]);
// only show animation when treeData does not change
if (!treeData) {
const motionResult = calcMotionKeys(prevKeys, newState.filteredExpandedKeys, keyEntities);
let {
motionKeys
} = motionResult;
const {
motionType
} = motionResult;
if (props.showFilteredOnly) {
motionKeys = motionKeys.filter(key => prevState.filteredShownKeys.has(key));
}
if (motionType === 'hide') {
// cache flatten nodes: expandedKeys changed may not be triggered by interaction
newState.cachedFlattenNodes = cloneDeep(prevState.flattenNodes);
}
newState.motionKeys = new Set(motionKeys);
newState.motionType = motionType;
}
}
newState.flattenNodes = flattenTreeData(treeData || prevState.treeData, newState.filteredExpandedKeys || prevState.filteredExpandedKeys, keyMaps, props.showFilteredOnly && prevState.filteredShownKeys);
}
}
// selectedKeys: single mode controlled
const isMultiple = props.multiple;
if (!isMultiple) {
if (needUpdate('value')) {
newState.selectedKeys = findKeysForValues(normalizeValue(props.value, withObject, keyMaps), valueEntities, isMultiple);
} else if (!prevProps && props.defaultValue) {
newState.selectedKeys = findKeysForValues(normalizeValue(props.defaultValue, withObject, keyMaps), valueEntities, isMultiple);
} else if (treeData) {
// If `treeData` changed, we also need check it
if (props.value) {
newState.selectedKeys = findKeysForValues(normalizeValue(props.value, withObject, keyMaps) || '', valueEntities, isMultiple);
} else {
newState.selectedKeys = updateKeys(prevState.selectedKeys, keyEntities);
}
}
} else {
// checkedKeys: multiple mode controlled || data changed
let checkedKeyValues;
if (needUpdate('value')) {
checkedKeyValues = findKeysForValues(normalizeValue(props.value, withObject, keyMaps), valueEntities, isMultiple);
} else if (!prevProps && props.defaultValue) {
checkedKeyValues = findKeysForValues(normalizeValue(props.defaultValue, withObject, keyMaps), valueEntities, isMultiple);
} else if (treeData) {
// If `treeData` changed, we also need check it
if (props.value) {
checkedKeyValues = findKeysForValues(normalizeValue(props.value, withObject, keyMaps) || [], valueEntities, isMultiple);
} else {
checkedKeyValues = updateKeys(props.checkRelation === 'related' ? prevState.checkedKeys : prevState.realCheckedKeys, keyEntities);
}
}
if (checkedKeyValues) {
if (props.checkRelation === 'unRelated') {
newState.realCheckedKeys = new Set(checkedKeyValues);
} else if (props.checkRelation === 'related') {
const {
checkedKeys,
halfCheckedKeys
} = calcCheckedKeys(checkedKeyValues, keyEntities);
newState.checkedKeys = checkedKeys;
newState.halfCheckedKeys = halfCheckedKeys;
}
}
}
// loadedKeys
if (needUpdate('loadedKeys')) {
newState.loadedKeys = new Set(props.loadedKeys);
}
// ================== rePosKey ==================
if (needUpdateTreeData || needUpdate('value')) {
newState.rePosKey = rePosKey + 1;
}
// ================ disableStrictly =================
if (treeData && props.disableStrictly && props.checkRelation === 'related') {
newState.disabledKeys = calcDisabledKeys(keyEntities, keyMaps);
}
return newState;
}
get adapter() {
var _this = this;
const filterAdapter = {
updateInputValue: value => {
this.setState({
inputValue: value
});
}
};
const treeSelectAdapter = {
registerClickOutsideHandler: cb => {
this.adapter.unregisterClickOutsideHandler();
const clickOutsideHandler = e => {
const optionInstance = this.optionsRef && this.optionsRef.current;
const triggerDom = this.triggerRef && this.triggerRef.current;
const optionsDom = ReactDOM.findDOMNode(optionInstance);
const target = e.target;
const path = e.composedPath && e.composedPath() || [target];
if (optionsDom && (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) && triggerDom && !triggerDom.contains(target) && !(path.includes(triggerDom) || path.includes(optionsDom))) {
cb(e);
}
};
this.clickOutsideHandler = clickOutsideHandler;
document.addEventListener('mousedown', clickOutsideHandler, false);
},
unregisterClickOutsideHandler: () => {
if (!this.clickOutsideHandler) {
return;
}
document.removeEventListener('mousedown', this.clickOutsideHandler, false);
this.clickOutsideHandler = null;
},
rePositionDropdown: () => {
let {
rePosKey
} = this.state;
rePosKey = rePosKey + 1;
this.setState({
rePosKey
});
}
};
const treeAdapter = {
updateState: states => {
this.setState(Object.assign({}, states));
},
notifySelect: (selectKey, bool, node) => {
this.props.onSelect && this.props.onSelect(selectKey, bool, node);
},
notifySearch: (input, filteredExpandedKeys, filteredNodes) => {
this.props.onSearch && this.props.onSearch(input, filteredExpandedKeys, filteredNodes);
},
cacheFlattenNodes: bool => {
this.setState({
cachedFlattenNodes: bool ? cloneDeep(this.state.flattenNodes) : undefined
});
},
notifyLoad: (newLoadedKeys, data) => {
const {
onLoad
} = this.props;
_isFunction(onLoad) && onLoad(newLoadedKeys, data);
},
notifyClear: e => {
this.props.onClear && this.props.onClear(e);
}
};
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, super.adapter), filterAdapter), treeSelectAdapter), treeAdapter), {
updateLoadKeys: (data, resolve) => {
this.setState(_ref2 => {
let {
loadedKeys,
loadingKeys
} = _ref2;
return this.foundation.handleNodeLoad(loadedKeys, loadingKeys, data, resolve);
});
},
updateState: states => {
this.setState(Object.assign({}, states));
},
openMenu: () => {
this.setState({
isOpen: true
}, () => {
this.props.onVisibleChange(true);
});
},
closeMenu: cb => {
this.setState({
isOpen: false
}, () => {
cb && cb();
this.props.onVisibleChange(false);
});
},
getTriggerWidth: () => {
const el = this.triggerRef.current;
return el && el.getBoundingClientRect().width;
},
setOptionWrapperWidth: width => {
this.setState({
dropdownMinWidth: width
});
},
notifyChange: (value, node, e) => {
this.props.onChange && this.props.onChange(value, node, e);
},
notifyChangeWithObject: (node, e) => {
this.props.onChange && this.props.onChange(node, e);
},
notifyExpand: (expandedKeys, _ref3) => {
let {
expanded: bool,
node
} = _ref3;
this.props.onExpand && this.props.onExpand([...expandedKeys], {
expanded: bool,
node
});
if (bool && this.props.loadData) {
this.onNodeLoad(node);
}
},
notifyFocus: function () {
_this.props.onFocus && _this.props.onFocus(...arguments);
},
notifyBlur: function () {
_this.props.onBlur && _this.props.onBlur(...arguments);
},
toggleHovering: bool => {
this.setState({
isHovering: bool
});
},
updateInputFocus: bool => {
if (bool) {
if (this.inputRef && this.inputRef.current) {
const {
preventScroll
} = this.props;
this.inputRef.current.focus({
preventScroll
});
}
if (this.tagInputRef && this.tagInputRef.current) {
this.tagInputRef.current.focus();
}
} else {
if (this.inputRef && this.inputRef.current) {
this.inputRef.current.blur();
}
if (this.tagInputRef && this.tagInputRef.current) {
this.tagInputRef.current.blur();
}
}
},
updateIsFocus: bool => {
this.setState({
isFocus: bool
});
}
});
}
componentDidMount() {
this.foundation.init();
}
componentWillUnmount() {
this.foundation.destroy();
}
render() {
const content = this.renderContent();
const {
motion,
zIndex,
mouseLeaveDelay,
mouseEnterDelay,
autoAdjustOverflow,
stopPropagation,
getPopupContainer,
dropdownMargin,
position
} = this.props;
const {
isOpen,
rePosKey
} = this.state;
const selection = this.renderSelection();
const pos = position ? position : 'bottomLeft';
return /*#__PURE__*/React.createElement(Popover, {
stopPropagation: stopPropagation,
getPopupContainer: getPopupContainer,
zIndex: zIndex,
motion: motion,
margin: dropdownMargin,
ref: this.optionsRef,
content: content,
visible: isOpen,
trigger: "custom",
rePosKey: rePosKey,
position: pos,
autoAdjustOverflow: autoAdjustOverflow,
mouseLeaveDelay: mouseLeaveDelay,
mouseEnterDelay: mouseEnterDelay,
onVisibleChange: this.handlePopoverVisibleChange,
afterClose: this.afterClose
}, selection);
}
}
TreeSelect.contextType = ConfigContext;
TreeSelect.propTypes = {
'aria-describedby': PropTypes.string,
'aria-errormessage': PropTypes.string,
'aria-invalid': PropTypes.bool,
'aria-labelledby': PropTypes.string,
'aria-required': PropTypes.bool,
borderless: PropTypes.bool,
loadedKeys: PropTypes.arrayOf(PropTypes.string),
loadData: PropTypes.func,
onLoad: PropTypes.func,
arrowIcon: PropTypes.node,
clearIcon: PropTypes.node,
defaultOpen: PropTypes.bool,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]),
defaultExpandAll: PropTypes.bool,
defaultExpandedKeys: PropTypes.array,
expandAll: PropTypes.bool,
disabled: PropTypes.bool,
disableStrictly: PropTypes.bool,
// Whether to turn on the input box filtering function, when it is a function, it represents a custom filtering function
filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
multiple: PropTypes.bool,
searchPlaceholder: PropTypes.string,
searchAutoFocus: PropTypes.bool,
virtualize: PropTypes.object,
treeNodeFilterProp: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onSearch: PropTypes.func,
onSelect: PropTypes.func,
onExpand: PropTypes.func,
onChangeWithObject: PropTypes.bool,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
expandedKeys: PropTypes.array,
autoExpandParent: PropTypes.bool,
showClear: PropTypes.bool,
showSearchClear: PropTypes.bool,
autoAdjustOverflow: PropTypes.bool,
showFilteredOnly: PropTypes.bool,
showLine: PropTypes.bool,
motionExpand: PropTypes.bool,
emptyContent: PropTypes.node,
keyMaps: PropTypes.object,
leafOnly: PropTypes.bool,
treeData: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.any
})),
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
dropdownMargin: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
motion: PropTypes.bool,
placeholder: PropTypes.string,
maxTagCount: PropTypes.number,
size: PropTypes.oneOf(strings.SIZE_SET),
className: PropTypes.string,
style: PropTypes.object,
treeNodeLabelProp: PropTypes.string,
suffix: PropTypes.node,
prefix: PropTypes.node,
insetLabel: PropTypes.node,
insetLabelId: PropTypes.string,
zIndex: PropTypes.number,
getPopupContainer: PropTypes.func,
dropdownMatchSelectWidth: PropTypes.bool,
validateStatus: PropTypes.oneOf(strings.STATUS),
mouseEnterDelay: Prop