foundation.ts 31 KB

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