foundation.ts 35 KB

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