foundation.ts 42 KB

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