foundation.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. /**
  2. * The drag and drop handler implementation is referenced from rc-tree
  3. * https://github.com/react-component/tree
  4. */
  5. import { isUndefined, difference, pick, cloneDeep, get } from 'lodash';
  6. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  7. import {
  8. flattenTreeData,
  9. findDescendantKeys,
  10. findAncestorKeys,
  11. filter,
  12. normalizedArr,
  13. normalizeKeyList,
  14. getMotionKeys,
  15. calcCheckedKeysForChecked,
  16. calcCheckedKeysForUnchecked,
  17. calcCheckedKeys,
  18. getValueOrKey,
  19. getDragNodesKeys,
  20. calcDropRelativePosition,
  21. calcDropActualPosition
  22. } from './treeUtil';
  23. export { KeyMapProps } from './treeUtil';
  24. export interface BasicTreeNodeProps {
  25. [x: string]: any;
  26. expanded?: boolean;
  27. selected?: boolean;
  28. checked?: boolean;
  29. halfChecked?: boolean;
  30. active?: boolean;
  31. disabled?: boolean;
  32. loaded?: boolean;
  33. loading?: boolean;
  34. isLeaf?: boolean;
  35. pos?: string;
  36. children?: BasicTreeNodeData[];
  37. icon?: any;
  38. directory?: boolean;
  39. selectedKey?: string;
  40. motionKey?: string[] | string;
  41. eventKey?: string
  42. }
  43. export interface BasicTreeNodeData {
  44. [x: string]: any;
  45. key?: string;
  46. value?: number | string;
  47. label?: any;
  48. icon?: any;
  49. disabled?: boolean;
  50. isLeaf?: boolean;
  51. children?: BasicTreeNodeData[]
  52. }
  53. export interface BasicKeyEntities {
  54. [key: string]: BasicKeyEntity
  55. }
  56. export interface BasicKeyEntity {
  57. children?: BasicKeyEntities;
  58. data?: BasicTreeNodeData;
  59. ind?: number;
  60. key?: string;
  61. level?: number;
  62. parent?: undefined | BasicKeyEntity;
  63. parentPos?: null | string;
  64. pos?: string
  65. }
  66. export interface BasicDragTreeNode extends BasicTreeNodeData {
  67. expanded: boolean;
  68. /**
  69. * The positional relationship of the current node in the entire
  70. * treeData, such as the 0th node of the 2nd node of the 1st node
  71. * of the 0th layer: '0-1-2-0'
  72. */
  73. pos: string
  74. }
  75. export interface BasicFlattenNode {
  76. _innerDataTag?: boolean;
  77. children?: BasicFlattenNode[];
  78. data?: BasicTreeNodeData;
  79. key?: string;
  80. label?: any;
  81. parent?: null | BasicFlattenNode;
  82. pos?: string;
  83. value?: string
  84. }
  85. export interface BasicDragProps {
  86. event: any;
  87. node: BasicDragTreeNode
  88. }
  89. export interface BasicDragEnterProps extends BasicDragProps {
  90. expandedKeys?: string[]
  91. }
  92. export type ExpandAction = false | 'click' | 'doubleClick';
  93. export type BasicValue = string | number | BasicTreeNodeData | Array<BasicTreeNodeData | string | number>;
  94. export interface BasicOnDragProps {
  95. event: any;
  96. node: BasicDragTreeNode;
  97. dragNode: BasicDragTreeNode;
  98. dragNodesKeys: string[];
  99. /**
  100. * dropPosition represents the position of the dragged node being
  101. * dropped in the current level. If inserted before the 0th node
  102. * of the same level, it is -1, after the 0th node, it is 1, and
  103. * it is 0 when it falls on it. And so on. With dropToGap, a more
  104. * complete judgment can be obtained.
  105. */
  106. dropPosition: number;
  107. /**
  108. * Indicates whether the dragged node is dropped between nodes, if
  109. * it is false, it is dropped above a node
  110. */
  111. dropToGap: boolean
  112. }
  113. export interface BasicRenderFullLabelProps {
  114. /* Click the callback of the entire row to control the expansion behavior and selection */
  115. onClick: (e: any) => void;
  116. /* Right-click the callback for the entire row */
  117. onContextMenu: (e: any) => void;
  118. /* Double-click the entire line of callback */
  119. onDoubleClick: (e: any) => void;
  120. /* Class name, including built-in styles such as indentation, expand button, filter, disable, select, etc. */
  121. className: string;
  122. /* Expand callback */
  123. onExpand: (e: any) => void;
  124. /* The original data of the row */
  125. data: BasicTreeNodeData;
  126. /* The level of the line can be used to customize the indentation value */
  127. level: number;
  128. /* The style required for virtualization, if virtualization is used, the style must be assigned to the DOM element */
  129. style: any;
  130. /* Multi-select click callback */
  131. onCheck: (e: any) => void;
  132. /* icon of Expand button */
  133. expandIcon: any;
  134. /* Selected state */
  135. checkStatus: {
  136. /* Whether to select in the multi-select state */
  137. checked: boolean;
  138. /* Whether to half-select in the multi-select state */
  139. halfChecked: boolean
  140. };
  141. /* Expand status */
  142. expandStatus: {
  143. /* Has it been expanded */
  144. expanded: boolean;
  145. /* Is it unfolding */
  146. loading: boolean
  147. };
  148. /* Whether the node meets the search conditions */
  149. filtered: boolean | undefined;
  150. /* Current search box input */
  151. searchWord: string | undefined
  152. }
  153. export interface BasicSearchRenderProps {
  154. className: string;
  155. placeholder: string;
  156. prefix: any;
  157. showClear?: boolean;
  158. value: string;
  159. onChange: (value: string) => void
  160. }
  161. export interface TreeDataSimpleJson {
  162. [x: string]: string | TreeDataSimpleJson
  163. }
  164. export interface Virtualize {
  165. itemSize: number;
  166. height?: number | string;
  167. width?: number | string
  168. }
  169. export type CheckRelation = 'related' | 'unRelated';
  170. export interface BasicTreeProps {
  171. autoExpandParent?: boolean;
  172. autoExpandWhenDragEnter?: boolean;
  173. blockNode?: boolean;
  174. children?: any;
  175. className?: string;
  176. expandAll?: boolean;
  177. defaultExpandAll?: boolean;
  178. defaultExpandedKeys?: string[];
  179. defaultValue?: BasicValue;
  180. directory?: boolean;
  181. disabled?: boolean;
  182. disableStrictly?: boolean;
  183. draggable?: boolean;
  184. emptyContent?: any;
  185. expandAction?: ExpandAction;
  186. expandedKeys?: string[];
  187. filterTreeNode?: boolean | ((inputValue: string, treeNodeString: string, data?: BasicTreeNodeData) => boolean);
  188. hideDraggingNode?: boolean;
  189. labelEllipsis?: boolean;
  190. leafOnly?: boolean;
  191. loadData?: (treeNode?: BasicTreeNodeData) => Promise<void>;
  192. loadedKeys?: string[];
  193. motion?: boolean;
  194. multiple?: boolean;
  195. onChange?: (value?: BasicValue) => void;
  196. onChangeWithObject?: boolean;
  197. onDoubleClick?: (e: any, node: BasicTreeNodeData) => void;
  198. onDragEnd?: (dragProps: BasicDragProps) => void;
  199. onDragEnter?: (dragEnterProps: BasicDragEnterProps) => void;
  200. onDragLeave?: (dragProps: BasicDragProps) => void;
  201. onDragOver?: (dragProps: BasicDragProps) => void;
  202. onDragStart?: (dragProps: BasicDragProps) => void;
  203. onDrop?: (onDragProps: BasicOnDragProps) => void;
  204. onExpand?: (expandedKeys: string[], expandedOtherProps: BasicExpandedOtherProps) => void;
  205. onLoad?: (loadedKeys?: Set<string>, treeNode?: BasicTreeNodeData) => void;
  206. onContextMenu?: (e: any, node: BasicTreeNodeData) => void;
  207. onSearch?: (sunInput: string, filteredExpandedKeys: string[]) => void;
  208. onSelect?: (selectedKey: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
  209. preventScroll?: boolean;
  210. renderDraggingNode?: (nodeInstance: HTMLElement, node: BasicTreeNodeData) => HTMLElement;
  211. renderFullLabel?: (renderFullLabelProps: BasicRenderFullLabelProps) => any;
  212. renderLabel?: (label?: any, treeNode?: BasicTreeNodeData) => any;
  213. searchClassName?: string;
  214. searchPlaceholder?: string;
  215. searchRender?: ((searchRenderProps: BasicSearchRenderProps) => any) | false;
  216. searchStyle?: any;
  217. showClear?: boolean;
  218. showFilteredOnly?: boolean;
  219. style?: any;
  220. treeData?: BasicTreeNodeData[];
  221. treeDataSimpleJson?: TreeDataSimpleJson;
  222. treeNodeFilterProp?: string;
  223. value?: BasicValue;
  224. virtualize?: Virtualize;
  225. icon?: any;
  226. checkRelation?: CheckRelation;
  227. 'aria-label'?: string
  228. }
  229. /* Data maintained internally. At the React framework level, corresponding to state */
  230. export interface BasicTreeInnerData {
  231. /* The input content of the input box */
  232. inputValue: string;
  233. /* keyEntities */
  234. keyEntities: BasicKeyEntities;
  235. /* treeData */
  236. treeData: BasicTreeNodeData[];
  237. /* Expanded node */
  238. flattenNodes: BasicFlattenNode[];
  239. /* The selected node when single-selected */
  240. selectedKeys: string[];
  241. /* Select all nodes in multiple selection */
  242. checkedKeys: Set<string>;
  243. /* Half-selected node when multiple selection */
  244. halfCheckedKeys: Set<string>;
  245. /* real selected nodes in multiple selection */
  246. realCheckedKeys: Set<string>;
  247. /* Animation node */
  248. motionKeys: Set<string>;
  249. /* Animation type */
  250. motionType: string;
  251. /* Expand node */
  252. expandedKeys: Set<string>;
  253. /* Searched node */
  254. filteredKeys: Set<string>;
  255. /* The ancestor node expanded because of the searched node*/
  256. filteredExpandedKeys: Set<string>;
  257. /* Because of the searched node, the expanded ancestor node + the searched node + the descendant nodes of the searched node */
  258. filteredShownKeys: Set<string>;
  259. /* Prev props */
  260. prevProps: null | BasicTreeProps;
  261. /* loaded nodes */
  262. loadedKeys: Set<string>;
  263. /* loading nodes */
  264. loadingKeys: Set<string>;
  265. /* cache */
  266. cachedFlattenNodes: BasicFlattenNode[] | undefined;
  267. cachedKeyValuePairs: { [x: string]: string };
  268. /* Strictly disabled node */
  269. disabledKeys: Set<string>;
  270. /* Is dragging */
  271. dragging: boolean;
  272. /* Dragged node */
  273. dragNodesKeys: Set<string>;
  274. /* DragOver node */
  275. dragOverNodeKey: string[] | string | null;
  276. /* Drag position */
  277. dropPosition: number | null
  278. }
  279. export interface BasicExpandedOtherProps {
  280. expanded: boolean;
  281. node: BasicTreeNodeData
  282. }
  283. export interface TreeAdapter extends DefaultAdapter<BasicTreeProps, BasicTreeInnerData> {
  284. updateInputValue: (value: string) => void;
  285. focusInput: () => void;
  286. updateState: (states: Partial<BasicTreeInnerData>) => void;
  287. notifyExpand: (expandedKeys: Set<string>, { expanded, node }: BasicExpandedOtherProps) => void;
  288. notifySelect: (selectKey: string, bool: boolean, node: BasicTreeNodeData) => void;
  289. notifyChange: (value: BasicValue) => void;
  290. notifySearch: (input: string, filteredExpandedKeys: string[]) => void;
  291. notifyRightClick: (e: any, node: BasicTreeNodeData) => void;
  292. notifyDoubleClick: (e: any, node: BasicTreeNodeData) => void;
  293. cacheFlattenNodes: (bool: boolean) => void;
  294. setDragNode: (treeNode: any) => void
  295. }
  296. export default class TreeFoundation extends BaseFoundation<TreeAdapter, BasicTreeProps, BasicTreeInnerData> {
  297. delayedDragEnterLogic: any;
  298. constructor(adapter: TreeAdapter) {
  299. super({
  300. ...adapter,
  301. });
  302. }
  303. _isMultiple() {
  304. return this.getProp('multiple');
  305. }
  306. _isAnimated() {
  307. return this.getProp('motion');
  308. }
  309. _isDisabled(treeNode: BasicTreeNodeProps = {}) {
  310. return this.getProp('disabled') || treeNode.disabled;
  311. }
  312. _isExpandControlled() {
  313. return !isUndefined(this.getProp('expandedKeys'));
  314. }
  315. _isLoadControlled() {
  316. return !isUndefined(this.getProp('loadedKeys'));
  317. }
  318. _isFilterable() {
  319. // filter can be boolean or function
  320. return Boolean(this.getProp('filterTreeNode'));
  321. }
  322. _showFilteredOnly() {
  323. const { inputValue } = this.getStates();
  324. const { showFilteredOnly } = this.getProps();
  325. return Boolean(inputValue) && showFilteredOnly;
  326. }
  327. getCopyFromState(items: string[] | string) {
  328. const res: Partial<BasicTreeInnerData> = {};
  329. normalizedArr(items).forEach(key => {
  330. res[key] = cloneDeep(this.getState(key));
  331. });
  332. return res;
  333. }
  334. getTreeNodeProps(key: string) {
  335. const {
  336. expandedKeys = new Set([]),
  337. selectedKeys = [],
  338. checkedKeys = new Set([]),
  339. halfCheckedKeys = new Set([]),
  340. realCheckedKeys = new Set([]),
  341. keyEntities = {},
  342. filteredKeys = new Set([]),
  343. inputValue = '',
  344. loadedKeys = new Set([]),
  345. loadingKeys = new Set([]),
  346. filteredExpandedKeys = new Set([]),
  347. disabledKeys = new Set([]),
  348. } = this.getStates();
  349. const { treeNodeFilterProp, checkRelation } = this.getProps();
  350. const entity = keyEntities[key];
  351. const notExist = !entity;
  352. if (notExist) {
  353. return null;
  354. }
  355. // if checkRelation is invalid, the checked status of node will be false
  356. let realChecked = false;
  357. let realHalfChecked = false;
  358. if (checkRelation === 'related') {
  359. realChecked = checkedKeys.has(key);
  360. realHalfChecked = halfCheckedKeys.has(key);
  361. } else if (checkRelation === 'unRelated') {
  362. realChecked = realCheckedKeys.has(key);
  363. realHalfChecked = false;
  364. }
  365. const isSearching = Boolean(inputValue);
  366. const treeNodeProps: BasicTreeNodeProps = {
  367. eventKey: key,
  368. expanded: isSearching ? filteredExpandedKeys.has(key) : expandedKeys.has(key),
  369. selected: selectedKeys.includes(key),
  370. checked: realChecked,
  371. halfChecked: realHalfChecked,
  372. pos: String(entity ? entity.pos : ''),
  373. level: entity.level,
  374. filtered: filteredKeys.has(key),
  375. loading: loadingKeys.has(key) && !loadedKeys.has(key),
  376. loaded: loadedKeys.has(key),
  377. keyword: inputValue,
  378. treeNodeFilterProp,
  379. };
  380. if (this.getProp('disableStrictly') && disabledKeys.has(key)) {
  381. treeNodeProps.disabled = true;
  382. }
  383. return treeNodeProps;
  384. }
  385. notifyJsonChange(key: string[] | string, e: any) {
  386. const data = this.getProp('treeDataSimpleJson');
  387. const selectedPath = normalizedArr(key).map(i => i.replace('-', '.'));
  388. const value = pick(data, selectedPath);
  389. this._adapter.notifyChange(value as BasicValue);
  390. }
  391. notifyMultipleChange(key: string[], e: any) {
  392. const { keyEntities } = this.getStates();
  393. const { leafOnly, checkRelation, keyMaps } = this.getProps();
  394. let value;
  395. let keyList = [];
  396. if (checkRelation === 'related') {
  397. keyList = normalizeKeyList(key, keyEntities, leafOnly);
  398. } else if (checkRelation === 'unRelated') {
  399. keyList = key;
  400. }
  401. if (this.getProp('onChangeWithObject')) {
  402. value = keyList.map((itemKey: string) => keyEntities[itemKey].data);
  403. } else {
  404. value = getValueOrKey(keyList.map((itemKey: string) => keyEntities[itemKey].data), keyMaps);
  405. }
  406. this._adapter.notifyChange(value);
  407. }
  408. notifyChange(key: string[] | string, e: any) {
  409. const isMultiple = this._isMultiple();
  410. const { keyMaps } = this.getProps();
  411. const { keyEntities } = this.getStates();
  412. if (this.getProp('treeDataSimpleJson')) {
  413. this.notifyJsonChange(key, e);
  414. } else if (isMultiple) {
  415. this.notifyMultipleChange(key as string[], e);
  416. } else {
  417. let value;
  418. if (this.getProp('onChangeWithObject')) {
  419. value = get(keyEntities, key).data;
  420. } else {
  421. const { data } = get(keyEntities, key);
  422. value = getValueOrKey(data, keyMaps);
  423. }
  424. this._adapter.notifyChange(value);
  425. }
  426. }
  427. handleInputChange(sugInput: string) {
  428. // Input is a controlled component, so the value value needs to be updated
  429. this._adapter.updateInputValue(sugInput);
  430. const { expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  431. const { showFilteredOnly, filterTreeNode, treeNodeFilterProp, keyMaps } = this.getProps();
  432. const realFilterProp = treeNodeFilterProp !== 'label' ? treeNodeFilterProp : get(keyMaps, 'label', 'label');
  433. let filteredOptsKeys: string[] = [];
  434. let expandedOptsKeys: string[] = [];
  435. let flattenNodes: BasicFlattenNode[] = [];
  436. let filteredShownKeys = new Set([]);
  437. if (!sugInput) {
  438. expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
  439. expandedOptsKeys.forEach(item => expandedKeys.add(item));
  440. flattenNodes = flattenTreeData(treeData, expandedKeys, keyMaps);
  441. } else {
  442. filteredOptsKeys = Object.values(keyEntities)
  443. .filter((item: BasicKeyEntity) => filter(sugInput, item.data, filterTreeNode, realFilterProp))
  444. .map((item: BasicKeyEntity) => item.key);
  445. expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
  446. const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
  447. filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
  448. flattenNodes = flattenTreeData(
  449. treeData,
  450. new Set(expandedOptsKeys),
  451. keyMaps,
  452. showFilteredOnly && filteredShownKeys
  453. );
  454. }
  455. const newFilteredExpandedKeys = new Set(expandedOptsKeys);
  456. this._adapter.notifySearch(sugInput, Array.from(newFilteredExpandedKeys));
  457. this._adapter.updateState({
  458. expandedKeys,
  459. flattenNodes,
  460. motionKeys: new Set([]),
  461. filteredKeys: new Set(filteredOptsKeys),
  462. filteredExpandedKeys: newFilteredExpandedKeys,
  463. filteredShownKeys,
  464. });
  465. }
  466. handleNodeSelect(e: any, treeNode: BasicTreeNodeProps) {
  467. const isDisabled = this._isDisabled(treeNode);
  468. if (isDisabled) {
  469. return;
  470. }
  471. if (!this._isMultiple()) {
  472. this.handleSingleSelect(e, treeNode);
  473. } else {
  474. this.handleMultipleSelect(e, treeNode);
  475. }
  476. }
  477. handleNodeRightClick(e: any, treeNode: BasicTreeNodeProps) {
  478. this._adapter.notifyRightClick(e, treeNode.data);
  479. }
  480. handleNodeDoubleClick(e: any, treeNode: BasicTreeNodeProps) {
  481. this._adapter.notifyDoubleClick(e, treeNode.data);
  482. }
  483. handleSingleSelect(e: any, treeNode: BasicTreeNodeProps) {
  484. let { selectedKeys } = this.getCopyFromState('selectedKeys');
  485. const { selected, eventKey, data } = treeNode;
  486. const targetSelected = !selected;
  487. this._adapter.notifySelect(eventKey, true, data);
  488. if (!targetSelected) {
  489. return;
  490. }
  491. if (!selectedKeys.includes(eventKey)) {
  492. selectedKeys = [eventKey];
  493. this.notifyChange(eventKey, e);
  494. if (!this._isControlledComponent()) {
  495. this._adapter.updateState({ selectedKeys });
  496. }
  497. }
  498. }
  499. calcCheckedKeys(eventKey: string, targetStatus: boolean) {
  500. const { keyEntities } = this.getStates();
  501. const { checkedKeys, halfCheckedKeys } = this.getCopyFromState(['checkedKeys', 'halfCheckedKeys']);
  502. return targetStatus ?
  503. calcCheckedKeysForChecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys) :
  504. calcCheckedKeysForUnchecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  505. }
  506. /*
  507. * Compute the checked state of the node
  508. */
  509. calcCheckedStatus(targetStatus: boolean, eventKey: string) {
  510. // From checked to unchecked, you can change it directly
  511. if (!targetStatus) {
  512. return targetStatus;
  513. }
  514. // Starting from unchecked, you need to judge according to the descendant nodes
  515. const { checkedKeys, keyEntities, disabledKeys } = this.getStates();
  516. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  517. const hasDisabled = descendantKeys.some((key: string) => disabledKeys.has(key));
  518. // If the descendant nodes are not disabled, they will be directly changed to checked
  519. if (!hasDisabled) {
  520. return targetStatus;
  521. }
  522. // If all descendant nodes that are not disabled are selected, return unchecked, otherwise, return checked
  523. const nonDisabledKeys = descendantKeys.filter((key: string) => !disabledKeys.has(key));
  524. const allChecked = nonDisabledKeys.every((key: string) => checkedKeys.has(key));
  525. return !allChecked;
  526. }
  527. /*
  528. * In strict disable mode, calculate the nodes of checked and halfCheckedKeys and return their corresponding keys
  529. */
  530. calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean) {
  531. const { keyEntities, disabledKeys } = this.getStates();
  532. const { checkedKeys } = this.getCopyFromState(['checkedKeys']);
  533. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  534. const hasDisabled = descendantKeys.some((key: string) => disabledKeys.has(key));
  535. // If none of the descendant nodes are disabled, follow the normal logic
  536. if (!hasDisabled) {
  537. return this.calcCheckedKeys(eventKey, targetStatus);
  538. }
  539. const nonDisabled = descendantKeys.filter((key: string) => !disabledKeys.has(key));
  540. const newCheckedKeys = targetStatus ?
  541. [...nonDisabled, ...checkedKeys] :
  542. difference(normalizeKeyList([...checkedKeys], keyEntities, true), nonDisabled);
  543. return calcCheckedKeys(newCheckedKeys, keyEntities);
  544. }
  545. /*
  546. * Handle the selection event in the case of multiple selection
  547. */
  548. handleMultipleSelect(e: any, treeNode: BasicTreeNodeProps) {
  549. const { disableStrictly, checkRelation } = this.getProps();
  550. const { realCheckedKeys } = this.getStates();
  551. // eventKey: The key value of the currently clicked node
  552. const { checked, eventKey, data } = treeNode;
  553. if (checkRelation === 'related') {
  554. // Find the checked state of the current node
  555. const targetStatus = disableStrictly ? this.calcCheckedStatus(!checked, eventKey) : !checked;
  556. const { checkedKeys, halfCheckedKeys } = disableStrictly ?
  557. this.calcNonDisabledCheckedKeys(eventKey, targetStatus) :
  558. this.calcCheckedKeys(eventKey, targetStatus);
  559. this._adapter.notifySelect(eventKey, targetStatus, data);
  560. this.notifyChange([...checkedKeys], e);
  561. if (!this._isControlledComponent()) {
  562. this._adapter.updateState({ checkedKeys, halfCheckedKeys });
  563. }
  564. } else if (checkRelation === 'unRelated') {
  565. const newRealCheckedKeys: Set<string> = new Set(realCheckedKeys);
  566. let targetStatus: boolean;
  567. if (realCheckedKeys.has(eventKey)) {
  568. newRealCheckedKeys.delete(eventKey);
  569. targetStatus = false;
  570. } else {
  571. newRealCheckedKeys.add(eventKey);
  572. targetStatus = true;
  573. }
  574. this._adapter.notifySelect(eventKey, targetStatus, data);
  575. this.notifyChange([...newRealCheckedKeys], e);
  576. if (!this._isControlledComponent()) {
  577. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys });
  578. }
  579. }
  580. }
  581. setExpandedStatus(treeNode: BasicTreeNodeProps) {
  582. const { inputValue, treeData, filteredShownKeys, keyEntities } = this.getStates();
  583. const { keyMaps } = this.getProps();
  584. const isSearching = Boolean(inputValue);
  585. const showFilteredOnly = this._showFilteredOnly();
  586. const expandedStateKey = isSearching ? 'filteredExpandedKeys' : 'expandedKeys';
  587. const expandedKeys = this.getCopyFromState(expandedStateKey)[expandedStateKey];
  588. let motionType = 'show';
  589. const { eventKey, expanded, data } = treeNode;
  590. if (!expanded) {
  591. expandedKeys.add(eventKey);
  592. } else if (expandedKeys.has(eventKey)) {
  593. expandedKeys.delete(eventKey);
  594. motionType = 'hide';
  595. }
  596. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  597. if (!this._isExpandControlled()) {
  598. const flattenNodes = flattenTreeData(
  599. treeData,
  600. expandedKeys,
  601. keyMaps,
  602. isSearching && showFilteredOnly && filteredShownKeys
  603. );
  604. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, expandedKeys, keyEntities) : [];
  605. const newState = {
  606. [expandedStateKey]: expandedKeys,
  607. flattenNodes,
  608. motionKeys: new Set(motionKeys),
  609. motionType,
  610. };
  611. this._adapter.updateState(newState);
  612. }
  613. return {
  614. expandedKeys,
  615. expanded: !expanded,
  616. data,
  617. };
  618. }
  619. handleNodeExpand(e: any, treeNode: BasicTreeNodeProps) {
  620. const { loadData } = this.getProps();
  621. if (!loadData && (!treeNode.children || !treeNode.children.length)) {
  622. return;
  623. }
  624. const { expandedKeys, data, expanded } = this.setExpandedStatus(treeNode);
  625. this._adapter.notifyExpand(expandedKeys, {
  626. expanded,
  627. node: data,
  628. });
  629. }
  630. handleNodeLoad(loadedKeys: Set<string>, loadingKeys: Set<string>, data: BasicTreeNodeData, resolve: (value?: any) => void) {
  631. const { loadData, onLoad } = this.getProps();
  632. const { key } = data;
  633. if (!loadData || loadedKeys.has(key) || loadingKeys.has(key)) {
  634. return {};
  635. }
  636. // Process the loaded data
  637. loadData(data).then(() => {
  638. const {
  639. loadedKeys: prevLoadedKeys,
  640. loadingKeys: prevLoadingKeys
  641. } = this.getCopyFromState(['loadedKeys', 'loadingKeys']);
  642. const newLoadedKeys = prevLoadedKeys.add(key);
  643. const newLoadingKeys = new Set([...prevLoadingKeys]);
  644. newLoadingKeys.delete(key);
  645. // onLoad should be triggered before internal setState to avoid `loadData` being triggered twice
  646. onLoad && onLoad(newLoadedKeys, data);
  647. if (!this._isLoadControlled()) {
  648. this._adapter.updateState({
  649. loadedKeys: newLoadedKeys,
  650. });
  651. }
  652. this._adapter.setState({
  653. loadingKeys: newLoadingKeys,
  654. } as any);
  655. resolve();
  656. });
  657. return {
  658. loadingKeys: loadingKeys.add(key),
  659. };
  660. }
  661. // Drag and drop related processing logic
  662. getDragEventNodeData(node: BasicTreeNodeData) {
  663. return {
  664. ...node.data,
  665. ...pick(node, ['expanded', 'pos', 'children']),
  666. };
  667. }
  668. triggerDragEvent(name: string, event: any, node: BasicTreeNodeData, extra = {}) {
  669. const callEvent = this.getProp(name);
  670. callEvent &&
  671. callEvent({
  672. event,
  673. node: this.getDragEventNodeData(node),
  674. ...extra,
  675. });
  676. }
  677. clearDragState = () => {
  678. this._adapter.updateState({
  679. dragOverNodeKey: '',
  680. dragging: false,
  681. });
  682. };
  683. handleNodeDragStart(e: any, treeNode: BasicTreeNodeData) {
  684. const { keyEntities } = this.getStates();
  685. const { hideDraggingNode, renderDraggingNode } = this.getProps();
  686. const { eventKey, nodeInstance, data } = treeNode;
  687. if (hideDraggingNode || renderDraggingNode) {
  688. let dragImg;
  689. if (typeof renderDraggingNode === 'function') {
  690. dragImg = renderDraggingNode(nodeInstance, data);
  691. } else if (hideDraggingNode) {
  692. dragImg = nodeInstance.cloneNode(true);
  693. dragImg.style.opacity = 0;
  694. }
  695. document.body.appendChild(dragImg);
  696. e.dataTransfer.setDragImage(dragImg, 0, 0);
  697. }
  698. this._adapter.setDragNode(treeNode);
  699. this._adapter.updateState({
  700. dragging: true,
  701. dragNodesKeys: new Set(getDragNodesKeys(eventKey, keyEntities)),
  702. });
  703. this.triggerDragEvent('onDragStart', e, treeNode);
  704. }
  705. handleNodeDragEnter(e: any, treeNode: BasicTreeNodeData, dragNode: any) {
  706. const { dragging, dragNodesKeys } = this.getStates();
  707. const { autoExpandWhenDragEnter } = this.getProps();
  708. const { pos, eventKey, expanded } = treeNode;
  709. if (!dragNode || dragNodesKeys.has(eventKey)) {
  710. return;
  711. }
  712. const dropPosition = calcDropRelativePosition(e, treeNode);
  713. // If the drag node is itself, skip
  714. if (dragNode.eventKey === eventKey && dropPosition === 0) {
  715. this._adapter.updateState({
  716. dragOverNodeKey: '',
  717. dropPosition: null,
  718. });
  719. return;
  720. }
  721. // Trigger dragenter after clearing the prev state in dragleave
  722. setTimeout(() => {
  723. this._adapter.updateState({
  724. dragOverNodeKey: eventKey,
  725. dropPosition,
  726. });
  727. // If autoExpand is already expanded or not allowed, trigger the event and return
  728. if (!autoExpandWhenDragEnter || expanded) {
  729. this.triggerDragEvent('onDragEnter', e, treeNode);
  730. return;
  731. }
  732. // Side effects of delayed drag
  733. if (!this.delayedDragEnterLogic) {
  734. this.delayedDragEnterLogic = {};
  735. }
  736. Object.keys(this.delayedDragEnterLogic).forEach(key => {
  737. clearTimeout(this.delayedDragEnterLogic[key]);
  738. });
  739. this.delayedDragEnterLogic[pos] = window.setTimeout(() => {
  740. if (!dragging) {
  741. return;
  742. }
  743. const { expandedKeys: newExpandedKeys } = this.setExpandedStatus(treeNode);
  744. this.triggerDragEvent('onDragEnter', e, treeNode, { expandedKeys: [...newExpandedKeys] });
  745. }, 400);
  746. }, 0);
  747. }
  748. handleNodeDragOver(e: any, treeNode: BasicTreeNodeData, dragNode: any) {
  749. const { dropPosition, dragNodesKeys, dragOverNodeKey } = this.getStates();
  750. const { eventKey } = treeNode;
  751. if (dragNodesKeys.has(eventKey)) {
  752. return;
  753. }
  754. // Update the drag position
  755. if (dragNode && eventKey === dragOverNodeKey) {
  756. const newPos = calcDropRelativePosition(e, treeNode);
  757. if (dropPosition === newPos) {
  758. return;
  759. }
  760. this._adapter.updateState({
  761. dropPosition: newPos,
  762. });
  763. }
  764. this.triggerDragEvent('onDragOver', e, treeNode);
  765. }
  766. handleNodeDragLeave(e: any, treeNode: BasicTreeNodeData) {
  767. this._adapter.updateState({
  768. dragOverNodeKey: '',
  769. });
  770. this.triggerDragEvent('onDragLeave', e, treeNode);
  771. }
  772. handleNodeDragEnd(e: any, treeNode: BasicTreeNodeData) {
  773. this.clearDragState();
  774. this.triggerDragEvent('onDragEnd', e, treeNode);
  775. this._adapter.setDragNode(null);
  776. }
  777. handleNodeDrop(e: any, treeNode: BasicTreeNodeData, dragNode: any) {
  778. const { dropPosition, dragNodesKeys } = this.getStates();
  779. const { eventKey, pos } = treeNode;
  780. this.clearDragState();
  781. if (dragNodesKeys.has(eventKey)) {
  782. return;
  783. }
  784. const dropRes = {
  785. dragNode: dragNode ? this.getDragEventNodeData(dragNode) : null,
  786. dragNodesKeys: [...dragNodesKeys],
  787. dropPosition: calcDropActualPosition(pos, dropPosition),
  788. dropToGap: dropPosition !== 0,
  789. };
  790. this.triggerDragEvent('onDrop', e, treeNode, dropRes);
  791. this._adapter.setDragNode(null);
  792. }
  793. }