foundation.ts 32 KB

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