foundation.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  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?: (selectedKeys: 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: (selectedKeys: 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. // eslint-disable-next-line max-len
  194. export default class TreeSelectFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TreeSelectAdapter<P, S>, P, S> {
  195. constructor(adapter: TreeSelectAdapter<P, S>) {
  196. super({ ...adapter });
  197. }
  198. init() {
  199. const { searchAutoFocus, searchPosition, filterTreeNode } = this.getProps();
  200. const triggerSearch = searchPosition === strings.SEARCH_POSITION_TRIGGER && filterTreeNode;
  201. const triggerSearchAutoFocus = searchAutoFocus && triggerSearch;
  202. this._setDropdownWidth();
  203. const isOpen = (this.getProp('defaultOpen') || triggerSearchAutoFocus) && !this._isDisabled();
  204. if (isOpen) {
  205. this.open();
  206. }
  207. if (triggerSearchAutoFocus) {
  208. this.handleTriggerFocus(null);
  209. }
  210. }
  211. destroy() {
  212. // Ensure that event monitoring will be uninstalled, and the user may not trigger closePanel
  213. this._adapter.unregisterClickOutsideHandler();
  214. }
  215. _setDropdownWidth() {
  216. const { style, dropdownMatchSelectWidth } = this.getProps();
  217. let width;
  218. if (dropdownMatchSelectWidth) {
  219. if (style && isNumber(style.width)) {
  220. width = style.width;
  221. } else if (style && isString(style.width) && !style.width.includes('%')) {
  222. width = style.width;
  223. } else {
  224. width = this._adapter.getTriggerWidth();
  225. }
  226. this._adapter.setOptionWrapperWidth(width);
  227. }
  228. }
  229. _isMultiple() {
  230. return this.getProp('multiple');
  231. }
  232. _isAnimated() {
  233. return this.getProp('motionExpand');
  234. }
  235. _isDisabled(treeNode = {} as BasicTreeNodeProps) {
  236. return this.getProp('disabled') || treeNode.disabled;
  237. }
  238. _isExpandControlled() {
  239. return this.getProp('expandedKeys');
  240. }
  241. _isSelectToClose() {
  242. return !this.getProp('expandAction');
  243. }
  244. _isLoadControlled() {
  245. return this.getProp('loadedKeys');
  246. }
  247. _showFilteredOnly() {
  248. const { inputValue } = this.getStates();
  249. const { showFilteredOnly } = this.getProps();
  250. return Boolean(inputValue) && showFilteredOnly;
  251. }
  252. findDataForValue(findValue: string) {
  253. const { value, defaultValue } = this.getProps();
  254. let valueArr = [];
  255. if (value) {
  256. valueArr = Array.isArray(value) ? value : [value];
  257. } else if (defaultValue) {
  258. valueArr = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
  259. }
  260. return valueArr.find(item => {
  261. return item.value === findValue || item.key === findValue;
  262. });
  263. }
  264. constructDataForValue(value: string) {
  265. const { treeNodeLabelProp } = this.getProps();
  266. return {
  267. key: value,
  268. [treeNodeLabelProp]: value
  269. };
  270. }
  271. getDataForKeyNotInKeyEntities(value: string) {
  272. const { onChangeWithObject } = this.getProps();
  273. if (onChangeWithObject) {
  274. return this.findDataForValue(value);
  275. } else {
  276. return this.constructDataForValue(value);
  277. }
  278. }
  279. getCopyFromState(items: string | string[]) {
  280. const res = {};
  281. normalizedArr(items).forEach(key => {
  282. res[key] = cloneDeep(this.getState(key));
  283. });
  284. return res as BasicTreeInnerData;
  285. }
  286. getTreeNodeProps(key: string) {
  287. const {
  288. expandedKeys = new Set([]),
  289. selectedKeys = [],
  290. checkedKeys = new Set([]),
  291. halfCheckedKeys = new Set([]),
  292. realCheckedKeys = new Set([]),
  293. keyEntities = {},
  294. filteredKeys = new Set([]),
  295. inputValue = '',
  296. loadedKeys,
  297. loadingKeys,
  298. filteredExpandedKeys = new Set([]),
  299. disabledKeys = new Set([]),
  300. } = this.getStates();
  301. const { treeNodeFilterProp, checkRelation } = this.getProps();
  302. const entity = keyEntities[key];
  303. const notExist = !entity;
  304. if (notExist) {
  305. return null;
  306. }
  307. // if checkRelation is invalid, the checked status of node will be false
  308. let realChecked = false;
  309. let realHalfChecked = false;
  310. if (checkRelation === 'related') {
  311. realChecked = checkedKeys.has(key);
  312. realHalfChecked = halfCheckedKeys.has(key);
  313. } else if (checkRelation === 'unRelated') {
  314. realChecked = realCheckedKeys.has(key);
  315. realHalfChecked = false;
  316. }
  317. const isSearching = Boolean(inputValue);
  318. const treeNodeProps: BasicTreeNodeProps = {
  319. eventKey: key,
  320. expanded: isSearching && !this._isExpandControlled()
  321. ? filteredExpandedKeys.has(key)
  322. : expandedKeys.has(key),
  323. selected: selectedKeys.includes(key),
  324. checked: realChecked,
  325. halfChecked: realHalfChecked,
  326. pos: String(entity ? entity.pos : ''),
  327. level: entity.level,
  328. filtered: filteredKeys.has(key),
  329. keyword: inputValue,
  330. treeNodeFilterProp,
  331. loading: loadingKeys.has(key) && !loadedKeys.has(key),
  332. loaded: loadedKeys.has(key),
  333. };
  334. if (this.getProp('disableStrictly') && disabledKeys.has(key)) {
  335. treeNodeProps.disabled = true;
  336. }
  337. return treeNodeProps;
  338. }
  339. handleNodeLoad(loadedKeys: Set<string>, loadingKeys: Set<string>, data: BasicTreeNodeData, resolve: (value?: any) => void) {
  340. const { loadData } = this.getProps();
  341. const { key } = data;
  342. if (!loadData || loadedKeys.has(key) || loadingKeys.has(key)) {
  343. return {};
  344. }
  345. loadData(data).then(() => {
  346. const {
  347. loadedKeys: prevLoadedKeys,
  348. loadingKeys: prevLoadingKeys
  349. } = this.getCopyFromState(['loadedKeys', 'loadingKeys']);
  350. const newLoadedKeys: Set<string> = prevLoadedKeys.add(key);
  351. const newLoadingKeys: Set<string> = new Set([...prevLoadingKeys]);
  352. newLoadingKeys.delete(key);
  353. this._adapter.notifyLoad(newLoadedKeys, data);
  354. if (!this._isLoadControlled()) {
  355. this._adapter.updateState({
  356. loadedKeys: newLoadedKeys,
  357. });
  358. }
  359. this._adapter.setState({
  360. loadingKeys: newLoadingKeys,
  361. } as any);
  362. resolve();
  363. });
  364. return {
  365. loadingKeys: loadingKeys.add(key),
  366. };
  367. }
  368. /* istanbul ignore next */
  369. focusInput(bool: boolean) {
  370. this._adapter.updateInputFocus(bool);
  371. }
  372. _notifyMultipleChange(key: string[], e: any) {
  373. const { keyEntities } = this.getStates();
  374. const { leafOnly, checkRelation } = this.getProps();
  375. let keyList = [];
  376. if (checkRelation === 'related') {
  377. keyList = normalizeKeyList(key, keyEntities, leafOnly, true);
  378. } else if (checkRelation === 'unRelated') {
  379. keyList = key as string[];
  380. }
  381. const nodes = keyList.map(key => (keyEntities[key] && keyEntities[key].data.key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key));
  382. if (this.getProp('onChangeWithObject')) {
  383. this._adapter.notifyChangeWithObject(nodes, e);
  384. } else {
  385. const value = getValueOrKey(nodes);
  386. this._adapter.notifyChange(value, nodes, e);
  387. }
  388. }
  389. _notifyChange(key: any, e: any) {
  390. const { keyEntities } = this.getStates();
  391. if (this._isMultiple() && Array.isArray(key)) {
  392. this._notifyMultipleChange(key, e);
  393. } else {
  394. const nodes = isUndefined(key) ? key : keyEntities[key].data;
  395. const value = isUndefined(key) ? key : getValueOrKey(nodes);
  396. if (this.getProp('onChangeWithObject')) {
  397. this._adapter.notifyChangeWithObject(nodes, e);
  398. } else {
  399. this._adapter.notifyChange(value, nodes, e);
  400. }
  401. }
  402. }
  403. _registerClickOutsideHandler = (e) => {
  404. this._adapter.registerClickOutsideHandler(e => {
  405. this.handlerTriggerBlur(e);
  406. this.close(e);
  407. });
  408. }
  409. // Scenes that may trigger focus:
  410. // 1、click selection
  411. _notifyFocus(e: any) {
  412. this._adapter.notifyFocus(e);
  413. }
  414. handleTriggerFocus(e: any) {
  415. this._adapter.updateIsFocus(true);
  416. this._notifyFocus(e);
  417. this._registerClickOutsideHandler(e);
  418. }
  419. // Scenes that may trigger blur
  420. // 1、clickOutSide
  421. // 2、click option / press enter, and then select complete(when multiple is false
  422. // 3、press esc when dropdown list open
  423. _notifyBlur(e: any) {
  424. this._adapter.notifyBlur(e);
  425. }
  426. handlerTriggerBlur(e) {
  427. this._adapter.updateIsFocus(false);
  428. this._notifyBlur(e);
  429. this._adapter.unregisterClickOutsideHandler();
  430. }
  431. toggleHoverState(bool: boolean) {
  432. this._adapter.toggleHovering(bool);
  433. }
  434. open() {
  435. this._adapter.openMenu();
  436. this._setDropdownWidth();
  437. }
  438. close(e: any) {
  439. this._adapter.closeMenu();
  440. if (this.getProp('motionExpand')) {
  441. this._adapter.updateState({ motionKeys: new Set([]) });
  442. }
  443. }
  444. handleClick(e: any) {
  445. const isDisabled = this._isDisabled();
  446. const { isOpen, inputValue, isFocus } = this.getStates();
  447. const { searchPosition, clickTriggerToHide } = this.getProps();
  448. if (isDisabled) {
  449. return;
  450. } else {
  451. if (!isFocus) {
  452. this.handleTriggerFocus(e);
  453. }
  454. if (isOpen) {
  455. if (searchPosition === 'trigger' && inputValue) {
  456. return;
  457. }
  458. clickTriggerToHide && this.close(e);
  459. } else {
  460. this.open();
  461. }
  462. }
  463. }
  464. /**
  465. * A11y: simulate selection click
  466. */
  467. /* istanbul ignore next */
  468. handleSelectionEnterPress(e: any) {
  469. if (isEnterPress(e)) {
  470. this.handleClick(e);
  471. }
  472. }
  473. handleClear(e: any) {
  474. const { searchPosition, filterTreeNode } = this.getProps();
  475. const { inputValue, selectedKeys } = this.getStates();
  476. const isMultiple = this._isMultiple();
  477. const isControlled = this._isControlledComponent();
  478. const value: string | string[] | undefined = isMultiple ? [] : undefined;
  479. this._notifyChange(value, e);
  480. if (!isControlled) {
  481. // reposition dropdown when selected values change
  482. this._adapter.rePositionDropdown();
  483. this._adapter.updateState({
  484. selectedKeys: [],
  485. checkedKeys: new Set(),
  486. halfCheckedKeys: new Set(),
  487. realCheckedKeys: new Set([]),
  488. });
  489. }
  490. // When triggerSearch, clicking the clear button will trigger to clear Input
  491. if (filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER) {
  492. if (inputValue !== '') {
  493. if (isEmpty(selectedKeys)) {
  494. this.handleInputChange('');
  495. } else {
  496. this.clearInput();
  497. }
  498. }
  499. }
  500. }
  501. /**
  502. * A11y: simulate clear button click
  503. */
  504. /* istanbul ignore next */
  505. handleClearEnterPress(e: any) {
  506. if (isEnterPress(e)) {
  507. this.handleClear(e);
  508. }
  509. }
  510. removeTag(eventKey: BasicTreeNodeData['key']) {
  511. const { disableStrictly, checkRelation } = this.getProps();
  512. const { keyEntities, disabledKeys, realCheckedKeys } = this.getStates();
  513. const item = (keyEntities[eventKey] && keyEntities[eventKey].data.key === eventKey) ? keyEntities[eventKey].data : this.getDataForKeyNotInKeyEntities(eventKey);
  514. if (item.disabled || (disableStrictly && disabledKeys.has(eventKey))) {
  515. return;
  516. }
  517. if (checkRelation === 'unRelated') {
  518. const newRealCheckedKeys = new Set(realCheckedKeys);
  519. newRealCheckedKeys.delete(eventKey);
  520. this._notifyChange([...newRealCheckedKeys], null);
  521. if (!this._isControlledComponent()) {
  522. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys } as any);
  523. this._adapter.rePositionDropdown();
  524. }
  525. } else if (checkRelation === 'related') {
  526. const { checkedKeys, halfCheckedKeys } = this.calcCheckedKeys(eventKey, false);
  527. this._notifyChange([...checkedKeys], null);
  528. if (!this._isControlledComponent()) {
  529. this._adapter.updateState({ checkedKeys, halfCheckedKeys });
  530. this._adapter.rePositionDropdown();
  531. }
  532. }
  533. this._adapter.notifySelect(eventKey, false, item);
  534. // reposition dropdown when selected values change
  535. this._adapter.rePositionDropdown();
  536. }
  537. clearInput() {
  538. const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  539. const newExpandedKeys: Set<string> = new Set(expandedKeys);
  540. const isExpandControlled = this._isExpandControlled();
  541. const expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
  542. expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
  543. const newFlattenNodes = flattenTreeData(treeData, newExpandedKeys);
  544. this._adapter.updateState({
  545. expandedKeys: isExpandControlled ? expandedKeys : newExpandedKeys,
  546. flattenNodes: isExpandControlled ? flattenNodes : newFlattenNodes,
  547. inputValue: '',
  548. motionKeys: new Set([]),
  549. filteredKeys: new Set([]),
  550. filteredExpandedKeys: new Set(expandedOptsKeys),
  551. filteredShownKeys: new Set([])
  552. });
  553. }
  554. handleInputChange(sugInput: string) {
  555. // Input is used as controlled component
  556. this._adapter.updateInputValue(sugInput);
  557. const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  558. const { showFilteredOnly, filterTreeNode, treeNodeFilterProp } = this.getProps();
  559. const newExpandedKeys: Set<string> = new Set(expandedKeys);
  560. let filteredOptsKeys: string[] = [];
  561. let expandedOptsKeys = [];
  562. let newFlattenNodes = [];
  563. let filteredShownKeys = new Set([]);
  564. if (!sugInput) {
  565. expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities);
  566. expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
  567. newFlattenNodes = flattenTreeData(treeData, newExpandedKeys);
  568. } else {
  569. filteredOptsKeys = Object.values(keyEntities)
  570. .filter((item: BasicKeyEntity) => {
  571. const { data } = item;
  572. return filter(sugInput, data, filterTreeNode, treeNodeFilterProp);
  573. })
  574. .map((item: BasicKeyEntity) => item.key);
  575. expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
  576. const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
  577. filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
  578. newFlattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), showFilteredOnly && filteredShownKeys);
  579. }
  580. const newFilteredExpandedKeys = new Set(expandedOptsKeys);
  581. this._adapter.notifySearch(sugInput, Array.from(newFilteredExpandedKeys));
  582. this._adapter.updateState({
  583. expandedKeys: this._isExpandControlled() ? expandedKeys : newExpandedKeys,
  584. flattenNodes: this._isExpandControlled() ? flattenNodes : newFlattenNodes,
  585. motionKeys: new Set([]),
  586. filteredKeys: new Set(filteredOptsKeys),
  587. filteredExpandedKeys: newFilteredExpandedKeys,
  588. filteredShownKeys,
  589. });
  590. }
  591. handleNodeSelect(e: any, treeNode: BasicTreeNodeProps) {
  592. const isDisabled = this._isDisabled(treeNode);
  593. if (isDisabled) {
  594. return;
  595. }
  596. if (!this._isMultiple()) {
  597. this.handleSingleSelect(e, treeNode);
  598. } else {
  599. this.handleMultipleSelect(e, treeNode);
  600. }
  601. }
  602. handleSingleSelect(e: any, treeNode: BasicTreeNodeProps) {
  603. let { selectedKeys } = this.getCopyFromState('selectedKeys');
  604. const { clickToHide } = this.getProps();
  605. const { selected, eventKey, data } = treeNode;
  606. this._adapter.notifySelect(eventKey, true, data);
  607. if (!selectedKeys.includes(eventKey) && !selected) {
  608. selectedKeys = [eventKey];
  609. this._notifyChange(eventKey, e);
  610. if (!this._isControlledComponent()) {
  611. this._adapter.updateState({ selectedKeys });
  612. }
  613. }
  614. if (clickToHide && (this._isSelectToClose() || !data.children)) {
  615. this.close(e);
  616. this.handlerTriggerBlur(e);
  617. }
  618. }
  619. calcCheckedKeys(eventKey: BasicTreeNodeProps['eventKey'], targetStatus: boolean) {
  620. const { keyEntities } = this.getStates();
  621. const {
  622. checkedKeys,
  623. halfCheckedKeys
  624. } = this.getCopyFromState(['checkedKeys', 'halfCheckedKeys']);
  625. if (targetStatus) {
  626. return calcCheckedKeysForChecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  627. } else {
  628. return calcCheckedKeysForUnchecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  629. }
  630. }
  631. handleMultipleSelect(e: any, treeNode: BasicTreeNodeProps) {
  632. const { searchPosition, disableStrictly, checkRelation } = this.getProps();
  633. const { inputValue, realCheckedKeys } = this.getStates();
  634. const { checked, eventKey, data } = treeNode;
  635. if (checkRelation === 'related') {
  636. const targetStatus = disableStrictly ?
  637. this.calcCheckedStatus(!checked, eventKey) :
  638. !checked;
  639. const { checkedKeys, halfCheckedKeys } = disableStrictly ?
  640. this.calcNonDisabledCheckedKeys(eventKey, targetStatus) :
  641. this.calcCheckedKeys(eventKey, targetStatus);
  642. this._adapter.notifySelect(eventKey, targetStatus, data);
  643. this._notifyChange([...checkedKeys], e);
  644. if (!this._isControlledComponent()) {
  645. this._adapter.updateState({ checkedKeys, halfCheckedKeys });
  646. this._adapter.rePositionDropdown();
  647. }
  648. } else if (checkRelation === 'unRelated') {
  649. const newRealCheckedKeys: Set<string> = new Set(realCheckedKeys);
  650. let targetStatus: boolean;
  651. if (realCheckedKeys.has(eventKey)) {
  652. newRealCheckedKeys.delete(eventKey);
  653. targetStatus = false;
  654. } else {
  655. newRealCheckedKeys.add(eventKey);
  656. targetStatus = true;
  657. }
  658. this._adapter.notifySelect(eventKey, targetStatus, data);
  659. this._notifyChange([...newRealCheckedKeys], e);
  660. if (!this._isControlledComponent()) {
  661. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys });
  662. this._adapter.rePositionDropdown();
  663. }
  664. }
  665. if (searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue !== '') {
  666. this._adapter.updateState({ inputValue: '' });
  667. }
  668. }
  669. calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean) {
  670. const { keyEntities, disabledKeys } = this.getStates();
  671. const { checkedKeys } = this.getCopyFromState(['checkedKeys']);
  672. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  673. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  674. if (!hasDisabled) {
  675. return this.calcCheckedKeys(eventKey, targetStatus);
  676. }
  677. const nonDisabled = descendantKeys.filter(key => !disabledKeys.has(key));
  678. const newCheckedKeys = targetStatus ?
  679. [...nonDisabled, ...checkedKeys] :
  680. difference(normalizeKeyList([...checkedKeys], keyEntities, true), nonDisabled);
  681. return calcCheckedKeys(newCheckedKeys, keyEntities);
  682. }
  683. calcCheckedStatus(targetStatus: boolean, eventKey: string) {
  684. if (!targetStatus) {
  685. return targetStatus;
  686. }
  687. const { checkedKeys, keyEntities, disabledKeys } = this.getStates();
  688. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  689. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  690. if (!hasDisabled) {
  691. return targetStatus;
  692. }
  693. const nonDisabledKeys = descendantKeys.filter(key => !disabledKeys.has(key));
  694. const allChecked = nonDisabledKeys.every(key => checkedKeys.has(key));
  695. return !allChecked;
  696. }
  697. handleNodeExpandInSearch(e: any, treeNode: BasicTreeNodeProps) {
  698. const { treeData, filteredShownKeys, keyEntities } = this.getStates();
  699. const showFilteredOnly = this._showFilteredOnly();
  700. // clone otherwise will be modified unexpectedly
  701. const { filteredExpandedKeys } = this.getCopyFromState('filteredExpandedKeys');
  702. let motionType = 'show';
  703. const { eventKey, expanded, data } = treeNode;
  704. // debugger;
  705. if (!expanded) {
  706. filteredExpandedKeys.add(eventKey);
  707. } else if (filteredExpandedKeys.has(eventKey)) {
  708. filteredExpandedKeys.delete(eventKey);
  709. motionType = 'hide';
  710. }
  711. // cache prev flattenNodes, otherwise the calculation will remove hidden items
  712. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  713. if (!this._isExpandControlled()) {
  714. // debugger;
  715. const flattenNodes = flattenTreeData(treeData, filteredExpandedKeys, showFilteredOnly && filteredShownKeys);
  716. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, filteredExpandedKeys, keyEntities) : [];
  717. const newState = {
  718. filteredExpandedKeys,
  719. flattenNodes,
  720. motionKeys: new Set(motionKeys),
  721. motionType,
  722. };
  723. this._adapter.updateState(newState);
  724. }
  725. this._adapter.notifyExpand(filteredExpandedKeys, {
  726. expanded: !expanded,
  727. node: data,
  728. });
  729. }
  730. handleNodeExpand(e: any, treeNode: BasicTreeNodeProps) {
  731. // debugger;
  732. const { loadData } = this.getProps();
  733. const { inputValue, keyEntities } = this.getStates();
  734. const isSearching = Boolean(inputValue);
  735. if (!loadData && (!treeNode.children || !treeNode.children.length)) {
  736. return;
  737. }
  738. const isExpandControlled = this._isExpandControlled();
  739. if (isSearching && !isExpandControlled) {
  740. this.handleNodeExpandInSearch(e, treeNode);
  741. return;
  742. }
  743. const { treeData } = this.getStates();
  744. // clone otherwise will be modified unexpectedly
  745. const { expandedKeys } = this.getCopyFromState('expandedKeys');
  746. let motionType = 'show';
  747. const { eventKey, expanded, data } = treeNode;
  748. if (!expanded) {
  749. expandedKeys.add(eventKey);
  750. } else if (expandedKeys.has(eventKey)) {
  751. expandedKeys.delete(eventKey);
  752. motionType = 'hide';
  753. }
  754. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  755. if (!isExpandControlled) {
  756. // debugger;
  757. const flattenNodes = flattenTreeData(treeData, expandedKeys);
  758. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, expandedKeys, keyEntities) : [];
  759. const newState = {
  760. expandedKeys,
  761. flattenNodes,
  762. motionKeys: new Set(motionKeys),
  763. motionType,
  764. };
  765. this._adapter.updateState(newState);
  766. }
  767. this._adapter.notifyExpand(expandedKeys, {
  768. expanded: !expanded,
  769. node: data,
  770. });
  771. }
  772. /**
  773. * The selected items that need to be displayed in the search box when obtaining a single selection
  774. */
  775. getRenderTextInSingle() {
  776. const { renderSelectedItem: propRenderSelectedItem, treeNodeLabelProp } = this.getProps();
  777. const { selectedKeys, keyEntities } = this.getStates();
  778. const renderSelectedItem = isFunction(propRenderSelectedItem) ?
  779. propRenderSelectedItem :
  780. (item: BasicTreeNodeData) => get(item, treeNodeLabelProp, null);
  781. let item;
  782. if (selectedKeys.length) {
  783. const key = selectedKeys[0];
  784. item = (keyEntities[key] && keyEntities[key].data.key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
  785. }
  786. const renderText = item && treeNodeLabelProp in item ? renderSelectedItem(item) : null;
  787. return renderText;
  788. }
  789. /**
  790. * When the search box is on the trigger, the blur event handling method
  791. */
  792. handleInputTriggerBlur() {
  793. this._adapter.updateState({
  794. inputTriggerFocus: false
  795. });
  796. }
  797. /**
  798. * When the search box is on the trigger, the focus event processing method
  799. */
  800. handleInputTriggerFocus() {
  801. this.clearInput();
  802. this._adapter.updateState({
  803. inputTriggerFocus: true
  804. });
  805. }
  806. setLoadKeys(data: BasicTreeNodeData, resolve: (value?: any) => void) {
  807. this._adapter.updateLoadKeys(data, resolve);
  808. }
  809. }