index.tsx 29 KB

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