foundation.ts 32 KB

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