feat:transfer组件功能完善 (#3696)

* feat:transfer组件功能完善

* update-snapshot

* feat:transfer组件功能完善

* update-snapshot

* feat:transfer组件功能完善

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* update-snapshot

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* feat:transfer组件云舍4.0功能

* update-snapshot

Co-authored-by: sqzhou <zhoushengqiang01@baidu.com>
This commit is contained in:
zhou999 2022-05-12 10:23:04 +08:00 committed by GitHub
parent 8829f6346f
commit e22e2f80ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2116 additions and 729 deletions

View File

@ -959,40 +959,11 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
style="position: relative;" style="position: relative;"
tabindex="-1" tabindex="-1"
> >
<div <span
class="cxd-ResultBox-value" class="cxd-ResultBox-placeholder"
> >
<span 请选择
class="cxd-ResultBox-valueLabel" </span>
>
诸葛亮
</span>
<a
data-index="0"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<div
class="cxd-ResultBox-value"
>
<span
class="cxd-ResultBox-valueLabel"
>
李白
</span>
<a
data-index="1"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
</div>
<span <span
class="cxd-TransferDropDown-icon" class="cxd-TransferDropDown-icon"
> >
@ -1067,7 +1038,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
class="cxd-Table-checkCell" class="cxd-Table-checkCell"
> >
<label <label
class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--sm" class="cxd-Checkbox cxd-Checkbox--checkbox cxd-Checkbox--full cxd-Checkbox--sm"
> >
<input <input
type="checkbox" type="checkbox"
@ -1088,7 +1059,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
</thead> </thead>
<tbody> <tbody>
<tr <tr
class="is-active" class=""
> >
<td <td
class="cxd-Table-checkCell" class="cxd-Table-checkCell"
@ -1175,7 +1146,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
</td> </td>
</tr> </tr>
<tr <tr
class="is-active" class=""
> >
<td <td
class="cxd-Table-checkCell" class="cxd-Table-checkCell"
@ -1369,11 +1340,7 @@ exports[`Renderer:select table with labelField & valueField 1`] = `
</div> </div>
`; `;
exports[`Renderer:select table with labelField & valueField 2`] = ` exports[`Renderer:select table with labelField & valueField 2`] = `Object {}`;
Object {
"a": "zhugeliang,libai",
}
`;
exports[`Renderer:select tree 1`] = ` exports[`Renderer:select tree 1`] = `
<div> <div>

File diff suppressed because it is too large Load Diff

View File

@ -179,6 +179,7 @@ icon:
"type": "transfer", "type": "transfer",
"name": "transfer4", "name": "transfer4",
"selectMode": "tree", "selectMode": "tree",
"searchable": true,
"options": [ "options": [
{ {
"label": "法师", "label": "法师",
@ -283,116 +284,6 @@ icon:
} }
``` ```
### 支持搜索
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "带搜索",
"type": "transfer",
"name": "transfer6",
"selectMode": "chained",
"searchable": true,
"sortable": true,
"options": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
```
### 延时加载
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "延时加载",
"type": "transfer",
"name": "transfer7",
"selectMode": "tree",
"deferApi": "/api/mock2/form/deferOptions?label=${label}&waitSeconds=2",
"options": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"defer": true
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
```
### 关联选择模式 ### 关联选择模式
```schema: scope="body" ```schema: scope="body"
@ -506,7 +397,7 @@ icon:
} }
``` ```
leftOptions 动态加载,默认 source 接口是返回 options 部分,而 leftOptions 是没有对应的接口可以动态返回了。为了方便,目前如果 source 接口返回的选中中,第一个 option 是以下这种格式则也会把 options[0].leftOptions 当成 leftOptions, options[0].children 当 options。同时 options[0].leftDefaultValue 可以用来配置左侧选项的默认值。 `leftOptions` 动态加载,默认 `source` 接口是返回 options 部分,而 `leftOptions` 是没有对应的接口可以动态返回了。为了方便,目前如果 `source` 接口返回的选中中,第一个 option 是以下这种格式则也会把 options[0].leftOptions 当成 `leftOptions`, options[0].children 当 options。同时 options[0].leftDefaultValue 可以用来配置左侧选项的默认值。
``` ```
{ {
@ -524,13 +415,200 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
} }
``` ```
## searchApi
**发送** ### 延时加载
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "延时加载",
"type": "transfer",
"name": "transfer7",
"selectMode": "tree",
"deferApi": "/api/mock2/form/deferOptions?label=${label}&waitSeconds=2",
"options": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"defer": true
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
```
### 支持搜索
#### 左侧搜索功能
通过 `searchable` 字段来控制左侧选项栏的搜索功能。
在不设置 `searchApi` 情况下对输入框内容和对应列表项的value、label进行匹配匹配成功就会左侧面板中显示。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "带搜索",
"type": "transfer",
"name": "transfer6",
"selectMode": "chained",
"searchable": true,
"sortable": true,
"options": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
```
#### 右侧结果搜索功能
右侧结果搜索是通过`resultSearchable`字段开启设置该字段为true时开启。
开启结果搜索后目前默认通过value、label对输入内容进行模糊匹配。
目前树的延时加载不支持结果搜索功能。
```schema: scope="body"
{
"type": "page",
"body": {
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "树型展示",
"type": "transfer",
"name": "transfer4",
"selectMode": "tree",
"searchable": true,
"resultListModeFollowSelect": true,
"resultSearchable": true,
"options": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
}
```
#### searchApi
设置这个api可以实现左侧选项搜索结果的检索。
##### 发送
默认 GET携带 term 变量,值为搜索框输入的文字,可从上下文中取数据设置进去。 默认 GET携带 term 变量,值为搜索框输入的文字,可从上下文中取数据设置进去。
**响应** ##### 响应
格式要求如下: 格式要求如下:
@ -559,7 +637,132 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
适用于需选择的数据/信息源较多时,用户可直观的知道自己所选择的数据/信息的场景,一般左侧框为数据/信息源,右侧为已选数据/信息,被选中信息同时存在于 2 个框内。 适用于需选择的数据/信息源较多时,用户可直观的知道自己所选择的数据/信息的场景,一般左侧框为数据/信息源,右侧为已选数据/信息,被选中信息同时存在于 2 个框内。
## 自定义选项展示 ### 结果面板跟随模式
`resultListModeFollowSelect` 开启结果面板跟随模式。
#### 表格跟随模式
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "表格形式",
"type": "transfer",
"name": "transfer",
"selectMode": "table",
"resultListModeFollowSelect": true,
"columns": [
{
"name": "label",
"label": "英雄"
},
{
"name": "position",
"label": "位置"
}
],
"options": [
{
"label": "诸葛亮",
"value": "zhugeliang",
"position": "中单"
},
{
"label": "曹操",
"value": "caocao",
"position": "上单"
},
{
"label": "钟无艳",
"value": "zhongwuyan",
"position": "上单"
},
{
"label": "李白",
"value": "libai",
"position": "打野"
},
{
"label": "韩信",
"value": "hanxin",
"position": "打野"
},
{
"label": "云中君",
"value": "yunzhongjun",
"position": "打野"
}
]
}
]
}
```
#### 树形跟随模式
```schema: scope="body"
{
"type": "page",
"body": {
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "树型展示",
"type": "transfer",
"name": "transfer4",
"selectMode": "tree",
"searchable": true,
"resultListModeFollowSelect": true,
"options": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
}
```
### 自定义选项展示
```schema: scope="body" ```schema: scope="body"
{ {
@ -612,27 +815,31 @@ leftOptions 动态加载,默认 source 接口是返回 options 部分,而 le
除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置 除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置
| 属性名 | 类型 | 默认值 | 说明 | | 属性名 | 类型 | 默认值 | 说明 |
| ---------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------- | ----------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) | | options | `Array<object>`或`Array<string>` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) |
| source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) | | source | `string`或 [API](../../../docs/types/api) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) |
| delimiter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) | | delimeter | `string` | `false` | [拼接符](./options#%E6%8B%BC%E6%8E%A5%E7%AC%A6-delimiter) |
| joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) | | joinValues | `boolean` | `true` | [拼接值](./options#%E6%8B%BC%E6%8E%A5%E5%80%BC-joinvalues) |
| extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) | | extractValue | `boolean` | `false` | [提取值](./options#%E6%8F%90%E5%8F%96%E5%A4%9A%E9%80%89%E5%80%BC-extractvalue) |
| searchable | `boolean` | `false` | 当设置为 `true` 时表示可以通过输入部分内容检索出选项。 | | searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索可以设置这个api。|
| searchApi | [API](../../../docs/types/api) | | 如果想通过接口检索,可以设置个 api。 | | resultListModeFollowSelect | `boolean` | `false` | 结果面板跟随模式,目前只支持`list`、`table`、`tree`tree目前只支持非延时加载的`tree` |
| statistics | `boolean` | `true` | 是否显示统计数据 | | statistics | `boolean` | `true` | 是否显示统计数据|
| selectTitle | `string` | `"请选择"` | 左侧的标题文字 | | selectTitle | `string` | `"请选择"` | 左侧的标题文字 |
| resultTitle | `string` | `"当前选择"` | 右侧结果的标题文字 | | resultTitle | `string` | `"当前选择"` | 右侧结果的标题文字|
| sortable | `boolean` | `false` | 结果可以进行拖拽排序 | | sortable | `boolean` | `false` | 结果可以进行拖拽排序(结果列表为树时,不支持排序)|
| selectMode | `string` | `list` | 可选:`list`、`table`、`tree`、`chained`、`associated`。分别为:列表形式、表格形式、树形选择形式、级联选择形式,关联选择形式(与级联选择的区别在于,级联是无限极,而关联只有一级,关联左边可以是个 tree。 | | selectMode | `string` | `list` | 可选:`list`、`table`、`tree`、`chained`、`associated`。分别为:列表形式、表格形式、树形选择形式、级联选择形式,关联选择形式(与级联选择的区别在于,级联是无限极,而关联只有一级,关联左边可以是个 tree。 |
| searchResultMode | `string` | | 如果不设置将采用 `selectMode` 的值,可以单独配置,参考 `selectMode`,决定搜索结果的展示形式。 | | searchResultMode | `string` | | 如果不设置将采用 `selectMode` 的值,可以单独配置,参考 `selectMode`,决定搜索结果的展示形式。 |
| columns | `Array<Object>` | | 当展示形式为 `table` 可以用来配置展示哪些列,跟 table 中的 columns 配置相似,只是只有展示功能。 | | searchable | `boolean` | `false` | 左侧列表搜索功能当设置为 true 时表示可以通过输入部分内容检索出选项项。 |
| leftOptions | `Array<Object>` | | 当展示形式为 `associated` 时用来配置左边的选项集。 | | searchPlaceholder | `string` | | 左侧列表搜索框提示 |
| leftMode | `string` | | 当展示形式为 `associated` 时用来配置左边的选择形式,支持 `list` 或者 `tree`。默认为 `list`。 | | columns | `Array<Object>` | | 当展示形式为 `table` 可以用来配置展示哪些列,跟 table 中的 columns 配置相似,只是只有展示功能。 |
| rightMode | `string` | | 当展示形式为 `associated` 时用来配置右边的选择形式,可选:`list`、`table`、`tree`、`chained`。 | | leftOptions | `Array<Object>` | | 当展示形式为 `associated` 时用来配置左边的选项集。|
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 | | leftMode | `string` | | 当展示形式为 `associated` 时用来配置左边的选择形式,支持 `list` 或者 `tree`。默认为 `list`。|
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 | | rightMode | `string` | | 当展示形式为 `associated` 时用来配置右边的选择形式,可选:`list`、`table`、`tree`、`chained`。 |
| resultSearchable | `boolean` | `false` | 结果右则列表的检索功能当设置为true时可以通过输入检索模糊匹配检索内容目前树的延时加载不支持结果搜索功能 |
| resultSearchPlaceholder | `string` | | 右侧列表搜索框提示 |
| menuTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义选项展示 |
| valueTpl | `string` \| [SchemaNode](../../docs/types/schemanode) | | 用来自定义值的展示 |
## 事件表 ## 事件表

View File

@ -22,6 +22,7 @@
color: var(--Form-input-placeholderColor); color: var(--Form-input-placeholderColor);
text-align: center; text-align: center;
width: 100%; width: 100%;
height: 100%;
flex-basis: var(--Form-input-height); flex-basis: var(--Form-input-height);
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;

View File

@ -69,7 +69,7 @@
flex-direction: row; flex-direction: row;
> .#{$ns}Checkbox { > .#{$ns}Checkbox {
margin-right: px2rem(8px); margin-right: px2rem(10px);
} }
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -104,6 +104,10 @@
&-placeholder { &-placeholder {
@include checkboxes-placeholder(); @include checkboxes-placeholder();
height: 100%;
display: flex;
align-items: center;
justify-content: center;
} }
} }
@ -135,10 +139,6 @@
padding-right: var(--gap-md); padding-right: var(--gap-md);
} }
.#{$ns}Table-table > tbody > tr {
cursor: pointer;
}
.#{$ns}Table-table > tbody > tr.is-active { .#{$ns}Table-table > tbody > tr.is-active {
color: var(--Form-select-menu-onActive-color); color: var(--Form-select-menu-onActive-color);
background: var(--Form-select-menu-onActive-bg); background: var(--Form-select-menu-onActive-bg);
@ -148,24 +148,13 @@
.#{$ns}TreeSelection { .#{$ns}TreeSelection {
.#{$ns}Table-expandBtn { .#{$ns}Table-expandBtn {
color: var(--icon-color); color: var(--icon-color);
margin-right: 5px; margin-right: var(--gap-xs);
} }
&-sublist { &-sublist {
position: relative; position: relative;
margin: 0 0 0 px2rem(35px); margin: 0 0 0 px2rem(35px);
display: none; display: none;
&:before {
width: 1px;
content: '';
display: block;
position: absolute;
top: calc(var(--gap-xs) * -1);
bottom: calc(var(--Form-input-height) / 2);
left: -19px;
border-left: dashed 1px var(--icon-color);
}
} }
&-item { &-item {
@ -183,35 +172,17 @@
color: var(--text--muted-color); color: var(--text--muted-color);
} }
&-sublist &-item:before {
height: 1px;
content: '';
display: block;
position: absolute;
top: calc(var(--Form-input-height) / 2);
width: 19px;
left: -19px;
border-top: dashed 1px var(--icon-color);
}
&-itemInner { &-itemInner {
display: flex; display: flex;
align-items: center; align-items: center;
// height: var(--Form-input-height); line-height: var(--Tree-itemHeight);
line-height: var(--Form-input-lineHeight); position: relative;
font-size: var(--Form-input-fontSize); font-size: var(--Form-input-fontSize);
padding: calc( padding: 0 var(--gap-sm);
(
var(--Form-input-height) - var(--Form-input-lineHeight) *
var(--Form-input-fontSize)
) / 2
)
var(--gap-sm);
flex-direction: row; flex-direction: row;
> .#{$ns}Checkbox { > .#{$ns}Checkbox {
margin-right: 0; margin-right: var(--gap-sm);
margin-left: var(--gap-sm);
} }
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -234,7 +205,6 @@
flex-grow: 1; flex-grow: 1;
span { span {
margin-left: var(--gap-xs);
vertical-align: middle; vertical-align: middle;
} }
} }
@ -303,7 +273,7 @@
flex-grow: 1; flex-grow: 1;
span { span {
margin-left: var(--gap-xs); margin-left: px2rem(10px);
vertical-align: middle; vertical-align: middle;
} }
} }
@ -358,3 +328,18 @@
} }
} }
} }
.#{$ns}ResultTreeList {
height: 100%;
width: 100%;
}
.#{$ns}ResultTableList {
height: 100%;
width: 100%;
&-close-btn {
float: right;
cursor: pointer;
}
}

