mirror of
https://gitee.com/ant-design-vue/ant-design-vue.git
synced 2024-12-05 05:29:01 +08:00
add vc-lazy-load transfer
This commit is contained in:
parent
aaaa776ba0
commit
a1b671a8c4
@ -148,3 +148,5 @@ const TimelineItem = Timeline.Item
|
||||
export { Timeline, TimelineItem }
|
||||
|
||||
export { default as InputNumber } from './input-number'
|
||||
|
||||
export { default as Transfer } from './transfer'
|
||||
|
@ -39,3 +39,4 @@ import './table/style'
|
||||
import './progress/style'
|
||||
import './timeline/style'
|
||||
import './input-number/style'
|
||||
import './transfer/style'
|
||||
|
81
components/transfer/demo/advanced.md
Normal file
81
components/transfer/demo/advanced.md
Normal file
@ -0,0 +1,81 @@
|
||||
<cn>
|
||||
#### 高级用法
|
||||
穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Advanced
|
||||
You can customize the labels of the transfer buttons, the width and height of the columns, and what should be displayed in the footer.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-transfer
|
||||
:dataSource="mockData"
|
||||
showSearch
|
||||
:listStyle="{
|
||||
width: '250px',
|
||||
height: '300px',
|
||||
}"
|
||||
:operations="['to right', 'to left']"
|
||||
:targetKeys="targetKeys"
|
||||
@change="handleChange"
|
||||
:render="item=>`${item.title}-${item.description}`"
|
||||
>
|
||||
<a-button
|
||||
size="small"
|
||||
style="float:right;margin: 5px"
|
||||
@click="getMock"
|
||||
slot="footer"
|
||||
slot-scope="props"
|
||||
>
|
||||
reload
|
||||
</a-button>
|
||||
<span
|
||||
slot="notFoundContent"
|
||||
>
|
||||
没数据
|
||||
</span>
|
||||
</a-transfer>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
mockData: [],
|
||||
targetKeys: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMock()
|
||||
},
|
||||
methods: {
|
||||
getMock() {
|
||||
const targetKeys = [];
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const data = {
|
||||
key: i.toString(),
|
||||
title: `content${i + 1}`,
|
||||
description: `description of content${i + 1}`,
|
||||
chosen: Math.random() * 2 > 1,
|
||||
};
|
||||
if (data.chosen) {
|
||||
targetKeys.push(data.key);
|
||||
}
|
||||
mockData.push(data);
|
||||
}
|
||||
this.mockData = mockData
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
handleChange(targetKeys, direction, moveKeys) {
|
||||
console.log(targetKeys, direction, moveKeys);
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
69
components/transfer/demo/basic.md
Normal file
69
components/transfer/demo/basic.md
Normal file
@ -0,0 +1,69 @@
|
||||
<cn>
|
||||
#### 基本用法
|
||||
最基本的用法,展示了 `dataSource`、`targetKeys`、每行的渲染函数 `render` 以及回调函数 `onChange` `onSelectChange` `onScroll` 的用法。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Basic
|
||||
The most basic usage of `Transfer` involves providing the source data and target keys arrays, plus the rendering and some callback functions.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-transfer
|
||||
:dataSource="mockData"
|
||||
:titles="['Source', 'Target']"
|
||||
:targetKeys="targetKeys"
|
||||
:selectedKeys="selectedKeys"
|
||||
@change="handleChange"
|
||||
@selectChange="handleSelectChange"
|
||||
@scroll="handleScroll"
|
||||
:render="item=>item.title"
|
||||
>
|
||||
</a-transfer>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
mockData.push({
|
||||
key: i.toString(),
|
||||
title: `content${i + 1}`,
|
||||
description: `description of content${i + 1}`,
|
||||
disabled: i % 3 < 1,
|
||||
});
|
||||
}
|
||||
|
||||
const targetKeys = mockData
|
||||
.filter(item => +item.key % 3 > 1)
|
||||
.map(item => item.key);
|
||||
return {
|
||||
mockData,
|
||||
targetKeys,
|
||||
selectedKeys: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(nextTargetKeys, direction, moveKeys) {
|
||||
this.targetKeys = nextTargetKeys
|
||||
|
||||
console.log('targetKeys: ', this.targetKeys);
|
||||
console.log('direction: ', direction);
|
||||
console.log('moveKeys: ', moveKeys);
|
||||
},
|
||||
handleSelectChange(sourceSelectedKeys, targetSelectedKeys) {
|
||||
this.selectedKeys = [...sourceSelectedKeys, ...targetSelectedKeys]
|
||||
|
||||
console.log('sourceSelectedKeys: ', sourceSelectedKeys);
|
||||
console.log('targetSelectedKeys: ', targetSelectedKeys);
|
||||
},
|
||||
handleScroll(direction, e) {
|
||||
console.log('direction:', direction);
|
||||
console.log('target:', e.target);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
77
components/transfer/demo/custom-item.md
Normal file
77
components/transfer/demo/custom-item.md
Normal file
@ -0,0 +1,77 @@
|
||||
<cn>
|
||||
#### 自定义渲染行数据
|
||||
自定义渲染每一个 Transfer Item,可用于渲染复杂数据。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Custom datasource
|
||||
Custom each Transfer Item, and in this way you can render a complex datasource.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-transfer
|
||||
:dataSource="mockData"
|
||||
:listStyle="{
|
||||
width: '300px',
|
||||
height: '300px',
|
||||
}"
|
||||
:targetKeys="targetKeys"
|
||||
@change="handleChange"
|
||||
:render="renderItem"
|
||||
>
|
||||
</a-transfer>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
mockData: [],
|
||||
targetKeys: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMock()
|
||||
},
|
||||
methods: {
|
||||
getMock() {
|
||||
const targetKeys = [];
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const data = {
|
||||
key: i.toString(),
|
||||
title: `content${i + 1}`,
|
||||
description: `description of content${i + 1}`,
|
||||
chosen: Math.random() * 2 > 1,
|
||||
};
|
||||
if (data.chosen) {
|
||||
targetKeys.push(data.key);
|
||||
}
|
||||
mockData.push(data);
|
||||
}
|
||||
this.mockData = mockData
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
renderItem(item) {
|
||||
const customLabel = (
|
||||
<span className="custom-item">
|
||||
{item.title} - {item.description}
|
||||
</span>
|
||||
);
|
||||
|
||||
return {
|
||||
label: customLabel, // for displayed item
|
||||
value: item.title, // for title and filter matching
|
||||
};
|
||||
},
|
||||
handleChange(targetKeys, direction, moveKeys) {
|
||||
console.log(targetKeys, direction, moveKeys);
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
57
components/transfer/demo/index.vue
Normal file
57
components/transfer/demo/index.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import Basic from './basic.md'
|
||||
import Search from './search.md'
|
||||
import Advanced from './advanced.md'
|
||||
import CustomItem from './custom-item.md'
|
||||
import LargeData from './large-data.md'
|
||||
import CN from '../index.zh-CN.md'
|
||||
import US from '../index.en-US.md'
|
||||
|
||||
const md = {
|
||||
cn: `# 穿梭框
|
||||
双栏穿梭选择框。
|
||||
## 何时使用
|
||||
用直观的方式在两栏中移动元素,完成选择行为。
|
||||
|
||||
选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。
|
||||
其中,左边一栏为 'source',右边一栏为 'target',API 的设计也反映了这两个概念。。
|
||||
## 代码演示`,
|
||||
us: `# Transfer
|
||||
Transfer the elements between two columns in an intuitive and efficient way.
|
||||
|
||||
One or more elements can be selected from either column, one click on the proper 'direction' button, and the transfer is done. The left column is considered the 'source' and the right column is considered the 'target'. As you can see in the API description, these names are reflected in.
|
||||
## Examples
|
||||
`,
|
||||
}
|
||||
export default {
|
||||
category: 'Components',
|
||||
subtitle: '穿梭框',
|
||||
type: 'Data Entry',
|
||||
title: 'Transfer',
|
||||
cols: '1',
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<md cn={md.cn} us={md.us}/>
|
||||
<br/>
|
||||
<Basic />
|
||||
<br/>
|
||||
<Search />
|
||||
<br />
|
||||
<Advanced />
|
||||
<br/>
|
||||
<CustomItem />
|
||||
<br/>
|
||||
<LargeData />
|
||||
<br/>
|
||||
<api>
|
||||
<template slot='cn'>
|
||||
<CN/>
|
||||
</template>
|
||||
<US/>
|
||||
</api>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
</script>
|
60
components/transfer/demo/large-data.md
Normal file
60
components/transfer/demo/large-data.md
Normal file
@ -0,0 +1,60 @@
|
||||
<cn>
|
||||
#### 大数据性能测试
|
||||
2000 条数据。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Performance Test
|
||||
2000 items.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-transfer
|
||||
:dataSource="mockData"
|
||||
:targetKeys="targetKeys"
|
||||
@change="handleChange"
|
||||
:render="item=>item.title"
|
||||
>
|
||||
</a-transfer>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
mockData: [],
|
||||
targetKeys: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMock()
|
||||
},
|
||||
methods: {
|
||||
getMock() {
|
||||
const targetKeys = [];
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const data = {
|
||||
key: i.toString(),
|
||||
title: `content${i + 1}`,
|
||||
description: `description of content${i + 1}`,
|
||||
chosen: Math.random() * 2 > 1,
|
||||
};
|
||||
if (data.chosen) {
|
||||
targetKeys.push(data.key);
|
||||
}
|
||||
mockData.push(data);
|
||||
}
|
||||
this.mockData = mockData
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
handleChange(targetKeys, direction, moveKeys) {
|
||||
console.log(targetKeys, direction, moveKeys);
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
66
components/transfer/demo/search.md
Normal file
66
components/transfer/demo/search.md
Normal file
@ -0,0 +1,66 @@
|
||||
<cn>
|
||||
#### 带搜索框
|
||||
带搜索框的穿梭框,可以自定义搜索函数。
|
||||
</cn>
|
||||
|
||||
<us>
|
||||
#### Search
|
||||
Transfer with a search box.
|
||||
</us>
|
||||
|
||||
```html
|
||||
<template>
|
||||
<a-transfer
|
||||
:dataSource="mockData"
|
||||
showSearch
|
||||
:filterOption="filterOption"
|
||||
:targetKeys="targetKeys"
|
||||
@change="handleChange"
|
||||
:render="item=>item.title"
|
||||
>
|
||||
</a-transfer>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
mockData: [],
|
||||
targetKeys: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getMock()
|
||||
},
|
||||
methods: {
|
||||
getMock() {
|
||||
const targetKeys = [];
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const data = {
|
||||
key: i.toString(),
|
||||
title: `content${i + 1}`,
|
||||
description: `description of content${i + 1}`,
|
||||
chosen: Math.random() * 2 > 1,
|
||||
};
|
||||
if (data.chosen) {
|
||||
targetKeys.push(data.key);
|
||||
}
|
||||
mockData.push(data);
|
||||
}
|
||||
this.mockData = mockData
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
filterOption(inputValue, option) {
|
||||
return option.description.indexOf(inputValue) > -1;
|
||||
},
|
||||
handleChange(targetKeys, direction, moveKeys) {
|
||||
console.log(targetKeys, direction, moveKeys);
|
||||
this.targetKeys = targetKeys
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
48
components/transfer/index.en-US.md
Normal file
48
components/transfer/index.en-US.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
category: Components
|
||||
type: Data Entry
|
||||
cols: 1
|
||||
title: Transfer
|
||||
---
|
||||
|
||||
Double column transfer choice box.
|
||||
|
||||
## When To Use
|
||||
|
||||
Transfer the elements between two columns in an intuitive and efficient way.
|
||||
|
||||
One or more elements can be selected from either column, one click on the proper 'direction' button, and the transfer is done. The left column is considered the 'source' and the right column is considered the 'target'. As you can see in the API description, these names are reflected in.
|
||||
|
||||
## API
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ----------- | ---- | ------- |
|
||||
| className | A custom CSS class. | string | ['', ''] |
|
||||
| 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. | [TransferItem](https://git.io/vMM64)\[] | \[] |
|
||||
| filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | |
|
||||
| footer | A function used for rendering the footer. | (props): ReactNode | |
|
||||
| lazy | property of [react-lazy-load](https://github.com/loktar00/react-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 | |
|
||||
| notFoundContent | Text to display when a column is empty. | string\|ReactNode | 'The list is empty' |
|
||||
| operations | A set of operations that are sorted from bottom to top. | 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 React element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a React element and `value` is for title | Function(record) | |
|
||||
| searchPlaceholder | The hint text of the search box. | string | 'Search here' |
|
||||
| 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\[] | - |
|
||||
| onChange | A callback function that is executed when the transfer between columns is complete. | (targetKeys, direction, moveKeys): void | |
|
||||
| onScroll | A callback function which is executed when scroll options list | (direction, event): void | |
|
||||
| onSearchChange | A callback function which is executed when search field are changed | (direction: 'left'\|'right', event: Event): void | - |
|
||||
| onSelectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | |
|
||||
|
||||
## Warning
|
||||
|
||||
According the [standard](http://facebook.github.io/react/docs/lists-and-keys.html#keys) of React, 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.
|
||||
|
||||
If there's no `key` in your data, you should use `rowKey` to specify the key that will be used for uniquely identify each element.
|
||||
|
||||
```jsx
|
||||
// eg. your primary key is `uid`
|
||||
return <Transfer rowKey={record => record.uid} />;
|
||||
```
|
402
components/transfer/index.jsx
Normal file
402
components/transfer/index.jsx
Normal file
@ -0,0 +1,402 @@
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import { initDefaultProps, getOptionProps, getComponentFromProp } from '../_util/props-util'
|
||||
import BaseMixin from '../_util/BaseMixin'
|
||||
import classNames from 'classnames'
|
||||
import List from './list'
|
||||
import Operation from './operation'
|
||||
// import Search from './search'
|
||||
import LocaleReceiver from '../locale-provider/LocaleReceiver'
|
||||
import defaultLocale from '../locale-provider/default'
|
||||
|
||||
// function noop () {
|
||||
// }
|
||||
|
||||
export const TransferDirection = 'left' | 'right'
|
||||
|
||||
export const TransferItem = PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}).loose
|
||||
|
||||
export const TransferProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
dataSource: PropTypes.arrayOf(TransferItem),
|
||||
targetKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
selectedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
render: PropTypes.func,
|
||||
listStyle: PropTypes.object,
|
||||
titles: PropTypes.arrayOf(TransferItem),
|
||||
operations: PropTypes.arrayOf(TransferItem),
|
||||
showSearch: PropTypes.bool,
|
||||
filterOption: PropTypes.func,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
notFoundContent: PropTypes.any,
|
||||
rowKey: PropTypes.string,
|
||||
lazy: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
}
|
||||
|
||||
export const TransferLocale = {
|
||||
titles: PropTypes.arrayOf(PropTypes.string),
|
||||
notFoundContent: PropTypes.string,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
itemUnit: PropTypes.string,
|
||||
itemsUnit: PropTypes.string,
|
||||
}
|
||||
|
||||
const tranferProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
dataSource: PropTypes.arrayOf(TransferItem),
|
||||
targetKeys: PropTypes.array,
|
||||
listStyle: PropTypes.object,
|
||||
render: PropTypes.func,
|
||||
titles: PropTypes.array,
|
||||
operations: PropTypes.array,
|
||||
showSearch: PropTypes.bool,
|
||||
filterOption: PropTypes.func,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
notFoundContent: PropTypes.string,
|
||||
rowKey: PropTypes.func,
|
||||
lazy: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Transfer',
|
||||
mixins: [BaseMixin],
|
||||
props: initDefaultProps(tranferProps, {
|
||||
dataSource: [],
|
||||
showSearch: false,
|
||||
}),
|
||||
data () {
|
||||
this.splitedDataSource = {
|
||||
leftDataSource: [],
|
||||
rightDataSource: [],
|
||||
} | null
|
||||
const { selectedKeys = [], targetKeys = [] } = this
|
||||
return {
|
||||
leftFilter: '',
|
||||
rightFilter: '',
|
||||
sourceSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) === -1),
|
||||
targetSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) > -1),
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.currentProps = { ...this.$props }
|
||||
},
|
||||
watch: {
|
||||
'$props': {
|
||||
handler: function (nextProps) {
|
||||
const { sourceSelectedKeys, targetSelectedKeys, currentProps } = this
|
||||
|
||||
if (nextProps.targetKeys !== currentProps.targetKeys ||
|
||||
nextProps.dataSource !== currentProps.dataSource) {
|
||||
// clear cached splited dataSource
|
||||
this.splitedDataSource = null
|
||||
|
||||
if (!nextProps.selectedKeys) {
|
||||
// clear key nolonger existed
|
||||
// clear checkedKeys according to targetKeys
|
||||
const { dataSource, targetKeys = [] } = nextProps
|
||||
|
||||
const newSourceSelectedKeys = []
|
||||
const newTargetSelectedKeys = []
|
||||
dataSource.forEach(({ key }) => {
|
||||
if (sourceSelectedKeys.includes(key) && !targetKeys.includes(key)) {
|
||||
newSourceSelectedKeys.push(key)
|
||||
}
|
||||
if (targetSelectedKeys.includes(key) && targetKeys.includes(key)) {
|
||||
newTargetSelectedKeys.push(key)
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
sourceSelectedKeys: newSourceSelectedKeys,
|
||||
targetSelectedKeys: newTargetSelectedKeys,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (nextProps.selectedKeys) {
|
||||
const targetKeys = nextProps.targetKeys || []
|
||||
this.setState({
|
||||
sourceSelectedKeys: nextProps.selectedKeys.filter(key => !targetKeys.includes(key)),
|
||||
targetSelectedKeys: nextProps.selectedKeys.filter(key => targetKeys.includes(key)),
|
||||
})
|
||||
}
|
||||
this.currentProps = { ...this.$props }
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitDataSource (props) {
|
||||
if (this.splitedDataSource) {
|
||||
return this.splitedDataSource
|
||||
}
|
||||
|
||||
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.splitedDataSource = {
|
||||
leftDataSource,
|
||||
rightDataSource,
|
||||
}
|
||||
|
||||
return this.splitedDataSource
|
||||
},
|
||||
|
||||
moveTo (direction) {
|
||||
const { targetKeys = [], dataSource = [] } = this.$props
|
||||
const { sourceSelectedKeys, targetSelectedKeys } = this
|
||||
const moveKeys = direction === 'right' ? sourceSelectedKeys : targetSelectedKeys
|
||||
// filter the disabled options
|
||||
const newMoveKeys = moveKeys.filter((key) =>
|
||||
!dataSource.some(data => !!(key === data.key && data.disabled)),
|
||||
)
|
||||
// move items to target box
|
||||
const newTargetKeys = direction === 'right'
|
||||
? newMoveKeys.concat(targetKeys)
|
||||
: targetKeys.filter(targetKey => newMoveKeys.indexOf(targetKey) === -1)
|
||||
|
||||
// empty checked keys
|
||||
const oppositeDirection = direction === 'right' ? 'left' : 'right'
|
||||
this.setState({
|
||||
[this.getSelectedKeysName(oppositeDirection)]: [],
|
||||
})
|
||||
this.handleSelectChange(oppositeDirection, [])
|
||||
|
||||
this.$emit('change', newTargetKeys, direction, newMoveKeys)
|
||||
},
|
||||
moveToLeft () {
|
||||
this.moveTo('left')
|
||||
},
|
||||
moveToRight () {
|
||||
this.moveTo('right')
|
||||
},
|
||||
|
||||
handleSelectChange (direction, holder) {
|
||||
const { sourceSelectedKeys, targetSelectedKeys } = this
|
||||
|
||||
if (direction === 'left') {
|
||||
this.$emit('selectChange', holder, targetSelectedKeys)
|
||||
} else {
|
||||
this.$emit('selectChange', sourceSelectedKeys, holder)
|
||||
}
|
||||
},
|
||||
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.setState({
|
||||
[this.getSelectedKeysName(direction)]: holder,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleLeftSelectAll (filteredDataSource, checkAll) {
|
||||
this.handleSelectAll('left', filteredDataSource, checkAll)
|
||||
},
|
||||
handleRightSelectAll (filteredDataSource, checkAll) {
|
||||
this.handleSelectAll('right', filteredDataSource, checkAll)
|
||||
},
|
||||
|
||||
handleFilter (direction, e) {
|
||||
this.setState({
|
||||
// add filter
|
||||
[`${direction}Filter`]: e.target.value,
|
||||
})
|
||||
this.$emit('searchChange', direction, e)
|
||||
},
|
||||
|
||||
handleLeftFilter (e) {
|
||||
this.handleFilter('left', e)
|
||||
},
|
||||
handleRightFilter (e) {
|
||||
this.handleFilter('right', e)
|
||||
},
|
||||
|
||||
handleClear (direction) {
|
||||
this.setState({
|
||||
[`${direction}Filter`]: '',
|
||||
})
|
||||
},
|
||||
|
||||
handleLeftClear () {
|
||||
this.handleClear('left')
|
||||
},
|
||||
handleRightClear () {
|
||||
this.handleClear('right')
|
||||
},
|
||||
|
||||
handleSelect (direction, selectedItem, checked) {
|
||||
const { sourceSelectedKeys, targetSelectedKeys } = this
|
||||
const holder = direction === 'left' ? [...sourceSelectedKeys] : [...targetSelectedKeys]
|
||||
const index = holder.indexOf(selectedItem.key)
|
||||
if (index > -1) {
|
||||
holder.splice(index, 1)
|
||||
}
|
||||
if (checked) {
|
||||
holder.push(selectedItem.key)
|
||||
}
|
||||
this.handleSelectChange(direction, holder)
|
||||
|
||||
if (!this.selectedKeys) {
|
||||
this.setState({
|
||||
[this.getSelectedKeysName(direction)]: holder,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleLeftSelect (selectedItem, checked) {
|
||||
return this.handleSelect('left', selectedItem, checked)
|
||||
},
|
||||
|
||||
handleRightSelect (selectedItem, checked) {
|
||||
return this.handleSelect('right', selectedItem, checked)
|
||||
},
|
||||
|
||||
handleScroll (direction, e) {
|
||||
this.$emit('scroll', direction, e)
|
||||
},
|
||||
|
||||
handleLeftScroll (e) {
|
||||
this.handleScroll('left', e)
|
||||
},
|
||||
handleRightScroll (e) {
|
||||
this.handleScroll('right', e)
|
||||
},
|
||||
|
||||
getTitles (transferLocale) {
|
||||
if (this.titles) {
|
||||
return this.titles
|
||||
}
|
||||
return transferLocale.titles
|
||||
},
|
||||
|
||||
getSelectedKeysName (direction) {
|
||||
return direction === 'left' ? 'sourceSelectedKeys' : 'targetSelectedKeys'
|
||||
},
|
||||
|
||||
renderTransfer (locale) {
|
||||
const {
|
||||
prefixCls = 'ant-transfer',
|
||||
operations = [],
|
||||
showSearch,
|
||||
searchPlaceholder,
|
||||
listStyle,
|
||||
filterOption,
|
||||
lazy,
|
||||
render: renderItem,
|
||||
} = getOptionProps(this)
|
||||
const notFoundContent = getComponentFromProp(this, 'notFoundContent')
|
||||
const { leftFilter, rightFilter, sourceSelectedKeys, targetSelectedKeys, $scopedSlots } = this
|
||||
const { body, footer } = $scopedSlots
|
||||
const { leftDataSource, rightDataSource } = this.splitDataSource(this.$props)
|
||||
const leftActive = targetSelectedKeys.length > 0
|
||||
const rightActive = sourceSelectedKeys.length > 0
|
||||
|
||||
const cls = classNames(prefixCls)
|
||||
|
||||
const titles = this.getTitles(locale)
|
||||
return (
|
||||
<div class={cls}>
|
||||
<List
|
||||
prefixCls={`${prefixCls}-list`}
|
||||
titleText={titles[0]}
|
||||
dataSource={leftDataSource}
|
||||
filter={leftFilter}
|
||||
filterOption={filterOption}
|
||||
style={listStyle}
|
||||
checkedKeys={sourceSelectedKeys}
|
||||
handleFilter={this.handleLeftFilter}
|
||||
handleClear={this.handleLeftClear}
|
||||
handleSelect={this.handleLeftSelect}
|
||||
handleSelectAll={this.handleLeftSelectAll}
|
||||
renderItem={renderItem}
|
||||
showSearch={showSearch}
|
||||
searchPlaceholder={searchPlaceholder || locale.searchPlaceholder}
|
||||
notFoundContent={notFoundContent || locale.notFoundContent}
|
||||
itemUnit={locale.itemUnit}
|
||||
itemsUnit={locale.itemsUnit}
|
||||
body={body}
|
||||
footer={footer}
|
||||
lazy={lazy}
|
||||
onScroll={this.handleLeftScroll}
|
||||
/>
|
||||
<Operation
|
||||
class={`${prefixCls}-operation`}
|
||||
rightActive={rightActive}
|
||||
rightArrowText={operations[0]}
|
||||
moveToRight={this.moveToRight}
|
||||
leftActive={leftActive}
|
||||
leftArrowText={operations[1]}
|
||||
moveToLeft={this.moveToLeft}
|
||||
/>
|
||||
<List
|
||||
prefixCls={`${prefixCls}-list`}
|
||||
titleText={titles[1]}
|
||||
dataSource={rightDataSource}
|
||||
filter={rightFilter}
|
||||
filterOption={filterOption}
|
||||
style={listStyle}
|
||||
checkedKeys={targetSelectedKeys}
|
||||
handleFilter={this.handleRightFilter}
|
||||
handleClear={this.handleRightClear}
|
||||
handleSelect={this.handleRightSelect}
|
||||
handleSelectAll={this.handleRightSelectAll}
|
||||
renderItem={renderItem}
|
||||
showSearch={showSearch}
|
||||
searchPlaceholder={searchPlaceholder || locale.searchPlaceholder}
|
||||
notFoundContent={notFoundContent || locale.notFoundContent}
|
||||
itemUnit={locale.itemUnit}
|
||||
itemsUnit={locale.itemsUnit}
|
||||
body={body}
|
||||
footer={footer}
|
||||
lazy={lazy}
|
||||
onScroll={this.handleRightScroll}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
render () {
|
||||
return (
|
||||
<LocaleReceiver
|
||||
componentName='Transfer'
|
||||
defaultLocale={defaultLocale.Transfer}
|
||||
children={this.renderTransfer}
|
||||
>
|
||||
</LocaleReceiver>
|
||||
)
|
||||
},
|
||||
}
|
33
components/transfer/index.zh-CN.md
Normal file
33
components/transfer/index.zh-CN.md
Normal file
@ -0,0 +1,33 @@
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| className | 自定义类 | string | |
|
||||
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] |
|
||||
| filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | (inputValue, option): boolean | |
|
||||
| footer | 底部渲染函数 | (props): ReactNode | |
|
||||
| lazy | Transfer 使用了 [react-lazy-load](https://github.com/loktar00/react-lazy-load) 优化性能,这里可以设置相关参数。设为 `false` 可以关闭懒加载。 | object\|boolean | `{ height: 32, offset: 32 }` |
|
||||
| listStyle | 两个穿梭框的自定义样式 | object | |
|
||||
| notFoundContent | 当列表为空时显示的内容 | string\|ReactNode | '列表为空' |
|
||||
| operations | 操作文案集合,顺序从下至上 | string\[] | ['>', '<'] |
|
||||
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 `label` 字段为 ReactElement,`value` 字段为 title | Function(record) | |
|
||||
| searchPlaceholder | 搜索框的默认值 | string | '请输入搜索内容' |
|
||||
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] |
|
||||
| showSearch | 是否显示搜索框 | boolean | false |
|
||||
| targetKeys | 显示在右侧框数据的key集合 | string\[] | \[] |
|
||||
| titles | 标题集合,顺序从左至右 | string\[] | ['', ''] |
|
||||
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | |
|
||||
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | |
|
||||
| onSearchChange | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', event: Event): void | - |
|
||||
| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | |
|
||||
|
||||
## 注意
|
||||
|
||||
按照 React 的[规范](http://facebook.github.io/react/docs/lists-and-keys.html#keys),所有的组件数组必须绑定 key。在 Transfer 中,`dataSource`里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。
|
||||
|
||||
如果你的数据没有这个属性,务必使用 `rowKey` 来指定数据列的主键。
|
||||
|
||||
```jsx
|
||||
// 比如你的数据主键是 uid
|
||||
return <Transfer rowKey={record => record.uid} />;
|
||||
```
|
63
components/transfer/item.jsx
Normal file
63
components/transfer/item.jsx
Normal file
@ -0,0 +1,63 @@
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import classNames from 'classnames'
|
||||
// import PureRenderMixin from 'rc-util/lib/PureRenderMixin'
|
||||
import Lazyload from '../vc-lazy-load'
|
||||
import Checkbox from '../checkbox'
|
||||
|
||||
function noop () {
|
||||
}
|
||||
|
||||
export default {
|
||||
// shouldComponentUpdate (...args: any[]) {
|
||||
// return PureRenderMixin.shouldComponentUpdate.apply(this, args)
|
||||
// }
|
||||
props: {
|
||||
renderedText: PropTypes.any,
|
||||
renderedEl: PropTypes.any,
|
||||
item: PropTypes.any,
|
||||
lazy: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.object,
|
||||
]),
|
||||
checked: PropTypes.bool,
|
||||
prefixCls: PropTypes.string,
|
||||
},
|
||||
name: 'Item',
|
||||
render () {
|
||||
const { renderedText, renderedEl, item, lazy, checked, prefixCls } = this.$props
|
||||
|
||||
const className = classNames({
|
||||
[`${prefixCls}-content-item`]: true,
|
||||
[`${prefixCls}-content-item-disabled`]: item.disabled,
|
||||
})
|
||||
|
||||
const listItem = (
|
||||
<li
|
||||
class={className}
|
||||
title={renderedText}
|
||||
onClick={item.disabled ? noop : () => {
|
||||
this.$emit('click', item)
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={checked} disabled={item.disabled} />
|
||||
<span>{renderedEl}</span>
|
||||
</li>
|
||||
)
|
||||
let children = null
|
||||
if (lazy) {
|
||||
const lazyProps = {
|
||||
props: {
|
||||
height: 32,
|
||||
offset: 500,
|
||||
throttle: 0,
|
||||
debounce: false,
|
||||
...lazy,
|
||||
},
|
||||
}
|
||||
children = <Lazyload {...lazyProps}>{listItem}</Lazyload>
|
||||
} else {
|
||||
children = listItem
|
||||
}
|
||||
return children
|
||||
},
|
||||
}
|
268
components/transfer/list.jsx
Normal file
268
components/transfer/list.jsx
Normal file
@ -0,0 +1,268 @@
|
||||
import classNames from 'classnames'
|
||||
// import PureRenderMixin from 'rc-util/lib/PureRenderMixin'
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import { isValidElement, initDefaultProps } 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 triggerEvent from '../_util/triggerEvent'
|
||||
|
||||
const TransferItem = PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}).loose
|
||||
|
||||
function noop () {
|
||||
}
|
||||
|
||||
function isRenderResultPlainObject (result) {
|
||||
return result && !isValidElement(result) &&
|
||||
Object.prototype.toString.call(result) === '[object Object]'
|
||||
}
|
||||
|
||||
export const TransferListProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
titleText: PropTypes.string,
|
||||
dataSource: PropTypes.arrayOf(TransferItem),
|
||||
// dataSource: PropTypes.any,
|
||||
filter: PropTypes.string,
|
||||
filterOption: PropTypes.func,
|
||||
checkedKeys: PropTypes.arrayOf(PropTypes.string),
|
||||
handleFilter: PropTypes.func,
|
||||
handleSelect: PropTypes.func,
|
||||
handleSelectAll: PropTypes.func,
|
||||
handleClear: PropTypes.func,
|
||||
renderItem: PropTypes.func,
|
||||
showSearch: PropTypes.bool,
|
||||
searchPlaceholder: PropTypes.string,
|
||||
notFoundContent: PropTypes.any,
|
||||
itemUnit: PropTypes.string,
|
||||
itemsUnit: PropTypes.string,
|
||||
body: PropTypes.any,
|
||||
footer: PropTypes.any,
|
||||
lazy: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.object,
|
||||
]),
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'TransferList',
|
||||
mixins: [BaseMixin],
|
||||
props: initDefaultProps(TransferListProps, {
|
||||
dataSource: [],
|
||||
titleText: '',
|
||||
showSearch: false,
|
||||
renderItem: noop,
|
||||
lazy: {},
|
||||
}),
|
||||
data () {
|
||||
this.timer = null
|
||||
this.triggerScrollTimer = null
|
||||
return {
|
||||
mounted: false,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.setState({
|
||||
mounted: true,
|
||||
})
|
||||
}, 0)
|
||||
},
|
||||
beforeDestroy () {
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.triggerScrollTimer)
|
||||
},
|
||||
|
||||
// shouldComponentUpdate (...args: any[]) {
|
||||
// return PureRenderMixin.shouldComponentUpdate.apply(this, args)
|
||||
// }
|
||||
methods: {
|
||||
getCheckStatus (filteredDataSource) {
|
||||
const { checkedKeys } = this.$props
|
||||
if (checkedKeys.length === 0) {
|
||||
return 'none'
|
||||
} else if (filteredDataSource.every(item => checkedKeys.indexOf(item.key) >= 0)) {
|
||||
return 'all'
|
||||
}
|
||||
return 'part'
|
||||
},
|
||||
_handleSelect (selectedItem) {
|
||||
const { checkedKeys } = this.$props
|
||||
const result = checkedKeys.some((key) => key === selectedItem.key)
|
||||
this.handleSelect(selectedItem, !result)
|
||||
},
|
||||
_handleFilter (e) {
|
||||
this.handleSelect(e)
|
||||
if (!e.target.value) {
|
||||
return
|
||||
}
|
||||
// Manually trigger scroll event for lazy search bug
|
||||
// https://github.com/ant-design/ant-design/issues/5631
|
||||
this.triggerScrollTimer = window.setTimeout(() => {
|
||||
const listNode = this.$el.querySelectorAll('.ant-transfer-list-content')[0]
|
||||
if (listNode) {
|
||||
triggerEvent(listNode, 'scroll')
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
_handleClear (e) {
|
||||
this.handleClear(e)
|
||||
},
|
||||
matchFilter (text, item) {
|
||||
const { filter, filterOption } = this.$props
|
||||
if (filterOption) {
|
||||
return filterOption(filter, item)
|
||||
}
|
||||
return text.indexOf(filter) >= 0
|
||||
},
|
||||
renderItemHtml (item) {
|
||||
const { renderItem = noop } = this.$props
|
||||
const renderResult = renderItem(item)
|
||||
const isRenderResultPlain = isRenderResultPlainObject(renderResult)
|
||||
return {
|
||||
renderedText: isRenderResultPlain ? renderResult.value : renderResult,
|
||||
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
|
||||
}
|
||||
},
|
||||
filterNull (arr) {
|
||||
return arr.filter((item) => {
|
||||
return item !== null
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
render () {
|
||||
const {
|
||||
prefixCls, dataSource, titleText, checkedKeys, lazy,
|
||||
body = noop, footer = noop, showSearch, filter,
|
||||
searchPlaceholder, notFoundContent, itemUnit, itemsUnit,
|
||||
} = this.$props
|
||||
|
||||
// Custom Layout
|
||||
const footerDom = footer({ ...this.$props })
|
||||
const bodyDom = body({ ...this.$props })
|
||||
|
||||
const listCls = classNames(prefixCls, {
|
||||
[`${prefixCls}-with-footer`]: !!footerDom,
|
||||
})
|
||||
|
||||
const filteredDataSource = []
|
||||
const totalDataSource = []
|
||||
|
||||
const showItems = dataSource.map((item) => {
|
||||
const { renderedText, renderedEl } = this.renderItemHtml(item)
|
||||
if (filter && filter.trim() && !this.matchFilter(renderedText, item)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// all show items
|
||||
totalDataSource.push(item)
|
||||
if (!item.disabled) {
|
||||
// response to checkAll items
|
||||
filteredDataSource.push(item)
|
||||
}
|
||||
|
||||
const checked = checkedKeys.indexOf(item.key) >= 0
|
||||
return (
|
||||
<Item
|
||||
key={item.key}
|
||||
item={item}
|
||||
lazy={lazy}
|
||||
renderedText={renderedText}
|
||||
renderedEl={renderedEl}
|
||||
checked={checked}
|
||||
prefixCls={prefixCls}
|
||||
onClick={this._handleSelect}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const unit = dataSource.length > 1 ? itemsUnit : itemUnit
|
||||
|
||||
const search = showSearch ? (
|
||||
<div class={`${prefixCls}-body-search-wrapper`}>
|
||||
<Search
|
||||
prefixCls={`${prefixCls}-search`}
|
||||
onChange={this.handleFilter}
|
||||
handleClear={this.handleClear}
|
||||
placeholder={searchPlaceholder}
|
||||
value={filter}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
const transitionName = this.mounted ? `${prefixCls}-content-item-highlight` : ''
|
||||
const transitionProps = getTransitionProps(transitionName, {
|
||||
leave: noop,
|
||||
})
|
||||
const listBody = bodyDom || (
|
||||
<div
|
||||
class={showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`}
|
||||
>
|
||||
{search}
|
||||
<transition-group
|
||||
{...transitionProps}
|
||||
tag='div'
|
||||
class={`${prefixCls}-content-warp`}
|
||||
>
|
||||
{showItems && showItems.length && this.filterNull(showItems).length ? (
|
||||
<ul
|
||||
key='transferList'
|
||||
class={`${prefixCls}-content`}
|
||||
onScroll={(e) => {
|
||||
this.$emit('scroll', e)
|
||||
}}
|
||||
>
|
||||
{showItems}
|
||||
</ul>
|
||||
) : null}
|
||||
</transition-group>
|
||||
<div class={`${prefixCls}-body-not-found`}>
|
||||
{notFoundContent}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const listFooter = footerDom ? (
|
||||
<div class={`${prefixCls}-footer`}>
|
||||
{footerDom}
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const checkStatus = this.getCheckStatus(filteredDataSource)
|
||||
const checkedAll = checkStatus === 'all'
|
||||
const checkAllCheckbox = (
|
||||
<Checkbox
|
||||
ref='checkbox'
|
||||
checked={checkedAll}
|
||||
indeterminate={checkStatus === 'part'}
|
||||
onChange={() => {
|
||||
this.handleSelectAll(filteredDataSource, checkedAll)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div class={listCls}>
|
||||
<div class={`${prefixCls}-header`}>
|
||||
{checkAllCheckbox}
|
||||
<span class={`${prefixCls}-header-selected`}>
|
||||
<span>
|
||||
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + totalDataSource.length} {unit}
|
||||
</span>
|
||||
<span class={`${prefixCls}-header-title`}>
|
||||
{titleText}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{listBody}
|
||||
{listFooter}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
53
components/transfer/operation.jsx
Normal file
53
components/transfer/operation.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import { getOptionProps } from '../_util/props-util'
|
||||
import Button from '../button'
|
||||
|
||||
function noop () {
|
||||
}
|
||||
|
||||
export const TransferOperationProps = {
|
||||
className: PropTypes.string,
|
||||
leftArrowText: PropTypes.string,
|
||||
rightArrowText: PropTypes.string,
|
||||
moveToLeft: PropTypes.any,
|
||||
moveToRight: PropTypes.any,
|
||||
leftActive: PropTypes.bool,
|
||||
rightActive: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Operation',
|
||||
props: { ...TransferOperationProps },
|
||||
render () {
|
||||
const {
|
||||
moveToLeft = noop,
|
||||
moveToRight = noop,
|
||||
leftArrowText = '',
|
||||
rightArrowText = '',
|
||||
leftActive,
|
||||
rightActive,
|
||||
} = getOptionProps(this)
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
disabled={!leftActive}
|
||||
onClick={moveToLeft}
|
||||
icon='left'
|
||||
>
|
||||
{leftArrowText}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
size='small'
|
||||
disabled={!rightActive}
|
||||
onClick={moveToRight}
|
||||
icon='right'
|
||||
>
|
||||
{rightArrowText}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
49
components/transfer/search.jsx
Normal file
49
components/transfer/search.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
import PropTypes from '../_util/vue-types'
|
||||
import { initDefaultProps, getOptionProps } from '../_util/props-util'
|
||||
import Icon from '../icon'
|
||||
import Input from '../input'
|
||||
|
||||
export const TransferSearchProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Search',
|
||||
props: initDefaultProps(TransferSearchProps, {
|
||||
placeholder: '',
|
||||
}),
|
||||
methods: {
|
||||
handleChange (e) {
|
||||
this.$emit('change', e)
|
||||
},
|
||||
handleClear (e) {
|
||||
e.preventDefault()
|
||||
this.$emit('handleClear', e)
|
||||
},
|
||||
},
|
||||
render () {
|
||||
const { placeholder, value, prefixCls } = getOptionProps(this)
|
||||
const icon = (value && value.length > 0) ? (
|
||||
<a href='#' class={`${prefixCls}-action`} onClick={this.handleClear}>
|
||||
<Icon type='cross-circle' />
|
||||
</a>
|
||||
) : (
|
||||
<span class={`${prefixCls}-action`}><Icon type='search' /></span>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
class={prefixCls}
|
||||
value={value}
|
||||
ref='input'
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
7
components/transfer/style/index.jsx
Normal file
7
components/transfer/style/index.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import '../../style/index.less'
|
||||
import './index.less'
|
||||
|
||||
// style dependencies
|
||||
import '../../checkbox/style'
|
||||
import '../../button/style'
|
||||
import '../../input/style'
|
174
components/transfer/style/index.less
Normal file
174
components/transfer/style/index.less
Normal file
@ -0,0 +1,174 @@
|
||||
@import "../../style/themes/default";
|
||||
@import "../../style/mixins/index";
|
||||
@import "../../checkbox/style/mixin";
|
||||
|
||||
@transfer-prefix-cls: ~"@{ant-prefix}-transfer";
|
||||
|
||||
.@{transfer-prefix-cls} {
|
||||
.reset-component;
|
||||
position: relative;
|
||||
|
||||
&-list {
|
||||
border: @border-width-base @border-style-base @border-color-base;
|
||||
display: inline-block;
|
||||
border-radius: @border-radius-base;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 200px;
|
||||
padding-top: 34px;
|
||||
|
||||
&-with-footer {
|
||||
padding-bottom: 34px;
|
||||
}
|
||||
|
||||
&-search {
|
||||
padding: 0 @control-padding-horizontal-sm;
|
||||
&-action {
|
||||
color: @disabled-color;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
width: 28px;
|
||||
line-height: @input-height-base;
|
||||
text-align: center;
|
||||
.@{iconfont-css-prefix} {
|
||||
transition: all .3s;
|
||||
color: @disabled-color;
|
||||
&:hover {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
span& {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
padding: 6px @control-padding-horizontal;
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
background: @component-background;
|
||||
color: @text-color;
|
||||
border-bottom: @border-width-base @border-style-base @border-color-split;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
&-title {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
font-size: @font-size-base;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&-search-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-body-with-search {
|
||||
padding-top: @input-height-base + 8px;
|
||||
}
|
||||
|
||||
&-content-warp {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
> .LazyLoad {
|
||||
animation: transferHighlightIn 1s;
|
||||
}
|
||||
|
||||
&-item {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 6px @control-padding-horizontal;
|
||||
min-height: 32px;
|
||||
transition: all .3s;
|
||||
> span {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-item:not(&-item-disabled):hover {
|
||||
cursor: pointer;
|
||||
background-color: @item-hover-bg;
|
||||
}
|
||||
|
||||
&-item-disabled {
|
||||
cursor: not-allowed;
|
||||
color: @btn-disable-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-body-not-found {
|
||||
padding-top: 0;
|
||||
color: @disabled-color;
|
||||
text-align: center;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
&-content-warp:empty + &-body-not-found {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-top: @border-width-base @border-style-base @border-color-split;
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-operation {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
margin: 0 8px;
|
||||
vertical-align: middle;
|
||||
|
||||
.@{ant-prefix}-btn {
|
||||
display: block;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes transferHighlightIn {
|
||||
0% {
|
||||
background: @primary-2;
|
||||
}
|
||||
100% {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
38
components/vc-lazy-load/demo/index.jsx
Normal file
38
components/vc-lazy-load/demo/index.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import LazyLoad from '../src/LazyLoad'
|
||||
|
||||
import './style.less'
|
||||
|
||||
const Application = {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
Scroll to load images.
|
||||
<div class='filler' />
|
||||
<LazyLoad height={762} offsetVertical={300}>
|
||||
<img src='http://apod.nasa.gov/apod/image/1502/HDR_MVMQ20Feb2015ouellet1024.jpg' />
|
||||
</LazyLoad>
|
||||
<div class='filler' />
|
||||
<LazyLoad height={683} offsetVertical={300}>
|
||||
<img src='http://apod.nasa.gov/apod/image/1502/2015_02_20_conj_bourque1024.jpg' />
|
||||
<span/>
|
||||
</LazyLoad>
|
||||
<div class='filler' />
|
||||
<div class='ScrollableContainer'>
|
||||
<div class='filler' />
|
||||
<div class='filler' />
|
||||
<div class='filler' />
|
||||
<LazyLoad height={480}>
|
||||
<img src='http://apod.nasa.gov/apod/image/1502/MarsPlume_jaeschke_480.gif' />
|
||||
</LazyLoad>
|
||||
</div>
|
||||
<div class='filler' />
|
||||
<LazyLoad height={720} offsetVertical={300}>
|
||||
<img src='http://apod.nasa.gov/apod/image/1502/ToadSky_Lane_1080_annotated.jpg' />
|
||||
</LazyLoad>
|
||||
<div class='filler' />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default Application
|
18
components/vc-lazy-load/demo/style.less
Normal file
18
components/vc-lazy-load/demo/style.less
Normal file
@ -0,0 +1,18 @@
|
||||
.LazyLoad {
|
||||
opacity: 0;
|
||||
transition: all 2s ease-in-out;
|
||||
|
||||
&.is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.filler {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.ScrollableContainer {
|
||||
height: 200px;
|
||||
overflow: scroll;
|
||||
background-color: grey;
|
||||
}
|
3
components/vc-lazy-load/index.js
Normal file
3
components/vc-lazy-load/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
import LazyLoad from './src/LazyLoad'
|
||||
export default LazyLoad
|
162
components/vc-lazy-load/src/LazyLoad.jsx
Normal file
162
components/vc-lazy-load/src/LazyLoad.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
import PropTypes from '../../_util/vue-types'
|
||||
import BaseMixin from '../../_util/BaseMixin'
|
||||
import addEventListener from '../../_util/Dom/addEventListener'
|
||||
import { initDefaultProps } from '../../_util/props-util'
|
||||
import warning from '../../_util/warning'
|
||||
import debounce from 'lodash/debounce'
|
||||
import throttle from 'lodash/throttle'
|
||||
import parentScroll from './utils/parentScroll'
|
||||
import inViewport from './utils/inViewport'
|
||||
|
||||
const lazyLoadProps = {
|
||||
debounce: PropTypes.bool,
|
||||
elementType: PropTypes.string,
|
||||
height: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
offset: PropTypes.number,
|
||||
offsetBottom: PropTypes.number,
|
||||
offsetHorizontal: PropTypes.number,
|
||||
offsetLeft: PropTypes.number,
|
||||
offsetRight: PropTypes.number,
|
||||
offsetTop: PropTypes.number,
|
||||
offsetVertical: PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
throttle: PropTypes.number,
|
||||
width: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
onContentVisible: PropTypes.func,
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LazyLoad',
|
||||
mixins: [BaseMixin],
|
||||
props: initDefaultProps(lazyLoadProps, {
|
||||
elementType: 'div',
|
||||
debounce: true,
|
||||
offset: 0,
|
||||
offsetBottom: 0,
|
||||
offsetHorizontal: 0,
|
||||
offsetLeft: 0,
|
||||
offsetRight: 0,
|
||||
offsetTop: 0,
|
||||
offsetVertical: 0,
|
||||
throttle: 250,
|
||||
}),
|
||||
data () {
|
||||
if (this.throttle > 0) {
|
||||
if (this.debounce) {
|
||||
this.lazyLoadHandler = debounce(this.lazyLoadHandler, this.throttle)
|
||||
} else {
|
||||
this.lazyLoadHandler = throttle(this.lazyLoadHandler, this.throttle)
|
||||
}
|
||||
}
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this._mounted = true
|
||||
const eventNode = this.getEventNode()
|
||||
|
||||
this.lazyLoadHandler()
|
||||
|
||||
if (this.lazyLoadHandler.flush) {
|
||||
this.lazyLoadHandler.flush()
|
||||
}
|
||||
|
||||
this.resizeHander = addEventListener(window, 'resize', this.lazyLoadHandler)
|
||||
this.scrollHander = addEventListener(eventNode, 'scroll', this.lazyLoadHandler)
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
visible (val) {
|
||||
if (!val) {
|
||||
this.lazyLoadHandler()
|
||||
}
|
||||
},
|
||||
},
|
||||
// shouldComponentUpdate (_nextProps, nextState) {
|
||||
// return nextState.visible
|
||||
// }
|
||||
beforeDestroy () {
|
||||
this._mounted = false
|
||||
if (this.lazyLoadHandler.cancel) {
|
||||
this.lazyLoadHandler.cancel()
|
||||
}
|
||||
|
||||
this.detachListeners()
|
||||
},
|
||||
methods: {
|
||||
getEventNode () {
|
||||
return parentScroll(this.$el)
|
||||
},
|
||||
getOffset () {
|
||||
const {
|
||||
offset, offsetVertical, offsetHorizontal,
|
||||
offsetTop, offsetBottom, offsetLeft, offsetRight, threshold,
|
||||
} = this.$props
|
||||
|
||||
const _offsetAll = threshold || offset
|
||||
const _offsetVertical = offsetVertical || _offsetAll
|
||||
const _offsetHorizontal = offsetHorizontal || _offsetAll
|
||||
|
||||
return {
|
||||
top: offsetTop || _offsetVertical,
|
||||
bottom: offsetBottom || _offsetVertical,
|
||||
left: offsetLeft || _offsetHorizontal,
|
||||
right: offsetRight || _offsetHorizontal,
|
||||
}
|
||||
},
|
||||
lazyLoadHandler () {
|
||||
if (!this._mounted) {
|
||||
return
|
||||
}
|
||||
const offset = this.getOffset()
|
||||
const node = this.$el
|
||||
const eventNode = this.getEventNode()
|
||||
|
||||
if (inViewport(node, eventNode, offset)) {
|
||||
const { onContentVisible } = this.$props
|
||||
|
||||
this.setState({ visible: true }, () => {
|
||||
if (onContentVisible) {
|
||||
onContentVisible()
|
||||
}
|
||||
})
|
||||
this.detachListeners()
|
||||
}
|
||||
},
|
||||
detachListeners () {
|
||||
this.resizeHander && this.resizeHander.remove()
|
||||
this.scrollHander && this.scrollHander.remove()
|
||||
},
|
||||
},
|
||||
render (createElement) {
|
||||
const children = this.$slots.default
|
||||
if (children.length !== 1) {
|
||||
warning(false, 'lazyLoad组件只能包含一个子元素')
|
||||
return null
|
||||
}
|
||||
const { height, width, elementType } = this.$props
|
||||
const { visible } = this
|
||||
|
||||
const elStyles = {
|
||||
height: typeof height === 'number' ? height + 'px' : height,
|
||||
width: typeof width === 'number' ? width + 'px' : width,
|
||||
}
|
||||
const elClasses = {
|
||||
'LazyLoad': true,
|
||||
'is-visible': visible,
|
||||
}
|
||||
|
||||
return createElement(elementType, {
|
||||
class: elClasses,
|
||||
style: elStyles,
|
||||
}, [visible ? children[0] : null])
|
||||
},
|
||||
}
|
12
components/vc-lazy-load/src/utils/getElementPosition.js
Normal file
12
components/vc-lazy-load/src/utils/getElementPosition.js
Normal file
@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Finds element's position relative to the whole document,
|
||||
* rather than to the viewport as it is the case with .getBoundingClientRect().
|
||||
*/
|
||||
export default function getElementPosition (element) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
|
||||
return {
|
||||
top: rect.top + window.pageYOffset,
|
||||
left: rect.left + window.pageXOffset,
|
||||
}
|
||||
}
|
38
components/vc-lazy-load/src/utils/inViewport.js
Normal file
38
components/vc-lazy-load/src/utils/inViewport.js
Normal file
@ -0,0 +1,38 @@
|
||||
import getElementPosition from './getElementPosition'
|
||||
|
||||
const isHidden = (element) =>
|
||||
element.offsetParent === null
|
||||
|
||||
export default function inViewport (element, container, customOffset) {
|
||||
if (isHidden(element)) {
|
||||
return false
|
||||
}
|
||||
|
||||
let top
|
||||
let bottom
|
||||
let left
|
||||
let right
|
||||
|
||||
if (typeof container === 'undefined' || container === window) {
|
||||
top = window.pageYOffset
|
||||
left = window.pageXOffset
|
||||
bottom = top + window.innerHeight
|
||||
right = left + window.innerWidth
|
||||
} else {
|
||||
const containerPosition = getElementPosition(container)
|
||||
|
||||
top = containerPosition.top
|
||||
left = containerPosition.left
|
||||
bottom = top + container.offsetHeight
|
||||
right = left + container.offsetWidth
|
||||
}
|
||||
|
||||
const elementPosition = getElementPosition(element)
|
||||
|
||||
return (
|
||||
top <= elementPosition.top + element.offsetHeight + customOffset.top &&
|
||||
bottom >= elementPosition.top - customOffset.bottom &&
|
||||
left <= elementPosition.left + element.offsetWidth + customOffset.left &&
|
||||
right >= elementPosition.left - customOffset.right
|
||||
)
|
||||
}
|
39
components/vc-lazy-load/src/utils/parentScroll.js
Normal file
39
components/vc-lazy-load/src/utils/parentScroll.js
Normal file
@ -0,0 +1,39 @@
|
||||
const style = (element, prop) => {
|
||||
let styleVal = ''
|
||||
if (typeof getComputedStyle !== 'undefined') {
|
||||
styleVal = window.getComputedStyle(element, null).getPropertyValue(prop)
|
||||
} else {
|
||||
styleVal = element.style[prop]
|
||||
}
|
||||
return styleVal
|
||||
}
|
||||
|
||||
const overflow = (element) =>
|
||||
style(element, 'overflow') + style(element, 'overflow-y') + style(element, 'overflow-x')
|
||||
|
||||
const scrollParent = (element) => {
|
||||
if (!(element instanceof window.HTMLElement)) {
|
||||
return window
|
||||
}
|
||||
|
||||
let parent = element
|
||||
|
||||
while (parent) {
|
||||
if (parent === document.body || parent === document.documentElement) {
|
||||
break
|
||||
}
|
||||
|
||||
if (!parent.parentNode) {
|
||||
break
|
||||
}
|
||||
if (/(scroll|auto)/.test(overflow(parent))) {
|
||||
return parent
|
||||
}
|
||||
|
||||
parent = parent.parentNode
|
||||
}
|
||||
|
||||
return window
|
||||
}
|
||||
|
||||
export default scrollParent
|
@ -72,7 +72,8 @@ export default {
|
||||
|
||||
const child = this.$slots.default
|
||||
if (child.length !== 1) {
|
||||
warning(false, '只能包含一个子元素')
|
||||
warning(false, 'm-feedback组件只能包含一个子元素')
|
||||
return null
|
||||
}
|
||||
let childProps = {
|
||||
on: disabled ? {} : {
|
||||
|
@ -51,6 +51,6 @@ Spin | done
|
||||
Switch | done
|
||||
Steps | done
|
||||
Progress | done
|
||||
Slider | done InputNumber做完补全demo
|
||||
Slider | done
|
||||
Timeline | done
|
||||
Transfer
|
||||
|
@ -3,7 +3,7 @@ const AsyncComp = () => {
|
||||
const hashs = window.location.hash.split('/')
|
||||
const d = hashs[hashs.length - 1]
|
||||
return {
|
||||
component: import(`../components/input-number/demo/${d}`),
|
||||
component: import(`../components/transfer/demo/${d}`),
|
||||
}
|
||||
}
|
||||
export default [
|
||||
|
Loading…
Reference in New Issue
Block a user