mirror of
https://github.com/ant-design/ant-design.git
synced 2025-01-18 14:13:37 +08:00
Merge pull request #735 from ant-design/component-transfer
Component transfer
This commit is contained in:
commit
6c1f601ab8
76
components/transfer/demo/advanced.md
Normal file
76
components/transfer/demo/advanced.md
Normal file
@ -0,0 +1,76 @@
|
||||
# 高级用法
|
||||
|
||||
- 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);
|
||||
````
|
69
components/transfer/demo/basic.md
Normal file
69
components/transfer/demo/basic.md
Normal file
@ -0,0 +1,69 @@
|
||||
# 基本用法
|
||||
|
||||
- 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);
|
||||
````
|
65
components/transfer/demo/search.md
Normal file
65
components/transfer/demo/search.md
Normal file
@ -0,0 +1,65 @@
|
||||
# 带搜索框
|
||||
|
||||
- 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);
|
||||
````
|
249
components/transfer/index.jsx
Normal file
249
components/transfer/index.jsx
Normal file
@ -0,0 +1,249 @@
|
||||
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;
|
31
components/transfer/index.md
Normal file
31
components/transfer/index.md
Normal file
@ -0,0 +1,31 @@
|
||||
# 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) | | |
|
141
components/transfer/list.jsx
Normal file
141
components/transfer/list.jsx
Normal file
@ -0,0 +1,141 @@
|
||||
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;
|
52
components/transfer/operation.jsx
Normal file
52
components/transfer/operation.jsx
Normal file
@ -0,0 +1,52 @@
|
||||
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;
|
41
components/transfer/search.jsx
Normal file
41
components/transfer/search.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
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;
|
1
index.js
1
index.js
@ -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";
|
||||
|
124
style/components/transfer.less
Normal file
124
style/components/transfer.less
Normal file
@ -0,0 +1,124 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user