新增 TabsTransfer 渲染器

This commit is contained in:
2betop 2020-05-26 20:28:09 +08:00
parent 4b6c04b411
commit 9576641503
23 changed files with 826 additions and 119 deletions

View File

@ -17,6 +17,127 @@ export default {
label: 'Email',
type: 'email',
name: 'email'
},
{
label: 'Transfer',
type: 'transfer',
name: 'transfer',
sortable: true,
selectMode: 'nested',
searchable: true,
options: [
{
label: '诸葛亮',
value: 'zhugeliang'
},
{
label: '曹操',
value: 'caocao'
},
{
label: '钟无艳',
value: 'zhongwuyan'
},
{
label: '野核',
children: [
{
label: '李白',
value: 'libai'
},
{
label: '韩信',
value: 'hanxin'
},
{
label: '云中君',
value: 'yunzhongjun'
}
]
}
]
},
{
label: 'Transfer',
type: 'tabs-transfer',
name: 'transfer2',
sortable: true,
selectMode: 'tree',
searchable: true,
options: [
{
label: '成员',
selectMode: 'tree',
children: [
{
label: '诸葛亮',
value: 'zhugeliang'
},
{
label: '曹操',
value: 'caocao'
},
{
label: '钟无艳',
value: 'zhongwuyan'
},
{
label: '野核',
children: [
{
label: '李白',
value: 'libai'
},
{
label: '韩信',
value: 'hanxin'
},
{
label: '云中君',
value: 'yunzhongjun'
}
]
}
]
},
{
label: '用户',
children: [
{
label: '诸葛亮',
value: 'zhugeliang2'
},
{
label: '曹操',
value: 'caocao2'
},
{
label: '钟无艳',
value: 'zhongwuyan2'
},
{
label: '野核',
children: [
{
label: '李白',
value: 'libai2'
},
{
label: '韩信',
value: 'hanxin2'
},
{
label: '云中君',
value: 'yunzhongjun2'
}
]
}
]
}
]
}
]
}

View File

@ -0,0 +1,58 @@
.#{$ns}SearchBox {
display: inline-flex;
flex-direction: row;
line-height: $Form-input-lineHeight;
font-size: $Form-input-fontSize;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
height: 30px;
width: 30px;
padding: 0 8px;
transition: all 0.3s ease-in-out;
border: $Form-input-borderWidth solid transparent;
border-radius: $Form-input-borderRadius;
&:hover {
background-color: rgba($Form-input-bg, 0.6);
}
&.is-active {
background-color: $Form-input-bg;
border: $Form-input-borderWidth solid $Form-input-borderColor;
width: 150px;
> input {
flex-grow: 1;
}
}
&-activeBtn,
&-cancelBtn {
cursor: pointer;
color: $icon-color;
&:hover {
color: $icon-onHover-color;
}
}
> input {
outline: none;
border: none;
background: transparent;
color: $Form-input-color;
width: 0;
height: $Form-input-lineHeight * $Form-input-fontSize;
&::placeholder {
color: $Form-input-placeholderColor;
user-select: none;
}
}
height: 30px;
}

View File

