# Transfer

穿梭框，分为简单版和自定义复杂版

## 简单穿梭框 Transfer

穿梭框，简单实现，仅支持列表

通过鼠标 `双击` 条目可实现选中穿梭。

按住 `ctrl` 执行多选

按住 `shift` 执行范围选择

### basic use

```js
import { Transfer } from 'amos-framework';

<Transfer
  height={200}
  title="选择数据"
  sdata={this.state.sourceData}
  tdata={this.state.targetData}
  onChange={this.handleChange}
  onSearch={this.handleSearch}
  renderOption={item => `${item.label}-${item.description}`}
/>
```

### 案例演示

#### Transfer 基本使用

---demo
```js
import { Transfer } from 'amos-framework';

const sourceData = [
  { id: 1, label: '张一', description: '描述1' },
  { id: 2, label: '张二', description: '描述2' },
  { id: 3, label: '张三', description: '描述3' },
  { id: 4, label: '张四', description: '描述4' },
  { id: 5, label: '张五', description: '描述5' },
  { id: 6, label: '李一', description: '描述6' },
  { id: 7, label: '李二', description: '描述7' },
  { id: 8, label: '李三', description: '描述8' },
  { id: 9, label: '李四', description: '描述9' }
];
const targetData = [
  { id: 10, label: '王一', description: '描述10' },
  { id: 11, label: '王二', description: '描述11' }
];

class Demo extends Component {
  state = {
    sourceData,
    targetData
  }

  handleChange = (sourceData, targetData) => {
    this.setState({
      sourceData: sourceData,
      targetData: targetData
    });
  }

  handleSearch = (label, keyValue) => {
    return label.indexOf(keyValue) !== -1;
  }

  render() {
    return (
      <Transfer
        height={200}
        title="已选的用户"
        sdata={this.state.sourceData}
        tdata={this.state.targetData}
        onChange={this.handleChange}
        onSearch={this.handleSearch}
        renderOption={item => `${item.label}-${item.description}`}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer 高级使用

> 注意，案例中的 moveNextItem 和 movePrevItem 仅仅是为了demo演示，实际项目使用需要使用 `amos-tool 中提供的 arrayUtils` 模块。

---demo
```js
import { Transfer, Icon, Flex } from 'amos-framework';

// import { arrayUtils } from 'amos-tool';

/**
 * 数组向后移动一位
 * 推荐使用 `arrayUtils.moveIndex.next(arr, indices)`
 * @param {*} arr
 * @param {*} indices
 * @returns
 */
function moveNextItem(arr, indices){
  indices.sort((a, b) => a - b); // 将索引排序，确保从小到大
  for (let i = indices.length - 1; i >= 0; i--) {
    const index = indices[i];
    if (index >= 0 && index < arr.length - 1) {
      // 只在有效范围内移动
      [arr[index], arr[index + 1]] = [arr[index + 1], arr[index]];
    }
  }
  return arr;
}

/**
 * 数组向前移动一位
 * 推荐使用 `arrayUtils.moveIndex.prev(arr, indices)`
 * @param {*} arr
 * @param {*} indices
 * @returns
 */
function movePrevItem(arr, indices){
  indices.sort((a, b) => a - b); // 将索引排序，确保从小到大
  for (let i = indices.length - 1; i >= 0; i--) {
    const index = indices[i];
    if (index >= 1 && index < arr.length) {
      // 只在有效范围内移动
      [arr[index - 1], arr[index]] = [arr[index], arr[index - 1]];
    }
  }
  return arr;
}

function getData(start, cnt){
  const result = [];
  for (let i = start; i < cnt; i++) {
    result.push({
      id: `${i}`, label: `喵哥-${i}`, description: `描述-${i}`
    });
  }

  return result;
}

const sourceData = getData(0, 15);
const targetData = getData(15, 30);
// const sourceData = getData(0, 5);
// const targetData = getData(5, 7);

