添加关联勾选模式
This commit is contained in:
parent
da91ffae0f
commit
9906a615d8
|
@ -426,6 +426,110 @@ export default {
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
,
|
||||
{
|
||||
label: '关联选择模式',
|
||||
type: 'transfer',
|
||||
name: 'b',
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
deferApi: '/api/mock2/form/deferOptions?label=${label}',
|
||||
selectMode: 'associated',
|
||||
leftMode: 'tree',
|
||||
leftOptions: [
|
||||
{
|
||||
label: '法师',
|
||||
children: [
|
||||
{
|
||||
label: '诸葛亮',
|
||||
value: 'zhugeliang'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '战士',
|
||||
children: [
|
||||
{
|
||||
label: '曹操',
|
||||
value: 'caocao'
|
||||
},
|
||||
{
|
||||
label: '钟无艳',
|
||||
value: 'zhongwuyan'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '打野',
|
||||
children: [
|
||||
{
|
||||
label: '李白',
|
||||
value: 'libai'
|
||||
},
|
||||
{
|
||||
label: '韩信',
|
||||
value: 'hanxin'
|
||||
},
|
||||
{
|
||||
label: '云中君',
|
||||
value: 'yunzhongjun'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
options: [
|
||||
{
|
||||
ref: 'zhugeliang',
|
||||
children: [
|
||||
{
|
||||
label: 'A',
|
||||
value: 'a'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
ref: 'caocao',
|
||||
children: [
|
||||
{
|
||||
label: 'B',
|
||||
value: 'b'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'C',
|
||||
value: 'c'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
ref: 'zhongwuyan',
|
||||
children: [
|
||||
{
|
||||
label: 'D',
|
||||
value: 'd'
|
||||
},
|
||||
|
||||
{
|
||||
label: 'E',
|
||||
value: 'e'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
ref: 'libai',
|
||||
defer: true
|
||||
},
|
||||
{
|
||||
ref: 'hanxin',
|
||||
defer: true
|
||||
},
|
||||
{
|
||||
ref: 'yunzhongjun',
|
||||
defer: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@
|
|||
}
|
||||
|
||||
&--sm.#{$ns}Checkbox--full {
|
||||
input {
|
||||
input[type='checkbox'] {
|
||||
&:checked + i {
|
||||
&:before {
|
||||
width: $Checkbox--sm--full-inner-size;
|
||||
|
@ -312,7 +312,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.#{$ns}ListCheckboxes {
|
||||
.#{$ns}ListCheckboxes,
|
||||
.#{$ns}ListRadios {
|
||||
&-group:not(:first-child) > &-itemLabel {
|
||||
border-top: px2rem(1px) solid $ListMenu-divider-color;
|
||||
}
|
||||
|
@ -362,6 +363,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.#{$ns}ListRadios {
|
||||
&-item {
|
||||
&.is-active {
|
||||
color: $Form-select-menu-onActive-color;
|
||||
background-color: $Form-select-menu-onActive-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}TableCheckboxes {
|
||||
.#{$ns}Table-content {
|
||||
border-top: $Table-borderWidth solid $Table-borderColor;
|
||||
|
@ -395,7 +405,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.#{$ns}TreeCheckboxes {
|
||||
.#{$ns}TreeCheckboxes,
|
||||
.#{$ns}TreeRadios {
|
||||
.#{$ns}Table-expandBtn {
|
||||
color: $icon-color;
|
||||
margin-right: 5px;
|
||||
|
@ -420,10 +431,9 @@
|
|||
|
||||
&-item {
|
||||
position: relative;
|
||||
|
||||
&.is-expanded > .#{$ns}TreeCheckboxes-sublist {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&-item.is-expanded > &-sublist {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-item:not(:last-child) > &-sublist:before {
|
||||
|
@ -463,6 +473,11 @@
|
|||
background-color: $Tree-item-onHover-bg;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: $Form-select-menu-onActive-color;
|
||||
background-color: $Form-select-menu-onActive-bg;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
pointer-events: none;
|
||||
color: $text--muted-color;
|
||||
|
@ -536,3 +551,23 @@
|
|||
@include checkboxes-placeholder();
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}AssociatedCheckboxes {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&-left {
|
||||
border-right: 1px solid $borderColor;
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
@include checkboxes-placeholder();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,164 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {CheckboxesProps, Checkboxes} from './Checkboxes';
|
||||
import {Options, Option} from './Select';
|
||||
import ListMenu from './ListMenu';
|
||||
import {autobind} from '../utils/helper';
|
||||
import ListRadios from './ListRadios';
|
||||
import {themeable} from '../theme';
|
||||
import uncontrollable from 'uncontrollable';
|
||||
import ListCheckboxes from './ListCheckboxes';
|
||||
import TableCheckboxes from './TableCheckboxes';
|
||||
import TreeCheckboxes from './TreeCheckboxes';
|
||||
import ChainedCheckboxes from './ChainedCheckboxes';
|
||||
import Spinner from './Spinner';
|
||||
import TreeRadios from './TreeRadios';
|
||||
|
||||
export interface AssociatedCheckboxesProps {}
|
||||
export interface AssociatedCheckboxesProps extends CheckboxesProps {
|
||||
leftOptions: Options;
|
||||
leftMode?: 'tree' | 'list';
|
||||
rightMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
columns?: Array<any>;
|
||||
cellRender?: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export class AssociatedCheckboxes extends React.Component<
|
||||
AssociatedCheckboxesProps
|
||||
export interface AssociatedCheckboxesState {
|
||||
leftValue?: Option;
|
||||
}
|
||||
|
||||
export class AssociatedCheckboxes extends Checkboxes<
|
||||
AssociatedCheckboxesProps,
|
||||
AssociatedCheckboxesState
|
||||
> {
|
||||
state: AssociatedCheckboxesState = {};
|
||||
|
||||
@autobind
|
||||
leftOption2Value(option: Option) {
|
||||
return option.value;
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleLeftSelect(value: Option) {
|
||||
const {options, onDeferLoad} = this.props;
|
||||
this.setState({leftValue: value});
|
||||
|
||||
const selectdOption = ListRadios.resolveSelected(
|
||||
value,
|
||||
options,
|
||||
option => option.ref
|
||||
);
|
||||
|
||||
if (selectdOption && onDeferLoad && selectdOption.defer) {
|
||||
onDeferLoad(selectdOption);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>todo</div>;
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
leftOptions,
|
||||
options,
|
||||
option2value,
|
||||
rightMode,
|
||||
onChange,
|
||||
columns,
|
||||
value,
|
||||
leftMode,
|
||||
cellRender
|
||||
} = this.props;
|
||||
|
||||
const selectdOption = ListRadios.resolveSelected(
|
||||
this.state.leftValue,
|
||||
options,
|
||||
option => option.ref
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('AssociatedCheckboxes', className)}>
|
||||
<div className={cx('AssociatedCheckboxes-left')}>
|
||||
{leftMode === 'tree' ? (
|
||||
<TreeRadios
|
||||
option2value={this.leftOption2Value}
|
||||
options={leftOptions}
|
||||
value={this.state.leftValue}
|
||||
onChange={this.handleLeftSelect}
|
||||
showRadio={false}
|
||||
/>
|
||||
) : (
|
||||
<ListRadios
|
||||
option2value={this.leftOption2Value}
|
||||
options={leftOptions}
|
||||
value={this.state.leftValue}
|
||||
onChange={this.handleLeftSelect}
|
||||
showRadio={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={cx('AssociatedCheckboxes-right')}>
|
||||
{this.state.leftValue ? (
|
||||
selectdOption ? (
|
||||
selectdOption.defer && selectdOption.loading ? (
|
||||
<Spinner size="sm" show />
|
||||
) : rightMode === 'table' ? (
|
||||
<TableCheckboxes
|
||||
columns={columns!}
|
||||
value={value}
|
||||
options={selectdOption.children || []}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
cellRender={cellRender}
|
||||
/>
|
||||
) : rightMode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
value={value}
|
||||
options={selectdOption.children || []}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
/>
|
||||
) : rightMode === 'chained' ? (
|
||||
<ChainedCheckboxes
|
||||
value={value}
|
||||
options={selectdOption.children || []}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
/>
|
||||
) : (
|
||||
<ListCheckboxes
|
||||
value={value}
|
||||
options={selectdOption.children || []}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className={cx('AssociatedCheckboxes-placeholder')}>
|
||||
配置错误,选项无法与左侧选项对应
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className={cx('AssociatedCheckboxes-placeholder')}>
|
||||
请先选择左侧数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(
|
||||
uncontrollable(AssociatedCheckboxes, {
|
||||
value: 'onChange'
|
||||
})
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@ export class ListCheckboxes extends Checkboxes {
|
|||
} = this.props;
|
||||
const valueArray = this.valueArray;
|
||||
|
||||
// todo 支持 option.defer 延时加载
|
||||
if (Array.isArray(option.children)) {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import {Checkboxes} from './Checkboxes';
|
||||
import {themeable, ThemeProps} from '../theme';
|
||||
import React from 'react';
|
||||
import uncontrollable from 'uncontrollable';
|
||||
import Checkbox from './Checkbox';
|
||||
import {Option, Options} from './Select';
|
||||
import {findTree, autobind} from '../utils/helper';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
export interface ListRadiosProps extends ThemeProps {
|
||||
options: Options;
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
value?: any;
|
||||
onChange?: (value: any) => void;
|
||||
onDeferLoad?: (option: Option) => void;
|
||||
option2value?: (option: Option) => any;
|
||||
itemClassName?: string;
|
||||
itemRender: (option: Option) => JSX.Element;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
showRadio?: boolean;
|
||||
}
|
||||
|
||||
export class ListRadios<
|
||||
T extends ListRadiosProps = ListRadiosProps,
|
||||
S = any
|
||||
> extends React.Component<T, S> {
|
||||
selected: Option | undefined | null;
|
||||
|
||||
static defaultProps = {
|
||||
placeholder: '暂无选项',
|
||||
itemRender: (option: Option) => <span>{option.label}</span>
|
||||
};
|
||||
static resolveSelected(
|
||||
value: any,
|
||||
options: Options,
|
||||
option2value: (option: Option) => any = (option: Option) => option
|
||||
) {
|
||||
return findTree(options, option => isEqual(option2value(option), value));
|
||||
}
|
||||
|
||||
@autobind
|
||||
toggleOption(option: Option) {
|
||||
const {onChange, clearable, value, options, option2value} = this.props;
|
||||
|
||||
let newValue: Option | null = option;
|
||||
|
||||
if (clearable) {
|
||||
const prevSelected = ListRadios.resolveSelected(
|
||||
value,
|
||||
options,
|
||||
option2value
|
||||
);
|
||||
|
||||
if (prevSelected) {
|
||||
newValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
onChange?.(newValue && option2value ? option2value(newValue) : newValue);
|
||||
}
|
||||
|
||||
renderOption(option: Option, index: number) {
|
||||
const {
|
||||
disabled,
|
||||
classnames: cx,
|
||||
itemClassName,
|
||||
itemRender,
|
||||
showRadio
|
||||
} = this.props;
|
||||
const selected = this.selected;
|
||||
|
||||
if (Array.isArray(option.children)) {
|
||||
return (
|
||||
<div key={index} className={cx('ListRadios-group', option.className)}>
|
||||
<div className={cx('ListRadios-itemLabel')}>{itemRender(option)}</div>
|
||||
|
||||
<div className={cx('ListRadios-items', option.className)}>
|
||||
{option.children.map((child, index) =>
|
||||
this.renderOption(child, index)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cx(
|
||||
'ListRadios-item',
|
||||
itemClassName,
|
||||
option.className,
|
||||
disabled || option.disabled ? 'is-disabled' : '',
|
||||
selected === option ? 'is-active' : ''
|
||||
)}
|
||||
onClick={() => this.toggleOption(option)}
|
||||
>
|
||||
<div className={cx('ListRadios-itemLabel')}>{itemRender(option)}</div>
|
||||
|
||||
{showRadio !== false ? (
|
||||
<Checkbox
|
||||
type="radio"
|
||||
size="sm"
|
||||
checked={selected === option}
|
||||
disabled={disabled || option.disabled}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
options,
|
||||
className,
|
||||
placeholder,
|
||||
classnames: cx,
|
||||
option2value
|
||||
} = this.props;
|
||||
|
||||
this.selected = ListRadios.resolveSelected(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('ListRadios', className)}>
|
||||
{body && body.length ? (
|
||||
body
|
||||
) : (
|
||||
<div className={cx('ListRadios-placeholder')}>{placeholder}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const themedListRadios = themeable(
|
||||
uncontrollable(ListRadios, {
|
||||
value: 'onChange'
|
||||
})
|
||||
);
|
||||
|
||||
themedListRadios.resolveSelected = ListRadios.resolveSelected;
|
||||
|
||||
export default themedListRadios;
|
|
@ -26,7 +26,9 @@ export class ResultBox extends React.Component<ResultBoxProps> {
|
|||
clearable: false,
|
||||
placeholder: '暂无结果',
|
||||
inputPlaceholder: '手动输入内容',
|
||||
itemRender: (option: any) => <span>{String(option.label)}</span>
|
||||
itemRender: (option: any) => (
|
||||
<span>{`${option.scopeLabel || ''}${option.label}`}</span>
|
||||
)
|
||||
};
|
||||
|
||||
state = {
|
||||
|
|
|
@ -24,7 +24,9 @@ export interface ResultListProps extends ThemeProps {
|
|||
export class ResultList extends React.Component<ResultListProps> {
|
||||
static defaultProps: Pick<ResultListProps, 'placeholder' | 'itemRender'> = {
|
||||
placeholder: '请先选择数据',
|
||||
itemRender: (option: Option) => <span>{option.label}</span>
|
||||
itemRender: (option: any) => (
|
||||
<span>{`${option.scopeLabel || ''}${option.label}`}</span>
|
||||
)
|
||||
};
|
||||
|
||||
id = guid();
|
||||
|
|
|
@ -18,7 +18,9 @@ export interface TableCheckboxesProps extends CheckboxesProps {
|
|||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
|
@ -31,7 +33,9 @@ export class TableCheckboxes extends Checkboxes<TableCheckboxesProps> {
|
|||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => <span>{resolveVariable(column.name, option)}</span>
|
||||
};
|
||||
|
||||
|
@ -107,7 +111,9 @@ export class TableCheckboxes extends Checkboxes<TableCheckboxesProps> {
|
|||
<Checkbox size="sm" checked={checked} />
|
||||
</td>
|
||||
{columns.map((column, colIndex) => (
|
||||
<td key={colIndex}>{cellRender(column, option)}</td>
|
||||
<td key={colIndex}>
|
||||
{cellRender(column, option, colIndex, rowIndex)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -9,12 +9,24 @@ import ListCheckboxes from './ListCheckboxes';
|
|||
import {Options, Option} from './Select';
|
||||
import Transfer, {TransferProps} from './Transfer';
|
||||
import {themeable} from '../theme';
|
||||
import AssociatedCheckboxes from './AssociatedCheckboxes';
|
||||
|
||||
export interface TabsTransferProps
|
||||
extends Omit<
|
||||
TransferProps,
|
||||
'selectMode' | 'columns' | 'selectRender' | 'statistics'
|
||||
> {}
|
||||
> {
|
||||
cellRender?: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
}
|
||||
|
||||
export class TabsTransfer extends React.Component<TabsTransferProps> {
|
||||
static defaultProps = {
|
||||
|
@ -31,7 +43,8 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
classnames: cx,
|
||||
value,
|
||||
onChange,
|
||||
option2value
|
||||
option2value,
|
||||
cellRender
|
||||
} = this.props;
|
||||
const options = searchResult || [];
|
||||
const mode = searchResultMode;
|
||||
|
@ -45,6 +58,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
cellRender={cellRender}
|
||||
/>
|
||||
) : mode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
|
@ -86,7 +100,8 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
onChange,
|
||||
onSearch: searchable,
|
||||
option2value,
|
||||
onDeferLoad
|
||||
onDeferLoad,
|
||||
cellRender
|
||||
} = this.props;
|
||||
|
||||
if (!Array.isArray(options) || !options.length) {
|
||||
|
@ -132,6 +147,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
cellRender={cellRender}
|
||||
/>
|
||||
) : option.selectMode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
|
@ -152,6 +168,17 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
|
|||
onDeferLoad={onDeferLoad}
|
||||
defaultSelectedIndex={option.defaultSelectedIndex}
|
||||
/>
|
||||
) : option.selectMode === 'associated' ? (
|
||||
<AssociatedCheckboxes
|
||||
className={cx('Transfer-checkboxes')}
|
||||
options={option.children || []}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
leftMode={option.leftMode}
|
||||
leftOptions={option.leftOptions}
|
||||
/>
|
||||
) : (
|
||||
<ListCheckboxes
|
||||
className={cx('Transfer-checkboxes')}
|
||||
|
|
|
@ -12,6 +12,7 @@ import InputBox from './InputBox';
|
|||
import {Icon} from './icons';
|
||||
import debounce from 'lodash/debounce';
|
||||
import ChainedCheckboxes from './ChainedCheckboxes';
|
||||
import AssociatedCheckboxes from './AssociatedCheckboxes';
|
||||
|
||||
export interface TransferProps extends ThemeProps, CheckboxesProps {
|
||||
inline?: boolean;
|
||||
|
@ -19,12 +20,25 @@ export interface TransferProps extends ThemeProps, CheckboxesProps {
|
|||
showArrow?: boolean;
|
||||
|
||||
selectTitle: string;
|
||||
selectMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated';
|
||||
columns?: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
}>;
|
||||
cellRender?: (
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) => JSX.Element;
|
||||
leftOptions?: Array<Option>;
|
||||
leftMode?: 'tree' | 'list';
|
||||
rightMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
|
||||
// search 相关
|
||||
searchResultMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
|
@ -246,7 +260,8 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
classnames: cx,
|
||||
value,
|
||||
onChange,
|
||||
option2value
|
||||
option2value,
|
||||
cellRender
|
||||
} = this.props;
|
||||
const options = this.state.searchResult || [];
|
||||
const mode = searchResultMode || selectMode;
|
||||
|
@ -260,6 +275,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
cellRender={cellRender}
|
||||
/>
|
||||
) : mode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
|
@ -300,7 +316,11 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
onChange,
|
||||
option2value,
|
||||
classnames: cx,
|
||||
onDeferLoad
|
||||
onDeferLoad,
|
||||
leftOptions,
|
||||
leftMode,
|
||||
rightMode,
|
||||
cellRender
|
||||
} = this.props;
|
||||
|
||||
return selectMode === 'table' ? (
|
||||
|
@ -312,6 +332,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
cellRender={cellRender}
|
||||
/>
|
||||
) : selectMode === 'tree' ? (
|
||||
<TreeCheckboxes
|
||||
|
@ -331,6 +352,19 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
|
|||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
/>
|
||||
) : selectMode === 'associated' ? (
|
||||
<AssociatedCheckboxes
|
||||
className={cx('Transfer-checkboxes')}
|
||||
options={options || []}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
option2value={option2value}
|
||||
onDeferLoad={onDeferLoad}
|
||||
columns={columns}
|
||||
leftOptions={leftOptions || []}
|
||||
leftMode={leftMode}
|
||||
rightMode={rightMode}
|
||||
/>
|
||||
) : (
|
||||
<ListCheckboxes
|
||||
className={cx('Transfer-checkboxes')}
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
import {themeable} from '../theme';
|
||||
import React from 'react';
|
||||
import uncontrollable from 'uncontrollable';
|
||||
import Checkbox from './Checkbox';
|
||||
import {Option} from './Select';
|
||||
import {autobind, eachTree, everyTree} from '../utils/helper';
|
||||
import Spinner from './Spinner';
|
||||
import {ListRadiosProps, ListRadios} from './ListRadios';
|
||||
|
||||
export interface TreeRadiosProps extends ListRadiosProps {
|
||||
expand?: 'all' | 'first' | 'root' | 'none';
|
||||
}
|
||||
|
||||
export interface TreeRadiosState {
|
||||
expanded: Array<string>;
|
||||
}
|
||||
|
||||
export class TreeRadios extends ListRadios<TreeRadiosProps, TreeRadiosState> {
|
||||
state: TreeRadiosState = {
|
||||
expanded: []
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
...ListRadios.defaultProps,
|
||||
expand: 'first' as 'first'
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.syncExpanded();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TreeRadiosProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (
|
||||
!this.state.expanded.length &&
|
||||
(props.expand !== prevProps.expand || props.options !== prevProps.options)
|
||||
) {
|
||||
this.syncExpanded();
|
||||
}
|
||||
}
|
||||
|
||||
syncExpanded() {
|
||||
const options = this.props.options;
|
||||
const mode = this.props.expand;
|
||||
const expanded: Array<string> = [];
|
||||
|
||||
if (!Array.isArray(options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'first' || mode === 'root') {
|
||||
options.every((option, index) => {
|
||||
if (Array.isArray(option.children)) {
|
||||
expanded.push(`${index}`);
|
||||
return mode === 'root';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else if (mode === 'all') {
|
||||
everyTree(options, (option, index, level, paths, indexes) => {
|
||||
if (Array.isArray(option.children)) {
|
||||
expanded.push(`${indexes.concat(index).join('-')}`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({expanded});
|
||||
}
|
||||
|
||||
toggleCollapsed(option: Option, index: string) {
|
||||
const onDeferLoad = this.props.onDeferLoad;
|
||||
const expanded = this.state.expanded.concat();
|
||||
const idx = expanded.indexOf(index);
|
||||
|
||||
if (~idx) {
|
||||
expanded.splice(idx, 1);
|
||||
} else {
|
||||
expanded.push(index);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
expanded: expanded
|
||||
},
|
||||
option.defer && onDeferLoad ? () => onDeferLoad(option) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
renderItem(option: Option, index: number, indexes: Array<number> = []) {
|
||||
const {
|
||||
disabled,
|
||||
classnames: cx,
|
||||
itemClassName,
|
||||
itemRender,
|
||||
showRadio
|
||||
} = this.props;
|
||||
const id = indexes.join('-');
|
||||
let hasChildren = Array.isArray(option.children) && option.children.length;
|
||||
|
||||
const checked = option === this.selected;
|
||||
const expaned = !!~this.state.expanded.indexOf(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cx(
|
||||
'TreeRadios-item',
|
||||
disabled || option.disabled || (option.defer && option.loading)
|
||||
? 'is-disabled'
|
||||
: '',
|
||||
expaned ? 'is-expanded' : '',
|
||||
checked ? 'is-active' : ''
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'TreeRadios-itemInner',
|
||||
itemClassName,
|
||||
option.className,
|
||||
checked ? 'is-active' : ''
|
||||
)}
|
||||
onClick={() => this.toggleOption(option)}
|
||||
>
|
||||
{hasChildren || option.defer ? (
|
||||
<a
|
||||
onClick={(e: React.MouseEvent<any>) => {
|
||||
e.stopPropagation();
|
||||
this.toggleCollapsed(option, id);
|
||||
}}
|
||||
className={cx('Table-expandBtn', expaned ? 'is-active' : '')}
|
||||
>
|
||||
<i />
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
<div className={cx('TreeRadios-itemLabel')}>{itemRender(option)}</div>
|
||||
|
||||
{option.defer && option.loading ? <Spinner show size="sm" /> : null}
|
||||
|
||||
{(!option.defer || option.loaded) &&
|
||||
option.value !== undefined &&
|
||||
showRadio !== false ? (
|
||||
<Checkbox
|
||||
type="radio"
|
||||
size="sm"
|
||||
checked={checked}
|
||||
disabled={disabled || option.disabled}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{hasChildren ? (
|
||||
<div className={cx('TreeRadios-sublist')}>
|
||||
{option.children!.map((option, key) =>
|
||||
this.renderItem(option, key, indexes.concat(key))
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
options,
|
||||
className,
|
||||
placeholder,
|
||||
classnames: cx,
|
||||
option2value
|
||||
} = this.props;
|
||||
|
||||
this.selected = ListRadios.resolveSelected(value, options, option2value);
|
||||
let body: Array<React.ReactNode> = [];
|
||||
|
||||
if (Array.isArray(options) && options.length) {
|
||||
body = options.map((option, key) => this.renderItem(option, key, [key]));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('TreeRadios', className)}>
|
||||
{body && body.length ? (
|
||||
body
|
||||
) : (
|
||||
<div className={cx('TreeRadios-placeholder')}>{placeholder}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default themeable(
|
||||
uncontrollable(TreeRadios, {
|
||||
value: 'onChange'
|
||||
})
|
||||
);
|
|
@ -47,6 +47,8 @@ import TreeCheckboxes from './TreeCheckboxes';
|
|||
import ChainedCheckboxes from './ChainedCheckboxes';
|
||||
import ResultBox from './ResultBox';
|
||||
import InputBox from './InputBox';
|
||||
import ListRadios from './ListRadios';
|
||||
import TreeRadios from './TreeRadios';
|
||||
|
||||
export {
|
||||
NotFound,
|
||||
|
@ -96,5 +98,7 @@ export {
|
|||
TreeCheckboxes,
|
||||
ChainedCheckboxes,
|
||||
ResultBox,
|
||||
InputBox
|
||||
InputBox,
|
||||
ListRadios,
|
||||
TreeRadios
|
||||
};
|
||||
|
|
|
@ -27,7 +27,8 @@ export class TabsTransferRenderer extends TransferRenderer<TabsTransferProps> {
|
|||
loading,
|
||||
searchable,
|
||||
searchResultMode,
|
||||
showArrow
|
||||
showArrow,
|
||||
deferLoad
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -41,6 +42,7 @@ export class TabsTransferRenderer extends TransferRenderer<TabsTransferProps> {
|
|||
searchResultMode={searchResultMode}
|
||||
onSearch={searchable ? this.handleSearch : undefined}
|
||||
showArrow={showArrow}
|
||||
onDeferLoad={deferLoad}
|
||||
/>
|
||||
|
||||
<Spinner overlay key="info" show={loading} />
|
||||
|
|
|
@ -13,11 +13,16 @@ import {Api} from '../../types';
|
|||
import Spinner from '../../components/Spinner';
|
||||
import find from 'lodash/find';
|
||||
import {optionValueCompare} from '../../components/Select';
|
||||
import {resolveVariable} from '../../utils/tpl-builtin';
|
||||
|
||||
export interface TransferProps extends OptionsControlProps {
|
||||
showArrow?: boolean;
|
||||
sortable?: boolean;
|
||||
selectMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated';
|
||||
leftOptions?: Array<Option>;
|
||||
leftMode?: 'tree' | 'list' | 'chained';
|
||||
rightMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
|
||||
searchResultMode?: 'table' | 'list' | 'tree' | 'chained';
|
||||
columns?: Array<any>;
|
||||
searchable?: boolean;
|
||||
|
@ -137,6 +142,31 @@ export class TransferRenderer<
|
|||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderCell(
|
||||
column: {
|
||||
name: string;
|
||||
label: string;
|
||||
[propName: string]: any;
|
||||
},
|
||||
option: Option,
|
||||
colIndex: number,
|
||||
rowIndex: number
|
||||
) {
|
||||
const {render, data} = this.props;
|
||||
return render(
|
||||
`cell/${colIndex}/${rowIndex}`,
|
||||
{
|
||||
type: 'text',
|
||||
...column
|
||||
},
|
||||
{
|
||||
value: resolveVariable(column.name, option),
|
||||
data: createObject(data, option)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
|
@ -150,7 +180,10 @@ export class TransferRenderer<
|
|||
loading,
|
||||
searchable,
|
||||
searchResultMode,
|
||||
deferLoad
|
||||
deferLoad,
|
||||
leftOptions,
|
||||
leftMode,
|
||||
rightMode
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -167,6 +200,10 @@ export class TransferRenderer<
|
|||
columns={columns}
|
||||
onSearch={searchable ? this.handleSearch : undefined}
|
||||
onDeferLoad={deferLoad}
|
||||
leftOptions={leftOptions}
|
||||
leftMode={leftMode}
|
||||
rightMode={rightMode}
|
||||
cellRender={this.renderCell}
|
||||
/>
|
||||
|
||||
<Spinner overlay key="info" show={loading} />
|
||||
|
|
Loading…
Reference in New Issue