@ -348,7 +348,7 @@
@for $i from 2 through 10 {
tr.#{$ns}Table-tr--#{$i}th.is-expanded {
.#{$ns}Table-expandCell:before {
right: px2rem(9px) + px2rem(-20px) * ($i - 1);
right: px2rem(7px) + px2rem(-20px) * ($i - 1);
}
}
@ -367,7 +367,7 @@
width: px2rem(1px);
top: 0;
bottom: 0;
left: px2rem(-10px) + px2rem(20px) * ($i - 2);
left: px2rem(-8px) + px2rem(20px) * ($i - 2);
height: auto;
background-color: $Table-tree-borderColor;
}
@ -376,8 +376,8 @@
content: '';
position: absolute;
height: px2rem(1px);
top: px2rem(20px);
left: px2rem(-10px) + px2rem(20px) * ($i - 2);
top: px2rem(18px);
left: px2rem(-8px) + px2rem(20px) * ($i - 2);
width: px2rem(10px);
background-color: $Table-tree-borderColor;
}
@ -395,7 +395,7 @@
tr.#{$ns}Table-tr--#{$i}th.is-last:not(.is-expanded) {
.#{$ns}Table-expandCell + td {
&::before {
height: px2rem(20px);
height: px2rem(18px);
bottom: auto;
}
}
@ -437,7 +437,7 @@
bottom: 0;
height: 100%;
background-color: $Table-tree-borderColor;
right: px2rem(9px) + px2rem(-20px) * ($i - 1);
right: px2rem(7px) + px2rem(-20px) * ($i - 1);
}
}
}
@ -449,9 +449,9 @@
content: '';
position: absolute;
width: px2rem(1px);
top: px2rem(30px);
top: px2rem(28px);
bottom: 0;
right: px2rem(9px);
right: px2rem(7px);
height: auto;
background-color: $Table-tree-borderColor;
}
@ -682,30 +682,33 @@
position: relative;
z-index: 1;
color: $Table-expandBtn-color;
display: inline-flex;
justify-content: center;
align-items: center;
width: px2rem(14px);
line-height: 1;
height: 16px;
> i {
display: inline-block;
width: px2rem(16px);
text-align: center;
cursor: pointer;
font-style: normal;
transition: transform ease-in-out 0.2s, top ease-in-out 0.2s;
position: relative;
left: 1px;
transform-origin: 50% 50%;
&:before {
display: inline-block;
line-height: 1;
font-size: $Table-expandBtn-fontSize;
font-family: $Table-expandBtn-vendor;
content: $Table-expandBtn-icon;
transition: transform ease-in-out 0.2s, top ease-in-out 0.2s;
position: relative;
top: -2px;
}
}
&.is-active > i::before {
&.is-active > i {
transform: rotate(90deg);
transform-origin: 50% 50%;
top: 0;
}
}

View File

@ -313,6 +313,10 @@
}
.#{$ns}ListCheckboxes {
&-group:not(:first-child) > &-itemLabel {
border-top: px2rem(1px) solid $ListMenu-divider-color;
}
&-group > &-itemLabel {
font-size: $fontSizeSm;
padding: $gap-xs $gap-xs;
@ -417,11 +421,16 @@
&-item {
position: relative;
&.is-collapsed > .#{$ns}TreeCheckboxes-sublist {
display: none;
}
}
&-item:not(:last-child) > &-sublist:before {
bottom: 0;
}
&-sublist &-item:before {
height: 1px;
content: '';
@ -449,6 +458,10 @@
cursor: pointer;
user-select: none;
&:hover {
background-color: $Tree-item-onHover-bg;
}
&.is-disabled {
pointer-events: none;
color: $text--muted-color;
@ -458,4 +471,14 @@
&-itemLabel {
flex-grow: 1;
}
&-placeholder {
height: $Form-input-height;
line-height: $Form-input-lineHeight;
font-size: $Form-input-fontSize;
padding: (
$Form-input-height - $Form-input-lineHeight * $Form-input-fontSize
)/2 $gap-sm;
color: $text--muted-color;
}
}

View File

@ -23,6 +23,7 @@
display: flex;
flex-direction: column;
justify-content: center;
font-size: $Form-input-fontSize;
}
&-items {

View File

@ -11,6 +11,7 @@
&-title {
display: flex;
align-items: center;
background: $Table-thead-bg;
height: px2rem(30px);
line-height: $Form-input-lineHeight;
@ -30,16 +31,16 @@
&-select,
&-result {
width: 0;
min-width: px2rem(200px);
flex-basis: px2rem(200px);
flex-grow: 1;
border: $Form-input-borderWidth solid $Form-input-borderColor;
display: flex;
flex-direction: column;
}
&-checkboxes,
&-selections {
&-select > &-checkboxes,
&-result > &-selections {
flex-grow: 1;
height: 0;
overflow: auto;
@ -71,6 +72,59 @@
color: $text--muted-color;
}
}
&-tabs {
display: flex;
flex-direction: column;
height: 100%;
> .#{$ns}Tabs-links {
padding: 5px 0 0 5px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
> .#{$ns}Tabs-link > a:first-child {
font-size: 12px;
padding: 7px 8px;
}
.#{$ns}TabsTransfer-tabsMid {
flex-grow: 1;
}
> .#{$ns}SearchBox {
margin: 0 5px 0 10px;
padding: 0 5px;
width: 25px;
&.is-active {
width: 150px;
margin-right: 10px;
padding-left: 10px;
}
}
}
> .#{$ns}Tabs-content {
flex-grow: 1;
height: 0;
overflow: auto;
position: relative;
padding: 5px 0;
}
}
}
.#{$ns}TabsTransfer {
.#{$ns}Transfer-title {
height: 40px;
}
// .#{$ns}Transfer-result {
// flex-grow: unset;
// }
}
.#{$ns}TransferControl {

