重构 tag 渲染器

This commit is contained in:
2betop 2020-05-15 16:47:08 +08:00
parent 6452ee38a7
commit f4f10b0d5e
16 changed files with 455 additions and 344 deletions

View File

@ -65,7 +65,37 @@ export default {
label: '标签',
placeholder: '',
clearable: true,
options: ['lixiaolong', 'zhouxingxing', 'yipingpei', 'liyuanfang']
options: [
{
label: '诸葛亮',
value: 'zhugeliang'
},
{
label: '曹操',
value: 'caocao'
},
{
label: '钟无艳',
value: 'zhongwuyan'
},
{
label: '野核',
children: [
{
label: '李白',
value: 'libai'
},
{
label: '韩信',
value: 'hanxin'
},
{
label: '云中君',
value: 'yunzhongjun'
}
]
}
]
},
{
type: 'divider'

View File

@ -1,6 +1,3 @@
import React from 'react';
import ResultBox from '../../../src/components/ResultBox';
export default {
type: 'page',
title: '表单页面',
@ -20,38 +17,6 @@ export default {
label: 'Email',
type: 'email',
name: 'email'
},
{
name: 'checkboxes',
type: 'checkboxes',
joinValues: false,
options: [
{
label: '张学友',
value: 'a'
},
{
label: '刘德华',
value: 'b'
},
{
label: '黎明',
value: 'c'
},
{
label: '郭富城',
value: 'd'
}
]
},
{
label: 'Result',
name: 'checkboxes',
component: ({value, onChange}) => {
return <ResultBox value={value} onChange={onChange} allowInput />;
}
}
]
}

View File

@ -382,6 +382,8 @@ $Spinner-width: px2rem(26px) !default;
$Spinner-height: px2rem(26px) !default;
$Spinner--lg-width: px2rem(50px) !default;
$Spinner--lg-height: px2rem(50px) !default;
$Spinner--sm-width: px2rem(16px) !default;
$Spinner--sm-height: px2rem(16px) !default;
$Spinner-bg: url('./spinner-default.svg?__inline') !default;
@ -589,6 +591,7 @@ $Remark-borderColor: $borderColor !default;
$Remark-onHover-borderColor: $borderColor !default;
$Remark-marginLeft: $gap-sm !default;
// ResultBox
$ResultBox-value-bg: #f5f5f5 !default;
$ResultBox-value--onHover-bg: #ebebeb !default;
$ResultBox-value-color: #000 !default;
@ -597,6 +600,22 @@ $ResultBox-icon-color: #999 !default;
$ResultBox-icon--onHover-color: #666666 !default;
$ResultBox-icon--onDisabled-color: #ebebeb !default;
// ListMenu
$ListMenu-bordrColor: $borderColor !default;
$listMenu--onActive-borderColor: $info !default;
$ListMenu-borderWidth: px2rem(1px) !default;
$ListMenu-borderRadius: px2rem(2px) !default;
$ListMenu-item-height: px2rem(34px) !default;
$ListMenu-divider-color: lighten($borderColor, 2.5%) !default;
$ListMenu-item-bg: $white !default;
$ListMenu-item-color: $text-color !default;
$ListMenu-item--onHover-color: $info !default;
$ListMenu-item--onHover-bg: rgba(0, 126, 255, 0.08) !default;
$ListMenu-item--onActive-color: $info !default;
$ListMenu-item--onActive-bg: transparent !default;
$ListMenu-item--onDisabled-color: $text--muted-color !default;
$ListMenu-item--onDisabled-bg: transparent !default;
// Form
$Form-fontSize: $fontSizeBase !default;
$Form-description-color: lighten($text-color, 10%) !default;

View File

