index.tsx 33 KB

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