UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

582 lines 18 kB
import _isFunction from "lodash/isFunction"; import _isUndefined from "lodash/isUndefined"; import _isNull from "lodash/isNull"; import _isArray from "lodash/isArray"; import _isString from "lodash/isString"; import _noop from "lodash/noop"; var __rest = this && this.__rest || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import React from 'react'; import cls from 'classnames'; import PropTypes from 'prop-types'; import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/tagInput/constants'; import '@douyinfe/semi-foundation/lib/es/tagInput/tagInput.css'; import TagInputFoundation from '@douyinfe/semi-foundation/lib/es/tagInput/foundation'; import { isSemiIcon } from '../_utils'; import BaseComponent from '../_base/baseComponent'; import Tag from '../tag'; import Input from '../input'; import Popover from '../popover'; import Paragraph from '../typography/paragraph'; import { IconClear, IconHandle } from '@douyinfe/semi-icons'; import { Sortable } from '../_sortable'; const prefixCls = cssClasses.PREFIX; function SortContainer(props) { return /*#__PURE__*/React.createElement("div", Object.assign({ className: `${prefixCls}-sortable-list` }, props)); } class TagInput extends BaseComponent { constructor(props) { super(props); this.handleInputChange = e => { this.foundation.handleInputChange(e); }; this.handleKeyDown = e => { this.foundation.handleKeyDown(e); }; this.handleInputFocus = e => { this.foundation.handleInputFocus(e); }; this.handleInputBlur = e => { this.foundation.handleInputBlur(e); }; this.handleClearBtn = e => { this.foundation.handleClearBtn(e); }; /* istanbul ignore next */ this.handleClearEnterPress = e => { this.foundation.handleClearEnterPress(e); }; this.handleTagClose = idx => { this.foundation.handleTagClose(idx); }; this.handleInputMouseLeave = e => { this.foundation.handleInputMouseLeave(); }; this.handleClick = e => { this.foundation.handleClick(e); }; this.handleInputMouseEnter = e => { this.foundation.handleInputMouseEnter(); }; this.handleClickPrefixOrSuffix = e => { this.foundation.handleClickPrefixOrSuffix(e); }; this.handlePreventMouseDown = e => { this.foundation.handlePreventMouseDown(e); }; this.getAllTags = () => { const { tagsArray } = this.state; return tagsArray.map((value, index) => this.renderTag(value, index)); }; this.renderTag = (value, index, sortableHandle) => { const { size, disabled, renderTagItem, showContentTooltip, draggable } = this.props; const { active } = this.state; const showIconHandler = active && draggable; const tagCls = cls(`${prefixCls}-wrapper-tag`, { [`${prefixCls}-wrapper-tag-size-${size}`]: size, [`${prefixCls}-wrapper-tag-icon`]: showIconHandler }); const typoCls = cls(`${prefixCls}-wrapper-typo`, { [`${prefixCls}-wrapper-typo-disabled`]: disabled }); const itemWrapperCls = cls({ [`${prefixCls}-drag-item`]: showIconHandler, [`${prefixCls}-wrapper-tag-icon`]: showIconHandler }); const DragHandle = sortableHandle && sortableHandle(() => /*#__PURE__*/React.createElement(IconHandle, { className: `${prefixCls}-drag-handler` })); const elementKey = showIconHandler ? value : `${index}${value}`; const onClose = () => { !disabled && this.handleTagClose(index); }; if (_isFunction(renderTagItem)) { return /*#__PURE__*/React.createElement("div", { className: itemWrapperCls, key: elementKey }, showIconHandler && sortableHandle ? /*#__PURE__*/React.createElement(DragHandle, null) : null, renderTagItem(value, index, onClose)); } else { return /*#__PURE__*/React.createElement(Tag, { className: tagCls, color: "white", size: size === 'small' ? 'small' : 'large', type: "light", onClose: onClose, closable: !disabled, key: elementKey, visible: true, "aria-label": `${!disabled ? 'Closable ' : ''}Tag: ${value}` }, showIconHandler && sortableHandle ? /*#__PURE__*/React.createElement(DragHandle, null) : null, /*#__PURE__*/React.createElement(Paragraph, { className: typoCls, ellipsis: { showTooltip: showContentTooltip, rows: 1 } }, value)); } }; this.renderSortTag = props => { const { id: item, sortableHandle } = props; const { tagsArray } = this.state; const index = tagsArray.indexOf(item); return this.renderTag(item, index, sortableHandle); }; this.onSortEnd = callbackProps => { this.foundation.handleSortEnd(callbackProps); }; this.handleInputCompositionStart = e => { this.foundation.handleInputCompositionStart(e); }; this.handleInputCompositionEnd = e => { this.foundation.handleInputCompositionEnd(e); }; this.foundation = new TagInputFoundation(this.adapter); this.state = { tagsArray: props.defaultValue || [], inputValue: '', focusing: false, hovering: false, active: false, entering: false }; this.inputRef = /*#__PURE__*/React.createRef(); this.tagInputRef = /*#__PURE__*/React.createRef(); this.clickOutsideHandler = null; } static getDerivedStateFromProps(nextProps, prevState) { const { value, inputValue } = nextProps; const { tagsArray: prevTagsArray } = prevState; let tagsArray; if (_isArray(value)) { tagsArray = value; } else if ('value' in nextProps && !value) { tagsArray = []; } else { tagsArray = prevTagsArray; } return { tagsArray, inputValue: _isString(inputValue) ? inputValue : prevState.inputValue }; } get adapter() { return Object.assign(Object.assign({}, super.adapter), { setInputValue: inputValue => { this.setState({ inputValue }); }, setTagsArray: tagsArray => { this.setState({ tagsArray }); }, setFocusing: focusing => { this.setState({ focusing }); }, toggleFocusing: isFocus => { const { preventScroll } = this.props; const input = this.inputRef && this.inputRef.current; if (isFocus) { input && input.focus({ preventScroll }); } else { input && input.blur(); } this.setState({ focusing: isFocus }); }, setHovering: hovering => { this.setState({ hovering }); }, setActive: active => { this.setState({ active }); }, setEntering: entering => { this.setState({ entering }); }, getClickOutsideHandler: () => { return this.clickOutsideHandler; }, notifyBlur: e => { this.props.onBlur(e); }, notifyFocus: e => { this.props.onFocus(e); }, notifyInputChange: (v, e) => { this.props.onInputChange(v, e); }, notifyTagChange: v => { this.props.onChange(v); }, notifyTagAdd: v => { this.props.onAdd(v); }, notifyTagRemove: (v, idx) => { this.props.onRemove(v, idx); }, notifyKeyDown: e => { this.props.onKeyDown(e); }, registerClickOutsideHandler: cb => { const clickOutsideHandler = e => { const tagInputDom = this.tagInputRef && this.tagInputRef.current; const target = e.target; const path = e.composedPath && e.composedPath() || [target]; if (tagInputDom && !tagInputDom.contains(target) && !path.includes(tagInputDom)) { cb(e); } }; this.clickOutsideHandler = clickOutsideHandler; document.addEventListener('click', clickOutsideHandler, false); }, unregisterClickOutsideHandler: () => { document.removeEventListener('click', this.clickOutsideHandler, false); this.clickOutsideHandler = null; } }); } componentDidMount() { const { disabled, autoFocus, preventScroll } = this.props; if (!disabled && autoFocus) { this.inputRef.current.focus({ preventScroll }); this.foundation.handleClick(); } this.foundation.init(); } renderClearBtn() { const { hovering, tagsArray, inputValue } = this.state; const { showClear, disabled, clearIcon } = this.props; const clearCls = cls(`${prefixCls}-clearBtn`, { [`${prefixCls}-clearBtn-invisible`]: !hovering || inputValue === '' && tagsArray.length === 0 || disabled }); if (showClear) { return /*#__PURE__*/React.createElement("div", { role: "button", tabIndex: 0, "aria-label": "Clear TagInput value", className: clearCls, onClick: e => this.handleClearBtn(e), onKeyPress: e => this.handleClearEnterPress(e) }, clearIcon ? clearIcon : /*#__PURE__*/React.createElement(IconClear, null)); } return null; } renderPrefix() { const { prefix, insetLabel, insetLabelId } = this.props; const labelNode = prefix || insetLabel; if (_isNull(labelNode) || _isUndefined(labelNode)) { return null; } const prefixWrapperCls = cls(`${prefixCls}-prefix`, { [`${prefixCls}-inset-label`]: insetLabel, [`${prefixCls}-prefix-text`]: labelNode && _isString(labelNode), [`${prefixCls}-prefix-icon`]: isSemiIcon(labelNode) }); return ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events React.createElement("div", { className: prefixWrapperCls, onMouseDown: this.handlePreventMouseDown, onClick: this.handleClickPrefixOrSuffix, id: insetLabelId, "x-semi-prop": "prefix" }, labelNode) ); } renderSuffix() { const { suffix } = this.props; if (_isNull(suffix) || _isUndefined(suffix)) { return null; } const suffixWrapperCls = cls(`${prefixCls}-suffix`, { [`${prefixCls}-suffix-text`]: suffix && _isString(suffix), [`${prefixCls}-suffix-icon`]: isSemiIcon(suffix) }); return ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions React.createElement("div", { className: suffixWrapperCls, onMouseDown: this.handlePreventMouseDown, onClick: this.handleClickPrefixOrSuffix, "x-semi-prop": "suffix" }, suffix) ); } renderTags() { const { disabled, maxTagCount, showRestTagsPopover, restTagsPopoverProps = {}, draggable, expandRestTagsOnClick } = this.props; const { tagsArray, active } = this.state; const restTagsCls = cls(`${prefixCls}-wrapper-n`, { [`${prefixCls}-wrapper-n-disabled`]: disabled }); const allTags = this.getAllTags(); let restTags = []; let tags = [...allTags]; if ((!active || !expandRestTagsOnClick) && maxTagCount && maxTagCount < allTags.length) { tags = allTags.slice(0, maxTagCount); restTags = allTags.slice(maxTagCount); } const restTagsContent = /*#__PURE__*/React.createElement("span", { className: restTagsCls }, "+", tagsArray.length - maxTagCount); const sortableListItems = allTags.map((item, index) => ({ item: item, key: tagsArray[index] })); if (active && draggable && sortableListItems.length > 0) { return /*#__PURE__*/React.createElement(Sortable, { items: tagsArray, onSortEnd: this.onSortEnd, renderItem: this.renderSortTag, container: SortContainer, prefix: prefixCls, transition: null, dragOverlayCls: `${prefixCls}-right-item-drag-item-move` }); } return /*#__PURE__*/React.createElement(React.Fragment, null, tags, restTags.length > 0 && (showRestTagsPopover ? (/*#__PURE__*/React.createElement(Popover, Object.assign({ content: restTags, showArrow: true, trigger: "hover", position: "top", autoAdjustOverflow: true }, restTagsPopoverProps), restTagsContent)) : restTagsContent)); } blur() { this.inputRef.current.blur(); // unregister clickOutside event this.foundation.clickOutsideCallBack(); } focus() { const { preventScroll, disabled } = this.props; this.inputRef.current.focus({ preventScroll }); if (!disabled) { // register clickOutside event this.foundation.handleClick(); } } render() { const _a = this.props, { size, style, className, disabled, placeholder, validateStatus, prefix, insetLabel, suffix } = _a, rest = __rest(_a, ["size", "style", "className", "disabled", "placeholder", "validateStatus", "prefix", "insetLabel", "suffix"]); const { focusing, hovering, tagsArray, inputValue, active } = this.state; const tagInputCls = cls(prefixCls, className, { [`${prefixCls}-focus`]: focusing || active, [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-hover`]: hovering && !disabled, [`${prefixCls}-error`]: validateStatus === 'error', [`${prefixCls}-warning`]: validateStatus === 'warning', [`${prefixCls}-small`]: size === 'small', [`${prefixCls}-large`]: size === 'large', [`${prefixCls}-with-prefix`]: !!prefix || !!insetLabel, [`${prefixCls}-with-suffix`]: !!suffix }); const inputCls = cls(`${prefixCls}-wrapper-input`, `${prefixCls}-wrapper-input-${size}`); const wrapperCls = cls(`${prefixCls}-wrapper`); return ( /*#__PURE__*/ // eslint-disable-next-line React.createElement("div", Object.assign({ ref: this.tagInputRef, style: style, className: tagInputCls, "aria-disabled": disabled, "aria-label": this.props['aria-label'], "aria-invalid": validateStatus === 'error', onMouseEnter: e => { this.handleInputMouseEnter(e); }, onMouseLeave: e => { this.handleInputMouseLeave(e); }, onClick: e => { this.handleClick(e); } }, this.getDataAttr(rest)), this.renderPrefix(), /*#__PURE__*/React.createElement("div", { className: wrapperCls }, this.renderTags(), /*#__PURE__*/React.createElement(Input, { "aria-label": 'input value', ref: this.inputRef, className: inputCls, disabled: disabled, value: inputValue, size: size, placeholder: tagsArray.length === 0 ? placeholder : '', onKeyDown: e => { this.handleKeyDown(e); }, onChange: (v, e) => { this.handleInputChange(e); }, onBlur: e => { this.handleInputBlur(e); }, onFocus: e => { this.handleInputFocus(e); }, onCompositionStart: this.handleInputCompositionStart, onCompositionEnd: this.handleInputCompositionEnd })), this.renderClearBtn(), this.renderSuffix()) ); } } TagInput.propTypes = { children: PropTypes.node, clearIcon: PropTypes.node, style: PropTypes.object, className: PropTypes.string, disabled: PropTypes.bool, allowDuplicates: PropTypes.bool, max: PropTypes.number, maxTagCount: PropTypes.number, maxLength: PropTypes.number, showRestTagsPopover: PropTypes.bool, restTagsPopoverProps: PropTypes.object, showContentTooltip: PropTypes.oneOfType([PropTypes.shape({ type: PropTypes.string, opts: PropTypes.object }), PropTypes.bool]), defaultValue: PropTypes.array, value: PropTypes.array, inputValue: PropTypes.string, placeholder: PropTypes.string, separator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), showClear: PropTypes.bool, addOnBlur: PropTypes.bool, draggable: PropTypes.bool, expandRestTagsOnClick: PropTypes.bool, autoFocus: PropTypes.bool, renderTagItem: PropTypes.func, onBlur: PropTypes.func, onFocus: PropTypes.func, onChange: PropTypes.func, onInputChange: PropTypes.func, onExceed: PropTypes.func, onInputExceed: PropTypes.func, onAdd: PropTypes.func, onRemove: PropTypes.func, onKeyDown: PropTypes.func, size: PropTypes.oneOf(strings.SIZE_SET), validateStatus: PropTypes.oneOf(strings.STATUS), prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 'aria-label': PropTypes.string, preventScroll: PropTypes.bool }; TagInput.defaultProps = { showClear: false, addOnBlur: false, allowDuplicates: true, showRestTagsPopover: true, autoFocus: false, draggable: false, expandRestTagsOnClick: true, showContentTooltip: true, separator: ',', size: 'default', validateStatus: 'default', onBlur: _noop, onFocus: _noop, onChange: _noop, onInputChange: _noop, onExceed: _noop, onInputExceed: _noop, onAdd: _noop, onRemove: _noop, onKeyDown: _noop }; export default TagInput;