使用vue 封装一个拖拽组件
TIP
封装一个支持拖拽的组件,效果图如下:
功能支持
- 1.支持方向(LB、LT、RB、RT)
- 2.仅支持在屏幕内拖动(边缘检测)
- 3.支持设置边缘margin
支持拖拽
TIP
在仅支持拖拽默认在左上角还是比较好实现的,具体如下:
vue
<template>
<div
ref="draggableRef"
class="draggable"
:style="style"
>
<slot />
</div>
</template>
<script>
export default {
name: 'DraggableComponent',
props: {
initialValue: {
type: Object,
required: false,
default: () => ({ x: 0, y: 0 }),
},
},
data() {
return {
currentValue: { x: 0, y: 0 },
isDragging: false,
startX: 0,
startY: 0,
diffX: 0,
diffY: 0,
};
},
computed: {
style() {
return `left: ${this.currentValue.x + this.diffX}px; top: ${this.currentValue.y + this.diffY}px`;
},
},
watch: {
initialValue: {
handler(val) {
this.currentValue = val;
},
immediate: true,
},
},
mounted() {
this.$nextTick(() => {
const { draggableRef } = this.$refs;
if (draggableRef) {
draggableRef.addEventListener('mousedown', this.startDrag);
document.addEventListener('mousemove', this.moveDrag);
document.addEventListener('mouseup', this.endDrag);
}
});
},
beforeDestroy() {
const { draggableRef } = this.$refs;
draggableRef.removeEventListener('mousedown', this.startDrag);
document.removeEventListener('mousemove', this.moveDrag);
document.removeEventListener('mouseup', this.endDrag);
},
methods: {
startDrag({ clientX: x1, clientY: y1 }) {
this.isDragging = true;
document.onselectstart = () => false;
this.startX = x1;
this.startY = y1;
},
moveDrag({ clientX: x2, clientY: y2 }) {
if (this.isDragging) {
this.diffX = x2 - this.startX;
this.diffY = y2 - this.startY;
}
},
endDrag() {
this.isDragging = false;
document.onselectstart = () => true;
this.currentValue.x += this.diffX;
this.currentValue.y += this.diffY;
this.diffX = 0;
this.diffY = 0;
},
},
};
</script>
<style>
.draggable {
position: fixed;
z-index: 9;
}
</style>
使用方式:
vue
<template>
<div class="wrap">
<DraggableComponent>
<div style="width: 300px; height: 200px;background: red;">
123
</div>
</DraggableComponent>
</template>
<script>
import DraggableComponent from '@/components/draggable/index.vue';
export default {
components: { DraggableComponent },
};
</script>
TIP
代码解释就是:在鼠标摁下的时候开始isDragging
字段,然后还是计算move
的距离,最后赋值给拖动的元素。
支持4个方向以及增加边缘检测
TIP
向着目标实现 完整代码如下:
vue
<template>
<div
ref="draggableRef"
class="draggable"
:style="style"
>
<slot />
</div>
</template>
<script>
const getScrollBarWidth = () => {
const outer = document.createElement('div');
outer.style.overflow = 'scroll';
outer.style.height = '200px';
outer.style.width = '100px';
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
const scrollBarWidth = widthNoScroll - widthWithScroll;
return scrollBarWidth;
};
const START_DIRECTION = {
RT: 'RT', // right -> top
RB: 'RB', // right -> bottom
LT: 'LT', // left -> top
LB: 'LB', // left -> bottom
};
export default {
name: 'DraggableComponent',
props: {
initialValue: {
type: Object,
required: false,
default: () => ({ x: 24, y: 24 }),
},
direction: {
type: String,
required: false,
default: START_DIRECTION.LT, // LT | LB | RB | RT
},
windowOnly: {
type: Boolean,
required: false,
default: false,
},
margin: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
currentValue: { x: 0, y: 0 },
isDragging: false,
startX: 0,
startY: 0,
diffX: 0,
diffY: 0,
maxValue: {
width: 0,
height: 0,
},
};
},
computed: {
style() {
const bottomValue = this.currentValue.y - this.diffY;
const topValue = this.currentValue.y + this.diffY;
const rightValue = this.currentValue.x - this.diffX;
const leftValue = this.currentValue.x + this.diffX;
if (this.direction === START_DIRECTION.RB) {
if (this.windowOnly) {
const [currentRightValue, currentBottomValue] = this.getWindowOnlyValue(START_DIRECTION.RB, rightValue, bottomValue);
return `right: ${currentRightValue}px; bottom: ${currentBottomValue}px`;
}
return `right: ${rightValue}px; bottom: ${bottomValue}px`;
}
if (this.direction === START_DIRECTION.RT) {
if (this.windowOnly) {
const [currentRightValue, currentTopValue] = this.getWindowOnlyValue(START_DIRECTION.RT, rightValue, topValue);
return `right: ${currentRightValue}px; top: ${currentTopValue}px`;
}
return `right: ${rightValue}px; top: ${topValue}px`;
}
if (this.direction === START_DIRECTION.LT) {
if (this.windowOnly) {
const [currentLeftValue, currentTopValue] = this.getWindowOnlyValue(START_DIRECTION.LT, leftValue, topValue);
return `left: ${currentLeftValue}px; top: ${currentTopValue}px`;
}
return `left: ${leftValue}px; top: ${topValue}px`;
}
if (this.direction === START_DIRECTION.LB) {
if (this.windowOnly) {
const [currentLeftValue, currentBottomValue] = this.getWindowOnlyValue(START_DIRECTION.LB, leftValue, bottomValue);
return `left: ${currentLeftValue}px; bottom: ${currentBottomValue}px`;
}
return `left: ${leftValue}px; bottom: ${bottomValue}px`;
}
return `left: ${leftValue}px; top: ${bottomValue}px`;
},
},
watch: {
initialValue: {
handler(val) {
this.currentValue = val;
},
immediate: true,
},
},
mounted() {
this.$nextTick(() => {
const { draggableRef } = this.$refs;
if (draggableRef) {
this.isWindowOnlyCheck();
draggableRef.addEventListener('mousedown', this.startDrag);
document.addEventListener('mousemove', this.moveDrag);
document.addEventListener('mouseup', this.endDrag);
}
});
window.addEventListener('resize', this.isWindowOnlyCheck);
},
beforeDestroy() {
const { draggableRef } = this.$refs;
draggableRef.removeEventListener('mousedown', this.startDrag);
document.removeEventListener('mousemove', this.moveDrag);
document.removeEventListener('mouseup', this.endDrag);
window.removeEventListener('resize', this.isWindowOnlyCheck);
},
methods: {
startDrag({ clientX: x1, clientY: y1 }) {
this.isDragging = true;
document.onselectstart = () => false;
this.startX = x1;
this.startY = y1;
},
moveDrag({ clientX: x2, clientY: y2 }) {
if (this.isDragging) {
this.diffX = x2 - this.startX;
this.diffY = y2 - this.startY;
}
},
getWindowOnlyValue(direction = START_DIRECTION.LT, value1 = 0, value2 = 0) {
const { height: maxHeight, width: maxWidth } = this.maxValue;
const minRight = this.margin;
const maxRight = maxWidth;
const minBottom = this.margin;
const maxBottom = maxHeight;
const minLeft = this.margin;
const maxLeft = maxWidth;
const minTop = this.margin;
const maxTop = maxHeight;
let currentRight = 0;
let currentLeft = 0;
if (value1 < 0) {
currentRight = value1 < minRight ? minRight : value1;
currentLeft = value1 < maxLeft ? maxLeft : value1;
} else {
currentRight = value1 > maxRight ? maxRight : value1;
currentLeft = value1 > maxLeft ? maxLeft : value1;
}
let currentBottom = 0;
let currentTop = 0;
if (value2 < 0) {
currentBottom = value2 < minBottom ? minBottom : value2;
currentTop = value2 < minTop ? minTop : value2;
} else {
currentBottom = value2 > maxBottom ? maxBottom : value2;
currentTop = value2 > maxTop ? maxTop : value2;
}
if (direction === START_DIRECTION.RB) {
return [currentRight, currentBottom];
}
if (direction === START_DIRECTION.RT) {
return [currentRight, currentTop];
}
if (direction === START_DIRECTION.LB) {
return [currentLeft, currentBottom];
}
if (direction === START_DIRECTION.LT) {
return [currentLeft, currentTop];
}
return [minLeft, maxLeft, minTop, maxTop];
},
endDrag() {
document.onselectstart = () => true;
const bottomValue = this.currentValue.y - this.diffY;
const topValue = this.currentValue.y + this.diffY;
const rightValue = this.currentValue.x - this.diffX;
const leftValue = this.currentValue.x + this.diffX;
if (this.direction === START_DIRECTION.RB) {
if (this.windowOnly) {
[this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.RB, rightValue, bottomValue);
} else {
this.currentValue.x = rightValue;
this.currentValue.y = bottomValue;
}
}
if (this.direction === START_DIRECTION.RT) {
if (this.windowOnly) {
[this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.RT, rightValue, topValue);
} else {
this.currentValue.x = rightValue;
this.currentValue.y = topValue;
}
}
if (this.direction === START_DIRECTION.LT) {
if (this.windowOnly) {
[this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.LT, leftValue, topValue);
} else {
this.currentValue.x = leftValue;
this.currentValue.y = topValue;
}
}
if (this.direction === START_DIRECTION.LB) {
if (this.windowOnly) {
[this.currentValue.x, this.currentValue.y] = this.getWindowOnlyValue(START_DIRECTION.LB, leftValue, bottomValue);
} else {
this.currentValue.x = leftValue;
this.currentValue.y = bottomValue;
}
}
this.isDragging = false;
this.diffX = 0;
this.diffY = 0;
},
isWindowOnlyCheck() {
if (this.windowOnly) {
const { draggableRef } = this.$refs;
const { width: conatinerWidth, height: containerHeight } = draggableRef?.getBoundingClientRect() || { width: 0, height: 0 };
const maxWidth = window.innerWidth;
const maxHeight = window.innerHeight;
const isScroll = document.body.scrollHeight > window.innerHeight;
this.maxValue = {
width: maxWidth - conatinerWidth - this.margin - (isScroll ? getScrollBarWidth() : 0),
height: maxHeight - containerHeight - this.margin,
};
}
},
},
};
</script>
<style>
.draggable {
position: fixed;
z-index: 9;
}
</style>
使用方式:
vue
<template>
<div class="wrap">
<DraggableComponent
start-direction="RB"
window-only
:margin="10"
>
<div style="width: 300px; height: 200px;background: red;">
123
</div>
</DraggableComponent>
</template>
<script>
import DraggableComponent from '@/components/draggable/index.vue';
export default {
components: { DraggableComponent },
};
</script>
结束语
TIP
在增加边缘检测的时候主要是要把滚动条的宽度需要参与计算,不然就会不准确。