feat: 添加 tabs-transfer-picker 组件 (#2972)

This commit is contained in:
liaoxuezhi 2021-11-17 18:03:07 +08:00 committed by GitHub
parent 019dac8040
commit dd8229a938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 751 additions and 537 deletions

View File

@ -0,0 +1,126 @@
---
title: TabsTransferPicker 穿梭选择器
description:
type: 0
group: null
menuName: TabsTransferPicker 穿梭选择器
icon:
---
在[TabsTransfer 组合穿梭器](./tabs-transfer)的基础上扩充了弹窗选择模式,展示值用的是简单的 input 框,但是编辑的操作是弹窗个穿梭框来完成。
适合用来做复杂选人组件。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "组合穿梭器",
"type": "tabs-transfer-picker",
"name": "a",
"sortable": true,
"selectMode": "tree",
"searchable": true,
"pickerSize": "md",
"options": [
{
"label": "成员",
"selectMode": "tree",
"children": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
},
{
"label": "用户",
"selectMode": "chained",
"children": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
]
}
```
## 属性表
更多配置请参考[TabsTransfer 组合穿梭器](./tabs-transfer)。

View File

@ -23,92 +23,41 @@ icon:
"searchable": true,
"options": [
{
"label": "成员",
"selectMode": "tree",
"label": "法师",
"children": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "用户",
"selectMode": "chained",
"label": "战士",
"children": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang2"
}
]
"label": "曹操",
"value": "caocao"
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao2"
},
{
"label": "钟无艳",
"value": "zhongwuyan2"
}
]
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai2"
},
{
"label": "韩信",
"value": "hanxin2"
},
{
"label": "云中君",
"value": "yunzhongjun2"
}
]
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}

View File

@ -34,6 +34,7 @@ order: 60
}
}
```
## 配置工具栏
```schema: scope="body"
@ -106,7 +107,7 @@ order: 60
## 隐藏头部
去掉头部默认只展示内容tab第一项的内容
去掉头部,默认只展示内容 tab 第一项的内容
```schema: scope="body"
{
@ -121,9 +122,9 @@ order: 60
}
```
## 设置style
## 设置 style
默认tabs只有一项的时候没有选中状态
默认 tabs 只有一项的时候没有选中状态
```schema: scope="body"
{
@ -140,8 +141,6 @@ order: 60
}
```
## 去掉分隔线
```schema: scope="body"
@ -157,9 +156,9 @@ order: 60
}
```
## source动态数据
## source 动态数据
配置 source 属性,根据某个数据来动态生成。具体使用参考Tabs选项卡组件
配置 source 属性,根据某个数据来动态生成。具体使用参考 Tabs 选项卡组件
## 图标
@ -215,26 +214,26 @@ order: 60
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------------------- | --------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ |
| type | `string` | `"portlet"` | 指定为 Portlet 渲染器 |
| className | `string` | | 外层 Dom 的类名 |
| tabsClassName | `string` | | Tabs Dom 的类名 |
| contentClassName | `string` | | Tabs content Dom 的类名 |
| tabs | `Array` | | tabs 内容 |
| source | `Object` | | tabs 关联数据,关联后可以重复生成选项卡 |
| toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏不随tab切换而变化 |
| style | `string \| Object` | | 自定义样式|
| description | [模板](../../docs/concepts/template)| | 标题右侧信息 |
| hideHeader | `boolean` | false | 隐藏头部 |
| divider | `boolean` | false | 去掉分隔线 |
| tabs[x].title | `string` | | Tab 标题 |
| tabs[x].icon | `icon` | | Tab 的图标 |
| tabs[x].tab | [SchemaNode](../types/schemanode) | | 内容区 |
| tabs[x].toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏随tab切换而变化
| tabs[x].reload | `boolean` | | 设置以后内容每次都会重新渲染,对于 crud 的重新拉取很有用 |
| tabs[x].unmountOnExit | `boolean` | | 每次退出都会销毁当前 tab 栏内容 |
| tabs[x].className | `string` | `"bg-white b-l b-r b-b wrapper-md"` | Tab 区域样式 |
| mountOnEnter | `boolean` | false | 只有在点中 tab 的时候才渲染 |
| unmountOnExit | `boolean` | false | 切换 tab 的时候销毁 |
| scrollable | `boolean` | false | 是否导航支持内容溢出滚动,`vertical`和`chrome`模式下不支持该属性;`chrome`模式默认压缩标签 |
| 属性名 | 类型 | 默认值 | 说明 |
| --------------------- | ------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------ |
| type | `string` | `"portlet"` | 指定为 Portlet 渲染器 |
| className | `string` | | 外层 Dom 的类名 |
| tabsClassName | `string` | | Tabs Dom 的类名 |
| contentClassName | `string` | | Tabs content Dom 的类名 |
| tabs | `Array` | | tabs 内容 |
| source | `Object` | | tabs 关联数据,关联后可以重复生成选项卡 |
| toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏,不随 tab 切换而变化 |
| style | `string \| Object` | | 自定义样式 |
| description | [模板](../../docs/concepts/template) | | 标题右侧信息 |
| hideHeader | `boolean` | false | 隐藏头部 |
| divider | `boolean` | false | 去掉分隔线 |
| tabs[x].title | `string` | | Tab 标题 |
| tabs[x].icon | `icon` | | Tab 的图标 |
| tabs[x].tab | [SchemaNode](../types/schemanode) | | 内容区 |
| tabs[x].toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏,随 tab 切换而变化 |
| tabs[x].reload | `boolean` | | 设置以后内容每次都会重新渲染,对于 crud 的重新拉取很有用 |
| tabs[x].unmountOnExit | `boolean` | | 每次退出都会销毁当前 tab 栏内容 |
| tabs[x].className | `string` | `"bg-white b-l b-r b-b wrapper-md"` | Tab 区域样式 |
| mountOnEnter | `boolean` | false | 只有在点中 tab 的时候才渲染 |
| unmountOnExit | `boolean` | false | 切换 tab 的时候销毁 |
| scrollable | `boolean` | false | 是否导航支持内容溢出滚动,`vertical`和`chrome`模式下不支持该属性;`chrome`模式默认压缩标签 |

View File

@ -658,6 +658,14 @@ export const components = [
makeMarkdownRenderer
)
},
{
label: 'TransferPicker 穿梭选择器',
path: '/zh-CN/components/form/transfer-picker',
getComponent: () =>
import('../../docs/zh-CN/components/form/transfer-picker.md').then(
makeMarkdownRenderer
)
},
{
label: 'TabsTransfer 组合穿梭器',
path: '/zh-CN/components/form/tabs-transfer',
@ -667,12 +675,12 @@ export const components = [
)
},
{
label: 'TransferPicker 穿梭选择器',
path: '/zh-CN/components/form/transfer-picker',
label: 'TabsTransferPicker 组合穿梭选择器',
path: '/zh-CN/components/form/tabs-transfer-picker',
getComponent: () =>
import('../../docs/zh-CN/components/form/transfer-picker.md').then(
makeMarkdownRenderer
)
import(
'../../docs/zh-CN/components/form/tabs-transfer-picker.md'
).then(makeMarkdownRenderer)
},
{
label: 'InputTree 树形选择框',

View File

@ -111,6 +111,7 @@ import {TreeSelectControlSchema} from './renderers/Form/TreeSelect';
import {UUIDControlSchema} from './renderers/Form/UUID';
import {FormControlSchema} from './renderers/Form/Control';
import {TransferPickerControlSchema} from './renderers/Form/TransferPicker';
import {TabsTransferPickerControlSchema} from './renderers/Form/TabsTransferPicker';
// 每加个类型,这补充一下。
export type SchemaType =
@ -311,6 +312,7 @@ export type SchemaType =
| 'textarea'
| 'transfer'
| 'transfer-picker'
| 'tabs-transfer-picker'
| 'input-tree'
| 'tree-select'
| 'table-view'
@ -435,6 +437,7 @@ export type SchemaObject =
| TextareaControlSchema
| TransferControlSchema
| TransferPickerControlSchema
| TabsTransferPickerControlSchema
| TreeControlSchema
| TreeSelectControlSchema;

View File

@ -0,0 +1,85 @@
import {localeable} from '../locale';
import {themeable} from '../theme';
import {uncontrollable} from 'uncontrollable';
import React from 'react';
import ResultBox from './ResultBox';
import {Icon} from './icons';
import PickerContainer from './PickerContainer';
import {autobind} from '../utils/helper';
import TabsTransfer, {TabsTransferProps} from './TabsTransfer';
export interface TabsTransferPickerProps
extends Omit<TabsTransferProps, 'itemRender'> {
// 新的属性?
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export class TransferPicker extends React.Component<TabsTransferPickerProps> {
@autobind
handleClose() {
this.setState({
inputValue: '',
searchResult: null
});
}
@autobind
handleConfirm(value: any) {
this.props.onChange?.(value);
this.handleClose();
}
render() {
const {
classnames: cx,
value,
translate: __,
disabled,
className,
onChange,
size,
...rest
} = this.props;
return (
<PickerContainer
title={__('Select.placeholder')}
popOverRender={({onClose, value, onChange}) => {
return <TabsTransfer {...rest} value={value} onChange={onChange} />;
}}
value={value}
onConfirm={this.handleConfirm}
onCancel={this.handleClose}
size={size}
>
{({onClick, isOpened}) => (
<ResultBox
className={cx(
'TransferPicker',
className,
isOpened ? 'is-active' : ''
)}
allowInput={false}
result={value}
onResultChange={onChange}
onResultClick={onClick}
placeholder={__('Select.placeholder')}
disabled={disabled}
>
<span className={cx('TransferPicker-icon')}>
<Icon icon="pencil" className="icon" />
</span>
</ResultBox>
)}
</PickerContainer>
);
}
}
export default themeable(
localeable(
uncontrollable(TransferPicker, {
value: 'onChange'
})
)
);

View File

@ -1,46 +1,19 @@
import {localeable} from '../locale';
import {themeable} from '../theme';
import {Transfer, TransferProps} from './Transfer';
import Transfer, {TransferProps} from './Transfer';
import {uncontrollable} from 'uncontrollable';
import React from 'react';
import ResultBox from './ResultBox';
import {Icon} from './icons';
import PickerContainer from './PickerContainer';
import InputBox from './InputBox';
import {BaseSelection} from './Selection';
import {autobind, flattenTree} from '../utils/helper';
import ResultList from './ResultList';
import {Options} from './Select';
import {autobind} from '../utils/helper';
export interface TransferPickerProps extends TransferProps {
export interface TransferPickerProps extends Omit<TransferProps, 'itemRender'> {
// 新的属性?
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export class TransferPicker extends Transfer<TransferPickerProps> {
handlePickerToggleAll(value: any, onChange: (value: any) => void) {
const {options, option2value} = this.props;
let valueArray = BaseSelection.value2array(value, options, option2value);
const availableOptions = flattenTree(options).filter(
(option, index, list) =>
!option.disabled &&
option.value !== void 0 &&
list.indexOf(option) === index
);
if (valueArray.length < availableOptions.length) {
valueArray = availableOptions;
} else {
valueArray = [];
}
let newValue: string | Options = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(newValue);
}
export class TransferPicker extends React.Component<TransferPickerProps> {
@autobind
handleClose() {
this.setState({
@ -63,92 +36,15 @@ export class TransferPicker extends Transfer<TransferPickerProps> {
disabled,
className,
onChange,
onSearch,
options,
option2value,
inline,
showArrow,
resultTitle,
statistics,
sortable,
resultItemRender,
size
size,
...rest
} = this.props;
return (
<PickerContainer
title={__('Select.placeholder')}
popOverRender={({onClose, value, onChange}) => {
this.valueArray = BaseSelection.value2array(
value,
options,
option2value
);
this.availableOptions = flattenTree(options).filter(
(option, index, list) =>
!option.disabled &&
option.value !== void 0 &&
list.indexOf(option) === index
);
return (
<>
<div
className={cx(
'Transfer',
className,
inline ? 'Transfer--inline' : ''
)}
>
<div className={cx('Transfer-select')}>
{this.renderSelect({
...this.props,
value,
onChange,
onToggleAll: () =>
this.handlePickerToggleAll(value, onChange)
})}
</div>
<div className={cx('Transfer-mid')}>
{showArrow /*todo 需要改成确认模式,即:点了按钮才到右边 */ ? (
<div className={cx('Transfer-arrow')}>
<Icon icon="right-arrow" className="icon" />
</div>
) : null}
</div>
<div className={cx('Transfer-result')}>
<div className={cx('Transfer-title')}>
<span>
{__(resultTitle || 'Transfer.selectd')}
{statistics !== false ? (
<span>
{this.valueArray.length}/
{this.availableOptions.length}
</span>
) : null}
</span>
<a
onClick={this.clearAll}
className={cx(
'Transfer-clearAll',
disabled || !this.valueArray.length ? 'is-disabled' : ''
)}
>
{__('clear')}
</a>
</div>
<ResultList
className={cx('Transfer-selections')}
sortable={sortable}
disabled={disabled}
value={value}
onChange={onChange}
placeholder={__('Transfer.selectFromLeft')}
itemRender={resultItemRender}
/>
</div>
</div>
</>
);
return <Transfer {...rest} value={value} onChange={onChange} />;
}}
value={value}
onConfirm={this.handleConfirm}

View File

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

View File

@ -0,0 +1,123 @@
import {
OptionsControlProps,
OptionsControl,
FormOptionsControl
} from './Options';
import React from 'react';
import {Api} from '../../types';
import Spinner from '../../components/Spinner';
import {BaseTransferRenderer} from './Transfer';
import TabsTransfer from '../../components/TabsTransfer';
import {SchemaApi} from '../../Schema';
import TransferPicker from '../../components/TransferPicker';
import TabsTransferPicker from '../../components/TabsTransferPicker';
/**
* TabsTransferPicker 穿
* https://baidu.gitee.io/amis/docs/components/form/tabs-transfer-picker
*/
export interface TabsTransferPickerControlSchema extends FormOptionsControl {
type: 'tabs-transfer-picker';
/**
*
*/
showArrow?: boolean;
/**
*
*/
sortable?: boolean;
/**
*
*/
searchResultMode?: 'table' | 'list' | 'tree' | 'chained';
/**
*
*/
searchable?: boolean;
/**
* API
*/
searchApi?: SchemaApi;
/**
*
*/
selectTitle?: string;
/**
*
*/
resultTitle?: string;
/**
*
*/
pickerSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export interface TabsTransferProps
extends OptionsControlProps,
Omit<
TabsTransferPickerControlSchema,
| 'type'
| 'options'
| 'inputClassName'
| 'className'
| 'descriptionClassName'
> {}
@OptionsControl({
type: 'tabs-transfer-picker'
})
export class TabsTransferPickerRenderer extends BaseTransferRenderer<TabsTransferProps> {
render() {
const {
className,
classnames: cx,
options,
selectedOptions,
sortable,
loading,
searchable,
searchResultMode,
showArrow,
deferLoad,
disabled,
selectTitle,
resultTitle,
pickerSize,
columns,
leftMode,
leftOptions
} = this.props;
return (
<div className={cx('TabsTransferControl', className)}>
<TabsTransferPicker
value={selectedOptions}
disabled={disabled}
options={options}
onChange={this.handleChange}
option2value={this.option2value}
sortable={sortable}
searchResultMode={searchResultMode}
onSearch={searchable ? this.handleSearch : undefined}
showArrow={showArrow}
onDeferLoad={deferLoad}
selectTitle={selectTitle}
resultTitle={resultTitle}
size={pickerSize}
leftMode={leftMode}
leftOptions={leftOptions}
/>
<Spinner overlay key="info" show={loading} />
</div>
);
}
}

View File

@ -89,7 +89,10 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
disabled,
selectTitle,
resultTitle,
pickerSize
pickerSize,
columns,
leftMode,
leftOptions
} = this.props;
return (
@ -108,6 +111,9 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
selectTitle={selectTitle}
resultTitle={resultTitle}
size={pickerSize}
columns={columns}
leftMode={leftMode}
leftOptions={leftOptions}
/>
<Spinner overlay key="info" show={loading} />

View File

@ -6,15 +6,21 @@ import {Renderer, RendererProps} from '../factory';
import {resolveVariable} from '../utils/tpl-builtin';
import {str2AsyncFunction} from '../utils/api';
import {
isVisible,
autobind,
isDisabled,
isObject,
createObject
isVisible,
autobind,
isDisabled,
isObject,
createObject
} from '../utils/helper';
import {filter} from '../utils/tpl';
import {SchemaTpl, SchemaClassName, BaseSchema, SchemaCollection, SchemaIcon} from '../Schema';
import {
SchemaTpl,
SchemaClassName,
BaseSchema,
SchemaCollection,
SchemaIcon
} from '../Schema';
import {ActionSchema} from './Action';
@ -23,314 +29,298 @@ import {ActionSchema} from './Action';
* https://baidu.gitee.io/amis/docs/components/portlet
*/
export interface PortletTabSchema extends Omit<BaseSchema, 'type'> {
/**
* Tab
*/
title?: string;
/**
* Tab
*/
title?: string;
/**
*
* @deprecated body
*/
tab?: SchemaCollection;
/**
*
* @deprecated body
*/
tab?: SchemaCollection;
/**
* tab切换而切换
*/
toolbar?: Array<ActionSchema>;
/**
* tab切换而切换
*/
toolbar?: Array<ActionSchema>;
/**
*
*/
body?: SchemaCollection;
/**
*
*/
body?: SchemaCollection;
/**
*
*/
icon?: SchemaIcon;
/**
*
*/
icon?: SchemaIcon;
iconPosition?: 'left' | 'right';
iconPosition?: 'left' | 'right';
/**
*
*/
reload?: boolean;
/**
*
*/
reload?: boolean;
/**
*
*/
mountOnEnter?: boolean;
/**
*
*/
mountOnEnter?: boolean;
/**
*
*/
unmountOnExit?: boolean;
/**
*
*/
unmountOnExit?: boolean;
}
export interface PortletSchema extends Omit<BaseSchema, 'type'> {
/**
* portlet
*/
type: 'portlet';
/**
* portlet
*/
type: 'portlet';
tabs: Array<PortletTabSchema>;
tabs: Array<PortletTabSchema>;
/**
*
*/
source?: string;
/**
*
*/
source?: string;
/**
*
*/
tabsClassName?: SchemaClassName;
/**
*
*/
tabsClassName?: SchemaClassName;
/**
*
*/
tabsMode?: '' | 'line' | 'card' | 'radio' | 'vertical' | 'tiled';
/**
*
*/
tabsMode?: '' | 'line' | 'card' | 'radio' | 'vertical' | 'tiled';
/**
*
*/
contentClassName?: SchemaClassName;
/**
*
*/
contentClassName?: SchemaClassName;
/**
*
*/
linksClassName?: SchemaClassName;
/**
*
*/
linksClassName?: SchemaClassName;
/**
*
*/
mountOnEnter?: boolean;
/**
*
*/
mountOnEnter?: boolean;
/**
*
*/
unmountOnExit?: boolean;
/**
*
*/
unmountOnExit?: boolean;
/**
* tab切换
*/
toolbar?: Array<ActionSchema>;
/**
* tab切换
*/
toolbar?: Array<ActionSchema>;
/**
*
*/
scrollable?: boolean;
/**
*
*/
scrollable?: boolean;
/**
* header和内容是否展示分割线
*/
divider?: boolean;
/**
* header和内容是否展示分割线
*/
divider?: boolean;
/**
*
*/
description?: SchemaTpl;
/**
*
*/
description?: SchemaTpl;
/**
*
*/
hideHeader?: boolean;
/**
*
*/
hideHeader?: boolean;
/**
*
*/
style?: string | {
/**
*
*/
style?:
| string
| {
[propName: string]: any;
};
};
}
export interface PortletProps
extends RendererProps,
Omit<PortletSchema, 'className' | 'contentClassName'>{
activeKey?: number;
tabRender?: (tab: PortletTabSchema, props: PortletProps, index: number) => JSX.Element;
extends RendererProps,
Omit<PortletSchema, 'className' | 'contentClassName'> {
activeKey?: number;
tabRender?: (
tab: PortletTabSchema,
props: PortletProps,
index: number
) => JSX.Element;
}
export interface PortletState {
activeKey?: number;
activeKey?: number;
}
export class Portlet extends React.Component<PortletProps, PortletState> {
static defaultProps: Partial<PortletProps> = {
className: '',
mode: 'line',
divider: true
static defaultProps: Partial<PortletProps> = {
className: '',
mode: 'line',
divider: true
};
renderTab?: (
tab: PortletTabSchema,
props: PortletProps,
index: number
) => JSX.Element;
constructor(props: PortletProps) {
super(props);
const activeKey = props.activeKey || 0;
this.state = {
activeKey
};
renderTab?: (tab: PortletTabSchema, props: PortletProps, index: number) => JSX.Element;
constructor(props: PortletProps) {
super(props);
}
const activeKey = props.activeKey || 0;
this.state = {
activeKey
};
@autobind
handleSelect(key: number) {
const {onSelect, tabs} = this.props;
if (typeof key === 'number' && key < tabs.length) {
this.setState({
activeKey: key
});
}
@autobind
handleSelect(key: number) {
const {onSelect, tabs} = this.props;
if (typeof key === 'number' && key < tabs.length) {
this.setState({
activeKey: key
});
}
if (typeof onSelect === 'string') {
const selectFunc = str2AsyncFunction(onSelect, 'key', 'props');
selectFunc && selectFunc(key, this.props);
} else if (typeof onSelect === 'function') {
onSelect(key, this.props);
}
}
if (typeof onSelect === 'string') {
const selectFunc = str2AsyncFunction(onSelect, 'key', 'props');
selectFunc && selectFunc(key, this.props);
} else if (typeof onSelect === 'function') {
onSelect(key, this.props);
}
renderToolbarItem(toolbar: Array<ActionSchema>) {
const {render} = this.props;
let actions: Array<JSX.Element> = [];
if (Array.isArray(toolbar)) {
toolbar.forEach((action, index) =>
actions.push(
render(
`toolbar/${index}`,
{
type: 'button',
level: 'link',
size: 'sm',
...(action as any)
},
{
key: index
}
)
)
);
}
return actions;
}
renderToolbar() {
const {toolbar, classnames: cx, classPrefix: ns, tabs} = this.props;
const activeKey = this.state.activeKey;
let tabToolbar = null;
let tabToolbarTpl = null;
// tabs里的toolbar
const toolbarTpl = toolbar ? (
<div className={cx(`${ns}toolbar`)}>
{this.renderToolbarItem(toolbar)}
</div>
) : null;
// tab里的toolbar
if (typeof activeKey !== 'undefined') {
tabToolbar = tabs[activeKey]!.toolbar;
tabToolbarTpl = tabToolbar ? (
<div className={cx(`${ns}tab-toolbar`)}>
{this.renderToolbarItem(tabToolbar)}
</div>
) : null;
}
renderToolbarItem(toolbar: Array<ActionSchema>) {
const {render} = this.props;
let actions: Array<JSX.Element> = []
if (Array.isArray(toolbar)) {
toolbar.forEach((action, index) =>
actions.push(
render(
`toolbar/${index}`,
{
type: 'button',
level: 'link',
size: 'sm',
...(action as any)
},
{
key: index
}
)
)
);
}
return actions;
return toolbarTpl || tabToolbarTpl ? (
<div className={cx(`${ns}Portlet-toolbar`)}>
{toolbarTpl}
{tabToolbarTpl}
</div>
) : null;
}
renderDesc() {
const {
description: descTpl,
render,
classnames: cx,
classPrefix: ns,
data
} = this.props;
const desc = filter(descTpl, data);
return desc ? (
<span className={cx(`${ns}Portlet-header-desc`)}>{desc}</span>
) : null;
}
renderTabs() {
const {
classnames: cx,
classPrefix: ns,
tabsClassName,
contentClassName,
linksClassName,
tabRender,
render,
data,
mode: dMode,
tabsMode,
unmountOnExit,
source,
mountOnEnter,
scrollable,
divider
} = this.props;
const mode = tabsMode || dMode;
const arr = resolveVariable(source, data);
let tabs = this.props.tabs;
if (!tabs) {
return null;
}
renderToolbar() {
const {toolbar, classnames: cx, classPrefix: ns, tabs} = this.props;
const activeKey = this.state.activeKey;
let tabToolbar = null;
let tabToolbarTpl = null;
// tabs里的toolbar
const toolbarTpl = toolbar ? (
<div className={cx(`${ns}toolbar`)}>
{this.renderToolbarItem(toolbar)}
</div>
) : null;
tabs = Array.isArray(tabs) ? tabs : [tabs];
let children: Array<JSX.Element | null> = [];
// tab里的toolbar
if (typeof activeKey !== 'undefined') {
tabToolbar = tabs[activeKey]!.toolbar;
tabToolbarTpl = tabToolbar ? (
<div className={cx(`${ns}tab-toolbar`)}>
{this.renderToolbarItem(tabToolbar)}
</div>
) : null;
}
return (
toolbarTpl || tabToolbarTpl
? (<div className={cx(`${ns}Portlet-toolbar`)}>
{toolbarTpl}
{tabToolbarTpl}
</div>)
: null
const tabClassname = cx(`${ns}Portlet-tab`, tabsClassName, {
['unactive-select']: tabs.length <= 1,
['no-divider']: !divider
});
if (Array.isArray(arr)) {
arr.forEach((value, index) => {
const ctx = createObject(
data,
isObject(value) ? {index, ...value} : {item: value, index}
);
}
renderDesc() {
const {description : descTpl, render, classnames: cx, classPrefix: ns, data} = this.props;
const desc = filter(descTpl, data);
return desc
? <span className={cx(`${ns}Portlet-header-desc`)}>{desc}</span>
: null;
}
renderTabs() {
const {
classnames: cx,
classPrefix: ns,
tabsClassName,
contentClassName,
linksClassName,
tabRender,
render,
data,
mode: dMode,
tabsMode,
unmountOnExit,
source,
mountOnEnter,
scrollable,
divider
} = this.props;
const mode = tabsMode || dMode;
const arr = resolveVariable(source, data);
let tabs = this.props.tabs;
if (!tabs) {
return null;
}
tabs = Array.isArray(tabs) ? tabs : [tabs];
let children: Array<JSX.Element | null> = [];
const tabClassname = cx(`${ns}Portlet-tab`, tabsClassName, {
['unactive-select']: tabs.length <=1,
['no-divider']: !divider
});
if (Array.isArray(arr)) {
arr.forEach((value, index) => {
const ctx = createObject(
data,
isObject(value) ? {index, ...value} : {item: value, index}
);
children.push(
...tabs.map((tab, tabIndex) =>
isVisible(tab, ctx) ? (
<Tab
{...(tab as any)}
title={filter(tab.title, ctx)}
disabled={isDisabled(tab, ctx)}
key={`${index * 1000 + tabIndex}`}
eventKey={index * 1000 + tabIndex}
mountOnEnter={mountOnEnter}
unmountOnExit={
typeof tab.reload === 'boolean'
? tab.reload
: typeof tab.unmountOnExit === 'boolean'
? tab.unmountOnExit
: unmountOnExit
}
>
{render(
`item/${index}/${tabIndex}`,
(tab as any)?.type ? (tab as any) : tab.tab || tab.body,
{
data: ctx
}
)}
</Tab>
) : null
)
);
});
} else {
children = tabs.map((tab, index) =>
isVisible(tab, data) ? (
children.push(
...tabs.map((tab, tabIndex) =>
isVisible(tab, ctx) ? (
<Tab
{...(tab as any)}
title={filter(tab.title, data)}
disabled={isDisabled(tab, data)}
key={index}
eventKey={index}
title={filter(tab.title, ctx)}
disabled={isDisabled(tab, ctx)}
key={`${index * 1000 + tabIndex}`}
eventKey={index * 1000 + tabIndex}
mountOnEnter={mountOnEnter}
unmountOnExit={
typeof tab.reload === 'boolean'
@ -340,66 +330,94 @@ export class Portlet extends React.Component<PortletProps, PortletState> {
: unmountOnExit
}
>
{this.renderTab
? this.renderTab(tab, this.props, index)
: tabRender
? tabRender(tab, this.props, index)
: render(
`tab/${index}`,
(tab as any)?.type ? (tab as any) : tab.tab || tab.body
)}
{render(
`item/${index}/${tabIndex}`,
(tab as any)?.type ? (tab as any) : tab.tab || tab.body,
{
data: ctx
}
)}
</Tab>
) : null
);
}
return (
<CTabs
classPrefix={ns}
classnames={cx}
mode={mode}
className={tabClassname}
contentClassName={contentClassName}
linksClassName={linksClassName}
activeKey={this.state.activeKey}
onSelect={this.handleSelect}
toolbar={this.renderToolbar()}
additionBtns={this.renderDesc()}
scrollable={scrollable}
>
{children}
</CTabs>
)
);
});
} else {
children = tabs.map((tab, index) =>
isVisible(tab, data) ? (
<Tab
{...(tab as any)}
title={filter(tab.title, data)}
disabled={isDisabled(tab, data)}
key={index}
eventKey={index}
mountOnEnter={mountOnEnter}
unmountOnExit={
typeof tab.reload === 'boolean'
? tab.reload
: typeof tab.unmountOnExit === 'boolean'
? tab.unmountOnExit
: unmountOnExit
}
>
{this.renderTab
? this.renderTab(tab, this.props, index)
: tabRender
? tabRender(tab, this.props, index)
: render(
`tab/${index}`,
(tab as any)?.type ? (tab as any) : tab.tab || tab.body
)}
</Tab>
) : null
);
}
render() {
const {
className,
data,
classnames: cx,
classPrefix: ns,
style,
hideHeader
} = this.props;
const portletClassname = cx(`${ns}Portlet`, className, {
['no-header']: hideHeader
});
const styleVar =
typeof style === 'string'
? resolveVariable(style, data) || {}
: mapValues(style, s => resolveVariable(s, data) || s);
return (
<CTabs
classPrefix={ns}
classnames={cx}
mode={mode}
className={tabClassname}
contentClassName={contentClassName}
linksClassName={linksClassName}
activeKey={this.state.activeKey}
onSelect={this.handleSelect}
toolbar={this.renderToolbar()}
additionBtns={this.renderDesc()}
scrollable={scrollable}
>
{children}
</CTabs>
);
}
return (
<div className={portletClassname} style={styleVar}>
{this.renderTabs()}
</div>
)
}
render() {
const {
className,
data,
classnames: cx,
classPrefix: ns,
style,
hideHeader
} = this.props;
const portletClassname = cx(`${ns}Portlet`, className, {
['no-header']: hideHeader
});
const styleVar =
typeof style === 'string'
? resolveVariable(style, data) || {}
: mapValues(style, s => resolveVariable(s, data) || s);
return (
<div className={portletClassname} style={styleVar}>
{this.renderTabs()}
</div>
);
}
}
@Renderer({
type: 'portlet'
})
export class PortletRenderer extends Portlet {
}
export class PortletRenderer extends Portlet {}