1
0

foundation.ts 32 KB

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