index.tsx 30 KB

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