index.tsx 34 KB

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