foundation.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. import { isNumber, isFunction, get, isUndefined, isString, cloneDeep, isEmpty, difference } from 'lodash';
  2. import { strings } from '../treeSelect/constants';
  3. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  4. import {
  5. flattenTreeData,
  6. findDescendantKeys,
  7. findAncestorKeys,
  8. filter,
  9. normalizedArr,
  10. normalizeKeyList,
  11. getMotionKeys,
  12. calcCheckedKeysForChecked,
  13. calcCheckedKeys,
  14. calcCheckedKeysForUnchecked,
  15. getValueOrKey
  16. } from '../tree/treeUtil';
  17. import {
  18. BasicTreeInnerData,
  19. BasicTreeProps,
  20. BasicTreeNodeData,
  21. BasicTreeNodeProps,
  22. BasicKeyEntity,
  23. BasicExpandedOtherProps
  24. } from '../tree/foundation';
  25. import { Motion } from '../utils/type';
  26. import isEnterPress from '../utils/isEnterPress';
  27. /* Here ValidateStatus is the same as ValidateStatus in baseComponent */
  28. export type ValidateStatus = 'error' | 'warning' | 'default';
  29. export type Size = 'small' | 'large' | 'default';
  30. export type BasicRenderSelectedItemInMultiple = (
  31. treeNode: BasicTreeNodeData,
  32. otherProps: { index: number | string; onClose: (tagContent: any, e: any) => void }
  33. ) => {
  34. isRenderInTag: boolean;
  35. content: any;
  36. };
  37. export type BasicRenderSelectedItemInSingle = (treeNode: BasicTreeNodeData) => any;
  38. export type BasicRenderSelectedItem = BasicRenderSelectedItemInSingle | BasicRenderSelectedItemInMultiple;
  39. export interface BasicTriggerRenderProps {
  40. [x: string]: any;
  41. componentProps: BasicTreeSelectProps;
  42. disabled: boolean;
  43. inputValue: string;
  44. placeholder: string;
  45. value: BasicTreeNodeData[];
  46. onClear: (e: any) => void;
  47. }
  48. export type BasicOnChangeWithObject = (node: BasicTreeNodeData[] | BasicTreeNodeData, e: any) => void;
  49. export type BasicOnChangeWithBasic = (
  50. value: BasicTreeNodeData['value'],
  51. node: BasicTreeNodeData[] | BasicTreeNodeData,
  52. e: any
  53. ) => void;
  54. export interface BasicOnChange {
  55. (node: BasicTreeNodeData[] | BasicTreeNodeData, e: any): void;
  56. (
  57. value: BasicTreeNodeData['value'] | Array<BasicTreeNodeData['value']>,
  58. node: BasicTreeNodeData[] | BasicTreeNodeData,
  59. e: any
  60. ): void;
  61. }
  62. export interface BasicTreeSelectProps extends Pick<BasicTreeProps,
  63. 'virtualize'
  64. | 'renderFullLabel'
  65. | 'renderLabel'
  66. | 'autoExpandParent'
  67. | 'className'
  68. | 'defaultExpandAll'
  69. | 'defaultExpandedKeys'
  70. | 'defaultValue'
  71. | 'disabled'
  72. | 'emptyContent'
  73. | 'expandAction'
  74. | 'expandedKeys'
  75. | 'filterTreeNode'
  76. | 'labelEllipsis'
  77. | 'leafOnly'
  78. | 'multiple'
  79. | 'onChangeWithObject'
  80. | 'showClear'
  81. | 'showFilteredOnly'
  82. | 'style'
  83. | 'treeData'
  84. | 'treeNodeFilterProp'
  85. | 'value'
  86. | 'onExpand'
  87. | 'expandAll'
  88. | 'disableStrictly'
  89. | 'aria-label'
  90. | 'checkRelation'
  91. > {
  92. motion?: Motion;
  93. mouseEnterDelay?: number;
  94. mouseLeaveDelay?: number;
  95. arrowIcon?: any;
  96. autoAdjustOverflow?: boolean;
  97. clickToHide?: boolean;
  98. defaultOpen?: boolean;
  99. dropdownClassName?: string;
  100. dropdownMatchSelectWidth?: boolean;
  101. dropdownStyle?: any;
  102. insetLabel?: any;
  103. maxTagCount?: number;
  104. motionExpand?: boolean;
  105. optionListStyle?: any;
  106. outerBottomSlot?: any;
  107. outerTopSlot?: any;
  108. placeholder?: string;
  109. prefix?: any;
  110. searchAutoFocus?: boolean;
  111. searchPlaceholder?: string;
  112. showSearchClear?: boolean;
  113. size?: Size;
  114. suffix?: any;
  115. treeNodeLabelProp?: string;
  116. validateStatus?: ValidateStatus;
  117. zIndex?: number;
  118. searchPosition?: string;
  119. stopPropagation?: boolean | string;
  120. loadedKeys?: string[];
  121. loadData?: (data: BasicTreeNodeData) => Promise<void>;
  122. onSelect?: (selectedKeys: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
  123. searchRender?: (inputProps: any) => any;
  124. renderSelectedItem?: BasicRenderSelectedItem;
  125. getPopupContainer?: () => HTMLElement;
  126. // triggerRender?: (props: BasicTriggerRenderProps) => any;
  127. onBlur?: (e: any) => void;
  128. onSearch?: (sunInput: string, filteredExpandedKeys: string[]) => void;
  129. onChange?: BasicOnChange;
  130. onFocus?: (e: any) => void;
  131. onVisibleChange?: (isVisible: boolean) => void;
  132. onLoad?: (keys: Set<string>, data: BasicTreeNodeData) => void;
  133. }
  134. export interface BasicTreeSelectInnerData extends Pick<BasicTreeInnerData,
  135. 'keyEntities'
  136. | 'treeData'
  137. | 'flattenNodes'
  138. | 'selectedKeys'
  139. | 'checkedKeys'
  140. | 'halfCheckedKeys'
  141. | 'motionKeys'
  142. | 'motionType'
  143. | 'expandedKeys'
  144. | 'filteredKeys'
  145. | 'filteredExpandedKeys'
  146. | 'filteredShownKeys'
  147. | 'cachedKeyValuePairs'
  148. | 'inputValue'
  149. | 'disabledKeys'
  150. | 'loadedKeys'
  151. | 'loadingKeys'
  152. | 'realCheckedKeys'
  153. > {
  154. inputTriggerFocus: boolean;
  155. isOpen: boolean;
  156. isInput: boolean;
  157. rePosKey: number;
  158. dropdownMinWidth: null | number;
  159. isHovering: boolean;
  160. prevProps: BasicTreeSelectProps;
  161. }
  162. export interface TreeSelectAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  163. updateInputValue: (value: string) => void;
  164. registerClickOutsideHandler: (cb: (e: any) => void) => void;
  165. unregisterClickOutsideHandler: () => void;
  166. rePositionDropdown: () => void;
  167. updateState: (states: Partial<BasicTreeSelectInnerData>) => void;
  168. notifySelect: (selectedKeys: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
  169. notifySearch: (input: string, filteredExpandedKeys: string[]) => void;
  170. cacheFlattenNodes: (bool: boolean) => void;
  171. openMenu: () => void;
  172. closeMenu: (cb?: () => void) => void;
  173. getTriggerWidth: () => boolean | number;
  174. setOptionWrapperWidth: (width: null | number) => void;
  175. notifyChange: BasicOnChangeWithBasic;
  176. notifyChangeWithObject: BasicOnChangeWithObject;
  177. notifyExpand: (expandedKeys: Set<string>, expandedOtherProps: BasicExpandedOtherProps) => void;
  178. notifyFocus: (e: any) => void;
  179. notifyBlur: (e: any) => void;
  180. toggleHovering: (bool: boolean) => void;
  181. notifyLoad: (newLoadedKeys: Set<string>, data: BasicTreeNodeData) => void;
  182. updateInputFocus: (bool: boolean) => void;
  183. updateLoadKeys: (data: BasicTreeNodeData, resolve: (value?: any) => void) => void;
  184. }
  185. // eslint-disable-next-line max-len
  186. export default class TreeSelectFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TreeSelectAdapter<P, S>, P, S> {
  187. constructor(adapter: TreeSelectAdapter<P, S>) {
  188. super({ ...adapter });
  189. }
  190. init() {
  191. const { searchAutoFocus, searchPosition, filterTreeNode } = this.getProps();
  192. const triggerSearch = searchPosition === strings.SEARCH_POSITION_TRIGGER && filterTreeNode;
  193. const triggerSearchAutoFocus = searchAutoFocus && triggerSearch;
  194. this._setDropdownWidth();
  195. const isOpen = (this.getProp('defaultOpen') || triggerSearchAutoFocus) && !this._isDisabled();
  196. if (isOpen) {
  197. this.open();
  198. }
  199. }
  200. destroy() {
  201. // Ensure that event monitoring will be uninstalled, and the user may not trigger closePanel
  202. this._adapter.unregisterClickOutsideHandler();
  203. }
  204. _setDropdownWidth() {
  205. const { style, dropdownMatchSelectWidth } = this.getProps();
  206. let width;
  207. if (dropdownMatchSelectWidth) {
  208. if (style && isNumber(style.width)) {
  209. width = style.width;
  210. } else if (style && isString(style.width) && !style.width.includes('%')) {
  211. width = style.width;
  212. } else {
  213. width = this._adapter.getTriggerWidth();
  214. }
  215. this._adapter.setOptionWrapperWidth(width);
  216. }
  217. }
  218. _isMultiple() {
  219. return this.getProp('multiple');
  220. }
  221. _isAnimated() {
  222. return this.getProp('motionExpand');
  223. }
  224. _isDisabled(treeNode = {} as BasicTreeNodeProps) {
  225. return this.getProp('disabled') || treeNode.disabled;
  226. }
  227. _isExpandControlled() {
  228. return this.getProp('expandedKeys');
  229. }
  230. _isSelectToClose() {
  231. return !this.getProp('expandAction');
  232. }
  233. _isLoadControlled() {
  234. return this.getProp('loadedKeys');
  235. }
  236. _showFilteredOnly() {
  237. const { inputValue } = this.getStates();
  238. const { showFilteredOnly } = this.getProps();
  239. return Boolean(inputValue) && showFilteredOnly;
  240. }
  241. getCopyFromState(items: string | string[]) {
  242. const res = {};
  243. normalizedArr(items).forEach(key => {
  244. res[key] = cloneDeep(this.getState(key));
  245. });
  246. return res as BasicTreeInnerData;
  247. }
  248. getTreeNodeProps(key: string) {
  249. const {
  250. expandedKeys = new Set([]),
  251. selectedKeys = [],
  252. checkedKeys = new Set([]),
  253. halfCheckedKeys = new Set([]),
  254. realCheckedKeys = new Set([]),
  255. keyEntities = {},
  256. filteredKeys = new Set([]),
  257. inputValue = '',
  258. loadedKeys,
  259. loadingKeys,
  260. filteredExpandedKeys = new Set([]),
  261. disabledKeys = new Set([]),
  262. } = this.getStates();
  263. const { treeNodeFilterProp, checkRelation } = this.getProps();
  264. const entity = keyEntities[key];
  265. const notExist = !entity;
  266. if (notExist) {
  267. return null;
  268. }
  269. // if checkRelation is invalid, the checked status of node will be false
  270. let realChecked = false;
  271. let realHalfChecked = false;
  272. if (checkRelation === 'related') {
  273. realChecked = checkedKeys.has(key);
  274. realHalfChecked = halfCheckedKeys.has(key);
  275. } else if (checkRelation === 'unRelated') {
  276. realChecked = realCheckedKeys.has(key);
  277. realHalfChecked = false;
  278. }
  279. const isSearching = Boolean(inputValue);
  280. const treeNodeProps: BasicTreeNodeProps = {
  281. eventKey: key,
  282. expanded: isSearching && !this._isExpandControlled()
  283. ? filteredExpandedKeys.has(key)
  284. : expandedKeys.has(key),
  285. selected: selectedKeys.includes(key),
  286. checked: realChecked,
  287. halfChecked: realHalfChecked,
  288. pos: String(entity ? entity.pos : ''),
  289. level: entity.level,
  290. filtered: filteredKeys.has(key),
  291. keyword: inputValue,
  292. treeNodeFilterProp,
  293. loading: loadingKeys.has(key) && !loadedKeys.has(key),
  294. loaded: loadedKeys.has(key),
  295. };
  296. if (this.getProp('disableStrictly') && disabledKeys.has(key)) {
  297. treeNodeProps.disabled = true;
  298. }
  299. return treeNodeProps;
  300. }
  301. handleNodeLoad(loadedKeys: Set<string>, loadingKeys: Set<string>, data: BasicTreeNodeData, resolve: (value?: any) => void) {
  302. const { loadData } = this.getProps();
  303. const { key } = data;
  304. if (!loadData || loadedKeys.has(key) || loadingKeys.has(key)) {
  305. return {};
  306. }
  307. loadData(data).then(() => {
  308. const {
  309. loadedKeys: prevLoadedKeys,
  310. loadingKeys: prevLoadingKeys
  311. } = this.getCopyFromState(['loadedKeys', 'loadingKeys']);
  312. const newLoadedKeys: Set<string> = prevLoadedKeys.add(key);
  313. const newLoadingKeys: Set<string> = new Set([...prevLoadingKeys]);
  314. newLoadingKeys.delete(key);
  315. this._adapter.notifyLoad(newLoadedKeys, data);
  316. if (!this._isLoadControlled()) {
  317. this._adapter.updateState({
  318. loadedKeys: newLoadedKeys,
  319. });
  320. }
  321. this._adapter.setState({
  322. loadingKeys: newLoadingKeys,
  323. } as any);
  324. resolve();
  325. });
  326. return {
  327. loadingKeys: loadingKeys.add(key),
  328. };
  329. }
  330. /* istanbul ignore next */
  331. focusInput(bool: boolean) {
  332. this._adapter.updateInputFocus(bool);
  333. }
  334. _notifyMultipleChange(key: string[], e: any) {
  335. const { keyEntities } = this.getStates();
  336. const { leafOnly, checkRelation } = this.getProps();
  337. let keyList = [];
  338. if (checkRelation === 'related') {
  339. keyList = normalizeKeyList(key, keyEntities, leafOnly);
  340. } else if (checkRelation === 'unRelated') {
  341. keyList = key as string[];
  342. }
  343. const nodes = keyList.map(i => keyEntities[i].data);
  344. if (this.getProp('onChangeWithObject')) {
  345. this._adapter.notifyChangeWithObject(nodes, e);
  346. } else {
  347. const value = getValueOrKey(nodes);
  348. this._adapter.notifyChange(value, nodes, e);
  349. }
  350. }
  351. _notifyChange(key: any, e: any) {
  352. const { keyEntities } = this.getStates();
  353. if (this._isMultiple() && Array.isArray(key)) {
  354. this._notifyMultipleChange(key, e);
  355. } else {
  356. const nodes = isUndefined(key) ? key : keyEntities[key].data;
  357. const value = isUndefined(key) ? key : getValueOrKey(nodes);
  358. if (this.getProp('onChangeWithObject')) {
  359. this._adapter.notifyChangeWithObject(nodes, e);
  360. } else {
  361. this._adapter.notifyChange(value, nodes, e);
  362. }
  363. }
  364. }
  365. // Scenes that may trigger focus:
  366. // 1、click selection
  367. _notifyFocus(e: any) {
  368. this._adapter.notifyFocus(e);
  369. }
  370. // Scenes that may trigger blur
  371. // 1、clickOutSide
  372. // 2、click option / press enter, and then select complete(when multiple is false
  373. // 3、press esc when dropdown list open
  374. _notifyBlur(e: any) {
  375. this._adapter.notifyBlur(e);
  376. }
  377. toggleHoverState(bool: boolean) {
  378. this._adapter.toggleHovering(bool);
  379. }
  380. open() {
  381. this._adapter.openMenu();
  382. this._setDropdownWidth();
  383. this._adapter.registerClickOutsideHandler(e => {
  384. this.close(e);
  385. });
  386. }
  387. close(e: any) {
  388. this._adapter.closeMenu();
  389. this._adapter.unregisterClickOutsideHandler();
  390. this._notifyBlur(e);
  391. if (this.getProp('motionExpand')) {
  392. this._adapter.updateState({ motionKeys: new Set([]) });
  393. }
  394. }
  395. handleClick(e: any) {
  396. const isDisabled = this._isDisabled();
  397. const { isOpen, inputValue } = this.getStates();
  398. const { searchPosition } = this.getProps();
  399. if (isDisabled) {
  400. return;
  401. } else if (!isOpen) {
  402. this.open();
  403. this._notifyFocus(e);
  404. } else if (isOpen) {
  405. if (searchPosition === 'trigger' && inputValue) {
  406. return;
  407. }
  408. this.close(e);
  409. }
  410. }
  411. /**
  412. * A11y: simulate selection click
  413. */
  414. /* istanbul ignore next */
  415. handleSelectionEnterPress(e: any) {
  416. if (isEnterPress(e)) {
  417. this.handleClick(e);
  418. }
  419. }
  420. handleClear(e: any) {
  421. const { searchPosition, filterTreeNode } = this.getProps();
  422. const { inputValue, selectedKeys } = this.getStates();
  423. const isMultiple = this._isMultiple();
  424. const isControlled = this._isControlledComponent();
  425. const value: string | string[] | undefined = isMultiple ? [] : undefined;
  426. this._notifyChange(value, e);
  427. if (!isControlled) {
  428. // reposition dropdown when selected values change
  429. this._adapter.rePositionDropdown();
  430. this._adapter.updateState({
  431. selectedKeys: [],
  432. checkedKeys: new Set(),
  433. halfCheckedKeys: new Set(),
  434. realCheckedKeys: new Set([]),
  435. });
  436. }
  437. // When triggerSearch, clicking the clear button will trigger to clear Input
  438. if (filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER) {
  439. if (inputValue !== '') {
  440. if (isEmpty(selectedKeys)) {
  441. this.handleInputChange('');
  442. } else {
  443. this.clearInput();
  444. }
  445. }
  446. }
  447. }
  448. /**
  449. * A11y: simulate clear button click
  450. */
  451. /* istanbul ignore next */
  452. handleClearEnterPress(e: any) {
  453. if (isEnterPress(e)) {
  454. this.handleClear(e);
  455. }
  456. }
  457. removeTag(eventKey: BasicTreeNodeData['key']) {
  458. const { disableStrictly, checkRelation } = this.getProps();
  459. const { keyEntities, disabledKeys, realCheckedKeys } = this.getStates();
  460. const item = keyEntities[eventKey].data;
  461. if (item.disabled || (disableStrictly && disabledKeys.has(eventKey))) {
  462. return;
  463. }
  464. if (checkRelation === 'unRelated') {
  465. const newRealCheckedKeys = new Set(realCheckedKeys);
  466. newRealCheckedKeys.delete(eventKey);
  467. this._notifyChange([...newRealCheckedKeys], null);
  468. if (!this._isControlledComponent()) {
  469. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys } as any);
  470. this._adapter.rePositionDropdown();
  471. }
  472. } else if (checkRelation === 'related') {
  473. const { checkedKeys, halfCheckedKeys } = this.calcCheckedKeys(eventKey, false);
  474. this._notifyChange([...checkedKeys], null);
  475. if (!this._isControlledComponent()) {
  476. this._adapter.updateState({ checkedKeys, halfCheckedKeys });
  477. this._adapter.rePositionDropdown();
  478. }
  479. }
  480. this._adapter.notifySelect(eventKey, false, item);
  481. // reposition dropdown when selected values change
  482. this._adapter.rePositionDropdown();
  483. }
  484. clearInput() {
  485. const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  486. const newExpandedKeys: Set<string> = new Set(expandedKeys);
  487. const isExpandControlled = this._isExpandControlled();
  488. const expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
  489. expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
  490. const newFlattenNodes = flattenTreeData(treeData, newExpandedKeys);
  491. this._adapter.updateState({
  492. expandedKeys: isExpandControlled ? expandedKeys : newExpandedKeys,
  493. flattenNodes: isExpandControlled ? flattenNodes : newFlattenNodes,
  494. inputValue: '',
  495. motionKeys: new Set([]),
  496. filteredKeys: new Set([]),
  497. filteredExpandedKeys: new Set(expandedOptsKeys),
  498. filteredShownKeys: new Set([])
  499. });
  500. }
  501. handleInputChange(sugInput: string) {
  502. // Input is used as controlled component
  503. this._adapter.updateInputValue(sugInput);
  504. const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  505. const { showFilteredOnly, filterTreeNode, treeNodeFilterProp } = this.getProps();
  506. const newExpandedKeys: Set<string> = new Set(expandedKeys);
  507. let filteredOptsKeys: string[] = [];
  508. let expandedOptsKeys = [];
  509. let newFlattenNodes = [];
  510. let filteredShownKeys = new Set([]);
  511. if (!sugInput) {
  512. expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
  513. expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
  514. newFlattenNodes = flattenTreeData(treeData, newExpandedKeys);
  515. } else {
  516. filteredOptsKeys = Object.values(keyEntities)
  517. .filter((item: BasicKeyEntity) => {
  518. const { data } = item;
  519. return filter(sugInput, data, filterTreeNode, treeNodeFilterProp);
  520. })
  521. .map((item: BasicKeyEntity) => item.key);
  522. expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
  523. const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
  524. filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
  525. newFlattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), showFilteredOnly && filteredShownKeys);
  526. }
  527. const newFilteredExpandedKeys = new Set(expandedOptsKeys);
  528. this._adapter.notifySearch(sugInput, Array.from(newFilteredExpandedKeys));
  529. this._adapter.updateState({
  530. expandedKeys: this._isExpandControlled() ? expandedKeys : newExpandedKeys,
  531. flattenNodes: this._isExpandControlled() ? flattenNodes : newFlattenNodes,
  532. motionKeys: new Set([]),
  533. filteredKeys: new Set(filteredOptsKeys),
  534. filteredExpandedKeys: newFilteredExpandedKeys,
  535. filteredShownKeys,
  536. });
  537. }
  538. handleNodeSelect(e: any, treeNode: BasicTreeNodeProps) {
  539. const isDisabled = this._isDisabled(treeNode);
  540. if (isDisabled) {
  541. return;
  542. }
  543. if (!this._isMultiple()) {
  544. this.handleSingleSelect(e, treeNode);
  545. } else {
  546. this.handleMultipleSelect(e, treeNode);
  547. }
  548. }
  549. handleSingleSelect(e: any, treeNode: BasicTreeNodeProps) {
  550. let { selectedKeys } = this.getCopyFromState('selectedKeys');
  551. const { clickToHide } = this.getProps();
  552. const { selected, eventKey, data } = treeNode;
  553. this._adapter.notifySelect(eventKey, true, data);
  554. if (!selectedKeys.includes(eventKey) && !selected) {
  555. selectedKeys = [eventKey];
  556. this._notifyChange(eventKey, e);
  557. if (!this._isControlledComponent()) {
  558. this._adapter.updateState({ selectedKeys });
  559. }
  560. }
  561. if (clickToHide && (this._isSelectToClose() || !data.children)) {
  562. this.close(e);
  563. }
  564. }
  565. calcCheckedKeys(eventKey: BasicTreeNodeProps['eventKey'], targetStatus: boolean) {
  566. const { keyEntities } = this.getStates();
  567. const {
  568. checkedKeys,
  569. halfCheckedKeys
  570. } = this.getCopyFromState(['checkedKeys', 'halfCheckedKeys']);
  571. if (targetStatus) {
  572. return calcCheckedKeysForChecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  573. } else {
  574. return calcCheckedKeysForUnchecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  575. }
  576. }
  577. handleMultipleSelect(e: any, treeNode: BasicTreeNodeProps) {
  578. const { searchPosition, disableStrictly, checkRelation } = this.getProps();
  579. const { inputValue, realCheckedKeys } = this.getStates();
  580. const { checked, eventKey, data } = treeNode;
  581. if (checkRelation === 'related') {
  582. const targetStatus = disableStrictly ?
  583. this.calcCheckedStatus(!checked, eventKey) :
  584. !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. this._adapter.rePositionDropdown();
  593. }
  594. } else if (checkRelation === 'unRelated') {
  595. const newRealCheckedKeys: Set<string> = new Set(realCheckedKeys);
  596. let targetStatus: boolean;
  597. if (realCheckedKeys.has(eventKey)) {
  598. newRealCheckedKeys.delete(eventKey);
  599. targetStatus = false;
  600. } else {
  601. newRealCheckedKeys.add(eventKey);
  602. targetStatus = true;
  603. }
  604. this._adapter.notifySelect(eventKey, targetStatus, data);
  605. this._notifyChange([...newRealCheckedKeys], e);
  606. if (!this._isControlledComponent()) {
  607. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys });
  608. this._adapter.rePositionDropdown();
  609. }
  610. }
  611. if (searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue !== '') {
  612. this._adapter.updateState({ inputValue: '' });
  613. }
  614. }
  615. calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean) {
  616. const { keyEntities, disabledKeys } = this.getStates();
  617. const { checkedKeys } = this.getCopyFromState(['checkedKeys']);
  618. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  619. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  620. if (!hasDisabled) {
  621. return this.calcCheckedKeys(eventKey, targetStatus);
  622. }
  623. const nonDisabled = descendantKeys.filter(key => !disabledKeys.has(key));
  624. const newCheckedKeys = targetStatus ?
  625. [...nonDisabled, ...checkedKeys] :
  626. difference(normalizeKeyList([...checkedKeys], keyEntities, true), nonDisabled);
  627. return calcCheckedKeys(newCheckedKeys, keyEntities);
  628. }
  629. calcCheckedStatus(targetStatus: boolean, eventKey: string) {
  630. if (!targetStatus) {
  631. return targetStatus;
  632. }
  633. const { checkedKeys, keyEntities, disabledKeys } = this.getStates();
  634. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  635. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  636. if (!hasDisabled) {
  637. return targetStatus;
  638. }
  639. const nonDisabledKeys = descendantKeys.filter(key => !disabledKeys.has(key));
  640. const allChecked = nonDisabledKeys.every(key => checkedKeys.has(key));
  641. return !allChecked;
  642. }
  643. handleNodeExpandInSearch(e: any, treeNode: BasicTreeNodeProps) {
  644. const { treeData, filteredShownKeys, keyEntities } = this.getStates();
  645. const showFilteredOnly = this._showFilteredOnly();
  646. // clone otherwise will be modified unexpectedly
  647. const { filteredExpandedKeys } = this.getCopyFromState('filteredExpandedKeys');
  648. let motionType = 'show';
  649. const { eventKey, expanded, data } = treeNode;
  650. // debugger;
  651. if (!expanded) {
  652. filteredExpandedKeys.add(eventKey);
  653. } else if (filteredExpandedKeys.has(eventKey)) {
  654. filteredExpandedKeys.delete(eventKey);
  655. motionType = 'hide';
  656. }
  657. // cache prev flattenNodes, otherwise the calculation will remove hidden items
  658. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  659. if (!this._isExpandControlled()) {
  660. // debugger;
  661. const flattenNodes = flattenTreeData(treeData, filteredExpandedKeys, showFilteredOnly && filteredShownKeys);
  662. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, filteredExpandedKeys, keyEntities) : [];
  663. const newState = {
  664. filteredExpandedKeys,
  665. flattenNodes,
  666. motionKeys: new Set(motionKeys),
  667. motionType,
  668. };
  669. this._adapter.updateState(newState);
  670. }
  671. this._adapter.notifyExpand(filteredExpandedKeys, {
  672. expanded: !expanded,
  673. node: data,
  674. });
  675. }
  676. handleNodeExpand(e: any, treeNode: BasicTreeNodeProps) {
  677. // debugger;
  678. const { loadData } = this.getProps();
  679. const { inputValue, keyEntities } = this.getStates();
  680. const isSearching = Boolean(inputValue);
  681. if (!loadData && (!treeNode.children || !treeNode.children.length)) {
  682. return;
  683. }
  684. const isExpandControlled = this._isExpandControlled();
  685. if (isSearching && !isExpandControlled) {
  686. this.handleNodeExpandInSearch(e, treeNode);
  687. return;
  688. }
  689. const { treeData } = this.getStates();
  690. // clone otherwise will be modified unexpectedly
  691. const { expandedKeys } = this.getCopyFromState('expandedKeys');
  692. let motionType = 'show';
  693. const { eventKey, expanded, data } = treeNode;
  694. if (!expanded) {
  695. expandedKeys.add(eventKey);
  696. } else if (expandedKeys.has(eventKey)) {
  697. expandedKeys.delete(eventKey);
  698. motionType = 'hide';
  699. }
  700. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  701. if (!isExpandControlled) {
  702. // debugger;
  703. const flattenNodes = flattenTreeData(treeData, expandedKeys);
  704. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, expandedKeys, keyEntities) : [];
  705. const newState = {
  706. expandedKeys,
  707. flattenNodes,
  708. motionKeys: new Set(motionKeys),
  709. motionType,
  710. };
  711. this._adapter.updateState(newState);
  712. }
  713. this._adapter.notifyExpand(expandedKeys, {
  714. expanded: !expanded,
  715. node: data,
  716. });
  717. }
  718. /**
  719. * The selected items that need to be displayed in the search box when obtaining a single selection
  720. */
  721. getRenderTextInSingle() {
  722. const { renderSelectedItem: propRenderSelectedItem, treeNodeLabelProp } = this.getProps();
  723. const { selectedKeys, keyEntities } = this.getStates();
  724. const renderSelectedItem = isFunction(propRenderSelectedItem) ?
  725. propRenderSelectedItem :
  726. (item: BasicTreeNodeData) => get(item, treeNodeLabelProp, null);
  727. const item = selectedKeys.length && keyEntities[selectedKeys[0]] ?
  728. keyEntities[selectedKeys[0]].data :
  729. undefined;
  730. const renderText = item && treeNodeLabelProp in item ? renderSelectedItem(item) : null;
  731. return renderText;
  732. }
  733. /**
  734. * When the search box is on the trigger, the blur event handling method
  735. */
  736. handleInputTriggerBlur() {
  737. this._adapter.updateState({
  738. inputTriggerFocus: false
  739. });
  740. }
  741. /**
  742. * When the search box is on the trigger, the focus event processing method
  743. */
  744. handleInputTriggerFocus() {
  745. this.clearInput();
  746. this._adapter.updateState({
  747. inputTriggerFocus: true
  748. });
  749. }
  750. setLoadKeys(data: BasicTreeNodeData, resolve: (value?: any) => void) {
  751. this._adapter.updateLoadKeys(data, resolve);
  752. }
  753. }