Implementing a Draggable and Resizable Container Component in Vue
This article explains how to create a Vue component that supports both dragging and resizing by rendering CSS edges and corners, handling mouse events to track movement, calculating new dimensions, enforcing minimum sizes, and providing a complete template, script, and style implementation.
When a UI requires drag‑and‑resize functionality, the core idea is to place invisible handles on the four sides and four corners of a container and update its size and position based on mouse movements.
Feature Demonstration
Images illustrate the scaling and moving effects; a demo link is provided.
Design Thinking for Scaling
Use CSS to draw four borders and four corner handles.
Position the lines and corners with CSS positioning.
Listen for mouse down and move events.
During movement, adjust the container's width, height, and position.
Core Design
Basic HTML Structure
<template>
<!-- Insert into body conditionally -->
<div ref="draggableContainer" class="draggable-container" @mousedown="startDrag" :style="containerStyle">
<slot></slot>
<span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)"></span>
</div>
</template>Basic Data
data: {
resizeTypes: ["lt", "t", "rt", "r", "rb", "b", "lb", "l"],
position: { x: this.left, y: this.top },
size: { width: this.width, height: this.height },
originMouseX: 0, originMouseY: 0,
originContainX: 0, originContainY: 0,
originWidth: 0, originHeight: 0,
resizeType: ""
};Core Code and Logic
Container new width = initial width + mouse movement distance
The algorithm records the container's initial size and mouse position, computes the delta, and updates width, height, and position according to the dragged edge.
1. Record Initial States
// Mouse down, start resize
startResize(event) {
this.originMouseX = event.clientX;
this.originMouseY = event.clientY;
this.originWidth = this.size.width;
this.originHeight = this.size.height;
this.originContainX = this.position.x;
this.originContainY = this.position.y;
},2. Compute New Width for Right Edge
// deltaX = mouse move distance
const deltaX = event.clientX - this.originMouseX;
newWidth = this.originWidth + deltaX;3. Determine Which Edge Is Dragged
<span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)"></span>4. Enforce Minimum Width/Height
if (newWidth >= this.minWidth) { this.size.width = newWidth; }
if (newHeight >= this.minHeight) { this.size.height = newHeight; }5. Add Drag‑Move Logic
The full component combines drag‑move and resize logic.
Complete Code
<template>
<div ref="draggableContainer" class="draggable-container" @mousedown="startDrag" :style="containerStyle">
<slot></slot>
<span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)"></span>
</div>
</template>
<script>
export default {
props: { zIndex: {type: Number, default: 1}, left:{type:Number,default:0}, top:{type:Number,default:0}, width:{type:Number,default:300}, height:{type:Number,default:300}, minWidth:{type:Number,default:100}, minHeight:{type:Number,default:100} },
data() { return { resizeTypes:["lt","t","rt","r","rb","b","lb","l"], position:{x:this.left,y:this.top}, size:{width:this.width,height:this.height}, originMouseX:0, originMouseY:0, originContainX:0, originContainY:0, originWidth:0, originHeight:0, resizeType:"" }; },
computed:{ containerStyle(){ return { top:`${this.position.y}px`, left:`${this.position.x}px`, width:`${this.size.width}px`, height:`${this.size.height}px`, zIndex:this.zIndex }; } },
methods:{
startDrag(event){ this.originMouseX=event.clientX; this.originMouseY=event.clientY; this.originContainX=this.position.x; this.originContainY=this.position.y; document.addEventListener("mousemove",this.handleDrag); document.addEventListener("mouseup",this.stopDrag); },
handleDrag(event){ this.position.x = this.originContainX + event.clientX - this.originMouseX; this.position.y = this.originContainY + event.clientY - this.originMouseY; },
startResize(event,type){ this.resizeType=type; this.originMouseX=event.clientX; this.originMouseY=event.clientY; this.originWidth=this.size.width; this.originHeight=this.size.height; this.originContainX=this.position.x; this.originContainY=this.position.y; event.stopPropagation(); document.addEventListener("mousemove",this.handleResize); document.addEventListener("mouseup",this.stopDrag); },
handleResize(event){ const deltaX=event.clientX-this.originMouseX; const deltaY=event.clientY-this.originMouseY; let newWidth=this.originWidth; let newHeight=this.originHeight; switch(this.resizeType){ case "lt": newWidth=this.originWidth-deltaX; newHeight=this.originHeight-deltaY; if(newWidth>=this.minWidth){ this.position.x=this.originContainX+deltaX; this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.position.y=this.originContainY+deltaY; this.size.height=newHeight; } break; case "t": newHeight=this.originHeight-deltaY; if(newHeight>=this.minHeight){ this.position.y=this.originContainY+deltaY; this.size.height=newHeight; } break; case "rt": newWidth=this.originWidth+deltaX; newHeight=this.originHeight-deltaY; if(newWidth>=this.minWidth){ this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.position.y=this.originContainY+deltaY; this.size.height=newHeight; } break; case "r": newWidth=this.originWidth+deltaX; if(newWidth>=this.minWidth){ this.size.width=newWidth; } break; case "rb": newWidth=this.originWidth+deltaX; newHeight=this.originHeight+deltaY; if(newWidth>=this.minWidth){ this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.size.height=newHeight; } break; case "b": newHeight=this.originHeight+deltaY; if(newHeight>=this.minHeight){ this.size.height=newHeight; } break; case "lb": newWidth=this.originWidth-deltaX; newHeight=this.originHeight+deltaY; if(newWidth>=this.minWidth){ this.position.x=this.originContainX+deltaX; this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.size.height=newHeight; } break; case "l": newWidth=this.originWidth-deltaX; if(newWidth>=this.minWidth){ this.position.x=this.originContainX+deltaX; this.size.width=newWidth; } break; } },
stopDrag(){ document.removeEventListener("mousemove",this.handleDrag); document.removeEventListener("mousemove",this.handleResize); document.removeEventListener("mouseup",this.stopDrag); }
},
beforeDestroy(){ this.stopDrag(); }
};
</script>
<style lang="scss" scoped>
.draggable-container{ position:fixed; cursor:move; user-select:none; background:#ccc; span{position:absolute;display:block;} .l-resize,.r-resize{width:8px;height:100%;top:0;cursor:w-resize;} .l-resize{left:-6px;} .r-resize{right:-6px;} .t-resize,.b-resize{width:100%;height:8px;left:0;cursor:s-resize;} .t-resize{top:-6px;} .b-resize{bottom:-6px;} .lt-resize,.rt-resize,.rb-resize,.lb-resize{width:15px;height:15px;z-index:10;} .lt-resize,.lb-resize{left:-8px;} .lt-resize,.rt-resize{top:-8px;} .rt-resize,.rb-resize{right:-8px;} .rb-resize,.lb-resize{bottom:-8px;} .lt-resize,.rb-resize{cursor:se-resize;} .rt-resize,.lb-resize{cursor:sw-resize;} }
</style>Component Usage
<DraggableContainer :width="400" :height="400" :min-height="300" :min-width="300">
<div>能拖动我了</div>
</DraggableContainer>Conclusion
Part of the design references the popular third‑party library vxe‑modal.
The article provides a complete, functional draggable‑and‑resizable component that can be extended with custom features as needed.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.