add vc-lazy-load transfer

This commit is contained in:
wangxueliang 2018-04-07 00:20:45 +08:00
parent aaaa776ba0
commit a1b671a8c4
27 changed files with 1824 additions and 3 deletions

View File

@ -148,3 +148,5 @@ const TimelineItem = Timeline.Item
export { Timeline, TimelineItem }
export { default as InputNumber } from './input-number'
export { default as Transfer } from './transfer'

View File

@ -39,3 +39,4 @@ import './table/style'
import './progress/style'
import './timeline/style'
import './input-number/style'
import './transfer/style'

View 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>
```

View 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>
```

View 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>
```

View 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>

View 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>
```

View 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>
```

View 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} />;
```

View 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>
)
},
}

View 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} />;
```

View 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
},
}

View 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>
)
},
}

View 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>
)
},
}

View 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>
)
},
}

View File

@ -0,0 +1,7 @@
import '../../style/index.less'
import './index.less'
// style dependencies
import '../../checkbox/style'
import '../../button/style'
import '../../input/style'

View 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;
}
}

View 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

View 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;
}

View File

@ -0,0 +1,3 @@
import LazyLoad from './src/LazyLoad'
export default LazyLoad

View 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])
},
}

View 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,
}
}

View 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
)
}

View 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

View File

@ -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 ? {} : {

View File

@ -51,6 +51,6 @@ Spin | done
Switch | done
Steps | done
Progress | done
Slider | done InputNumber做完补全demo
Slider | done
Timeline | done
Transfer

View File

@ -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 [