@ -0,0 +1,63 @@
.#{$ns}ListMenu {
min-width: px2rem(200px);
border: $ListMenu-borderWidth solid $ListMenu-bordrColor;
border-radius: $ListMenu-borderRadius;
&-groupLabel {
font-size: $fontSizeXs;
color: $text--muted-color;
padding: (
$ListMenu-item-height - $Form-input-lineHeight * $Form-input-fontSize -
$gap-sm
)/2 0 0 ($Form-select-paddingX - $gap-xs);
}
&-group:not(:first-child) > &-groupLabel {
border-top: px2rem(1px) solid $ListMenu-divider-color;
}
&-item {
display: flex;
min-height: $ListMenu-item-height;
background: $ListMenu-item-bg;
color: $ListMenu-item-color;
line-height: $Form-input-lineHeight;
font-size: $Form-input-fontSize;
cursor: pointer;
padding: (
$ListMenu-item-height - $Form-input-lineHeight * $Form-input-fontSize
)/2 $Form-select-paddingX;
&.is-active {
color: $ListMenu-item--onActive-color;
background-color: $ListMenu-item--onActive-bg;
}
// &:hover,
&.is-highlight {
color: $ListMenu-item--onHover-color;
background-color: $ListMenu-item--onHover-bg;
}
&.is-disabled {
color: $ListMenu-item--onDisabled-color;
background-color: $ListMenu-item--onDisabled-bg;
}
}
&-placeholder {
display: block;
min-height: $ListMenu-item-height;
color: $text--muted-color;
line-height: $Form-input-lineHeight;
font-size: $Form-input-fontSize;
cursor: pointer;
padding: (
$ListMenu-item-height - $Form-input-lineHeight * $Form-input-fontSize
)/2 $Form-select-paddingX;
}
}
.#{$ns}PopOver > .#{$ns}ListMenu {
border-color: $listMenu--onActive-borderColor;
}

View File

@ -31,11 +31,23 @@
&-clear {
@include input-clear();
width: px2rem(26px);
height: px2rem(26px);
margin: 0 px2rem(-2px);
&:hover {
background: $ResultBox-value-bg;
}
> svg {
width: px2rem(12px);
height: px2rem(12px);
}
}
> svg {
display: inline-block;
width: 14px;
width: px2rem(14px);
color: $icon-color;
}
@ -51,8 +63,8 @@
background: $ResultBox-value-bg;
color: $ResultBox-value-color;
font-size: $Form-input-fontSize;
padding: 0 5px;
min-height: 24px;
padding: 0 px2rem(5px);
min-height: px2rem(24px);
flex-wrap: nowrap;
display: inline-flex;
align-items: center;
@ -61,11 +73,16 @@
> a {
cursor: pointer;
margin-left: 5px;
margin-left: px2rem(5px);
color: $ResultBox-icon-color;
&:hover {
color: $ResultBox-icon--onHover-color;
}
> svg {
width: px2rem(10px);
height: px2rem(10px);
}
}
&:hover {

View File

@ -38,6 +38,11 @@
height: $Spinner--lg-height;
}
&--sm {
width: $Spinner--sm-width;
height: $Spinner--sm-height;
}
transition: ease-out all 0.3s;
}

View File