View File

@ -5,6 +5,10 @@
min-height: px2rem(300px); min-height: px2rem(300px);
position: relative; position: relative;
&-searchbox {
width: 100%;
}
&--inline { &--inline {
display: inline-flex; display: inline-flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -70,6 +74,10 @@
} }
} }
.#{$ns}-ResultTreeList {
border-top: 1px solid var(--borderColor);
}
.#{$ns}AssociatedSelection { .#{$ns}AssociatedSelection {
overflow: hidden; overflow: hidden;
@ -79,6 +87,10 @@
} }
} }
&-select {
overflow: hidden;
}
&-search + &-selection { &-search + &-selection {
border-top: 1px solid var(--borderColor); border-top: 1px solid var(--borderColor);
} }
@ -129,6 +141,14 @@
color: var(--text--muted-color); color: var(--text--muted-color);
} }
} }
.#{$ns}Tree {
padding: px2rem(2px) px2rem(10px);
&-itemLabel:hover::after {
left: 0;
}
}
} }
.#{$ns}TabsTransfer { .#{$ns}TabsTransfer {
@ -230,6 +250,7 @@
.#{$ns}TransferControl { .#{$ns}TransferControl {
position: relative; position: relative;
overflow: hidden;
&.is-inline { &.is-inline {
display: inline-block; display: inline-block;

View File

@ -155,16 +155,65 @@
.#{$ns}Transfer { .#{$ns}Transfer {
&-title { &-title {
height: var(--Form-input-height); flex: 0 0 #{px2rem(38px)};
padding-left: #{px2rem(16px)}; height: #{px2rem(38px)};
padding-right: #{px2rem(16px)}; padding-left: var(--gap-base);
padding-right: var(--gap-base);
} }
&-result {
>.#{$ns}Selections {
height: 100%;
}
}
&-table-title {
background-color: unset;
}
.#{$ns}ListCheckboxes { .#{$ns}ListCheckboxes {
.#{$ns}ListCheckboxes-item { .#{$ns}ListCheckboxes-item {
padding-left: #{px2rem(16px)}; padding-left: #{px2rem(16px)};
padding-right: #{px2rem(16px)}; padding-right: #{px2rem(16px)};
} }
} }
.#{$ns}TableSelection {
.#{$ns}Table-table {
border-top: 0;
border-left: 0;
border-right: 0;
}
tbody {
tr {
&.is-active {
color: unset;
}
}
}
}
}
.#{$ns}GroupedSelection {
&-item {
padding-left: var(--gap-base);
padding-right: var(--gap-base);
&.is-active {
color: unset;
}
}
}
.#{$ns}Selections {
height: auto;
&-item {
padding-left: var(--gap-base);
padding-right: var(--gap-base);
}
} }
.#{$ns}Modal { .#{$ns}Modal {

