foundation.ts 38 KB

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