* add transfer

* Transfer: add English doc

* Transfer: add tests

* update locale files
pull/4526/head
杨奕 2017-04-28 16:15:49 +08:00 committed by baiyaaaaa
parent 349894d107
commit ec3895fdba
43 changed files with 1557 additions and 6 deletions

View File

@ -71,11 +71,11 @@ export default {
},
{
filename: path.join('../../examples/docs/zh-CN', `${componentname}.md`),
content: `## ${chineseName}`
content: `## ${ComponentName} ${chineseName}`
},
{
filename: path.join('../../examples/docs/en-US', `${componentname}.md`),
content: `## ${componentname}`
content: `## ${ComponentName}`
},
{
filename: path.join('../../test/unit/specs', `${componentname}.spec.js`),

View File

@ -61,5 +61,6 @@
"collapse": "./packages/collapse/index.js",
"collapse-item": "./packages/collapse-item/index.js",
"cascader": "./packages/cascader/index.js",
"color-picker": "./packages/color-picker/index.js"
"color-picker": "./packages/color-picker/index.js",
"transfer": "./packages/transfer/index.js"
}

View File

@ -0,0 +1,275 @@
<script>
export default {
data() {
const generateData = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `Option ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
const generateData2 = _ => {
const data = [];
const states = ['California', 'Illinois', 'Maryland', 'Texas', 'Florida', 'Colorado', 'Connecticut '];
const initials = ['CA', 'IL', 'MD', 'TX', 'FL', 'CO', 'CT'];
states.forEach((city, index) => {
data.push({
label: city,
key: index,
initial: initials[index]
});
});
return data;
};
const generateData3 = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
value: i,
desc: `Option ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data: generateData(),
data2: generateData2(),
data3: generateData3(),
value1: [1, 4],
value2: [],
value3: [1],
value4: [],
filterMethod(query, item) {
return item.initial.toLowerCase().indexOf(query.toLowerCase()) > -1;
},
renderFunc(h, option) {
return <span>{ option.key } - { option.label }</span>;
}
};
},
methods: {
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys);
}
}
};
</script>
## Transfer
### Basic usage
:::demo Data is passed to Transfer via the `data` attribute. The data needs to be an object array, and each object should have these attributes: `key` being the identification of the data item, `label` being the displayed text, and `disabled` indicating if the data item is disabled. Items inside the target list are in sync with the variable binding to `v-model`, and the value of that variable is an array of target item keys. So, if you don't want the target list be initially empty, you can initialize the `v-model` with an array.
```html
<template>
<el-transfer
v-model="value1"
:data="data">
</el-transfer>
</template>
<script>
export default {
data() {
const generateData = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `Option ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data: generateData(),
value1: [1, 4]
};
}
};
</script>
```
:::
### Filterable
You can search and filter data items.
:::demo Set the `filterable` attribute to `true` to enable filter mode. By default, if the data item `label` contains the search keyword, it will be included in the search result. Also, you can implement you own filter method with the `filter-method` attribute. It takes a method and passes search keyword and each data item to it whenever the keyword changes. For a certain data item, if the method returns true, it will be included in the result list.
```html
<template>
<el-transfer
filterable
:filter-method="filterMethod"
filter-placeholder="State Abbreviations"
v-model="value2"
:data="data2">
</el-transfer>
</template>
<script>
export default {
data() {
const generateData2 = _ => {
const data = [];
const states = ['California', 'Illinois', 'Maryland', 'Texas', 'Florida', 'Colorado', 'Connecticut '];
const initials = ['CA', 'IL', 'MD', 'TX', 'FL', 'CO', 'CT'];
states.forEach((city, index) => {
data.push({
label: city,
key: index,
initial: initials[index]
});
});
return data;
};
return {
data2: generateData2(),
value2: [],
filterMethod(query, item) {
return item.initial.toLowerCase().indexOf(query.toLowerCase()) > -1;
}
};
}
};
</script>
```
:::
### Customizable
You can customize list titles, button texts, render function for data items, checking status texts in list footer and list footer contents.
:::demo Use `titles`, `button-texts`, `render-content` and `footer-format` to respectively customize list titles, button texts, render function for data items, checking status texts in list footer. For list footer contents, two named slots are provided: `left-footer` and `right-footer`. Plus, if you want some items initially checked, you can use `left-default-checked` and `right-default-checked`. Finally, this example demonstrate the `change` event. Note that this demo can't run in jsfiddle because it doesn't support JSX syntax. In a real project, `render-content` will work if relevant dependencies are correctly configured.
```html
<template>
<el-transfer
v-model="value3"
filterable
:left-default-checked="[2, 3]"
:right-default-checked="[1]"
:render-content="renderFunc"
:titles="['Source', 'Target']"
:button-texts="['To left', 'To right']"
:footer-format="{
noChecked: '${total}',
hasChecked: '${checked}/${total}'
}"
@change="handleChange"
:data="data">
<el-button class="transfer-footer" slot="left-footer" size="small">Operation</el-button>
<el-button class="transfer-footer" slot="right-footer" size="small">Operation</el-button>
</el-transfer>
</template>
<style>
.transfer-footer {
margin-left: 20px;
padding: 6px 5px;
}
</style>
<script>
export default {
data() {
const generateData = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `Option ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data: generateData(),
value3: [1],
renderFunc(h, option) {
return <span>{ option.key } - { option.label }</span>;
}
};
},
methods: {
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys);
}
}
};
</script>
```
:::
### Prop aliases
By default, Transfer looks for `key`, `label` and `disabled` in a data item. If your data items have different key names, you can use the `props` attribute to define aliases.
:::demo The data items in this example do not have `key`s or `label`s, instead they have `value`s and `desc`s. So you need to set aliases for `key` and `label`.
```html
<template>
<el-transfer
v-model="value4"
:props="{
key: 'value',
label: 'desc'
}"
:data="data3">
</el-transfer>
</template>
<script>
export default {
data() {
const generateData3 = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
value: i,
desc: `Option ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data3: generateData3(),
value4: []
};
}
};
</script>
```
:::
### Attributes
| Attribute | Description | Type | Accepted Values | Default |
|---------- |-------- |---------- |------------- |-------- |
| data | data source | array[{ key, label, disabled }] | — | [ ] |
| filterable | whether Transfer is filterable | boolean | — | false |
| filter-placeholder | placeholder for the filter input | string | — | Enter keyword |
| filter-method | custom filter method | function | — | — |
| titles | custom list titles | array | — | ['List 1', 'List 2'] |
| button-texts | custom button texts | array | — | [ ] |
| render-content | custom render function for data items | function(h, option) | — | — |
| footer-format | texts for checking status in list footer | object{noChecked, hasChecked} | — | { noChecked: '${total} items', hasChecked: '${checked}/${total} checked' } |
| props | prop aliases for data source | object{key, label, disabled} | — | — |
| left-default-checked | key array of initially checked data items of the left list | array | — | [ ] |
| right-default-checked | key array of initially checked data items of the right list | array | — | [ ] |
### Slot
| Name | Description |
|------|--------|
| left-footer | content of left list footer |
| right-footer | content of right list footer |
### Events
| Event Name | Description | Parameters |
|---------- |-------- |---------- |
| change | triggers when data items change in the right list | key array of current data items in the right list, transfer direction (left or right), moved item keys |

View File

@ -0,0 +1,281 @@
<style>
.demo-transfer {
.transfer-footer {
margin-left: 20px;
padding: 6px 5px;
}
}
</style>
<script>
export default {
data() {
const generateData = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `备选项 ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
const generateData2 = _ => {
const data = [];
const cities = ['上海', '北京', '广州', '深圳', '南京', '西安', '成都'];
const pinyin = ['shanghai', 'beijing', 'guangzhou', 'shenzhen', 'nanjing', 'xian', 'chengdu'];
cities.forEach((city, index) => {
data.push({
label: city,
key: index,
pinyin: pinyin[index]
});
});
return data;
};
const generateData3 = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
value: i,
desc: `备选项 ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data: generateData(),
data2: generateData2(),
data3: generateData3(),
value1: [1, 4],
value2: [],
value3: [1],
value4: [],
filterMethod(query, item) {
return item.pinyin.indexOf(query) > -1;
},
renderFunc(h, option) {
return <span>{ option.key } - { option.label }</span>;
}
};
},
methods: {
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys);
}
}
};
</script>
## Transfer 穿梭框
### 基础用法
:::demo Transfer 的数据通过 `data` 属性传入。数据需要是一个对象数组,每个对象有以下属性:`key` 为数据的唯一性标识,`label` 为显示文本,`disabled` 表示该项数据是否禁止转移。目标列表中的数据项会同步到绑定至 `v-model` 的变量,值为数据项的 `key` 所组成的数组。当然,如果希望在初始状态时目标列表不为空,可以像本例一样为 `v-model` 绑定的变量赋予一个初始值。
```html
<template>
<el-transfer v-model="value1" :data="data"></el-transfer>
</template>
<script>
export default {
data() {
const generateData = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `备选项 ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data: generateData(),
value1: [1, 4]
};
}
};
</script>
```
:::
### 可搜索
在数据很多的情况下,可以对数据进行搜索和过滤。
:::demo 设置 `filterable``true` 即可开启搜索模式。默认情况下,若数据项的 `label` 属性包含搜索关键字,则会在搜索结果中显示。你也可以使用 `filter-method` 定义自己的搜索逻辑。`filter-method` 接收一个方法,当搜索关键字变化时,会将当前的关键字和每个数据项传给该方法。若方法返回 `true`,则会在搜索结果中显示对应的数据项。
```html
<template>
<el-transfer
filterable
:filter-method="filterMethod"
filter-placeholder="请输入城市拼音"
v-model="value2"
:data="data2">
</el-transfer>
</template>
<script>
export default {
data() {
const generateData2 = _ => {
const data = [];
const cities = ['上海', '北京', '广州', '深圳', '南京', '西安', '成都'];
const pinyin = ['shanghai', 'beijing', 'guangzhou', 'shenzhen', 'nanjing', 'xian', 'chengdu'];
cities.forEach((city, index) => {
data.push({
label: city,
key: index,
pinyin: pinyin[index]
});
});
return data;
};
return {
data2: generateData2(),
value2: [],
filterMethod(query, item) {
return item.pinyin.indexOf(query) > -1;
}
};
}
};
</script>
```
:::
### 可自定义
可以对列表标题文案、按钮文案、数据项的渲染函数、列表底部的勾选状态文案、列表底部的内容区等进行自定义。
:::demo 可以使用 `titles`、`button-texts`、`render-content` 和 `footer-format` 属性分别对列表标题文案、按钮文案、数据项的渲染函数和列表底部的勾选状态文案进行自定义。对于列表底部的内容区,提供了两个具名 slot`left-footer` 和 `right-footer`。此外,如果希望某些数据项在初始化时就被勾选,可以使用 `left-default-checked``right-default-checked` 属性。最后,本例还展示了 `change` 事件的用法。注意:由于 jsfiddle 不支持 JSX 语法,所以本例在 jsfiddle 中无法运行。但是在实际的项目中,只要正确地配置了相关依赖,就可以正常运行。
```html
<template>
<el-transfer
v-model="value3"
filterable
:left-default-checked="[2, 3]"
:right-default-checked="[1]"
:render-content="renderFunc"
:titles="['Source', 'Target']"
:button-texts="['到左边', '到右边']"
:footer-format="{
noChecked: '${total}',
hasChecked: '${checked}/${total}'
}"
@change="handleChange"
:data="data">
<el-button class="transfer-footer" slot="left-footer" size="small">操作</el-button>
<el-button class="transfer-footer" slot="right-footer" size="small">操作</el-button>
</el-transfer>
</template>
<style>
.transfer-footer {
margin-left: 20px;
padding: 6px 5px;
}
</style>
<script>
export default {
data() {
const generateData = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `备选项 ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data: generateData(),
value3: [1],
renderFunc(h, option) {
return <span>{ option.key } - { option.label }</span>;
}
};
},
methods: {
handleChange(value, direction, movedKeys) {
console.log(value, direction, movedKeys);
}
}
};
</script>
```
:::
### 数据项属性别名
默认情况下Transfer 仅能识别数据项中的 `key`、`label` 和 `disabled` 字段。如果你的数据的字段名不同,可以使用 `props` 属性为它们设置别名。
:::demo 本例中的数据源没有 `key``label` 字段,在功能上与它们相同的字段名为 `value``desc`。因此可以使用`props` 属性为 `key``label` 设置别名。
```html
<template>
<el-transfer
v-model="value4"
:props="{
key: 'value',
label: 'desc'
}"
:data="data3">
</el-transfer>
</template>
<script>
export default {
data() {
const generateData3 = _ => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
value: i,
desc: `备选项 ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
return {
data3: generateData3(),
value4: []
};
}
};
</script>
```
:::
### Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------- |---------- |------------- |-------- |
| data | Transfer 的数据源 | array[{ key, label, disabled }] | — | [ ] |
| filterable | 是否可搜索 | boolean | — | false |
| filter-placeholder | 搜索框占位符 | string | — | 请输入搜索内容 |
| filter-method | 自定义搜索方法 | function | — | — |
| titles | 自定义列表标题 | array | — | ['列表 1', '列表 2'] |
| button-texts | 自定义按钮文案 | array | — | [ ] |
| render-content | 自定义数据项渲染函数 | function(h, option) | — | — |
| footer-format | 列表底部勾选状态文案 | object{noChecked, hasChecked} | — | { noChecked: '共 ${total} 项', hasChecked: '已选 ${checked}/${total} 项' } |
| props | 数据源的字段别名 | object{key, label, disabled} | — | — |
| left-default-checked | 初始状态下左侧列表的已勾选项的 key 数组 | array | — | [ ] |
| right-default-checked | 初始状态下右侧列表的已勾选项的 key 数组 | array | — | [ ] |
### Slot
| name | 说明 |
|------|--------|
| left-footer | 左侧列表底部的内容 |
| right-footer | 右侧列表底部的内容 |
### Events
| 事件名称 | 说明 | 回调参数 |
|---------- |-------- |---------- |
| change | 右侧列表元素变化时触发 | 当前值、数据移动的方向('left' / 'right')、发生移动的数据 key 数组 |

View File

@ -116,6 +116,10 @@
"path": "/color-picker",
"title": "ColorPicker 颜色选择器"
},
{
"path": "/transfer",
"title": "Transfer 穿梭框"
},
{
"path": "/form",
"title": "Form 表单"
@ -350,6 +354,10 @@
"path": "/color-picker",
"title": "ColorPicker"
},
{
"path": "/transfer",
"title": "Transfer"
},
{
"path": "/form",
"title": "Form"

View File

@ -589,4 +589,17 @@
--collapse-content-fill: var(--color-dark-white);
--collapse-content-size: 13px;
--collapse-content-color: var(--color-base-black);
/* Transfer
--------------------------*/
--transfer-border-color: var(--color-base-gray);
--transfer-box-shadow: var(--box-shadow-base);
--transfer-panel-width: 200px;
--transfer-panel-header-height: 36px;
--transfer-panel-header-background: var(--color-dark-white);
--transfer-panel-footer-height: 36px;
--transfer-panel-body-height: 246px;
--transfer-item-height: 32px;
--transfer-item-hover-background: var(--color-light-gray);
--transfer-filter-height: 22px;
}

View File

@ -59,3 +59,4 @@
@import "./collapse-item.css";
@import "./cascader.css";
@import "./color-picker.css";
@import "./transfer.css";

View File

@ -0,0 +1,168 @@
@charset "UTF-8";
@import "./common/var.css";
@import "./input.css";
@import "./button.css";
@import "./checkbox.css";
@import "./checkbox-group.css";
@component-namespace el {
@b transfer {
font-size: var(--font-size-base);
@e buttons {
display: inline-block;
vertical-align: middle;
padding: 0 10px;
.el-button {
display: block;
margin: 0 auto;
padding: 8px 12px;
&:first-child {
margin-bottom: 6px;
}
}
.el-button [class*="el-icon-"] + span {
margin-left: 0;
}
}
}
@b transfer-panel {
border: 1px solid var(--transfer-border-color);
background: var(--color-white);
box-shadow: var(--transfer-box-shadow);
display: inline-block;
vertical-align: middle;
width: var(--transfer-panel-width);
box-sizing: border-box;
position: relative;
@e body {
padding-bottom: var(--transfer-panel-footer-height);
height: var(--transfer-panel-body-height);
}
@e list {
margin: 0;
padding: 6px 0;
list-style: none;
height: var(--transfer-panel-body-height);
overflow: auto;
box-sizing: border-box;
@when filterable {
height: calc(var(--transfer-panel-body-height) - var(--transfer-filter-height) - 10px);
}
}
@e item {
height: var(--transfer-item-height);
line-height: var(--transfer-item-height);
padding-left: 20px;
display: block;
& + .el-transfer-panel__item {
margin-left: 0;
}
&.el-checkbox {
color: var(--color-extra-light-black);
}
&:hover {
background: var(--transfer-item-hover-background);
}
.el-checkbox__label {
width: 100%;
@utils-ellipsis;
display: block;
box-sizing: border-box;
padding-left: 28px;
}
.el-checkbox__input {
position: absolute;
top: 9px;
}
}
@e filter {
margin-top: 10px;
text-align: center;
padding: 0 10px;
width: 100%;
box-sizing: border-box;
.el-input__inner {
height: var(--transfer-filter-height);
width: 100%;
display: inline-block;
box-sizing: border-box;
}
.el-input__icon {
right: 10px;
}
.el-icon-circle-close {
cursor: pointer;
}
}
.el-transfer-panel__header {
height: var(--transfer-panel-header-height);
line-height: var(--transfer-panel-header-height);
background: var(--transfer-panel-header-background);
margin: 0;
padding-left: 20px;
border-bottom: 1px solid var(--transfer-border-color);
box-sizing: border-box;
color: var(--color-base-black);
}
.el-transfer-panel__footer {
height: var(--transfer-panel-footer-height);
background: var(--color-white);
margin: 0;
padding: 0;
border-top: 1px solid var(--transfer-border-color);
position: absolute;
bottom: 0;
left: 0;
width: 100%;
z-index: var(--index-normal);
@utils-vertical-center;
.el-checkbox {
padding-left: 20px;
color: var(--color-base-silver);
}
}
.el-transfer-panel__empty {
margin: 0;
height: var(--transfer-item-height);
line-height: var(--transfer-item-height);
padding: 6px 20px 0;
color: var(--color-base-silver);
}
.el-checkbox__label {
padding-left: 14px;
}
.el-checkbox__inner {
size: 14px;
border-radius: 3px;
&::after {
height: 6px;
width: 3px;
left: 4px;
}
}
}
}

View File

@ -0,0 +1,8 @@
import Transfer from './src/main';
/* istanbul ignore next */
Transfer.install = function(Vue) {
Vue.component(Transfer.name, Transfer);
};
export default Transfer;

View File

@ -0,0 +1,184 @@
<template>
<div class="el-transfer">
<transfer-panel
:filterable="filterable"
:filter-method="filterMethod"
:data="sourceData"
:render-content="renderContent"
:title="titles[0] || t('el.transfer.titles.0')"
:format="footerFormat"
:default-checked="leftDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
:props="props"
@checked-change="onSourceCheckedChange">
<slot name="left-footer"></slot>
</transfer-panel>
<div class="el-transfer__buttons">
<el-button
type="primary"
size="small"
@click.native="addToLeft"
:disabled="rightChecked.length === 0">
<i class="el-icon-arrow-left"></i>
<span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
</el-button>
<el-button
type="primary"
size="small"
@click.native="addToRight"
:disabled="leftChecked.length === 0">
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
<i class="el-icon-arrow-right"></i>
</el-button>
</div>
<transfer-panel
:filterable="filterable"
:filter-method="filterMethod"
:data="targetData"
:render-content="renderContent"
:title="titles[1] || t('el.transfer.titles.1')"
:format="footerFormat"
:default-checked="rightDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
:props="props"
@checked-change="onTargetCheckedChange">
<slot name="right-footer"></slot>
</transfer-panel>
</div>
</template>
<script>
import ElButton from 'element-ui/packages/button';
import Emitter from 'element-ui/src/mixins/emitter';
import Locale from 'element-ui/src/mixins/locale';
import TransferPanel from './transfer-panel.vue';
export default {
name: 'ElTransfer',
mixins: [Emitter, Locale],
components: {
TransferPanel,
ElButton
},
props: {
data: {
type: Array,
default() {
return [];
}
},
titles: {
type: Array,
default() {
return [];
}
},
buttonTexts: {
type: Array,
default() {
return [];
}
},
filterPlaceholder: {
type: String,
default: ''
},
filterMethod: Function,
leftDefaultChecked: {
type: Array,
default() {
return [];
}
},
rightDefaultChecked: {
type: Array,
default() {
return [];
}
},
renderContent: Function,
value: {
type: Array,
default() {
return [];
}
},
footerFormat: {
type: Object,
default() {
return {};
}
},
filterable: Boolean,
props: {
type: Object,
default() {
return {
label: 'label',
key: 'key',
disabled: 'disabled'
};
}
}
},
data() {
return {
leftChecked: [],
rightChecked: []
};
},
computed: {
sourceData() {
return this.data.filter(item => this.value.indexOf(item[this.props.key]) === -1);
},
targetData() {
return this.data.filter(item => this.value.indexOf(item[this.props.key]) > -1);
}
},
watch: {
value(val) {
this.dispatch('ElFormItem', 'el.form.change', val);
}
},
methods: {
onSourceCheckedChange(val) {
this.leftChecked = val;
},
onTargetCheckedChange(val) {
this.rightChecked = val;
},
addToLeft() {
let currentValue = this.value.slice();
this.rightChecked.forEach(item => {
const index = currentValue.indexOf(item);
if (index > -1) {
currentValue.splice(index, 1);
}
});
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'left', this.rightChecked);
},
addToRight() {
let currentValue = this.value.slice();
this.leftChecked.forEach(item => {
if (this.value.indexOf(item) === -1) {
currentValue = currentValue.concat(item);
}
});
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'right', this.leftChecked);
}
}
};
</script>

