1
0

foundation.ts 37 KB

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