index.tsx 30 KB

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