View File

@ -549,6 +549,7 @@ $ListMenu-item--onHover-bg: #eaf6fe;
@import '../components/images';
@import '../components/input-box';
@import '../components/result-box';
@import '../components/search-box';
@import '../components/list-menu';
@import '../components/form/fieldset';

View File

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

View File

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

View File

@ -6,9 +6,9 @@ import {Icon} from './icons';
export interface InputBoxProps
extends ThemeProps,
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix' | 'onChange'> {
value?: string;
onValueChange?: (value: string) => void;
onChange?: (value: string) => void;
onClear?: (e: React.MouseEvent<any>) => void;
clearable?: boolean;
disabled?: boolean;
@ -35,17 +35,15 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
@autobind
clearValue(e: any) {
const onClear = this.props.onChange;
const onValueChange = this.props.onValueChange;
onClear && onClear(e);
onValueChange && onValueChange('');
const onChange = this.props.onChange;
onClear?.(e);
onChange?.('');
}
@autobind
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const onChange = this.props.onChange;
const onValueChange = this.props.onValueChange;
onChange && onChange(e);
onValueChange && onValueChange(e.currentTarget.value);
onChange && onChange(e.currentTarget.value);
}
@autobind

View File

@ -0,0 +1,98 @@
import {Checkboxes} from './Checkboxes';
import {themeable} from '../theme';
import React from 'react';
import uncontrollable from 'uncontrollable';
import Checkbox from './Checkbox';
import {Option} from './Select';
export class NestedCheckboxes extends Checkboxes {
valueArray: Array<Option>;
renderOption(option: Option, index: number) {
const {
labelClassName,
disabled,
classnames: cx,
itemClassName,
itemRender
} = this.props;
const valueArray = this.valueArray;
if (Array.isArray(option.children)) {
return (
<div
key={index}
className={cx('ListCheckboxes-group', option.className)}
>
<div className={cx('ListCheckboxes-itemLabel')}>
{itemRender(option)}
</div>
<div className={cx('ListCheckboxes-items', option.className)}>
{option.children.map((child, index) =>
this.renderOption(child, index)
)}
</div>
</div>
);
}
return (
<div
key={index}
className={cx(
'ListCheckboxes-item',
itemClassName,
option.className,
disabled || option.disabled ? 'is-disabled' : ''
)}
onClick={() => this.toggleOption(option)}
>
<div className={cx('ListCheckboxes-itemLabel')}>
{itemRender(option)}
</div>
<Checkbox
checked={!!~valueArray.indexOf(option)}
disabled={disabled || option.disabled}
labelClassName={labelClassName}
description={option.description}
/>
</div>
);
}
render() {
const {
value,
options,
className,
placeholder,
classnames: cx,
option2value
} = this.props;
this.valueArray = Checkboxes.value2array(value, options, option2value);
let body: Array<React.ReactNode> = [];
if (Array.isArray(options) && options.length) {
body = options.map((option, key) => this.renderOption(option, key));
}
return (
<div className={cx('ListCheckboxes', className)}>
{body && body.length ? (
body
) : (
<div className={cx('ListCheckboxes-placeholder')}>{placeholder}</div>
)}
</div>
);
}
}
export default themeable(
uncontrollable(NestedCheckboxes, {
value: 'onChange'
})
);