@ -1,112 +1,7 @@
.#{$ns}TagControl {
@include input-text();
position: relative;
&-placeholder {
color: $Form-input-placeholderColor;
user-select: none;
position: absolute;
left: $Form-input-paddingX;
top: $Form-input-paddingY;
margin-top: 2 * $Form-input-borderWidth;
line-height: $Form-input-lineHeight;
}
&-input {
min-height: $Form-input-height;
height: auto;
}
&-valueWrap {
flex-grow: 1;
margin-bottom: -$gap-xs;
line-height: 1;
> input {
width: px2rem(100px);
vertical-align: middle;
position: relative;
z-index: 2;
margin-bottom: $gap-xs;
}
}
&-value {
cursor: pointer;
user-select: none;
white-space: nowrap;
vertical-align: middle;
line-height: $Form-input-lineHeight * $Form-input-fontSize - px2rem(2px);
display: inline-block;
font-size: $Form-selectValue-fontSize;
color: $Form-selectValue-color;
background: $Form-selectValue-bg;
border: px2rem(1px) solid $Form-selectValue-borderColor;
border-radius: 2px;
margin-right: $gap-xs;
margin-bottom: $gap-xs;
&.is-disabled {
pointer-events: none;
opacity: $Button-onDisabled-opacity;
}
}
&-valueIcon {
cursor: pointer;
border-right: px2rem(1px) solid $Form-selectValue-borderColor;
padding: 1px 5px;
&:hover {
background-color: darken($Form-selectValue-bg, 5%);
}
}
&-valueLabel {
padding: 0 $gap-xs;
}
&-sug {
margin-top: $Form-input-marginBottom;
&Tip {
color: $TagControl-sugTip-color;
margin-bottom: $Form-input-marginBottom;
}
&Item {
margin-right: $gap-sm;
margin-bottom: $gap-sm;
display: inline-block;
font-size: $TagControl-sugBtn-fontSize;
cursor: pointer;
user-select: none;
border: $TagControl-sugBtn-borderWidth solid transparent;
@include button-size(
$TagControl-sugBtn-paddingY,
$TagControl-sugBtn-paddingX,
$TagControl-sugBtn-fontSize,
$TagControl-sugBtn-lineHeight,
$TagControl-sugBtn-borderRadius,
$TagControl-sugBtn-height
);
@include button-variant(
$TagControl-sugBtn-bg,
$TagControl-sugBtn-border,
$TagControl-sugBtn-color,
$TagControl-sugBtn-onHover-bg,
$TagControl-sugBtn-onHover-border,
$TagControl-sugBtn-onHover-color,
$TagControl-sugBtn-onActive-bg,
$TagControl-sugBtn-onActive-border,
$TagControl-sugBtn-onActive-color
);
&.is-disabled {
pointer-events: none;
opacity: $Button-onDisabled-opacity;
}
}
> .#{$ns}TagControl-popover {
width: 100%;
}
}

View File

@ -490,6 +490,14 @@ $Card-actions-onChecked-onHover-bg: $white;
// Combo
$Combo--horizontal-dragger-top: px2rem(5px);
// ListMenu
$ListMenu-borderWidth: 0;
$ListMenu-borderRadius: 0;
$ListMenu-item-height: px2rem(30px);
$ListMenu-item-color: #333;
$ListMenu-item--onHover-color: #000;
$ListMenu-item--onHover-bg: #eaf6fe;
@import '../variables';
@import '../mixins';
@import '../base/reset';
@ -541,6 +549,7 @@ $Combo--horizontal-dragger-top: px2rem(5px);
@import '../components/images';
@import '../components/input-box';
@import '../components/result-box';
@import '../components/list-menu';
@import '../components/form/fieldset';
@import '../components/form/group';

View File

@ -204,6 +204,7 @@ pre {
@import '../components/images';
@import '../components/input-box';
@import '../components/result-box';
@import '../components/list-menu';
@import '../components/form/fieldset';
@import '../components/form/group';

View File

@ -69,6 +69,7 @@ $Form-input-borderColor: #cfdadd;
@import '../components/images';
@import '../components/input-box';
@import '../components/result-box';
@import '../components/list-menu';
@import '../components/form/fieldset';
@import '../components/form/group';

View File

@ -51,6 +51,7 @@ class InputInner extends React.Component<InputProps, InputState> {
return (
<input
type="text"
{...rest}
value={this.state.value}
ref={forwardedRef}

View File

@ -1,20 +1,20 @@
import React from 'react';
import {ThemeProps, themeable} from '../theme';
import uncontrollable from 'uncontrollable';
import Input from './Input';
import {autobind} from '../utils/helper';
import {Icon} from './icons';
export interface InputBoxProps
extends ThemeProps,
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
value?: string;
onChange?: (value: string) => void;
onValueChange?: (value: string) => void;
onClear?: (e: React.MouseEvent<any>) => void;
clearable?: boolean;
disabled?: boolean;
hasError?: boolean;
placeholder?: string;
result: JSX.Element;
prefix?: JSX.Element;
children?: JSX.Element;
}
@ -33,15 +33,19 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
};
@autobind
clearValue() {
const onChange = this.props.onChange;
onChange && onChange('');
clearValue(e: any) {
const onClear = this.props.onChange;
const onValueChange = this.props.onValueChange;
onClear && onClear(e);
onValueChange && onValueChange('');
}
@autobind
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const onChange = this.props.onChange;
onChange && onChange(e.currentTarget.value);
const onValueChange = this.props.onValueChange;
onChange && onChange(e);
onValueChange && onValueChange(e.currentTarget.value);
}
@autobind
@ -72,7 +76,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
hasError,
value,
placeholder,
result,
prefix: result,
children,
...rest
} = this.props;
@ -111,8 +115,4 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
}
}
export default themeable(
uncontrollable(InputBox, {
value: 'onChange'
})
);
export default themeable(InputBox);

