foundation.ts 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  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. }
  196. export default class TreeSelectFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TreeSelectAdapter<P, S>, P, S> {
  197. constructor(adapter: TreeSelectAdapter<P, S>) {
  198. super({ ...adapter });
  199. }
  200. init() {
  201. const { searchAutoFocus, searchPosition, filterTreeNode } = this.getProps();
  202. const triggerSearch = searchPosition === strings.SEARCH_POSITION_TRIGGER && filterTreeNode;
  203. const triggerSearchAutoFocus = searchAutoFocus && triggerSearch;
  204. this._setDropdownWidth();
  205. const able = !this._isDisabled();
  206. const isOpen = (this.getProp('defaultOpen') || triggerSearchAutoFocus) && able;
  207. if (isOpen) {
  208. this.open();
  209. this._registerClickOutsideHandler();
  210. }
  211. if (triggerSearchAutoFocus && able) {
  212. this.handleTriggerFocus(null);
  213. }
  214. }
  215. destroy() {
  216. // Ensure that event monitoring will be uninstalled, and the user may not trigger closePanel
  217. this._adapter.unregisterClickOutsideHandler();
  218. }
  219. _setDropdownWidth() {
  220. const { style, dropdownMatchSelectWidth } = this.getProps();
  221. let width;
  222. if (dropdownMatchSelectWidth) {
  223. if (style && isNumber(style.width)) {
  224. width = style.width;
  225. } else if (style && isString(style.width) && !style.width.includes('%')) {
  226. width = style.width;
  227. } else {
  228. width = this._adapter.getTriggerWidth();
  229. }
  230. this._adapter.setOptionWrapperWidth(width);
  231. }
  232. }
  233. _isMultiple() {
  234. return this.getProp('multiple');
  235. }
  236. _isAnimated() {
  237. return this.getProp('motionExpand');
  238. }
  239. _isDisabled(treeNode = {} as BasicTreeNodeProps) {
  240. return this.getProp('disabled') || treeNode.disabled;
  241. }
  242. _isExpandControlled() {
  243. return this.getProp('expandedKeys');
  244. }
  245. _isSelectToClose() {
  246. return !this.getProp('expandAction');
  247. }
  248. _isLoadControlled() {
  249. return this.getProp('loadedKeys');
  250. }
  251. _showFilteredOnly() {
  252. const { inputValue } = this.getStates();
  253. const { showFilteredOnly } = this.getProps();
  254. return Boolean(inputValue) && showFilteredOnly;
  255. }
  256. findDataForValue(findValue: string) {
  257. const { value, defaultValue, keyMaps } = this.getProps();
  258. const realValueName = get(keyMaps, 'value', 'value');
  259. const realKeyName = get(keyMaps, 'key', 'key');
  260. let valueArr = [];
  261. if (value) {
  262. valueArr = Array.isArray(value) ? value : [value];
  263. } else if (defaultValue) {
  264. valueArr = Array.isArray(defaultValue) ? defaultValue : [defaultValue];
  265. }
  266. return valueArr.find(item => {
  267. return item[realValueName] === findValue || item[realKeyName] === findValue;
  268. });
  269. }
  270. constructDataForValue(value: string) {
  271. const { treeNodeLabelProp, keyMaps } = this.getProps();
  272. const keyName = get(keyMaps, 'key', 'key');
  273. const labelName = get(keyMaps, 'label', treeNodeLabelProp);
  274. return {
  275. [keyName]: value,
  276. [labelName]: value
  277. };
  278. }
  279. getDataForKeyNotInKeyEntities(value: string) {
  280. const { onChangeWithObject } = this.getProps();
  281. if (onChangeWithObject) {
  282. return this.findDataForValue(value);
  283. } else {
  284. return this.constructDataForValue(value);
  285. }
  286. }
  287. handleKeyDown = (e: any) => {
  288. if (e.key === ESC_KEY) {
  289. const isOpen = this.getState('isOpen');
  290. isOpen && this.close(e);
  291. }
  292. }
  293. getTreeNodeProps(key: string) {
  294. const {
  295. expandedKeys = new Set([]),
  296. selectedKeys = [],
  297. checkedKeys = new Set([]),
  298. halfCheckedKeys = new Set([]),
  299. realCheckedKeys = new Set([]),
  300. keyEntities = {},
  301. filteredKeys = new Set([]),
  302. inputValue = '',
  303. loadedKeys,
  304. loadingKeys,
  305. filteredExpandedKeys = new Set([]),
  306. disabledKeys = new Set([]),
  307. } = this.getStates();
  308. const { treeNodeFilterProp, checkRelation } = this.getProps();
  309. const entity = keyEntities[key];
  310. const notExist = !entity;
  311. if (notExist) {
  312. return null;
  313. }
  314. // if checkRelation is invalid, the checked status of node will be false
  315. let realChecked = false;
  316. let realHalfChecked = false;
  317. if (checkRelation === 'related') {
  318. realChecked = checkedKeys.has(key);
  319. realHalfChecked = halfCheckedKeys.has(key);
  320. } else if (checkRelation === 'unRelated') {
  321. realChecked = realCheckedKeys.has(key);
  322. realHalfChecked = false;
  323. }
  324. const isSearching = Boolean(inputValue);
  325. const treeNodeProps: BasicTreeNodeProps = {
  326. eventKey: key,
  327. expanded: isSearching ? filteredExpandedKeys.has(key) : expandedKeys.has(key),
  328. selected: selectedKeys.includes(key),
  329. checked: realChecked,
  330. halfChecked: realHalfChecked,
  331. pos: String(entity ? entity.pos : ''),
  332. level: entity.level,
  333. filtered: filteredKeys.has(key),
  334. keyword: inputValue,
  335. treeNodeFilterProp,
  336. loading: loadingKeys.has(key) && !loadedKeys.has(key),
  337. loaded: loadedKeys.has(key),
  338. };
  339. if (this.getProp('disableStrictly') && disabledKeys.has(key)) {
  340. treeNodeProps.disabled = true;
  341. }
  342. return treeNodeProps;
  343. }
  344. handleNodeLoad(loadedKeys: Set<string>, loadingKeys: Set<string>, data: BasicTreeNodeData, resolve: (value?: any) => void) {
  345. const { loadData } = this.getProps();
  346. const { key } = data;
  347. if (!loadData || loadedKeys.has(key) || loadingKeys.has(key)) {
  348. return {};
  349. }
  350. loadData(data).then(() => {
  351. const prevLoadedKeys = new Set(this.getState('loadedKeys')) as Set<string>;
  352. const prevLoadingKeys = new Set(this.getState('loadingKeys')) as Set<string>;
  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, autoMergeValue } = this.getProps();
  378. let keyList = [];
  379. if (checkRelation === 'related') {
  380. keyList = autoMergeValue ? normalizeKeyList(key, keyEntities, leafOnly, true) : key;
  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 = () => {
  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();
  426. }
  427. onClickSingleTriggerSearchItem = (e: any) => {
  428. this.focusInput(true);
  429. }
  430. // Scenes that may trigger blur
  431. // 1、clickOutSide
  432. // 2、click option / press enter, and then select complete(when multiple is false
  433. // 3、press esc when dropdown list open
  434. _notifyBlur(e: any) {
  435. this._adapter.notifyBlur(e);
  436. }
  437. handlerTriggerBlur(e) {
  438. const isFocus = this.getState('isFocus');
  439. if (!isFocus) {
  440. return;
  441. }
  442. this._adapter.updateIsFocus(false);
  443. this._notifyBlur(e);
  444. this._adapter.unregisterClickOutsideHandler();
  445. }
  446. toggleHoverState(bool: boolean) {
  447. this._adapter.toggleHovering(bool);
  448. }
  449. open() {
  450. this._adapter.openMenu();
  451. this._setDropdownWidth();
  452. }
  453. close(e: any) {
  454. this._adapter.closeMenu();
  455. if (this.getProp('motionExpand')) {
  456. this._adapter.updateState({ motionKeys: new Set([]) });
  457. }
  458. }
  459. handleClick(e: any) {
  460. const isDisabled = this._isDisabled();
  461. const { isOpen, inputValue, isFocus } = this.getStates();
  462. const { searchPosition, clickTriggerToHide } = this.getProps();
  463. if (isDisabled) {
  464. return;
  465. } else {
  466. if (!isFocus) {
  467. this.handleTriggerFocus(e);
  468. }
  469. if (isOpen) {
  470. if (searchPosition === 'trigger' && inputValue) {
  471. return;
  472. }
  473. clickTriggerToHide && this.close(e);
  474. } else {
  475. this.open();
  476. }
  477. }
  478. }
  479. /**
  480. * A11y: simulate selection click
  481. */
  482. /* istanbul ignore next */
  483. handleSelectionEnterPress(e: any) {
  484. if (isEnterPress(e)) {
  485. this.handleClick(e);
  486. }
  487. }
  488. handleClear(e: any) {
  489. const { searchPosition, filterTreeNode } = this.getProps();
  490. const { inputValue, selectedKeys } = this.getStates();
  491. const isMultiple = this._isMultiple();
  492. const isControlled = this._isControlledComponent();
  493. const value: string | string[] | undefined = isMultiple ? [] : undefined;
  494. this._notifyChange(value, e);
  495. if (!isControlled) {
  496. // reposition dropdown when selected values change
  497. this._adapter.rePositionDropdown();
  498. this._adapter.updateState({
  499. selectedKeys: [],
  500. checkedKeys: new Set(),
  501. halfCheckedKeys: new Set(),
  502. realCheckedKeys: new Set([]),
  503. });
  504. }
  505. // When triggerSearch, clicking the clear button will trigger to clear Input
  506. if (filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER) {
  507. if (inputValue !== '') {
  508. if (isEmpty(selectedKeys)) {
  509. this.handleInputChange('');
  510. } else {
  511. this.clearInput();
  512. }
  513. }
  514. }
  515. this._adapter.notifyClear(e);
  516. }
  517. /**
  518. * A11y: simulate clear button click
  519. */
  520. /* istanbul ignore next */
  521. handleClearEnterPress(e: any) {
  522. if (isEnterPress(e)) {
  523. this.handleClear(e);
  524. }
  525. }
  526. removeTag(eventKey: BasicTreeNodeData['key']) {
  527. const { disableStrictly, checkRelation, keyMaps } = this.getProps();
  528. const { keyEntities, disabledKeys, realCheckedKeys } = this.getStates();
  529. const item = (keyEntities[eventKey] && keyEntities[eventKey].key === eventKey) ? keyEntities[eventKey].data : this.getDataForKeyNotInKeyEntities(eventKey);
  530. const disabledName = get(keyMaps, 'disabled', 'disabled');
  531. if (item[disabledName] || (disableStrictly && disabledKeys.has(eventKey))) {
  532. return;
  533. }
  534. if (checkRelation === 'unRelated') {
  535. const newRealCheckedKeys = new Set(realCheckedKeys);
  536. newRealCheckedKeys.delete(eventKey);
  537. this._notifyChange([...newRealCheckedKeys], null);
  538. if (!this._isControlledComponent()) {
  539. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys } as any);
  540. this._adapter.rePositionDropdown();
  541. }
  542. } else if (checkRelation === 'related') {
  543. const { checkedKeys, halfCheckedKeys } = this.calcCheckedKeys(eventKey, false);
  544. this._notifyChange([...checkedKeys], null);
  545. if (!this._isControlledComponent()) {
  546. this._adapter.updateState({ checkedKeys, halfCheckedKeys });
  547. this._adapter.rePositionDropdown();
  548. }
  549. }
  550. this._adapter.notifySelect(eventKey, false, item);
  551. // reposition dropdown when selected values change
  552. this._adapter.rePositionDropdown();
  553. }
  554. clearInput() {
  555. const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  556. const { keyMaps } = this.getProps();
  557. const newExpandedKeys: Set<string> = new Set(expandedKeys);
  558. const isExpandControlled = this._isExpandControlled();
  559. const expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities, false);
  560. expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
  561. const newFlattenNodes = flattenTreeData(treeData, newExpandedKeys, keyMaps);
  562. this._adapter.updateState({
  563. expandedKeys: newExpandedKeys,
  564. flattenNodes: newFlattenNodes,
  565. inputValue: '',
  566. motionKeys: new Set([]),
  567. filteredKeys: new Set([]),
  568. filteredExpandedKeys: new Set(expandedOptsKeys),
  569. filteredShownKeys: new Set([])
  570. });
  571. this._adapter.rePositionDropdown();
  572. }
  573. handleInputChange(sugInput: string) {
  574. // Input is used as controlled component
  575. this._adapter.updateInputValue(sugInput);
  576. const { flattenNodes, expandedKeys, selectedKeys, keyEntities, treeData } = this.getStates();
  577. const { showFilteredOnly, filterTreeNode, treeNodeFilterProp, keyMaps } = this.getProps();
  578. const realFilterProp = treeNodeFilterProp !== 'label' ? treeNodeFilterProp : get(keyMaps, 'label', 'label');
  579. const newExpandedKeys: Set<string> = new Set(expandedKeys);
  580. let filteredNodes: BasicTreeNodeData[] = [];
  581. let filteredOptsKeys: string[] = [];
  582. let expandedOptsKeys = [];
  583. let newFlattenNodes = [];
  584. let filteredShownKeys = new Set([]);
  585. if (!sugInput) {
  586. expandedOptsKeys = findAncestorKeys(selectedKeys, keyEntities, false);
  587. expandedOptsKeys.forEach(item => newExpandedKeys.add(item));
  588. newFlattenNodes = flattenTreeData(treeData, newExpandedKeys, keyMaps);
  589. } else {
  590. const filteredOpts = Object.values(keyEntities)
  591. .filter((item: BasicKeyEntity) => {
  592. const { data } = item;
  593. return filter(sugInput, data, filterTreeNode, realFilterProp);
  594. });
  595. filteredNodes = filteredOpts.map((item: BasicKeyEntity) => item.data);
  596. filteredOptsKeys = filteredOpts.map((item: BasicKeyEntity) => item.key);
  597. expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
  598. const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
  599. filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
  600. newFlattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), keyMaps, showFilteredOnly && filteredShownKeys);
  601. }
  602. const newFilteredExpandedKeys = new Set(expandedOptsKeys);
  603. this._adapter.notifySearch(sugInput, Array.from(newFilteredExpandedKeys), filteredNodes);
  604. this._adapter.updateState({
  605. expandedKeys: newExpandedKeys,
  606. flattenNodes: newFlattenNodes,
  607. motionKeys: new Set([]),
  608. filteredKeys: new Set(filteredOptsKeys),
  609. filteredExpandedKeys: newFilteredExpandedKeys,
  610. filteredShownKeys,
  611. });
  612. this._adapter.rePositionDropdown();
  613. }
  614. handleNodeSelect(e: any, treeNode: BasicTreeNodeProps) {
  615. const isDisabled = this._isDisabled(treeNode);
  616. if (isDisabled) {
  617. return;
  618. }
  619. if (!this._isMultiple()) {
  620. this.handleSingleSelect(e, treeNode);
  621. } else {
  622. this.handleMultipleSelect(e, treeNode);
  623. }
  624. }
  625. handleSingleSelect(e: any, treeNode: BasicTreeNodeProps) {
  626. let selectedKeys = [...this.getState('selectedKeys')];
  627. const { clickToHide } = this.getProps();
  628. const { selected, eventKey, data } = treeNode;
  629. this._adapter.notifySelect(eventKey, true, data);
  630. if (!selectedKeys.includes(eventKey) && !selected) {
  631. selectedKeys = [eventKey];
  632. this._notifyChange(eventKey, e);
  633. if (!this._isControlledComponent()) {
  634. this._adapter.updateState({ selectedKeys });
  635. }
  636. }
  637. if (clickToHide && (this._isSelectToClose() || !data.children)) {
  638. this.close(e);
  639. this.handlerTriggerBlur(e);
  640. }
  641. }
  642. calcCheckedKeys(eventKey: BasicTreeNodeProps['eventKey'], targetStatus: boolean) {
  643. const { keyEntities } = this.getStates();
  644. const checkedKeys = new Set(this.getState('checkedKeys')) as Set<string>;
  645. const halfCheckedKeys = new Set(this.getState('halfCheckedKeys')) as Set<string>;
  646. if (targetStatus) {
  647. return calcCheckedKeysForChecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  648. } else {
  649. return calcCheckedKeysForUnchecked(eventKey, keyEntities, checkedKeys, halfCheckedKeys);
  650. }
  651. }
  652. handleMultipleSelect(e: any, treeNode: BasicTreeNodeProps) {
  653. const { searchPosition, disableStrictly, checkRelation } = this.getProps();
  654. const { inputValue, realCheckedKeys } = this.getStates();
  655. const { checked, eventKey, data } = treeNode;
  656. if (checkRelation === 'related') {
  657. const targetStatus = disableStrictly ?
  658. this.calcCheckedStatus(!checked, eventKey) :
  659. !checked;
  660. const { checkedKeys, halfCheckedKeys } = disableStrictly ?
  661. this.calcNonDisabledCheckedKeys(eventKey, targetStatus) :
  662. this.calcCheckedKeys(eventKey, targetStatus);
  663. this._adapter.notifySelect(eventKey, targetStatus, data);
  664. this._notifyChange([...checkedKeys], e);
  665. if (!this._isControlledComponent()) {
  666. this._adapter.updateState({ checkedKeys, halfCheckedKeys });
  667. this._adapter.rePositionDropdown();
  668. }
  669. } else if (checkRelation === 'unRelated') {
  670. const newRealCheckedKeys: Set<string> = new Set(realCheckedKeys);
  671. let targetStatus: boolean;
  672. if (realCheckedKeys.has(eventKey)) {
  673. newRealCheckedKeys.delete(eventKey);
  674. targetStatus = false;
  675. } else {
  676. newRealCheckedKeys.add(eventKey);
  677. targetStatus = true;
  678. }
  679. this._adapter.notifySelect(eventKey, targetStatus, data);
  680. this._notifyChange([...newRealCheckedKeys], e);
  681. if (!this._isControlledComponent()) {
  682. this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys });
  683. this._adapter.rePositionDropdown();
  684. }
  685. }
  686. if (searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue !== '') {
  687. this._adapter.updateState({ inputValue: '' });
  688. }
  689. }
  690. calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean) {
  691. const { keyEntities, disabledKeys } = this.getStates();
  692. const checkedKeys = new Set(this.getState('checkedKeys'));
  693. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  694. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  695. if (!hasDisabled) {
  696. return this.calcCheckedKeys(eventKey, targetStatus);
  697. }
  698. const nonDisabled = descendantKeys.filter(key => !disabledKeys.has(key));
  699. const newCheckedKeys = targetStatus ?
  700. [...nonDisabled, ...checkedKeys] :
  701. difference(normalizeKeyList([...checkedKeys], keyEntities, true, true), nonDisabled);
  702. return calcCheckedKeys(newCheckedKeys, keyEntities);
  703. }
  704. calcCheckedStatus(targetStatus: boolean, eventKey: string) {
  705. if (!targetStatus) {
  706. return targetStatus;
  707. }
  708. const { checkedKeys, keyEntities, disabledKeys } = this.getStates();
  709. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  710. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  711. if (!hasDisabled) {
  712. return targetStatus;
  713. }
  714. const nonDisabledKeys = descendantKeys.filter(key => !disabledKeys.has(key));
  715. const allChecked = nonDisabledKeys.every(key => checkedKeys.has(key));
  716. return !allChecked;
  717. }
  718. handleNodeExpandInSearch(e: any, treeNode: BasicTreeNodeProps) {
  719. const { treeData, filteredShownKeys, keyEntities } = this.getStates();
  720. const { keyMaps } = this.getProps();
  721. const showFilteredOnly = this._showFilteredOnly();
  722. // clone otherwise will be modified unexpectedly
  723. const filteredExpandedKeys = new Set(this.getState('filteredExpandedKeys')) as Set<string>;
  724. let motionType = 'show';
  725. const { eventKey, expanded, data } = treeNode;
  726. // debugger;
  727. if (!expanded) {
  728. filteredExpandedKeys.add(eventKey);
  729. } else if (filteredExpandedKeys.has(eventKey)) {
  730. filteredExpandedKeys.delete(eventKey);
  731. motionType = 'hide';
  732. }
  733. // cache prev flattenNodes, otherwise the calculation will remove hidden items
  734. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  735. if (!this._isExpandControlled()) {
  736. // debugger;
  737. const flattenNodes = flattenTreeData(treeData, filteredExpandedKeys, keyMaps, showFilteredOnly && filteredShownKeys);
  738. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, filteredExpandedKeys, keyEntities) : [];
  739. const newState = {
  740. filteredExpandedKeys,
  741. flattenNodes,
  742. motionKeys: new Set(motionKeys),
  743. motionType,
  744. };
  745. this._adapter.updateState(newState);
  746. }
  747. this._adapter.notifyExpand(filteredExpandedKeys, {
  748. expanded: !expanded,
  749. node: data,
  750. });
  751. }
  752. handleNodeExpand(e: any, treeNode: BasicTreeNodeProps) {
  753. // debugger;
  754. const { loadData, keyMaps } = this.getProps();
  755. const { inputValue, keyEntities } = this.getStates();
  756. const isSearching = Boolean(inputValue);
  757. if (!loadData && (!treeNode.children || !treeNode.children.length)) {
  758. return;
  759. }
  760. const isExpandControlled = this._isExpandControlled();
  761. if (isSearching) {
  762. this.handleNodeExpandInSearch(e, treeNode);
  763. return;
  764. }
  765. const { treeData } = this.getStates();
  766. // clone otherwise will be modified unexpectedly
  767. const expandedKeys = new Set(this.getState('expandedKeys')) as Set<string>;
  768. let motionType = 'show';
  769. const { eventKey, expanded, data } = treeNode;
  770. if (!expanded) {
  771. expandedKeys.add(eventKey);
  772. } else if (expandedKeys.has(eventKey)) {
  773. expandedKeys.delete(eventKey);
  774. motionType = 'hide';
  775. }
  776. this._adapter.cacheFlattenNodes(motionType === 'hide' && this._isAnimated());
  777. if (!isExpandControlled) {
  778. // debugger;
  779. const flattenNodes = flattenTreeData(treeData, expandedKeys, keyMaps);
  780. const motionKeys = this._isAnimated() ? getMotionKeys(eventKey, expandedKeys, keyEntities) : [];
  781. const newState = {
  782. expandedKeys,
  783. flattenNodes,
  784. motionKeys: new Set(motionKeys),
  785. motionType,
  786. };
  787. this._adapter.updateState(newState);
  788. }
  789. this._adapter.notifyExpand(expandedKeys, {
  790. expanded: !expanded,
  791. node: data,
  792. });
  793. }
  794. /**
  795. * The selected items that need to be displayed in the search box when obtaining a single selection
  796. */
  797. getRenderTextInSingle() {
  798. const { renderSelectedItem: propRenderSelectedItem, treeNodeLabelProp, keyMaps } = this.getProps();
  799. const { selectedKeys, keyEntities } = this.getStates();
  800. const realLabelName = get(keyMaps, 'label', treeNodeLabelProp);
  801. const renderSelectedItem = isFunction(propRenderSelectedItem) ?
  802. propRenderSelectedItem :
  803. (item: BasicTreeNodeData) => get(item, realLabelName, null);
  804. let item;
  805. if (selectedKeys.length) {
  806. const key = selectedKeys[0];
  807. item = (keyEntities[key] && keyEntities[key].key === key) ? keyEntities[key].data : this.getDataForKeyNotInKeyEntities(key);
  808. }
  809. const renderText = item ? renderSelectedItem(item) : null;
  810. return renderText;
  811. }
  812. /**
  813. * When the search box is on the trigger, the blur event handling method
  814. */
  815. handleInputTriggerBlur() {
  816. this._adapter.updateState({
  817. inputTriggerFocus: false
  818. });
  819. }
  820. /**
  821. * When the search box is on the trigger, the focus event processing method
  822. */
  823. handleInputTriggerFocus() {
  824. this.clearInput();
  825. this._adapter.updateState({
  826. inputTriggerFocus: true
  827. });
  828. }
  829. setLoadKeys(data: BasicTreeNodeData, resolve: (value?: any) => void) {
  830. this._adapter.updateLoadKeys(data, resolve);
  831. }
  832. handlePopoverVisibleChange(isVisible: boolean) {
  833. const { filterTreeNode, searchAutoFocus, searchPosition } = this.getProps();
  834. // 将 inputValue 清空,如果有选中值的话,选中项能够快速回显
  835. // Clear the inputValue. If there is a selected value, the selected item can be quickly echoed.
  836. if (isVisible === false && filterTreeNode) {
  837. this.clearInputValue();
  838. }
  839. if (filterTreeNode && searchPosition === strings.SEARCH_POSITION_DROPDOWN && isVisible && searchAutoFocus) {
  840. this.focusInput(true);
  841. }
  842. }
  843. handleAfterClose() {
  844. // flattenNode 的变化将导致弹出层面板中的选项数目变化
  845. // 在弹层完全收起之后,再通过 clearInput 重新计算 state 中的 expandedKey, flattenNode
  846. // 防止在弹出层未收起时弹层面板中选项数目变化导致视觉上出现弹层闪动问题
  847. // Changes to flattenNode will cause the number of options in the popup panel to change
  848. // After the pop-up layer is completely closed, recalculate the expandedKey and flattenNode in the state through clearInput.
  849. // 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.
  850. const { filterTreeNode } = this.getProps();
  851. filterTreeNode && this.clearInput();
  852. }
  853. }