mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
重构 tag 渲染器
This commit is contained in:
parent
6452ee38a7
commit
f4f10b0d5e
@ -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'
|
||||
|
@ -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 />;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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;
|
||||
|
63
scss/components/_list-menu.scss
Normal file
63
scss/components/_list-menu.scss
Normal 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;
|
||||
}
|
@ -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 {
|
||||
|
@ -38,6 +38,11 @@
|
||||
height: $Spinner--lg-height;
|
||||
}
|
||||
|
||||
&--sm {
|
||||
width: $Spinner--sm-width;
|
||||
height: $Spinner--sm-height;
|
||||
}
|
||||
|
||||
transition: ease-out all 0.3s;
|
||||
}
|
||||
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -51,6 +51,7 @@ class InputInner extends React.Component<InputProps, InputState> {
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
{...rest}
|
||||
value={this.state.value}
|
||||
ref={forwardedRef}
|
||||
|
@ -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
109
src/components/ListMenu.tsx
Normal 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);
|
@ -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'
|
||||
})
|
||||
);
|
||||
|
@ -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: '',
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user