foundation.ts 38 KB

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