index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import {
  5. noop,
  6. isString,
  7. isArray,
  8. isNull,
  9. isUndefined,
  10. isFunction
  11. } from 'lodash-es';
  12. import { cssClasses, strings } from '@douyinfe/semi-foundation/tagInput/constants';
  13. import '@douyinfe/semi-foundation/tagInput/tagInput.scss';
  14. import TagInputFoundation, { TagInputAdapter } from '@douyinfe/semi-foundation/tagInput/foundation';
  15. import { ArrayElement } from '../_base/base';
  16. import BaseComponent from '../_base/baseComponent';
  17. import Tag from '../tag';
  18. import Input from '../input';
  19. import Popover, { PopoverProps } from '../popover';
  20. import Paragraph from '../typography/paragraph';
  21. import { IconClear } from '@douyinfe/semi-icons';
  22. export type Size = ArrayElement<typeof strings.SIZE_SET>;
  23. export type RestTagsPopoverProps = PopoverProps;
  24. type ValidateStatus = "default" | "error" | "warning";
  25. export interface TagInputProps {
  26. className?: string;
  27. defaultValue?: string[];
  28. disabled?: boolean;
  29. inputValue?: string;
  30. maxLength?: number;
  31. max?: number;
  32. maxTagCount?: number;
  33. showRestTagsPopover?: boolean;
  34. restTagsPopoverProps?: RestTagsPopoverProps;
  35. showContentTooltip?: boolean;
  36. allowDuplicates?: boolean;
  37. addOnBlur?: boolean;
  38. onAdd?: (addedValue: string[]) => void;
  39. onBlur?: (e: React.MouseEvent<HTMLInputElement>) => void;
  40. onChange?: (value: string[]) => void;
  41. onExceed?: ((value: string[]) => void);
  42. onFocus?: (e: React.MouseEvent<HTMLInputElement>) => void;
  43. onInputChange?: (value: string, e: React.MouseEvent<HTMLInputElement>) => void;
  44. onInputExceed?: ((value: string) => void);
  45. onRemove?: (removedValue: string, idx: number) => void;
  46. placeholder?: string;
  47. prefix?: React.ReactNode;
  48. renderTagItem?: (value: string, index: number) => React.ReactNode;
  49. separator?: string | string[];
  50. showClear?: boolean;
  51. size?: Size;
  52. style?: React.CSSProperties;
  53. suffix?: React.ReactNode;
  54. validateStatus?: ValidateStatus;
  55. value?: string[];
  56. autoFocus?: boolean;
  57. }
  58. export interface TagInputState {
  59. tagsArray?: string[];
  60. inputValue?: string;
  61. focusing?: boolean;
  62. hovering?: boolean;
  63. }
  64. const prefixCls = cssClasses.PREFIX;
  65. class TagInput extends BaseComponent<TagInputProps, TagInputState> {
  66. static propTypes = {
  67. children: PropTypes.node,
  68. style: PropTypes.object,
  69. className: PropTypes.string,
  70. disabled: PropTypes.bool,
  71. allowDuplicates: PropTypes.bool,
  72. max: PropTypes.number,
  73. maxTagCount: PropTypes.number,
  74. maxLength: PropTypes.number,
  75. showRestTagsPopover: PropTypes.bool,
  76. restTagsPopoverProps: PropTypes.object,
  77. showContentTooltip: PropTypes.bool,
  78. defaultValue: PropTypes.array,
  79. value: PropTypes.array,
  80. inputValue: PropTypes.string,
  81. placeholder: PropTypes.string,
  82. separator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  83. showClear: PropTypes.bool,
  84. addOnBlur: PropTypes.bool,
  85. autoFocus: PropTypes.bool,
  86. renderTagItem: PropTypes.func,
  87. onBlur: PropTypes.func,
  88. onFocus: PropTypes.func,
  89. onChange: PropTypes.func,
  90. onInputChange: PropTypes.func,
  91. onExceed: PropTypes.func,
  92. onInputExceed: PropTypes.func,
  93. onAdd: PropTypes.func,
  94. onRemove: PropTypes.func,
  95. size: PropTypes.oneOf(strings.SIZE_SET),
  96. validateStatus: PropTypes.oneOf(strings.STATUS),
  97. prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  98. suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
  99. };
  100. static defaultProps = {
  101. showClear: false,
  102. addOnBlur: false,
  103. allowDuplicates: true,
  104. showRestTagsPopover: true,
  105. autoFocus: false,
  106. showContentTooltip: true,
  107. separator: ',',
  108. size: 'default' as const,
  109. validateStatus: 'default' as const,
  110. onBlur: noop,
  111. onFocus: noop,
  112. onChange: noop,
  113. onInputChange: noop,
  114. onExceed: noop,
  115. onInputExceed: noop,
  116. onAdd: noop,
  117. onRemove: noop
  118. };
  119. inputRef: React.RefObject<HTMLInputElement>;
  120. constructor(props: TagInputProps) {
  121. super(props);
  122. this.foundation = new TagInputFoundation(this.adapter);
  123. this.state = {
  124. tagsArray: props.defaultValue || [],
  125. inputValue: '',
  126. focusing: false,
  127. hovering: false
  128. };
  129. this.inputRef = React.createRef();
  130. }
  131. static getDerivedStateFromProps(nextProps: TagInputProps, prevState: TagInputState) {
  132. const {
  133. value,
  134. inputValue,
  135. } = nextProps;
  136. return {
  137. tagsArray: isArray(value) ? value : prevState.tagsArray,
  138. inputValue: isString(inputValue) ? inputValue : prevState.inputValue
  139. };
  140. }
  141. get adapter(): TagInputAdapter {
  142. return {
  143. ...super.adapter,
  144. setInputValue: (inputValue: string) => {
  145. this.setState({ inputValue });
  146. },
  147. setTagsArray: (tagsArray: string[]) => {
  148. this.setState({ tagsArray });
  149. },
  150. setFocusing: (focusing: boolean) => {
  151. this.setState({ focusing });
  152. },
  153. setHovering: (hovering: boolean) => {
  154. this.setState({ hovering });
  155. },
  156. notifyBlur: (e: React.MouseEvent<HTMLInputElement>) => {
  157. this.props.onBlur(e);
  158. },
  159. notifyFocus: (e: React.MouseEvent<HTMLInputElement>) => {
  160. this.props.onFocus(e);
  161. },
  162. notifyInputChange: (v: string, e: React.MouseEvent<HTMLInputElement>) => {
  163. this.props.onInputChange(v, e);
  164. },
  165. notifyTagChange: (v: string[]) => {
  166. this.props.onChange(v);
  167. },
  168. notifyTagAdd: (v: string[]) => {
  169. this.props.onAdd(v);
  170. },
  171. notifyTagRemove: (v: string, idx: number) => {
  172. this.props.onRemove(v, idx);
  173. }
  174. };
  175. }
  176. componentDidMount() {
  177. const { disabled, autoFocus } = this.props;
  178. if (!disabled && autoFocus) {
  179. this.inputRef.current.focus();
  180. }
  181. }
  182. handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  183. this.foundation.handleInputChange(e);
  184. };
  185. handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  186. this.foundation.handleKeyDown(e);
  187. };
  188. handleInputFocus = (e: React.MouseEvent<HTMLInputElement>) => {
  189. this.foundation.handleInputFocus(e);
  190. };
  191. handleInputBlur = (e: React.MouseEvent<HTMLInputElement>) => {
  192. this.foundation.handleInputBlur(e);
  193. };
  194. handleClearBtn = (e: React.MouseEvent<HTMLDivElement>) => {
  195. this.foundation.handleClearBtn(e);
  196. };
  197. handleTagClose = (idx: number) => {
  198. this.foundation.handleTagClose(idx);
  199. };
  200. handleInputMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
  201. this.foundation.handleInputMouseLeave();
  202. };
  203. handleInputMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
  204. this.foundation.handleInputMouseEnter();
  205. };
  206. renderClearBtn() {
  207. const { hovering, tagsArray, inputValue } = this.state;
  208. const { showClear, disabled } = this.props;
  209. const clearCls = cls(`${prefixCls}-clearBtn`, {
  210. [`${prefixCls}-clearBtn-invisible`]: !hovering || (inputValue === '' && tagsArray.length === 0) || disabled,
  211. });
  212. if (showClear) {
  213. return (
  214. <div className={clearCls} onClick={e => this.handleClearBtn(e)}>
  215. <IconClear />
  216. </div>
  217. );
  218. }
  219. return null;
  220. }
  221. renderPrefix() {
  222. const { prefix } = this.props;
  223. if (isNull(prefix) || isUndefined(prefix)) {
  224. return null;
  225. }
  226. const prefixWrapperCls = cls(`${prefixCls}-prefix`, {
  227. [`${prefixCls}-prefix-text`]: prefix && isString(prefix),
  228. // eslint-disable-next-line max-len
  229. [`${prefixCls}-prefix-icon`]: React.isValidElement(prefix) && !(prefix && isString(prefix)),
  230. });
  231. return <div className={prefixWrapperCls}>{prefix}</div>;
  232. }
  233. renderSuffix() {
  234. const { suffix } = this.props;
  235. if (isNull(suffix) || isUndefined(suffix)) {
  236. return null;
  237. }
  238. const suffixWrapperCls = cls(`${prefixCls}-suffix`, {
  239. [`${prefixCls}-suffix-text`]: suffix && isString(suffix),
  240. // eslint-disable-next-line max-len
  241. [`${prefixCls}-suffix-icon`]: React.isValidElement(suffix) && !(suffix && isString(suffix)),
  242. });
  243. return <div className={suffixWrapperCls}>{suffix}</div>;
  244. }
  245. renderTags() {
  246. const {
  247. size,
  248. disabled,
  249. renderTagItem,
  250. maxTagCount,
  251. showContentTooltip,
  252. showRestTagsPopover,
  253. restTagsPopoverProps = {},
  254. } = this.props;
  255. const { tagsArray } = this.state;
  256. const tagCls = cls(`${prefixCls}-wrapper-tag`, {
  257. [`${prefixCls}-wrapper-tag-size-${size}`]: size,
  258. });
  259. const typoCls = cls(`${prefixCls}-wrapper-typo`, {
  260. [`${prefixCls}-wrapper-typo-disabled`]: disabled
  261. });
  262. const spanNotWithPopoverCls = cls(`${prefixCls}-wrapper-n`, {
  263. [`${prefixCls}-wrapper-n-disabled`]: disabled
  264. });
  265. const restTags: Array<React.ReactNode> = [];
  266. const tags: Array<React.ReactNode> = [];
  267. tagsArray.forEach((value, index) => {
  268. let item = null;
  269. if (isFunction(renderTagItem)) {
  270. item = renderTagItem(value, index);
  271. } else {
  272. item = (
  273. <Tag
  274. className={tagCls}
  275. color="white"
  276. size={size === 'small' ? 'small' : 'large'}
  277. type="light"
  278. onClose={() => {
  279. !disabled && this.handleTagClose(index);
  280. }}
  281. closable={!disabled}
  282. key={`${index}${value}`}
  283. visible
  284. >
  285. <Paragraph
  286. className={typoCls}
  287. ellipsis={{ showTooltip: showContentTooltip, rows: 1 }}
  288. >
  289. {value}
  290. </Paragraph>
  291. </Tag>
  292. );
  293. }
  294. if (maxTagCount && index >= maxTagCount) {
  295. restTags.push(item);
  296. } else {
  297. tags.push(item);
  298. }
  299. });
  300. return (
  301. <>
  302. {tags}
  303. {
  304. restTags.length > 0 &&
  305. (
  306. showRestTagsPopover && !disabled ?
  307. (
  308. <Popover
  309. content={restTags}
  310. showArrow
  311. trigger="hover"
  312. position="top"
  313. autoAdjustOverflow
  314. {...restTagsPopoverProps}
  315. >
  316. <span className={cls(`${prefixCls}-wrapper-n`)}>
  317. +{tagsArray.length - maxTagCount}
  318. </span>
  319. </Popover>
  320. ) :
  321. (
  322. <span className={spanNotWithPopoverCls}>
  323. {`+${tagsArray.length - maxTagCount}`}
  324. </span>
  325. )
  326. )
  327. }
  328. </>
  329. );
  330. }
  331. blur() {
  332. this.inputRef.current.blur();
  333. }
  334. focus() {
  335. this.inputRef.current.focus();
  336. }
  337. render() {
  338. const {
  339. size,
  340. style,
  341. className,
  342. disabled,
  343. placeholder,
  344. validateStatus,
  345. } = this.props;
  346. const {
  347. focusing,
  348. hovering,
  349. tagsArray,
  350. inputValue
  351. } = this.state;
  352. const tagInputCls = cls(prefixCls, className, {
  353. [`${prefixCls}-focus`]: focusing,
  354. [`${prefixCls}-disabled`]: disabled,
  355. [`${prefixCls}-hover`]: hovering && !disabled,
  356. [`${prefixCls}-error`]: validateStatus === 'error',
  357. [`${prefixCls}-warning`]: validateStatus === 'warning'
  358. });
  359. const inputCls = cls(`${prefixCls}-wrapper-input`);
  360. const wrapperCls = cls(`${prefixCls}-wrapper`);
  361. return (
  362. <div
  363. style={style}
  364. className={tagInputCls}
  365. onMouseEnter={e => {
  366. this.handleInputMouseEnter(e);
  367. }}
  368. onMouseLeave={e => {
  369. this.handleInputMouseLeave(e);
  370. }}
  371. >
  372. {this.renderPrefix()}
  373. <div className={wrapperCls}>
  374. {this.renderTags()}
  375. <Input
  376. ref={this.inputRef as any}
  377. className={inputCls}
  378. disabled={disabled}
  379. value={inputValue}
  380. size={size}
  381. placeholder={tagsArray.length === 0 ? placeholder : ''}
  382. onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
  383. this.handleKeyDown(e);
  384. }}
  385. onChange={(v: string, e: React.ChangeEvent<HTMLInputElement>) => {
  386. this.handleInputChange(e);
  387. }}
  388. onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
  389. this.handleInputBlur(e as any);
  390. }}
  391. onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
  392. this.handleInputFocus(e as any);
  393. }}
  394. />
  395. </div>
  396. {this.renderClearBtn()}
  397. {this.renderSuffix()}
  398. </div>
  399. );
  400. }
  401. }
  402. export default TagInput;
  403. export { ValidateStatus };