index.tsx 32 KB

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