SortableJS实现拖动排序
概述
实际项目中,偶尔会遇到关于拖动相关的业务,比较常见的就是拖动调整顺序,前端比较知名的拖动库Sortable,封装的很完善,包括位置计算,过渡转化等,这里记录一期使用Sortable实现拖动排序。
最终效果
代码
框架使用vue3
<template>
<div class="sortjs-demon">
<div class="drag-content">
<div class="draggle-box" ref="okuabriginBox">
<div
:class="['origin-box', 'box-item']"
v-for="(item, index) in originData"
:key="item.id"
ref="boxItem"
:data-id="item.id"
>
{{ item.label }}
<div class="my-handle"></div>
<div class="ignore-elements">禁止拖动</div>
</div>
</div>
<div
:class="['draggle-box', 'target-box', enterActive ? 'active' : '']"
ref="targetBox"
>
<div
class="box-item"
v-for="item in targetData"
:data-id="item.id"
:key="item.id"
>
{{ item.label }}
<div class="my-handle"></div>
<div class="ignore-elements">禁止拖动</div>
</div>
</div>
</div>
<div class="text-content">
<div class="box">originData: {{ stringFy(originData) }}</div>
<div class="box">targetData: {{ stringFy(targetData) }}</div>
</div>
<!-- <button @click="handleAdd">添加</button> -->
</div>
</template>
<script lang="ts" setup>
import Sortable from "sortablejs";
import { nextTick, onMounted, ref, watch } from "vue";
const backupList = [
{
id: 1,
label: "第一个",
},
{
id: 2,
label: "第二个",
},
{
id: 3,
label: "第三个",
},
{
id: 4,
label: "第四个",
},
];
let originData = ref(backupList);
let targetData: any = ref([]);
const originBox = ref<HTMLElement>();
const targetBox = ref<HTMLElement>();
const enterActive = ref(false);
const stringFy = JSON.stringify;
watch(originData, () => {
console.log("listDataChange", originData.value);
});
watch(targetData, () => {
console.log("targetListChange", targetData.value);
});
function initDraggle() {
const option = {
sort: true, //拖动过程中是否调整对应排列顺序
disabled: false, //是否禁止脱拽
animation: 300, //动画延时
group: {
name: "shared", //用于在多个列表之间拖动分组名(相同的才能互相拖拽到对应列表
// pull: "clone",//pull===>默认为移动过去,拷贝过去则设置true
put: true, //是否允许添加到这个列表
},
handle: ".my-handle", //拖动句柄,设置之后,只有拖动对应项中的设置当前类的元素才能生效
filter: ".ignore-elements", //添加当前类的元素会忽略拖动
preventOnFilter: true,
ghostClass: "ghostClass", //占位元素类名
chosenClass: "chosenClass", //选中元素类名
dragClass: "dragClass", //拖拽中元素
invertSwap: false, //是否反转交换
forceFallback: true, //TODO:禁止h5拖拽出的阴影(内部本质上模拟了拖拽出的阴影部分,克隆的一份原来的dom元素进行定位)
fallbackClass: "fallbackClass", //使用forceFallback时克隆的DOM元素的类名(等同于dragClass)
removeCloneOnHide: true, //当克隆元素不显示时删除它,而不是仅仅隐藏它
// direction: 'vertical',
// easing: "cubic-bezier(0.55, 0, 1, 0.45)",//动画曲线
// setData: function (dataTransfer: any, dragEl: any) {
// console.log(dataTransfer, dragEl);
// },
// 按下
// onChoose: function (evt: any) {
// console.log("onChoose", evt);
// },
// 松开按下
// onUnchoose: function (evt: any) {
// console.log("onUnChoose", evt);
// },
// 脱拽开始==>dragstart
// onStart: function (evt: any) {
// console.log("onStart", evt);
// },
// 脱拽结束
// onEnd: function (evt: any) {
// // var itemEl = evt.item; //当前脱拽的元素
// // evt.to; // 进入到的目标列表
// // evt.from; // 原来的脱拽列表
// // evt.oldIndex; // 脱拽元素在原来的位置的索引
// // evt.newIndex; // 脱拽元素在当前位置的索引
// console.log("onEnd", evt);
// },
// 元素从另一个列表中拖放到列表中
onAdd: function (evt: any) {
if (evt.to === originBox.value) {
updateData(
originBox.value!,
originData,
targetData,
Number(evt.item.dataset.id)
);
} else if (evt.to === targetBox.value) {
updateData(
targetBox.value!,
targetData,
originData,
Number(evt.item.dataset.id)
);
}
},
// TODO:脱拽后改变原来dom结构位置的时候(可以做响应式数据交换,脱拽排序更新等)===>同一个列表变化
onUpdate: function (evt: any) {
console.log("onUpdate", evt);
if (evt.from === evt.to && evt.from === originBox.value) {
const oldIndex = evt.oldIndex;
const newIndex = evt.newIndex;
const temp = originData.value[oldIndex];
originData.value[oldIndex] = originData.value[newIndex];
originData.value[newIndex] = temp;
} else if (evt.from === evt.to && evt.from === targetBox.value) {
const oldIndex = evt.oldIndex;
const newIndex = evt.newIndex;
const temp = targetData.value[oldIndex];
targetData.value[oldIndex] = targetData.value[newIndex];
targetData.value[newIndex] = temp;
}
},
// 通过对列表的任何更改(添加/更新/删除)调用
// onSort: function (evt: any) {
// console.log("onSort", evt);
// },
// element从列表中移除到另一个列表中
// onRemove: function (evt: any) {
// console.log("onRemove", evt);
// },
// 尝试拖动已过滤的元素(配置过filter属性的元素)
// onFilter: function (evt: any) {
// console.log("onFilter", evt);
// },
// 当您在列表中或在列表之间移动项目时调用
onMove: function (evt: any, originalEvent: any) {
evt.dragged; // 当前拖拽元素
evt.draggedRect; //拖拽元素的位置信息(本质上调用getBoundingClientRect())
evt.related; // 拖拽关联的元素
evt.relatedRect; // 拖拽关联元素的位置信息
evt.willInsertAfter; // Boolean 如果Sortable默认会在目标后插入拖动元素,则为true
originalEvent.clientY; // 当前鼠标Y位置
originalEvent.clientX; // 当前鼠标X位置
// return false; — for cancel//取消位置交换
// return -1; — insert before target//插入到目标元素前面
// return 1; — insert after target//插入目标元素后面
// return true; —根据方向保留默认插入点
// return void; —跟返回true一样
// console.log("onMove", evt, originalEvent);
},
// 克隆元素时候调用(forceFallback:true,或者拖拽到另外一个列表时候调用)
// onClone: function (evt: any) {
// console.log("onClone", evt);
// },
//当拖动元素改变位置时调用
// onChange: function (/**Event*/ evt: any) {
// console.log("onChange", evt);
// },
};
Sortable.create(originBox.value, option);
Sortable.create(targetBox.value, option);
}
function updateData(
dom: HTMLElement,
updateListRef: any,
originDataListRef: any,
id: number
) {
nextTick(() => {
const targetChildren = [...dom.children] as HTMLElement[];
const res: any = [];
targetChildren.forEach((element) => {
const findItem = backupList.find(
(item) => item.id === Number(element.dataset.id)
);
if (findItem) {
res.push(findItem);
}
});
updateListRef.value = res;
originDataListRef.value = originDataListRef.value.filter(
(item: any) => item.id !== id
);
});
}
onMounted(() => {
initDraggle();
});
</script>
<style lang="less">
.sortjs-demon {
margin-top: 30px;
.drag-content {
display: flex;
justify-content: space-around;
}
.text-content {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30px;
}
.my-handle {
width: 20px;
height: 20px;
background-color: #008c8c;
position: absolute;
right: 0;
top: 0;
}
.ghostClass {
border: 1px solid red;
}
.chosenClass {
// background-color: #008c8c!important;
}
.dragClass {
background-color: yellow !important;
}
.fallbackClass {
background-color: purple !important;
}
.ignore-elements {
width: 50px;
height: 30px;
text-align: center;
line-height: 30px;
font-size: 12px;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
background-color: green;
}
* {
margin: 0;
padding: 0;
}
.origin-box.dragging {
user-select: none;
background-color: red;
}
.draggle-box {
width: 500px;
height: 500px;
border: 1px solid #000;
display: flex;
}
.target-box {
.box-item {
background-color: blue;
}
}
.draggle-box.active {
border-color: red;
}
.box-item {
border-radius: 50%;
margin-right: 3px;
text-align: center;
line-height: 100px;
width: 100px;
user-select: none;
height: 100px;
position: relative;
}
.box-item.is-drag {
background-color: #008c8c;
}
.origin-box {
background-color: pink;
}
.draggle-box-wrap {
display: flex;
justify-content: space-around;
margin-top: 30px;
}
}
</style>
转载自:https://juejin.cn/post/7257177072678453305