@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
JavaScript
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;