提交 6c1f601a 编写于 作者: A afc163

Merge pull request #735 from ant-design/component-transfer

Component transfer
# 高级用法
- order: 2
穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。
---
````jsx
import { Transfer, Button } from 'antd';
const container = document.getElementById('components-transfer-demo-advanced');
const App = React.createClass({
getInitialState() {
return {
mockData: [],
targetKeys: [],
};
},
componentDidMount() {
this.getMock();
},
getMock() {
let targetKeys = [];
let mockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i,
title: '内容' + (i + 1),
description: '内容' + (i + 1) + '的描述',
chosen: Math.random() * 2 > 1
};
if (data.chosen) {
targetKeys.push(data.key);
}
mockData.push(data);
}
this.setState({
mockData: mockData,
targetKeys: targetKeys,
});
},
handleChange(targetKeys) {
this.setState({
targetKeys: targetKeys,
});
},
renderFooter(props) {
return <Button type="ghost" size="small" style={{ float: 'right', margin: '5' }}
onClick={this.getMock}>刷新</Button>;
},
render() {
return <div>
<Transfer
dataSource={this.state.mockData}
showSearch
listStyle={{
width: 250,
height: 300,
}}
operations={['向右操作文案', '向左操作文案']}
targetKeys={this.state.targetKeys}
onChange={this.handleChange}
render={(item) => { return item.title + '-' + item.description; }}
footer={this.renderFooter}/>
</div>;
}
});
ReactDOM.render(<App />, container);
````
# 基本用法
- order: 0
最基本的用法。
---
````jsx
import { Transfer, Button } from 'antd';
const container = document.getElementById('components-transfer-demo-basic');
const App = React.createClass({
getInitialState() {
return {
mockData: [],
targetKeys: [],
};
},
componentDidMount() {
this.getMock();
},
getMock() {
let targetKeys = [];
let mockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i,
title: '内容' + (i + 1),
description: '内容' + (i + 1) + '的描述',
chosen: Math.random() * 2 > 1
};
if (data.chosen) {
targetKeys.push(data.key);
}
mockData.push(data);
}
this.setState({
mockData: mockData,
targetKeys: targetKeys,
});
},
handleChange(targetKeys) {
this.setState({
targetKeys: targetKeys,
});
},
renderFooter(props) {
return <Button type="primary" size="small" style={{ float: 'right', margin: '5' }}
onClick={this.getMock}>刷新</Button>;
},
render() {
return <div>
<Transfer
dataSource={this.state.mockData}
targetKeys={this.state.targetKeys}
onChange={this.handleChange}
render={(item) => { return item.title; }} />
</div>;
}
});
ReactDOM.render(<App />, container);
````
# 带搜索框
- order: 1
带搜索框的穿梭框。
---
````jsx
import { Transfer } from 'antd';
const container = document.getElementById('components-transfer-demo-search');
const App = React.createClass({
getInitialState() {
return {
mockData: [],
targetKeys: [],
};
},
componentDidMount() {
this.getMock();
},
getMock() {
let targetKeys = [];
let mockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i,
title: '内容' + (i + 1),
description: '内容' + (i + 1) + '的描述',
chosen: Math.random() * 2 > 1
};
if (data.chosen) {
targetKeys.push(data.key);
}
mockData.push(data);
}
this.setState({
mockData: mockData,
targetKeys: targetKeys,
});
},
handleChange(targetKeys) {
this.setState({
targetKeys: targetKeys,
});
},
render() {
return <div>
<Transfer
dataSource={this.state.mockData}
showSearch
targetKeys={this.state.targetKeys}
onChange={this.handleChange}
render={(item) => { return item.title;}} />
</div>;
}
});
ReactDOM.render(<App />, container);
````
import React, { Component, PropTypes } from 'react';
import List from './list.jsx';
import Operation from './operation.jsx';
import Search from './search.jsx';
import classNames from 'classnames';
function noop() {
}
class Transfer extends Component {
constructor(props) {
super(props);
this.state = {
leftFilter: '',
rightFilter: '',
leftCheckedKeys: [],
rightCheckedKeys: [],
};
}
splitDataSource() {
const { targetKeys, dataSource } = this.props;
let leftDataSource = Object.assign([], dataSource);
let rightDataSource = [];
if ( targetKeys.length > 0 ) {
targetKeys.forEach((targetKey) => {
rightDataSource.push(leftDataSource.find((data, index) => {
if( data.key === targetKey ) {
leftDataSource.splice(index, 1);
return true;
}
}));
});
}
return {
leftDataSource: leftDataSource,
rightDataSource: rightDataSource,
};
}
moveTo(direction) {
const { targetKeys } = this.props;
const { leftCheckedKeys, rightCheckedKeys } = this.state;
// move items to target box
const newTargetKeys = direction === 'right' ?
leftCheckedKeys.concat(targetKeys) :
targetKeys.filter((targetKey) => !rightCheckedKeys.some((checkedKey) => targetKey === checkedKey));
// empty checked keys
this.setState({
[direction === 'right' ? 'leftCheckedKeys' : 'rightCheckedKeys']: [],
});
this.props.onChange(newTargetKeys);
}
getGlobalCheckStatus(direction) {
const { leftDataSource, rightDataSource } = this.splitDataSource();
const { leftFilter, rightFilter, leftCheckedKeys, rightCheckedKeys } = this.state;
const dataSource = direction === 'left' ? leftDataSource : rightDataSource;
const filter = direction === 'left' ? leftFilter : rightFilter;
const checkedKeys = direction === 'left' ? leftCheckedKeys : rightCheckedKeys;
const filteredDataSource = this.filterDataSource(dataSource, filter);
let globalCheckStatus;
if ( checkedKeys.length > 0 ) {
if ( checkedKeys.length < filteredDataSource.length ) {
globalCheckStatus = 'part';
} else {
globalCheckStatus = 'all';
}
} else {
globalCheckStatus = 'none';
}
return globalCheckStatus;
}
filterDataSource(dataSource, filter) {
return dataSource.filter(item => {
const itemText = this.props.render(item);
return this.matchFilter(itemText, filter);
});
}
matchFilter(text, filterText) {
const regex = new RegExp(filterText);
return text.match(regex);
}
handleSelectAll(direction) {
const { leftDataSource, rightDataSource } = this.splitDataSource();
const { leftFilter, rightFilter } = this.state;
const dataSource = direction === 'left' ? leftDataSource : rightDataSource;
const filter = direction === 'left' ? leftFilter : rightFilter;
const checkStatus = this.getGlobalCheckStatus(direction);
let holder = [];
if ( checkStatus === 'all' ) {
holder = [];
} else {
holder = this.filterDataSource(dataSource, filter).map(item => item.key);
}
this.setState({
[direction + 'CheckedKeys']: holder,
});
}
handleFilter(direction, e) {
this.setState({
// deselect all
[direction + 'CheckedKeys']: [],
// add filter
[direction + 'Filter']: e.target.value,
});
}
handleClear(direction) {
this.setState({
[direction + 'Filter']: '',
});
}
handleSelect(direction, selectedItem, checked) {
const { leftCheckedKeys, rightCheckedKeys } = this.state;
const holder = direction === 'left' ? leftCheckedKeys : rightCheckedKeys;
const index = holder.findIndex((key) => key === selectedItem.key);
if ( index > -1 ) {
holder.splice(index, 1);
}
if ( checked ) {
holder.push(selectedItem.key);
}
this.setState({
[direction + 'CheckedKeys']: holder,
});
}
render() {
const {
prefixCls, titles, operations, showSearch,
searchPlaceholder, body, footer, listStyle, className,
} = this.props;
const { leftFilter, rightFilter, leftCheckedKeys, rightCheckedKeys } = this.state;
const { leftDataSource, rightDataSource } = this.splitDataSource();
const leftActive = rightCheckedKeys.length > 0;
const rightActive = leftCheckedKeys.length > 0;
const leftCheckStatus = this.getGlobalCheckStatus('left');
const rightCheckStatus = this.getGlobalCheckStatus('right');
const cls = classNames({
[className]: !!className,
prefixCls: true,
});
return <div className={cls}>
<List titleText={titles[0]}
dataSource={leftDataSource}
filter={leftFilter}
style={listStyle}
checkedKeys={leftCheckedKeys}
checkStatus={leftCheckStatus}
handleFilter={this.handleFilter.bind(this, 'left')}
handleClear={this.handleClear.bind(this, 'left')}
handleSelect={this.handleSelect.bind(this, 'left')}
handleSelectAll={this.handleSelectAll.bind(this, 'left')}
position="left"
render={this.props.render}
showSearch={showSearch}
searchPlaceholder={searchPlaceholder}
body={body}
footer={footer}
prefixCls={prefixCls + '-list'}
/>
<Operation rightActive={rightActive}
rightArrowText={operations[0]}
moveToRight={this.moveTo.bind(this, 'right')}
leftActive={leftActive}
leftArrowText={operations[1]}
moveToLeft={this.moveTo.bind(this, 'left')}
className={prefixCls + '-operation'}
/>
<List titleText={titles[1]}
dataSource={rightDataSource}
filter={rightFilter}
style={listStyle}
checkedKeys={rightCheckedKeys}
checkStatus={rightCheckStatus}
handleFilter={this.handleFilter.bind(this, 'right')}
handleClear={this.handleClear.bind(this, 'right')}
handleSelect={this.handleSelect.bind(this, 'right')}
handleSelectAll={this.handleSelectAll.bind(this, 'right')}
position="right"
render={this.props.render}
showSearch={showSearch}
searchPlaceholder={searchPlaceholder}
body={body}
footer={footer}
prefixCls={prefixCls + '-list'}
/>
</div>;
}
}
Transfer.defaultProps = {
prefixCls: 'ant-transfer',
dataSource: [],
render: noop,
targetKeys: [],
onChange: noop,
titles: ['源列表', '目的列表'],
operations: [],
showSearch: false,
searchPlaceholder: '请输入搜索内容',
body: noop,
footer: noop,
};
Transfer.propTypes = {
prefixCls: PropTypes.string,
dataSource: PropTypes.array,
render: PropTypes.func,
targetKeys: PropTypes.array,
onChange: PropTypes.func,
height: PropTypes.number,
listStyle: PropTypes.object,
className: PropTypes.string,
titles: PropTypes.array,
operations: PropTypes.array,
showSearch: PropTypes.bool,
searchPlaceholder: PropTypes.string,
body: PropTypes.func,
footer: PropTypes.func,
};
Transfer.List = List;
Transfer.Operation = Operation;
Transfer.Search = Search;
export default Transfer;
# Transfer
- category: Components
- chinese: 穿梭框
- type: 表单
- cols: 1
---
双栏穿梭选择框。
## 何时使用
用直观的方式在两栏中移动元素,完成选择行为。
## API
| 参数 | 说明 | 类型 | 默认值 |
|-----------|------------------------------------------|------------|--------|
| dataSource | 数据源 | Array | [] |
| render | 每行数据渲染函数 | Function(record) | |
| targetKeys | 显示在右侧框数据的key集合 | Array | [] |
| onChange | 变化时回调函数 | Function(newTargetKeys) | |
| listStyle | 两个穿梭框的自定义样式 | Object | |
| className | 自定义类 | String | |
| titles | 标题集合,顺序从左至右 | Array | ['源列表', '目的列表'] |
| operations | 操作文案集合,顺序从上至下 | Array | [] |
| showSearch | 是否显示搜索框 | Boolean | false |
| searchPlaceholder | 搜索框的默认值 | String | 请输入搜索的内容 |
| footer | 底部渲染函数 | Function(props) | | |
import React, { Component, PropTypes } from 'react';
import Checkbox from '../checkbox';
import Search from './search.jsx';
import classNames from 'classnames';
function noop() {
}
class TransferList extends Component {
constructor(props) {
super(props);
}
handleSelectALl() {
this.props.handleSelectAll();
}
handleSelect(selectedItem) {
const { checkedKeys } = this.props;
const result = checkedKeys.some((key) => key === selectedItem.key);
this.props.handleSelect(selectedItem, !result);
}
handleFilter(e) {
this.props.handleFilter(e);
}
handleClear() {
this.props.handleClear();
}
renderCheckbox(props) {
const { prefixCls } = props;
const checkboxCls = classNames({
[`${prefixCls}-checkbox`]: true,
[`${prefixCls}-checkbox-indeterminate`]: props.checkPart,
[`${prefixCls}-checkbox-checked`]: (!props.checkPart) && props.checked,
[`${prefixCls}-checkbox-disabled`]: !!props.disabled,
});
let customEle = null;
if (typeof props.checkable !== 'boolean') {
customEle = props.checkable;
}
return <span ref="checkbox"
className={checkboxCls}
onClick={(!props.disabled) && this.handleSelectALl.bind(this)}>
{customEle}
</span>;
}
matchFilter(text, filterText) {
const regex = new RegExp(filterText);
return text.match(regex);
}
render() {
const { prefixCls, dataSource, titleText, filter, checkedKeys, checkStatus, body, footer, showSearch } = this.props;
// Custom Layout
const footerDom = footer({...this.props});
const bodyDom = body({...this.props});
const listCls = classNames({
[prefixCls]: true,
[prefixCls + '-with-footer']: !!footerDom,
});
return <div className={listCls} {...this.props}>
<div className={`${prefixCls}-header`}>
{this.renderCheckbox({
prefixCls: 'ant-transfer',
checked: checkStatus === 'all',
checkPart: checkStatus === 'part',
checkable: <span className={`ant-transfer-checkbox-inner`}></span>
})}<span className={`${prefixCls}-header-selected`}><span>{(checkedKeys.length > 0 ? checkedKeys.length + '/' : '') + dataSource.length}</span>
<span className={`${prefixCls}-header-title`}>{titleText}</span></span>
</div>
{ bodyDom ? bodyDom :
<div className={ showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`}>
{ showSearch ? <div className={`${prefixCls}-body-search-wrapper`}>
<Search prefixCls={`${prefixCls}-search`} onChange={this.handleFilter.bind(this)} handleClear={this.handleClear.bind(this)} value={filter} />
</div> : null }
<ul>
{ dataSource.length > 0 ?
dataSource.map((item) => {
// apply filter
const itemText = this.props.render(item);
const filterResult = this.matchFilter(itemText, filter);
const renderedText = this.props.render(item);
if ( filterResult ) {
return <li onClick={this.handleSelect.bind(this, item)} key={item.key} title={renderedText}>
<Checkbox checked={checkedKeys.some((key) => key === item.key)} />
{ renderedText }
</li>;
}
}) : <div className={`${prefixCls}-body-not-found`}>
Not Found
</div>
}
</ul>
</div>}
{ footerDom ? <div className={`${prefixCls}-footer`}>
{ footerDom }
</div> : null }
</div>;
}
}
TransferList.defaultProps = {
dataSource: [],
titleText: '',
showSearch: false,
searchPlaceholder: '',
handleFilter: noop,
handleSelect: noop,
handleSelectAll: noop,
render: noop,
//advanced
body: noop,
footer: noop,
};
TransferList.propTypes = {
prefixCls: PropTypes.string,
dataSource: PropTypes.array,
showSearch: PropTypes.bool,
searchPlaceholder: PropTypes.string,
titleText: PropTypes.string,
style: PropTypes.object,
handleFilter: PropTypes.func,
handleSelect: PropTypes.func,
handleSelectAll: PropTypes.func,
render: PropTypes.func,
body: PropTypes.func,
footer: PropTypes.func,
};
export default TransferList;
import React, { Component, PropTypes } from 'react';
import Button from '../button';
import Icon from '../icon';
function noop() {
}
class TransferOperation extends Component {
render() {
const {
moveToLeft,
moveToRight,
leftArrowText,
rightArrowText,
leftActive,
rightActive,
className,
} = this.props;
const moveToLeftButton = (
<Button type="primary" size="small" disabled={!leftActive} onClick={moveToLeft}>
{<span><Icon type="left" />{leftArrowText}</span>}
</Button>
);
const moveToRightButton = (
<Button type="primary" size="small" disabled={!rightActive} onClick={moveToRight}>
{<span>{rightArrowText}<Icon type="right" /></span>}
</Button>
);
return <div className={className}>
{moveToLeftButton}
{moveToRightButton}
</div>;
}
}
TransferOperation.defaultProps = {
leftArrowText: '',
rightArrowText: '',
moveToLeft: noop,
moveToRight: noop,
};
TransferOperation.propTypes = {
className: PropTypes.string,
leftArrowText: PropTypes.string,
rightArrowText: PropTypes.string,
moveToLeft: PropTypes.func,
moveToRight: PropTypes.func,
};
export default TransferOperation;
import React, { Component, PropTypes } from 'react';
import Icon from '../icon';
function noop() {
}
class Search extends Component {
constructor(props) {
super(props);
}
handleChange(e) {
this.props.onChange(e);
}
render() {
const {placeholder, value, prefixCls} = this.props;
return <div>
<input placeholder={placeholder} className={ prefixCls + ' ant-input' } value={ value } ref="input"
onChange={this.handleChange.bind(this)}/>
{ value && value.length > 0 ?
<a href="javascirpt:;" className={ prefixCls + '-action' } onClick={this.props.handleClear}>
<Icon type="cross-circle" />
</a>
: <span className={ prefixCls + '-action' }><Icon type="search" /></span>
}
</div>;
}
}
Search.defaultProps = {
placeholder: '请输入搜索内容',
onChange: noop,
};
Search.propTypes = {
prefixCls: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func
};
export default Search;
......@@ -42,6 +42,7 @@ const antd = {
Input: require('./components/input'),
Calendar: require('./components/calendar'),
TimePicker: require('./components/time-picker'),
Transfer: require('./components/transfer'),
};
antd.version = require('./package.json').version;
......
......@@ -37,3 +37,4 @@
@import "spin";
@import "calendar";
@import "timepicker";
@import "transfer";
@transfer-prefix-cls: ~"@{css-prefix}transfer";
.antCheckboxFn(@checkbox-prefix-cls: ant-transfer-checkbox);
.@{transfer-prefix-cls} {
position: relative;
&-list {
font-size: 12px;
border: 1px solid @border-color-base;
display: inline-block;
border-radius: @border-radius-base;
vertical-align: middle;
position: relative;
width: 160px;
height: 200px;
padding-top: 33px;
&-with-footer {
padding-bottom: 33px;
}
&-search {
&-action {
color: #ccc;
position: absolute;
top: 2px;
right: 2px;
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
font-size: 14px;
}
}
&-header {
padding: 7px 16px;
border-radius: @border-radius-base @border-radius-base 0 0;
background: #fff;
color: #666;
border-bottom: 1px solid @border-color-split;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
&-title {
float: right;
}
}
&-body {
font-size: 12px;
line-height: 1.5;
position: relative;
height: 100%;
&-search-wrapper {
position: absolute;
top: 0;
left: 0;
height: 28px;
padding: 4px;
width: 100%;
}
&-not-found {
margin-top: 24px;
color: #ccc;
text-align: center;
}
ul {
height: 100%;
overflow: auto;
li {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 7px 16px;
transition: all 0.3s ease;
&:hover {
cursor: pointer;
background-color: tint(@primary-color, 90%);
}
}
}
}
&-body-with-search {
padding-top: 34px;
}
&-footer {
border-top: 1px solid @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-btn {
float: left;
clear: both;
&:first-child {
margin-bottom: 4px;
}
.anticon {
.iconfont-size-under-12px(10px);
}
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册