UNPKG

@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
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