foundation.ts 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. import { isEqual, get, difference, isUndefined, assign, isEmpty, isNumber, includes, isFunction, isObject } from 'lodash';
  2. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  3. import {
  4. findAncestorKeys,
  5. calcCheckedKeysForUnchecked,
  6. calcCheckedKeysForChecked,
  7. calcCheckedKeys,
  8. findDescendantKeys,
  9. normalizeKeyList
  10. } from '../tree/treeUtil';
  11. import { Motion } from '../utils/type';
  12. import {
  13. filter,
  14. convertDataToEntities,
  15. normalizedArr,
  16. isValid,
  17. calcMergeType,
  18. getKeysByValuePath,
  19. getKeyByPos
  20. } from './util';
  21. import { strings } from './constants';
  22. import isEnterPress from '../utils/isEnterPress';
  23. import { ESC_KEY } from '../utils/keyCode';
  24. export interface BasicData {
  25. data: BasicCascaderData;
  26. disabled: boolean;
  27. key: string;
  28. searchText: any[];
  29. pathData?: BasicCascaderData[]
  30. }
  31. export interface Virtualize {
  32. itemSize: number;
  33. height?: number | string;
  34. width?: number | string
  35. }
  36. export interface BasicEntities {
  37. [idx: string]: BasicEntity
  38. }
  39. export interface BasicEntity {
  40. _notExist?: boolean;
  41. /* children list */
  42. children?: Array<BasicEntity>;
  43. /* treedata */
  44. data: BasicCascaderData;
  45. /* index */
  46. ind: number;
  47. /* key */
  48. key: string;
  49. /* node level */
  50. level: number;
  51. /* parent data */
  52. parent?: BasicEntity;
  53. /* parent key */
  54. parentKey?: string;
  55. /* key path */
  56. path: Array<string>;
  57. /* pos in treeData */
  58. pos: string;
  59. /* value path */
  60. valuePath: Array<string>
  61. }
  62. export interface BasicCascaderData {
  63. [x: string]: any;
  64. value: string | number;
  65. label: any;
  66. disabled?: boolean;
  67. isLeaf?: boolean;
  68. loading?: boolean;
  69. children?: BasicCascaderData[]
  70. }
  71. export type CascaderType = 'large' | 'small' | 'default';
  72. /* The basic type of the value of Cascader */
  73. export type BasicSimpleValueType = string | number | BasicCascaderData;
  74. /* The value of Cascader */
  75. export type BasicValue = BasicSimpleValueType | Array<BasicSimpleValueType> | Array<Array<BasicSimpleValueType>>;
  76. export type ShowNextType = 'click' | 'hover';
  77. export interface BasicTriggerRenderProps {
  78. /* Props passed to Cascader by all users */
  79. componentProps: BasicCascaderProps;
  80. /* Whether to disable Cascader */
  81. disabled: boolean;
  82. /** The hierarchical position of the selected node in treeData,
  83. * as in the following example, when Zhejiang-Hangzhou-Xiaoshan
  84. * District is selected, the value here is 0-0-1 */
  85. value?: string | Set<string>;
  86. /* The input value of the current input box */
  87. inputValue: string;
  88. /* Cascader's placeholder */
  89. placeholder?: string;
  90. /** The function used to update the value of the input box. You
  91. * should call this function when the value of the Input component
  92. * customized by triggerRender is updated to synchronize the state
  93. * with Cascader. */
  94. onSearch: (inputValue: string) => void;
  95. /* This function is the same as onSearch (supported since v2.32.0),
  96. * because this function was used before, and to align with TreeSelect,
  97. * use onSearch instead of onChange is more suitable,
  98. * onChange needs to be deleted in the next Major
  99. */
  100. onChange: (inputValue: string) => void;
  101. /* Function to clear the value */
  102. onClear: (e: any) => void;
  103. onRemove: (key: string) => void
  104. }
  105. export interface BasicScrollPanelProps {
  106. panelIndex: number;
  107. activeNode: BasicCascaderData
  108. }
  109. export interface BasicCascaderProps {
  110. borderless?: boolean;
  111. mouseEnterDelay?: number;
  112. mouseLeaveDelay?: number;
  113. separator?: string;
  114. arrowIcon?: any;
  115. changeOnSelect?: boolean;
  116. multiple?: boolean;
  117. autoMergeValue?: boolean;
  118. defaultValue?: BasicValue;
  119. disabled?: boolean;
  120. dropdownClassName?: string;
  121. dropdownStyle?: any;
  122. emptyContent?: any;
  123. filterLeafOnly?: boolean;
  124. motion?: boolean;
  125. filterTreeNode?: ((inputValue: string, treeNodeString: string, data?: BasicCascaderData) => boolean) | boolean;
  126. filterSorter?: (first: BasicCascaderData, second: BasicCascaderData, inputValue: string) => number;
  127. filterRender?: (props: any) => any;
  128. placeholder?: string;
  129. searchPlaceholder?: string;
  130. size?: CascaderType;
  131. className?: string;
  132. treeData?: Array<BasicCascaderData>;
  133. treeNodeFilterProp?: string;
  134. displayProp?: string;
  135. maxTagCount?: number;
  136. max?: number;
  137. showRestTagsPopover?: boolean;
  138. restTagsPopoverProps?: any;
  139. children?: any;
  140. zIndex?: number;
  141. value?: BasicValue;
  142. prefix?: any;
  143. suffix?: any;
  144. insetLabel?: any;
  145. style?: any;
  146. stopPropagation?: boolean | string;
  147. showClear?: boolean;
  148. autoAdjustOverflow?: boolean;
  149. defaultOpen?: boolean;
  150. onChangeWithObject?: boolean;
  151. bottomSlot?: any;
  152. topSlot?: any;
  153. showNext?: ShowNextType;
  154. disableStrictly?: boolean;
  155. leafOnly?: boolean;
  156. enableLeafClick?: boolean;
  157. preventScroll?: boolean;
  158. virtualizeInSearch?: Virtualize;
  159. onClear?: () => void;
  160. triggerRender?: (props: BasicTriggerRenderProps) => any;
  161. onListScroll?: (e: any, panel: BasicScrollPanelProps) => void;
  162. loadData?: (selectOptions: BasicCascaderData[]) => Promise<void>;
  163. onLoad?: (newLoadedKeys: Set<string>, data: BasicCascaderData) => void;
  164. onDropdownVisibleChange?: (visible: boolean) => void;
  165. getPopupContainer?: () => HTMLElement;
  166. onChange?: (value: BasicValue) => void;
  167. onSearch?: (value: string) => void;
  168. onSelect?: (value: string | number | Array<string | number>) => void;
  169. onExceed?: (checkedItem: BasicEntity[]) => void;
  170. displayRender?: (selected: Array<string> | BasicEntity, idx?: number) => any;
  171. onBlur?: (e: any) => void;
  172. onFocus?: (e: any) => void
  173. }
  174. export interface BasicCascaderInnerData {
  175. isOpen: boolean;
  176. rePosKey: number;
  177. keyEntities: BasicEntities;
  178. selectedKeys: Set<string>;
  179. activeKeys: Set<string>;
  180. filteredKeys: Set<string>;
  181. inputValue: string;
  182. isSearching: boolean;
  183. inputPlaceHolder: string;
  184. prevProps: BasicCascaderProps;
  185. isHovering: boolean;
  186. checkedKeys: Set<string>;
  187. halfCheckedKeys: Set<string>;
  188. resolvedCheckedKeys: Set<string>;
  189. loadedKeys: Set<string>;
  190. loadingKeys: Set<string>;
  191. loading: boolean;
  192. treeData?: Array<BasicCascaderData>;
  193. isFocus?: boolean;
  194. isInput?: boolean;
  195. disabledKeys?: Set<string>;
  196. showInput?: boolean
  197. }
  198. export interface CascaderAdapter extends DefaultAdapter<BasicCascaderProps, BasicCascaderInnerData> {
  199. notifyClear?: () => void;
  200. updateInputValue: (value: string) => void;
  201. updateInputPlaceHolder: (value: string) => void;
  202. focusInput: () => void;
  203. blurInput: () => void;
  204. registerClickOutsideHandler: (cb: (e: any) => void) => void;
  205. unregisterClickOutsideHandler: () => void;
  206. rePositionDropdown: () => void;
  207. updateStates: (states: Partial<BasicCascaderInnerData>) => void;
  208. openMenu: () => void;
  209. closeMenu: (cb?: () => void) => void;
  210. updateSelection: (selectedKeys: Set<string>) => void;
  211. notifyChange: (value: BasicValue) => void;
  212. notifySelect: (selected: string | number | Array<string | number>) => void;
  213. notifyOnSearch: (input: string) => void;
  214. notifyFocus: (e: any) => void;
  215. notifyBlur: (e: any) => void;
  216. notifyDropdownVisibleChange: (visible: boolean) => void;
  217. toggleHovering: (bool: boolean) => void;
  218. notifyLoadData: (selectedOpt: BasicCascaderData[], callback: (data?: BasicEntities) => void) => void;
  219. notifyOnLoad: (newLoadedKeys: Set<string>, data: BasicCascaderData) => void;
  220. notifyListScroll: (e: any, panel: BasicScrollPanelProps) => void;
  221. notifyOnExceed: (data: BasicEntity[]) => void;
  222. toggleInputShow: (show: boolean, cb: () => void) => void;
  223. updateFocusState: (focus: boolean) => void;
  224. updateLoadingKeyRefValue: (keys: Set<string>) => void;
  225. getLoadingKeyRefValue: () => Set<string>;
  226. updateLoadedKeyRefValue: (keys: Set<string>) => void;
  227. getLoadedKeyRefValue: () => Set<string>
  228. }
  229. export default class CascaderFoundation extends BaseFoundation<CascaderAdapter, BasicCascaderProps, BasicCascaderInnerData> {
  230. constructor(adapter: CascaderAdapter) {
  231. super({ ...adapter });
  232. }
  233. init() {
  234. const isOpen = this.getProp('open') || this.getProp('defaultOpen');
  235. this.collectOptions(true);
  236. this._adapter.updateLoadingKeyRefValue(new Set());
  237. this._adapter.updateLoadedKeyRefValue(new Set());
  238. if (isOpen && !this._isDisabled()) {
  239. this.open();
  240. }
  241. }
  242. handleKeyDown = (e: any) => {
  243. if (e.key === ESC_KEY) {
  244. const isOpen = this.getState('isOpen');
  245. isOpen && this.close(e);
  246. }
  247. }
  248. destroy() {
  249. this._adapter.unregisterClickOutsideHandler();
  250. }
  251. _isDisabled() {
  252. return this.getProp('disabled');
  253. }
  254. _isFilterable() {
  255. return Boolean(this.getProp('filterTreeNode')); // filter can be boolean or function
  256. }
  257. _notifyChange(item: BasicEntity | BasicData | Set<string>) {
  258. const { onChangeWithObject, multiple } = this.getProps();
  259. const valueProp: string | any[] = onChangeWithObject ? [] : 'value';
  260. if (multiple) {
  261. const valuePath: BasicValue = [];
  262. // @ts-ignore
  263. item.forEach((checkedKey: string) => {
  264. const valuePathItem = this.getItemPropPath(checkedKey, valueProp);
  265. valuePath.push(valuePathItem as any);
  266. });
  267. this._adapter.notifyChange(valuePath);
  268. } else {
  269. const valuePath = isUndefined(item) || !('key' in item) ?
  270. [] :
  271. this.getItemPropPath(item.key, valueProp);
  272. this._adapter.notifyChange(valuePath);
  273. }
  274. }
  275. _isLeaf(item: BasicCascaderData) {
  276. if (this.getProp('loadData')) {
  277. return Boolean(item.isLeaf);
  278. }
  279. return !item.children || !item.children.length;
  280. }
  281. _clearInput() {
  282. this._adapter.updateInputValue('');
  283. }
  284. // Scenes that may trigger blur:
  285. // 1、clickOutSide
  286. _notifyBlur(e: any) {
  287. this._adapter.notifyBlur(e);
  288. }
  289. // Scenes that may trigger focus:
  290. // 1、click selection
  291. _notifyFocus(e: any) {
  292. this._adapter.notifyFocus(e);
  293. }
  294. _isOptionDisabled(key: string, keyEntities: BasicEntities) {
  295. const isDisabled = findAncestorKeys([key], keyEntities, true)
  296. .some(item => keyEntities[item].data.disabled);
  297. return isDisabled;
  298. }
  299. // prop: is array, return all data
  300. getItemPropPath(selectedKey: string, prop: string | any[], keyEntities?: BasicEntities) {
  301. const searchMap = keyEntities || this.getState('keyEntities');
  302. const selectedItem = searchMap[selectedKey];
  303. let path = [];
  304. if (!selectedItem) {
  305. // do nothing
  306. } else if (selectedItem._notExist) {
  307. path = selectedItem.path;
  308. } else {
  309. const keyPath = selectedItem.path;
  310. path = Array.isArray(prop) ?
  311. keyPath.map((key: string) => searchMap[key].data) :
  312. keyPath.map((key: string) => searchMap[key].data[prop]);
  313. }
  314. return path;
  315. }
  316. _getCacheValue(keyEntities: BasicEntities) {
  317. const { selectedKeys } = this.getStates();
  318. const selectedKey = Array.from(selectedKeys as Set<string>)[0];
  319. let cacheValue;
  320. /* selectedKeys does not match keyEntities */
  321. if (isEmpty(keyEntities[selectedKey])) {
  322. if (includes(selectedKey, 'not-exist-')) {
  323. /* Get the value behind not-exist- */
  324. const targetValue = selectedKey.match(/not-exist-(\S*)/)[1];
  325. if (isEmpty(keyEntities[targetValue])) {
  326. cacheValue = targetValue;
  327. } else {
  328. /**
  329. * 典型的场景是: 假设我们选中了 0-0 这个节点,此时 selectedKeys=Set('0-0'),
  330. * 输入框会显示 0-0 的 label。当 treeData 发生更新,假设此时 0-0 在 treeData
  331. * 中不存在,则 selectedKeys=Set('not-exist-0-0'),此时输入框显示的是 0-0,
  332. * 也就是显示 not-exist- 后的内容。当treeData再次更新,假设此时 0-0 在 treeData
  333. * 中存在,则 selectedKeys=Set('0-0'),此时输入框显示 0-0 的 label。 这个地
  334. * 方做的操作就是,为了例子中第二次更新后 0-0 label 能够正常显示。
  335. */
  336. /**
  337. * The typical scenario is: suppose we select the 0-0 node, at this time
  338. * selectedKeys=Set('0-0'), the input box will display a 0-0 label. When
  339. * treeData is updated, assuming 0-0 does not exist in treeData at this
  340. * time, then selectedKeys=Set('not-exist-0-0'), at this time the input
  341. * box displays 0-0, which means not-exist -After the content. When treeData
  342. * is updated again, assuming that 0-0 exists in treeData at this time,
  343. * then selectedKeys=Set('0-0'), and the input box displays a label of
  344. * 0-0 at this time. The operation done here is for the 0-0 label to be
  345. * displayed normally after the second update in the example.
  346. */
  347. cacheValue = keyEntities[targetValue].valuePath;
  348. }
  349. } else {
  350. cacheValue = selectedKey;
  351. }
  352. /* selectedKeys match keyEntities */
  353. } else {
  354. /* selectedKeys match keyEntities */
  355. cacheValue = keyEntities[selectedKey].valuePath;
  356. }
  357. return cacheValue;
  358. }
  359. collectOptions(init = false) {
  360. const { treeData, value, defaultValue } = this.getProps();
  361. const keyEntities = convertDataToEntities(treeData);
  362. this._adapter.rePositionDropdown();
  363. let cacheValue;
  364. /* when mount */
  365. if (init) {
  366. cacheValue = defaultValue;
  367. } else if (!isEmpty(keyEntities)) {
  368. cacheValue = this._getCacheValue(keyEntities);
  369. }
  370. const selectedValue = !this._isControlledComponent() ? cacheValue : (isUndefined(value) ? [] : value);
  371. if (isValid(selectedValue)) {
  372. this.updateSelectedKey(selectedValue, keyEntities);
  373. } else {
  374. this._adapter.updateStates({ keyEntities });
  375. }
  376. }
  377. // call when props.value change
  378. handleValueChange(value: BasicValue) {
  379. const { keyEntities } = this.getStates();
  380. this.updateSelectedKey(value, keyEntities);
  381. }
  382. /**
  383. * When single selection, the clear objects of
  384. * selectedKeys, activeKeys, filteredKeys, input, etc.
  385. */
  386. _getClearSelectedKey(filterable: boolean) {
  387. const updateStates: Partial<BasicCascaderInnerData> = {};
  388. const { searchPlaceholder, placeholder, multiple } = this.getProps();
  389. updateStates.selectedKeys = new Set([]);
  390. updateStates.activeKeys = new Set([]);
  391. updateStates.filteredKeys = new Set([]);
  392. if (filterable && !multiple) {
  393. updateStates.inputPlaceHolder = searchPlaceholder || placeholder || '';
  394. updateStates.inputValue = '';
  395. }
  396. return updateStates;
  397. }
  398. updateSelectedKey(value: BasicValue, keyEntities: BasicEntities) {
  399. const { changeOnSelect, onChangeWithObject, multiple } = this.getProps();
  400. const {
  401. activeKeys,
  402. loading,
  403. keyEntities: keyEntityState,
  404. selectedKeys: selectedKeysState
  405. } = this.getStates();
  406. const loadingKeys = this._adapter.getLoadingKeyRefValue();
  407. const filterable = this._isFilterable();
  408. const loadingActive = [...activeKeys].filter(i => loadingKeys.has(i));
  409. const normalizedValue = normalizedArr(value);
  410. const valuePath = onChangeWithObject && isObject(normalizedValue[0]) ? normalizedValue.map(i => i.value) : normalizedValue;
  411. const selectedKeys = getKeysByValuePath(valuePath);
  412. let updateStates: Partial<BasicCascaderInnerData> = {};
  413. const selectedKey = selectedKeys.length > 0 ? selectedKeys[0] : undefined;
  414. const selectedItem = selectedKey ? keyEntities[selectedKey] : undefined;
  415. if (selectedItem) {
  416. /**
  417. * When changeOnSelect is turned on, or the target option is a leaf option,
  418. * the option is considered to be selected, even if the option is disabled
  419. */
  420. if (changeOnSelect || this._isLeaf(selectedItem.data)) {
  421. updateStates.selectedKeys = new Set([selectedKey]);
  422. if (!loadingActive.length) {
  423. updateStates.activeKeys = new Set(selectedItem.path);
  424. }
  425. if (filterable && !multiple) {
  426. const displayText = this.renderDisplayText(selectedKey, keyEntities);
  427. updateStates.inputPlaceHolder = displayText;
  428. /*
  429. * displayText should not be assign to inputValue,
  430. * cause inputValue should only change by user enter
  431. */
  432. // updateStates.inputValue = displayText;
  433. }
  434. /**
  435. * If selectedKeys does not meet the update conditions,
  436. * and state.selectedKeys is the same as selectedKeys
  437. * at this time, state.selectedKeys should be cleared.
  438. * A typical scenario is:
  439. * The originally selected node is the leaf node, but
  440. * after props.treeData is dynamically updated, the node
  441. * is a non-leaf node. At this point, selectedKeys should
  442. * be cleared.
  443. */
  444. } else if (isEqual(selectedKeys, Array.from(selectedKeysState))) {
  445. updateStates = this._getClearSelectedKey(filterable);
  446. }
  447. } else if (value && (value as any).length) {
  448. const val = valuePath[valuePath.length - 1];
  449. const key = `not-exist-${val}`;
  450. const optionNotExist = {
  451. data: {
  452. label: val,
  453. value: val,
  454. },
  455. key,
  456. path: valuePath,
  457. _notExist: true,
  458. };
  459. updateStates.selectedKeys = new Set([key]);
  460. if (filterable && !multiple) {
  461. const displayText = this._defaultRenderText(valuePath);
  462. updateStates.inputPlaceHolder = displayText;
  463. /*
  464. * displayText should not be assign to inputValue,
  465. * cause inputValue should only change by user enter
  466. */
  467. // updateStates.inputValue = displayText;
  468. }
  469. keyEntities[key] = optionNotExist as BasicEntity;
  470. // Fix: 1155, if the data is loaded asynchronously to update treeData, the emptying operation should not be done when entering the updateSelectedKey method
  471. } else if (loading) {
  472. // Use assign to avoid overwriting the'not-exist- * 'property of keyEntities after asynchronous loading
  473. // Overwriting'not-exist- * 'will cause selectionContent to be emptied unexpectedly when clicking on a dropDown item
  474. updateStates.keyEntities = assign(keyEntityState, keyEntities);
  475. this._adapter.updateStates(updateStates);
  476. return;
  477. } else {
  478. updateStates = this._getClearSelectedKey(filterable);
  479. }
  480. updateStates.keyEntities = keyEntities;
  481. this._adapter.updateStates(updateStates);
  482. }
  483. open() {
  484. const filterable = this._isFilterable();
  485. const { multiple } = this.getProps();
  486. this._adapter.openMenu();
  487. if (filterable) {
  488. this._clearInput();
  489. !multiple && this.toggle2SearchInput(true);
  490. }
  491. if (this._isControlledComponent()) {
  492. this.reCalcActiveKeys();
  493. }
  494. this._adapter.notifyDropdownVisibleChange(true);
  495. this._adapter.registerClickOutsideHandler(e => this.close(e));
  496. }
  497. reCalcActiveKeys() {
  498. const { selectedKeys, activeKeys, keyEntities } = this.getStates();
  499. const selectedKey = [...selectedKeys][0];
  500. const selectedItem = keyEntities[selectedKey];
  501. if (!selectedItem) {
  502. return;
  503. }
  504. const newActiveKeys: Set<string> = new Set(selectedItem.path);
  505. if (!isEqual(newActiveKeys, activeKeys)) {
  506. this._adapter.updateStates({
  507. activeKeys: newActiveKeys,
  508. });
  509. }
  510. }
  511. close(e: any, key?: string) {
  512. const { multiple } = this.getProps();
  513. this._adapter.closeMenu();
  514. this._adapter.notifyDropdownVisibleChange(false);
  515. this._adapter.unregisterClickOutsideHandler();
  516. if (this._isFilterable()) {
  517. const { selectedKeys, isSearching } = this.getStates();
  518. let inputValue = '';
  519. if (key && !multiple) {
  520. inputValue = this.renderDisplayText(key);
  521. } else if (selectedKeys.size && !multiple) {
  522. inputValue = this.renderDisplayText([...selectedKeys][0]);
  523. }
  524. this._adapter.updateStates({ inputValue });
  525. !multiple && this.toggle2SearchInput(false);
  526. !multiple && this._adapter.updateFocusState(false);
  527. }
  528. this._notifyBlur(e);
  529. }
  530. focus() {
  531. const { filterTreeNode } = this.getProps();
  532. if (filterTreeNode) {
  533. this._adapter.focusInput();
  534. }
  535. this._adapter.updateFocusState(true);
  536. }
  537. blur() {
  538. const { filterTreeNode } = this.getProps();
  539. if (filterTreeNode) {
  540. this._adapter.blurInput();
  541. }
  542. this._adapter.updateFocusState(false);
  543. }
  544. toggle2SearchInput(isShow: boolean) {
  545. if (isShow) {
  546. this._adapter.toggleInputShow(isShow, () => this.focus());
  547. } else {
  548. this._adapter.toggleInputShow(isShow, () => undefined);
  549. }
  550. }
  551. updateSearching = (isSearching: boolean)=>{
  552. this._adapter.updateStates({ isSearching: false });
  553. }
  554. handleItemClick(e: any, item: BasicEntity | BasicData) {
  555. const isDisabled = this._isDisabled();
  556. if (isDisabled) {
  557. return;
  558. }
  559. this.handleSingleSelect(e, item);
  560. this._adapter.rePositionDropdown();
  561. }
  562. handleItemHover(e: any, item: BasicEntity) {
  563. const isDisabled = this._isDisabled();
  564. if (isDisabled) {
  565. return;
  566. }
  567. this.handleShowNextByHover(item);
  568. }
  569. handleShowNextByHover(item: BasicEntity) {
  570. const { keyEntities } = this.getStates();
  571. const { data, key } = item;
  572. const isLeaf = this._isLeaf(data);
  573. const activeKeys = keyEntities[key].path;
  574. this._adapter.updateStates({
  575. activeKeys: new Set(activeKeys)
  576. });
  577. if (!isLeaf) {
  578. this.notifyIfLoadData(item);
  579. }
  580. }
  581. onItemCheckboxClick(item: BasicEntity | BasicData) {
  582. const isDisabled = this._isDisabled();
  583. if (isDisabled) {
  584. return;
  585. }
  586. this._handleMultipleSelect(item);
  587. this._adapter.rePositionDropdown();
  588. }
  589. handleClick(e: any) {
  590. const isDisabled = this._isDisabled();
  591. const isFilterable = this._isFilterable();
  592. const { isOpen } = this.getStates();
  593. if (isDisabled) {
  594. return;
  595. } else if (!isOpen) {
  596. this.open();
  597. this._notifyFocus(e);
  598. } else if (isOpen && !isFilterable) {
  599. this.close(e);
  600. }
  601. }
  602. /**
  603. * A11y: simulate selection click
  604. */
  605. /* istanbul ignore next */
  606. handleSelectionEnterPress(keyboardEvent: any) {
  607. if (isEnterPress(keyboardEvent)) {
  608. this.handleClick(keyboardEvent);
  609. }
  610. }
  611. toggleHoverState(bool: boolean) {
  612. this._adapter.toggleHovering(bool);
  613. }
  614. _defaultRenderText(path: any[], displayRender?: BasicCascaderProps['displayRender']) {
  615. const separator = this.getProp('separator');
  616. if (displayRender && typeof displayRender === 'function') {
  617. return displayRender(path);
  618. } else {
  619. return path.join(separator);
  620. }
  621. }
  622. renderDisplayText(targetKey: string, keyEntities?: BasicEntities) {
  623. const renderFunc = this.getProp('displayRender');
  624. const displayProp = this.getProp('displayProp');
  625. const displayPath = this.getItemPropPath(targetKey, displayProp, keyEntities);
  626. return this._defaultRenderText(displayPath, renderFunc);
  627. }
  628. handleNodeLoad(item: BasicEntity | BasicData) {
  629. const { data, key } = item;
  630. const prevLoadingKeys = new Set(this._adapter.getLoadingKeyRefValue());
  631. const prevLoadedKeys = new Set(this._adapter.getLoadedKeyRefValue());
  632. const newLoadedKeys = prevLoadedKeys.add(key);
  633. const newLoadingKeys = new Set([...prevLoadingKeys]);
  634. newLoadingKeys.delete(key);
  635. // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
  636. this._adapter.notifyOnLoad(newLoadedKeys, data);
  637. this._adapter.updateLoadingKeyRefValue(newLoadingKeys);
  638. this._adapter.updateLoadedKeyRefValue(newLoadedKeys);
  639. this._adapter.updateStates({
  640. loadingKeys: newLoadingKeys,
  641. loadedKeys: newLoadedKeys
  642. });
  643. }
  644. notifyIfLoadData(item: BasicEntity | BasicData) {
  645. const { data, key } = item;
  646. this._adapter.updateStates({ loading: false });
  647. if (!data.isLeaf && !data.children && this.getProp('loadData')) {
  648. const loadedKeys = this._adapter.getLoadedKeyRefValue();
  649. const loadingKeys = new Set(this._adapter.getLoadingKeyRefValue());
  650. if (loadedKeys.has(key) || loadingKeys.has(key)) {
  651. return;
  652. }
  653. this._adapter.updateStates({ loading: true });
  654. const { keyEntities } = this.getStates();
  655. const optionPath = this.getItemPropPath(key, [], keyEntities);
  656. const newLoadingKeys = loadingKeys.add(key);
  657. this._adapter.updateLoadingKeyRefValue(newLoadingKeys);
  658. this._adapter.updateStates({ loadingKeys: newLoadingKeys });
  659. this._adapter.notifyLoadData(optionPath, this.handleNodeLoad.bind(this, item));
  660. }
  661. }
  662. handleSingleSelect(e: any, item: BasicEntity | BasicData) {
  663. const { changeOnSelect: allowChange, filterLeafOnly, multiple, enableLeafClick } = this.getProps();
  664. const { keyEntities, selectedKeys, isSearching } = this.getStates();
  665. const filterable = this._isFilterable();
  666. const { data, key } = item;
  667. const isLeaf = this._isLeaf(data);
  668. const activeKeys = keyEntities[key].path;
  669. const selectedKey = [key];
  670. const hasChanged = key !== [...selectedKeys][0];
  671. if (!isLeaf && !allowChange && !isSearching) {
  672. this._adapter.updateStates({ activeKeys: new Set(activeKeys) });
  673. this.notifyIfLoadData(item);
  674. return;
  675. }
  676. if (multiple) {
  677. this._adapter.updateStates({ activeKeys: new Set(activeKeys) });
  678. if (isLeaf && enableLeafClick) {
  679. this.onItemCheckboxClick(item);
  680. }
  681. } else {
  682. this._adapter.notifySelect(data.value);
  683. if (hasChanged) {
  684. this._notifyChange(item);
  685. this.notifyIfLoadData(item);
  686. if (this._isControlledComponent()) {
  687. this._adapter.updateStates({ activeKeys: new Set(activeKeys) });
  688. if (isLeaf) {
  689. this.close(e);
  690. }
  691. return;
  692. }
  693. this._adapter.updateStates({
  694. activeKeys: new Set(activeKeys),
  695. selectedKeys: new Set(selectedKey),
  696. });
  697. const displayText = this.renderDisplayText(key);
  698. if (filterable) {
  699. this._adapter.updateInputPlaceHolder(displayText);
  700. }
  701. if (isLeaf) {
  702. this.close(e, key);
  703. } else if (!filterLeafOnly && isSearching) {
  704. this.close(e, key);
  705. }
  706. } else {
  707. this.close(e);
  708. }
  709. }
  710. }
  711. _handleMultipleSelect(item: BasicEntity | BasicData) {
  712. const { key } = item;
  713. const { checkedKeys, keyEntities, resolvedCheckedKeys } = this.getStates();
  714. const { autoMergeValue, max, disableStrictly, leafOnly } = this.getProps();
  715. // prev checked status
  716. const prevCheckedStatus = checkedKeys.has(key);
  717. // next checked status
  718. const curCheckedStatus = disableStrictly ?
  719. this.calcCheckedStatus(!prevCheckedStatus, key) :
  720. !prevCheckedStatus;
  721. // calculate all key of nodes that are checked or half checked
  722. const {
  723. checkedKeys: curCheckedKeys,
  724. halfCheckedKeys: curHalfCheckedKeys
  725. } = disableStrictly ?
  726. this.calcNonDisabledCheckedKeys(key, curCheckedStatus) :
  727. this.calcCheckedKeys(key, curCheckedStatus);
  728. const mergeType = calcMergeType(autoMergeValue, leafOnly);
  729. const isLeafOnlyMerge = mergeType === strings.LEAF_ONLY_MERGE_TYPE;
  730. const isNoneMerge = mergeType === strings.NONE_MERGE_TYPE;
  731. const curResolvedCheckedKeys = new Set(normalizeKeyList(curCheckedKeys, keyEntities, isLeafOnlyMerge));
  732. const curRealCheckedKeys = isNoneMerge
  733. ? curCheckedKeys
  734. : curResolvedCheckedKeys;
  735. if (isNumber(max)) {
  736. if (!isNoneMerge) {
  737. // When it exceeds max, the quantity is allowed to be reduced, and no further increase is allowed
  738. if (resolvedCheckedKeys.size < curResolvedCheckedKeys.size && curResolvedCheckedKeys.size > max) {
  739. const checkedEntities: BasicEntity[] = [];
  740. curResolvedCheckedKeys.forEach(itemKey => {
  741. checkedEntities.push(keyEntities[itemKey]);
  742. });
  743. this._adapter.notifyOnExceed(checkedEntities);
  744. return;
  745. }
  746. } else {
  747. // When it exceeds max, the quantity is allowed to be reduced, and no further increase is allowed
  748. if (checkedKeys.size < curCheckedKeys.size && curCheckedKeys.size > max) {
  749. const checkedEntities: BasicEntity[] = [];
  750. curCheckedKeys.forEach((itemKey: string) => {
  751. checkedEntities.push(keyEntities[itemKey]);
  752. });
  753. this._adapter.notifyOnExceed(checkedEntities);
  754. return;
  755. }
  756. }
  757. }
  758. if (!this._isControlledComponent()) {
  759. this._adapter.updateStates({
  760. checkedKeys: curCheckedKeys,
  761. halfCheckedKeys: curHalfCheckedKeys,
  762. resolvedCheckedKeys: curResolvedCheckedKeys
  763. });
  764. }
  765. // The click event during multiple selection will definitely cause the checked state of node to change,
  766. // so there is no need to judge the value to change.
  767. this._notifyChange(curRealCheckedKeys);
  768. if (curCheckedStatus) {
  769. this._notifySelect(curRealCheckedKeys);
  770. }
  771. this._adapter.updateStates({ inputValue: '' });
  772. }
  773. calcNonDisabledCheckedKeys(eventKey: string, targetStatus: boolean) {
  774. const { keyEntities, disabledKeys } = this.getStates();
  775. const checkedKeys = new Set(this.getState('checkedKeys'));
  776. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  777. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  778. if (!hasDisabled) {
  779. return this.calcCheckedKeys(eventKey, targetStatus);
  780. }
  781. const nonDisabled = descendantKeys.filter(key => !disabledKeys.has(key));
  782. const newCheckedKeys = targetStatus
  783. ? [...nonDisabled, ...checkedKeys]
  784. : difference(normalizeKeyList([...checkedKeys], keyEntities, true), nonDisabled);
  785. return calcCheckedKeys(newCheckedKeys, keyEntities);
  786. }
  787. calcCheckedStatus(targetStatus: boolean, eventKey: string) {
  788. if (!targetStatus) {
  789. return targetStatus;
  790. }
  791. const { checkedKeys, keyEntities, disabledKeys } = this.getStates();
  792. const descendantKeys = normalizeKeyList(findDescendantKeys([eventKey], keyEntities, false), keyEntities, true);
  793. const hasDisabled = descendantKeys.some(key => disabledKeys.has(key));
  794. if (!hasDisabled) {
  795. return targetStatus;
  796. }
  797. const nonDisabledKeys = descendantKeys.filter(key => !disabledKeys.has(key));
  798. const allChecked = nonDisabledKeys.every(key => checkedKeys.has(key));
  799. return !allChecked;
  800. }
  801. _notifySelect(keys: Set<string>) {
  802. const { keyEntities } = this.getStates();
  803. const values: (string | number)[] = [];
  804. keys.forEach(key => {
  805. const valueItem = keyEntities[key]?.data?.value;
  806. valueItem !== undefined && values.push(valueItem);
  807. });
  808. const formatValue: number | string | Array<string | number> = values.length === 1 ?
  809. values[0] :
  810. values;
  811. this._adapter.notifySelect(formatValue);
  812. }
  813. /**
  814. * calculate all key of nodes that are checked or half checked
  815. * @param {string} key key of node
  816. * @param {boolean} curCheckedStatus checked status of node
  817. */
  818. calcCheckedKeys(key: string, curCheckedStatus: boolean) {
  819. const { keyEntities } = this.getStates();
  820. const checkedKeys = new Set(this.getState('checkedKeys')) as Set<string>;
  821. const halfCheckedKeys = new Set(this.getState('halfCheckedKeys')) as Set<string>;
  822. return curCheckedStatus ?
  823. calcCheckedKeysForChecked(key, keyEntities, checkedKeys, halfCheckedKeys) :
  824. calcCheckedKeysForUnchecked(key, keyEntities, checkedKeys, halfCheckedKeys);
  825. }
  826. handleInputChange(sugInput: string) {
  827. this._adapter.updateInputValue(sugInput);
  828. const { keyEntities } = this.getStates();
  829. const { treeNodeFilterProp, filterTreeNode, filterLeafOnly } = this.getProps();
  830. let filteredKeys: string[] = [];
  831. if (sugInput) {
  832. filteredKeys = (Object.values(keyEntities) as BasicEntity[])
  833. .filter(item => {
  834. const { key, _notExist, data } = item;
  835. if (_notExist) {
  836. return false;
  837. }
  838. const filteredPath = this.getItemPropPath(key, treeNodeFilterProp);
  839. return filter(sugInput, data, filterTreeNode, filteredPath);
  840. })
  841. .filter(
  842. item => (filterTreeNode && !filterLeafOnly) ||
  843. this._isLeaf(item as unknown as BasicCascaderData)
  844. )
  845. .map(item => item.key);
  846. }
  847. this._adapter.updateStates({
  848. isSearching: Boolean(sugInput),
  849. filteredKeys: new Set(filteredKeys),
  850. });
  851. this._adapter.notifyOnSearch(sugInput);
  852. }
  853. handleClear() {
  854. const { isSearching } = this.getStates();
  855. const { searchPlaceholder, placeholder, multiple } = this.getProps();
  856. const isFilterable = this._isFilterable();
  857. const isControlled = this._isControlledComponent();
  858. const newState: Partial<BasicCascaderInnerData> = {};
  859. if (multiple) {
  860. newState.isSearching = false;
  861. this._adapter.updateInputValue('');
  862. this._adapter.notifyOnSearch('');
  863. newState.checkedKeys = new Set([]);
  864. newState.halfCheckedKeys = new Set([]);
  865. newState.selectedKeys = new Set([]);
  866. newState.activeKeys = new Set([]);
  867. newState.resolvedCheckedKeys = new Set([]);
  868. this._adapter.notifyChange([]);
  869. } else {
  870. // if click clearBtn when not searching, clear selected and active values as well
  871. if (isFilterable && isSearching) {
  872. newState.isSearching = false;
  873. this._adapter.updateInputValue('');
  874. this._adapter.notifyOnSearch('');
  875. } else {
  876. if (isFilterable) {
  877. newState.inputValue = '';
  878. newState.inputPlaceHolder = searchPlaceholder || placeholder || '';
  879. this._adapter.updateInputValue('');
  880. this._adapter.notifyOnSearch('');
  881. }
  882. if (!isControlled) {
  883. newState.selectedKeys = new Set([]);
  884. }
  885. newState.activeKeys = new Set([]);
  886. newState.filteredKeys = new Set([]);
  887. this._adapter.notifyChange([]);
  888. }
  889. }
  890. this._adapter.updateStates(newState);
  891. this._adapter.notifyClear();
  892. this._adapter.rePositionDropdown();
  893. }
  894. /**
  895. * A11y: simulate clear button click
  896. */
  897. /* istanbul ignore next */
  898. handleClearEnterPress(keyboardEvent: any) {
  899. if (isEnterPress(keyboardEvent)) {
  900. this.handleClear();
  901. }
  902. }
  903. getRenderData() {
  904. const { keyEntities, isSearching } = this.getStates();
  905. const isFilterable = this._isFilterable();
  906. if (isSearching && isFilterable) {
  907. return this.getFilteredData();
  908. }
  909. return (Object.values(keyEntities) as BasicEntity[])
  910. .filter(item => item.parentKey === null && !item._notExist)
  911. // @ts-ignore
  912. .sort((a, b) => parseInt(a.ind, 10) - parseInt(b.ind, 10));
  913. }
  914. getFilteredData() {
  915. const { treeNodeFilterProp, filterSorter } = this.getProps();
  916. const { filteredKeys, keyEntities, inputValue } = this.getStates();
  917. const filteredList: BasicData[] = [];
  918. const filteredKeyArr = [...filteredKeys];
  919. filteredKeyArr.forEach(key => {
  920. const item = keyEntities[key];
  921. if (!item) {
  922. return;
  923. }
  924. const pathData = this.getItemPropPath(key, []);
  925. const itemSearchPath = pathData.map(item => item[treeNodeFilterProp]);
  926. const isDisabled = this._isOptionDisabled(key, keyEntities);
  927. filteredList.push({
  928. data: item.data,
  929. pathData,
  930. key,
  931. disabled: isDisabled,
  932. searchText: itemSearchPath
  933. });
  934. });
  935. if (isFunction(filterSorter)) {
  936. filteredList.sort((a, b) => {
  937. return filterSorter(a.pathData, b.pathData, inputValue);
  938. });
  939. }
  940. return filteredList;
  941. }
  942. handleListScroll(e: any, ind: number) {
  943. const { activeKeys, keyEntities } = this.getStates();
  944. const lastActiveKey = [...activeKeys][activeKeys.size - 1];
  945. const data = lastActiveKey ? get(keyEntities, [lastActiveKey, 'data'], null) : null;
  946. this._adapter.notifyListScroll(e, { panelIndex: ind, activeNode: data });
  947. }
  948. handleTagRemoveByKey = (key: string) => {
  949. const { keyEntities } = this.getStates();
  950. const { disabled } = this.getProps();
  951. if (disabled) {
  952. /* istanbul ignore next */
  953. return;
  954. }
  955. const removedItem = keyEntities[key] ?? {};
  956. !removedItem?.data?.disable && this._handleMultipleSelect(removedItem);
  957. }
  958. handleTagRemoveInTrigger = (pos: string) => {
  959. const { treeData } = this.getStates();
  960. const key = getKeyByPos(pos, treeData);
  961. this.handleTagRemoveByKey(key);
  962. }
  963. }