class Demo extends Component {
  constructor(props) {
    super(props);
    this.state = {
      sourceData,
      targetData
    }

    this.myRef = React.createRef();
  }

  onSortUp = () => {
    if (!this.tid){
      return;
    }
    const { targetData } = this.state;
    const tid = this.tid;
    // 上移
    const idx = [];
    for (let i = 0; i < tid.length; i++) {
      const id = tid[i];
      const index = targetData.findIndex(t => t.id === id);
      idx.push(index);
    }
    const newArr = movePrevItem(targetData, idx);

    this.onSortChange(newArr);
  }

  onSortDown = () => {
    if (!this.tid){
      return;
    }
    const { targetData } = this.state;
    const tid = this.tid;
    // 下移
    const idx = [];
    for (let i = 0; i < tid.length; i++) {
      const id = tid[i];
      const index = targetData.findIndex(t => t.id === id);
      idx.push(index);
    }
    const newArr = moveNextItem(targetData, idx);

    this.onSortChange(newArr, () => {
      const { targetData } = this.state;
      const lastId = tid[tid.length - 1];
      const lastItem = targetData.find(t => t.id === lastId);
      // 滚动到指定的 item
      this.myRef.current.scrollTargetActiveItemToView(lastItem, false);
    });
  }

  onSortChange = (nextTargetData, cb) => {
    this.setState({
      targetData: nextTargetData
    }, cb);
  }

  handleTargetItemSelect = (tid) => {
    // tid
    this.tid = tid;
  }

  handleChange = (sourceData, targetData, direct, diffIds) => {
    console.log('direct | diffIds', direct, diffIds);
    this.setState({
      sourceData: sourceData,
      targetData: targetData
    }, () => {
      // 设置选中之前移动的
      switch (direct) {
        case 's2t': // 选中 target
          this.asyncTragetItemSelected(targetData, diffIds);
          break;
        case 't2s':
          this.asyncSourceItemSelected(sourceData, diffIds);
          break;
        default:
          break;
      }
    });
  }

  asyncTragetItemSelected = (datas, ids) => {
    const tars = datas.filter(d => ids.includes(d.id));
    this.myRef.current.tRef.current.setCustomItemSelect(tars);

    const lastItem = tars[tars.length - 1];
    this.myRef.current.scrollTargetActiveItemToView(lastItem, false);
  }

  asyncSourceItemSelected = (datas, ids) => {
    const tars = datas.filter(d => ids.includes(d.id));
    this.myRef.current.sRef.current.setCustomItemSelect(tars);

    const lastItem = tars[tars.length - 1];
    this.myRef.current.scrollSourceActiveItemToView(lastItem, false);
  }

