foundation.ts 31 KB

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