index.tsx 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. import React, { Fragment, ReactNode, CSSProperties, MouseEvent } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import cls from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import CascaderFoundation, {
  6. /* Corresponding to the state of react */
  7. BasicCascaderInnerData,
  8. /* Corresponding to the props of react */
  9. BasicCascaderProps,
  10. BasicTriggerRenderProps,
  11. BasicScrollPanelProps,
  12. CascaderAdapter,
  13. CascaderType
  14. } from '@douyinfe/semi-foundation/cascader/foundation';
  15. import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants';
  16. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  17. import { isEqual, isString, isEmpty, isFunction, isNumber, noop, flatten } from 'lodash';
  18. import '@douyinfe/semi-foundation/cascader/cascader.scss';
  19. import { IconClear, IconChevronDown } from '@douyinfe/semi-icons';
  20. import { findKeysForValues, convertDataToEntities, calcMergeType } from '@douyinfe/semi-foundation/cascader/util';
  21. import { calcCheckedKeys, normalizeKeyList, calcDisabledKeys } from '@douyinfe/semi-foundation/tree/treeUtil';
  22. import ConfigContext from '../configProvider/context';
  23. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  24. import Input from '../input/index';
  25. import Popover, { PopoverProps } from '../popover/index';
  26. import Item, { CascaderData, Entities, Entity, Data } from './item';
  27. import Trigger from '../trigger';
  28. import Tag from '../tag';
  29. import TagInput from '../tagInput';
  30. import { Motion } from '../_base/base';
  31. import { isSemiIcon } from '../_utils';
  32. export { CascaderType, ShowNextType } from '@douyinfe/semi-foundation/cascader/foundation';
  33. export { CascaderData, Entity, Data, CascaderItemProps } from './item';
  34. export interface ScrollPanelProps extends BasicScrollPanelProps {
  35. activeNode: CascaderData;
  36. }
  37. export interface TriggerRenderProps extends BasicTriggerRenderProps {
  38. componentProps: CascaderProps;
  39. onClear: (e: React.MouseEvent) => void;
  40. }
  41. /* The basic type of the value of Cascader */
  42. export type SimpleValueType = string | number | CascaderData;
  43. /* The value of Cascader */
  44. export type Value = SimpleValueType | Array<SimpleValueType> | Array<Array<SimpleValueType>>;
  45. export interface CascaderProps extends BasicCascaderProps {
  46. arrowIcon?: ReactNode;
  47. defaultValue?: Value;
  48. dropdownStyle?: CSSProperties;
  49. emptyContent?: ReactNode;
  50. motion?: Motion;
  51. treeData?: Array<CascaderData>;
  52. restTagsPopoverProps?: PopoverProps;
  53. children?: ReactNode;
  54. value?: Value;
  55. prefix?: ReactNode;
  56. suffix?: ReactNode;
  57. insetLabel?: ReactNode;
  58. style?: CSSProperties;
  59. bottomSlot?: ReactNode;
  60. topSlot?: ReactNode;
  61. triggerRender?: (props: TriggerRenderProps) => ReactNode;
  62. onListScroll?: (e: React.UIEvent<HTMLUListElement, UIEvent>, panel: ScrollPanelProps) => void;
  63. loadData?: (selectOptions: CascaderData[]) => Promise<void>;
  64. onLoad?: (newLoadedKeys: Set<string>, data: CascaderData) => void;
  65. onChange?: (value: Value) => void;
  66. onExceed?: (checkedItem: Entity[]) => void;
  67. displayRender?: (selected: Array<string> | Entity, idx?: number) => ReactNode;
  68. onBlur?: (e: MouseEvent) => void;
  69. onFocus?: (e: MouseEvent) => void;
  70. validateStatus?: ValidateStatus;
  71. }
  72. export interface CascaderState extends BasicCascaderInnerData {
  73. keyEntities: Entities;
  74. prevProps: CascaderProps;
  75. treeData?: Array<CascaderData>;
  76. }
  77. const prefixcls = cssClasses.PREFIX;
  78. const resetkey = 0;
  79. class Cascader extends BaseComponent<CascaderProps, CascaderState> {
  80. static contextType = ConfigContext;
  81. static propTypes = {
  82. arrowIcon: PropTypes.node,
  83. changeOnSelect: PropTypes.bool,
  84. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  85. disabled: PropTypes.bool,
  86. dropdownClassName: PropTypes.string,
  87. dropdownStyle: PropTypes.object,
  88. emptyContent: PropTypes.node,
  89. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  90. /* show search input, if passed in a function, used as custom filter */
  91. filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
  92. filterLeafOnly: PropTypes.bool,
  93. placeholder: PropTypes.string,
  94. searchPlaceholder: PropTypes.string,
  95. size: PropTypes.oneOf<CascaderType>(strings.SIZE_SET),
  96. style: PropTypes.object,
  97. className: PropTypes.string,
  98. treeData: PropTypes.arrayOf(
  99. PropTypes.shape({
  100. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  101. label: PropTypes.any,
  102. }),
  103. ),
  104. treeNodeFilterProp: PropTypes.string,
  105. suffix: PropTypes.node,
  106. prefix: PropTypes.node,
  107. insetLabel: PropTypes.node,
  108. displayProp: PropTypes.string,
  109. displayRender: PropTypes.func,
  110. onChange: PropTypes.func,
  111. onSearch: PropTypes.func,
  112. onSelect: PropTypes.func,
  113. onBlur: PropTypes.func,
  114. onFocus: PropTypes.func,
  115. children: PropTypes.node,
  116. getPopupContainer: PropTypes.func,
  117. zIndex: PropTypes.number,
  118. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
  119. validateStatus: PropTypes.oneOf<CascaderProps['validateStatus']>(strings.VALIDATE_STATUS),
  120. showNext: PropTypes.oneOf([strings.SHOW_NEXT_BY_CLICK, strings.SHOW_NEXT_BY_HOVER]),
  121. stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
  122. showClear: PropTypes.bool,
  123. defaultOpen: PropTypes.bool,
  124. autoAdjustOverflow: PropTypes.bool,
  125. onDropdownVisibleChange: PropTypes.func,
  126. triggerRender: PropTypes.func,
  127. onListScroll: PropTypes.func,
  128. onChangeWithObject: PropTypes.bool,
  129. bottomSlot: PropTypes.node,
  130. topSlot: PropTypes.node,
  131. multiple: PropTypes.bool,
  132. autoMergeValue: PropTypes.bool,
  133. maxTagCount: PropTypes.number,
  134. showRestTagsPopover: PropTypes.bool,
  135. restTagsPopoverProps: PropTypes.object,
  136. max: PropTypes.number,
  137. separator: PropTypes.string,
  138. onExceed: PropTypes.func,
  139. onClear: PropTypes.func,
  140. loadData: PropTypes.func,
  141. onLoad: PropTypes.func,
  142. loadedKeys: PropTypes.array,
  143. disableStrictly: PropTypes.bool,
  144. leafOnly: PropTypes.bool,
  145. enableLeafClick: PropTypes.bool,
  146. };
  147. static defaultProps = {
  148. leafOnly: false,
  149. arrowIcon: <IconChevronDown />,
  150. stopPropagation: true,
  151. motion: true,
  152. defaultOpen: false,
  153. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  154. showClear: false,
  155. autoClearSearchValue: true,
  156. changeOnSelect: false,
  157. disabled: false,
  158. disableStrictly: false,
  159. autoMergeValue: true,
  160. multiple: false,
  161. filterTreeNode: false,
  162. filterLeafOnly: true,
  163. showRestTagsPopover: false,
  164. restTagsPopoverProps: {},
  165. separator: ' / ',
  166. size: 'default' as const,
  167. treeNodeFilterProp: 'label' as const,
  168. displayProp: 'label' as const,
  169. treeData: [] as Array<CascaderData>,
  170. showNext: strings.SHOW_NEXT_BY_CLICK,
  171. onExceed: noop,
  172. onClear: noop,
  173. onDropdownVisibleChange: noop,
  174. onListScroll: noop,
  175. enableLeafClick: false,
  176. };
  177. options: any;
  178. isEmpty: boolean;
  179. inputRef: React.RefObject<typeof Input>;
  180. triggerRef: React.RefObject<HTMLDivElement>;
  181. optionsRef: React.RefObject<any>;
  182. clickOutsideHandler: any;
  183. mergeType: string;
  184. constructor(props: CascaderProps) {
  185. super(props);
  186. this.state = {
  187. disabledKeys: new Set(),
  188. isOpen: props.defaultOpen,
  189. /* By changing rePosKey, the dropdown position can be refreshed */
  190. rePosKey: resetkey,
  191. /* A data structure for storing cascader data items */
  192. keyEntities: {},
  193. /* Selected and show tick icon */
  194. selectedKeys: new Set([]),
  195. /* The key of the activated node */
  196. activeKeys: new Set([]),
  197. /* The key of the filtered node */
  198. filteredKeys: new Set([]),
  199. /* Value of input box */
  200. inputValue: '',
  201. /* Is searching */
  202. isSearching: false,
  203. /* The placeholder of input box */
  204. inputPlaceHolder: props.searchPlaceholder || props.placeholder,
  205. /* Cache props */
  206. prevProps: {},
  207. /* Is hovering */
  208. isHovering: false,
  209. /* Key of checked node, when multiple */
  210. checkedKeys: new Set([]),
  211. /* Key of half checked node, when multiple */
  212. halfCheckedKeys: new Set([]),
  213. /* Auto merged checkedKeys or leaf checkedKeys, when multiple */
  214. resolvedCheckedKeys: new Set([]),
  215. /* Keys of loaded item */
  216. loadedKeys: new Set(),
  217. /* Keys of loading item */
  218. loadingKeys: new Set(),
  219. /* Mark whether this rendering has triggered asynchronous loading of data */
  220. loading: false
  221. };
  222. this.options = {};
  223. this.isEmpty = false;
  224. this.mergeType = calcMergeType(props.autoMergeValue, props.leafOnly);
  225. this.inputRef = React.createRef();
  226. this.triggerRef = React.createRef();
  227. this.optionsRef = React.createRef();
  228. this.clickOutsideHandler = null;
  229. this.foundation = new CascaderFoundation(this.adapter);
  230. }
  231. get adapter(): CascaderAdapter {
  232. const filterAdapter: Pick<CascaderAdapter, 'updateInputValue' | 'updateInputPlaceHolder' | 'focusInput'> = {
  233. updateInputValue: value => {
  234. this.setState({ inputValue: value });
  235. },
  236. updateInputPlaceHolder: value => {
  237. this.setState({ inputPlaceHolder: value });
  238. },
  239. focusInput: () => {
  240. if (this.inputRef && this.inputRef.current) {
  241. // TODO: check the reason
  242. (this.inputRef.current as any).focus();
  243. }
  244. },
  245. };
  246. const cascaderAdapter: Pick<CascaderAdapter,
  247. 'registerClickOutsideHandler'
  248. | 'unregisterClickOutsideHandler'
  249. | 'rePositionDropdown'
  250. > = {
  251. registerClickOutsideHandler: cb => {
  252. const clickOutsideHandler = (e: Event) => {
  253. const optionInstance = this.optionsRef && this.optionsRef.current;
  254. const triggerDom = this.triggerRef && this.triggerRef.current;
  255. const optionsDom = ReactDOM.findDOMNode(optionInstance);
  256. const target = e.target as Element;
  257. if (
  258. optionsDom &&
  259. (!optionsDom.contains(target) || !optionsDom.contains(target.parentNode)) &&
  260. triggerDom &&
  261. !triggerDom.contains(target)
  262. ) {
  263. cb(e);
  264. }
  265. };
  266. this.clickOutsideHandler = clickOutsideHandler;
  267. document.addEventListener('mousedown', clickOutsideHandler, false);
  268. },
  269. unregisterClickOutsideHandler: () => {
  270. document.removeEventListener('mousedown', this.clickOutsideHandler, false);
  271. },
  272. rePositionDropdown: () => {
  273. let { rePosKey } = this.state;
  274. rePosKey = rePosKey + 1;
  275. this.setState({ rePosKey });
  276. },
  277. };
  278. return {
  279. ...super.adapter,
  280. ...filterAdapter,
  281. ...cascaderAdapter,
  282. updateStates: states => {
  283. this.setState({ ...states } as CascaderState);
  284. },
  285. openMenu: () => {
  286. this.setState({ isOpen: true });
  287. },
  288. closeMenu: cb => {
  289. this.setState({ isOpen: false }, () => {
  290. cb && cb();
  291. });
  292. },
  293. updateSelection: selectedKeys => this.setState({ selectedKeys }),
  294. notifyChange: value => {
  295. this.props.onChange && this.props.onChange(value);
  296. },
  297. notifySelect: selected => {
  298. this.props.onSelect && this.props.onSelect(selected);
  299. },
  300. notifyOnSearch: input => {
  301. this.props.onSearch && this.props.onSearch(input);
  302. },
  303. notifyFocus: (...v) => {
  304. this.props.onFocus && this.props.onFocus(...v);
  305. },
  306. notifyBlur: (...v) => {
  307. this.props.onBlur && this.props.onBlur(...v);
  308. },
  309. notifyDropdownVisibleChange: visible => {
  310. this.props.onDropdownVisibleChange(visible);
  311. },
  312. toggleHovering: bool => {
  313. this.setState({ isHovering: bool });
  314. },
  315. notifyLoadData: (selectedOpt, callback) => {
  316. const { loadData } = this.props;
  317. if (loadData) {
  318. new Promise<void>(resolve => {
  319. loadData(selectedOpt).then(() => {
  320. callback();
  321. this.setState({ loading: false });
  322. resolve();
  323. });
  324. });
  325. }
  326. },
  327. notifyOnLoad: (newLoadedKeys, data) => {
  328. const { onLoad } = this.props;
  329. onLoad && onLoad(newLoadedKeys, data);
  330. },
  331. notifyListScroll: (e, { panelIndex, activeNode }) => {
  332. this.props.onListScroll(e, { panelIndex, activeNode });
  333. },
  334. notifyOnExceed: data => this.props.onExceed(data),
  335. notifyClear: () => this.props.onClear(),
  336. };
  337. }
  338. static getDerivedStateFromProps(props: CascaderProps, prevState: CascaderState) {
  339. const {
  340. multiple,
  341. value,
  342. defaultValue,
  343. onChangeWithObject,
  344. leafOnly,
  345. autoMergeValue,
  346. } = props;
  347. const { prevProps } = prevState;
  348. let keyEntities = prevState.keyEntities || {};
  349. const newState: Partial<CascaderState> = { };
  350. const needUpdate = (name: string) => {
  351. const firstInProps = isEmpty(prevProps) && name in props;
  352. const nameHasChange = prevProps && !isEqual(prevProps[name], props[name]);
  353. return firstInProps || nameHasChange;
  354. };
  355. const needUpdateData = () => {
  356. const firstInProps = !prevProps && 'treeData' in props;
  357. const treeDataHasChange = prevProps && prevProps.treeData !== props.treeData;
  358. return firstInProps || treeDataHasChange;
  359. };
  360. const needUpdateTreeData = needUpdate('treeData') || needUpdateData();
  361. const needUpdateValue = needUpdate('value') || (isEmpty(prevProps) && defaultValue);
  362. if (multiple) {
  363. // when value and treedata need updated
  364. if (needUpdateTreeData || needUpdateValue) {
  365. // update state.keyEntities
  366. if (needUpdateTreeData) {
  367. newState.treeData = props.treeData;
  368. keyEntities = convertDataToEntities(props.treeData);
  369. newState.keyEntities = keyEntities;
  370. }
  371. let realKeys: Array<string> | Set<string> = prevState.checkedKeys;
  372. // when data was updated
  373. if (needUpdateValue) {
  374. // normallizedValue is used to save the value in two-dimensional array format
  375. let normallizedValue: SimpleValueType[][] = [];
  376. const realValue = needUpdate('value') ? value : defaultValue;
  377. // eslint-disable-next-line max-depth
  378. if (Array.isArray(realValue)) {
  379. normallizedValue = Array.isArray(realValue[0]) ?
  380. realValue as SimpleValueType[][] :
  381. [realValue] as SimpleValueType[][];
  382. } else {
  383. normallizedValue = [[realValue]];
  384. }
  385. // formatValuePath is used to save value of valuePath
  386. const formatValuePath: (string | number)[][] = [];
  387. normallizedValue.forEach((valueItem: SimpleValueType[]) => {
  388. const formatItem: (string | number)[] = onChangeWithObject ?
  389. (valueItem as CascaderData[]).map(i => i.value) :
  390. valueItem as (string | number)[];
  391. formatValuePath.push(formatItem);
  392. });
  393. // formatKeys is used to save key of value
  394. const formatKeys: any[] = [];
  395. formatValuePath.forEach(v => {
  396. const formatKeyItem = findKeysForValues(v, keyEntities);
  397. !isEmpty(formatKeyItem) && formatKeys.push(formatKeyItem);
  398. });
  399. realKeys = formatKeys;
  400. }
  401. const calRes = calcCheckedKeys(flatten(realKeys as string[]), keyEntities);
  402. const checkedKeys = new Set(calRes.checkedKeys);
  403. const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
  404. // disableStrictly
  405. if (props.disableStrictly) {
  406. newState.disabledKeys = calcDisabledKeys(keyEntities);
  407. }
  408. const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;
  409. newState.prevProps = props;
  410. newState.checkedKeys = checkedKeys;
  411. newState.halfCheckedKeys = halfCheckedKeys;
  412. newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));
  413. }
  414. }
  415. return newState;
  416. }
  417. componentDidMount() {
  418. this.foundation.init();
  419. }
  420. componentWillUnmount() {
  421. this.foundation.destroy();
  422. }
  423. componentDidUpdate(prevProps: CascaderProps) {
  424. let isOptionsChanged = false;
  425. if (!isEqual(prevProps.treeData, this.props.treeData)) {
  426. isOptionsChanged = true;
  427. this.foundation.collectOptions();
  428. }
  429. if (prevProps.value !== this.props.value && !isOptionsChanged) {
  430. this.foundation.handleValueChange(this.props.value);
  431. }
  432. }
  433. handleInputChange = (value: string) => {
  434. this.foundation.handleInputChange(value);
  435. };
  436. handleTagRemove = (e: any, tagValuePath: Array<string | number>) => {
  437. this.foundation.handleTagRemove(e, tagValuePath);
  438. };
  439. renderTagItem = (value: string | Array<string>, idx: number, type: string) => {
  440. const { keyEntities, disabledKeys } = this.state;
  441. const { size, disabled, displayProp, displayRender, disableStrictly } = this.props;
  442. const nodeKey = type === strings.IS_VALUE ?
  443. findKeysForValues(value, keyEntities)[0] :
  444. value;
  445. const isDsiabled = disabled ||
  446. keyEntities[nodeKey].data.disabled ||
  447. (disableStrictly && disabledKeys.has(nodeKey));
  448. if (!isEmpty(keyEntities) && !isEmpty(keyEntities[nodeKey])) {
  449. const tagCls = cls(`${prefixcls}-selection-tag`, {
  450. [`${prefixcls}-selection-tag-disabled`]: isDsiabled,
  451. });
  452. // custom render tags
  453. if (isFunction(displayRender)) {
  454. return displayRender(keyEntities[nodeKey], idx);
  455. // default render tags
  456. } else {
  457. return (
  458. <Tag
  459. size={size === 'default' ? 'large' : size}
  460. key={`tag-${nodeKey}-${idx}`}
  461. color="white"
  462. className={tagCls}
  463. closable
  464. onClose={(tagChildren, e) => {
  465. // When value has not changed, prevent clicking tag closeBtn to close tag
  466. e.preventDefault();
  467. this.handleTagRemove(e, keyEntities[nodeKey].valuePath);
  468. }}
  469. >
  470. {displayProp === 'label' && keyEntities[nodeKey].data.label}
  471. {displayProp === 'value' && keyEntities[nodeKey].data.value}
  472. </Tag>
  473. );
  474. }
  475. }
  476. return null;
  477. };
  478. renderTagInput() {
  479. const {
  480. size,
  481. disabled,
  482. placeholder,
  483. maxTagCount,
  484. showRestTagsPopover,
  485. restTagsPopoverProps,
  486. } = this.props;
  487. const {
  488. inputValue,
  489. checkedKeys,
  490. keyEntities,
  491. resolvedCheckedKeys
  492. } = this.state;
  493. const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);
  494. const tagValue: Array<Array<string>> = [];
  495. const realKeys = this.mergeType === strings.NONE_MERGE_TYPE
  496. ? checkedKeys
  497. : resolvedCheckedKeys;
  498. [...realKeys].forEach(checkedKey => {
  499. if (!isEmpty(keyEntities[checkedKey])) {
  500. tagValue.push(keyEntities[checkedKey].valuePath);
  501. }
  502. });
  503. return (
  504. <TagInput
  505. className={tagInputcls}
  506. ref={this.inputRef as any}
  507. disabled={disabled}
  508. size={size}
  509. // TODO Modify logic, not modify type
  510. value={tagValue as unknown as string[]}
  511. showRestTagsPopover={showRestTagsPopover}
  512. restTagsPopoverProps={restTagsPopoverProps}
  513. maxTagCount={maxTagCount}
  514. renderTagItem={(value, index) => this.renderTagItem(value, index, strings.IS_VALUE)}
  515. inputValue={inputValue}
  516. onInputChange={this.handleInputChange}
  517. // TODO Modify logic, not modify type
  518. onRemove={v => this.handleTagRemove(null, v as unknown as (string | number)[])}
  519. placeholder={placeholder}
  520. />
  521. );
  522. }
  523. renderInput() {
  524. const { size, disabled } = this.props;
  525. const inputcls = cls(`${prefixcls}-input`);
  526. const { inputValue, inputPlaceHolder } = this.state;
  527. const inputProps = {
  528. disabled,
  529. value: inputValue,
  530. className: inputcls,
  531. onChange: this.handleInputChange,
  532. placeholder: inputPlaceHolder,
  533. };
  534. const wrappercls = cls({
  535. [`${prefixcls}-search-wrapper`]: true,
  536. });
  537. return (
  538. <div className={wrappercls}>
  539. <Input
  540. ref={this.inputRef as any}
  541. size={size}
  542. {...inputProps}
  543. />
  544. </div>
  545. );
  546. }
  547. handleItemClick = (e: MouseEvent, item: Entity | Data) => {
  548. this.foundation.handleItemClick(e, item);
  549. };
  550. handleItemHover = (e: MouseEvent, item: Entity) => {
  551. this.foundation.handleItemHover(e, item);
  552. };
  553. onItemCheckboxClick = (item: Entity | Data) => {
  554. this.foundation.onItemCheckboxClick(item);
  555. };
  556. handleListScroll = (e: React.UIEvent<HTMLUListElement, UIEvent>, ind: number) => {
  557. this.foundation.handleListScroll(e, ind);
  558. };
  559. renderContent = () => {
  560. const {
  561. inputValue,
  562. isSearching,
  563. activeKeys,
  564. selectedKeys,
  565. checkedKeys,
  566. halfCheckedKeys,
  567. loadedKeys,
  568. loadingKeys
  569. } = this.state;
  570. const {
  571. filterTreeNode,
  572. dropdownClassName,
  573. dropdownStyle,
  574. loadData,
  575. emptyContent,
  576. separator,
  577. topSlot,
  578. bottomSlot,
  579. showNext,
  580. multiple
  581. } = this.props;
  582. const searchable = Boolean(filterTreeNode) && isSearching;
  583. const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
  584. const renderData = this.foundation.getRenderData();
  585. const content = (
  586. <div className={popoverCls} role="list-box" style={dropdownStyle}>
  587. {topSlot}
  588. <Item
  589. activeKeys={activeKeys}
  590. selectedKeys={selectedKeys}
  591. separator={separator}
  592. loadedKeys={loadedKeys}
  593. loadingKeys={loadingKeys}
  594. onItemClick={this.handleItemClick}
  595. onItemHover={this.handleItemHover}
  596. showNext={showNext}
  597. onItemCheckboxClick={this.onItemCheckboxClick}
  598. onListScroll={this.handleListScroll}
  599. searchable={searchable}
  600. keyword={inputValue}
  601. emptyContent={emptyContent}
  602. loadData={loadData}
  603. data={renderData}
  604. multiple={multiple}
  605. checkedKeys={checkedKeys}
  606. halfCheckedKeys={halfCheckedKeys}
  607. />
  608. {bottomSlot}
  609. </div>
  610. );
  611. return content;
  612. };
  613. renderPlusN = (hiddenTag: Array<ReactNode>) => {
  614. const { disabled, showRestTagsPopover, restTagsPopoverProps } = this.props;
  615. const plusNCls = cls(`${prefixcls}-selection-n`, {
  616. [`${prefixcls}-selection-n-disabled`]: disabled
  617. });
  618. const renderPlusNChildren = (
  619. <span className={plusNCls}>
  620. +{hiddenTag.length}
  621. </span>
  622. );
  623. return (
  624. showRestTagsPopover && !disabled ?
  625. (
  626. <Popover
  627. content={hiddenTag}
  628. showArrow
  629. trigger="hover"
  630. position="top"
  631. autoAdjustOverflow
  632. {...restTagsPopoverProps}
  633. >
  634. {renderPlusNChildren}
  635. </Popover>
  636. ) :
  637. renderPlusNChildren
  638. );
  639. };
  640. renderMultipleTags = () => {
  641. const { autoMergeValue, maxTagCount } = this.props;
  642. const { checkedKeys, resolvedCheckedKeys } = this.state;
  643. const realKeys = this.mergeType === strings.NONE_MERGE_TYPE
  644. ? checkedKeys
  645. : resolvedCheckedKeys;
  646. const displayTag: Array<ReactNode> = [];
  647. const hiddenTag: Array<ReactNode> = [];
  648. [...realKeys].forEach((checkedKey, idx) => {
  649. const notExceedMaxTagCount = !isNumber(maxTagCount) || maxTagCount >= idx + 1;
  650. const item = this.renderTagItem(checkedKey, idx, strings.IS_KEY);
  651. if (notExceedMaxTagCount) {
  652. displayTag.push(item);
  653. } else {
  654. hiddenTag.push(item);
  655. }
  656. });
  657. return (
  658. <>
  659. {displayTag}
  660. {!isEmpty(hiddenTag) && this.renderPlusN(hiddenTag)}
  661. </>
  662. );
  663. };
  664. renderSelectContent = () => {
  665. const { placeholder, filterTreeNode, multiple } = this.props;
  666. const { selectedKeys, checkedKeys } = this.state;
  667. const searchable = Boolean(filterTreeNode);
  668. if (!searchable) {
  669. if (multiple) {
  670. if (isEmpty(checkedKeys)) {
  671. return <span className={`${prefixcls}-selection-placeholder`}>{placeholder}</span>;
  672. }
  673. return this.renderMultipleTags();
  674. } else {
  675. const displayText = selectedKeys.size ?
  676. this.foundation.renderDisplayText([...selectedKeys][0]) :
  677. '';
  678. const spanCls = cls({
  679. [`${prefixcls}-selection-placeholder`]: !displayText || !displayText.length,
  680. });
  681. return <span className={spanCls}>{displayText ? displayText : placeholder}</span>;
  682. }
  683. }
  684. const input = multiple ? this.renderTagInput() : this.renderInput();
  685. return input;
  686. };
  687. renderSuffix = () => {
  688. const { suffix }: any = this.props;
  689. const suffixWrapperCls = cls({
  690. [`${prefixcls}-suffix`]: true,
  691. [`${prefixcls}-suffix-text`]: suffix && isString(suffix),
  692. [`${prefixcls}-suffix-icon`]: isSemiIcon(suffix),
  693. });
  694. return <div className={suffixWrapperCls}>{suffix}</div>;
  695. };
  696. renderPrefix = () => {
  697. const { prefix, insetLabel } = this.props;
  698. const labelNode: any = prefix || insetLabel;
  699. const prefixWrapperCls = cls({
  700. [`${prefixcls}-prefix`]: true,
  701. // to be doublechecked
  702. [`${prefixcls}-inset-label`]: insetLabel,
  703. [`${prefixcls}-prefix-text`]: labelNode && isString(labelNode),
  704. [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
  705. });
  706. return <div className={prefixWrapperCls}>{labelNode}</div>;
  707. };
  708. renderCustomTrigger = () => {
  709. const { disabled, triggerRender, multiple } = this.props;
  710. const { selectedKeys, inputValue, inputPlaceHolder, resolvedCheckedKeys, checkedKeys } = this.state;
  711. let realValue;
  712. if (multiple) {
  713. if (this.mergeType === strings.NONE_MERGE_TYPE) {
  714. realValue = checkedKeys;
  715. } else {
  716. realValue = resolvedCheckedKeys;
  717. }
  718. } else {
  719. realValue = [...selectedKeys][0];
  720. }
  721. return (
  722. <Trigger
  723. value={realValue}
  724. inputValue={inputValue}
  725. onChange={this.handleInputChange}
  726. onClear={this.handleClear}
  727. placeholder={inputPlaceHolder}
  728. disabled={disabled}
  729. triggerRender={triggerRender}
  730. componentName={'Cascader'}
  731. componentProps={{ ...this.props }}
  732. />
  733. );
  734. };
  735. handleMouseOver = () => {
  736. this.foundation.toggleHoverState(true);
  737. };
  738. handleMouseLeave = () => {
  739. this.foundation.toggleHoverState(false);
  740. };
  741. handleClear = (e: MouseEvent) => {
  742. e && e.stopPropagation();
  743. this.foundation.handleClear();
  744. };
  745. showClearBtn = () => {
  746. const { showClear, disabled, multiple } = this.props;
  747. const { selectedKeys, isOpen, isHovering, checkedKeys } = this.state;
  748. const hasValue = selectedKeys.size;
  749. const multipleWithHaveValue = multiple && checkedKeys.size;
  750. return showClear && (hasValue || multipleWithHaveValue) && !disabled && (isOpen || isHovering);
  751. };
  752. renderClearBtn = () => {
  753. const clearCls = cls(`${prefixcls}-clearbtn`);
  754. const allowClear = this.showClearBtn();
  755. if (allowClear) {
  756. return (
  757. <div className={clearCls} onClick={this.handleClear}>
  758. <IconClear />
  759. </div>
  760. );
  761. }
  762. return null;
  763. };
  764. renderArrow = () => {
  765. const { arrowIcon } = this.props;
  766. const showClearBtn = this.showClearBtn();
  767. if (showClearBtn) {
  768. return null;
  769. }
  770. return arrowIcon ? <div className={cls(`${prefixcls}-arrow`)}>{arrowIcon}</div> : null;
  771. };
  772. renderSelection = () => {
  773. const {
  774. disabled,
  775. multiple,
  776. filterTreeNode,
  777. style,
  778. size,
  779. className,
  780. validateStatus,
  781. prefix,
  782. suffix,
  783. insetLabel,
  784. triggerRender,
  785. showClear,
  786. } = this.props;
  787. const { isOpen, isFocus, isInput, checkedKeys } = this.state;
  788. const filterable = Boolean(filterTreeNode);
  789. const useCustomTrigger = typeof triggerRender === 'function';
  790. const classNames = useCustomTrigger ?
  791. cls(className) :
  792. cls(prefixcls, className, {
  793. [`${prefixcls}-focus`]: isFocus || (isOpen && !isInput),
  794. [`${prefixcls}-disabled`]: disabled,
  795. [`${prefixcls}-single`]: true,
  796. [`${prefixcls}-filterable`]: filterable,
  797. [`${prefixcls}-error`]: validateStatus === 'error',
  798. [`${prefixcls}-warning`]: validateStatus === 'warning',
  799. [`${prefixcls}-small`]: size === 'small',
  800. [`${prefixcls}-large`]: size === 'large',
  801. [`${prefixcls}-with-prefix`]: prefix || insetLabel,
  802. [`${prefixcls}-with-suffix`]: suffix,
  803. });
  804. const mouseEvent = showClear ?
  805. {
  806. onMouseEnter: () => this.handleMouseOver(),
  807. onMouseLeave: () => this.handleMouseLeave(),
  808. } :
  809. {};
  810. const sectionCls = cls(`${prefixcls}-selection`, {
  811. [`${prefixcls}-selection-multiple`]: multiple && !isEmpty(checkedKeys),
  812. });
  813. const inner = useCustomTrigger ?
  814. this.renderCustomTrigger() :
  815. [
  816. <Fragment key={'prefix'}>{prefix || insetLabel ? this.renderPrefix() : null}</Fragment>,
  817. <Fragment key={'selection'}>
  818. <div className={sectionCls}>{this.renderSelectContent()}</div>
  819. </Fragment>,
  820. <Fragment key={'clearbtn'}>{this.renderClearBtn()}</Fragment>,
  821. <Fragment key={'suffix'}>{suffix ? this.renderSuffix() : null}</Fragment>,
  822. <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
  823. ];
  824. return (
  825. <div
  826. className={classNames}
  827. style={style}
  828. ref={this.triggerRef}
  829. onClick={e => this.foundation.handleClick(e)}
  830. {...mouseEvent}
  831. >
  832. {inner}
  833. </div>
  834. );
  835. };
  836. render() {
  837. const {
  838. zIndex,
  839. getPopupContainer,
  840. autoAdjustOverflow,
  841. stopPropagation,
  842. mouseLeaveDelay,
  843. mouseEnterDelay,
  844. } = this.props;
  845. const { isOpen, rePosKey } = this.state;
  846. const { direction } = this.context;
  847. const content = this.renderContent();
  848. const selection = this.renderSelection();
  849. const pos = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  850. const mergedMotion: Motion = this.foundation.getMergedMotion();
  851. return (
  852. <Popover
  853. getPopupContainer={getPopupContainer}
  854. zIndex={zIndex}
  855. motion={mergedMotion}
  856. ref={this.optionsRef}
  857. content={content}
  858. visible={isOpen}
  859. trigger="custom"
  860. rePosKey={rePosKey}
  861. position={pos}
  862. autoAdjustOverflow={autoAdjustOverflow}
  863. stopPropagation={stopPropagation}
  864. mouseLeaveDelay={mouseLeaveDelay}
  865. mouseEnterDelay={mouseEnterDelay}
  866. >
  867. {selection}
  868. </Popover>
  869. );
  870. }
  871. }
  872. export default Cascader;