1
0

index.tsx 32 KB

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