index.tsx 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. import React, { MouseEvent, KeyboardEvent } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import ConfigContext, { ContextValue } from '../configProvider/context';
  5. import TreeFoundation, { TreeAdapter } from '@douyinfe/semi-foundation/tree/foundation';
  6. import {
  7. convertDataToEntities,
  8. flattenTreeData,
  9. calcExpandedKeysForValues,
  10. calcMotionKeys,
  11. convertJsonToData,
  12. findKeysForValues,
  13. calcCheckedKeys,
  14. calcExpandedKeys,
  15. filterTreeData,
  16. normalizeValue,
  17. updateKeys,
  18. calcDisabledKeys
  19. } from '@douyinfe/semi-foundation/tree/treeUtil';
  20. import { cssClasses, strings } from '@douyinfe/semi-foundation/tree/constants';
  21. import BaseComponent from '../_base/baseComponent';
  22. import { isEmpty, isEqual, get, isFunction, pick, isUndefined } from 'lodash';
  23. import { cloneDeep } from './treeUtil';
  24. import Input from '../input/index';
  25. import { FixedSizeList as VirtualList } from 'react-window';
  26. import AutoSizer from './autoSizer';
  27. import TreeContext from './treeContext';
  28. import TreeNode from './treeNode';
  29. import NodeList from './nodeList';
  30. import LocaleConsumer from '../locale/localeConsumer';
  31. import '@douyinfe/semi-foundation/tree/tree.scss';
  32. import { IconSearch } from '@douyinfe/semi-icons';
  33. import { Locale as LocaleObject } from '../locale/interface';
  34. import {
  35. TreeProps,
  36. TreeState,
  37. TreeNodeProps,
  38. TreeNodeData,
  39. FlattenNode,
  40. KeyEntity,
  41. OptionProps,
  42. ScrollData,
  43. } from './interface';
  44. import CheckboxGroup from '../checkbox/checkboxGroup';
  45. export * from './interface';
  46. export type { AutoSizerProps } from './autoSizer';
  47. const prefixcls = cssClasses.PREFIX;
  48. class Tree extends BaseComponent<TreeProps, TreeState> {
  49. static contextType = ConfigContext;
  50. static propTypes = {
  51. autoMergeValue: PropTypes.bool,
  52. blockNode: PropTypes.bool,
  53. className: PropTypes.string,
  54. showClear: PropTypes.bool,
  55. defaultExpandAll: PropTypes.bool,
  56. defaultExpandedKeys: PropTypes.array,
  57. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  58. directory: PropTypes.bool,
  59. disabled: PropTypes.bool,
  60. emptyContent: PropTypes.node,
  61. expandAll: PropTypes.bool,
  62. expandedKeys: PropTypes.array,
  63. filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  64. icon: PropTypes.node,
  65. onChangeWithObject: PropTypes.bool,
  66. motion: PropTypes.bool,
  67. multiple: PropTypes.bool,
  68. onChange: PropTypes.func,
  69. onExpand: PropTypes.func,
  70. onSearch: PropTypes.func,
  71. onSelect: PropTypes.func,
  72. onContextMenu: PropTypes.func,
  73. onDoubleClick: PropTypes.func,
  74. searchClassName: PropTypes.string,
  75. searchPlaceholder: PropTypes.string,
  76. searchStyle: PropTypes.object,
  77. selectedKey: PropTypes.string,
  78. showFilteredOnly: PropTypes.bool,
  79. showLine: PropTypes.bool,
  80. style: PropTypes.object,
  81. treeData: PropTypes.arrayOf(
  82. PropTypes.shape({
  83. key: PropTypes.string,
  84. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  85. label: PropTypes.any,
  86. isLeaf: PropTypes.bool,
  87. })
  88. ),
  89. keyMaps: PropTypes.object,
  90. treeDataSimpleJson: PropTypes.object,
  91. treeNodeFilterProp: PropTypes.string,
  92. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
  93. virtualize: PropTypes.object,
  94. autoExpandParent: PropTypes.bool,
  95. expandAction: PropTypes.oneOf(strings.EXPAND_ACTION),
  96. searchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  97. renderLabel: PropTypes.func,
  98. renderFullLabel: PropTypes.func,
  99. leafOnly: PropTypes.bool,
  100. loadedKeys: PropTypes.array,
  101. loadData: PropTypes.func,
  102. onLoad: PropTypes.func,
  103. disableStrictly: PropTypes.bool,
  104. draggable: PropTypes.bool,
  105. autoExpandWhenDragEnter: PropTypes.bool,
  106. hideDraggingNode: PropTypes.bool,
  107. renderDraggingNode: PropTypes.func,
  108. onDragEnd: PropTypes.func,
  109. onDragEnter: PropTypes.func,
  110. onDragLeave: PropTypes.func,
  111. onDragOver: PropTypes.func,
  112. onDragStart: PropTypes.func,
  113. onDrop: PropTypes.func,
  114. labelEllipsis: PropTypes.bool,
  115. checkRelation: PropTypes.string,
  116. 'aria-label': PropTypes.string,
  117. preventScroll: PropTypes.bool,
  118. };
  119. static defaultProps = {
  120. showClear: true,
  121. disabled: false,
  122. blockNode: true,
  123. multiple: false,
  124. filterTreeNode: false,
  125. autoExpandParent: false,
  126. treeNodeFilterProp: 'label',
  127. defaultExpandAll: false,
  128. expandAll: false,
  129. onChangeWithObject: false,
  130. motion: true,
  131. leafOnly: false,
  132. showFilteredOnly: false,
  133. showLine: false,
  134. expandAction: false,
  135. disableStrictly: false,
  136. draggable: false,
  137. autoExpandWhenDragEnter: true,
  138. checkRelation: 'related',
  139. autoMergeValue: true,
  140. };
  141. static TreeNode: typeof TreeNode;
  142. inputRef: React.RefObject<typeof Input>;
  143. optionsRef: React.RefObject<any>;
  144. dragNode: any;
  145. onNodeClick: any;
  146. onMotionEnd: any;
  147. context: ContextValue;
  148. virtualizedListRef: React.RefObject<any>;
  149. constructor(props: TreeProps) {
  150. super(props);
  151. this.state = {
  152. inputValue: '',
  153. keyEntities: {},
  154. treeData: [],
  155. flattenNodes: [],
  156. selectedKeys: [],
  157. checkedKeys: new Set(),
  158. halfCheckedKeys: new Set(),
  159. realCheckedKeys: new Set([]),
  160. motionKeys: new Set([]),
  161. motionType: 'hide',
  162. expandedKeys: new Set(props.expandedKeys),
  163. filteredKeys: new Set(),
  164. filteredExpandedKeys: new Set(),
  165. filteredShownKeys: new Set(),
  166. prevProps: null,
  167. loadedKeys: new Set(),
  168. loadingKeys: new Set(),
  169. cachedFlattenNodes: undefined,
  170. cachedKeyValuePairs: {},
  171. disabledKeys: new Set(),
  172. dragging: false,
  173. dragNodesKeys: new Set(),
  174. dragOverNodeKey: null,
  175. dropPosition: null,
  176. };
  177. this.inputRef = React.createRef();
  178. this.optionsRef = React.createRef();
  179. this.foundation = new TreeFoundation(this.adapter);
  180. this.dragNode = null;
  181. this.virtualizedListRef = React.createRef();
  182. }
  183. /**
  184. * Process of getDerivedStateFromProps was inspired by rc-tree
  185. * https://github.com/react-component/tree
  186. */
  187. static getDerivedStateFromProps(props: TreeProps, prevState: TreeState) {
  188. const { prevProps } = prevState;
  189. const { keyMaps } = props;
  190. let treeData;
  191. let keyEntities = prevState.keyEntities || {};
  192. let valueEntities = prevState.cachedKeyValuePairs || {};
  193. const isSeaching = Boolean(props.filterTreeNode && prevState.inputValue && prevState.inputValue.length);
  194. const newState: Partial<TreeState> = {
  195. prevProps: props,
  196. };
  197. const isExpandControlled = 'expandedKeys' in props;
  198. // Accept a props field as a parameter to determine whether to update the field
  199. const needUpdate = (name: string) => {
  200. const firstInProps = !prevProps && name in props;
  201. const nameHasChange = prevProps && !isEqual(prevProps[name], props[name]);
  202. return firstInProps || nameHasChange;
  203. };
  204. // Determine whether treeData has changed
  205. const needUpdateData = () => {
  206. const firstInProps = !prevProps && 'treeData' in props;
  207. const treeDataHasChange = prevProps && prevProps.treeData !== props.treeData;
  208. return firstInProps || treeDataHasChange;
  209. };
  210. const needUpdateTreeData = needUpdate('treeData');
  211. const needUpdateSimpleJson = needUpdate('treeDataSimpleJson');
  212. // Update the data of tree in state
  213. if (needUpdateTreeData || (props.draggable && needUpdateData())) {
  214. treeData = props.treeData;
  215. newState.treeData = treeData;
  216. const entitiesMap = convertDataToEntities(treeData, keyMaps);
  217. newState.keyEntities = {
  218. ...entitiesMap.keyEntities,
  219. };
  220. keyEntities = newState.keyEntities;
  221. newState.cachedKeyValuePairs = { ...entitiesMap.valueEntities };
  222. valueEntities = newState.cachedKeyValuePairs;
  223. } else if (needUpdateSimpleJson) {
  224. // Convert treeDataSimpleJson to treeData
  225. treeData = convertJsonToData(props.treeDataSimpleJson);
  226. newState.treeData = treeData;
  227. const entitiesMap = convertDataToEntities(treeData, keyMaps);
  228. newState.keyEntities = {
  229. ...entitiesMap.keyEntities,
  230. };
  231. keyEntities = newState.keyEntities;
  232. newState.cachedKeyValuePairs = { ...entitiesMap.valueEntities };
  233. valueEntities = newState.cachedKeyValuePairs;
  234. }
  235. // If treeData keys changes, we won't show animation
  236. if (treeData && props.motion) {
  237. if (prevProps && props.motion) {
  238. newState.motionKeys = new Set([]);
  239. newState.motionType = null;
  240. }
  241. }
  242. const dataUpdated = needUpdateSimpleJson || needUpdateTreeData;
  243. const expandAllWhenDataChange = dataUpdated && props.expandAll;
  244. if (!isSeaching) {
  245. // Update expandedKeys
  246. if (needUpdate('expandedKeys') || (prevProps && needUpdate('autoExpandParent'))) {
  247. newState.expandedKeys = calcExpandedKeys(
  248. props.expandedKeys,
  249. keyEntities,
  250. props.autoExpandParent || !prevProps
  251. );
  252. // only show animation when treeData does not change
  253. if (prevProps && props.motion && !treeData) {
  254. const { motionKeys, motionType } = calcMotionKeys(
  255. prevState.expandedKeys,
  256. newState.expandedKeys,
  257. keyEntities
  258. );
  259. newState.motionKeys = new Set(motionKeys);
  260. newState.motionType = motionType;
  261. if (motionType === 'hide') {
  262. // cache flatten nodes: expandedKeys changed may not be triggered by interaction
  263. newState.cachedFlattenNodes = cloneDeep(prevState.flattenNodes);
  264. }
  265. }
  266. } else if ((!prevProps && (props.defaultExpandAll || props.expandAll)) || expandAllWhenDataChange) {
  267. newState.expandedKeys = new Set(Object.keys(keyEntities));
  268. } else if (!prevProps && props.defaultExpandedKeys) {
  269. newState.expandedKeys = calcExpandedKeys(props.defaultExpandedKeys, keyEntities);
  270. } else if (!prevProps && props.defaultValue) {
  271. newState.expandedKeys = calcExpandedKeysForValues(
  272. props.defaultValue,
  273. keyEntities,
  274. props.multiple,
  275. valueEntities
  276. );
  277. } else if (!prevProps && props.value) {
  278. newState.expandedKeys = calcExpandedKeysForValues(
  279. props.value,
  280. keyEntities,
  281. props.multiple,
  282. valueEntities
  283. );
  284. } else if ((!isExpandControlled && dataUpdated) && props.value) {
  285. // 当 treeData 已经设置具体的值,并且设置了 props.loadData ,则认为 treeData 的更新是因为 loadData 导致的
  286. // 如果是因为 loadData 导致 treeData改变, 此时在这里重新计算 key 会导致为未选中的展开项目被收起
  287. // 所以此时不需要重新计算 expandedKeys,因为在点击展开按钮时候已经把被展开的项添加到 expandedKeys 中
  288. // When treeData has a specific value and props.loadData is set, it is considered that the update of treeData is caused by loadData
  289. // If the treeData is changed because of loadData, recalculating the key here will cause the unselected expanded items to be collapsed
  290. // So there is no need to recalculate expandedKeys at this time, because the expanded item has been added to expandedKeys when the expand button is clicked
  291. if (!(prevState.treeData && prevState.treeData?.length > 0 && props.loadData)) {
  292. newState.expandedKeys = calcExpandedKeysForValues(
  293. props.value,
  294. keyEntities,
  295. props.multiple,
  296. valueEntities
  297. );
  298. }
  299. }
  300. if (!newState.expandedKeys) {
  301. delete newState.expandedKeys;
  302. }
  303. // Update flattenNodes
  304. if (treeData || newState.expandedKeys) {
  305. const flattenNodes = flattenTreeData(
  306. treeData || prevState.treeData,
  307. newState.expandedKeys || prevState.expandedKeys,
  308. keyMaps
  309. );
  310. newState.flattenNodes = flattenNodes;
  311. }
  312. } else {
  313. let filteredState;
  314. // treeData changed while searching
  315. if (treeData) {
  316. // Get filter data
  317. filteredState = filterTreeData({
  318. treeData,
  319. inputValue: prevState.inputValue,
  320. filterTreeNode: props.filterTreeNode,
  321. filterProps: props.treeNodeFilterProp,
  322. showFilteredOnly: props.showFilteredOnly,
  323. keyEntities: newState.keyEntities,
  324. prevExpandedKeys: [...prevState.filteredExpandedKeys],
  325. keyMaps: keyMaps
  326. });
  327. newState.flattenNodes = filteredState.flattenNodes;
  328. newState.motionKeys = new Set([]);
  329. newState.filteredKeys = filteredState.filteredKeys;
  330. newState.filteredShownKeys = filteredState.filteredShownKeys;
  331. newState.filteredExpandedKeys = filteredState.filteredExpandedKeys;
  332. }
  333. // expandedKeys changed while searching
  334. if (props.expandedKeys) {
  335. newState.filteredExpandedKeys = calcExpandedKeys(
  336. props.expandedKeys,
  337. keyEntities,
  338. props.autoExpandParent || !prevProps
  339. );
  340. if (prevProps && props.motion) {
  341. const prevKeys = prevState ? prevState.filteredExpandedKeys : new Set([]);
  342. // only show animation when treeData does not change
  343. if (!treeData) {
  344. const motionResult = calcMotionKeys(
  345. prevKeys,
  346. newState.filteredExpandedKeys,
  347. keyEntities
  348. );
  349. let { motionKeys } = motionResult;
  350. const { motionType } = motionResult;
  351. if (props.showFilteredOnly) {
  352. motionKeys = motionKeys.filter(key => prevState.filteredShownKeys.has(key));
  353. }
  354. if (motionType === 'hide') {
  355. // cache flatten nodes: expandedKeys changed may not be triggered by interaction
  356. newState.cachedFlattenNodes = cloneDeep(prevState.flattenNodes);
  357. }
  358. newState.motionKeys = new Set(motionKeys);
  359. newState.motionType = motionType;
  360. }
  361. }
  362. newState.flattenNodes = flattenTreeData(
  363. treeData || prevState.treeData,
  364. newState.filteredExpandedKeys || prevState.filteredExpandedKeys,
  365. keyMaps,
  366. props.showFilteredOnly && prevState.filteredShownKeys
  367. );
  368. }
  369. }
  370. // Handle single selection and multiple selection in controlled mode
  371. const withObject = props.onChangeWithObject;
  372. const isMultiple = props.multiple;
  373. if (!isMultiple) {
  374. // When getting single selection, the selected node
  375. if (needUpdate('value')) {
  376. newState.selectedKeys = findKeysForValues(
  377. // In both cases whether withObject is turned on, the value is standardized to string
  378. normalizeValue(props.value, withObject, keyMaps),
  379. valueEntities,
  380. isMultiple
  381. );
  382. } else if (!prevProps && props.defaultValue) {
  383. newState.selectedKeys = findKeysForValues(
  384. normalizeValue(props.defaultValue, withObject, keyMaps),
  385. valueEntities,
  386. isMultiple
  387. );
  388. } else if (treeData) {
  389. // If `treeData` changed, we also need check it
  390. if (props.value) {
  391. newState.selectedKeys = findKeysForValues(
  392. normalizeValue(props.value, withObject, keyMaps) || '',
  393. valueEntities,
  394. isMultiple
  395. );
  396. }
  397. }
  398. } else {
  399. let checkedKeyValues;
  400. // Get the selected node during multiple selection
  401. if (needUpdate('value')) {
  402. checkedKeyValues = findKeysForValues(
  403. normalizeValue(props.value, withObject, keyMaps),
  404. valueEntities,
  405. isMultiple
  406. );
  407. } else if (!prevProps && props.defaultValue) {
  408. checkedKeyValues = findKeysForValues(
  409. normalizeValue(props.defaultValue, withObject, keyMaps),
  410. valueEntities,
  411. isMultiple
  412. );
  413. } else if (treeData) {
  414. // If `treeData` changed, we also need check it
  415. if (props.value) {
  416. checkedKeyValues = findKeysForValues(
  417. normalizeValue(props.value, withObject, keyMaps) || [],
  418. valueEntities,
  419. isMultiple
  420. );
  421. } else {
  422. checkedKeyValues = updateKeys(props.checkRelation === 'related' ? prevState.checkedKeys : prevState.realCheckedKeys, keyEntities);
  423. }
  424. }
  425. if (checkedKeyValues) {
  426. if (props.checkRelation === 'unRelated') {
  427. newState.realCheckedKeys = new Set(checkedKeyValues);
  428. } else if (props.checkRelation === 'related') {
  429. const { checkedKeys, halfCheckedKeys } = calcCheckedKeys(checkedKeyValues, keyEntities);
  430. newState.checkedKeys = checkedKeys;
  431. newState.halfCheckedKeys = halfCheckedKeys;
  432. }
  433. }
  434. }
  435. // update loadedKeys
  436. if (needUpdate('loadedKeys')) {
  437. newState.loadedKeys = new Set(props.loadedKeys);
  438. }
  439. // update disableStrictly
  440. if (treeData && props.disableStrictly && props.checkRelation === 'related') {
  441. newState.disabledKeys = calcDisabledKeys(keyEntities, keyMaps);
  442. }
  443. return newState;
  444. }
  445. get adapter(): TreeAdapter {
  446. const filterAdapter: Pick<TreeAdapter, 'updateInputValue' | 'focusInput'> = {
  447. updateInputValue: value => {
  448. this.setState({ inputValue: value });
  449. },
  450. focusInput: () => {
  451. const { preventScroll } = this.props;
  452. if (this.inputRef && this.inputRef.current) {
  453. (this.inputRef.current as any).focus({ preventScroll });
  454. }
  455. },
  456. };
  457. return {
  458. ...super.adapter,
  459. ...filterAdapter,
  460. updateState: states => {
  461. this.setState({ ...states } as TreeState);
  462. },
  463. notifyExpand: (expandedKeys, { expanded: bool, node }) => {
  464. this.props.onExpand && this.props.onExpand([...expandedKeys], { expanded: bool, node });
  465. if (bool && this.props.loadData) {
  466. this.onNodeLoad(node);
  467. }
  468. },
  469. notifySelect: (selectKey, bool, node) => {
  470. this.props.onSelect && this.props.onSelect(selectKey, bool, node);
  471. },
  472. notifyChange: value => {
  473. this.props.onChange && this.props.onChange(value);
  474. },
  475. notifySearch: (input: string, filteredExpandedKeys: string[]) => {
  476. this.props.onSearch && this.props.onSearch(input, filteredExpandedKeys);
  477. },
  478. notifyRightClick: (e, node) => {
  479. this.props.onContextMenu && this.props.onContextMenu(e, node);
  480. },
  481. notifyDoubleClick: (e, node) => {
  482. this.props.onDoubleClick && this.props.onDoubleClick(e, node);
  483. },
  484. cacheFlattenNodes: bool => {
  485. this.setState({ cachedFlattenNodes: bool ? cloneDeep(this.state.flattenNodes) : undefined });
  486. },
  487. setDragNode: treeNode => {
  488. this.dragNode = treeNode;
  489. },
  490. };
  491. }
  492. search = (value: string) => {
  493. this.foundation.handleInputChange(value);
  494. };
  495. scrollTo = (scrollData: ScrollData) => {
  496. const { key, align = 'center' } = scrollData;
  497. const { flattenNodes } = this.state;
  498. if (key) {
  499. const index = flattenNodes?.findIndex((node) => {
  500. return node.key === key;
  501. });
  502. index >= 0 && (this.virtualizedListRef.current as any)?.scrollToItem(index, align);
  503. }
  504. }
  505. renderInput() {
  506. const {
  507. searchClassName,
  508. searchStyle,
  509. searchRender,
  510. searchPlaceholder,
  511. showClear
  512. } = this.props;
  513. const inputcls = cls(`${prefixcls}-input`);
  514. const { inputValue } = this.state;
  515. const inputProps = {
  516. value: inputValue,
  517. className: inputcls,
  518. onChange: (value: string) => this.search(value),
  519. prefix: <IconSearch />,
  520. showClear,
  521. placeholder: searchPlaceholder,
  522. };
  523. const wrapperCls = cls(`${prefixcls}-search-wrapper`, searchClassName);
  524. return (
  525. <div className={wrapperCls} style={searchStyle}>
  526. <LocaleConsumer componentName="Tree">
  527. {(locale: LocaleObject) => {
  528. inputProps.placeholder = searchPlaceholder || get(locale, 'searchPlaceholder');
  529. if (isFunction(searchRender)) {
  530. return searchRender({ ...inputProps });
  531. }
  532. if (searchRender === false) {
  533. return null;
  534. }
  535. return (
  536. <Input
  537. aria-label='Filter Tree'
  538. ref={this.inputRef as any}
  539. {...inputProps}
  540. />
  541. );
  542. }}
  543. </LocaleConsumer>
  544. </div>
  545. );
  546. }
  547. renderEmpty = () => {
  548. const { emptyContent } = this.props;
  549. if (emptyContent) {
  550. return <TreeNode empty emptyContent={this.props.emptyContent} />;
  551. } else {
  552. return (
  553. <LocaleConsumer componentName="Tree">
  554. {(locale: LocaleObject) => <TreeNode empty emptyContent={get(locale, 'emptyText')} />}
  555. </LocaleConsumer>
  556. );
  557. }
  558. };
  559. onNodeSelect = (e: MouseEvent | KeyboardEvent, treeNode: TreeNodeProps) => {
  560. this.foundation.handleNodeSelect(e, treeNode);
  561. };
  562. onNodeLoad = (data: TreeNodeData) => (
  563. new Promise(resolve => {
  564. // We need to get the latest state of loading/loaded keys
  565. this.setState(({ loadedKeys = new Set([]), loadingKeys = new Set([]) }) => (
  566. this.foundation.handleNodeLoad(loadedKeys, loadingKeys, data, resolve)
  567. ));
  568. })
  569. );
  570. onNodeCheck = (e: MouseEvent | KeyboardEvent, treeNode: TreeNodeProps) => {
  571. this.foundation.handleNodeSelect(e, treeNode);
  572. };
  573. onNodeExpand = (e: MouseEvent | KeyboardEvent, treeNode: TreeNodeProps) => {
  574. this.foundation.handleNodeExpand(e, treeNode);
  575. };
  576. onNodeRightClick = (e: MouseEvent, treeNode: TreeNodeProps) => {
  577. this.foundation.handleNodeRightClick(e, treeNode);
  578. };
  579. onNodeDoubleClick = (e: MouseEvent, treeNode: TreeNodeProps) => {
  580. this.foundation.handleNodeDoubleClick(e, treeNode);
  581. };
  582. onNodeDragStart = (e: React.DragEvent<HTMLLIElement>, treeNode: TreeNodeProps) => {
  583. this.foundation.handleNodeDragStart(e, treeNode);
  584. };
  585. onNodeDragEnter = (e: React.DragEvent<HTMLLIElement>, treeNode: TreeNodeProps) => {
  586. this.foundation.handleNodeDragEnter(e, treeNode, this.dragNode);
  587. };
  588. onNodeDragOver = (e: React.DragEvent<HTMLLIElement>, treeNode: TreeNodeProps) => {
  589. this.foundation.handleNodeDragOver(e, treeNode, this.dragNode);
  590. };
  591. onNodeDragLeave = (e: React.DragEvent<HTMLLIElement>, treeNode: TreeNodeProps) => {
  592. this.foundation.handleNodeDragLeave(e, treeNode);
  593. };
  594. onNodeDragEnd = (e: React.DragEvent<HTMLLIElement>, treeNode: TreeNodeProps) => {
  595. this.foundation.handleNodeDragEnd(e, treeNode);
  596. };
  597. onNodeDrop = (e: React.DragEvent<HTMLLIElement>, treeNode: TreeNodeProps) => {
  598. this.foundation.handleNodeDrop(e, treeNode, this.dragNode);
  599. };
  600. getTreeNodeRequiredProps = () => {
  601. const { expandedKeys, selectedKeys, checkedKeys, halfCheckedKeys, keyEntities, filteredKeys } = this.state;
  602. return {
  603. expandedKeys: expandedKeys || new Set(),
  604. selectedKeys: selectedKeys || [],
  605. checkedKeys: checkedKeys || new Set(),
  606. halfCheckedKeys: halfCheckedKeys || new Set(),
  607. filteredKeys: filteredKeys || new Set(),
  608. keyEntities,
  609. };
  610. };
  611. getTreeNodeKey = (treeNode: TreeNodeData) => {
  612. const { data } = treeNode;
  613. const { key } = data;
  614. return key;
  615. };
  616. renderTreeNode = (treeNode: FlattenNode, ind?: number, style?: React.CSSProperties) => {
  617. const { data, key } = treeNode;
  618. const treeNodeProps = this.foundation.getTreeNodeProps(key);
  619. if (!treeNodeProps) {
  620. return null;
  621. }
  622. const { keyMaps, showLine } = this.props;
  623. const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
  624. const children = data[get(keyMaps, 'children', 'children')];
  625. !isUndefined(children) && (props.children = children);
  626. return <TreeNode {...treeNodeProps} {...data} {...props} showLine={showLine} data={data} style={isEmpty(style) ? {} : style} />;
  627. };
  628. itemKey = (index: number, data: KeyEntity) => {
  629. // Find the item at the specified index.
  630. const item = data[index];
  631. // Return a value that uniquely identifies this item.
  632. return item.key;
  633. };
  634. option = ({ index, style, data }: OptionProps) => (
  635. this.renderTreeNode(data[index], index, style)
  636. );
  637. renderNodeList() {
  638. const { flattenNodes, cachedFlattenNodes, motionKeys, motionType } = this.state;
  639. const { virtualize, motion } = this.props;
  640. const { direction } = this.context;
  641. if (isEmpty(flattenNodes)) {
  642. return undefined;
  643. }
  644. if (!virtualize || isEmpty(virtualize)) {
  645. return (
  646. <NodeList
  647. flattenNodes={flattenNodes}
  648. flattenList={cachedFlattenNodes}
  649. motionKeys={motion ? motionKeys : new Set([])}
  650. motionType={motionType}
  651. onMotionEnd={this.onMotionEnd}
  652. renderTreeNode={this.renderTreeNode}
  653. />
  654. );
  655. }
  656. return (
  657. <AutoSizer defaultHeight={virtualize.height} defaultWidth={virtualize.width}>
  658. {({ height, width }: { width: string | number; height: string | number }) => (
  659. <VirtualList
  660. ref={this.virtualizedListRef}
  661. itemCount={flattenNodes.length}
  662. itemSize={virtualize.itemSize}
  663. height={height}
  664. width={width}
  665. itemKey={this.itemKey}
  666. itemData={flattenNodes as any}
  667. className={`${prefixcls}-virtual-list`}
  668. style={{ direction }}
  669. >
  670. {this.option}
  671. </VirtualList>
  672. )}
  673. </AutoSizer>
  674. );
  675. }
  676. render() {
  677. const {
  678. keyEntities,
  679. motionKeys,
  680. motionType,
  681. inputValue,
  682. filteredKeys,
  683. dragOverNodeKey,
  684. dropPosition,
  685. checkedKeys,
  686. realCheckedKeys,
  687. } = this.state;
  688. const {
  689. blockNode,
  690. className,
  691. style,
  692. filterTreeNode,
  693. disabled,
  694. icon,
  695. directory,
  696. multiple,
  697. showFilteredOnly,
  698. showLine,
  699. motion,
  700. expandAction,
  701. loadData,
  702. renderLabel,
  703. draggable,
  704. renderFullLabel,
  705. labelEllipsis,
  706. virtualize,
  707. checkRelation,
  708. ...rest
  709. } = this.props;
  710. const wrapperCls = cls(`${prefixcls}-wrapper`, className);
  711. const listCls = cls(`${prefixcls}-option-list`, {
  712. [`${prefixcls}-option-list-block`]: blockNode,
  713. });
  714. const searchNoRes = Boolean(inputValue) && !filteredKeys.size;
  715. const noData = isEmpty(keyEntities) || (showFilteredOnly && searchNoRes);
  716. const ariaAttr = {
  717. role: noData ? 'none' : 'tree'
  718. };
  719. if (ariaAttr.role === 'tree') {
  720. ariaAttr['aria-multiselectable'] = multiple ? true : false;
  721. }
  722. return (
  723. <TreeContext.Provider
  724. value={{
  725. treeDisabled: disabled,
  726. treeIcon: icon,
  727. motion,
  728. motionKeys,
  729. motionType,
  730. filterTreeNode,
  731. keyEntities,
  732. onNodeClick: this.onNodeClick,
  733. onNodeExpand: this.onNodeExpand,
  734. onNodeSelect: this.onNodeSelect,
  735. onNodeCheck: this.onNodeCheck,
  736. onNodeRightClick: this.onNodeRightClick,
  737. onNodeDoubleClick: this.onNodeDoubleClick,
  738. renderTreeNode: this.renderTreeNode,
  739. onNodeDragStart: this.onNodeDragStart,
  740. onNodeDragEnter: this.onNodeDragEnter,
  741. onNodeDragOver: this.onNodeDragOver,
  742. onNodeDragLeave: this.onNodeDragLeave,
  743. onNodeDragEnd: this.onNodeDragEnd,
  744. onNodeDrop: this.onNodeDrop,
  745. expandAction,
  746. directory,
  747. multiple,
  748. showFilteredOnly,
  749. isSearching: Boolean(inputValue),
  750. loadData,
  751. onNodeLoad: this.onNodeLoad,
  752. renderLabel,
  753. draggable,
  754. renderFullLabel,
  755. dragOverNodeKey,
  756. dropPosition,
  757. labelEllipsis: typeof labelEllipsis === 'undefined' ? virtualize : labelEllipsis,
  758. }}
  759. >
  760. <div aria-label={this.props['aria-label']} className={wrapperCls} style={style} {...this.getDataAttr(rest)}>
  761. {filterTreeNode ? this.renderInput() : null}
  762. <div className={listCls} {...ariaAttr}>
  763. {noData ? this.renderEmpty() : (multiple ?
  764. (<CheckboxGroup value={Array.from(checkRelation === 'related' ? checkedKeys : realCheckedKeys)}>
  765. {this.renderNodeList()}
  766. </CheckboxGroup>) :
  767. this.renderNodeList()
  768. )}
  769. </div>
  770. </div>
  771. </TreeContext.Provider>
  772. );
  773. }
  774. }
  775. Tree.TreeNode = TreeNode;
  776. export default Tree;