Skip to content

Commit

Permalink
feat(transfer): support draggable on target list
Browse files Browse the repository at this point in the history
  • Loading branch information
uyarn committed Jul 24, 2023
1 parent 5b418a9 commit 9af7497
Show file tree
Hide file tree
Showing 11 changed files with 812 additions and 41 deletions.
48 changes: 48 additions & 0 deletions src/transfer/_example/target-draggable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<div>
{{ JSON.stringify(this.targetValue) }}
<t-transfer
:data="list"
v-model="targetValue"
:checked.sync="checked"
@change="onChange"
@checked-change="handleCheckedChange"
:targetDraggable="true"
targetSort="push"
/>
</div>
</template>
<script>
const list = [];
for (let i = 0; i < 20; i++) {
list.push({
value: i.toString(),
label: `内容${i + 1}`,
disabled: i % 4 < 1,
});
}
export default {
data() {
return {
list,
targetValue: [],
checked: ['2'],
};
},
methods: {
handleCheckedChange({
checked, sourceChecked, targetChecked, type,
}) {
console.log('handleCheckedChange', {
checked,
sourceChecked,
targetChecked,
type,
});
},
onChange(newTargetValue) {
console.log(newTargetValue);
},
},
};
</script>
157 changes: 137 additions & 20 deletions src/transfer/components/transfer-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
} from '../interface';
import { PageInfo, TdPaginationProps, Pagination as TPagination } from '../../pagination';
import { Checkbox as TCheckbox, CheckboxGroup as TCheckboxGroup, CheckboxProps } from '../../checkbox';
import { findTopNode, getLeafCount, getDataValues } from '../utils';
import {
findTopNode, getLeafCount, getDataValues, TARGET,
} from '../utils';
import ripple from '../../utils/ripple';
import Search from './transfer-search';
import { renderTNodeJSXDefault } from '../../utils/render-tnode';
Expand Down Expand Up @@ -76,6 +78,10 @@ export default mixins(keepAnimationMixins, classPrefixMixins).extend({
type: Boolean as PropType<boolean>,
default: false,
},
draggable: Boolean,
currentValue: {
type: Array as PropType<Array<TransferValue>>,
},
},
data() {
return {
Expand All @@ -84,6 +90,10 @@ export default mixins(keepAnimationMixins, classPrefixMixins).extend({
defaultCurrent: 1,
// 用于兼容处理 Pagination 的非受控属性
defaultPageSize: 0,
// 支持targetList 排序
draggingIndex: null,
dragoverIndex: null,
dragoverPos: '',
};
},
computed: {
Expand Down Expand Up @@ -191,6 +201,75 @@ export default mixins(keepAnimationMixins, classPrefixMixins).extend({
};
this.$emit('search', event);
},
onDragStart(e: DragEvent) {
const index = Number((e.target as HTMLElement).dataset.index);
this.draggingIndex = index;
},
onDragOver(e: DragEvent) {
e.preventDefault();
if (e.currentTarget) {
const currentElement = e.currentTarget as HTMLElement;
const index = Number(currentElement.dataset.index);
const elemHeight = currentElement.offsetHeight;
const dragY = e.clientY - currentElement.getBoundingClientRect().top;
const insertAreaPercent = 0.3;
const insertAreaHeight = elemHeight * insertAreaPercent;

this.dragoverIndex = index;

if (this.dragoverIndex === this.draggingIndex) {
this.dragoverPos = '';
return;
}
if (dragY < insertAreaHeight) {
this.dragoverPos = 'top';
} else if (dragY > elemHeight - insertAreaHeight) {
this.dragoverPos = 'bottom';
} else {
this.dragoverPos = 'center';
}
}
},
onDragLeave() {
this.dragoverPos = '';
this.dragoverIndex = null;
},
onDragEnd() {
this.draggingIndex = null;
this.dragoverIndex = null;
this.dragoverPos = '';
},
onDrop(e: DragEvent) {
e.preventDefault();

const { draggingIndex, dragoverIndex, dragoverPos } = this;

this.draggingIndex = null;
this.dragoverIndex = null;
this.dragoverPos = '';
if (draggingIndex === dragoverIndex) {
return;
}

const newData = [...this.currentValue];

const sourceItem = this.curPageData[draggingIndex].value;
const targetItem = this.curPageData[dragoverIndex].value;
const sourceIndex = newData.indexOf(sourceItem);
let targetIndex = newData.indexOf(targetItem);

newData.splice(sourceIndex, 1);

if (draggingIndex < dragoverIndex) {
targetIndex -= 1;
}

if (dragoverPos === 'bottom') {
targetIndex += 1;
}
newData.splice(targetIndex, 0, sourceItem);
this.$emit('dataChange', newData);
},
renderTitle() {
const defaultNode = this.title && typeof this.title === 'string' ? <template>{this.title}</template> : null;
const titleNode = renderTNodeJSXDefault(this, 'title', {
Expand All @@ -203,25 +282,63 @@ export default mixins(keepAnimationMixins, classPrefixMixins).extend({
},
renderContent() {
const rootNode = findTopNode(this);
const defaultNode = (
<TCheckboxGroup value={this.checkedValue} onChange={this.handleCheckedChange}>
{this.curPageData.map((item, index) => (
<TCheckbox
disabled={this.disabled || item.disabled}
value={item.value}
class={[`${this.componentName}__list-item`]}
key={item.key}
v-ripple={this.keepAnimation.ripple}
{...{ props: this.checkboxProps }}
>
{renderTNodeJSXDefault(this, 'transferItem', {
defaultNode: <span>{item.label}</span>,
params: { data: item.data, index, type: this.listType },
})}
</TCheckbox>
))}
</TCheckboxGroup>
);
const isDraggable = this.draggable && this.listType === TARGET;
let defaultNode;
if (!isDraggable) {
defaultNode = (
<TCheckboxGroup value={this.checkedValue} onChange={this.handleCheckedChange}>
{this.curPageData.map((item, index) => (
<TCheckbox
disabled={this.disabled || item.disabled}
value={item.value}
class={[`${this.componentName}__list-item`]}
key={item.key}
v-ripple={this.keepAnimation.ripple && !this.draggable}
{...{ props: this.checkboxProps }}
>
{renderTNodeJSXDefault(this, 'transferItem', {
defaultNode: <span>{item.label}</span>,
params: { data: item.data, index, type: this.listType },
})}
</TCheckbox>
))}
</TCheckboxGroup>
);
} else {
defaultNode = (
<TCheckboxGroup
value={this.checkedValue}
onChange={this.handleCheckedChange}
key={JSON.stringify(this.currentValue)}
>
{this.curPageData.map((item, index) => (
<div
draggable={isDraggable}
onDragend={this.onDragEnd}
onDragstart={this.onDragStart}
onDragover={this.onDragOver}
onDragleave={this.onDragLeave}
onDrop={this.onDrop}
data-index={index}
>
<TCheckbox
disabled={this.disabled || item.disabled}
value={item.value}
class={[`${this.componentName}__list-item`]}
key={item.key}
v-ripple={this.keepAnimation.ripple && !this.draggable}
{...{ props: this.checkboxProps }}
>
{renderTNodeJSXDefault(this, 'transferItem', {
defaultNode: <span>{item.label}</span>,
params: { data: item.data, index, type: this.listType },
})}
</TCheckbox>
</div>
))}
</TCheckboxGroup>
);
}

return (
<div class={`${this.componentName}__list-content narrow-scrollbar`} onScroll={this.scroll}>
Expand Down
6 changes: 4 additions & 2 deletions src/transfer/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* updated at 2021-12-12 16:59:59
* */

import { TdTransferProps } from './type';
Expand Down Expand Up @@ -33,13 +32,13 @@ export default {
type: String as PropType<TdTransferProps['direction']>,
default: 'both' as TdTransferProps['direction'],
validator(val: TdTransferProps['direction']): boolean {
if (!val) return true;
return ['left', 'right', 'both'].includes(val);
},
},
/** 禁用全部操作:搜索、选中、移动、分页等。[源列表, 目标列表],示例:[true, false] 或者 true */
disabled: {
type: [Boolean, Array] as PropType<TdTransferProps['disabled']>,
default: false,
},
/** 列表为空时呈现的内容。值类型为数组,则表示分别控制源列表和目标列表数据为空的呈现内容 */
empty: {
Expand Down Expand Up @@ -72,11 +71,14 @@ export default {
type: [Boolean, Array] as PropType<TdTransferProps['showCheckAll']>,
default: true,
},
/** 是否允许通过拖拽对目标列表进行排序 */
targetDraggable: Boolean,
/** 目标数据列表排列顺序 */
targetSort: {
type: String as PropType<TdTransferProps['targetSort']>,
default: 'original' as TdTransferProps['targetSort'],
validator(val: TdTransferProps['targetSort']): boolean {
if (!val) return true;
return ['original', 'push', 'unshift'].includes(val);
},
},
Expand Down
8 changes: 4 additions & 4 deletions src/transfer/transfer.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ name | type | default | description | required
-- | -- | -- | -- | --
checkboxProps | Object | - | Typescript:`CheckboxProps`[Checkbox API Documents](./checkbox?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
checked | Array | [] | `.sync` is supported。Typescript:`Array<TransferValue>` | N
defaultChecked | Array | [] | uncontrolled property。Typescript:`Array<TransferValue>` | N
data | Array | [] | Typescript:`Array<T>` | N
direction | String | both | optionsleft/right/both | N
disabled | Boolean / Array | false | Typescript:`boolean \| Array<boolean>` | N
direction | String | both | options: left/right/both | N
disabled | Boolean / Array | - | Typescript:`boolean \| Array<boolean>` | N
empty | String / Array / Slot / Function | '' | Typescript:`EmptyType \| Array<EmptyType> \| TNode` `type EmptyType = string \| TNode `[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
footer | Array / Slot / Function | - | Typescript:`Array<string \| TNode> \| TNode<{ type: TransferListType }>`[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
keys | Object | - | Typescript:`KeysType`[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
operation | Array / Slot / Function | - | Typescript:`Array<string \| TNode> \| TNode<{ direction: 'left' \| 'right' }>`[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
pagination | Object / Array | - | Typescript:`PaginationProps \| Array<PaginationProps>`[Pagination API Documents](./pagination?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
search | Boolean / Object / Array | false | Typescript:`SearchOption \| Array<SearchOption>` `type SearchOption = boolean \| InputProps`[Input API Documents](./input?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
showCheckAll | Boolean / Array | true | Typescript:`boolean \| Array<boolean>` | N
targetSort | String | original | options:original/push/unshift | N
targetDraggable | Boolean | false | allowed to sort the target list by dragging | N
targetSort | String | original | options: original/push/unshift | N
title | Array / Slot / Function | [] | Typescript:`Array<TitleType> \| TNode<{ type: TransferListType }>` `type TitleType = string \| TNode` `type TransferListType = 'source' \| 'target'`[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
transferItem | Slot / Function | - | Typescript:`TNode<TransferItem<T>>` `interface TransferItem<T extends DataOption = DataOption> { data: T; index: number; type: TransferListType}`[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
tree | Slot / Function | 传入 Tree 组件定义树形结构 | Typescript:`(tree: TreeProps) => TNode`[Tree API Documents](./tree?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
Expand Down
4 changes: 2 additions & 2 deletions src/transfer/transfer.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
-- | -- | -- | -- | --
checkboxProps | Object | - | 用于控制复选框属性。TS 类型:`CheckboxProps`[Checkbox API Documents](./checkbox?tab=api)[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
checked | Array | [] | 数据列表选中项。支持语法糖 `.sync`。TS 类型:`Array<TransferValue>` | N
defaultChecked | Array | [] | 数据列表选中项。非受控属性。TS 类型:`Array<TransferValue>` | N
data | Array | [] | 全量数据。TS 类型:`Array<T>` | N
direction | String | both | 穿梭框可操作方向。可选项:left/right/both | N
disabled | Boolean / Array | false | 禁用全部操作:搜索、选中、移动、分页等。[源列表, 目标列表],示例:[true, false] 或者 true。TS 类型:`boolean \| Array<boolean>` | N
disabled | Boolean / Array | - | 禁用全部操作:搜索、选中、移动、分页等。[源列表, 目标列表],示例:[true, false] 或者 true。TS 类型:`boolean \| Array<boolean>` | N
empty | String / Array / Slot / Function | '' | 列表为空时呈现的内容。值类型为数组,则表示分别控制源列表和目标列表数据为空的呈现内容。TS 类型:`EmptyType \| Array<EmptyType> \| TNode` `type EmptyType = string \| TNode `[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
footer | Array / Slot / Function | - | 穿梭框底部内容。TS 类型:`Array<string \| TNode> \| TNode<{ type: TransferListType }>`[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
keys | Object | - | 用来定义选项文本和选项值字段,示例:`{ label: 'text', value: 'id' }`,表示选项文本取 `text` 字段,选项值取 `id` 字段。TS 类型:`KeysType`[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
operation | Array / Slot / Function | - | 方向操作按钮。默认显示组件内置操作图标。自定义操作图标示例:['向左', '向右'] 或者 `[() => <i class='left' />, () => <i class='left' />]` 或者 `(h, direction) => direction === 'left' ? '《' : '》'`。TS 类型:`Array<string \| TNode> \| TNode<{ direction: 'left' \| 'right' }>`[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
pagination | Object / Array | - | 分页配置,值为空则不显示。具体 API 参考分页组件。值类型为数组,表示可分别控制源列表和目标列表分页组件。TS 类型:`PaginationProps \| Array<PaginationProps>`[Pagination API Documents](./pagination?tab=api)[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
search | Boolean / Object / Array | false | 搜索框配置,值为 false 表示不显示搜索框;值为 true 表示显示默认搜索框;值类型为对象,用于透传 Props 到 Input 组件;值类型为数组,则分别表示控制两侧搜索框。TS 类型:`SearchOption \| Array<SearchOption>` `type SearchOption = boolean \| InputProps`[Input API Documents](./input?tab=api)[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
showCheckAll | Boolean / Array | true | 是否显示全选,值类型为数组则表示分别控制源列表和目标列表。TS 类型:`boolean \| Array<boolean>` | N
targetDraggable | Boolean | false | 是否允许通过拖拽对目标列表进行排序 | N
targetSort | String | original | 目标数据列表排列顺序。可选项:original/push/unshift | N
title | Array / Slot / Function | [] | 穿梭框标题,示例:['源列表', '目标列表'] 或者 `[() => 'A', () => 'B']` 或者 `({ type }) => type === 'source' ? '源' : '目标'`。TS 类型:`Array<TitleType> \| TNode<{ type: TransferListType }>` `type TitleType = string \| TNode` `type TransferListType = 'source' \| 'target'`[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
transferItem | Slot / Function | - | 自定义渲染节点。TS 类型:`TNode<TransferItem<T>>` `interface TransferItem<T extends DataOption = DataOption> { data: T; index: number; type: TransferListType}`[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/transfer/type.ts) | N
Expand Down
16 changes: 12 additions & 4 deletions src/transfer/transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ import {
getTransferData,
filterTransferData,
TRANSFER_NAME,
SOURCE,
TARGET,
} from './utils';

const SOURCE = 'source';
const TARGET = 'target';

export default mixins(getConfigReceiverMixins('transfer')).extend({
name: TRANSFER_NAME,
components: {
Expand All @@ -39,7 +38,10 @@ export default mixins(getConfigReceiverMixins('transfer')).extend({
prop: 'value',
event: 'change',
},
props,
props: {
...props,
targetDraggable: Boolean,
},
data(): {
SOURCE: TransferListType;
TARGET: TransferListType;
Expand Down Expand Up @@ -202,6 +204,9 @@ export default mixins(getConfigReceiverMixins('transfer')).extend({
handlePageChange(pageInfo: PageInfo, listType: TransferListType) {
emitEvent<Parameters<TdTransferProps['onPageChange']>>(this, 'page-change', pageInfo, { type: listType });
},
handleDataChange(data: Array<TransferValue>) {
this.$emit('change', data, null);
},
renderTransferList(listType: TransferListType) {
const scopedSlots = pick(this.$scopedSlots, ['title', 'empty', 'footer', 'operation', 'transferItem', 'default']);
return (
Expand All @@ -222,10 +227,13 @@ export default mixins(getConfigReceiverMixins('transfer')).extend({
onScroll={($event: Event) => this.handleScroll($event, listType)}
onSearch={this.handleSearch}
onPageChange={(pageInfo: PageInfo) => this.handlePageChange(pageInfo, listType)}
onDataChange={this.handleDataChange}
scopedSlots={scopedSlots}
t={this.t}
global={this.global}
isTreeMode={this.isTreeMode}
currentValue={this.value}
draggable={this.targetDraggable && listType === TARGET}
></transfer-list>
);
},
Expand Down
Loading

0 comments on commit 9af7497

Please sign in to comment.