foundation.ts 37 KB

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