mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-25 11:40:04 +08:00
Merge pull request #16457 from ant-design/resolve-feature-conflict
Resolve feature conflict
This commit is contained in:
commit
896011f586
@ -564,7 +564,9 @@
|
||||
|
||||
// Transfer
|
||||
// ---
|
||||
@transfer-header-height: 40px;
|
||||
@transfer-disabled-bg: @disabled-bg;
|
||||
@transfer-list-height: 200px;
|
||||
|
||||
// Message
|
||||
// ---
|
||||
|
@ -4,7 +4,7 @@ import PureRenderMixin from 'rc-util/lib/PureRenderMixin';
|
||||
import Lazyload from 'react-lazy-load';
|
||||
import Checkbox from '../checkbox';
|
||||
|
||||
export default class Item extends React.Component<any, any> {
|
||||
export default class ListItem extends React.Component<any, any> {
|
||||
shouldComponentUpdate(...args: any[]) {
|
||||
return PureRenderMixin.shouldComponentUpdate.apply(this, args);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`List should render correctly 1`] = `
|
||||
exports[`Transfer.List should render correctly 1`] = `
|
||||
<div
|
||||
class="ant-transfer-list"
|
||||
>
|
||||
@ -16,6 +16,7 @@ exports[`List should render correctly 1`] = `
|
||||
<input
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
@ -52,6 +53,7 @@ exports[`List should render correctly 1`] = `
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
@ -72,6 +74,7 @@ exports[`List should render correctly 1`] = `
|
||||
<input
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
@ -93,6 +96,7 @@ exports[`List should render correctly 1`] = `
|
||||
class="ant-checkbox-input"
|
||||
disabled=""
|
||||
type="checkbox"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Search should show cross icon when input value exists 1`] = `
|
||||
exports[`Transfer.Search should show cross icon when input value exists 1`] = `
|
||||
<Search
|
||||
placeholder=""
|
||||
value=""
|
||||
@ -65,7 +65,7 @@ exports[`Search should show cross icon when input value exists 1`] = `
|
||||
</Search>
|
||||
`;
|
||||
|
||||
exports[`Search should show cross icon when input value exists 2`] = `
|
||||
exports[`Transfer.Search should show cross icon when input value exists 2`] = `
|
||||
<Search
|
||||
placeholder=""
|
||||
value="a"
|
||||
|
77
components/transfer/__tests__/customize.test.js
Normal file
77
components/transfer/__tests__/customize.test.js
Normal file
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Transfer from '../index';
|
||||
|
||||
describe('Transfer.Customize', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
afterEach(() => {
|
||||
errorSpy.mockReset();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should warning use body', () => {
|
||||
mount(<Transfer body={() => null} />);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Transfer] `body` is internal usage and will bre removed, please use `children` instead.',
|
||||
);
|
||||
});
|
||||
|
||||
describe('deprecated function', () => {
|
||||
const dataSource = [];
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
dataSource.push({
|
||||
key: i.toString(),
|
||||
});
|
||||
}
|
||||
const commonProps = {
|
||||
dataSource,
|
||||
selectedKeys: ['1'],
|
||||
targetKeys: ['2'],
|
||||
};
|
||||
|
||||
it('should not exist in render props', () => {
|
||||
mount(
|
||||
<Transfer {...commonProps}>
|
||||
{props => {
|
||||
expect('handleFilter' in props).toBeFalsy();
|
||||
expect('handleSelect' in props).toBeFalsy();
|
||||
expect('handleSelectAll' in props).toBeFalsy();
|
||||
expect('handleClear' in props).toBeFalsy();
|
||||
expect('body' in props).toBeFalsy();
|
||||
expect('checkedKeys' in props).toBeFalsy();
|
||||
}}
|
||||
</Transfer>,
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn if called in body', () => {
|
||||
let init = true;
|
||||
|
||||
mount(
|
||||
<Transfer
|
||||
{...commonProps}
|
||||
body={({ handleSelect, handleSelectAll }) => {
|
||||
if (init) {
|
||||
handleSelect('', true);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Transfer] `handleSelect` will be removed, please use `onSelect` instead.',
|
||||
);
|
||||
errorSpy.mockReset();
|
||||
handleSelectAll([], true);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Transfer] `handleSelectAll` will be removed, please use `onSelectAll` instead.',
|
||||
);
|
||||
}
|
||||
init = false;
|
||||
return null;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -5,7 +5,7 @@ import Transfer from '..';
|
||||
import TransferList from '../list';
|
||||
import TransferOperation from '../operation';
|
||||
import TransferSearch from '../search';
|
||||
import TransferItem from '../item';
|
||||
import TransferItem from '../ListItem';
|
||||
import Button from '../../button';
|
||||
import Checkbox from '../../checkbox';
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, mount } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
import List from '../list';
|
||||
import Checkbox from '../../checkbox';
|
||||
|
||||
@ -25,10 +25,21 @@ const listCommonProps = {
|
||||
lazy: false,
|
||||
};
|
||||
|
||||
describe('List', () => {
|
||||
describe('Transfer.List', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render correctly', () => {
|
||||
const wrapper = render(<List {...listCommonProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const wrapper = mount(<List {...listCommonProps} />);
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ListBody').state().mounted).toBeTruthy();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should check top Checkbox while all available items are checked', () => {
|
||||
|
@ -3,7 +3,7 @@ import { mount } from 'enzyme';
|
||||
import Search from '../search';
|
||||
import Transfer from '../index';
|
||||
|
||||
describe('Search', () => {
|
||||
describe('Transfer.Search', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
afterEach(() => {
|
||||
|
161
components/transfer/demo/table-transfer.md
Normal file
161
components/transfer/demo/table-transfer.md
Normal file
@ -0,0 +1,161 @@
|
||||
---
|
||||
order: 5
|
||||
title:
|
||||
zh-CN: 表格穿梭框
|
||||
en-US: Table Transfer
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
使用 Table 组件作为自定义渲染列表。
|
||||
|
||||
## en-US
|
||||
|
||||
Customize render list with Table component.
|
||||
|
||||
```jsx
|
||||
import { Transfer, Switch, Table, Tag } from 'antd';
|
||||
import difference from 'lodash/difference';
|
||||
|
||||
// Customize Table Transfer
|
||||
const TableTransfer = ({ leftColumns, rightColumns, ...restProps }) => (
|
||||
<Transfer {...restProps} showSelectAll={false}>
|
||||
{({
|
||||
direction,
|
||||
filteredItems,
|
||||
onItemSelectAll,
|
||||
onItemSelect,
|
||||
selectedKeys: listSelectedKeys,
|
||||
disabled: listDisabled,
|
||||
}) => {
|
||||
const columns = direction === 'left' ? leftColumns : rightColumns;
|
||||
|
||||
const rowSelection = {
|
||||
getCheckboxProps: item => ({ disabled: listDisabled || item.disabled }),
|
||||
onSelectAll(selected, selectedRows) {
|
||||
const treeSelectedKeys = selectedRows
|
||||
.filter(item => !item.disabled)
|
||||
.map(({ key }) => key);
|
||||
const diffKeys = selected
|
||||
? difference(treeSelectedKeys, listSelectedKeys)
|
||||
: difference(listSelectedKeys, treeSelectedKeys);
|
||||
onItemSelectAll(diffKeys, selected);
|
||||
},
|
||||
onSelect({ key }, selected) {
|
||||
onItemSelect(key, selected);
|
||||
},
|
||||
selectedRowKeys: listSelectedKeys,
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredItems}
|
||||
size="small"
|
||||
style={{ pointerEvents: listDisabled ? 'none' : null }}
|
||||
onRow={({ key, disabled: itemDisabled }) => ({
|
||||
onClick: () => {
|
||||
if (itemDisabled || listDisabled) return;
|
||||
onItemSelect(key, !listSelectedKeys.includes(key));
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Transfer>
|
||||
);
|
||||
|
||||
const mockTags = ['cat', 'dog', 'bird'];
|
||||
|
||||
const mockData = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
mockData.push({
|
||||
key: i.toString(),
|
||||
title: `content${i + 1}`,
|
||||
description: `description of content${i + 1}`,
|
||||
disabled: i % 4 === 0,
|
||||
tag: mockTags[i % 3],
|
||||
});
|
||||
}
|
||||
|
||||
const originTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
|
||||
|
||||
const leftTableColumns = [
|
||||
{
|
||||
dataIndex: 'title',
|
||||
title: 'Name',
|
||||
},
|
||||
{
|
||||
dataIndex: 'tag',
|
||||
title: 'Tag',
|
||||
render: tag => <Tag>{tag}</Tag>,
|
||||
},
|
||||
{
|
||||
dataIndex: 'description',
|
||||
title: 'Description',
|
||||
},
|
||||
];
|
||||
const rightTableColumns = [
|
||||
{
|
||||
dataIndex: 'title',
|
||||
title: 'Name',
|
||||
},
|
||||
];
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
targetKeys: originTargetKeys,
|
||||
disabled: false,
|
||||
showSearch: false,
|
||||
};
|
||||
|
||||
onChange = nextTargetKeys => {
|
||||
this.setState({ targetKeys: nextTargetKeys });
|
||||
};
|
||||
|
||||
triggerDisable = disabled => {
|
||||
this.setState({ disabled });
|
||||
};
|
||||
|
||||
triggerShowSearch = showSearch => {
|
||||
this.setState({ showSearch });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { targetKeys, disabled, showSearch } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TableTransfer
|
||||
dataSource={mockData}
|
||||
targetKeys={targetKeys}
|
||||
disabled={disabled}
|
||||
showSearch={showSearch}
|
||||
onChange={this.onChange}
|
||||
filterOption={(inputValue, item) =>
|
||||
item.title.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1
|
||||
}
|
||||
leftColumns={leftTableColumns}
|
||||
rightColumns={rightTableColumns}
|
||||
/>
|
||||
<Switch
|
||||
unCheckedChildren="disabled"
|
||||
checkedChildren="disabled"
|
||||
checked={disabled}
|
||||
onChange={this.triggerDisable}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
<Switch
|
||||
unCheckedChildren="showSearch"
|
||||
checkedChildren="showSearch"
|
||||
checked={showSearch}
|
||||
onChange={this.triggerShowSearch}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
```
|
131
components/transfer/demo/tree-transfer.md
Normal file
131
components/transfer/demo/tree-transfer.md
Normal file
@ -0,0 +1,131 @@
|
||||
---
|
||||
order: 6
|
||||
title:
|
||||
zh-CN: 树穿梭框
|
||||
en-US: Tree Transfer
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
使用 Tree 组件作为自定义渲染列表。
|
||||
|
||||
## en-US
|
||||
|
||||
Customize render list with Tree component.
|
||||
|
||||
```jsx
|
||||
import { Transfer, Tree } from 'antd';
|
||||
|
||||
const { TreeNode } = Tree;
|
||||
|
||||
// Customize Table Transfer
|
||||
const isChecked = (selectedKeys, eventKey) => {
|
||||
return selectedKeys.indexOf(eventKey) !== -1;
|
||||
};
|
||||
|
||||
const generateTree = (treeNodes = [], checkedKeys = []) => {
|
||||
return treeNodes.map(({ children, ...props }) => (
|
||||
<TreeNode {...props} disabled={checkedKeys.includes(props.key)}>
|
||||
{generateTree(children, checkedKeys)}
|
||||
</TreeNode>
|
||||
));
|
||||
};
|
||||
|
||||
const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
|
||||
const transferDataSource = [];
|
||||
function flatten(list = []) {
|
||||
list.forEach(item => {
|
||||
transferDataSource.push(item);
|
||||
flatten(item.children);
|
||||
});
|
||||
}
|
||||
flatten(dataSource);
|
||||
|
||||
return (
|
||||
<Transfer
|
||||
{...restProps}
|
||||
targetKeys={targetKeys}
|
||||
dataSource={transferDataSource}
|
||||
className="tree-transfer"
|
||||
render={item => item.title}
|
||||
showSelectAll={false}
|
||||
>
|
||||
{({ direction, onItemSelect, selectedKeys }) => {
|
||||
if (direction === 'left') {
|
||||
const checkedKeys = [...selectedKeys, ...targetKeys];
|
||||
return (
|
||||
<Tree
|
||||
blockNode
|
||||
checkable
|
||||
checkStrictly
|
||||
defaultExpandAll
|
||||
checkedKeys={checkedKeys}
|
||||
onCheck={(
|
||||
_,
|
||||
{
|
||||
node: {
|
||||
props: { eventKey },
|
||||
},
|
||||
},
|
||||
) => {
|
||||
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
|
||||
}}
|
||||
onSelect={(
|
||||
_,
|
||||
{
|
||||
node: {
|
||||
props: { eventKey },
|
||||
},
|
||||
},
|
||||
) => {
|
||||
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
|
||||
}}
|
||||
>
|
||||
{generateTree(dataSource, targetKeys)}
|
||||
</Tree>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</Transfer>
|
||||
);
|
||||
};
|
||||
|
||||
const treeData = [
|
||||
{ key: '0-0', title: '0-0' },
|
||||
{
|
||||
key: '0-1',
|
||||
title: '0-1',
|
||||
children: [{ key: '0-1-0', title: '0-1-0' }, { key: '0-1-1', title: '0-1-1' }],
|
||||
},
|
||||
{ key: '0-2', title: '0-3' },
|
||||
];
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
targetKeys: [],
|
||||
};
|
||||
|
||||
onChange = targetKeys => {
|
||||
console.log('Target Keys:', targetKeys);
|
||||
this.setState({ targetKeys });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { targetKeys } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={this.onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
```
|
||||
|
||||
<style>
|
||||
.tree-transfer .ant-transfer-list:first-child {
|
||||
width: 50%;
|
||||
flex: none;
|
||||
}
|
||||
</style>
|
@ -18,28 +18,48 @@ One or more elements can be selected from either column, one click on the proper
|
||||
|
||||
## 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)\[] | \[] |
|
||||
| disabled | Whether disabled transfer | boolean | false |
|
||||
| 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 | |
|
||||
| locale | i18n text including filter, empty text, item unit, etc | object | `{ itemUnit: 'item', itemsUnit: 'items', notFoundContent: 'The list is empty', searchPlaceholder: 'Search here' }` |
|
||||
| operations | A set of operations that are sorted from top to bottom. | string\[] | \['>', '<'] |
|
||||
| operationStyle | A custom CSS style used for rendering the operations column. | object | |
|
||||
| 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) | |
|
||||
| selectedKeys | A set of keys of selected items. | string\[] | \[] |
|
||||
| showSearch | If included, a search box is shown on each column. | boolean | false |
|
||||
| style | A custom CSS style used for rendering wrapper element. | object | |
|
||||
| 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 | |
|
||||
| onSearch | A callback function which is executed when search field are changed | (direction: 'left'\|'right', value: string): void | - |
|
||||
| onSelectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | |
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 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)\[] | \[] | |
|
||||
| disabled | Whether disabled transfer | boolean | false | |
|
||||
| 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 | | |
|
||||
| locale | i18n text including filter, empty text, item unit, etc | object | `{ itemUnit: 'item', itemsUnit: 'items', notFoundContent: 'The list is empty', searchPlaceholder: 'Search here' }` | |
|
||||
| operations | A set of operations that are sorted from top to bottom. | string\[] | \['>', '<'] | |
|
||||
| operationStyle | A custom CSS style used for rendering the operations column. | object | | |
|
||||
| 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) | | |
|
||||
| selectedKeys | A set of keys of selected items. | string\[] | \[] | |
|
||||
| showSearch | If included, a search box is shown on each column. | boolean | false | |
|
||||
| showSelectAll | Show select all checkbox on the header | boolean | true | 3.17.0 |
|
||||
| style | A custom CSS style used for rendering wrapper element. | object | | |
|
||||
| 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 | | |
|
||||
| onSearch | A callback function which is executed when search field are changed | (direction: 'left'\|'right', value: string): void | - | |
|
||||
| onSelectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | | |
|
||||
|
||||
### Render Props
|
||||
|
||||
New in 3.17.0. Transfer accept `children` to customize render list, using follow props:
|
||||
|
||||
| Property | Description | Type | Version |
|
||||
| --------------- | ----------------------- | ----------------------------------- | ------- |
|
||||
| direction | List render direction | 'left' \| 'right' | 3.17.0 |
|
||||
| disabled | Disable list or not | boolean | 3.17.0 |
|
||||
| filteredItems | Filtered items | TransferItem[] | 3.17.0 |
|
||||
| onItemSelect | Select item | (key: string, selected: boolean) | 3.17.0 |
|
||||
| onItemSelectAll | Select a group of items | (keys: string[], selected: boolean) | 3.17.0 |
|
||||
| selectedKeys | Selected items | string[] | 3.17.0 |
|
||||
|
||||
#### example
|
||||
|
||||
```jsx
|
||||
<Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>
|
||||
```
|
||||
|
||||
## Warning
|
||||
|
||||
|
@ -9,21 +9,29 @@ import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
||||
import defaultLocale from '../locale-provider/default';
|
||||
import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
import { TransferListBodyProps } from './renderListBody';
|
||||
|
||||
export { TransferListProps } from './list';
|
||||
export { TransferOperationProps } from './operation';
|
||||
export { TransferSearchProps } from './search';
|
||||
|
||||
function noop() {}
|
||||
|
||||
export type TransferDirection = 'left' | 'right';
|
||||
type TransferRender = (record: TransferItem) => React.ReactNode;
|
||||
|
||||
export interface RenderResultObject {
|
||||
label: React.ReactElement;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type RenderResult = React.ReactElement | RenderResultObject | string | null;
|
||||
|
||||
type TransferRender = (item: TransferItem) => RenderResult;
|
||||
|
||||
export interface TransferItem {
|
||||
key: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
export interface TransferProps {
|
||||
@ -53,6 +61,8 @@ export interface TransferProps {
|
||||
onSearch?: (direction: TransferDirection, value: string) => void;
|
||||
lazy?: {} | boolean;
|
||||
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLDivElement>) => void;
|
||||
children?: (props: TransferListBodyProps) => React.ReactNode;
|
||||
showSelectAll?: boolean;
|
||||
}
|
||||
|
||||
export interface TransferLocale {
|
||||
@ -71,7 +81,6 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
|
||||
static defaultProps = {
|
||||
dataSource: [],
|
||||
render: noop as TransferRender,
|
||||
locale: {},
|
||||
showSearch: false,
|
||||
};
|
||||
@ -127,10 +136,14 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
'please use `locale` instead.',
|
||||
);
|
||||
|
||||
warning(
|
||||
!('body' in props),
|
||||
'Transfer',
|
||||
'`body` is internal usage and will bre removed, please use `children` instead.',
|
||||
);
|
||||
|
||||
const { selectedKeys = [], targetKeys = [] } = props;
|
||||
this.state = {
|
||||
leftFilter: '',
|
||||
rightFilter: '',
|
||||
sourceSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) === -1),
|
||||
targetSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) > -1),
|
||||
};
|
||||
@ -205,43 +218,57 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelectAll = (direction: TransferDirection, selectedKeys: string[], checkAll: boolean) => {
|
||||
const originalSelectedKeys = this.state[this.getSelectedKeysName(direction)] || [];
|
||||
|
||||
let mergedCheckedKeys = [];
|
||||
if (checkAll) {
|
||||
// Merge current keys with origin key
|
||||
mergedCheckedKeys = Array.from(new Set([...originalSelectedKeys, ...selectedKeys]));
|
||||
} else {
|
||||
// Remove current keys from origin keys
|
||||
mergedCheckedKeys = originalSelectedKeys.filter(
|
||||
(key: string) => selectedKeys.indexOf(key) === -1,
|
||||
);
|
||||
}
|
||||
|
||||
this.handleSelectChange(direction, mergedCheckedKeys);
|
||||
|
||||
if (!this.props.selectedKeys) {
|
||||
this.setState({
|
||||
[this.getSelectedKeysName(direction)]: mergedCheckedKeys,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectAll = (
|
||||
direction: TransferDirection,
|
||||
filteredDataSource: TransferItem[],
|
||||
checkAll: boolean,
|
||||
) => {
|
||||
const originalSelectedKeys = this.state[this.getSelectedKeysName(direction)] || [];
|
||||
const currentKeys = filteredDataSource.map(item => item.key);
|
||||
// Only operate current keys from original selected keys
|
||||
const newKeys1 = originalSelectedKeys.filter((key: string) => 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.props.selectedKeys) {
|
||||
this.setState({
|
||||
[this.getSelectedKeysName(direction)]: holder,
|
||||
});
|
||||
}
|
||||
warning(
|
||||
false,
|
||||
'Transfer',
|
||||
'`handleSelectAll` will be removed, please use `onSelectAll` instead.',
|
||||
);
|
||||
this.onItemSelectAll(direction, filteredDataSource.map(({ key }) => key), !checkAll);
|
||||
};
|
||||
|
||||
// [Legacy] Old prop `body` pass origin check as arg. It's confusing.
|
||||
// TODO: Remove this in next version.
|
||||
handleLeftSelectAll = (filteredDataSource: TransferItem[], checkAll: boolean) =>
|
||||
this.handleSelectAll('left', filteredDataSource, checkAll);
|
||||
this.handleSelectAll('left', filteredDataSource, !checkAll);
|
||||
handleRightSelectAll = (filteredDataSource: TransferItem[], checkAll: boolean) =>
|
||||
this.handleSelectAll('right', filteredDataSource, checkAll);
|
||||
this.handleSelectAll('right', filteredDataSource, !checkAll);
|
||||
|
||||
onLeftItemSelectAll = (selectedKeys: string[], checkAll: boolean) =>
|
||||
this.onItemSelectAll('left', selectedKeys, checkAll);
|
||||
onRightItemSelectAll = (selectedKeys: string[], checkAll: boolean) =>
|
||||
this.onItemSelectAll('right', selectedKeys, checkAll);
|
||||
|
||||
handleFilter = (direction: TransferDirection, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { onSearchChange, onSearch } = this.props;
|
||||
const value = e.target.value;
|
||||
this.setState({
|
||||
// add filter
|
||||
[`${direction}Filter`]: value,
|
||||
});
|
||||
if (onSearchChange) {
|
||||
warning(false, 'Transfer', '`onSearchChange` is deprecated. Please use `onSearch` instead.');
|
||||
onSearchChange(direction, e);
|
||||
@ -256,9 +283,6 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
|
||||
handleClear = (direction: TransferDirection) => {
|
||||
const { onSearch } = this.props;
|
||||
this.setState({
|
||||
[`${direction}Filter`]: '',
|
||||
});
|
||||
if (onSearch) {
|
||||
onSearch(direction, '');
|
||||
}
|
||||
@ -267,15 +291,15 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
handleLeftClear = () => this.handleClear('left');
|
||||
handleRightClear = () => this.handleClear('right');
|
||||
|
||||
handleSelect = (direction: TransferDirection, selectedItem: TransferItem, checked: boolean) => {
|
||||
onItemSelect = (direction: TransferDirection, selectedKey: string, checked: boolean) => {
|
||||
const { sourceSelectedKeys, targetSelectedKeys } = this.state;
|
||||
const holder = direction === 'left' ? [...sourceSelectedKeys] : [...targetSelectedKeys];
|
||||
const index = holder.indexOf(selectedItem.key);
|
||||
const index = holder.indexOf(selectedKey);
|
||||
if (index > -1) {
|
||||
holder.splice(index, 1);
|
||||
}
|
||||
if (checked) {
|
||||
holder.push(selectedItem.key);
|
||||
holder.push(selectedKey);
|
||||
}
|
||||
this.handleSelectChange(direction, holder);
|
||||
|
||||
@ -286,13 +310,19 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
}
|
||||
};
|
||||
|
||||
handleLeftSelect = (selectedItem: TransferItem, checked: boolean) => {
|
||||
return this.handleSelect('left', selectedItem, checked);
|
||||
handleSelect = (direction: TransferDirection, selectedItem: TransferItem, checked: boolean) => {
|
||||
warning(false, 'Transfer', '`handleSelect` will be removed, please use `onSelect` instead.');
|
||||
this.onItemSelect(direction, selectedItem.key, checked);
|
||||
};
|
||||
handleLeftSelect = (selectedItem: TransferItem, checked: boolean) =>
|
||||
this.handleSelect('left', selectedItem, checked);
|
||||
handleRightSelect = (selectedItem: TransferItem, checked: boolean) =>
|
||||
this.handleSelect('right', selectedItem, checked);
|
||||
|
||||
handleRightSelect = (selectedItem: TransferItem, checked: boolean) => {
|
||||
return this.handleSelect('right', selectedItem, checked);
|
||||
};
|
||||
onLeftItemSelect = (selectedKey: string, checked: boolean) =>
|
||||
this.onItemSelect('left', selectedKey, checked);
|
||||
onRightItemSelect = (selectedKey: string, checked: boolean) =>
|
||||
this.onItemSelect('right', selectedKey, checked);
|
||||
|
||||
handleScroll = (direction: TransferDirection, e: React.SyntheticEvent<HTMLDivElement>) => {
|
||||
const { onScroll } = this.props;
|
||||
@ -348,16 +378,21 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
filterOption,
|
||||
render,
|
||||
lazy,
|
||||
children,
|
||||
showSelectAll,
|
||||
} = this.props;
|
||||
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
|
||||
const locale = this.getLocale(transferLocale, renderEmpty);
|
||||
const { leftFilter, rightFilter, sourceSelectedKeys, targetSelectedKeys } = this.state;
|
||||
const { sourceSelectedKeys, targetSelectedKeys } = this.state;
|
||||
|
||||
const { leftDataSource, rightDataSource } = this.separateDataSource(this.props);
|
||||
const leftActive = targetSelectedKeys.length > 0;
|
||||
const rightActive = sourceSelectedKeys.length > 0;
|
||||
|
||||
const cls = classNames(className, prefixCls, disabled && `${prefixCls}-disabled`);
|
||||
const cls = classNames(className, prefixCls, {
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[`${prefixCls}-customize-list`]: !!children,
|
||||
});
|
||||
|
||||
const titles = this.getTitles(locale);
|
||||
return (
|
||||
@ -366,7 +401,6 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
prefixCls={`${prefixCls}-list`}
|
||||
titleText={titles[0]}
|
||||
dataSource={leftDataSource}
|
||||
filter={leftFilter}
|
||||
filterOption={filterOption}
|
||||
style={listStyle}
|
||||
checkedKeys={sourceSelectedKeys}
|
||||
@ -374,13 +408,18 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
handleClear={this.handleLeftClear}
|
||||
handleSelect={this.handleLeftSelect}
|
||||
handleSelectAll={this.handleLeftSelectAll}
|
||||
onItemSelect={this.onLeftItemSelect}
|
||||
onItemSelectAll={this.onLeftItemSelectAll}
|
||||
render={render}
|
||||
showSearch={showSearch}
|
||||
body={body}
|
||||
renderList={children}
|
||||
footer={footer}
|
||||
lazy={lazy}
|
||||
onScroll={this.handleLeftScroll}
|
||||
disabled={disabled}
|
||||
direction="left"
|
||||
showSelectAll={showSelectAll}
|
||||
{...locale}
|
||||
/>
|
||||
<Operation
|
||||
@ -398,7 +437,6 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
prefixCls={`${prefixCls}-list`}
|
||||
titleText={titles[1]}
|
||||
dataSource={rightDataSource}
|
||||
filter={rightFilter}
|
||||
filterOption={filterOption}
|
||||
style={listStyle}
|
||||
checkedKeys={targetSelectedKeys}
|
||||
@ -406,13 +444,18 @@ class Transfer extends React.Component<TransferProps, any> {
|
||||
handleClear={this.handleRightClear}
|
||||
handleSelect={this.handleRightSelect}
|
||||
handleSelectAll={this.handleRightSelectAll}
|
||||
onItemSelect={this.onRightItemSelect}
|
||||
onItemSelectAll={this.onRightItemSelectAll}
|
||||
render={render}
|
||||
showSearch={showSearch}
|
||||
body={body}
|
||||
renderList={children}
|
||||
footer={footer}
|
||||
lazy={lazy}
|
||||
onScroll={this.handleRightScroll}
|
||||
disabled={disabled}
|
||||
direction="right"
|
||||
showSelectAll={showSelectAll}
|
||||
{...locale}
|
||||
/>
|
||||
</div>
|
||||
|
@ -19,26 +19,48 @@ title: Transfer
|
||||
|
||||
## API
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| className | 自定义类 | string | |
|
||||
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] |
|
||||
| disabled | 是否禁用 | boolean | false |
|
||||
| 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 | |
|
||||
| locale | 各种语言 | object | `{ itemUnit: '项', itemsUnit: '项', notFoundContent: '列表为空', searchPlaceholder: '请输入搜索内容' }` |
|
||||
| operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] |
|
||||
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 `label` 字段为 ReactElement,`value` 字段为 title | Function(record) | |
|
||||
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] |
|
||||
| showSearch | 是否显示搜索框 | boolean | false |
|
||||
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] |
|
||||
| titles | 标题集合,顺序从左至右 | string\[] | \['', ''] |
|
||||
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | |
|
||||
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | |
|
||||
| onSearch | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', value: string): void | - |
|
||||
| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | |
|
||||
### Transfer
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| className | 自定义类 | string | | |
|
||||
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] | |
|
||||
| disabled | 是否禁用 | boolean | false | |
|
||||
| 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 | | |
|
||||
| locale | 各种语言 | object | `{ itemUnit: '项', itemsUnit: '项', notFoundContent: '列表为空', searchPlaceholder: '请输入搜索内容' }` | |
|
||||
| operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] | |
|
||||
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 `label` 字段为 ReactElement,`value` 字段为 title | Function(record) | | |
|
||||
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | |
|
||||
| showSearch | 是否显示搜索框 | boolean | false | |
|
||||
| showSelectAll | 是否展示全选勾选框 | boolean | true | 3.17.0 |
|
||||
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
|
||||
| titles | 标题集合,顺序从左至右 | string\[] | \['', ''] | |
|
||||
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | | |
|
||||
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | | |
|
||||
| onSearch | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', value: string): void | - | |
|
||||
| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | | |
|
||||
|
||||
### Render Props
|
||||
|
||||
3.17.0 新增。Transfer 支持接收 `children` 自定义渲染列表,并返回以下参数:
|
||||
|
||||
| 参数 | 说明 | 类型 | 版本 |
|
||||
| --------------- | -------------- | ----------------------------------- | ------ |
|
||||
| direction | 渲染列表的方向 | 'left' \| 'right' | 3.17.0 |
|
||||
| disabled | 是否禁用列表 | boolean | 3.17.0 |
|
||||
| filteredItems | 过滤后的数据 | TransferItem[] | 3.17.0 |
|
||||
| onItemSelect | 勾选条目 | (key: string, selected: boolean) | 3.17.0 |
|
||||
| onItemSelectAll | 勾选一组条目 | (keys: string[], selected: boolean) | 3.17.0 |
|
||||
| selectedKeys | 选中的条目 | string[] | 3.17.0 |
|
||||
|
||||
#### 参考示例
|
||||
|
||||
```jsx
|
||||
<Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import Animate from 'rc-animate';
|
||||
import PureRenderMixin from 'rc-util/lib/PureRenderMixin';
|
||||
import Checkbox from '../checkbox';
|
||||
import { TransferItem } from './index';
|
||||
import { TransferItem, TransferDirection, RenderResult, RenderResultObject } from './index';
|
||||
import Search from './search';
|
||||
import Item from './item';
|
||||
import defaultRenderList, { TransferListBodyProps, OmitProps } from './renderListBody';
|
||||
import triggerEvent from '../_util/triggerEvent';
|
||||
|
||||
function noop() {}
|
||||
const defaultRender = () => null;
|
||||
|
||||
function isRenderResultPlainObject(result: any) {
|
||||
function isRenderResultPlainObject(result: RenderResult) {
|
||||
return (
|
||||
result &&
|
||||
!React.isValidElement(result) &&
|
||||
@ -19,61 +19,80 @@ function isRenderResultPlainObject(result: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export interface RenderedItem {
|
||||
renderedText: string;
|
||||
renderedEl: React.ReactNode;
|
||||
item: TransferItem;
|
||||
}
|
||||
|
||||
type RenderListFunction = (props: TransferListBodyProps) => React.ReactNode;
|
||||
|
||||
export interface TransferListProps {
|
||||
prefixCls: string;
|
||||
titleText: string;
|
||||
dataSource: TransferItem[];
|
||||
filter: string;
|
||||
filterOption?: (filterText: any, item: any) => boolean;
|
||||
filterOption?: (filterText: string, item: TransferItem) => boolean;
|
||||
style?: React.CSSProperties;
|
||||
checkedKeys: string[];
|
||||
handleFilter: (e: any) => void;
|
||||
handleSelect: (selectedItem: any, checked: boolean) => void;
|
||||
handleSelectAll: (dataSource: any[], checkAll: boolean) => void;
|
||||
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSelect: (selectedItem: TransferItem, checked: boolean) => void;
|
||||
/** [Legacy] Only used when `body` prop used. */
|
||||
handleSelectAll: (dataSource: TransferItem[], checkAll: boolean) => void;
|
||||
onItemSelect: (key: string, check: boolean) => void;
|
||||
onItemSelectAll: (dataSource: string[], checkAll: boolean) => void;
|
||||
handleClear: () => void;
|
||||
render?: (item: any) => any;
|
||||
render?: (item: TransferItem) => RenderResult;
|
||||
showSearch?: boolean;
|
||||
searchPlaceholder: string;
|
||||
notFoundContent: React.ReactNode;
|
||||
itemUnit: string;
|
||||
itemsUnit: string;
|
||||
body?: (props: TransferListProps) => React.ReactNode;
|
||||
renderList?: RenderListFunction;
|
||||
footer?: (props: TransferListProps) => React.ReactNode;
|
||||
lazy?: boolean | {};
|
||||
onScroll: Function;
|
||||
disabled?: boolean;
|
||||
direction: TransferDirection;
|
||||
showSelectAll?: boolean;
|
||||
}
|
||||
|
||||
export default class TransferList extends React.Component<TransferListProps, any> {
|
||||
interface TransferListState {
|
||||
/** Filter input value */
|
||||
filterValue: string;
|
||||
}
|
||||
|
||||
function renderListNode(renderList: RenderListFunction | undefined, props: TransferListBodyProps) {
|
||||
let bodyContent: React.ReactNode = renderList ? renderList(props) : null;
|
||||
const customize: boolean = !!bodyContent;
|
||||
if (!customize) {
|
||||
bodyContent = defaultRenderList(props);
|
||||
}
|
||||
return {
|
||||
customize,
|
||||
bodyContent,
|
||||
};
|
||||
}
|
||||
|
||||
export default class TransferList extends React.Component<TransferListProps, TransferListState> {
|
||||
static defaultProps = {
|
||||
dataSource: [],
|
||||
titleText: '',
|
||||
showSearch: false,
|
||||
render: noop,
|
||||
lazy: {},
|
||||
};
|
||||
|
||||
timer: number;
|
||||
triggerScrollTimer: number;
|
||||
notFoundNode: HTMLDivElement;
|
||||
|
||||
constructor(props: TransferListProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mounted: false,
|
||||
filterValue: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.setState({
|
||||
mounted: true,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
clearTimeout(this.triggerScrollTimer);
|
||||
}
|
||||
|
||||
@ -81,25 +100,23 @@ export default class TransferList extends React.Component<TransferListProps, any
|
||||
return PureRenderMixin.shouldComponentUpdate.apply(this, args);
|
||||
}
|
||||
|
||||
getCheckStatus(filteredDataSource: TransferItem[]) {
|
||||
getCheckStatus(filteredItems: TransferItem[]) {
|
||||
const { checkedKeys } = this.props;
|
||||
if (checkedKeys.length === 0) {
|
||||
return 'none';
|
||||
} else if (filteredDataSource.every(item => checkedKeys.indexOf(item.key) >= 0)) {
|
||||
} else if (filteredItems.every(item => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)) {
|
||||
return 'all';
|
||||
}
|
||||
return 'part';
|
||||
}
|
||||
|
||||
handleSelect = (selectedItem: TransferItem) => {
|
||||
const { checkedKeys } = this.props;
|
||||
const result = checkedKeys.some(key => key === selectedItem.key);
|
||||
this.props.handleSelect(selectedItem, !result);
|
||||
};
|
||||
|
||||
handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const {
|
||||
target: { value: filterValue },
|
||||
} = e;
|
||||
this.setState({ filterValue });
|
||||
this.props.handleFilter(e);
|
||||
if (!e.target.value) {
|
||||
if (!filterValue) {
|
||||
return;
|
||||
}
|
||||
// Manually trigger scroll event for lazy search bug
|
||||
@ -114,45 +131,51 @@ export default class TransferList extends React.Component<TransferListProps, any
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.setState({ filterValue: '' });
|
||||
this.props.handleClear();
|
||||
};
|
||||
|
||||
matchFilter = (text: string, item: TransferItem) => {
|
||||
const { filter, filterOption } = this.props;
|
||||
const { filterValue } = this.state;
|
||||
const { filterOption } = this.props;
|
||||
if (filterOption) {
|
||||
return filterOption(filter, item);
|
||||
return filterOption(filterValue, item);
|
||||
}
|
||||
return text.indexOf(filter) >= 0;
|
||||
return text.indexOf(filterValue) >= 0;
|
||||
};
|
||||
|
||||
renderItem = (item: TransferItem) => {
|
||||
const { render = noop } = this.props;
|
||||
const renderResult = render(item);
|
||||
renderItem = (item: TransferItem): RenderedItem => {
|
||||
const { render = defaultRender } = this.props;
|
||||
const renderResult: RenderResult = render(item);
|
||||
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
|
||||
return {
|
||||
renderedText: isRenderResultPlain ? renderResult.value : renderResult,
|
||||
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
|
||||
renderedText: isRenderResultPlain
|
||||
? (renderResult as RenderResultObject).value
|
||||
: (renderResult as string),
|
||||
renderedEl: isRenderResultPlain ? (renderResult as RenderResultObject).label : renderResult,
|
||||
item,
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { filterValue } = this.state;
|
||||
const {
|
||||
prefixCls,
|
||||
dataSource,
|
||||
titleText,
|
||||
checkedKeys,
|
||||
lazy,
|
||||
disabled,
|
||||
body,
|
||||
footer,
|
||||
showSearch,
|
||||
style,
|
||||
filter,
|
||||
searchPlaceholder,
|
||||
notFoundContent,
|
||||
itemUnit,
|
||||
itemsUnit,
|
||||
onScroll,
|
||||
renderList,
|
||||
onItemSelectAll,
|
||||
showSelectAll,
|
||||
} = this.props;
|
||||
|
||||
// Custom Layout
|
||||
@ -163,38 +186,24 @@ export default class TransferList extends React.Component<TransferListProps, any
|
||||
[`${prefixCls}-with-footer`]: !!footerDom,
|
||||
});
|
||||
|
||||
const filteredDataSource: TransferItem[] = [];
|
||||
const totalDataSource: TransferItem[] = [];
|
||||
// ====================== Get filtered, checked item list ======================
|
||||
const filteredItems: TransferItem[] = [];
|
||||
const filteredRenderItems: RenderedItem[] = [];
|
||||
|
||||
const showItems = dataSource.map(item => {
|
||||
const { renderedText, renderedEl } = this.renderItem(item);
|
||||
if (filter && filter.trim() && !this.matchFilter(renderedText, item)) {
|
||||
dataSource.forEach(item => {
|
||||
const renderedItem = this.renderItem(item);
|
||||
const { renderedText } = renderedItem;
|
||||
|
||||
// Filter skip
|
||||
if (filterValue && filterValue.trim() && !this.matchFilter(renderedText, item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// all show items
|
||||
totalDataSource.push(item);
|
||||
if (!item.disabled) {
|
||||
// response to checkAll items
|
||||
filteredDataSource.push(item);
|
||||
}
|
||||
|
||||
const checked = checkedKeys.indexOf(item.key) >= 0;
|
||||
return (
|
||||
<Item
|
||||
disabled={disabled}
|
||||
key={item.key}
|
||||
item={item}
|
||||
lazy={lazy}
|
||||
renderedText={renderedText}
|
||||
renderedEl={renderedEl}
|
||||
checked={checked}
|
||||
prefixCls={prefixCls}
|
||||
onClick={this.handleSelect}
|
||||
/>
|
||||
);
|
||||
filteredItems.push(item);
|
||||
filteredRenderItems.push(renderedItem);
|
||||
});
|
||||
|
||||
// ================================= List Body =================================
|
||||
const unit = dataSource.length > 1 ? itemsUnit : itemUnit;
|
||||
|
||||
const search = showSearch ? (
|
||||
@ -204,64 +213,86 @@ export default class TransferList extends React.Component<TransferListProps, any
|
||||
onChange={this.handleFilter}
|
||||
handleClear={this.handleClear}
|
||||
placeholder={searchPlaceholder}
|
||||
value={filter}
|
||||
value={filterValue}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const searchNotFound = showItems.every(item => item === null) && (
|
||||
const searchNotFound = !filteredItems.length && (
|
||||
<div className={`${prefixCls}-body-not-found`}>{notFoundContent}</div>
|
||||
);
|
||||
|
||||
const listBody = bodyDom || (
|
||||
<div
|
||||
className={classNames(
|
||||
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`,
|
||||
)}
|
||||
>
|
||||
{search}
|
||||
{!searchNotFound && (
|
||||
<Animate
|
||||
component="ul"
|
||||
componentProps={{ onScroll }}
|
||||
className={`${prefixCls}-content`}
|
||||
transitionName={this.state.mounted ? `${prefixCls}-content-item-highlight` : ''}
|
||||
transitionLeave={false}
|
||||
>
|
||||
{showItems}
|
||||
</Animate>
|
||||
)}
|
||||
{searchNotFound}
|
||||
</div>
|
||||
);
|
||||
let listBody: React.ReactNode = bodyDom;
|
||||
if (!listBody) {
|
||||
let bodyNode: React.ReactNode = searchNotFound;
|
||||
if (!bodyNode) {
|
||||
const { bodyContent, customize } = renderListNode(renderList, {
|
||||
...omit(this.props, OmitProps),
|
||||
filteredItems,
|
||||
filteredRenderItems,
|
||||
selectedKeys: checkedKeys,
|
||||
});
|
||||
|
||||
// We should wrap customize list body in a classNamed div to use flex layout.
|
||||
bodyNode = customize ? (
|
||||
<div className={`${prefixCls}-body-customize-wrapper`}>{bodyContent}</div>
|
||||
) : (
|
||||
bodyContent
|
||||
);
|
||||
}
|
||||
|
||||
listBody = (
|
||||
<div
|
||||
className={classNames(
|
||||
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`,
|
||||
)}
|
||||
>
|
||||
{search}
|
||||
{bodyNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ================================ List Footer ================================
|
||||
const listFooter = footerDom ? <div className={`${prefixCls}-footer`}>{footerDom}</div> : null;
|
||||
|
||||
const checkStatus = this.getCheckStatus(filteredDataSource);
|
||||
const checkStatus = this.getCheckStatus(filteredItems);
|
||||
const checkedAll = checkStatus === 'all';
|
||||
const checkAllCheckbox = (
|
||||
const checkAllCheckbox = showSelectAll !== false && (
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
checked={checkedAll}
|
||||
indeterminate={checkStatus === 'part'}
|
||||
onChange={() => this.props.handleSelectAll(filteredDataSource, checkedAll)}
|
||||
onChange={() => {
|
||||
// Only select enabled items
|
||||
onItemSelectAll(
|
||||
filteredItems.filter(item => !item.disabled).map(({ key }) => key),
|
||||
!checkedAll,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// ================================== Render ===================================
|
||||
return (
|
||||
<div className={listCls} style={style}>
|
||||
{/* Header */}
|
||||
<div className={`${prefixCls}-header`}>
|
||||
{checkAllCheckbox}
|
||||
<span className={`${prefixCls}-header-selected`}>
|
||||
<span>
|
||||
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + totalDataSource.length}{' '}
|
||||
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + filteredItems.length}{' '}
|
||||
{unit}
|
||||
</span>
|
||||
<span className={`${prefixCls}-header-title`}>{titleText}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
{listBody}
|
||||
|
||||
{/* Footer */}
|
||||
{listFooter}
|
||||
</div>
|
||||
);
|
||||
|
84
components/transfer/renderListBody.tsx
Normal file
84
components/transfer/renderListBody.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import * as React from 'react';
|
||||
import Animate from 'rc-animate';
|
||||
import raf from '../_util/raf';
|
||||
import { Omit, tuple } from '../_util/type';
|
||||
import { TransferItem } from '.';
|
||||
import { TransferListProps, RenderedItem } from './list';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
export const OmitProps = tuple(
|
||||
'handleFilter',
|
||||
'handleSelect',
|
||||
'handleSelectAll',
|
||||
'handleClear',
|
||||
'body',
|
||||
'checkedKeys',
|
||||
);
|
||||
export type OmitProp = (typeof OmitProps)[number];
|
||||
type PartialTransferListProps = Omit<TransferListProps, OmitProp>;
|
||||
|
||||
export interface TransferListBodyProps extends PartialTransferListProps {
|
||||
filteredItems: TransferItem[];
|
||||
filteredRenderItems: RenderedItem[];
|
||||
selectedKeys: string[];
|
||||
}
|
||||
|
||||
class ListBody extends React.Component<TransferListBodyProps> {
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
private mountId: number;
|
||||
|
||||
componentDidMount() {
|
||||
this.mountId = raf(() => {
|
||||
this.setState({ mounted: true });
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
raf.cancel(this.mountId);
|
||||
}
|
||||
|
||||
onItemSelect = (item: TransferItem) => {
|
||||
const { onItemSelect, selectedKeys } = this.props;
|
||||
const checked = selectedKeys.indexOf(item.key) >= 0;
|
||||
onItemSelect(item.key, !checked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { mounted } = this.state;
|
||||
const { prefixCls, onScroll, filteredRenderItems, lazy, selectedKeys } = this.props;
|
||||
|
||||
return (
|
||||
<Animate
|
||||
component="ul"
|
||||
componentProps={{ onScroll }}
|
||||
className={`${prefixCls}-content`}
|
||||
transitionName={mounted ? `${prefixCls}-content-item-highlight` : ''}
|
||||
transitionLeave={false}
|
||||
>
|
||||
{filteredRenderItems.map(({ renderedEl, renderedText, item }: RenderedItem) => {
|
||||
const { disabled } = item;
|
||||
const checked = selectedKeys.indexOf(item.key) >= 0;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
disabled={disabled}
|
||||
key={item.key}
|
||||
item={item}
|
||||
lazy={lazy}
|
||||
renderedText={renderedText}
|
||||
renderedEl={renderedEl}
|
||||
checked={checked}
|
||||
prefixCls={prefixCls}
|
||||
onClick={this.onItemSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Animate>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default (props: TransferListBodyProps) => <ListBody {...props} />;
|
62
components/transfer/style/customize.less
Normal file
62
components/transfer/style/customize.less
Normal file
@ -0,0 +1,62 @@
|
||||
@import './index.less';
|
||||
|
||||
@table-prefix-cls: ~'@{ant-prefix}-table';
|
||||
|
||||
.@{transfer-prefix-cls}-customize-list {
|
||||
display: flex;
|
||||
|
||||
.@{transfer-prefix-cls}-operation {
|
||||
flex: none;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.@{transfer-prefix-cls}-list {
|
||||
flex: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: @transfer-list-height;
|
||||
|
||||
&-body {
|
||||
&-with-search {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
// Search box in customize mode do not need fix top
|
||||
&-search-wrapper {
|
||||
position: relative;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&-customize-wrapper {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =================== Hook Components ===================
|
||||
.@{table-prefix-cls}-wrapper {
|
||||
.@{table-prefix-cls}-small {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
|
||||
> .@{table-prefix-cls}-content {
|
||||
// Header background color
|
||||
> .@{table-prefix-cls}-body > table > .@{table-prefix-cls}-thead > tr > th {
|
||||
background: @table-header-bg;
|
||||
}
|
||||
|
||||
.@{table-prefix-cls}-row:last-child td {
|
||||
border-bottom: @border-width-base @border-style-base @border-color-split;
|
||||
}
|
||||
}
|
||||
|
||||
.@{table-prefix-cls}-body {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.@{table-prefix-cls}-pagination.@{ant-prefix}-pagination {
|
||||
margin: 16px 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../checkbox/style/mixin';
|
||||
@import './customize.less';
|
||||
|
||||
@transfer-prefix-cls: ~'@{ant-prefix}-transfer';
|
||||
|
||||
@transfer-header-vertical-padding: (
|
||||
@transfer-header-height - 1px - @font-size-base * @line-height-base
|
||||
) / 2;
|
||||
|
||||
.@{transfer-prefix-cls} {
|
||||
.reset-component;
|
||||
|
||||
@ -19,8 +24,8 @@
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
height: 200px;
|
||||
padding-top: 34px;
|
||||
height: @transfer-list-height;
|
||||
padding-top: @transfer-header-height;
|
||||
vertical-align: middle;
|
||||
border: @border-width-base @border-style-base @border-color-base;
|
||||
border-radius: @border-radius-base;
|
||||
@ -33,9 +38,9 @@
|
||||
padding: 0 @control-padding-horizontal-sm;
|
||||
&-action {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
width: 28px;
|
||||
color: @disabled-color;
|
||||
line-height: @input-height-base;
|
||||
@ -58,7 +63,9 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 6px @control-padding-horizontal;
|
||||
// border-top is on the transfer dom. We should minus 1px for this
|
||||
padding: (@transfer-header-vertical-padding - 1px) @control-padding-horizontal
|
||||
@transfer-header-vertical-padding;
|
||||
overflow: hidden;
|
||||
color: @text-color;
|
||||
background: @component-background;
|
||||
@ -81,12 +88,12 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-body-with-search {
|
||||
padding-top: @input-height-base + 8px;
|
||||
padding-top: @input-height-base + 24px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
|
Loading…
Reference in New Issue
Block a user