From 8f732fc4216fca01923a290a38c3a1638f54c4c9 Mon Sep 17 00:00:00 2001
From: tangjinzhou <415800467@qq.com>
Date: Fri, 28 Feb 2020 16:04:37 +0800
Subject: [PATCH] feat: transfer support custom children
---
build/config.js | 2 +-
components/_util/getTransitionProps.js | 3 +-
.../transfer/{item.jsx => ListItem.jsx} | 4 +-
.../__tests__/__snapshots__/demo.test.js.snap | 281 +++++++++++++++--
.../__snapshots__/index.test.js.snap | 30 +-
.../__tests__/__snapshots__/list.test.js.snap | 11 +-
components/transfer/__tests__/search.test.js | 2 +-
components/transfer/demo/index.vue | 12 +-
components/transfer/demo/table-transfer.md | 155 ++++++++++
components/transfer/index.en-US.md | 82 ++++-
components/transfer/index.jsx | 250 ++++++++-------
components/transfer/index.zh-CN.md | 82 ++++-
components/transfer/list.jsx | 290 +++++++++++-------
components/transfer/renderListBody.jsx | 108 +++++++
components/transfer/style/index.less | 2 +-
15 files changed, 1021 insertions(+), 293 deletions(-)
rename components/transfer/{item.jsx => ListItem.jsx} (94%)
create mode 100644 components/transfer/demo/table-transfer.md
create mode 100644 components/transfer/renderListBody.jsx
diff --git a/build/config.js b/build/config.js
index 0ccb888ad..a7b14a769 100644
--- a/build/config.js
+++ b/build/config.js
@@ -1,5 +1,5 @@
module.exports = {
dev: {
- componentName: 'tabs', // dev components
+ componentName: 'transfer', // dev components
},
};
diff --git a/components/_util/getTransitionProps.js b/components/_util/getTransitionProps.js
index 19d7a312a..623060c8d 100644
--- a/components/_util/getTransitionProps.js
+++ b/components/_util/getTransitionProps.js
@@ -1,7 +1,7 @@
import animate from './css-animation';
const noop = () => {};
const getTransitionProps = (transitionName, opt = {}) => {
- const { beforeEnter, enter, afterEnter, leave, afterLeave, appear = true, tag } = opt;
+ const { beforeEnter, enter, afterEnter, leave, afterLeave, appear = true, tag, nativeOn } = opt;
const transitionProps = {
props: {
appear,
@@ -22,6 +22,7 @@ const getTransitionProps = (transitionName, opt = {}) => {
}),
afterLeave: afterLeave || noop,
},
+ nativeOn,
};
// transition-group
if (tag) {
diff --git a/components/transfer/item.jsx b/components/transfer/ListItem.jsx
similarity index 94%
rename from components/transfer/item.jsx
rename to components/transfer/ListItem.jsx
index c76bd829a..90bf42173 100644
--- a/components/transfer/item.jsx
+++ b/components/transfer/ListItem.jsx
@@ -6,7 +6,7 @@ import Checkbox from '../checkbox';
function noop() {}
export default {
- name: 'Item',
+ name: 'ListItem',
props: {
renderedText: PropTypes.any,
renderedEl: PropTypes.any,
@@ -42,7 +42,7 @@ export default {
}
>
- {renderedEl}
+ {renderedEl}
);
let children = null;
diff --git a/components/transfer/__tests__/__snapshots__/demo.test.js.snap b/components/transfer/__tests__/__snapshots__/demo.test.js.snap
index 6e7642872..df2c17eef 100644
--- a/components/transfer/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/transfer/__tests__/__snapshots__/demo.test.js.snap
@@ -8,7 +8,7 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
-
+
@@ -29,7 +29,7 @@ exports[`renders ./components/transfer/demo/advanced.md correctly 1`] = `
-
+
@@ -59,7 +59,7 @@ exports[`renders ./components/transfer/demo/basic.md correctly 1`] = `
-
+
@@ -74,7 +74,7 @@ exports[`renders ./components/transfer/demo/basic.md correctly 1`] = `
-
+
@@ -85,14 +85,14 @@ exports[`renders ./components/transfer/demo/basic.md correctly 1`] = `
enabled
@@ -104,7 +104,7 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = `
-
+
@@ -125,7 +125,7 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = `
-
+
@@ -137,8 +137,16 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = `
-
@@ -155,7 +163,7 @@ exports[`renders ./components/transfer/demo/large-data.md correctly 1`] = `
-
+
@@ -2156,7 +2164,7 @@ exports[`renders ./components/transfer/demo/large-data.md correctly 1`] = `
-
+
@@ -2171,8 +2179,16 @@ exports[`renders ./components/transfer/demo/large-data.md correctly 1`] = `
-
@@ -2189,7 +2205,7 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
-
+
@@ -2210,7 +2226,7 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
-
+
@@ -2225,8 +2241,16 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
-
@@ -2234,3 +2258,224 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = `
`;
+
+exports[`renders ./components/transfer/demo/table-transfer.md correctly 1`] = `
+
+`;
diff --git a/components/transfer/__tests__/__snapshots__/index.test.js.snap b/components/transfer/__tests__/__snapshots__/index.test.js.snap
index 98bab0925..b32fec2be 100644
--- a/components/transfer/__tests__/__snapshots__/index.test.js.snap
+++ b/components/transfer/__tests__/__snapshots__/index.test.js.snap
@@ -4,9 +4,12 @@ exports[`Transfer should render correctly 1`] = `
@@ -15,7 +18,11 @@ exports[`Transfer should render correctly 1`] = `
`;
@@ -24,7 +31,11 @@ exports[`Transfer should show sorted targetkey 1`] = `
@@ -33,9 +44,12 @@ exports[`Transfer should show sorted targetkey 1`] = `
`;
diff --git a/components/transfer/__tests__/__snapshots__/list.test.js.snap b/components/transfer/__tests__/__snapshots__/list.test.js.snap
index 4f5da4fa6..ffedae977 100644
--- a/components/transfer/__tests__/__snapshots__/list.test.js.snap
+++ b/components/transfer/__tests__/__snapshots__/list.test.js.snap
@@ -3,9 +3,12 @@
exports[`List should render correctly 1`] = `
`;
diff --git a/components/transfer/__tests__/search.test.js b/components/transfer/__tests__/search.test.js
index 47309dd57..1512da2e8 100644
--- a/components/transfer/__tests__/search.test.js
+++ b/components/transfer/__tests__/search.test.js
@@ -108,7 +108,7 @@ describe('Search', () => {
input.trigger('input');
expect(errorSpy.mock.calls[0][0]).toMatch(
- 'Warning: `searchChange` in Transfer is deprecated. Please use `search` instead.',
+ 'Warning: [antdv: Transfer] `searchChange` in Transfer is deprecated. Please use `search` instead.',
);
expect(onSearchChange.mock.calls[0][0]).toEqual('left');
expect(onSearchChange.mock.calls[0][1].target.value).toEqual('a');
diff --git a/components/transfer/demo/index.vue b/components/transfer/demo/index.vue
index 22bda1441..4b3fda214 100644
--- a/components/transfer/demo/index.vue
+++ b/components/transfer/demo/index.vue
@@ -3,6 +3,7 @@ import Basic from './basic.md';
import Search from './search.md';
import Advanced from './advanced.md';
import CustomItem from './custom-item.md';
+import TableTransfer from './table-transfer';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
@@ -44,10 +45,13 @@ export default {
return (
-
-
-
-
+
+
+
+
+
+
+
diff --git a/components/transfer/demo/table-transfer.md b/components/transfer/demo/table-transfer.md
new file mode 100644
index 000000000..1ec93be79
--- /dev/null
+++ b/components/transfer/demo/table-transfer.md
@@ -0,0 +1,155 @@
+
+#### 表格穿梭框
+使用 Table 组件作为自定义渲染列表。
+
+
+
+#### Table Transfer
+Customize render list with Table component.
+
+
+```tpl
+
+
+
+
+```
diff --git a/components/transfer/index.en-US.md b/components/transfer/index.en-US.md
index 3103a4fb6..6f870432f 100644
--- a/components/transfer/index.en-US.md
+++ b/components/transfer/index.en-US.md
@@ -1,20 +1,21 @@
## API
-| Property | Description | Type | Default |
-| --- | --- | --- | --- |
-| dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop. | \[{key: string.isRequired,title: string.isRequired,description: string,disabled: bool}\] | \[] |
-| disabled | Whether disabled transfer | boolean | false |
-| filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | |
-| footer | customize the progress dot by setting a scoped slot | slot="footer" slot-scope="props" | |
-| lazy | property of vc-lazy-load for lazy rendering items. Turn off it by set to `false`. | object\|boolean | `{ height: 32, offset: 32 }` |
-| listStyle | A custom CSS style used for rendering the transfer columns. | object | |
-| locale | i18n text including filter, empty text, item unit, etc | object | `{ itemUnit: 'item', itemsUnit: 'items', notFoundContent: 'The list is empty', searchPlaceholder: 'Search here' }` |
-| operations | A set of operations that are sorted from top to bottom. | string\[] | \['>', '<'] |
-| render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a element and `value` is for title | Function(record) | |
-| selectedKeys | A set of keys of selected items. | string\[] | \[] |
-| showSearch | If included, a search box is shown on each column. | boolean | false |
-| targetKeys | A set of keys of elements that are listed on the right column. | string\[] | \[] |
-| titles | A set of titles that are sorted from left to right. | string\[] | - |
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop. | \[{key: string.isRequired,title: string.isRequired,description: string,disabled: bool}\] | \[] | |
+| disabled | Whether disabled transfer | boolean | false | |
+| filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | | |
+| footer | customize the progress dot by setting a scoped slot | slot="footer" slot-scope="props" | | |
+| lazy | property of vc-lazy-load for lazy rendering items. Turn off it by set to `false`. | object\|boolean | `{ height: 32, offset: 32 }` | |
+| listStyle | A custom CSS style used for rendering the transfer columns. | object | | |
+| locale | i18n text including filter, empty text, item unit, etc | object | `{ itemUnit: 'item', itemsUnit: 'items', notFoundContent: 'The list is empty', searchPlaceholder: 'Search here' }` | |
+| operations | A set of operations that are sorted from top to bottom. | string\[] | \['>', '<'] | |
+| render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a element and `value` is for title | Function(record) | | |
+| selectedKeys | A set of keys of selected items. | string\[] | \[] | |
+| showSearch | If included, a search box is shown on each column. | boolean | false | |
+| showSelectAll | Show select all checkbox on the header | boolean | true | 1.5.0 |
+| targetKeys | A set of keys of elements that are listed on the right column. | string\[] | \[] | |
+| titles | A set of titles that are sorted from left to right. | string\[] | - | |
### events
@@ -25,6 +26,57 @@
| search | A callback function which is executed when search field are changed | (direction: 'left'\|'right', value: string): void | - |
| selectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | |
+### Render Props
+
+New in 1.5.0. Transfer accept `children` to customize render list, using follow props:
+
+```json
+{
+ "props": {
+ "direction": String,
+ "disabled": Boolean,
+ "filteredItems": Array,
+ "selectedKeys": Array
+ },
+ "on": {
+ "itemSelect": Function,
+ "itemSelectAll": Function
+ }
+}
+```
+
+| Property | Description | Type | Version |
+| --------------- | ----------------------- | ----------------------------------- | ------- |
+| direction | List render direction | 'left' \| 'right' | 1.5.0 |
+| disabled | Disable list or not | boolean | 1.5.0 |
+| filteredItems | Filtered items | TransferItem[] | 1.5.0 |
+| onItemSelect | Select item | (key: string, selected: boolean) | 1.5.0 |
+| onItemSelectAll | Select a group of items | (keys: string[], selected: boolean) | 1.5.0 |
+| selectedKeys | Selected items | string[] | 1.5.0 |
+
+#### example
+
+```html
+
+
+
+
+
+```
+
## Warning
According the standard of Vue, the key should always be supplied directly to the elements in the array. In Transfer, the keys should be set on the elements included in `dataSource` array. By default, `key` property is used as an unique identifier.
diff --git a/components/transfer/index.jsx b/components/transfer/index.jsx
index 6e382ad5f..c8d670b27 100644
--- a/components/transfer/index.jsx
+++ b/components/transfer/index.jsx
@@ -32,7 +32,7 @@ export const TransferProps = {
targetKeys: PropTypes.arrayOf(PropTypes.string),
selectedKeys: PropTypes.arrayOf(PropTypes.string),
render: PropTypes.func,
- listStyle: PropTypes.object,
+ listStyle: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
operationStyle: PropTypes.object,
titles: PropTypes.arrayOf(PropTypes.string),
operations: PropTypes.arrayOf(PropTypes.string),
@@ -43,6 +43,7 @@ export const TransferProps = {
locale: PropTypes.object,
rowKey: PropTypes.func,
lazy: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
+ showSelectAll: PropTypes.bool,
};
export const TransferLocale = {
@@ -59,6 +60,7 @@ const Transfer = {
dataSource: [],
locale: {},
showSearch: false,
+ listStyle: () => {},
}),
inject: {
configProvider: { default: () => ConfigConsumerProps },
@@ -70,12 +72,6 @@ const Transfer = {
// 'Transfer[notFoundContent] and Transfer[searchPlaceholder] will be removed, ' +
// 'please use Transfer[locale] instead.',
// )
-
- this.separatedDataSource =
- {
- leftDataSource: [],
- rightDataSource: [],
- } | null;
const { selectedKeys = [], targetKeys = [] } = this;
return {
leftFilter: '',
@@ -112,6 +108,33 @@ const Transfer = {
},
},
methods: {
+ getSelectedKeysName(direction) {
+ return direction === 'left' ? 'sourceSelectedKeys' : 'targetSelectedKeys';
+ },
+
+ getTitles(transferLocale) {
+ if (this.titles) {
+ return this.titles;
+ }
+ return transferLocale.titles || ['', ''];
+ },
+
+ getLocale(transferLocale, renderEmpty) {
+ const h = this.$createElement;
+ // Keep old locale props still working.
+ const oldLocale = {
+ notFoundContent: renderEmpty(h, 'Transfer'),
+ };
+ const notFoundContent = getComponentFromProp(this, 'notFoundContent');
+ if (notFoundContent) {
+ oldLocale.notFoundContent = notFoundContent;
+ }
+ if (hasProp(this, 'searchPlaceholder')) {
+ oldLocale.searchPlaceholder = this.$props.searchPlaceholder;
+ }
+
+ return { ...transferLocale, ...oldLocale, ...this.$props.locale };
+ },
updateState() {
const { sourceSelectedKeys, targetSelectedKeys } = this;
this.separatedDataSource = null;
@@ -136,37 +159,6 @@ const Transfer = {
});
}
},
- separateDataSource(props) {
- if (this.separatedDataSource) {
- return this.separatedDataSource;
- }
-
- const { dataSource, rowKey, targetKeys = [] } = props;
-
- const leftDataSource = [];
- const rightDataSource = new Array(targetKeys.length);
- dataSource.forEach(record => {
- if (rowKey) {
- record.key = rowKey(record);
- }
-
- // rightDataSource should be ordered by targetKeys
- // leftDataSource should be ordered by dataSource
- const indexOfKey = targetKeys.indexOf(record.key);
- if (indexOfKey !== -1) {
- rightDataSource[indexOfKey] = record;
- } else {
- leftDataSource.push(record);
- }
- });
-
- this.separatedDataSource = {
- leftDataSource,
- rightDataSource,
- };
-
- return this.separatedDataSource;
- },
moveTo(direction) {
const { targetKeys = [], dataSource = [] } = this.$props;
@@ -198,51 +190,61 @@ const Transfer = {
this.moveTo('right');
},
- handleSelectChange(direction, holder) {
- const { sourceSelectedKeys, targetSelectedKeys } = this;
+ onItemSelectAll(direction, selectedKeys, checkAll) {
+ const originalSelectedKeys = this.$data[this.getSelectedKeysName(direction)] || [];
- if (direction === 'left') {
- this.$emit('selectChange', holder, targetSelectedKeys);
+ let mergedCheckedKeys = [];
+ if (checkAll) {
+ // Merge current keys with origin key
+ mergedCheckedKeys = Array.from(new Set([...originalSelectedKeys, ...selectedKeys]));
} else {
- this.$emit('selectChange', sourceSelectedKeys, holder);
+ // Remove current keys from origin keys
+ mergedCheckedKeys = originalSelectedKeys.filter(key => selectedKeys.indexOf(key) === -1);
}
- },
- handleSelectAll(direction, filteredDataSource, checkAll) {
- const originalSelectedKeys = this[this.getSelectedKeysName(direction)] || [];
- const currentKeys = filteredDataSource.map(item => item.key);
- // Only operate current keys from original selected keys
- const newKeys1 = originalSelectedKeys.filter(key => currentKeys.indexOf(key) === -1);
- const newKeys2 = [...originalSelectedKeys];
- currentKeys.forEach(key => {
- if (newKeys2.indexOf(key) === -1) {
- newKeys2.push(key);
- }
- });
- const holder = checkAll ? newKeys1 : newKeys2;
- this.handleSelectChange(direction, holder);
- if (!this.selectedKeys) {
+ this.handleSelectChange(direction, mergedCheckedKeys);
+
+ if (!this.$props.selectedKeys) {
this.setState({
- [this.getSelectedKeysName(direction)]: holder,
+ [this.getSelectedKeysName(direction)]: mergedCheckedKeys,
});
}
},
- handleLeftSelectAll(filteredDataSource, checkAll) {
- this.handleSelectAll('left', filteredDataSource, checkAll);
+ handleSelectAll(direction, filteredDataSource, checkAll) {
+ this.onItemSelectAll(
+ direction,
+ filteredDataSource.map(({ key }) => key),
+ !checkAll,
+ );
},
+
+ // [Legacy] Old prop `body` pass origin check as arg. It's confusing.
+ // TODO: Remove this in next version.
+ handleLeftSelectAll(filteredDataSource, checkAll) {
+ return this.handleSelectAll('left', filteredDataSource, !checkAll);
+ },
+
handleRightSelectAll(filteredDataSource, checkAll) {
- this.handleSelectAll('right', filteredDataSource, checkAll);
+ return this.handleSelectAll('right', filteredDataSource, !checkAll);
+ },
+
+ onLeftItemSelectAll(selectedKeys, checkAll) {
+ return this.onItemSelectAll('left', selectedKeys, checkAll);
+ },
+
+ onRightItemSelectAll(selectedKeys, checkAll) {
+ return this.onItemSelectAll('right', selectedKeys, checkAll);
},
handleFilter(direction, e) {
const value = e.target.value;
- this.setState({
- // add filter
- [`${direction}Filter`]: value,
- });
if (getListeners(this).searchChange) {
- warning(false, '`searchChange` in Transfer is deprecated. Please use `search` instead.');
+ warning(
+ false,
+ 'Transfer',
+ '`searchChange` in Transfer is deprecated. Please use `search` instead.',
+ );
this.$emit('searchChange', direction, e);
}
this.$emit('search', direction, value);
@@ -256,9 +258,6 @@ const Transfer = {
},
handleClear(direction) {
- this.setState({
- [`${direction}Filter`]: '',
- });
this.$emit('search', direction, '');
},
@@ -269,15 +268,15 @@ const Transfer = {
this.handleClear('right');
},
- handleSelect(direction, selectedItem, checked) {
+ onItemSelect(direction, selectedKey, checked) {
const { sourceSelectedKeys, targetSelectedKeys } = this;
const holder = direction === 'left' ? [...sourceSelectedKeys] : [...targetSelectedKeys];
- const index = holder.indexOf(selectedItem.key);
+ const index = holder.indexOf(selectedKey);
if (index > -1) {
holder.splice(index, 1);
}
if (checked) {
- holder.push(selectedItem.key);
+ holder.push(selectedKey);
}
this.handleSelectChange(direction, holder);
@@ -288,6 +287,11 @@ const Transfer = {
}
},
+ handleSelect(direction, selectedItem, checked) {
+ warning(false, 'Transfer', '`handleSelect` will be removed, please use `onSelect` instead.');
+ this.onItemSelect(direction, selectedItem.key, checked);
+ },
+
handleLeftSelect(selectedItem, checked) {
return this.handleSelect('left', selectedItem, checked);
},
@@ -296,6 +300,13 @@ const Transfer = {
return this.handleSelect('right', selectedItem, checked);
},
+ onLeftItemSelect(selectedKey, checked) {
+ return this.onItemSelect('left', selectedKey, checked);
+ },
+ onRightItemSelect(selectedKey, checked) {
+ return this.onItemSelect('right', selectedKey, checked);
+ },
+
handleScroll(direction, e) {
this.$emit('scroll', direction, e);
},
@@ -307,32 +318,46 @@ const Transfer = {
this.handleScroll('right', e);
},
- getTitles(transferLocale) {
- if (this.titles) {
- return this.titles;
+ handleSelectChange(direction, holder) {
+ const { sourceSelectedKeys, targetSelectedKeys } = this;
+
+ if (direction === 'left') {
+ this.$emit('selectChange', holder, targetSelectedKeys);
+ } else {
+ this.$emit('selectChange', sourceSelectedKeys, holder);
}
- return transferLocale.titles || ['', ''];
+ },
+ handleListStyle(listStyle, direction) {
+ if (typeof listStyle === 'function') {
+ return listStyle({ direction });
+ }
+ return listStyle;
},
- getSelectedKeysName(direction) {
- return direction === 'left' ? 'sourceSelectedKeys' : 'targetSelectedKeys';
- },
+ separateDataSource() {
+ const { dataSource, rowKey, targetKeys = [] } = this.$props;
- getLocale(transferLocale, renderEmpty) {
- const h = this.$createElement;
- // Keep old locale props still working.
- const oldLocale = {
- notFoundContent: renderEmpty(h, 'Transfer'),
+ const leftDataSource = [];
+ const rightDataSource = new Array(targetKeys.length);
+ dataSource.forEach(record => {
+ if (rowKey) {
+ record.key = rowKey(record);
+ }
+
+ // rightDataSource should be ordered by targetKeys
+ // leftDataSource should be ordered by dataSource
+ const indexOfKey = targetKeys.indexOf(record.key);
+ if (indexOfKey !== -1) {
+ rightDataSource[indexOfKey] = record;
+ } else {
+ leftDataSource.push(record);
+ }
+ });
+
+ return {
+ leftDataSource,
+ rightDataSource,
};
- const notFoundContent = getComponentFromProp(this, 'notFoundContent');
- if (notFoundContent) {
- oldLocale.notFoundContent = notFoundContent;
- }
- if (hasProp(this, 'searchPlaceholder')) {
- oldLocale.searchPlaceholder = this.$props.searchPlaceholder;
- }
-
- return { ...transferLocale, ...oldLocale, ...this.$props.locale };
},
renderTransfer(transferLocale) {
@@ -346,55 +371,59 @@ const Transfer = {
operationStyle,
filterOption,
lazy,
+ showSelectAll,
} = props;
+ const children = getComponentFromProp(this, 'children', {}, false);
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
const renderEmpty = this.configProvider.renderEmpty;
const locale = this.getLocale(transferLocale, renderEmpty);
- const {
- leftFilter,
- rightFilter,
- sourceSelectedKeys,
- targetSelectedKeys,
- $scopedSlots,
- } = this;
+ const { sourceSelectedKeys, targetSelectedKeys, $scopedSlots } = this;
const { body, footer } = $scopedSlots;
const renderItem = props.render;
- const { leftDataSource, rightDataSource } = this.separateDataSource(this.$props);
+ const { leftDataSource, rightDataSource } = this.separateDataSource();
const leftActive = targetSelectedKeys.length > 0;
const rightActive = sourceSelectedKeys.length > 0;
- const cls = classNames(prefixCls, disabled && `${prefixCls}-disabled`);
-
+ const cls = classNames(prefixCls, {
+ [`${prefixCls}-disabled`]: disabled,
+ [`${prefixCls}-customize-list`]: !!children,
+ });
const titles = this.getTitles(locale);
return (
', '<'] |
-| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 element。或者返回一个普通对象,其中 `label` 字段为 element,`value` 字段为 title | Function(record) | |
-| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] |
-| showSearch | 是否显示搜索框 | boolean | false |
-| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] |
-| titles | 标题集合,顺序从左至右 | string\[] | \['', ''] |
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | \[{key: string.isRequired,title: string.isRequired,description: string,disabled: bool}\]\[] | \[] | |
+| disabled | 是否禁用 | boolean | false | |
+| filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | (inputValue, option): boolean | | |
+| footer | 可以设置为一个 作用域插槽 | slot="footer" slot-scope="props" | | |
+| lazy | Transfer 使用了 [vc-lazy-load]优化性能,这里可以设置相关参数。设为 `false` 可以关闭懒加载。 | object\|boolean | `{ height: 32, offset: 32 }` | |
+| listStyle | 两个穿梭框的自定义样式 | object | | |
+| locale | 各种语言 | object | `{ itemUnit: '项', itemsUnit: '项', notFoundContent: '列表为空', searchPlaceholder: '请输入搜索内容' }` | |
+| operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] | |
+| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 element。或者返回一个普通对象,其中 `label` 字段为 element,`value` 字段为 title | Function(record) | | |
+| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | |
+| showSearch | 是否显示搜索框 | boolean | false | |
+| showSelectAll | 是否展示全选勾选框 | boolean | true | 1.5.0 |
+| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
+| titles | 标题集合,顺序从左至右 | string\[] | \['', ''] | |
### 事件
@@ -25,6 +26,57 @@
| search | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', value: string): void | - |
| selectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | |
+### Render Props
+
+1.5.0 新增。Transfer 支持接收 `children` 自定义渲染列表,并返回以下参数:
+
+```json
+{
+ "props": {
+ "direction": String,
+ "disabled": Boolean,
+ "filteredItems": Array,
+ "selectedKeys": Array
+ },
+ "on": {
+ "itemSelect": Function,
+ "itemSelectAll": Function
+ }
+}
+```
+
+| 参数 | 说明 | 类型 | 版本 |
+| ------------- | -------------- | ----------------------------------- | ----- |
+| direction | 渲染列表的方向 | 'left' \| 'right' | 1.5.0 |
+| disabled | 是否禁用列表 | boolean | 1.5.0 |
+| filteredItems | 过滤后的数据 | TransferItem[] | 1.5.0 |
+| selectedKeys | 选中的条目 | string[] | 1.5.0 |
+| itemSelect | 勾选条目 | (key: string, selected: boolean) | 1.5.0 |
+| itemSelectAll | 勾选一组条目 | (keys: string[], selected: boolean) | 1.5.0 |
+
+#### 参考示例
+
+```html
+
+
+
+
+
+```
+
## 注意
按照 Vue 最新的规范,所有的组件数组最好绑定 key。在 Transfer 中,`dataSource`里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。
diff --git a/components/transfer/list.jsx b/components/transfer/list.jsx
index fd9379795..8111509d9 100644
--- a/components/transfer/list.jsx
+++ b/components/transfer/list.jsx
@@ -1,15 +1,14 @@
import classNames from 'classnames';
import PropTypes from '../_util/vue-types';
-import { isValidElement, initDefaultProps } from '../_util/props-util';
+import { isValidElement, initDefaultProps, getListeners } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
-import getTransitionProps from '../_util/getTransitionProps';
import Checkbox from '../checkbox';
import Search from './search';
-import Item from './item';
+import defaultRenderList from './renderListBody';
import triggerEvent from '../_util/triggerEvent';
import addEventListener from '../vc-util/Dom/addEventListener';
-function noop() {}
+const defaultRender = () => null;
const TransferItem = {
key: PropTypes.string.isRequired,
@@ -44,11 +43,26 @@ export const TransferListProps = {
itemUnit: PropTypes.string,
itemsUnit: PropTypes.string,
body: PropTypes.any,
+ renderList: PropTypes.any,
footer: PropTypes.any,
lazy: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
disabled: PropTypes.bool,
+ direction: PropTypes.string,
+ showSelectAll: PropTypes.bool,
};
+function renderListNode(h, renderList, props) {
+ let bodyContent = renderList ? renderList(props) : null;
+ const customize = !!bodyContent;
+ if (!customize) {
+ bodyContent = defaultRenderList(h, props);
+ }
+ return {
+ customize,
+ bodyContent,
+ };
+}
+
export default {
name: 'TransferList',
mixins: [BaseMixin],
@@ -56,35 +70,33 @@ export default {
dataSource: [],
titleText: '',
showSearch: false,
- renderItem: noop,
lazy: {},
}),
data() {
this.timer = null;
this.triggerScrollTimer = null;
return {
- mounted: false,
+ filterValue: '',
};
},
- mounted() {
- this.timer = setTimeout(() => {
- this.setState({
- mounted: true,
- });
- }, 0);
- this.$nextTick(() => {
- if (this.$refs.listContentWrapper) {
- const listContentWrapperDom = this.$refs.listContentWrapper.$el;
- this.scrollEvent = addEventListener(listContentWrapperDom, 'scroll', this.handleScroll);
- }
- });
- },
+ // mounted() {
+ // this.timer = setTimeout(() => {
+ // this.setState({
+ // mounted: true,
+ // });
+ // }, 0);
+ // this.$nextTick(() => {
+ // if (this.$refs.listContentWrapper) {
+ // const listContentWrapperDom = this.$refs.listContentWrapper.$el;
+ // this.scrollEvent = addEventListener(listContentWrapperDom, 'scroll', this.handleScroll);
+ // }
+ // });
+ // },
beforeDestroy() {
- clearTimeout(this.timer);
clearTimeout(this.triggerScrollTimer);
- if (this.scrollEvent) {
- this.scrollEvent.remove();
- }
+ // if (this.scrollEvent) {
+ // this.scrollEvent.remove();
+ // }
},
updated() {
this.$nextTick(() => {
@@ -101,23 +113,132 @@ export default {
handleScroll(e) {
this.$emit('scroll', e);
},
- getCheckStatus(filteredDataSource) {
+ getCheckStatus(filteredItems) {
const { checkedKeys } = this.$props;
if (checkedKeys.length === 0) {
return 'none';
- } else if (filteredDataSource.every(item => checkedKeys.indexOf(item.key) >= 0)) {
+ }
+ if (filteredItems.every(item => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)) {
return 'all';
}
return 'part';
},
+
+ getFilteredItems(dataSource, filterValue) {
+ const filteredItems = [];
+ const filteredRenderItems = [];
+
+ dataSource.forEach(item => {
+ const renderedItem = this.renderItemHtml(item);
+ const { renderedText } = renderedItem;
+
+ // Filter skip
+ if (filterValue && filterValue.trim() && !this.matchFilter(renderedText, item)) {
+ return null;
+ }
+
+ filteredItems.push(item);
+ filteredRenderItems.push(renderedItem);
+ });
+
+ return { filteredItems, filteredRenderItems };
+ },
+
+ getListBody(
+ prefixCls,
+ searchPlaceholder,
+ filterValue,
+ filteredItems,
+ notFoundContent,
+ bodyDom,
+ filteredRenderItems,
+ checkedKeys,
+ renderList,
+ showSearch,
+ disabled,
+ ) {
+ const search = showSearch ? (
+
+
+
+ ) : null;
+
+ let listBody = bodyDom;
+ if (!listBody) {
+ let bodyNode;
+
+ const { bodyContent, customize } = renderListNode(this.$createElement, renderList, {
+ props: { ...this.$props, filteredItems, filteredRenderItems, selectedKeys: checkedKeys },
+ on: getListeners(this),
+ });
+
+ // We should wrap customize list body in a classNamed div to use flex layout.
+ if (customize) {
+ bodyNode = {bodyContent}
;
+ } else {
+ bodyNode = filteredItems.length ? (
+ bodyContent
+ ) : (
+ {notFoundContent}
+ );
+ }
+
+ listBody = (
+
+ {search}
+ {bodyNode}
+
+ );
+ }
+ return listBody;
+ },
+
+ getCheckBox(filteredItems, showSelectAll, disabled) {
+ const checkStatus = this.getCheckStatus(filteredItems);
+ const checkedAll = checkStatus === 'all';
+ const checkAllCheckbox = showSelectAll !== false && (
+ {
+ // Only select enabled items
+ this.$emit(
+ 'itemSelectAll',
+ filteredItems.filter(item => !item.disabled).map(({ key }) => key),
+ !checkedAll,
+ );
+ }}
+ />
+ );
+
+ return checkAllCheckbox;
+ },
+
_handleSelect(selectedItem) {
const { checkedKeys } = this.$props;
const result = checkedKeys.some(key => key === selectedItem.key);
this.handleSelect(selectedItem, !result);
},
_handleFilter(e) {
- this.handleFilter(e);
- if (!e.target.value) {
+ const { handleFilter } = this.$props;
+ const {
+ target: { value: filterValue },
+ } = e;
+ this.setState({ filterValue });
+ handleFilter(e);
+ if (!filterValue) {
return;
}
// Manually trigger scroll event for lazy search bug
@@ -131,22 +252,25 @@ export default {
}, 0);
},
_handleClear(e) {
+ this.setState({ filterValue: '' });
this.handleClear(e);
},
matchFilter(text, item) {
- const { filter, filterOption } = this.$props;
+ const { filterValue } = this.$data;
+ const { filterOption } = this.$props;
if (filterOption) {
- return filterOption(filter, item);
+ return filterOption(filterValue, item);
}
- return text.indexOf(filter) >= 0;
+ return text.indexOf(filterValue) >= 0;
},
renderItemHtml(item) {
- const { renderItem = noop } = this.$props;
+ const { renderItem = defaultRender } = this.$props;
const renderResult = renderItem(item);
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
return {
renderedText: isRenderResultPlain ? renderResult.value : renderResult,
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
+ item,
};
},
filterNull(arr) {
@@ -157,21 +281,22 @@ export default {
},
render() {
+ const { filterValue } = this.$data;
const {
prefixCls,
dataSource,
titleText,
checkedKeys,
- lazy,
disabled,
body,
footer,
showSearch,
- filter,
searchPlaceholder,
notFoundContent,
itemUnit,
itemsUnit,
+ renderList,
+ showSelectAll,
} = this.$props;
// Custom Layout
@@ -182,96 +307,31 @@ export default {
[`${prefixCls}-with-footer`]: !!footerDom,
});
- const filteredDataSource = [];
- const totalDataSource = [];
+ // ====================== Get filtered, checked item list ======================
- const showItems = dataSource.map(item => {
- const { renderedText, renderedEl } = this.renderItemHtml(item);
- if (filter && filter.trim() && !this.matchFilter(renderedText, item)) {
- return null;
- }
+ const { filteredItems, filteredRenderItems } = this.getFilteredItems(dataSource, filterValue);
- // all show items
- totalDataSource.push(item);
- if (!item.disabled) {
- // response to checkAll items
- filteredDataSource.push(item);
- }
-
- const checked = checkedKeys.indexOf(item.key) >= 0;
- return (
-
- );
- });
+ // ================================= List Body =================================
const unit = dataSource.length > 1 ? itemsUnit : itemUnit;
- const search = showSearch ? (
-
-
-
- ) : null;
- const transitionName = this.mounted ? `${prefixCls}-content-item-highlight` : '';
- const transitionProps = getTransitionProps(transitionName, {
- leave: noop,
- });
-
- const searchNotFound = showItems.every(item => item === null) && (
- {notFoundContent}
- );
- const listBody = bodyDom || (
-
- {search}
- {!searchNotFound && (
-
- {showItems}
-
- )}
- {searchNotFound}
-
+ const listBody = this.getListBody(
+ prefixCls,
+ searchPlaceholder,
+ filterValue,
+ filteredItems,
+ notFoundContent,
+ bodyDom,
+ filteredRenderItems,
+ checkedKeys,
+ renderList,
+ showSearch,
+ disabled,
);
const listFooter = footerDom ? : null;
- const checkStatus = this.getCheckStatus(filteredDataSource);
- const checkedAll = checkStatus === 'all';
- const checkAllCheckbox = (
- {
- this.handleSelectAll(filteredDataSource, checkedAll);
- }}
- />
- );
+ const checkAllCheckbox = this.getCheckBox(filteredItems, showSelectAll, disabled);
return (
@@ -279,7 +339,7 @@ export default {
{checkAllCheckbox}