From 39f54ac1af23ecf32ef7cb0878c18f09c28813ae Mon Sep 17 00:00:00 2001 From: wangxueliang Date: Sat, 7 Apr 2018 00:20:45 +0800 Subject: [PATCH 1/3] add vc-lazy-load transfer --- components/index.js | 2 + components/style.js | 1 + components/transfer/demo/advanced.md | 81 ++++ components/transfer/demo/basic.md | 69 +++ components/transfer/demo/custom-item.md | 77 ++++ components/transfer/demo/index.vue | 57 +++ components/transfer/demo/large-data.md | 60 +++ components/transfer/demo/search.md | 66 +++ components/transfer/index.en-US.md | 48 +++ components/transfer/index.jsx | 402 ++++++++++++++++++ components/transfer/index.zh-CN.md | 33 ++ components/transfer/item.jsx | 63 +++ components/transfer/list.jsx | 268 ++++++++++++ components/transfer/operation.jsx | 53 +++ components/transfer/search.jsx | 49 +++ components/transfer/style/index.jsx | 7 + components/transfer/style/index.less | 174 ++++++++ components/vc-lazy-load/demo/index.jsx | 38 ++ components/vc-lazy-load/demo/style.less | 18 + components/vc-lazy-load/index.js | 3 + components/vc-lazy-load/src/LazyLoad.jsx | 162 +++++++ .../src/utils/getElementPosition.js | 12 + .../vc-lazy-load/src/utils/inViewport.js | 38 ++ .../vc-lazy-load/src/utils/parentScroll.js | 39 ++ .../vc-m-feedback/src/TouchFeedback.jsx | 3 +- contributors.md | 2 +- examples/routes.js | 2 +- 27 files changed, 1824 insertions(+), 3 deletions(-) create mode 100644 components/transfer/demo/advanced.md create mode 100644 components/transfer/demo/basic.md create mode 100644 components/transfer/demo/custom-item.md create mode 100644 components/transfer/demo/index.vue create mode 100644 components/transfer/demo/large-data.md create mode 100644 components/transfer/demo/search.md create mode 100644 components/transfer/index.en-US.md create mode 100644 components/transfer/index.jsx create mode 100644 components/transfer/index.zh-CN.md create mode 100644 components/transfer/item.jsx create mode 100644 components/transfer/list.jsx create mode 100644 components/transfer/operation.jsx create mode 100644 components/transfer/search.jsx create mode 100644 components/transfer/style/index.jsx create mode 100644 components/transfer/style/index.less create mode 100644 components/vc-lazy-load/demo/index.jsx create mode 100644 components/vc-lazy-load/demo/style.less create mode 100644 components/vc-lazy-load/index.js create mode 100644 components/vc-lazy-load/src/LazyLoad.jsx create mode 100644 components/vc-lazy-load/src/utils/getElementPosition.js create mode 100644 components/vc-lazy-load/src/utils/inViewport.js create mode 100644 components/vc-lazy-load/src/utils/parentScroll.js diff --git a/components/index.js b/components/index.js index 92e252232..e8df83a9d 100644 --- a/components/index.js +++ b/components/index.js @@ -148,3 +148,5 @@ const TimelineItem = Timeline.Item export { Timeline, TimelineItem } export { default as InputNumber } from './input-number' + +export { default as Transfer } from './transfer' diff --git a/components/style.js b/components/style.js index ffd2dec1b..8f24af65c 100644 --- a/components/style.js +++ b/components/style.js @@ -39,3 +39,4 @@ import './table/style' import './progress/style' import './timeline/style' import './input-number/style' +import './transfer/style' diff --git a/components/transfer/demo/advanced.md b/components/transfer/demo/advanced.md new file mode 100644 index 000000000..4d55756dd --- /dev/null +++ b/components/transfer/demo/advanced.md @@ -0,0 +1,81 @@ + +#### 高级用法 +穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。 + + + +#### 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. + + +```html + + +``` + + + diff --git a/components/transfer/demo/basic.md b/components/transfer/demo/basic.md new file mode 100644 index 000000000..dd6cabf2b --- /dev/null +++ b/components/transfer/demo/basic.md @@ -0,0 +1,69 @@ + +#### 基本用法 +最基本的用法,展示了 `dataSource`、`targetKeys`、每行的渲染函数 `render` 以及回调函数 `onChange` `onSelectChange` `onScroll` 的用法。 + + + +#### Basic +The most basic usage of `Transfer` involves providing the source data and target keys arrays, plus the rendering and some callback functions. + + +```html + + +``` + diff --git a/components/transfer/demo/custom-item.md b/components/transfer/demo/custom-item.md new file mode 100644 index 000000000..b11c759ff --- /dev/null +++ b/components/transfer/demo/custom-item.md @@ -0,0 +1,77 @@ + +#### 自定义渲染行数据 +自定义渲染每一个 Transfer Item,可用于渲染复杂数据。 + + + +#### Custom datasource +Custom each Transfer Item, and in this way you can render a complex datasource. + + +```html + + +``` + + + diff --git a/components/transfer/demo/index.vue b/components/transfer/demo/index.vue new file mode 100644 index 000000000..ad1bd2c24 --- /dev/null +++ b/components/transfer/demo/index.vue @@ -0,0 +1,57 @@ + diff --git a/components/transfer/demo/large-data.md b/components/transfer/demo/large-data.md new file mode 100644 index 000000000..c165059e9 --- /dev/null +++ b/components/transfer/demo/large-data.md @@ -0,0 +1,60 @@ + +#### 大数据性能测试 +2000 条数据。 + + + +#### Performance Test +2000 items. + + +```html + + +``` + + diff --git a/components/transfer/demo/search.md b/components/transfer/demo/search.md new file mode 100644 index 000000000..ae3e00886 --- /dev/null +++ b/components/transfer/demo/search.md @@ -0,0 +1,66 @@ + +#### 带搜索框 +带搜索框的穿梭框,可以自定义搜索函数。 + + + +#### Search +Transfer with a search box. + + +```html + + +``` + + + diff --git a/components/transfer/index.en-US.md b/components/transfer/index.en-US.md new file mode 100644 index 000000000..43788fc1f --- /dev/null +++ b/components/transfer/index.en-US.md @@ -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 record.uid} />; +``` diff --git a/components/transfer/index.jsx b/components/transfer/index.jsx new file mode 100644 index 000000000..4c255d2d3 --- /dev/null +++ b/components/transfer/index.jsx @@ -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 ( +
+ + + +
+ ) + }, + }, + render () { + return ( + + + ) + }, +} diff --git a/components/transfer/index.zh-CN.md b/components/transfer/index.zh-CN.md new file mode 100644 index 000000000..1687bc535 --- /dev/null +++ b/components/transfer/index.zh-CN.md @@ -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 record.uid} />; +``` diff --git a/components/transfer/item.jsx b/components/transfer/item.jsx new file mode 100644 index 000000000..45fabe8d2 --- /dev/null +++ b/components/transfer/item.jsx @@ -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 = ( +
  • { + this.$emit('click', item) + }} + > + + {renderedEl} +
  • + ) + let children = null + if (lazy) { + const lazyProps = { + props: { + height: 32, + offset: 500, + throttle: 0, + debounce: false, + ...lazy, + }, + } + children = {listItem} + } else { + children = listItem + } + return children + }, +} diff --git a/components/transfer/list.jsx b/components/transfer/list.jsx new file mode 100644 index 000000000..14943d2b3 --- /dev/null +++ b/components/transfer/list.jsx @@ -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 ( + + ) + }) + + const unit = dataSource.length > 1 ? itemsUnit : itemUnit + + const search = showSearch ? ( +
    + +
    + ) : null + const transitionName = this.mounted ? `${prefixCls}-content-item-highlight` : '' + const transitionProps = getTransitionProps(transitionName, { + leave: noop, + }) + const listBody = bodyDom || ( +
    + {search} + + {showItems && showItems.length && this.filterNull(showItems).length ? ( +
      { + this.$emit('scroll', e) + }} + > + {showItems} +
    + ) : null} +
    +
    + {notFoundContent} +
    +
    + ) + + const listFooter = footerDom ? ( +
    + {footerDom} +
    + ) : null + + const checkStatus = this.getCheckStatus(filteredDataSource) + const checkedAll = checkStatus === 'all' + const checkAllCheckbox = ( + { + this.handleSelectAll(filteredDataSource, checkedAll) + }} + /> + ) + + return ( +
    +
    + {checkAllCheckbox} + + + {(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + totalDataSource.length} {unit} + + + {titleText} + + +
    + {listBody} + {listFooter} +
    + ) + }, +} diff --git a/components/transfer/operation.jsx b/components/transfer/operation.jsx new file mode 100644 index 000000000..5ecfad70f --- /dev/null +++ b/components/transfer/operation.jsx @@ -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 ( +
    + + +
    + ) + }, +} diff --git a/components/transfer/search.jsx b/components/transfer/search.jsx new file mode 100644 index 000000000..e1f903bc0 --- /dev/null +++ b/components/transfer/search.jsx @@ -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) ? ( + + + + ) : ( + + ) + + return ( +
    + + {icon} +
    + ) + }, +} diff --git a/components/transfer/style/index.jsx b/components/transfer/style/index.jsx new file mode 100644 index 000000000..f8d5f69be --- /dev/null +++ b/components/transfer/style/index.jsx @@ -0,0 +1,7 @@ +import '../../style/index.less' +import './index.less' + +// style dependencies +import '../../checkbox/style' +import '../../button/style' +import '../../input/style' diff --git a/components/transfer/style/index.less b/components/transfer/style/index.less new file mode 100644 index 000000000..97513a073 --- /dev/null +++ b/components/transfer/style/index.less @@ -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; + } +} diff --git a/components/vc-lazy-load/demo/index.jsx b/components/vc-lazy-load/demo/index.jsx new file mode 100644 index 000000000..fc58f480b --- /dev/null +++ b/components/vc-lazy-load/demo/index.jsx @@ -0,0 +1,38 @@ +import LazyLoad from '../src/LazyLoad' + +import './style.less' + +const Application = { + render () { + return ( +
    + Scroll to load images. +
    + + + +
    + + + + +
    +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + ) + }, +} + +export default Application diff --git a/components/vc-lazy-load/demo/style.less b/components/vc-lazy-load/demo/style.less new file mode 100644 index 000000000..85673b660 --- /dev/null +++ b/components/vc-lazy-load/demo/style.less @@ -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; +} diff --git a/components/vc-lazy-load/index.js b/components/vc-lazy-load/index.js new file mode 100644 index 000000000..5cb986ba5 --- /dev/null +++ b/components/vc-lazy-load/index.js @@ -0,0 +1,3 @@ + +import LazyLoad from './src/LazyLoad' +export default LazyLoad diff --git a/components/vc-lazy-load/src/LazyLoad.jsx b/components/vc-lazy-load/src/LazyLoad.jsx new file mode 100644 index 000000000..9f6787e04 --- /dev/null +++ b/components/vc-lazy-load/src/LazyLoad.jsx @@ -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]) + }, +} diff --git a/components/vc-lazy-load/src/utils/getElementPosition.js b/components/vc-lazy-load/src/utils/getElementPosition.js new file mode 100644 index 000000000..f3088c872 --- /dev/null +++ b/components/vc-lazy-load/src/utils/getElementPosition.js @@ -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, + } +} diff --git a/components/vc-lazy-load/src/utils/inViewport.js b/components/vc-lazy-load/src/utils/inViewport.js new file mode 100644 index 000000000..7a8e196db --- /dev/null +++ b/components/vc-lazy-load/src/utils/inViewport.js @@ -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 + ) +} diff --git a/components/vc-lazy-load/src/utils/parentScroll.js b/components/vc-lazy-load/src/utils/parentScroll.js new file mode 100644 index 000000000..883a39f08 --- /dev/null +++ b/components/vc-lazy-load/src/utils/parentScroll.js @@ -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 diff --git a/components/vc-m-feedback/src/TouchFeedback.jsx b/components/vc-m-feedback/src/TouchFeedback.jsx index 336f1b4b3..1fb92026e 100755 --- a/components/vc-m-feedback/src/TouchFeedback.jsx +++ b/components/vc-m-feedback/src/TouchFeedback.jsx @@ -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 ? {} : { diff --git a/contributors.md b/contributors.md index 43064e78b..17f1f15b6 100644 --- a/contributors.md +++ b/contributors.md @@ -51,6 +51,6 @@ Spin | done Switch | done Steps | done Progress | done -Slider | done InputNumber做完补全demo +Slider | done Timeline | done Transfer diff --git a/examples/routes.js b/examples/routes.js index 0d3dafd6f..defbe8463 100644 --- a/examples/routes.js +++ b/examples/routes.js @@ -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 [ From ffc48c72afc66bb2faa73fab613651e81dc8a666 Mon Sep 17 00:00:00 2001 From: wangxueliang Date: Sat, 7 Apr 2018 10:32:13 +0800 Subject: [PATCH 2/3] fix doc --- components/input-number/demo/index.vue | 2 +- components/transfer/demo/basic.md | 4 +-- components/transfer/index.en-US.md | 42 +++++++++----------------- components/transfer/index.jsx | 21 +++++++------ components/transfer/index.zh-CN.md | 27 +++++++++-------- components/transfer/list.jsx | 15 +++++---- site/demo.js | 1 + site/routes.js | 2 +- 8 files changed, 53 insertions(+), 61 deletions(-) diff --git a/components/input-number/demo/index.vue b/components/input-number/demo/index.vue index 8f5ff5519..373c4587f 100644 --- a/components/input-number/demo/index.vue +++ b/components/input-number/demo/index.vue @@ -13,7 +13,7 @@ const md = { ## 何时使用 当需要获取标准数值时。 ## 代码演示`, - us: `# Data Entry + us: `# InputNumber Enter a number within certain range with the mouse or keyboard. ## When To Use When a numeric value needs to be provided. diff --git a/components/transfer/demo/basic.md b/components/transfer/demo/basic.md index dd6cabf2b..f85f18d12 100644 --- a/components/transfer/demo/basic.md +++ b/components/transfer/demo/basic.md @@ -1,6 +1,6 @@ #### 基本用法 -最基本的用法,展示了 `dataSource`、`targetKeys`、每行的渲染函数 `render` 以及回调函数 `onChange` `onSelectChange` `onScroll` 的用法。 +最基本的用法,展示了 `dataSource`、`targetKeys`、每行的渲染函数 `render` 以及回调函数 `change` `selectChange` `scroll` 的用法。 @@ -41,7 +41,7 @@ export default { return { mockData, targetKeys, - selectedKeys: [], + selectedKeys: ['1', '4'], } }, methods: { diff --git a/components/transfer/index.en-US.md b/components/transfer/index.en-US.md index 43788fc1f..384026974 100644 --- a/components/transfer/index.en-US.md +++ b/components/transfer/index.en-US.md @@ -1,48 +1,36 @@ ---- -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)\[] | \[] | +| dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop. | \[{key: string.isRequired,title: string.isRequired,description: string,disabled: bool}\] | \[] | | 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 }` | +| footer | customize the progress dot by setting a scoped slot | slot="footer" slot-scope="props" | | +| lazy | property of vc-lazy-load for lazy rendering items. Turn off it by set to `false`. | object\|boolean | `{ height: 32, offset: 32 }` | | listStyle | A custom CSS style used for rendering the transfer columns. | object | | -| notFoundContent | Text to display when a column is empty. | string\|ReactNode | 'The list is empty' | +| notFoundContent | Text to display when a column is empty. | string\|slot | '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) | | +| render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a element and `value` is for title | Function(record) | | | 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 | | + +### events +| Events Name | Description | Arguments | +| --- | --- | --- | +| change | A callback function that is executed when the transfer between columns is complete. | (targetKeys, direction, moveKeys): void | | +| scroll | A callback function which is executed when scroll options list | (direction, event): void | | +| searchChange | A callback function which is executed when search field are changed | (direction: 'left'\|'right', event: Event): void | - | +| selectChange | 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. +According the standard of Vue, the key should always be supplied directly to the elements in the array. In Transfer, the keys should be set on the elements included in `dataSource` array. By default, `key` property is used as an unique identifier. 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 record.uid} />; +return ; ``` diff --git a/components/transfer/index.jsx b/components/transfer/index.jsx index 4c255d2d3..e64e9f99b 100644 --- a/components/transfer/index.jsx +++ b/components/transfer/index.jsx @@ -13,12 +13,12 @@ import defaultLocale from '../locale-provider/default' export const TransferDirection = 'left' | 'right' -export const TransferItem = PropTypes.shape({ - key: PropTypes.string, - title: PropTypes.string, +export const TransferItem = { + key: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, description: PropTypes.string, disabled: PropTypes.bool, -}).loose +} export const TransferProps = { prefixCls: PropTypes.string, @@ -27,8 +27,8 @@ export const TransferProps = { selectedKeys: PropTypes.arrayOf(PropTypes.string), render: PropTypes.func, listStyle: PropTypes.object, - titles: PropTypes.arrayOf(TransferItem), - operations: PropTypes.arrayOf(TransferItem), + titles: PropTypes.arrayOf(PropTypes.string), + operations: PropTypes.arrayOf(PropTypes.string), showSearch: PropTypes.bool, filterOption: PropTypes.func, searchPlaceholder: PropTypes.string, @@ -50,12 +50,13 @@ export const TransferLocale = { const tranferProps = { prefixCls: PropTypes.string, - dataSource: PropTypes.arrayOf(TransferItem), - targetKeys: PropTypes.array, + dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose), + targetKeys: PropTypes.arrayOf(PropTypes.string), + selectedKeys: PropTypes.arrayOf(PropTypes.string), listStyle: PropTypes.object, render: PropTypes.func, - titles: PropTypes.array, - operations: PropTypes.array, + titles: PropTypes.arrayOf(PropTypes.string), + operations: PropTypes.arrayOf(PropTypes.string), showSearch: PropTypes.bool, filterOption: PropTypes.func, searchPlaceholder: PropTypes.string, diff --git a/components/transfer/index.zh-CN.md b/components/transfer/index.zh-CN.md index 1687bc535..b2c821838 100644 --- a/components/transfer/index.zh-CN.md +++ b/components/transfer/index.zh-CN.md @@ -2,32 +2,35 @@ | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| className | 自定义类 | string | | -| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] | +| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | \[{key: string.isRequired,title: string.isRequired,description: string,disabled: bool}\]\[] | \[] | | 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 }` | +| footer | 可以设置为一个 作用域插槽 | slot="footer" slot-scope="props" | | +| lazy | Transfer 使用了 [vc-lazy-load]优化性能,这里可以设置相关参数。设为 `false` 可以关闭懒加载。 | object\|boolean | `{ height: 32, offset: 32 }` | | listStyle | 两个穿梭框的自定义样式 | object | | -| notFoundContent | 当列表为空时显示的内容 | string\|ReactNode | '列表为空' | +| notFoundContent | 当列表为空时显示的内容 | string\|slot | '列表为空' | | operations | 操作文案集合,顺序从下至上 | string\[] | ['>', '<'] | -| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 `label` 字段为 ReactElement,`value` 字段为 title | Function(record) | | +| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 element。或者返回一个普通对象,其中 `label` 字段为 element,`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 | | + +### 事件 +| 事件名称 | 说明 | 回调参数 | +| --- | --- | --- | +| change | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | | +| scroll | 选项列表滚动时的回调函数 | (direction, event): void | | +| searchChange | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', event: Event): void | - | +| selectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | | ## 注意 -按照 React 的[规范](http://facebook.github.io/react/docs/lists-and-keys.html#keys),所有的组件数组必须绑定 key。在 Transfer 中,`dataSource`里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。 +按照Vue最新的规范,所有的组件数组最好绑定 key。在 Transfer 中,`dataSource`里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。 如果你的数据没有这个属性,务必使用 `rowKey` 来指定数据列的主键。 ```jsx // 比如你的数据主键是 uid -return record.uid} />; +return ; ``` diff --git a/components/transfer/list.jsx b/components/transfer/list.jsx index 14943d2b3..785bd1e9c 100644 --- a/components/transfer/list.jsx +++ b/components/transfer/list.jsx @@ -9,14 +9,14 @@ import Search from './search' import Item from './item' import triggerEvent from '../_util/triggerEvent' -const TransferItem = PropTypes.shape({ - key: PropTypes.string, - title: PropTypes.string, +function noop () { +} + +const TransferItem = { + key: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, description: PropTypes.string, disabled: PropTypes.bool, -}).loose - -function noop () { } function isRenderResultPlainObject (result) { @@ -27,8 +27,7 @@ function isRenderResultPlainObject (result) { export const TransferListProps = { prefixCls: PropTypes.string, titleText: PropTypes.string, - dataSource: PropTypes.arrayOf(TransferItem), - // dataSource: PropTypes.any, + dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose), filter: PropTypes.string, filterOption: PropTypes.func, checkedKeys: PropTypes.arrayOf(PropTypes.string), diff --git a/site/demo.js b/site/demo.js index c032ecb56..9b1bcf475 100644 --- a/site/demo.js +++ b/site/demo.js @@ -40,3 +40,4 @@ export { default as progress } from 'antd/progress/demo/index.vue' export { default as timeline } from 'antd/timeline/demo/index.vue' export { default as table } from 'antd/table/demo/index.vue' export { default as inputNumber } from 'antd/input-number/demo/index.vue' +export { default as transfer } from 'antd/transfer/demo/index.vue' diff --git a/site/routes.js b/site/routes.js index a5002d67d..9f0203788 100644 --- a/site/routes.js +++ b/site/routes.js @@ -2,7 +2,7 @@ import Demo from './components/demo.vue' const AsyncComp = () => { const d = window.location.hash.replace('#', '') return { - component: import(`../components/input-number/demo/${d}`), + component: import(`../components/transfer/demo/${d}`), } } export default [ From 389bcf6de3a610e9186dfdac9e6884100a9977d1 Mon Sep 17 00:00:00 2001 From: wangxueliang Date: Sat, 7 Apr 2018 10:41:09 +0800 Subject: [PATCH 3/3] fix --- components/transfer/index.jsx | 23 +++-------------------- contributors.md | 2 +- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/components/transfer/index.jsx b/components/transfer/index.jsx index e64e9f99b..13230713f 100644 --- a/components/transfer/index.jsx +++ b/components/transfer/index.jsx @@ -22,7 +22,7 @@ export const TransferItem = { export const TransferProps = { prefixCls: PropTypes.string, - dataSource: PropTypes.arrayOf(TransferItem), + dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose), targetKeys: PropTypes.arrayOf(PropTypes.string), selectedKeys: PropTypes.arrayOf(PropTypes.string), render: PropTypes.func, @@ -33,7 +33,7 @@ export const TransferProps = { filterOption: PropTypes.func, searchPlaceholder: PropTypes.string, notFoundContent: PropTypes.any, - rowKey: PropTypes.string, + rowKey: PropTypes.func, lazy: PropTypes.oneOfType([ PropTypes.object, PropTypes.bool, @@ -48,27 +48,10 @@ export const TransferLocale = { itemsUnit: PropTypes.string, } -const tranferProps = { - prefixCls: PropTypes.string, - dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose), - targetKeys: PropTypes.arrayOf(PropTypes.string), - selectedKeys: PropTypes.arrayOf(PropTypes.string), - listStyle: PropTypes.object, - render: PropTypes.func, - titles: PropTypes.arrayOf(PropTypes.string), - operations: PropTypes.arrayOf(PropTypes.string), - 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, { + props: initDefaultProps(TransferProps, { dataSource: [], showSearch: false, }), diff --git a/contributors.md b/contributors.md index 17f1f15b6..999ea4053 100644 --- a/contributors.md +++ b/contributors.md @@ -53,4 +53,4 @@ Steps | done Progress | done Slider | done Timeline | done -Transfer +Transfer | done