View File

@ -0,0 +1,229 @@
<template>
<div class="el-transfer-panel">
<p class="el-transfer-panel__header">{{ title }}</p>
<div class="el-transfer-panel__body">
<el-input
class="el-transfer-panel__filter"
v-model="query"
size="small"
:placeholder="placeholder"
:icon="inputIcon"
@mouseenter.native="inputHover = true"
@mouseleave.native="inputHover = false"
@click="clearQuery"
v-if="filterable"></el-input>
<el-checkbox-group
v-model="checked"
v-show="!hasNoMatch && data.length > 0"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list">
<el-checkbox
class="el-transfer-panel__item"
:label="item[keyProp]"
:disabled="item[disabledProp]"
v-for="item in filteredData">
<option-content :option="item"></option-content>
</el-checkbox>
</el-checkbox-group>
<p
class="el-transfer-panel__empty"
v-show="hasNoMatch">{{ t('el.transfer.noMatch') }}</p>
<p
class="el-transfer-panel__empty"
v-show="data.length === 0 && !hasNoMatch">{{ t('el.transfer.noData') }}</p>
</div>
<p class="el-transfer-panel__footer">
<el-checkbox
v-model="allChecked"
@change="handleAllCheckedChange"
:indeterminate="isIndeterminate">{{ checkedSummary }}</el-checkbox>
<slot></slot>
</p>
</div>
</template>
<script>
import ElCheckboxGroup from 'element-ui/packages/checkbox-group';
import ElCheckbox from 'element-ui/packages/checkbox';
import ElInput from 'element-ui/packages/input';
import Locale from 'element-ui/src/mixins/locale';
export default {
mixins: [Locale],
name: 'ElTransferPanel',
componentName: 'ElTransferPanel',
components: {
ElCheckboxGroup,
ElCheckbox,
ElInput,
OptionContent: {
props: {
option: Object
},
render(h) {
const getParent = vm => {
if (vm.$options.componentName === 'ElTransferPanel') {
return vm;
} else if (vm.$parent) {
return getParent(vm.$parent);
} else {
return vm;
}
};
const parent = getParent(this);
return parent.renderContent
? parent.renderContent(h, this.option)
: <span>{ this.option[parent.labelProp] || this.option[parent.keyProp] }</span>;
}
}
},
props: {
data: {
type: Array,
default() {
return [];
}
},
renderContent: Function,
placeholder: String,
title: String,
filterable: Boolean,
format: Object,
filterMethod: Function,
defaultChecked: Array,
props: Object
},
data() {
return {
checked: [],
allChecked: false,
query: '',
inputHover: false
};
},
watch: {
checked(val) {
this.updateAllChecked();
this.$emit('checked-change', val);
},
data() {
const checked = [];
const filteredDataKeys = this.filteredData.map(item => item[this.keyProp]);
this.checked.forEach(item => {
if (filteredDataKeys.indexOf(item) > -1) {
checked.push(item);
}
});
this.checked = checked;
},
checkableData() {
this.updateAllChecked();
},
defaultChecked: {
immediate: true,
handler(val, oldVal) {
if (oldVal && val.length === oldVal.length &&
val.every(item => oldVal.indexOf(item) > -1)) return;
const checked = [];
const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
val.forEach(item => {
if (checkableDataKeys.indexOf(item) > -1) {
checked.push(item);
}
});
this.checked = checked;
}
}
},
computed: {
filteredData() {
return this.data.filter(item => {
if (typeof this.filterMethod === 'function') {
return this.filterMethod(this.query, item);
} else {
const label = item[this.labelProp] || item[this.keyProp].toString();
return label.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
}
});
},
checkableData() {
return this.filteredData.filter(item => !item[this.disabledProp]);
},
checkedSummary() {
const checkedLength = this.checked.length;
const dataLength = this.data.length;
const { noChecked, hasChecked } = this.format;
if (noChecked && hasChecked) {
return checkedLength > 0
? hasChecked.replace(/\${checked}/g, checkedLength).replace(/\${total}/g, dataLength)
: noChecked.replace(/\${total}/g, dataLength);
} else {
return checkedLength > 0
? this.t('el.transfer.hasCheckedFormat', { total: dataLength, checked: checkedLength })
: this.t('el.transfer.noCheckedFormat', { total: dataLength });
}
},
isIndeterminate() {
const checkedLength = this.checked.length;
return checkedLength > 0 && checkedLength < this.checkableData.length;
},
hasNoMatch() {
return this.query.length > 0 && this.filteredData.length === 0;
},
inputIcon() {
return this.query.length > 0 && this.inputHover
? 'circle-close'
: 'search';
},
labelProp() {
return this.props.label || 'label';
},
keyProp() {
return this.props.key || 'key';
},
disabledProp() {
return this.props.disabled || 'disabled';
}
},
methods: {
updateAllChecked() {
const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
this.allChecked = checkableDataKeys.length > 0 &&
checkableDataKeys.every(item => this.checked.indexOf(item) > -1);
},
handleAllCheckedChange(value) {
this.checked = value.target.checked
? this.checkableData.map(item => item[this.keyProp])
: [];
},
clearQuery() {
if (this.inputIcon === 'circle-close') {
this.query = '';
}
}
}
};
</script>

