foundation.ts 36 KB

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