View File

@ -12,11 +12,11 @@ import {themeable} from '../theme';
import {uncontrollable} from 'uncontrollable'; import {uncontrollable} from 'uncontrollable';
import GroupedSelection from './GroupedSelection'; import GroupedSelection from './GroupedSelection';
import TableSelection from './TableSelection'; import TableSelection from './TableSelection';
import TreeSelection from './TreeSelection';
import GroupedSelecton from './GroupedSelection'; import GroupedSelecton from './GroupedSelection';
import ChainedSelection from './ChainedSelection'; import ChainedSelection from './ChainedSelection';
import {Icon} from './icons'; import {Icon} from './icons';
import {localeable} from '../locale'; import {localeable} from '../locale';
import Tree from './Tree';
export interface AssociatedSelectionProps extends BaseSelectionProps { export interface AssociatedSelectionProps extends BaseSelectionProps {
leftOptions: Options; leftOptions: Options;
@ -44,6 +44,7 @@ export class AssociatedSelection extends BaseSelection<
AssociatedSelectionProps, AssociatedSelectionProps,
AssociatedSelectionState AssociatedSelectionState
> { > {
state: AssociatedSelectionState = { state: AssociatedSelectionState = {
leftValue: this.props.leftDefaultValue leftValue: this.props.leftDefaultValue
}; };
@ -133,14 +134,12 @@ export class AssociatedSelection extends BaseSelection<
<div className={cx('AssociatedSelection', className)}> <div className={cx('AssociatedSelection', className)}>
<div className={cx('AssociatedSelection-left')}> <div className={cx('AssociatedSelection-left')}>
{leftMode === 'tree' ? ( {leftMode === 'tree' ? (
<TreeSelection <Tree
option2value={this.leftOption2Value}
options={leftOptions}
value={this.state.leftValue}
disabled={disabled}
onChange={this.handleLeftSelect}
multiple={false} multiple={false}
clearable={false} disabled={disabled}
value={this.state.leftValue}
options={leftOptions}
onChange={this.handleLeftSelect}
onDeferLoad={this.handleLeftDeferLoad} onDeferLoad={this.handleLeftDeferLoad}
/> />
) : ( ) : (
@ -192,14 +191,12 @@ export class AssociatedSelection extends BaseSelection<
multiple={multiple} multiple={multiple}
/> />
) : rightMode === 'tree' ? ( ) : rightMode === 'tree' ? (
<TreeSelection <Tree
value={value} value={value}
disabled={disabled} disabled={disabled}
options={selectdOption.children || []} options={selectdOption.children || []}
onChange={onChange} onChange={onChange!}
option2value={option2value}
multiple={multiple} multiple={multiple}
itemRender={itemRender}
/> />
) : rightMode === 'chained' ? ( ) : rightMode === 'chained' ? (
<ChainedSelection <ChainedSelection

View File

@ -1,12 +1,13 @@
import {BaseSelection} from './Selection';
import {themeable} from '../theme';
import React from 'react'; import React from 'react';
import {uncontrollable} from 'uncontrollable'; import {uncontrollable} from 'uncontrollable';
import {BaseSelection, BaseSelectionProps} from './Selection';
import {themeable} from '../theme';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import {Option} from './Select'; import {Option} from './Select';
import {localeable} from '../locale'; import {localeable} from '../locale';
export class GroupedSelection extends BaseSelection { export class GroupedSelection extends BaseSelection<BaseSelectionProps> {
valueArray: Array<Option>; valueArray: Array<Option>;
renderOption(option: Option, index: number) { renderOption(option: Option, index: number) {
@ -18,6 +19,7 @@ export class GroupedSelection extends BaseSelection {
itemRender, itemRender,
multiple multiple
} = this.props; } = this.props;
const valueArray = this.valueArray; const valueArray = this.valueArray;
if (Array.isArray(option.children)) { if (Array.isArray(option.children)) {

View File

@ -2,15 +2,21 @@
* *
*/ */
import React from 'react'; import React from 'react';
import {Option} from './Select'; import Sortable from 'sortablejs';
import {findDOMNode} from 'react-dom';
import {Option, Options} from './Select';
import {ThemeProps, themeable} from '../theme'; import {ThemeProps, themeable} from '../theme';
import {Icon} from './icons'; import {Icon} from './icons';
import {autobind, guid} from '../utils/helper'; import {autobind, guid} from '../utils/helper';
import Sortable from 'sortablejs';
import {findDOMNode} from 'react-dom';
import {LocaleProps, localeable} from '../locale'; import {LocaleProps, localeable} from '../locale';
import {BaseSelection, BaseSelectionProps} from './Selection';
import TransferSearch from './TransferSearch';
export interface ResultListProps extends ThemeProps, LocaleProps { export interface ResultListProps
extends ThemeProps,
LocaleProps,
BaseSelectionProps {
className?: string; className?: string;
value?: Array<Option>; value?: Array<Option>;
onChange?: (value: Array<Option>, optionModified?: boolean) => void; onChange?: (value: Array<Option>, optionModified?: boolean) => void;
@ -20,6 +26,23 @@ export interface ResultListProps extends ThemeProps, LocaleProps {
placeholder: string; placeholder: string;
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element; itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
itemClassName?: string; itemClassName?: string;
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;
searchable?: boolean;
onSearch?: Function;
} }
export interface ItemRenderStates { export interface ItemRenderStates {
@ -28,17 +51,35 @@ export interface ItemRenderStates {
onChange: (value: any, name: string) => void; onChange: (value: any, name: string) => void;
} }
export class ResultList extends React.Component<ResultListProps> { interface ResultListState {
searchResult: Options | null;
}
export class ResultList extends React.Component<
ResultListProps,
ResultListState
> {
static itemRender(option: any) { static itemRender(option: any) {
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>; return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
} }
static defaultProps: Pick<ResultListProps, 'placeholder' | 'itemRender'> = {
static defaultProps: Pick<
ResultListProps,
'placeholder' | 'itemRender'
> = {
placeholder: 'placeholder.selectData', placeholder: 'placeholder.selectData',
itemRender: ResultList.itemRender itemRender: ResultList.itemRender
}; };
state: ResultListState = {
searchResult: null
};
cancelSearch?: () => void;
id = guid(); id = guid();
sortable?: Sortable; sortable?: Sortable;
unmounted = false;
componentDidMount() { componentDidMount() {
this.props.sortable && this.initSortable(); this.props.sortable && this.initSortable();
@ -54,20 +95,7 @@ export class ResultList extends React.Component<ResultListProps> {
componentWillUnmount() { componentWillUnmount() {
this.desposeSortable(); this.desposeSortable();
} this.unmounted = true;
@autobind
handleRemove(e: React.MouseEvent<HTMLElement>) {
const index = parseInt(e.currentTarget.getAttribute('data-index')!, 10);
const {value, onChange} = this.props;
if (!Array.isArray(value)) {
return;
}
const newValue = value.concat();
newValue.splice(index, 1);
onChange?.(newValue);
} }
initSortable() { initSortable() {
@ -138,24 +166,65 @@ export class ResultList extends React.Component<ResultListProps> {
onChange?.(result, true); onChange?.(result, true);
} }
render() { @autobind
search(inputValue: string) {
const {onSearch, value} = this.props;
const searchResult = (value || []).filter(
item => onSearch && onSearch(inputValue, item)
);
this.setState({searchResult})
}
@autobind
clearSearch() {
this.setState({searchResult: null})
}
// 关闭表格最后一项
@autobind
handleCloseItem(option: Option) {
const {value, onChange, option2value, options, disabled} = this.props;
if (disabled || option.disabled) {
return;
}
// 删除普通值
let valueArray = BaseSelection.value2array(value, options, option2value);
let idx = valueArray.indexOf(option);
valueArray.splice(idx, 1);
let newValue: string | Array<Option> = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(newValue);
const {searchResult} = this.state;
if (searchResult) {
const searchArray = BaseSelection.value2array(
searchResult,
options,
option2value
);
const searchIdx = searchArray.indexOf(option);
searchResult.splice(searchIdx, 1);
this.setState({searchResult});
}
}
renderNormalList(value?: Options) {
const { const {
classnames: cx, classnames: cx,
className,
value,
placeholder, placeholder,
itemRender, itemRender,
disabled, disabled,
title,
itemClassName, itemClassName,
sortable, sortable,
translate: __ translate: __
} = this.props; } = this.props;
return ( return (
<div className={cx('Selections', className)}> <>
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
{Array.isArray(value) && value.length ? ( {Array.isArray(value) && value.length ? (
<div className={cx('Selections-items')}> <div className={cx('Selections-items')}>
{value.map((option, index) => ( {value.map((option, index) => (
@ -186,7 +255,9 @@ export class ResultList extends React.Component<ResultListProps> {
<a <a
className={cx('Selections-delBtn')} className={cx('Selections-delBtn')}
data-index={index} data-index={index}
onClick={this.handleRemove} onClick={(e: React.MouseEvent<HTMLElement>) =>
this.handleCloseItem(option)
}
> >
<Icon icon="close" className="icon" /> <Icon icon="close" className="icon" />
</a> </a>
@ -194,9 +265,36 @@ export class ResultList extends React.Component<ResultListProps> {
</div> </div>
))} ))}
</div> </div>
) : ( ) :
<div className={cx('Selections-placeholder')}>{__(placeholder)}</div> (<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)}
)} </>
);
}
render() {
const {
classnames: cx,
className,
title,
searchable,
value,
translate: __,
placeholder = __('Transfer.searchKeyword')
} = this.props;
const {searchResult} = this.state;
return (
<div className={cx('Selections', className)}>
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
{searchable ? (
<TransferSearch
placeholder={placeholder}
onSearch={this.search}
onCancelSearch={this.clearSearch}
/>
) : null}
{this.renderNormalList(searchResult !== null ? searchResult : value)}
</div> </div>
); );
} }

View File

@ -0,0 +1,231 @@
/**
* ()
*/
import React from 'react';
import {BaseSelection, BaseSelectionProps} from './Selection';
import {themeable} from '../theme';
import {Option, Options} from './Select';
import {resolveVariable} from '../utils/tpl-builtin';
import {localeable} from '../locale';
import {autobind} from '../utils/helper';
import TransferSearch from './TransferSearch';
import {CloseIcon} from './icons';
import TableSelection from './TableSelection';
export interface ResultTableSelectionProps extends BaseSelectionProps {
title?: string;
placeholder?: string;
searchable?: boolean;
onSearch?: Function;
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;
}
export interface ResultTableSelectionState {
tableOptions: Options;
searching: Boolean;
searchTableOptions: Options;
}
export class BaseResultTableSelection extends BaseSelection<ResultTableSelectionProps, ResultTableSelectionState> {
static defaultProps = {
...BaseSelection.defaultProps,
cellRender: (
column: {
name: string;
label: string;
[propName: string]: any;
},
option: Option,
colIndex: number,
rowIndex: number
) => <span>{resolveVariable(column.name, option)}</span>,
};
state: ResultTableSelectionState = {
tableOptions: [],
searching: false,
searchTableOptions: []
}
static getDerivedStateFromProps(props: ResultTableSelectionProps) {
const {
options,
value,
option2value,
} = props;
const valueArray = BaseSelection.value2array(value, options, option2value);
return {
tableOptions: valueArray
}
}
@autobind
handleCloseItem(option: Option) {
const {value, onChange, option2value, options, disabled} = this.props;
const {searching, searchTableOptions} = this.state;
if (disabled || option.disabled) {
return;
}
// 删除普通值
let valueArray = BaseSelection.value2array(value, options, option2value);
let idx = valueArray.indexOf(option);
valueArray.splice(idx, 1);
let newValue: string | Array<Option> = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(newValue);
if (searching) {
const searchArray = BaseSelection.value2array(
searchTableOptions,
options,
option2value
);
const searchIdx = searchArray.indexOf(option);
searchTableOptions.splice(searchIdx, 1);
this.setState({searchTableOptions});
}
}
@autobind
search(inputValue: string) {
// 结果为空,直接清空
if (!inputValue) {
this.clearSearch();
return;
}
const {value, onSearch} = this.props;
const searchOptions = ((value || []) as Options)
.filter(item => onSearch?.(inputValue, item));
this.setState({
searching: true,
searchTableOptions: searchOptions
});
}
@autobind
clearSearch() {
this.setState({
searching: false,
searchTableOptions: []
});
}
renderTable() {
const {
classnames: cx,
className,
columns,
cellRender,
value,
disabled,
option2value,
onChange,
translate: __,
placeholder
} = this.props;
const {searching, tableOptions, searchTableOptions} = this.state;
return (
<div className={cx('ResultTableList', className)}>
{
Array.isArray(value) && value.length ?
(
<TableSelection
columns={columns}
options={!searching ? tableOptions : searchTableOptions}
value={value}
disabled={disabled}
option2value={option2value}
onChange={onChange}
multiple={false}
cellRender={(
column: {
name: string;
label: string;
[propName: string]: any;
},
option: Option,
colIndex: number,
rowIndex: number
) => {
const raw = cellRender(column, option, colIndex, rowIndex);
if (colIndex === columns.length - 1) {
return (
<>
{raw}
{
<span
className={cx('ResultTableList-close-btn')}
onClick={(e: React.SyntheticEvent<HTMLElement>) => {
e.stopPropagation();
this.handleCloseItem(option);
}}
>
<CloseIcon />
</span>
}
</>)
}
return raw;
}}
/>
)
: (<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)
}
</div>
);
}
render() {
const {
classnames: cx,
className,
title,
searchable,
translate: __,
placeholder = __('Transfer.searchKeyword')
} = this.props;
return (
<div className={cx('Selections', className)}>
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
{searchable ? (
<TransferSearch
placeholder={placeholder}
onSearch={this.search}
onCancelSearch={this.clearSearch}
/>
) : null}
{this.renderTable()}
</div>
);
}
}
export default themeable(localeable(BaseResultTableSelection));

View File

@ -0,0 +1,298 @@
/**
* ()
*/
import React from 'react';
import {cloneDeep, isEqual, omit} from 'lodash';
import {Option, Options} from './Select';
import {ThemeProps, themeable} from '../theme';
import {autobind, noop} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
import {BaseSelectionProps} from './Selection';
import Tree from './Tree';
import TransferSearch from './TransferSearch';
export interface ResultTreeListProps extends ThemeProps, LocaleProps, BaseSelectionProps {
className?: string;
title?: string;
searchable?: boolean;
value: Array<Option>;
valueField: string;
onSearch?: Function;
onChange: (value: Array<Option>, optionModified?: boolean) => void;
placeholder: string;
itemRender: (option: Option, states: ItemRenderStates) => JSX.Element;
itemClassName?: string;
cellRender?: (
column: {
name: string;
label: string;
[propName: string]: any;
},
option: Option,
colIndex: number,
rowIndex: number
) => JSX.Element;
}
export interface ItemRenderStates {
index: number;
disabled?: boolean;
onChange: (value: any, name: string) => void;
}
interface ResultTreeListState {
searching: Boolean;
treeOptions: Options;
searchTreeOptions: Options;
}
// 递归找到对应选中节点(dfs)
function getDeep(
node: Option,
cb: (node: Option) => boolean,
pathNodes: Array<Option>,
valueField: string
) {
if (node[valueField] && cb(node)) {
node.isChecked = true;
for (let i = pathNodes.length - 2; i >= 0; i--) {
if (!pathNodes[i].isChecked) {
pathNodes[i].isChecked = true;
continue;
}
break;
}
} else if (node.children && Array.isArray(node.children)) {
node.children.forEach(n => {
pathNodes.push(n);
getDeep(n, cb, pathNodes, valueField);
pathNodes.pop();
});
}
}
// 递归删除树多余的节点
function deepCheckedTreeNode(nodes: Array<Option>) {
let arr: Array<Option> = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.isChecked) {
if (node.children && Array.isArray(node.children)) {
node.children = deepCheckedTreeNode(node.children);
}
arr.push(node);
}
}
return arr;
}
// 根据选项获取到结果
function getResultOptions(value: Options = [], options: Options, valueField: string) {
const newOptions = cloneDeep(options) as Array<Option>;
const callBack = (node: Option) =>
!!(value || []).find(target => target[valueField] === node[valueField]);
newOptions &&
newOptions.forEach(op => {
getDeep(op, callBack, [op], valueField);
});
return deepCheckedTreeNode(newOptions);
}
// 在包含回调函数情况下,遍历树
function deepTree(nodes: Options, cb: (node: Option) => void) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
cb(node);
if (node.children && Array.isArray(node.children)) {
deepTree(node.children, cb);
}
}
}
// 树的节点删除
function deepDeleteTree(nodes: Options, option: Option, valueField: string) {
let arr: Array<Option> = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isEqual(node, option)) {
continue;
}
if (node.children && Array.isArray(node.children)) {
node.children = deepDeleteTree(node.children, option, valueField);
}
if (
(node.children && node.children.length > 0) ||
node[valueField] !== undefined
) {
arr.push(node);
}
}
return arr;
}
export class BaseResultTreeList extends React.Component<ResultTreeListProps, ResultTreeListState> {
static itemRender(option: any) {
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
}
static defaultProps: Pick<ResultTreeListProps, 'placeholder' | 'itemRender'> = {
placeholder: 'placeholder.selectData',
itemRender: BaseResultTreeList.itemRender
};
state: ResultTreeListState = {
searching: false,
treeOptions: [],
searchTreeOptions: []
}
static getDerivedStateFromProps(props: ResultTreeListProps) {
const newOptions = getResultOptions(props.value, props.options, props.valueField);
return {
treeOptions: cloneDeep(newOptions)
}
}
// 删除非选中节点
@autobind
deleteTreeChecked(option: Option) {
const {value = [], onChange, valueField} = this.props;
const {searching, treeOptions} = this.state;
let temNode: Options = [];
const cb = (node: Option) => {
if (isEqual(node, option)) {
temNode = [node];
}
};
deepTree(treeOptions || [], cb);
let arr: Options = [];
const cb2 = (node: Option) => {
if (node.isChecked && node[valueField]) {
arr.push(node);
}
};
deepTree(temNode, cb2);
onChange &&
onChange(
value.filter(
item =>
!arr.find(arrItem =>
isEqual(omit(arrItem, ['isChecked', 'childrens']), item)
)
)
);
// 搜索时,重新生成树
searching && this.deleteResultTreeNode(option);
}
// 搜索树点击删除时,删除对应节点
deleteResultTreeNode(option: Option) {
const arr = deepDeleteTree(
cloneDeep(this.state.searchTreeOptions) || [],
option,
this.props.valueField
);
this.setState({searchTreeOptions: arr});
}
@autobind
search(inputValue: string) {
// 结果为空,直接清空
if (!inputValue) {
this.clearSearch();
return;
}
const {valueField, onSearch} = this.props;
let temOptions: Array<Option> = this.state.treeOptions || [];
const cb = (node: Option) => {
node.isChecked = false;
return true;
};
deepTree(temOptions, cb);
const callBack = (node: Option) => onSearch?.(inputValue, node);
temOptions &&
temOptions.forEach(op => {
getDeep(op, callBack, [op], valueField);
});
this.setState({
searching: true,
searchTreeOptions: deepCheckedTreeNode(temOptions)
});
}
@autobind
clearSearch() {
this.setState({
searching: false,
searchTreeOptions: []
});
}
renderTree() {
const {
className,
classnames: cx,
value,
placeholder,
valueField,
itemRender,
translate: __
} = this.props;
const {treeOptions, searching, searchTreeOptions} = this.state;
return (
<div className={cx('ResultTreeList', className)}>
{
Array.isArray(value) && value.length ?
(<Tree
className={cx('Transfer-tree')}
options={!searching ? treeOptions : searchTreeOptions}
valueField={valueField}
value={[]}
onChange={noop}
showIcon={false}
itemRender={itemRender}
removable
onDelete={(option: Option) => this.deleteTreeChecked(option)}
/>) :
(<div className={cx('Selections-placeholder')}>{__(placeholder)}</div>)
}
</div>
);
}
render() {
const {
classnames: cx,
className,
title,
searchable,
translate: __,
placeholder = __('Transfer.searchKeyword')
} = this.props;
return (
<div className={cx('Selections', className)}>
{title ? <div className={cx('Selections-title')}>{title}</div> : null}
{searchable ? (
<TransferSearch
placeholder={placeholder}
onSearch={this.search}
onCancelSearch={this.clearSearch}
/>
) : null}
{this.renderTree()}
</div>
);
}
}
export default themeable(localeable(BaseResultTreeList));

View File

@ -122,7 +122,6 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
return ( return (
<tr <tr
key={rowIndex} key={rowIndex}
onClick={e => e.defaultPrevented || this.toggleOption(option)}
className={cx( className={cx(
itemClassName, itemClassName,
option.className, option.className,
@ -131,8 +130,18 @@ export class TableSelection extends BaseSelection<TableSelectionProps> {
)} )}
> >
{multiple ? ( {multiple ? (
<td className={cx('Table-checkCell')} key="checkbox"> <td className={cx('Table-checkCell')}
<Checkbox size="sm" checked={checked} disabled={disabled} /> key="checkbox"
onClick={e => {
e.stopPropagation();
this.toggleOption(option);
}}
>
<Checkbox
size="sm"
checked={checked}
disabled={disabled}
/>
</td> </td>
) : null} ) : null}
{columns.map((column, colIndex) => ( {columns.map((column, colIndex) => (
@ -174,4 +183,4 @@ export default themeable(
value: 'onChange' value: 'onChange'
}) })
) )
); );

View File

@ -1,20 +1,32 @@
import React from 'react'; import React from 'react';
import {intersectionWith, differenceWith, includes, debounce, result} from 'lodash';
import {ThemeProps, themeable} from '../theme'; import {ThemeProps, themeable} from '../theme';
import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection'; import {BaseSelectionProps, BaseSelection, ItemRenderStates} from './Selection';
import {Options, Option} from './Select'; import {Options, Option} from './Select';
import {uncontrollable} from 'uncontrollable'; import {uncontrollable} from 'uncontrollable';
import ResultList from './ResultList'; import ResultList from './ResultList';
import TableSelection from './TableSelection'; import TableSelection from './TableSelection';
import TreeSelection from './TreeSelection';
import {autobind, flattenTree} from '../utils/helper'; import {autobind, flattenTree} from '../utils/helper';
import InputBox from './InputBox'; import InputBox from './InputBox';
import Checkbox from './Checkbox';
import Tree from './Tree';
import {Icon} from './icons'; import {Icon} from './icons';
import debounce from 'lodash/debounce';
import AssociatedSelection from './AssociatedSelection'; import AssociatedSelection from './AssociatedSelection';
import {LocaleProps, localeable} from '../locale'; import {LocaleProps, localeable} from '../locale';
import GroupedSelection from './GroupedSelection'; import GroupedSelection from './GroupedSelection';
import ChainedSelection from './ChainedSelection'; import ChainedSelection from './ChainedSelection';
import {ItemRenderStates as ResultItemRenderStates} from './ResultList'; import {ItemRenderStates as ResultItemRenderStates} from './ResultList';
import ResultTableList from './ResultTableList';
import ResultTreeList from './ResultTreeList';
export type SelectMode =
'table'
| 'group'
| 'list'
| 'tree'
| 'chained'
| 'associated';
export interface TransferProps export interface TransferProps
extends ThemeProps, extends ThemeProps,
@ -26,7 +38,7 @@ export interface TransferProps
multiple?: boolean; multiple?: boolean;
selectTitle?: string; selectTitle?: string;
selectMode?: 'table' | 'group' | 'list' | 'tree' | 'chained' | 'associated'; selectMode?: SelectMode;
columns?: Array<{ columns?: Array<{
name: string; name: string;
label: string; label: string;
@ -72,11 +84,16 @@ export interface TransferProps
) => JSX.Element; ) => JSX.Element;
resultTitle?: string; resultTitle?: string;
// 结果提示语
resultListModeFollowSelect?: boolean;
resultSearchPlaceholder?: string;
optionItemRender?: (option: Option, states: ItemRenderStates) => JSX.Element; optionItemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
resultItemRender?: ( resultItemRender?: (
option: Option, option: Option,
states: ResultItemRenderStates states: ResultItemRenderStates
) => JSX.Element; ) => JSX.Element;
resultSearchable?: boolean;
onResultSearch?: (text: string, item: Option) => boolean;
sortable?: boolean; sortable?: boolean;
onRef?: (ref: Transfer) => void; onRef?: (ref: Transfer) => void;
onSelectAll?: (options: Options) => void; onSelectAll?: (options: Options) => void;
@ -85,34 +102,76 @@ export interface TransferProps
export interface TransferState { export interface TransferState {
inputValue: string; inputValue: string;
searchResult: Options | null; searchResult: Options | null;
isTreeDeferLoad: boolean;
resultSelectMode: 'list' | 'tree' | 'table'
} }
export class Transfer< export class Transfer<
T extends TransferProps = TransferProps T extends TransferProps = TransferProps
> extends React.Component<T, TransferState> { > extends React.Component<T, TransferState> {
static defaultProps: Pick<TransferProps, 'multiple'> = {
multiple: true static defaultProps: Pick<
TransferProps,
'multiple' | 'resultListModeFollowSelect' | 'selectMode'
> = {
multiple: true,
resultListModeFollowSelect: false,
selectMode: 'list'
}; };
state = { state: TransferState = {
inputValue: '', inputValue: '',
searchResult: null searchResult: null,
isTreeDeferLoad: false,
resultSelectMode: 'list'
}; };
valueArray: Options; valueArray: Options;
availableOptions: Options; availableOptions: Options;
unmounted = false; unmounted = false;
cancelSearch?: () => void; cancelSearch?: () => void;
treeRef: any;
componentDidMount() { componentDidMount() {
this.props?.onRef?.(this); this.props?.onRef?.(this);
} }
static getDerivedStateFromProps(props: TransferProps) {
// 计算是否是懒加载模式
let isTreeDeferLoad: boolean = false;
props.selectMode === 'tree' &&
props.options.forEach(item => {
if (item.defer) {
isTreeDeferLoad = true;
}
});
// 计算结果的selectMode
let resultSelectMode = 'list';
if (props.selectMode === 'tree' && props.resultListModeFollowSelect && !isTreeDeferLoad) {
resultSelectMode = 'tree';
}
if (props.selectMode === 'table' && props.resultListModeFollowSelect) {
resultSelectMode = 'table';
}
return {
isTreeDeferLoad,
resultSelectMode
};
}
componentWillUnmount() { componentWillUnmount() {
this.lazySearch.cancel(); this.lazySearch.cancel();
this.unmounted = true; this.unmounted = true;
} }
@autobind
domRef(ref: any) {
this.treeRef = ref;
}
@autobind @autobind
toggleAll() { toggleAll() {
const {options, option2value, onChange, value, onSelectAll} = this.props; const {options, option2value, onChange, value, onSelectAll} = this.props;
@ -142,7 +201,7 @@ export class Transfer<
// 全选,给予动作全选使用 // 全选,给予动作全选使用
selectAll() { selectAll() {
const {options, option2value, onChange} = this.props;; const {options, option2value, onChange} = this.props;
const availableOptions = flattenTree(options).filter( const availableOptions = flattenTree(options).filter(
(option, index, list) => (option, index, list) =>
!option.disabled && !option.disabled &&
@ -169,22 +228,17 @@ export class Transfer<
} }
@autobind @autobind
handleSearch(text: string) { handleSearch(inputValue: string) {
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮 // text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
if (text) { this.setState({inputValue}, () => {
this.setState( if (inputValue) {
{ // 如果有取消搜索,先取消掉。
inputValue: text this.cancelSearch && this.cancelSearch();
}, this.lazySearch();
() => { } else {
// 如果有取消搜索,先取消掉。 this.handleSeachCancel();
this.cancelSearch && this.cancelSearch(); }
this.lazySearch(text); });
}
);
} else {
this.handleSeachCancel();
}
} }
@autobind @autobind
@ -196,34 +250,75 @@ export class Transfer<
} }
lazySearch = debounce( lazySearch = debounce(
(text: string) => { async () => {
(async (text: string) => { const {inputValue} = this.state;
const onSearch = this.props.onSearch!; if (!inputValue) {
let result = await onSearch( return;
text, }
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor) const onSearch = this.props.onSearch!;
); let result = await onSearch(
inputValue,
(cancelExecutor: () => void) => (this.cancelSearch = cancelExecutor)
);
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
throw new Error('onSearch 需要返回数组'); throw new Error('onSearch 需要返回数组');
} }
this.setState({ this.setState({
searchResult: result searchResult: result
}); });
})(text).catch(e => console.error(e));
}, },
250, 250,
{ {trailing: true, leading: false}
trailing: true,
leading: false
}
); );
getFlattenArr(options: Array<Option>) {
return flattenTree(options).filter(
(option, index, list) =>
!option.disabled &&
option.value !== void 0 &&
list.indexOf(option) === index
);
}
// 树搜索处理
@autobind
handleSearchTreeChange(values: Array<Option>, searchOptions: Array<Option>) {
const {onChange, value} = this.props;
const searchAvailableOptions = this.getFlattenArr(searchOptions);
const useArr = intersectionWith(
searchAvailableOptions,
values,
(a, b) => a.value === b.value
);
const unuseArr = differenceWith(
searchAvailableOptions,
values,
(a, b) => a.value === b.value
);
const newArr: Array<Option> = [];
Array.isArray(value) &&
value.forEach(item => {
if (!unuseArr.find(v => v.value === item.value)) {
newArr.push(item);
}
});
useArr.forEach(item => {
if (!newArr.find(v => v.value === item.value)) {
newArr.push(item);
}
});
onChange && onChange(newArr);
}
renderSelect( renderSelect(
props: TransferProps & { props: TransferProps & {
onToggleAll?: () => void; onToggleAll?: () => void;
@ -238,7 +333,8 @@ export class Transfer<
disabled, disabled,
options, options,
statistics, statistics,
translate: __ translate: __,
searchPlaceholder = __('Transfer.searchKeyword')
} = props; } = props;
if (selectRender) { if (selectRender) {
@ -250,6 +346,16 @@ export class Transfer<
}); });
} }
let checkedPartial = false;
let checkedAll = false;
checkedAll = this.availableOptions.every(
option => this.valueArray.indexOf(option) > -1
);
checkedPartial = this.availableOptions.some(
option => this.valueArray.indexOf(option) > -1
);
return ( return (
<> <>
<div <div
@ -259,14 +365,23 @@ export class Transfer<
)} )}
> >
<span> <span>
{includes(['list', 'tree'], selectMode) ? (
<Checkbox
checked={checkedPartial}
partial={checkedPartial && !checkedAll}
onChange={props.onToggleAll || this.toggleAll}
size="sm"
/>
) : null}
{__(selectTitle || 'Transfer.available')} {__(selectTitle || 'Transfer.available')}
{statistics !== false ? ( {statistics !== false ? (
<span> <span>
{this.valueArray.length}/{this.availableOptions.length} {this.availableOptions.length - this.valueArray.length}/
{this.availableOptions.length}
</span> </span>
) : null} ) : null}
</span> </span>
{selectMode !== 'table' ? ( {includes(['chained', 'associated'], selectMode) ? (
<a <a
onClick={props.onToggleAll || this.toggleAll} onClick={props.onToggleAll || this.toggleAll}
className={cx( className={cx(
@ -284,9 +399,9 @@ export class Transfer<
<InputBox <InputBox
value={this.state.inputValue} value={this.state.inputValue}
onChange={this.handleSearch} onChange={this.handleSearch}
placeholder={__('Transfer.searchKeyword')}
clearable={false} clearable={false}
onKeyDown={this.handleSearchKeyDown} onKeyDown={this.handleSearchKeyDown}
placeholder={searchPlaceholder}
> >
{this.state.searchResult !== null ? ( {this.state.searchResult !== null ? (
<a onClick={this.handleSeachCancel}> <a onClick={this.handleSeachCancel}>
@ -322,7 +437,8 @@ export class Transfer<
cellRender, cellRender,
multiple multiple
} = props; } = props;
const options = this.state.searchResult || []; const {isTreeDeferLoad, searchResult} = this.state;
const options = searchResult ?? [];
const mode = searchResultMode || selectMode; const mode = searchResultMode || selectMode;
const resultColumns = searchResultColumns || columns; const resultColumns = searchResultColumns || columns;
@ -341,16 +457,22 @@ export class Transfer<
multiple={multiple} multiple={multiple}
/> />
) : mode === 'tree' ? ( ) : mode === 'tree' ? (
<TreeSelection <Tree
onRef={this.domRef}
placeholder={noResultsText} placeholder={noResultsText}
className={cx('Transfer-selection')} className={cx('Transfer-selection')}
options={options} options={options}
value={value} value={value}
disabled={disabled} disabled={disabled}
onChange={onChange} onChange={(value: Array<any>) =>
option2value={option2value} this.handleSearchTreeChange(value, options)
}
joinValues={false}
showIcon={false}
multiple={true}
cascade={true}
onlyChildren={!isTreeDeferLoad}
itemRender={optionItemRender} itemRender={optionItemRender}
multiple={multiple}
/> />
) : mode === 'chained' ? ( ) : mode === 'chained' ? (
<ChainedSelection <ChainedSelection
@ -396,7 +518,8 @@ export class Transfer<
cellRender, cellRender,
leftDefaultValue, leftDefaultValue,
optionItemRender, optionItemRender,
multiple multiple,
noResultsText
} = props; } = props;
return selectMode === 'table' ? ( return selectMode === 'table' ? (
@ -413,16 +536,20 @@ export class Transfer<
multiple={multiple} multiple={multiple}
/> />
) : selectMode === 'tree' ? ( ) : selectMode === 'tree' ? (
<TreeSelection <Tree
onRef={this.domRef}
placeholder={noResultsText}
className={cx('Transfer-selection')} className={cx('Transfer-selection')}
options={options || []} options={options}
value={value} value={value}
disabled={disabled} onChange={onChange!}
onChange={onChange} onlyChildren={!this.state.isTreeDeferLoad}
option2value={option2value}
onDeferLoad={onDeferLoad}
itemRender={optionItemRender} itemRender={optionItemRender}
multiple={multiple} onDeferLoad={onDeferLoad}
joinValues={false}
showIcon={false}
multiple={true}
cascade={true}
/> />
) : selectMode === 'chained' ? ( ) : selectMode === 'chained' ? (
<ChainedSelection <ChainedSelection
@ -468,21 +595,90 @@ export class Transfer<
); );
} }
renderResult() {
const {
columns,
options,
disabled,
option2value,
classnames: cx,
cellRender,
onChange,
value,
resultItemRender,
resultSearchable,
resultSearchPlaceholder,
onResultSearch,
sortable,
translate: __
} = this.props;
const {resultSelectMode, isTreeDeferLoad} = this.state;
const searchable = !isTreeDeferLoad && resultSearchable;
const placeholder = resultSearchPlaceholder || __('Transfer.selectFromLeft');
return resultSelectMode === 'table'
? (
<ResultTableList
classnames={cx}
columns={columns!}
options={options || []}
value={value}
disabled={disabled}
option2value={option2value}
cellRender={cellRender}
onChange={onChange}
multiple={false}
searchable={searchable}
placeholder={placeholder}
onSearch={onResultSearch}
/>)
: resultSelectMode === 'tree'
? (
<ResultTreeList
classnames={cx}
options={options}
valueField={'value'}
value={value || []}
onChange={onChange!}
itemRender={resultItemRender}
searchable={searchable}
placeholder={placeholder}
onSearch={onResultSearch}
/>
): (
<ResultList
className={cx('Transfer-value')}
sortable={sortable}
disabled={disabled}
value={value}
onChange={onChange}
placeholder={placeholder}
itemRender={resultItemRender}
columns={columns!}
options={options || []}
option2value={option2value}
cellRender={cellRender}
searchable={searchable}
onSearch={onResultSearch}
/>);
}
render() { render() {
const { const {
inline, inline,
classnames: cx, classnames: cx,
className, className,
value, value,
onChange,
resultTitle, resultTitle,
sortable,
options, options,
option2value, option2value,
disabled, disabled,
statistics, statistics,
showArrow, showArrow,
resultItemRender, resultListModeFollowSelect,
selectMode = 'list',
translate: __ translate: __
} = this.props; } = this.props;
@ -494,6 +690,8 @@ export class Transfer<
list.indexOf(option) === index list.indexOf(option) === index
); );
const tableType = resultListModeFollowSelect && selectMode === 'table';
return ( return (
<div <div
className={cx('Transfer', className, inline ? 'Transfer--inline' : '')} className={cx('Transfer', className, inline ? 'Transfer--inline' : '')}
@ -509,13 +707,16 @@ export class Transfer<
) : null} ) : null}
</div> </div>
<div className={cx('Transfer-result')}> <div className={cx('Transfer-result')}>
<div className={cx('Transfer-title')}> <div
className={cx(
'Transfer-title',
tableType ? 'Transfer-table-title' : ''
)}
>
<span> <span>
{__(resultTitle || 'Transfer.selectd')} {__(resultTitle || 'Transfer.selectd')}
{statistics !== false ? ( {statistics !== false ? (
<span> <span>{this.valueArray.length}</span>
{this.valueArray.length}/{this.availableOptions.length}
</span>
) : null} ) : null}
</span> </span>
<a <a
@ -528,15 +729,7 @@ export class Transfer<
{__('clear')} {__('clear')}
</a> </a>
</div> </div>
<ResultList {this.renderResult()}
className={cx('Transfer-value')}
sortable={sortable}
disabled={disabled}
value={value}
onChange={onChange}
placeholder={__('Transfer.selectFromLeft')}
itemRender={resultItemRender}
/>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,129 @@
/**
* Tranasfer搜索
*/
import React from 'react';
import {debounce} from 'lodash';
import {ThemeProps, themeable} from '../theme';
import {Icon} from './icons';
import {autobind} from '../utils/helper';
import {LocaleProps, localeable} from '../locale';
import InputBox from './InputBox';
export interface TransferSearchProps
extends ThemeProps,
LocaleProps {
className?: string;
placeholder: string;
onSearch: Function;
onCancelSearch: Function
}
export interface ItemRenderStates {
index: number;
disabled?: boolean;
onChange: (value: any, name: string) => void;
}
interface TransferSearchState {
inputValue: string;
}
export class TransferSearch extends React.Component<
TransferSearchProps,
TransferSearchState
> {
static itemRender(option: any) {
return <span>{`${option.scopeLabel || ''}${option.label}`}</span>;
}
static defaultProps: Pick<TransferSearchProps, 'placeholder'> = {
placeholder: 'placeholder.selectData'
};
state: TransferSearchState = {
inputValue: ''
};
cancelSearch?: () => void;
componentWillUnmount() {
this.lazySearch.cancel();
}
@autobind
handleSearch(inputValue: string) {
// text 有值的时候,走搜索否则直接走 handleSeachCancel ,等同于右侧的 clear 按钮
this.setState({inputValue}, () => {
if (inputValue) {
// 如果有取消搜索,先取消掉。
this.cancelSearch && this.cancelSearch();
this.lazySearch();
} else {
this.handleSeachCancel();
}
});
}
lazySearch = debounce(
() => {
const {inputValue} = this.state;
// 防止由于防抖导致空值问题
if (!inputValue) {
return;
}
const {onSearch} = this.props;
onSearch!(inputValue);
},
250,
{trailing: true, leading: false}
);
@autobind
handleSearchKeyDown(e: React.KeyboardEvent<any>) {
if (e.key === 'Enter') {
e.preventDefault();
}
}
@autobind
handleSeachCancel() {
this.props.onCancelSearch?.();
this.setState({
inputValue: ''
});
}
render() {
const {
classnames: cx,
translate: __,
placeholder = __('Transfer.searchKeyword')
} = this.props;
const {inputValue} = this.state;
return (
<div className={cx('Transfer-search')}>
<InputBox
value={inputValue}
onChange={this.handleSearch}
clearable={false}
onKeyDown={this.handleSearchKeyDown}
placeholder={placeholder}
>
{!!inputValue ? (
<a onClick={this.handleSeachCancel}>
<Icon icon="close" className="icon" />
</a>
) : (
<Icon icon="search" className="icon" />
)}
</InputBox>
</div>
);
}
}
export default themeable(localeable(TransferSearch));

View File

@ -23,6 +23,7 @@ import {Icon, getIcon} from './icons';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import {LocaleProps, localeable} from '../locale'; import {LocaleProps, localeable} from '../locale';
import Spinner from './Spinner'; import Spinner from './Spinner';
import {ItemRenderStates} from './Selection';
interface IDropIndicator { interface IDropIndicator {
left: number; left: number;
@ -41,7 +42,7 @@ export interface IDropInfo {
interface TreeSelectorProps extends ThemeProps, LocaleProps { interface TreeSelectorProps extends ThemeProps, LocaleProps {
highlightTxt?: string; highlightTxt?: string;
onRef: any; onRef?: any;
showIcon?: boolean; showIcon?: boolean;
// 是否默认都展开 // 是否默认都展开
@ -124,6 +125,7 @@ interface TreeSelectorProps extends ThemeProps, LocaleProps {
onExpandTree?: (nodePathArr: any[]) => void; onExpandTree?: (nodePathArr: any[]) => void;
draggable?: boolean; draggable?: boolean;
onMove?: (dropInfo: IDropInfo) => void; onMove?: (dropInfo: IDropInfo) => void;
itemRender?: (option: Option, states: ItemRenderStates) => JSX.Element;
} }
interface TreeSelectorState { interface TreeSelectorState {
@ -806,6 +808,7 @@ export class TreeSelector extends React.Component<
editTip, editTip,
removeTip, removeTip,
translate: __, translate: __,
itemRender,
draggable draggable
} = this.props; } = this.props;
const { const {
@ -967,7 +970,15 @@ export class TreeSelector extends React.Component<
> >
{highlightTxt {highlightTxt
? highlight(`${item[labelField]}`, highlightTxt) ? highlight(`${item[labelField]}`, highlightTxt)
: `${item[labelField]}`} : itemRender ? (
itemRender(item, {
index: key,
multiple: multiple,
checked: checked,
onChange: () => this.handleCheck(item, !selfChecked),
disabled: disabled || item.disabled
}))
: `${item[labelField]}`}
</span> </span>
{!nodeDisabled && {!nodeDisabled &&

View File

@ -1,9 +1,11 @@
import React from 'react';
import find from 'lodash/find';
import { import {
OptionsControlProps, OptionsControlProps,
OptionsControl, OptionsControl,
FormOptionsControl FormOptionsControl
} from './Options'; } from './Options';
import React from 'react';
import Transfer, {Transfer as BaseTransfer} from '../../components/Transfer'; import Transfer, {Transfer as BaseTransfer} from '../../components/Transfer';
import type {Option} from './Options'; import type {Option} from './Options';
import { import {
@ -16,9 +18,7 @@ import {
getTree, getTree,
spliceTree spliceTree
} from '../../utils/helper'; } from '../../utils/helper';
import {Api} from '../../types';
import Spinner from '../../components/Spinner'; import Spinner from '../../components/Spinner';
import find from 'lodash/find';
import {optionValueCompare} from '../../components/Select'; import {optionValueCompare} from '../../components/Select';
import {resolveVariable} from '../../utils/tpl-builtin'; import {resolveVariable} from '../../utils/tpl-builtin';
import {SchemaApi, SchemaObject} from '../../Schema'; import {SchemaApi, SchemaObject} from '../../Schema';
@ -51,6 +51,11 @@ export interface TransferControlSchema extends FormOptionsControl {
*/ */
selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated'; selectMode?: 'table' | 'list' | 'tree' | 'chained' | 'associated';
/**
*
*/
resultListModeFollowSelect?: boolean;
/** /**
* selectMode associated * selectMode associated
*/ */
@ -86,6 +91,11 @@ export interface TransferControlSchema extends FormOptionsControl {
*/ */
searchable?: boolean; searchable?: boolean;
/**
* true时
*/
resultSearchable?: boolean;
/** /**
* API * API
*/ */
@ -110,6 +120,16 @@ export interface TransferControlSchema extends FormOptionsControl {
* *
*/ */
valueTpl?: SchemaObject; valueTpl?: SchemaObject;
/**
*
*/
searchPlaceholder?: string;
/**
*
*/
resultSearchPlaceholder?: string;
} }
export interface BaseTransferProps export interface BaseTransferProps
@ -271,6 +291,13 @@ export class BaseTransferRenderer<
} }
} }
@autobind
handleResultSearch(term: string, item: Option) {
const {valueField} = this.props;
const regexp = string2regExp(term);
return regexp.test(item[(valueField as string) || 'value']);
}
@autobind @autobind
optionItemRender(option: Option, states: ItemRenderStates) { optionItemRender(option: Option, states: ItemRenderStates) {
const {menuTpl, render, data} = this.props; const {menuTpl, render, data} = this.props;
@ -351,7 +378,7 @@ export class BaseTransferRenderer<
} }
render() { render() {
const { let {
className, className,
classnames: cx, classnames: cx,
selectedOptions, selectedOptions,
@ -370,7 +397,10 @@ export class BaseTransferRenderer<
selectTitle, selectTitle,
resultTitle, resultTitle,
menuTpl, menuTpl,
resultItemRender searchPlaceholder,
resultListModeFollowSelect = false,
resultSearchPlaceholder,
resultSearchable = false
} = this.props; } = this.props;
// 目前 LeftOptions 没有接口可以动态加载 // 目前 LeftOptions 没有接口可以动态加载
@ -412,6 +442,11 @@ export class BaseTransferRenderer<
cellRender={this.renderCell} cellRender={this.renderCell}
selectTitle={selectTitle} selectTitle={selectTitle}
resultTitle={resultTitle} resultTitle={resultTitle}
resultListModeFollowSelect={resultListModeFollowSelect}
onResultSearch={this.handleResultSearch}
searchPlaceholder={searchPlaceholder}
resultSearchable={resultSearchable}
resultSearchPlaceholder={resultSearchPlaceholder}
optionItemRender={this.optionItemRender} optionItemRender={this.optionItemRender}
resultItemRender={this.resultItemRender} resultItemRender={this.resultItemRender}
onSelectAll={this.onSelectAll} onSelectAll={this.onSelectAll}