View File

@ -8,7 +8,9 @@ import {autobind} from '../utils/helper';
export interface ResultBoxProps
extends ThemeProps,
Omit<InputBoxProps, 'result' | 'prefix'> {
Omit<InputBoxProps, 'result' | 'prefix' | 'onChange'> {
onChange?: (value: string) => void;
onResultClick?: (e: React.MouseEvent<HTMLElement>) => void;
result?: Array<any>;
itemRender: (value: any) => JSX.Element;
onResultChange?: (value: Array<any>) => void;
@ -67,6 +69,12 @@ export class ResultBox extends React.Component<ResultBoxProps> {
onResultChange && onResultChange(newResult);
}
@autobind
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const {onChange} = this.props;
onChange?.(e.currentTarget.value);
}
render() {
const {
className,
@ -84,6 +92,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
inputPlaceholder,
onResultChange,
onChange,
onResultClick,
...rest
} = this.props;
const isFocused = this.state.isFocused;
@ -97,6 +106,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
disabled ? 'is-disabled' : '',
hasError ? 'is-error' : ''
)}
onClick={onResultClick}
>
{Array.isArray(result) && result.length ? (
result.map((item, index) => (
@ -119,7 +129,7 @@ export class ResultBox extends React.Component<ResultBoxProps> {
<Input
{...rest}
value={value || ''}
onChange={onChange}
onChange={this.handleChange}
placeholder={
Array.isArray(result) && result.length
? inputPlaceholder

View File

@ -0,0 +1,82 @@
import React from 'react';
import {ThemeProps, themeable} from '../theme';
import {Icon} from './icons';
import uncontrollable from 'uncontrollable';
import {autobind} from '../utils/helper';
export interface SearchBoxProps extends ThemeProps {
name?: string;
onChange?: (text: string) => void;
placeholder?: string;
value?: string;
active?: boolean;
onActiveChange?: (active: boolean) => void;
onSearch?: (value: string) => void;
onCancel?: () => void;
}
export class SearchBox extends React.Component<SearchBoxProps> {
inputRef: React.RefObject<HTMLInputElement> = React.createRef();
@autobind
handleActive() {
const {onActiveChange} = this.props;
onActiveChange?.(true);
this.inputRef.current?.focus();
}
@autobind
handleCancel() {
const {onActiveChange, onChange, onCancel} = this.props;
onActiveChange?.(false);
onCancel?.();
onChange?.('');
}
@autobind
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const {onChange, onSearch} = this.props;
onChange?.(e.currentTarget.value);
onSearch?.(e.currentTarget.value);
}
render() {
const {
classnames: cx,
value,
active,
name,
onChange,
placeholder
} = this.props;
return (
<div className={cx('SearchBox', active ? 'is-active' : '')}>
<input
name={name}
onChange={this.handleChange}
value={value || ''}
placeholder={placeholder || '输入关键字'}
ref={this.inputRef}
/>
{active ? (
<a className={cx('SearchBox-cancelBtn')} onClick={this.handleCancel}>
<Icon icon="close" className="icon" />
</a>
) : (
<a className={cx('SearchBox-activeBtn')} onClick={this.handleActive}>
<Icon icon="search" className="icon" />
</a>
)}
</div>
);
}
}
export default themeable(
uncontrollable(SearchBox, {
active: 'onActiveChange',
value: 'onChange'
})
);

View File

@ -40,6 +40,7 @@ export interface OptionProps {
multi?: boolean;
multiple?: boolean;
valueField?: string;
labelField?: string;
simpleValue?: boolean; // 默认onChange 出去是整个 option 节点,如果配置了 simpleValue 就只包含值。
options: Options;
joinValues?: boolean;

View File

@ -7,7 +7,7 @@
import React from 'react';
import {Schema} from '../types';
import Transition, {ENTERED, ENTERING} from 'react-transition-group/Transition';
import {ClassNamesFn, themeable, ThemeProps} from '../theme';
import {themeable, ThemeProps} from '../theme';
import uncontrollable from 'uncontrollable';
const transitionStyles: {
@ -31,6 +31,54 @@ export interface TabProps extends ThemeProps {
toolbar?: React.ReactNode;
}
class TabComponent extends React.PureComponent<TabProps> {
contentDom: any;
contentRef = (ref: any) => (this.contentDom = ref);
render() {
const {
classnames: cx,
mountOnEnter,
reload,
unmountOnExit,
eventKey,
activeKey,
children,
className
} = this.props;
return (
<Transition
in={activeKey === eventKey}
mountOnEnter={mountOnEnter}
unmountOnExit={typeof reload === 'boolean' ? reload : unmountOnExit}
timeout={500}
>
{(status: string) => {
if (status === ENTERING) {
this.contentDom.offsetWidth;
}
return (
<div
ref={this.contentRef}
className={cx(
transitionStyles[status],
activeKey === eventKey ? 'is-active' : '',
'Tabs-pane',
className
)}
>
{children}
</div>
);
}}
</Transition>
);
}
}
export const Tab = themeable(TabComponent);
export interface TabsProps extends ThemeProps {
mode: '' | 'line' | 'card' | 'radio' | 'vertical';
tabsMode?: '' | 'line' | 'card' | 'radio' | 'vertical';
@ -50,6 +98,8 @@ export class Tabs extends React.Component<TabsProps> {
contentClassName: ''
};
static Tab = Tab;
handleSelect(key: string | number) {
const {onSelect} = this.props;
onSelect && onSelect(key);
@ -110,8 +160,7 @@ export class Tabs extends React.Component<TabsProps> {
tabsMode,
children,
additionBtns,
toolbar,
activeKey: activeKeyProps
toolbar
} = this.props;
if (!Array.isArray(children)) {
@ -146,56 +195,12 @@ export class Tabs extends React.Component<TabsProps> {
}
}
class TabComponent extends React.PureComponent<TabProps> {
contentDom: any;
contentRef = (ref: any) => (this.contentDom = ref);
render() {
const {
classnames: cx,
mountOnEnter,
reload,
unmountOnExit,
eventKey,
activeKey,
children,
className
} = this.props;
return (
<Transition
in={activeKey === eventKey}
mountOnEnter={mountOnEnter}
unmountOnExit={typeof reload === 'boolean' ? reload : unmountOnExit}
timeout={500}
>
{(status: string) => {
if (status === ENTERING) {
this.contentDom.offsetWidth;
}
return (
<div
ref={this.contentRef}
className={cx(
transitionStyles[status],
activeKey === eventKey ? 'is-active' : '',
'Tabs-pane',
className
)}
>
{children}
</div>
);
}}
</Transition>
);
}
}
export const Tab = themeable(TabComponent);
export default themeable(
const ThemedTabs = themeable(
uncontrollable(Tabs, {
activeKey: 'onSelect'
})
);
export default ThemedTabs as typeof ThemedTabs & {
Tab: typeof Tab;
};

View File

@ -11,12 +11,14 @@ import {autobind, flattenTree} from '../utils/helper';
import InputBox from './InputBox';
import {Icon} from './icons';
import debounce from 'lodash/debounce';
import NestedCheckboxes from './NestedCheckboxes';
export interface TransferPorps extends ThemeProps, CheckboxesProps {
inline?: boolean;
statistics?: boolean;
selectTitle: string;
selectMode?: 'table' | 'list' | 'tree';
selectMode?: 'table' | 'list' | 'tree' | 'nested';
columns?: Array<{
name: string;
label: string;
@ -24,7 +26,7 @@ export interface TransferPorps extends ThemeProps, CheckboxesProps {
}>;
// search 相关
searchResultMode?: 'table' | 'list' | 'tree';
searchResultMode?: 'table' | 'list' | 'tree' | 'nested';
searchResultColumns?: Array<{
name: string;
label: string;
@ -38,7 +40,13 @@ export interface TransferPorps extends ThemeProps, CheckboxesProps {
) => Promise<Options | void>;
// 自定义选择框相关
selectRender?: (props: TransferPorps) => JSX.Element;
selectRender?: (
props: Omit<TransferPorps, 'onSearch'> & {
onSearch: (text: string) => void;
onSearchCancel: () => void;
searchResult: Options | null;
}
) => JSX.Element;
resultTitle: string;
sortable?: boolean;
@ -99,8 +107,7 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
}
@autobind
handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const text = e.currentTarget.value;
handleSearch(text: string) {
this.setState(
{
inputValue: text
@ -158,11 +165,17 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
selectTitle,
onSearch,
disabled,
options
options,
statistics
} = this.props;
if (selectRender) {
return selectRender(this.props);
return selectRender({
...this.props,
onSearch: this.handleSearch,
onSearchCancel: this.handleSeachCancel,
searchResult: this.state.searchResult
});
}
return (
@ -174,8 +187,12 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
)}
>
<span>
{selectTitle}{this.valueArray.length}/
{this.availableOptions.length}
{selectTitle}
{statistics !== false ? (
<span>
{this.valueArray.length}/{this.availableOptions.length}
</span>
) : null}
</span>
{selectMode !== 'table' ? (
<a
@ -249,6 +266,15 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
onChange={onChange}
option2value={option2value}
/>
) : mode === 'nested' ? (
<NestedCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : (
<ListCheckboxes
placeholder={noResultsText}
@ -276,7 +302,7 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
<TableCheckboxes
className={cx('Transfer-checkboxes')}
columns={columns!}
options={options}
options={options || []}
value={value}
onChange={onChange}
option2value={option2value}
@ -284,7 +310,15 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
) : selectMode === 'tree' ? (
<TreeCheckboxes
className={cx('Transfer-checkboxes')}
options={options}
options={options || []}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : selectMode === 'nested' ? (
<NestedCheckboxes
className={cx('Transfer-checkboxes')}
options={options || []}
value={value}
onChange={onChange}
option2value={option2value}
@ -292,7 +326,7 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
) : (
<ListCheckboxes
className={cx('Transfer-checkboxes')}
options={options}
options={options || []}
value={value}
onChange={onChange}
option2value={option2value}
@ -311,7 +345,8 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
sortable,
options,
option2value,
disabled
disabled,
statistics
} = this.props;
this.valueArray = Checkboxes.value2array(value, options, option2value);
@ -328,8 +363,12 @@ export class Transfer extends React.Component<TransferPorps, TransferState> {
<div className={cx('Transfer-result')}>
<div className={cx('Transfer-title')}>
<span>
{resultTitle}{this.valueArray.length}/
{this.availableOptions.length}
{resultTitle}
{statistics !== false ? (
<span>
{this.valueArray.length}/{this.availableOptions.length}
</span>
) : null}
</span>
<a
onClick={this.clearAll}

View File

@ -200,7 +200,11 @@ export class TreeCheckboxes extends Checkboxes<
return (
<div className={cx('TreeCheckboxes', className)}>
{body && body.length ? body : <div>{placeholder}</div>}
{body && body.length ? (
body
) : (
<div className={cx('TreeCheckboxes-placeholder')}>{placeholder}</div>
)}
</div>
);
}

View File

@ -43,6 +43,8 @@ import Transfer from './Transfer';
import ListCheckboxes from './ListCheckboxes';
import TableCheckboxes from './TableCheckboxes';
import TreeCheckboxes from './TreeCheckboxes';
import ResultBox from './ResultBox';
import InputBox from './InputBox';
export {
NotFound,
@ -88,5 +90,7 @@ export {
Transfer,
ListCheckboxes,
TableCheckboxes,
TreeCheckboxes
TreeCheckboxes,
ResultBox,
InputBox
};

View File

@ -94,6 +94,7 @@ import './renderers/Form/IconPicker';
import './renderers/Form/Formula';
import './renderers/Form/FieldSet';
import './renderers/Form/Tabs';
import './renderers/Form/TabsTransfer';
import './renderers/Form/Group';
import './renderers/Form/InputGroup';
import './renderers/Grid';

View File

@ -0,0 +1,198 @@
import {OptionsControlProps, OptionsControl} from './Options';
import React from 'react';
import Transfer from '../../components/Transfer';
import {Api} from '../../types';
import Spinner from '../../components/Spinner';
import {TransferRenderer} from './Transfer';
import {autobind} from '../../utils/helper';
import Tabs, {Tab} from '../../components/Tabs';
import {Icon} from '../../components/icons';
import TableCheckboxes from '../../components/TableCheckboxes';
import TreeCheckboxes from '../../components/TreeCheckboxes';
import ListCheckboxes from '../../components/ListCheckboxes';
import SearchBox from '../../components/SearchBox';
import {Options} from '../../components/Select';
import NestedCheckboxes from '../../components/NestedCheckboxes';
export interface TabsTransferProps extends OptionsControlProps {
sortable?: boolean;
searchable?: boolean;
searchApi?: Api;
searchResultMode?: 'table' | 'list' | 'tree' | 'nested';
}
@OptionsControl({
type: 'tabs-transfer'
})
export class TabsTransferRenderer extends TransferRenderer<TabsTransferProps> {
renderSearchResult(searchResult: Options | null) {
const {
searchResultMode,
selectMode,
noResultsText,
searchResultColumns,
classnames: cx,
value,
onChange,
option2value
} = this.props;
const options = searchResult || [];
const mode = searchResultMode || selectMode;
return mode === 'table' ? (
<TableCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
columns={searchResultColumns!}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : mode === 'tree' ? (
<TreeCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : mode === 'nested' ? (
<NestedCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
) : (
<ListCheckboxes
placeholder={noResultsText}
className={cx('Transfer-checkboxes')}
options={options}
value={value}
onChange={onChange}
option2value={option2value}
/>
);
}
@autobind
renderSelect({onSearch, onSearchCancel, searchResult}: any) {
const {
options,
placeholder,
classnames: cx,
labelField,
selectedOptions,
searchable
} = this.props;
if (!Array.isArray(options) || !options.length) {
return <div>{placeholder || '暂无可选项'}</div>;
}
return (
<Tabs
mode="card"
className={cx('Transfer-tabs')}
activeKey={searchResult !== null ? 0 : undefined}
toolbar={
searchable ? (
<>
<span className={cx('TabsTransfer-tabsMid')}></span>
<SearchBox onSearch={onSearch} onCancel={onSearchCancel} />
</>
) : null
}
>
{searchResult !== null
? [
<Tab title="搜索结果" key={0} eventKey={0}>
{this.renderSearchResult(searchResult)}
</Tab>
]
: options.map((option, index) => (
<Tab
eventKey={index}
key={index}
title={option[labelField || 'label'] || option.title}
>
{option.selectMode === 'table' ? (
<TableCheckboxes
className={cx('Transfer-checkboxes')}
columns={option.columns as any}
options={option.children || []}
value={selectedOptions}
onChange={this.handleChange}
option2value={this.option2value}
/>
) : option.selectMode === 'tree' ? (
<TreeCheckboxes
className={cx('Transfer-checkboxes')}
options={option.children || []}
value={selectedOptions}
onChange={this.handleChange}
option2value={this.option2value}
/>
) : option.selectMode === 'nested' ? (
<NestedCheckboxes
className={cx('Transfer-checkboxes')}
options={option.children || []}
value={selectedOptions}
onChange={this.handleChange}
option2value={this.option2value}
/>
) : (
<ListCheckboxes
className={cx('Transfer-checkboxes')}
options={option.children || []}
value={selectedOptions}
onChange={this.handleChange}
option2value={this.option2value}
/>
)}
</Tab>
))}
</Tabs>
);
}
render() {
const {
className,
classnames: cx,
options,
selectedOptions,
sortable,
selectMode,
columns,
loading,
searchable,
searchResultMode
} = this.props;
return (
<div className={cx('TabsTransferControl', className)}>
<Transfer
statistics={false}
className="TabsTransfer"
selectRender={this.renderSelect}
value={selectedOptions}
options={options}
onChange={this.handleChange}
option2value={this.option2value}
sortable={sortable}
selectMode={selectMode}
searchResultMode={searchResultMode}
columns={columns}
onSearch={searchable ? this.handleSearch : undefined}
/>
<Spinner overlay key="info" show={loading} />
</div>
);
}
}

View File

@ -146,9 +146,9 @@ export default class TagControl extends React.PureComponent<
}
@autobind
handleInputChange(e: React.ChangeEvent<any>) {
handleInputChange(text: string) {
this.setState({
inputValue: e.currentTarget.value
inputValue: text
});
}
@ -301,13 +301,13 @@ export default class TagControl extends React.PureComponent<
name,
ref: this.input,
placeholder: placeholder || '暂无标签',
onChange: this.handleInputChange,
value: this.state.inputValue,
onKeyDown: this.handleKeyDown,
onFocus: this.handleFocus,
onBlur: this.handleBlur,
disabled
})}
onChange={this.handleInputChange}
className={cx('TagControl-input')}
result={selectedOptions}
onResultChange={this.handleChange}

View File

@ -6,7 +6,8 @@ import {
autobind,
filterTree,
string2regExp,
createObject
createObject,
findTree
} from '../../utils/helper';
import {Api} from '../../types';
import Spinner from '../../components/Spinner';
@ -22,10 +23,9 @@ export interface TransferProps extends OptionsControlProps {
searchApi?: Api;
}
@OptionsControl({
type: 'transfer'
})
export class TransferRenderer extends React.Component<TransferProps> {
export class TransferRenderer<
T extends OptionsControlProps = TransferProps
> extends React.Component<T> {
@autobind
handleChange(value: Array<Option>) {
const {
@ -43,11 +43,11 @@ export class TransferRenderer extends React.Component<TransferProps> {
if (Array.isArray(value)) {
if (joinValues || extractValue) {
newValue = value.map(item => {
const resolved = find(
const resolved = findTree(
options,
optionValueCompare(
item[valueField || 'value'],
valueField || 'value'
item[(valueField as string) || 'value'],
(valueField as string) || 'value'
)
);
@ -55,7 +55,7 @@ export class TransferRenderer extends React.Component<TransferProps> {
newOptions.push(item);
}
return item[valueField || 'value'];
return item[(valueField as string) || 'value'];
});
}
@ -124,8 +124,8 @@ export class TransferRenderer extends React.Component<TransferProps> {
(option: Option) => {
return !!(
(Array.isArray(option.children) && option.children.length) ||
regexp.test(option[labelField || 'label']) ||
regexp.test(option[valueField || 'value'])
regexp.test(option[(labelField as string) || 'label']) ||
regexp.test(option[(valueField as string) || 'value'])
);
},
0,
@ -169,3 +169,7 @@ export class TransferRenderer extends React.Component<TransferProps> {
);
}
}
export default OptionsControl({
type: 'transfer'
})(TransferRenderer);

View File

@ -127,8 +127,9 @@ export function themeable<
};
class EnhancedComponent extends React.Component<Props> {
static displayName = `Themeable(${ComposedComponent.displayName ||
ComposedComponent.name})`;
static displayName = `Themeable(${
ComposedComponent.displayName || ComposedComponent.name
})`;
static contextType = ThemeContext;
static ComposedComponent = ComposedComponent;
@ -147,7 +148,7 @@ export function themeable<
<ThemeContext.Provider value={theme}>
<ComposedComponent
{
...this.props as any /* todo, 解决这个类型问题 */
...(this.props as any) /* todo, 解决这个类型问题 */
}
{...injectedProps}
/>
@ -156,10 +157,9 @@ export function themeable<
}
}
return hoistNonReactStatic(
EnhancedComponent,
ComposedComponent
) as React.ComponentClass<Props> & {
const result = hoistNonReactStatic(EnhancedComponent, ComposedComponent);
return result as typeof result & {
ComposedComponent: T;
};
}