  render() {
    return (
      <Transfer
        ref={this.myRef}
        height={200}
        showSearch={false}
        sourceTitle="可选用户:"
        title="已选的用户"
        title={
          <Flex align="center" justify="space-between">
            <span>已选的用户</span>
            <Flex align="center" gap="mid" style={{ cursor: 'pointer' }}>
              <Icon icon="sort-asc" title="上移" onClick={this.onSortUp} />
              <Icon icon="sort-desc" title="下移" onClick={this.onSortDown} />
            </Flex>
          </Flex>
        }
        sdata={this.state.sourceData}
        tdata={this.state.targetData}
        onChange={this.handleChange}
        onTargetItemSelect={this.handleTargetItemSelect}
        itemNoWrap
        renderOption={item => `${item.label}-${item.description}`}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### Transfer props

| params  | type | default | description |
| --- | --- | --- | --- |
| prefixCls | string | `amos-transfer` | css class 前缀 |
| className | string | - | css class |
| height | number | - | 两个传输框高度，默认200px |
| title | ReactNode | - | 右侧传输框上方标题，默认`已选项` |
| sdata | array | - | 源数据，包含id、label属性 |
| tdata | array | - | 目标数据，包含id、label属性 |
| onChange | `Function: (sdata, tdata, direct, diffIds) => {}` | - | 传输框值改变后的回调函数。 |
| onSearch | `Function: (label, keyValue) => {}` | - | 搜索框关键词与列表数据匹配规则函数， 第一个参数为列表项数据, 第二个参数为搜索关键词 |
| renderOption | `Function: (sdataItem) => {}` | - | 每行数据渲染函数。注意，如果返回值是一个 ReactElement，则此时需要传入 onSearch 自定义匹配方法 |
| searchPlaceholder | string | - | 搜索框 placeholder，默认`请输入搜索关键词` |
| renderKey | `string or function` | id | 数据项唯一key值，默认 `id` |
| showSearch | Boolean | true | 是否显示搜索框，默认为 true |
| span | Number | 4 | SelectTable col span 值，默认为 4 |
| sourceTitle | ReactNode | - | source 列 title |
| onTargetItemSelect | `func: (tids) => {}` | - | 目标列选中项 id 集合 |

> 注意，since v 1.11.4 onChange add `direct, diffIds` params。direct 表示操作方向，diffIds 表示变化的id集合，可以用于后续默认选中、滚动到指定位置等。

## 高级穿梭框 Transfer2

选择一个或以上的选项后，点击对应的方向键，可以把选中的选项移动到另一栏。
其中，左边一栏为 `sdata`，右边一栏为 `tdata`。

### 案例演示

案例中内置的 `getMockData` 方法

```js
function getMockData(count = 20, useDisabled, useChoose){
  const mockData = [];
  for (let i = 0; i < count; i++) {
    const item = {
      key: i.toString(),
      title: `content${i + 1}`,
      description: `description of content${i + 1}`,
      disabled: i % 3 < 1
    };

    if (useDisabled){
      item.disabled = i % 3 < 1;
    }

    mockData.push(item);
  }

}
```

#### Transfer2 基本用法

最基本的用法，展示了 `dataSource`、`targetKeys`、每行的渲染函数 `renderOption` 以及回调函数 `onChange` `onSelectChange` `onScroll` 的用法。

---demo
```js
import { Transfer2 } from 'amos-framework';

const mockData = getMockData(20, true);

const targetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);

class Demo extends React.Component {
  state = {
    targetKeys,
    selectedKeys: [],
  }

  handleChange = (nextTargetKeys, direction, moveKeys) => {
    this.setState({ targetKeys: nextTargetKeys });

    console.log('targetKeys: ', nextTargetKeys);
    console.log('direction: ', direction);
    console.log('moveKeys: ', moveKeys);
  }

  handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
    this.setState({ selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys] });

    console.log('sourceSelectedKeys: ', sourceSelectedKeys);
    console.log('targetSelectedKeys: ', targetSelectedKeys);
  }

  handleScroll = (direction, e) => {
    console.log('direction:', direction);
    console.log('target:', e.target);
  }

  render() {
    const state = this.state;
    return (
      <Transfer2
        dataSource={mockData}
        titles={['Source', 'Target']}
        targetKeys={state.targetKeys}
        selectedKeys={state.selectedKeys}
        onChange={this.handleChange}
        onSelectChange={this.handleSelectChange}
        onScroll={this.handleScroll}
        renderOption={item => item.title}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 自定义渲染行数据

自定义渲染每一个 Transfer2 Item，可用于渲染复杂数据。

---demo
```js
import { Transfer2 } from 'amos-framework';

const mockData = getMockData(20, false, true);
const targetKeys = mockData.filter(md => md.choose).map(item => item.key);

class Demo extends React.Component {
  state = {
    mockData,
    targetKeys,
  }

  handleChange = (targetKeys, direction, moveKeys) => {
    console.log(targetKeys, direction, moveKeys);
    this.setState({ targetKeys });
  }
  renderItem = (item) => {
    const customLabel = (
      <span className="custom-item">
        {item.title} - {item.description}
      </span>
    );

    return {
      label: customLabel,  // for displayed item
      value: item.title,   // for title and filter matching
    };
  }
  render() {
    return (
      <Transfer2
        dataSource={this.state.mockData}
        listStyle={{
          width: 300,
          height: 300,
        }}
        targetKeys={this.state.targetKeys}
        onChange={this.handleChange}
        renderOption={this.renderItem}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 大数据性能测试

1000 条数据。

---demo
```js
import { Transfer2 } from 'amos-framework';

const mockData = getMockData(1000, false, true);
const targetKeys = mockData.filter(md => md.choose).map(item => item.key);

class Demo extends React.Component {
  state = {
    mockData,
    targetKeys
  }

  handleChange = (targetKeys, direction, moveKeys) => {
    console.log(targetKeys, direction, moveKeys);
    this.setState({ targetKeys });
  }
  render() {
    return (
      <Transfer2
        dataSource={this.state.mockData}
        targetKeys={this.state.targetKeys}
        onChange={this.handleChange}
        renderOption={item => item.title}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 带搜索框

带搜索框的穿梭框，可以自定义搜索函数。

---demo
```js
import { Transfer2 } from 'amos-framework';

const mockData = getMockData(20, false, true);
const targetKeys = mockData.filter(md => md.choose).map(item => item.key);

class Demo extends React.Component {
  state = {
    mockData,
    targetKeys
  }

  filterOption = (inputValue, option) => {
    return option.description.indexOf(inputValue) > -1;
  }

  handleChange = (targetKeys) => {
    this.setState({ targetKeys });
  }

  render() {
    return (
      <Transfer2
        dataSource={this.state.mockData}
        showSearch
        filterOption={this.filterOption}
        targetKeys={this.state.targetKeys}
        onChange={this.handleChange}
        renderOption={item => item.title}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 高级用法

穿梭框高级用法，可配置操作文案，可定制宽高，可对底部进行自定义渲染。

---demo
```js
import { Transfer2, Button } from 'amos-framework';

class Demo extends React.Component {
  state = {
    mockData: [],
    targetKeys: []
  }

  componentDidMount() {
    this.reload();
  }

  reload = () => {
    const mockData = getMockData(20, false, true);
    const targetKeys = mockData.filter(md => md.choose).map(item => item.key);
    this.setState({ mockData, targetKeys });
  }

  handleChange = (targetKeys) => {
    this.setState({ targetKeys });
  }

  renderFooter = () => {
    return (
      <Button
        size="sm"
        style={{ float: 'right', margin: 5 }}
        onClick={this.reload}
      >
        reload
      </Button>
    );
  }
  render() {
    return (
      <Transfer2
        dataSource={this.state.mockData}
        showSearch
        listStyle={{
          width: 250,
          height: 300,
        }}
        operations={['to right', 'to left']}
        targetKeys={this.state.targetKeys}
        onChange={this.handleChange}
        renderOption={item => `${item.title}-${item.description}`}
        footer={this.renderFooter}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 隐藏中间操作区

---demo
```js
import { Transfer2 } from 'amos-framework';

const mockData = getMockData(20, false, true);
const targetKeys = mockData.filter(md => md.choose).map(item => item.key);

class Demo extends React.Component {
  state = {
    targetKeys,
    selectedKeys: []
  }

  handleChange = (targetKeys) => {
    this.setState({ targetKeys });
  }

  handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
    const newTar = [
      ...this.state.targetKeys,
      ...sourceSelectedKeys
    ]
    this.setState({
      selectedKeys: [],
      targetKeys: newTar.filter(tk => !targetSelectedKeys.includes(tk))
    });
  }

  render() {
    return (
      <Transfer2
        showOperate={false}
        dataSource={mockData}
        targetKeys={this.state.targetKeys}
        onChange={this.handleChange}
        onSelectChange={this.handleSelectChange}
        selectedKeys={this.state.selectedKeys}
        renderOption={item => item.title}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 自定义渲染header

---demo
```js
import { Transfer2 } from 'amos-framework';

const mockData = getMockData(20, false, true);
const targetKeys = mockData.filter(md => md.choose).map(item => item.key);

const style = {
  position: 'absolute',
  top: 0,
  left: 0,
  width: '100%',
  padding: '7px 15px',
  overflow: 'hidden',
  color: 'rgba(0, 0, 0, 0.65)',
  background: 'white',
  borderBottom: '1px solid #e9e9e9',
  borderRadius: '4px 4px 0 0'
};

class Demo extends React.Component {
  state = {
    mockData,
    targetKeys
  }

  handleChange = (targetKeys) => {
    this.setState({ targetKeys });
  }

  renderHeader(props){
    const { type } = props;

    if (type === 'source'){
      return <div style={style}>未添加指标:</div>;
    }

    return <div style={style}>已添加指标:</div>;
  }

  render() {
    return (
      <Transfer2
        header={this.renderHeader}
        dataSource={this.state.mockData}
        targetKeys={this.state.targetKeys}
        onChange={this.handleChange}
        renderOption={item => item.title}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

#### Transfer2 自定义渲染 body

---demo
```js
import { Transfer2, Tree } from 'amos-framework';

const TreeNode = Tree.TreeNode;

const treeData = [
  { title: 'p0', key: '0', children: [
    { title: 'p0-1', key: '0-1' },
    { title: 'p0-2', key: '0-2' },
    { title: 'p0-3', key: '0-3' }
  ] },
  { title: 'p1', key: '1', children: [
    { title: 'p1-1', key: '1-1' },
    { title: 'p1-2', key: '1-2' },
    { title: 'p1-3', key: '1-3' }
  ] },
  { title: 'p2', key: '2', isLeaf: true }
];

const isChecked = (selectedKeys, eventKey) => selectedKeys.includes(eventKey);

/**
 * 扁平化，会携带 children 字段
 * @param {*} list
 */
function flatten1(list = []) {
  return list.reduce((prev, cur) => prev.concat([cur], flatten1(cur.children)), []);
}

/**
 * 扁平化，去掉 children 字段
 * @param {*} list
 */
function flatten(list = []) {
  return list.reduce((arr, { children = [], ...rest })=> arr.concat([rest], flatten(children)), []);
}

class Demo extends React.Component {
  state = {
    targetKeys: [],
    selectedKeys: []
  }

  handleChange = (nextTargetKeys, direction, moveKeys) => {
    this.setState({ targetKeys: nextTargetKeys });

    console.log('targetKeys: ', nextTargetKeys);
    console.log('direction: ', direction);
    console.log('moveKeys: ', moveKeys);
  }

  handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
    this.setState({ selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys] });

    console.log('sourceSelectedKeys: ', sourceSelectedKeys);
    console.log('targetSelectedKeys: ', targetSelectedKeys);
  }

  handleScroll = (direction, e) => {
    console.log('direction:', direction);
    console.log('target:', e.target);
  }

  renderTreeNodes = (data) => {
    const { targetKeys } = this.state;
    return data.map((item) => {
      const disabled = targetKeys.includes(item.key);
      if (item.children) {
        return (
          <TreeNode title={item.title} key={item.key} dataRef={item} disabled={disabled}>
            {this.renderTreeNodes(item.children)}
          </TreeNode>
        );
      }
      return <TreeNode key={item.key} {...item} disabled={disabled} />;
    });
  }

  renderBody = (props) => {
    const { targetKeys } = this.state;
    const { type, checkedKeys, handleSelect } = props;
    // 源
    if (type === 'source'){
      const cks = [...checkedKeys, ...targetKeys];
      // 设置样式 amos-transfer2-list-content 是为了使用默认 list 样式
      return (
        <Tree
          className="amos-transfer2-list-content"
          defaultExpandAll
          checkable
          checkedKeys={cks}
          tight
          checkStrictly
          onCheck={(ck, evt) => {
            const key = evt.node.eventKey;
            handleSelect({ key }, !isChecked(cks, key));
          }}
          onSelect={(ck, evt) => {
            const key = evt.node.eventKey;
            handleSelect({ key }, !isChecked(cks, key));
          }}
        >
          {this.renderTreeNodes(treeData)}
        </Tree>
      );
    }

    return false;
  }

  render() {
    const state = this.state;
    // 扁平化 树 数据
    const ds = flatten(treeData);
    return (
      <Transfer2
        dataSource={ds}
        body={this.renderBody}
        titles={['Source', 'Target']}
        targetKeys={state.targetKeys}
        selectedKeys={state.selectedKeys}
        onChange={this.handleChange}
        onSelectChange={this.handleSelectChange}
        onScroll={this.handleScroll}
        renderOption={item => item.title}
      />
    );
  }
}

ReactDOM.render(<Demo />, _react_runner_);
```
---demoend

### Transfer2 props

| params  | type | default | description |
| ------ | ------ | ------ | ------ |
| dataSource | [TransferItem](#TransferItem)[] | [] | 数据源，其中的数据将会被渲染到左边一栏中，`targetKeys` 中指定的除外。 |
| renderOption | `Function(record): ReactElement` | - | 每行数据渲染函数，该函数的入参为 `dataSource` 中的项，返回值为 ReactElement |
| targetKeys | `string[]` | [] | 显示在右侧框数据的key集合 |
| renderKey | `(data) => String`  | - | 自定义数据 key |
| selectedKeys | `string[]` | [] | 设置哪些项应该被选中 |
| onChange | `(targetKeys, direction, moveKeys): void` | - | 选项在两栏之间转移时的回调函数 |
| onSelectChange | `(sourceSelectedKeys, targetSelectedKeys): void` | - | 选中项发生改变时的回调函数 |
| onScroll | `(direction, event): void` | | 选项列表滚动时的回调函数 |
| listStyle | object |  | 两个穿梭框的自定义样式 |
| className | string |  | 自定义类 |
| titles | `string[]` | ['', ''] | 标题集合，顺序从左至右 |
| operations | `string[]` | `['>', '<']` | 操作文案集合，顺序从下至上 |
| showOperate | boolean | true | 显示中间 operate |
| showSearch | boolean | false | 是否显示搜索框 |
| filterOption | `(inputValue, option): boolean` | - | 接收 `inputValue` `option` 两个参数，当 `option` 符合筛选条件时，应返回 `true`，反之则返回 `false`。|
| searchPlaceholder | string | '请输入' | 搜索框的默认值 |
| notFoundContent | `string or ReactNode` | '列表为空'  | 当列表为空时显示的内容 |
| header | `(props): ReactNode` | - | header 自定义渲染，参数 props 为 `TransferList` 接收到的所有 props |
| body | `(props): ReactNode` | - | 内容区自定义渲染，参数 props 为 `TransferList` 接收到的所有 props |
| footer | `(props): ReactNode` | - | 底部自定义渲染函数，参数 props 为 `TransferList` 接收到的所有 props |
| lazy |  `object or boolean` | `{ height: 32, offset: 32 }` | Transfer 使用了 [crispfit-ui#LazyLoader](https://www.npmjs.com/package/crispfit-ui) 优化性能，这里可以设置相关参数。设为 `false` 可以关闭懒加载。 |
| onSearchChange | `(direction: 'left' or 'right', event: Event): void` | - | 搜索框内容时改变时的回调函数 |

> 注意： 如果需要使用 `body` 来自定义内容面板，可以通过 `props.type` 来进行判断，左侧: `type === 'source'`, 右侧: `type === 'target'`。

### TransferItem

```js
{
  key: '',
  label: '',
  value: '',
  ...
}
```

## 注意

按照 React 的[规范](http://facebook.github.io/react/docs/lists-and-keys.html#keys)，所有的组件数组必须绑定 key。在 Transfer 中，`dataSource`里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。

如果你的数据没有这个属性，务必使用 `renderKey` 来指定数据列的主键。

```jsx
// 比如你的数据主键是 uid
return <Transfer renderKey={record => record.uid} />;
```
