Files
lingji-work-fe/src/views/components/drag/DraggableResizable.vue

429 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div :id="id" ref="container" class="draggable-resizable" :style="containerStyle">
<div class="header" @mousedown.prevent="onMouseDown" :style="{ cursor: mouseCursor }">
{{ id }}
</div>
<div
v-for="dir in directions"
:key="dir"
:class="['resize-handle', dir]"
@mousedown.stop.prevent="onResizeHandleMouseDown(dir, $event)"
></div>
</div>
</template>
<script setup lang="ts" name="DraggableResizable">
import type { PropType } from 'vue';
import { ref, reactive, defineProps, defineEmits } from 'vue';
const props = defineProps({
id: {
type: String,
required: true,
},
x: {
type: Number,
required: true,
},
y: {
type: Number,
required: true,
},
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
zIndex: {
type: String,
default: '1',
required: true,
},
snapDistance: {
type: Number,
default: 20,
},
onDrag: {
type: Function,
required: true,
},
onResize: {
type: Function,
required: true,
},
detectCollision: {
type: Function,
required: true,
},
detectSnap: {
type: Function,
required: true,
},
checkClosestComponent: {
type: Function,
required: true,
},
setCurrentComponent: {
type: Function,
required: true,
},
handleCollision: {
type: Function,
required: true,
},
directions: {
type: Array as PropType<string[]>,
required: false,
default: ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
},
});
const emit = defineEmits(['drag', 'resize']);
const mouseCursor = ref('grab');
const isCollied = ref(false);
const isSnap = ref(false);
const startX = ref(0);
const startY = ref(0);
const startLeft = ref(0);
const startTop = ref(0);
const container = ref<HTMLElement | null>(null);
const ghost = ref<HTMLElement | null>(null);
const containerStyle = reactive({
id: `${props.id}`,
width: `${props.width}px`,
height: `${props.height}px`,
top: `${props.y}px`,
left: `${props.x}px`,
zIndex: props.zIndex,
transition: 'none',
});
const updatePosition = (left: number, top: number, snap = false) => {
isSnap.value = snap;
if (isSnap.value) {
containerStyle.transition = 'left 0.08s ease-out, top 0.08s ease-out';
} else {
containerStyle.transition = 'none';
}
containerStyle.left = `${left}px`;
containerStyle.top = `${top}px`;
};
const updateGhostPosition = (left: number, top: number) => {
if (ghost.value) {
ghost.value.style.left = `${left}px`;
ghost.value.style.top = `${top}px`;
}
};
const updateSize = (width: number, height: number) => {
containerStyle.width = `${width}px`;
containerStyle.height = `${height}px`;
};
const onMouseDown = (event: MouseEvent) => {
mouseCursor.value = 'grabbing';
startX.value = event.clientX;
startY.value = event.clientY;
startLeft.value = parseInt(containerStyle.left);
startTop.value = parseInt(containerStyle.top);
props.setCurrentComponent(containerStyle);
containerStyle.zIndex = '3';
ghost.value = document.createElement('div');
ghost.value.style.width = containerStyle.width;
ghost.value.style.height = containerStyle.height;
ghost.value.style.backgroundColor = '#E33B54';
ghost.value.style.zIndex = '1';
ghost.value.style.position = 'absolute';
ghost.value.style.left = containerStyle.left;
ghost.value.style.top = containerStyle.top;
const fatherEle = document.getElementById(props.id);
fatherEle?.appendChild(ghost.value);
const onMouseMove = (moveEvent: MouseEvent) => {
//* 使用 requestAnimationFrame 来优化性能
requestAnimationFrame(() => {
const newLeft = startLeft.value + (moveEvent.clientX - startX.value);
const newTop = startTop.value + (moveEvent.clientY - startY.value);
updateGhostPosition(newLeft, newTop);
// const closestComponent = props.checkClosestComponent(
// props.id,
// newLeft,
// newTop,
// parseInt(containerStyle.width),
// parseInt(containerStyle.height),
// );
//* console.log('closestComponent', closestComponent);
updatePosition(newLeft, newTop, false);
});
};
const onMouseUp = (moveEvent: MouseEvent) => {
moveAndResize(moveEvent);
mouseCursor.value = 'grab';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (isCollied.value) {
containerStyle.zIndex = '3';
} else {
containerStyle.zIndex = '1';
isCollied.value = false;
}
containerStyle.transition = 'none';
console.log('containerStyle', containerStyle);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
ghost.value.parentNode?.removeChild(ghost.value);
};
const moveAndResize = (moveEvent: MouseEvent) => {
const newLeft = startLeft.value + (moveEvent.clientX - startX.value);
const newTop = startTop.value + (moveEvent.clientY - startY.value);
console.log(newLeft);
console.log(newTop);
// containerStyle.zIndex = '3';
//* 使用 requestAnimationFrame 来优化性能
requestAnimationFrame(() => {
//* 检测吸附
const snapResult = props.detectSnap(
props.id,
newLeft,
newTop,
parseInt(containerStyle.width),
parseInt(containerStyle.height),
props.snapDistance,
);
let finalLeft = snapResult.left;
let finalTop = snapResult.top;
isSnap.value = snapResult.isSnap;
console.log('finalLeft', finalLeft);
console.log('finalTop', finalTop);
const colliedInfo = props.detectCollision(
props.id,
finalLeft,
finalTop,
parseInt(containerStyle.width),
parseInt(containerStyle.height),
);
isCollied.value = colliedInfo.value;
updatePosition(finalLeft, finalTop, isSnap.value);
emit('drag', props.id, parseInt(containerStyle.left), parseInt(containerStyle.top));
//* 检测边界和碰撞
//todo
//* if (!colliedInfo.value) {
//* updatePosition(finalLeft, finalTop, isSnap);
//* } else {
//* const colliedComponent = colliedInfo.colliedComponent;
//* const collidedDirection = colliedInfo.collidedDirection;
//* if (colliedComponent && collidedDirection) {
//* //* 左右两边碰撞,则只移动垂直方向
//* if (
//* collidedDirection === 'left' ||
//* (collidedDirection === 'right' &&
//* (parseInt(containerStyle.height) + finalTop >= colliedComponent.y ||
//* finalTop <= colliedComponent.y + parseInt(colliedComponent.height)))
//* ) {
//* containerStyle.top = `${finalTop}px`;
//* }
//* //* 上下两边碰撞,则只移动水平方向
//* if (
//* collidedDirection === 'top' ||
//* (collidedDirection === 'bottom' &&
//* (parseInt(containerStyle.width) + finalLeft >= colliedComponent.x ||
//* finalLeft <= colliedComponent.x + parseInt(colliedComponent.width)))
//* ) {
//* containerStyle.left = `${finalLeft}px`;
//* }
//* }
//* }
});
};
/**
* 组件resize事件
* @param dir
* @param event
*/
const onResizeHandleMouseDown = (dir: string, event: MouseEvent) => {
const startX = event.clientX;
const startY = event.clientY;
const startWidth = parseInt(containerStyle.width);
const startHeight = parseInt(containerStyle.height);
const startLeft = parseInt(containerStyle.left);
const startTop = parseInt(containerStyle.top);
const onMouseMove = (moveEvent: MouseEvent) => {
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
if (dir.includes('right')) {
newWidth = startWidth + (moveEvent.clientX - startX);
} else if (dir.includes('left')) {
newWidth = startWidth - (moveEvent.clientX - startX);
newLeft = startLeft + (moveEvent.clientX - startX);
}
if (dir.includes('bottom')) {
newHeight = startHeight + (moveEvent.clientY - startY);
} else if (dir.includes('top')) {
newHeight = startHeight - (moveEvent.clientY - startY);
newTop = startTop + (moveEvent.clientY - startY);
}
//* 使用 requestAnimationFrame 来优化性能
requestAnimationFrame(() => {
//* 检测吸附
const snapResult = props.detectSnap(props.id, newLeft, newTop, newWidth, newHeight, props.snapDistance);
let finalWidth = snapResult.width;
let finalHeight = snapResult.height;
let finalLeft = snapResult.left;
let finalTop = snapResult.top;
isSnap.value = snapResult.isSnap;
updateSize(finalWidth, finalHeight);
updatePosition(finalLeft, finalTop, isSnap.value);
return;
const colliedInfo = props.detectCollision(props.id, finalLeft, finalTop, finalWidth, finalHeight);
//* 检测碰撞
if (!colliedInfo.value) {
updateSize(finalWidth, finalHeight);
updatePosition(finalLeft, finalTop, isSnap.value);
} else {
//todo 当元素的某一边已经吸附且resize的方向并非和吸附的方向一致则仍可以进行updateSize
const colliedComponent = colliedInfo.colliedComponent;
const collidedDirection = colliedInfo.collidedDirection;
if (colliedComponent && collidedDirection) {
//* 左右两边碰撞,则只移动垂直方向
if (
collidedDirection === 'left' ||
(collidedDirection === 'right' &&
(parseInt(containerStyle.height) + finalTop >= colliedComponent.y ||
finalTop <= colliedComponent.y + parseInt(colliedComponent.height)))
) {
containerStyle.top = `${finalTop}px`;
}
//* 上下两边碰撞,则只移动水平方向
if (
collidedDirection === 'top' ||
(collidedDirection === 'bottom' &&
(parseInt(containerStyle.width) + finalLeft >= colliedComponent.x ||
finalLeft <= colliedComponent.x + parseInt(colliedComponent.width)))
) {
containerStyle.left = `${finalLeft}px`;
}
}
}
});
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
emit('resize', props.id, parseInt(containerStyle.width), parseInt(containerStyle.height));
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
//* 使用 ResizeObserver 来监听容器大小变化
const resizeObserver = new ResizeObserver((entries) => {
console.log(`entries, ${entries}`);
const entry = entries[0];
const { width, height } = entry.contentRect;
updateSize(width, height);
});
//* 在容器元素存在时,才监听其大小变化
if (container.value) {
resizeObserver.observe(container.value);
}
</script>
<style lang="scss" scoped>
.draggable-resizable {
position: absolute;
background-color: lightblue;
border: 1px solid #333;
box-sizing: border-box;
.header {
width: 100%;
background-color: #333;
height: 40px;
border-bottom: 1px solid #333;
color: #fff;
}
}
.resize-handle {
position: absolute;
width: 10px;
height: 10px;
background-color: #333;
z-index: 1;
}
.resize-handle.top {
top: -5px;
left: 50%;
transform: translateX(-50%);
cursor: n-resize;
}
.resize-handle.bottom {
bottom: -5px;
left: 50%;
transform: translateX(-50%);
cursor: s-resize;
}
.resize-handle.left {
left: -5px;
top: 50%;
transform: translateY(-50%);
cursor: w-resize;
}
.resize-handle.right {
right: -5px;
top: 50%;
transform: translateY(-50%);
cursor: e-resize;
}
.resize-handle.top-left {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.resize-handle.top-right {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.resize-handle.bottom-left {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
.resize-handle.bottom-right {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
</style>