109
src/components/ListMenu.tsx Normal file
View File

@ -0,0 +1,109 @@
import {ThemeProps, themeable} from '../theme';
import React from 'react';
import {Options, Option} from './Select';
export interface ListMenuProps extends ThemeProps {
options: Options;
disabled?: boolean;
selectedOptions?: Options;
highlightIndex?: number | null;
onSelect?: (e: any, option: Option) => void;
placeholder: string;
itemRender: (option: Option) => JSX.Element;
getItemProps: (props: {item: Option; index: number}) => any;
prefix?: JSX.Element;
}
interface RenderResult {
items: Array<JSX.Element>;
index: number;
}
export class ListMenu extends React.Component<ListMenuProps> {
static defaultProps = {
placeholder: '暂无选项',
itemRender: (option: Option) => <>{option.label}</>,
getItemProps: (props: {item: Option; index: number}) => null
};
renderItem(result: RenderResult, option: Option, optionIndex: number) {
const {
classnames: cx,
itemRender,
disabled,
getItemProps,
highlightIndex,
selectedOptions,
onSelect
} = this.props;
if (Array.isArray(option.children) && option.children.length) {
const stackResult = {
items: [],
index: result.index
};
result.items.push(
<div className={cx('ListMenu-group')} key={optionIndex}>
<div className={cx('ListMenu-groupLabel')}>{itemRender(option)}</div>
{
option.children.reduce(
(result: RenderResult, option, index) =>
this.renderItem(result, option, index),
stackResult
).items
}
</div>
);
result.index = stackResult.index;
return result;
}
const index = result.index++;
result.items.push(
<div
className={cx(
'ListMenu-item',
option.className,
disabled || option.disabled ? 'is-disabled' : '',
index === highlightIndex ? 'is-highlight' : '',
~(selectedOptions || []).indexOf(option) ? 'is-active' : ''
)}
key={index}
onClick={onSelect ? (e: any) => onSelect(e, option) : undefined}
{...getItemProps({
item: option,
index: index
})}
>
<div className={cx('ListMenu-itemLabel')}>{itemRender(option)}</div>
</div>
);
return result;
}
render() {
const {classnames: cx, options, placeholder, prefix, children} = this.props;
return (
<div className={cx('ListMenu')}>
{prefix}
{Array.isArray(options) && options.length ? (
options.reduce(
(result: RenderResult, option: Option, index) =>
this.renderItem(result, option, index),
{
items: [],
index: 0
}
).items
) : (
<span className={cx('ListMenu-placeholder')}>{placeholder}</span>
)}
{children}
</div>
);
}
}
export default themeable(ListMenu);

View File

