新的学习方法
- 用手写简单方法实现一个功能
- 然后用比较成熟的第三方解决方案
- 即能学习原理又能学习第三方库的使用
从两个DEMO开始
- Vue Draggable Next: Vue Draggable Next
- React Sortable HOC: React Sortable HOC
列表排序的三个阶段
- 拖动开始(dragstart)
- 被拖动图层的状态变化
- 会出一个浮层
- 拖动进行中(dragmove)
- 浮层会随着鼠标移动
- 条目发生换位:当浮层下沿超过被拖条目二分之一的时候,触发换位。
这是一个比较独特的需求
- 松开鼠标阶段(drop)
- 浮层消失
- 被拖动图层状态复原
- 数据被更新
拖动排序功能开发
第一阶段 Dragstart
- 被拖动图层的状态变化 常规做法
- 添加mouseDown事件,检查当前的target是那个元素,然后给他添加特定的状态
- 添加mouseMove事件,创一个和被拖动元素一模一样的的浮层,将它的定位设置
它的定位为绝对定位,并且随着鼠标的坐标更新。
- 使用HTML的Drag特性
- 文档地址:拖拽操作
- 浏览器的默认拖拽行为:支持图象,链接和选择的文本
- 其他元素默认情况是不可拖拽的。
如果想可以拖拽可以设置为draggable = true
- 使用dragstart事件监控拖动开始,并设置对应的属性
LayerList组件中添加draggable属性
// LayerList.vue
<li
class="ant-list-item"
v-for="item in list" :key="item.id"
:class="{ active: item.id === selectedId }"
@click="handleClick(item.id)"
draggable="true"
></li>
这样就可以有效果了:当拖动对应条目的时候,它会自动生成半透明的条目,并且跟随鼠标的移动。
接下来就开始使用dragstart事件监控拖动开始,并设置对应的属性
- 给被拖动元素添加特定的状态:使用一系列的事件来监控拖动的进度,使用
dragStart
开始拖动操作
// LayerList.vue
// html部分
<li
class="ant-list-item"
v-for="item in list" :key="item.id"
:class="{ active: item.id === selectedId, ghost: dragData.currentDragging === item.id}"
@click="handleClick(item.id)"
@dragstart="onDragStart($event, item.id)"
draggable="true"
></li>
// js部分(setup)
const dragData = reactive({
currentDragging: ''
})
const onDragStart = (e: DragEvent, id: string ) => {
dragData.currentDragging = id;
}
// css部分
.ant-list-item.ghost {
opacity: 0.5;
}
完成出来的效果:
接下来就是在鼠标松开的时候,特定的状态消失:使用drop
事件:
<ul :list="list" class="ant-list-items ant-list-border" @drop="onDrop"></ul>
const onDrop = (e: DragEvent ) => {
dragData.currentDragging = '';
}
但是这样做发现不起作用,后来发现是onDrog
事件并没有触发,原因:
dragenter 或 dragover 事件的监听程序用于表示有效的放置目标,也就是被拖拽项目可能放置的地方。网页或应用程序的大多数区域都不是放置数据的有效位置。因此,这些事件的默认处理是不允许放置。
指定放置对象
因为网页大部分区域不是有效的放置位置,这些事件的默认处理都是不允许放置,所以这个行为并不会被触发。
如果你想要允许放置,你必须取消 dragenter 和 dragover 事件来阻止默认的处理。你可以在属性定义的事件监听程序返回 false,或者调用事件的 preventDefault() 方法来实现这一点。在一个独立脚本中的定义的函数里,可能后者更可行。
最终添加阻止默认行为事件:
<ul :list="list" class="ant-list-items ant-list-border" @drop="onDrop" @dragover="onDragOver">
const onDragOver = (e: DragEvent) => {
e.preventDefault()
}
处理松开鼠标时进行排序
-
修改dragData 添加一个当前索引的属性
const dragData = reactive({ currentDragging: '', currentIndex: -1, });
-
@dragstart=“onDragStart($event, item.id, index)” 方法中多添加一个index参数
const onDragStart = (e: DragEvent, id: string, index: number) => { dragData.currentDragging = id; dragData.currentIndex = index; };
有了开始拖动的index之后,我们要知道drop的时候新的index,我们怎么在onDrop
方法中拿到新的index呢?因为在onDrop
中我们的参数是event
,使用event.target
可以拿到dom元素,把最新的index放到dom元素上面就可以了,使用:HTMLElement.dataset
3. 使用 HTMLElement.dataset
拿到最新的索引
HTMLElement.dataset属性允许无论是在读取模式和写入模式下访问在HTML 或 DOM中元素上设置的
所有自定义数据属性(data-*)集
它是一个DOMString的映射,每个自定义数据属性的一个条目。
请注意,dataset属性本身可以被读取,但不能直接写入,相反,所有的写入必股友是它的属性,这反过来
表示数据属性。
还要注意,一个HTML data-attribute 及其对应的DOM dataset.property 不共享相同的名称,但它
们总是相似的:
<li
class="ant-list-item"
:class="{
active: item.id === selectedId,
ghost: dragData.currentDragging === item.id,
}"
v-for="(item, index) in list"
:key="item.id"
@click="handleClick(item.id)"
@dragstart="onDragStart($event, item.id, index)"
:data-index="index"
draggable="true"
>
- 修改onDrop事件
const onDrop = (e: DragEvent) => {
const currentEle = e.target as HTMLElement;
if (currentEle.dataset.index) {
const moveIndex = parseInt(currentEle.dataset.index);
console.log(moveIndex);
}
dragData.currentDragging = "";
};
但是这样写moveIndex
是不一定存在的,因为e.target
是鼠标指向的元素,所以当在目标子元素上面进行释放的话,就会把目标当成子元素,比如如果释放到的元素是锁元素,则currentEle
就是锁元素。所以这里需要一个方法来向上进行检索,找到符合条件的父元素。
export const getParentElement = (element: HTMLElement, className: string) => {
while (element) {
if (element.classList && element.classList.contains(className)) {
return element;
} else {
element = element.parentNode as HTMLElement;
}
}
return null;
};
const onDrop = (e: DragEvent) => {
const currentEle = getParentElement(
e.target as HTMLElement,
'ant-list-item'
);
if (currentEle && currentEle.dataset.index) {
const moveIndex = parseInt(currentEle.dataset.index);
// 使用第三方库arrayMove改变数组
arrayMove.mutate(props.list, dragData.currentIndex, moveIndex);
}
dragData.currentDragging = '';
};
array-move
最终实现的效果:
在拖动时完成排序:
const onDragEnter = (e: DragEvent, index: number) => {
// 这里的判断是为了避免完成转换后,触发新的一次dragEnter事件
if (index !== dragData.currentIndex) {
console.log('enter', index, dragData.currentIndex);
arrayMove.mutate(props.list, dragData.currentIndex, index);
dragData.currentIndex = index;
end = index
}
};
这样就可以在拖动时完成排序了,onDrop
里面就不需要进行同样的操作了,修改一下onDrop
事件
let start = -1;
let end = -1;
const onDragStart = (e: DragEvent, id: string, index: number) => {
dragData.currentDragging = id;
dragData.currentIndex = index;
start = index;
};
const onDrop = (e: DragEvent) => {
context.emit('drop', { start, end})
dragData.currentDragging = '';
};
现在就完成了可拖动排序的简单编码,主要掌握三个阶段:
- 排序开始:监控被拖拽的元素,添加特殊状态和UI
- 移动阶段:进入别的列表的时候完成数据的交换
- drop阶段:松开按钮的时候,将状态恢复原状,并且发送对应的事件。
使用第三方库进行排序:
使用Vue Draggable进行排序:
npm i -S vuedraggable@next
将用draggable替换掉ul
<template>
<draggable
:list="list"
class="ant-list-items ant-list-bordered"
ghost-class="ghost"
handle=".handle"
item-key="id"
>
<template #item="{ element }">
<li
class="ant-list-item"
:class="{ active: element.id === selectedId }"
@click="handleClick(element.id)"
>
<a-tooltip :title="element.isHidden ? '显示' : '隐藏'">
<a-button
shape="circle"
@click.stop="
handleChange(element.id, 'isHidden', !element.isHidden)
"
>
<template v-slot:icon v-if="element.isHidden"
><EyeInvisibleOutlined />
</template>
<template v-slot:icon v-else><EyeOutlined /> </template>
</a-button>
</a-tooltip>
<a-tooltip :title="element.isLocked ? '解锁' : '锁定'">
<a-button
shape="circle"
@click.stop="
handleChange(element.id, 'isLocked', !element.isLocked)
"
>
<template v-slot:icon v-if="element.isLocked"
><LockOutlined/>
</template>
<template v-slot:icon v-else><UnlockOutlined /> </template>
</a-button>
</a-tooltip>
<inline-edit
class="edit-area"
:value="element.layerName"
@change="
(value) => {
handleChange(element.id, 'layerName', value);
}
"
></inline-edit>
<a-tooltip title="拖动排序">
<a-button shape="circle" class="handle">
<template v-slot:icon><DragOutlined /> </template
></a-button>
</a-tooltip>
</li>
</template>
</draggable>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import draggable from 'vuedraggable';
import {
EyeOutlined,
EyeInvisibleOutlined,
LockOutlined,
UnlockOutlined,
DragOutlined,
} from '@ant-design/icons-vue';
import { ComponentData } from '../store/editor';
import InlineEdit from '../components/InlineEdit.vue';
export default defineComponent({
props: {
list: {
type: Array as PropType<ComponentData[]>,
required: true,
},
selectedId: {
type: String,
required: true,
},
},
emits: ['select', 'change', 'drop'],
components: {
EyeOutlined,
EyeInvisibleOutlined,
LockOutlined,
UnlockOutlined,
InlineEdit,
draggable,
DragOutlined,
},
setup(props, context) {
const handleClick = (id: string) => {
context.emit('select', id);
};
const handleChange = (id: string, key: string, value: boolean) => {
const data = {
id,
key,
value,
isRoot: true,
};
context.emit('change', data);
};
return {
handleChange,
handleClick,
};
},
});
</script>
<style scoped>
.ant-list-item {
padding: 10px 15px;
transition: all 0.5s ease-out;
cursor: pointer;
justify-content: normal;
border: 1px solid #fff;
border-bottom-color: #f0f0f0;
}
.ant-list-item.active {
border: 1px solid #1890ff;
}
.ant-list-item.ghost {
opacity: 0.5;
}
.ant-list-item:hover {
background: #e6f7ff;
}
.ant-list-item > * {
margin-right: 10px;
}
.ant-list-item button {
font-size: 12px;
}
.ant-list-item .handle {
cursor: move;
margin-left: auto;
}
.ant-list-item .edit-area {
width: 100%;
}
</style>