View File

@ -63,6 +63,7 @@ import Collapse from '../packages/collapse/index.js';
import CollapseItem from '../packages/collapse-item/index.js';
import Cascader from '../packages/cascader/index.js';
import ColorPicker from '../packages/color-picker/index.js';
import Transfer from '../packages/transfer/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
@ -125,6 +126,7 @@ const components = [
Collapse,
CollapseItem,
Cascader,
Transfer,
ColorPicker,
CollapseTransition
];
@ -223,5 +225,6 @@ module.exports = {
Collapse,
CollapseItem,
Cascader,
ColorPicker
ColorPicker,
Transfer
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Няма данни'
},
transfer: {
noMatch: 'Няма намерени',
noData: 'Няма данни',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -89,6 +89,14 @@ export default {
},
tree: {
emptyText: 'Sense Dades'
},
transfer: {
noMatch: 'No hi ha dades que coincideixin',
noData: 'Sense Dades',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -92,6 +92,14 @@ export default {
},
tree: {
emptyText: 'Žádná data'
},
transfer: {
noMatch: 'Žádná shoda',
noData: 'Žádná data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -89,6 +89,14 @@ export default {
},
tree: {
emptyText: 'Ingen data'
},
transfer: {
noMatch: 'Ingen matchende data',
noData: 'Ingen data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -91,6 +91,14 @@ export default {
},
tree: {
emptyText: 'Keine Daten'
},
transfer: {
noMatch: 'Nichts gefunden.',
noData: 'Keine Datei',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -60,6 +60,10 @@ export default {
noData: 'Χωρίς δεδομένα',
placeholder: 'Επιλογή'
},
cascader: {
noMatch: 'Δεν βρέθηκαν αποτελέσματα',
placeholder: 'Επιλογή'
},
pagination: {
goto: 'Μετάβαση σε',
pagesize: '/σελίδα',
@ -86,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Χωρίς Δεδομένα'
},
transfer: {
noMatch: 'Δεν βρέθηκαν αποτελέσματα',
noData: 'Χωρίς δεδομένα',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'No Data'
},
transfer: {
noMatch: 'No matching data',
noData: 'No data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -89,6 +89,14 @@ export default {
},
tree: {
emptyText: 'Sin Datos'
},
transfer: {
noMatch: 'No hay datos que coincidan',
noData: 'Sin datos',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'اطلاعاتی وجود ندارد'
},
transfer: {
noMatch: 'هیچ داده‌ای پیدا نشد',
noData: 'اطلاعاتی وجود ندارد',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -60,6 +60,10 @@ export default {
noData: 'Ei tietoja',
placeholder: 'Valitse'
},
cascader: {
noMatch: 'Ei vastaavia tietoja',
placeholder: 'Valitse'
},
pagination: {
goto: 'Mene',
pagesize: '/sivu',
@ -86,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Ei tietoja'
},
transfer: {
noMatch: 'Ei vastaavia tietoja',
noData: 'Ei tietoja',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -57,7 +57,7 @@ export default {
select: {
loading: 'Chargement',
noMatch: 'Aucune correspondance',
noData: 'Aucun résultat',
noData: 'Aucune donnée',
placeholder: 'Choisir'
},
cascader: {
@ -89,6 +89,14 @@ export default {
},
tree: {
emptyText: 'Aucune donnée'
},
transfer: {
noMatch: 'Aucune correspondance',
noData: 'Aucune donnée',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Tidak Ada Data'
},
transfer: {
noMatch: 'Tidak ada data yang cocok',
noData: 'Tidak ada data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -57,7 +57,7 @@ export default {
select: {
loading: 'Caricamento',
noMatch: 'Nessuna corrispondenza',
noData: 'Nessun risultato',
noData: 'Nessun dato',
placeholder: 'Seleziona'
},
cascader: {
@ -89,6 +89,14 @@ export default {
},
tree: {
emptyText: 'Nessun dato'
},
transfer: {
noMatch: 'Nessuna corrispondenza',
noData: 'Nessun dato',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'データなし'
},
transfer: {
noMatch: 'データなし',
noData: 'データなし',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: '데이터 없음'
},
transfer: {
noMatch: '맞는 데이터가 없습니다',
noData: '데이터 없음',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -89,6 +89,14 @@ export default {
},
tree: {
emptyText: 'Ingen Data'
},
transfer: {
noMatch: 'Ingen samsvarende data',
noData: 'Ingen data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Geen data'
},
transfer: {
noMatch: 'Geen overeenkomende resultaten',
noData: 'Geen data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Brak danych'
},
transfer: {
noMatch: 'Brak dopasowań',
noData: 'Brak danych',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Sem dados'
},
transfer: {
noMatch: 'Sem resultados',
noData: 'Sem dados',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Sem dados'
},
transfer: {
noMatch: 'Sem correspondência',
noData: 'Sem dados',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Нет данных'
},
transfer: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -62,6 +62,10 @@ export default {
noData: 'Žiadne dáta',
placeholder: 'Vybrať'
},
cascader: {
noMatch: 'Žiadna zhoda',
placeholder: 'Vybrať'
},
pagination: {
goto: 'Choď na',
pagesize: 'na stranu',
@ -88,6 +92,14 @@ export default {
},
tree: {
emptyText: 'Žiadne dáta'
},
transfer: {
noMatch: 'Žiadna zhoda',
noData: 'Žiadne dáta',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -60,6 +60,10 @@ export default {
noData: 'Ingen data',
placeholder: 'Välj'
},
cascader: {
noMatch: 'Hittade inget',
placeholder: 'Välj'
},
pagination: {
goto: 'Gå till',
pagesize: '/sida',
@ -86,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Inga Data'
},
transfer: {
noMatch: 'Hittade inget',
noData: 'Ingen data',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'ไม่พบข้อมูล'
},
transfer: {
noMatch: 'ไม่พบข้อมูลที่ตรงกัน',
noData: 'ไม่พบข้อมูล',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Maglumat ýok'
},
transfer: {
noMatch: 'Hiçzat tapylmady',
noData: 'Hiçzat ýok',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Veri yok'
},
transfer: {
noMatch: 'Eşleşen veri bulunamadı',
noData: 'Veri yok',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Нема даних'
},
transfer: {
noMatch: 'Співпадінь не знайдено',
noData: 'Обрати',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: 'Không có dữ liệu'
},
transfer: {
noMatch: 'Dữ liệu không phù hợp',
noData: 'Không tìm thấy dữ liệu',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: '暂无数据'
},
transfer: {
noMatch: '无匹配数据',
noData: '无数据',
titles: ['列表 1', '列表 2'],
filterPlaceholder: '请输入搜索内容',
noCheckedFormat: '共 {total} 项',
hasCheckedFormat: '已选 {checked}/{total} 项'
}
}
};

View File

@ -90,6 +90,14 @@ export default {
},
tree: {
emptyText: '暫無資料'
},
transfer: {
noMatch: '無匹配資料',
noData: '無資料',
titles: ['List 1', 'List 2'], // to be translated
filterPlaceholder: 'Enter keyword', // to be translated
noCheckedFormat: '{total} items', // to be translated
hasCheckedFormat: '{checked}/{total} checked' // to be translated
}
}
};

View File

@ -0,0 +1,124 @@
import { createTest, createVue, destroyVM } from '../util';
import Transfer from 'packages/transfer';
describe('Transfer', () => {
let vm;
const getTestData = () => {
const data = [];
for (let i = 1; i <= 15; i++) {
data.push({
key: i,
label: `备选项 ${ i }`,
disabled: i % 4 === 0
});
}
return data;
};
const createTransfer = (props, opts) => {
return createVue(Object.assign({
template: `
<el-transfer :data="testData" ref="transfer" ${props}>
</el-transfer>
`,
created() {
this.testData = getTestData();
}
}, opts));
};
afterEach(() => {
destroyVM(vm);
});
it('create', () => {
vm = createTest(Transfer, true);
expect(vm.$el).to.exist;
});
it('default target list', () => {
vm = createTransfer('v-model="value"', {
data() {
return {
value: [1, 4]
};
}
});
expect(vm.$refs.transfer.sourceData.length).to.equal(13);
});
it('filterable', done => {
vm = createTransfer('v-model="value" filterable :filter-method="method"', {
data() {
return {
value: [],
method(query, option) {
return option.key === Number(query);
}
};
}
});
const transfer = vm.$refs.transfer;
const leftList = transfer.$el.querySelector('.el-transfer-panel').__vue__;
leftList.query = '1';
setTimeout(_ => {
expect(leftList.filteredData.length).to.equal(1);
done();
}, 50);
});
it('transfer', done => {
vm = createTransfer('v-model="value" :left-default-checked="[2, 3]" :right-default-checked="[1]"', {
data() {
return {
value: [1, 4]
};
}
});
const transfer = vm.$refs.transfer;
setTimeout(_ => {
transfer.addToLeft();
setTimeout(_ => {
expect(transfer.sourceData.length).to.equal(14);
transfer.addToRight();
setTimeout(_ => {
expect(transfer.sourceData.length).to.equal(12);
done();
}, 50);
}, 50);
}, 50);
});
it('customize', () => {
vm = createTransfer('v-model="value" :titles="titles" :render-content="renderFunc" :footer-format="format"', {
data() {
return {
value: [2],
titles: ['1', '2'],
format: { noChecked: 'no', hasChecked: 'has' },
renderFunc(h, option) {
return <span>{ option.key } - { option.label }</span>;
}
};
}
});
const transfer = vm.$refs.transfer.$el;
expect(transfer.querySelector('.el-transfer-panel__header').innerText).to.equal('1');
expect(transfer.querySelector('.el-checkbox__label span').innerText).to.equal('1 - 备选项 1');
expect(transfer.querySelector('.el-transfer-panel__footer .el-checkbox__label').innerText).to.equal('no');
});
it('check', () => {
vm = createTransfer('v-model="value"', {
data() {
return {
value: []
};
}
});
const leftList = vm.$refs.transfer.$el.querySelector('.el-transfer-panel').__vue__;
leftList.handleAllCheckedChange({ target: { checked: true } });
expect(leftList.checked.length).to.equal(12);
});
});