@ -8,15 +8,12 @@ import {autobind} from '../utils/helper';
export interface ResultBoxProps
extends ThemeProps,
Omit<InputBoxProps, 'value' | 'onChange'> {
value?: Array<any>;
Omit<InputBoxProps, 'result' | 'prefix'> {
result?: Array<any>;
itemRender: (value: any) => JSX.Element;
onChange?: (value: Array<any>) => void;
onResultChange?: (value: Array<any>) => void;
allowInput?: boolean;
inputPlaceholder: string;
inputValue?: string;
onInputChange?: (value: string) => void;
}
export class ResultBox extends React.Component<ResultBoxProps> {
@ -36,14 +33,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
@autobind
clearValue() {
const onChange = this.props.onChange;
onChange && onChange([]);
}
@autobind
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
const onInputChange = this.props.onInputChange;
onInputChange && onInputChange(e.currentTarget.value);
const onResultChange = this.props.onResultChange;
onResultChange && onResultChange([]);
}
@autobind
@ -69,11 +60,11 @@ export class ResultBox extends React.Component<ResultBoxProps> {
e.stopPropagation();
e.preventDefault();
const {value, onChange} = this.props;
const {result, onResultChange} = this.props;
const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10);
const newValue = Array.isArray(value) ? value.concat() : [];
newValue.splice(index, 1);
onChange && onChange(newValue);
const newResult = Array.isArray(result) ? result.concat() : [];
newResult.splice(index, 1);
onResultChange && onResultChange(newResult);
}
render() {
@ -84,15 +75,15 @@ export class ResultBox extends React.Component<ResultBoxProps> {
clearable,
disabled,
hasError,
result,
value,
placeholder,
result,
children,
itemRender,
onInputChange,
inputValue,
allowInput,
inputPlaceholder,
onResultChange,
onChange,
...rest
} = this.props;
const isFocused = this.state.isFocused;
@ -107,8 +98,8 @@ export class ResultBox extends React.Component<ResultBoxProps> {
hasError ? 'is-error' : ''
)}
>
{Array.isArray(value) && value.length ? (
value.map((item, index) => (
{Array.isArray(result) && result.length ? (
result.map((item, index) => (
<div className={cx('ResultBox-value')} key={index}>
<span className={cx('ResultBox-valueLabel')}>
{itemRender(item)}
@ -127,10 +118,10 @@ export class ResultBox extends React.Component<ResultBoxProps> {
{allowInput ? (
<Input
{...rest}
value={inputValue || ''}
onChange={this.handleInputChange}
value={value || ''}
onChange={onChange}
placeholder={
Array.isArray(value) && value.length
Array.isArray(result) && result.length
? inputPlaceholder
: placeholder
}
@ -141,8 +132,13 @@ export class ResultBox extends React.Component<ResultBoxProps> {
<span className={cx('ResultBox-mid')} />
)}
{clearable && !disabled && Array.isArray(value) && value.length ? (
<a onClick={this.clearValue} className={cx('ResultBox-clear')}>
{clearable && !disabled && Array.isArray(result) && result.length ? (
<a
// data-tooltip="清空"
// data-position="bottom"
onClick={this.clearValue}
className={cx('ResultBox-clear')}
>
<Icon icon="close" className="icon" />
</a>
) : null}
@ -156,6 +152,6 @@ export class ResultBox extends React.Component<ResultBoxProps> {
export default themeable(
uncontrollable(ResultBox, {
value: 'onChange',
inputValue: 'onInputChange'
result: 'onResultChange'
})
);

View File

@ -20,14 +20,17 @@ interface SpinnerProps {
overlay: boolean;
spinnerClassName: string;
mode: string;
size: string;
size: 'sm' | 'lg' | '';
classPrefix: string;
classnames: ClassNamesFn;
show: boolean;
}
export class Spinner extends React.Component<SpinnerProps, object> {
static defaultProps = {
static defaultProps: Pick<
SpinnerProps,
'overlay' | 'spinnerClassName' | 'size' | 'mode' | 'show'
> = {
overlay: false,
spinnerClassName: '',
mode: '',

View File

@ -9,6 +9,13 @@ import find from 'lodash/find';
import {Icon} from '../../components/icons';
import {Portal} from 'react-overlays';
import {findDOMNode} from 'react-dom';
import ResultBox from '../../components/ResultBox';
import {autobind, filterTree} from '../../utils/helper';
import Spinner from '../../components/Spinner';
import Overlay from '../../components/Overlay';
import PopOver from '../../components/PopOver';
import ListMenu from '../../components/ListMenu';
import {Options} from '../../components/Select';
// declare function matchSorter(items:Array<any>, input:any, options:any): Array<any>;
@ -22,38 +29,27 @@ export interface TagProps extends OptionsControlProps {
export interface TagState {
inputValue: string;
isFocused?: boolean;
isOpened?: boolean;
}
export default class TagControl extends React.PureComponent<
TagProps,
TagState
> {
input: React.RefObject<HTMLInputElement> = React.createRef();
input: React.RefObject<any> = React.createRef();
constructor(props: TagProps) {
super(props);
this.state = {
inputValue: '',
isFocused: false
};
this.focus = this.focus.bind(this);
this.clearValue = this.clearValue.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.getParent = this.getParent.bind(this);
}
static defaultProps: Partial<TagProps> = {
static defaultProps = {
resetValue: '',
labelField: 'label',
valueField: 'value',
placeholder: '',
multiple: true,
optionsTip: '最近您使用的标签'
placeholder: '暂无标签'
};
state = {
isOpened: false,
inputValue: '',
isFocused: false
};
componentWillReceiveProps(nextProps: TagProps) {
@ -66,54 +62,6 @@ export default class TagControl extends React.PureComponent<
}
}
focus() {
if (!this.input.current) {
return;
}
this.input.current.focus();
// 光标放到最后
const len = this.input.current.value.length;
len && this.input.current.setSelectionRange(len, len);
}
clearValue() {
const {onChange, resetValue} = this.props;
onChange(resetValue);
this.setState(
{
inputValue: resetValue
},
this.focus
);
}
removeItem(index: number) {
const {
selectedOptions,
onChange,
joinValues,
extractValue,
delimiter,
valueField
} = this.props;
const newValue = selectedOptions.concat();
newValue.splice(index, 1);
onChange(
joinValues
? newValue
.map(item => item[valueField || 'value'])
.join(delimiter || ',')
: extractValue
? newValue.map(item => item[valueField || 'value'])
: newValue
);
}
addItem(option: Option) {
const {
selectedOptions,
@ -142,18 +90,17 @@ export default class TagControl extends React.PureComponent<
);
}
handleClick() {
this.focus();
}
@autobind
handleFocus(e: any) {
this.setState({
isFocused: true
isFocused: true,
isOpened: true
});
this.props.onFocus && this.props.onFocus(e);
}
@autobind
handleBlur(e: any) {
const {
selectedOptions,
@ -169,6 +116,7 @@ export default class TagControl extends React.PureComponent<
this.setState(
{
isFocused: false,
isOpened: false,
inputValue: ''
},
value
@ -195,14 +143,50 @@ export default class TagControl extends React.PureComponent<
);
}
handleInputChange(evt: React.ChangeEvent<HTMLInputElement>) {
let value = evt.currentTarget.value;
@autobind
close() {
this.setState({
inputValue: value
isOpened: false
});
}
@autobind
handleInputChange(e: React.ChangeEvent<any>) {
this.setState({
inputValue: e.currentTarget.value
});
}
@autobind
handleChange(value: Array<Option>) {
const {
joinValues,
extractValue,
delimiter,
valueField,
onChange
} = this.props;
let newValue: any = Array.isArray(value) ? value.concat() : [];
if (joinValues || extractValue) {
newValue = value.map(item => item[valueField || 'value']);
}
if (joinValues) {
newValue = newValue.join(delimiter || ',');
}
onChange(newValue);
}
@autobind
renderItem(item: Option) {
const {labelField} = this.props;
return item[labelField || 'label'];
}
@autobind
handleKeyDown(evt: React.KeyboardEvent<HTMLInputElement>) {
const {
selectedOptions,
@ -230,6 +214,7 @@ export default class TagControl extends React.PureComponent<
);
} else if (value && (evt.key === 'Enter' || evt.key === delimiter)) {
evt.preventDefault();
evt.stopPropagation();
const newValue = selectedOptions.concat();
if (!find(newValue, item => item.value == value)) {
@ -255,8 +240,22 @@ export default class TagControl extends React.PureComponent<
}
}
@autobind
handleOptionChange(option: Option) {
if (this.state.inputValue || !option) {
return;
}
this.addItem(option);
}
@autobind
getTarget() {
return this.input.current;
}
@autobind
getParent() {
return (findDOMNode(this) as HTMLElement).parentNode;
return this.input.current && findDOMNode(this.input.current)!.parentElement;
}
reload() {
@ -271,87 +270,85 @@ export default class TagControl extends React.PureComponent<
disabled,
placeholder,
name,
options,
optionsTip,
clearable,
value,
loading,
spinnerClassName,
selectedOptions,
labelField
loading,
popOverContainer,
options
} = this.props;
const finnalOptions = Array.isArray(options)
? filterTree(
options,
item =>
(Array.isArray(item.children) && !!item.children.length) ||
(item.value !== undefined && !~selectedOptions.indexOf(item)),
0,
true
)
: [];
return (
<div
className={cx(className, `TagControl`, {
'is-focused': this.state.isFocused,
'is-disabled': disabled
})}
<Downshift
selectedItem={selectedOptions}
isOpen={this.state.isFocused}
inputValue={this.state.inputValue}
onChange={this.handleOptionChange}
itemToString={this.renderItem}
>
<div onClick={this.handleClick} className={cx('TagControl-input')}>
<div className={cx('TagControl-valueWrap')}>
{placeholder &&
!selectedOptions.length &&
!this.state.inputValue ? (
<div className={cx('TagControl-placeholder')}>{placeholder}</div>
) : null}
{({isOpen, highlightedIndex, getItemProps, getInputProps}) => {
return (
<div className={cx(className, `TagControl`)}>
<ResultBox
{...getInputProps({
name,
ref: this.input,
placeholder: placeholder || '暂无标签',
onChange: this.handleInputChange,
value: this.state.inputValue,
onKeyDown: this.handleKeyDown,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled
})}
result={selectedOptions}
onResultChange={this.handleChange}
itemRender={this.renderItem}
clearable={clearable}
allowInput
>
{loading ? <Spinner size="sm" /> : undefined}
</ResultBox>
{selectedOptions.map((item, index) => (
<div className={cx('TagControl-value')} key={index}>
<span
className={cx('TagControl-valueIcon')}
onClick={this.removeItem.bind(this, index)}
<Overlay
container={popOverContainer || this.getParent}
target={this.getTarget}
placement={'auto'}
show={isOpen && !!finnalOptions.length}
>
<PopOver
overlay
className={cx('TagControl-popover')}
onHide={this.close}
>
×
</span>
<span className={cx('TagControl-valueLabel')}>
{item[labelField || 'label']}
</span>
</div>
))}
<input
ref={this.input}
name={name}
value={this.state.inputValue}
onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
</div>
{clearable && !disabled && value ? (
<a onClick={this.clearValue} className={cx('TagControl-clear')}>
<Icon icon="close" className="icon" />
</a>
) : null}
{loading ? (
<i className={cx(`TagControl-spinner`, spinnerClassName)} />
) : null}
</div>
{options.length ? (
<Portal container={this.getParent}>
<div className={cx('TagControl-sug')}>
{optionsTip ? (
<div className={cx('TagControl-sugTip')}>{optionsTip}</div>
) : null}
{options.map((item, index) => (
<div
className={cx('TagControl-sugItem', {
'is-disabled': item.disabled || disabled
})}
key={index}
onClick={this.addItem.bind(this, item)}
>
{item.label}
</div>
))}
<ListMenu
options={finnalOptions}
itemRender={this.renderItem}
highlightIndex={highlightedIndex}
getItemProps={({item, index}) => ({
...getItemProps({
index,
item,
disabled: item.disabled
})
})}
/>
</PopOver>
</Overlay>
</div>
</Portal>
) : null}
</div>
);
}}
</Downshift>
);
}
}