特徴:
- カスタム期間 (デフォルトでは 300 ミリ秒) で長押しすると、ドラッグされた要素のコピーが生成されます。
- ドラッグ&ドロップで掴み手を表示
- ドラッグ プロセスは、ターゲットの衝突要素配列内にタッチされた要素があるかどうかをリアルタイムでリッスンして判断し、要素配列と関連する .getBoundingClientRect() パラメーターを返します。
sgDrag.vue コンポーネントのソース コード
<template>
<div :class="$options.name">
<img ref="cloneDrag" class="cloneDrag" :src="src" v-if="src" :style="{ ...style, ...customStyle }"
draggable="false" />
</div>
</template>
<script>
import html2canvas from 'html2canvas'; // npm install --save html2canvas
export default {
name: 'sgDrag',
data: () => ({
offset: { x: 0, y: 0, },//偏移量
style: { top: '0px', left: '0px', },
src: null,//克隆的元素图片
cloneDrag: null,//cloneDrag元素
originDrag: null,//被拖拽的原始元素
mouseLeaveOriginDrag: false,//当前鼠标已经离开原始元素
crashDoms: [],//记录碰撞接触的元素
timeout: null,
}),
props: [
"data",//可以被拖拽的元素数组(必选)
"targets",//需要被拖拽接触碰撞检测的元素(必选)
"type",//屏蔽(可选值:crash[碰撞接触]|mouse[鼠标点位接触],默认值:crash)
"disabled",//屏蔽
"customStyle",//自定义拖拽过程样式
"delay",//默认长按0.2秒
],
watch: {
data: {
handler(newValue, oldValue) {
newValue ? this.__addDragsEvents(newValue) : this.__removeDragsEvents(oldValue);
}, deep: true, immediate: true,
},
disabled: {
handler(newValue, oldValue) {
newValue ? this.__removeAllEvents() : this.__addAllEvents();
}, deep: true, immediate: true,
},
},
destroyed() {
this.__removeAllEvents();
},
methods: {
__addAllEvents() {
this.__addDragsEvents(this.data);
},
__removeAllEvents() {
this.__removeWindowEvents();
this.__removeDragsEvents(this.data);
this.mouseup_window();
},
__addWindowEvents() {
this.__removeWindowEvents();
addEventListener('mousemove', this.mousemove_window);
addEventListener('mouseup', this.mouseup_window);
},
__removeWindowEvents() {
removeEventListener('mousemove', this.mousemove_window);
removeEventListener('mouseup', this.mouseup_window);
},
// 初始化需要拖拽的DIV
__addDragsEvents(doms) {
(doms || []).forEach(dom => {
this.__addDraggedEvents(dom);
});
},
__removeDragsEvents(doms) {
(doms || []).forEach(dom => {
this.__removeDraggedEvents(dom);
});
},
__addDraggedEvents(dom) {
this.__removeDraggedEvents(dom);
dom.addEventListener('dragstart', this.dragstart);
dom.addEventListener('mousedown', this.mousedown);
dom.addEventListener('mouseup', this.mouseup);
dom.addEventListener('mouseleave', this.mouseleave);
},
__removeDraggedEvents(dom) {
dom.removeEventListener('dragstart', this.dragstart);
dom.removeEventListener('mousedown', this.mousedown);
dom.removeEventListener('mouseup', this.mouseup);
dom.removeEventListener('mouseleave', this.mouseleave);
},
dragstart(e) {
e.stopPropagation(); e.preventDefault(); return false;
},
mousedown(e) {
if (e.button === 2) return this.mouseup_window(e);//点击了鼠标右键
if (this.disabled) return this.mouseup_window(e);
this.mouseLeaveOriginDrag = false;
this.originDrag = e.currentTarget;
this.timeout = setTimeout(() => {
this.timeout = null;
if (this.originDrag) {
html2canvas(this.originDrag).then(canvas => {
// 在还未生成副本图片的时候就离开了原始元素
if (this.mouseLeaveOriginDrag) {
this.mouseup_window(e);
} else {
//只有生成了副本图片才可以开始记录鼠标移动监听
this.src = canvas.toDataURL('image/jpeg', 1.0);
this.$nextTick(() => {
if (this.disabled) return this.mouseup_window(e);
this.cloneDrag = this.$refs.cloneDrag;
if (this.originDrag) {
let or = this.originDrag.getBoundingClientRect();
this.offset = { x: e.clientX - or.x, y: e.clientY - or.y, };
this.$emit('dragStart', this.getResult(e));
this.mousemove_window(e);
this.__addWindowEvents();
}
});
}
});
}
}, typeof this.delay === 'undefined' ? 200 : this.delay);
},
mouseleave(e) {
if (this.disabled) return this.mouseup_window(e);
this.mouseLeaveOriginDrag || this.$emit('dragOut', this.getResult(e));
this.mouseLeaveOriginDrag = true;
},
mouseup(e) {
this.timeout && (clearTimeout(this.timeout), this.timeout = null);
},
mousemove_window(e) {
if (this.disabled) return this.mouseup_window(e);
if (this.cloneDrag) {
this.cloneDrag.setAttribute("dragging", "true"); //拖拽过程变成虚线的样子
let x = e.clientX - this.offset.x;
let y = e.clientY - this.offset.y;
this.style = { left: x + 'px', top: y + 'px', };
this.$nextTick(() => {
// 当新碰撞接触的对象数组和上次碰撞接触的对象数组不同的时候执行
this.crashDoms = this.getCrashDoms(e);
this.$emit('dragging', this.getResult(e));
});
}
},
mouseup_window(e) {
this.$emit('dragEnd', this.getResult(e));
this.offset = null;
this.style = null;
this.src = null;
this.originDrag && this.originDrag.removeAttribute('draggable');
this.originDrag = null;
this.mouseLeaveOriginDrag = false;
this.crashDoms = [];
this.__removeWindowEvents();
},
getResult(e) {
return {
$event: e,
originDrag: this.originDrag,
originDragRect: this.originDrag ? this.originDrag.getBoundingClientRect() : null,
cloneDrag: this.cloneDrag,
cloneDragRect: this.cloneDrag ? this.cloneDrag.getBoundingClientRect() : null,
crashDoms: this.crashDoms,
}
},
// 获取碰撞的DOM
getCrashDoms(e) {
let r = [];
let targets = this.targets;
targets['item'] && (targets = [].slice.call(targets));//NodeList对象
Array.isArray(targets) || (targets = [targets]);//单个元素处理成数组
if (targets && targets.length) {
switch (this.type) {
case 'mouse'://鼠标点位接触
r = targets.filter(target => this.isMouseInTraget(target, e))
break;
case 'crash'://碰撞接触
default:
r = targets.filter(target => this.isCrash(target, this.cloneDrag))
}
}
return r;// 获取被碰撞接触的内容
},
//鼠标在元素内部
isMouseInTraget(targetDom, mousePoint) {
if (!mousePoint) return false;
let tr = targetDom.getBoundingClientRect();
let t = { x1: tr.x, x2: tr.x + tr.width, y1: tr.y, y2: tr.y + tr.height };//目标对象的四角顶点坐标
let isIn =
mousePoint.clientX >= t.x1 && mousePoint.clientX <= t.x2 &&
mousePoint.clientY >= t.y1 && mousePoint.clientY <= t.y2;
return isIn;
},
//碰撞检测
isCrash(targetDom, moveDom) {
/*
targetDom:目标对象(即将要被碰撞的元素)
moveDom:移动的对象(可能是拖拽移动,也可能是其他原因导致其移动)
*/
if (targetDom === moveDom) return false;//如果目标对象和移动对象是同一个,返回未接触
let tr = targetDom.getBoundingClientRect(), mr = moveDom.getBoundingClientRect();
let t = { x1: tr.x, x2: tr.x + tr.width, y1: tr.y, y2: tr.y + tr.height };//目标对象的四角顶点坐标
let m = { x1: mr.x, x2: mr.x + mr.width, y1: mr.y, y2: mr.y + mr.height };//移动对象的四角顶点坐标
let a = { w: Math.min(t.x2, m.x2) - Math.max(t.x1, m.x1), h: Math.min(t.y2, m.y2) - Math.max(t.y1, m.y1) };//计算相交部分的宽度和高度
let area = (a.w > 0 ? a.w : 0) * (a.h > 0 ? a.h : 0);//计算相交部分的面积
return area ? true : false;//面积>0,即碰撞(这里可以根据业务需求,改成相交部分面积>具体的值才作为碰撞判断)
},
}
};
</script>
<style lang="scss" scoped>
.sgDrag {
pointer-events: none;
.cloneDrag {
position: fixed;
z-index: 999; //根据情况自己拿捏
left: 0;
top: 0;
pointer-events: auto;
cursor: grab;
&:active {
cursor: grabbing;
opacity: 0.618;
}
&[dragging] {
cursor: grabbing;
opacity: 0.9;
border: 1px dashed #409EFF;
transform: translate(-3px, -3px);
box-shadow: 5px 10px 0 rgba(0, 0, 0, 0.05);
}
}
}
</style>
アプリケーションコンポーネント
<template>
<div class="sg-body">
<sgDrag :data="data" :targets="targets" @dragStart="dragStart" @dragging="dragging" @dragOut="dragOut"
@dragEnd="dragEnd" :customStyle="{
'box-color': '#409EFF',
'box-shadow': '0 12px 22px 0 #409EFF'
}" />
<div class="drag-container">
<div class="title">从此处拖拽出去</div>
<div class="drag-div" :ref="`drag-div${i}`" v-for="(a, i) in items" :key="i" :index="i">{
{ a.label }}</div>
</div>
<div class="drag-in-container drag-container" ref="drag-in-container">
<div class="title">放入此处</div>
<div class="drag-div" :ref="`drag-div${i}`" v-for="(a, i) in dragInItems" :key="i" :index="i">{
{ a.label }}
</div>
</div>
</div>
</template>
<script>
import sgDrag from "@/vue/components/sgDrag";
export default {
components: { sgDrag },
data: () => ({
data: [],//可以被拖拽的元素数组
targets: [],//需要被拖拽接触碰撞检测的元素
checkboxGroupValue: [],
items: [...Array(20)].map((v, i) => ({ label: '显示文本' + i, value: i })),
dragInItems: [],//动态拖拽进入的元素列表
}),
mounted() {
this.data = [...Array(20)].map((v, i) => this.$refs[`drag-div${i}`][0]);
this.targets = [this.$refs['drag-in-container']];
console.log(this.data)
},
methods: {
dragStart(d) {
// console.log(`拖拽开始`, d);
},
dragging(d) {
// console.log(`拖拽中`, d);
let crashDoms = d.crashDoms;
this.targets.forEach(dom => {
crashDoms.includes(dom) ? dom.setAttribute('active', true) : dom.removeAttribute('active');
});
},
dragOut(d) {
// console.log(`拖拽的时候鼠标离开元素`, d);
},
dragEnd(d) {
// console.log(`拖拽结束`, d);
this.targets.forEach(dom => dom.removeAttribute('active'));
// 如果翻下拖拽那一刻,正好在右侧区域内
if (d.crashDoms.includes(this.targets[0])) {
let index = parseInt(d.originDrag.getAttribute('index'));
this.dragInItems.push(this.items.splice(index, 1)[0]);
console.log(this.dragInItems, this.items)
}
},
}
};
</script>
<style lang="scss" scoped>
.sg-body {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.title {
width: 100%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 18px;
}
.drag-div {
width: 100px;
height: 50px;
background-color: #409EFF;
margin: 2px;
display: flex;
justify-content: center;
align-items: center;
}
.drag-container {
width: 300px;
height: 500px;
overflow-y: auto;
background-color: #00000011;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
}
.drag-in-container {
background-color: #409EFF11;
box-sizing: border-box;
border: 1px dashed transparent;
&[active] {
border-color: #409EFF;
background-color: #409EFF22;
}
}
}
</style>
sgDrag 2.0 バージョンはこちら↓ 【sgDrag_v2】[独自の研究開発] クローン要素を生成する html2canvas のサイクルが固定されていないため、ドラッグ イベントの監視には、dragstart、drag、dragend が使用されます。https://blog.csdn.net/qq_37860634/article/details/131676350