稀土掘金技术社区 01月28日
轻松搞定拖拽缩放、移动,不怕领导叫我写拖拽!!
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文详细介绍了如何使用原生JavaScript和CSS实现一个可拖拽和缩放的容器组件。文章核心在于通过监听鼠标事件,计算鼠标移动距离,并动态改变容器的宽高和位置。组件通过CSS绘制边角控制点,实现八个方向的缩放功能。文章提供了完整的代码示例,包括拖拽和缩放的实现逻辑,以及最小宽高限制,使得读者可以快速理解和应用到实际项目中,为前端开发者提供了一个实用的解决方案。

🖱️**核心思路**:通过CSS绘制边角控制点,监听鼠标事件,计算鼠标移动距离,动态改变容器的宽高和位置,实现拖拽和缩放功能。

📐**尺寸计算**:容器的新宽度/高度等于初始宽度/高度加上/减去鼠标移动的距离。拖拽左边和上边时,需要同时改变容器的位置,保持视觉上的连续性。

🎛️**多方向缩放**:通过`resizeTypes`定义八个方向的缩放控制点,根据鼠标点击的控制点类型,计算并更新容器的尺寸和位置。

🔒**最小尺寸限制**:设置容器的最小宽度和高度,防止容器被拖拽到过小的尺寸,确保用户体验。

🧩**组件封装**:将拖拽和缩放逻辑封装成一个可复用的Vue组件,通过props接收初始位置、大小、最小尺寸等参数,方便在项目中集成使用。

原创 前端金熊 2025-01-25 09:03 重庆

点击关注公众号,“技术干货” 及时达!

点击关注公众号,“技术干货” 及时达!

经常会碰到需要拖拽缩放的情况,只要有思路,实现起来会非常顺畅。
功能的核心是鼠标放在四个边和角上,拖拽把容器放大或缩小

功能演示

缩放:


移动:

演示网址:宝藏导航


缩放设计思路

    使用css绘制四条边和四个角,

    通过css定位,控制四根线和四个角在对应的位置

    监听鼠标点击和移动事件

    在移动的过程中,改变容器的大小


核心设计

基础html结构

<template>  <!-- 使用 v-if 判断是否插入到 body 中 -->  <!-- 创建一个容器,支持拖拽,使用 ref 引用该容器 -->  <div    ref="draggableContainer"    class="draggable-container"    @mousedown="startDrag"    :style="containerStyle"  >    <!-- 插槽,用户可以将其他内容插入到这个容器中 -->    <slot></slot>
<!-- 创建缩放控制点,每个控制点代表一个边角,使用 v-for 循环渲染 --> <span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)" ></span> </div></template>

基础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 }, // 容器的尺寸  }

核心代码和思路

容器新宽度 = 容器初始宽度 + 鼠标移动距离

通过上面公式,我们需要记录

    容器的初始宽度

    鼠标移动距离 = 鼠标新位置 - 鼠标初始距离

1. 记录容器和鼠标初始状态

// 鼠标按下,开始拖拽startResize(event) {    // 记录鼠标初始位置    this.originMouseX = event.clientX;    this.originMouseY = event.clientY;
// 记录容器初始宽高 this.originWidth = this.size.width; this.originHeight = this.size.height;},

2. 计算拖拽后新宽度

根据:容器新宽度 = 容器初始宽度 + 鼠标移动距离

拖拽容器右边:

// 计算鼠标的移动距离 deltaXconst deltaX = event.clientX - this.originMouseX; // 容器新宽度 = 初始宽度 + 鼠标移动距离newWidth = this.originWidth + deltaX; 

拖拽容器右下角:
当我们拖拽容器右下角,容器的宽和高都会改变。我们需要把这个拆分成两个步骤来解决。

// 获取新宽度const deltaX = event.clientX - this.originMouseX; newWidth = this.originWidth + deltaX;
// 获取新高度const deltaY = event.clientY - this.originMouseY; // 计算鼠标的纵向位移newHeight = this.originHeight + deltaY;

拖拽左边和上边:
拖拽左边的时候,左边的定位不能始终都是在原来的位置。
假设:
    我们的初始位置是 left: 200px。左边向左拖拽50px后,需要变为left: 150px首先我们需要在开始的时候记录容器的初始位置

// 鼠标按下,开始拖拽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;}

改变宽高的同时,改变容器左上角的位置

// 改变高度const deltaX = event.clientX - this.originMouseX; newWidth = this.originWidth - deltaX;
// 改变左边的位置this.position.x = this.originContainX + deltaX;

3. 确定拖拽的是哪条边

我们在点击的时候会传递type,使用变量把type记录下。

<span  v-for="type in resizeTypes"  :key="type"  :class="`${type}-resize`"  @mousedown="startResize($event, type)"></span>
// 鼠标按下,开始拖拽startResize(event, type) {  this.resizeType = type; // 记录拖拽的边角的类型  ......}

// 开始拖拽的过程中,改变容器状态handleResize() {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; this.size.width = newWidth; this.position.x = this.originContainX + deltaX; newHeight = this.originHeight - deltaY; this.size.height = newHeight; this.position.y = this.originContainY + deltaY; break; case "t": // 上边 newHeight = this.originHeight - deltaY; this.size.height = newHeight; this.position.y = this.originContainY + deltaY; break;
右边,右下角同理......}

4.设置最小的拖拽宽和高

如果新拖拽的宽度,已经小于最小宽度。拖拽时不进行任何改动。

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;            右边边,右下角同理......}

5.添加拖拽移动

拖拽移动的详细内容,笔者写的另一篇文章:拖拽移动详细思路
下面的完整代码是结合了拖拽移动和缩放整合在一起,一个较为完整的拖拽组件


完整代码

<template>  <!-- 使用 v-if 判断是否插入到 body 中 -->  <!-- 创建一个容器,支持拖拽,使用 ref 引用该容器 -->  <div    ref="draggableContainer"    class="draggable-container"    @mousedown="startDrag"    :style="containerStyle"  >    <!-- 插槽,用户可以将其他内容插入到这个容器中 -->    <slot></slot>
<!-- 创建缩放控制点,每个控制点代表一个边角,使用 v-for 循环渲染 --> <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 }, // 容器的初始 X 位置 top: { type: Number, default: 0 }, // 容器的初始 Y 位置 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, // 鼠标初始 X 坐标 originMouseY: 0, // 鼠标初始 Y 坐标 originContainX: 0, // 容器初始 X 坐标 originContainY: 0, // 容器初始 Y 坐标 originWidth: 0, // 容器初始宽度 originHeight: 0, // 容器初始高度 resizeType: "", // 当前缩放类型 }; }, computed: { // 计算容器的样式 containerStyle() { return { top: `${this.position.y}px`, // 设置容器的 top 样式 left: `${this.position.x}px`, // 设置容器的 left 样式 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>$lineOffset: -6px;$cornerOffset: -8px;/* 拖拽容器的样式 */.draggable-container { position: fixed; /* 绝对定位 */ cursor: move; /* 鼠标移动时显示抓手指针 */ user-select: none; /* 禁止选中文本 */ background-color: #ccc; span { position: absolute; display: block; } /* 左边和右边 */ .l-resize, .r-resize { width: 8px; height: 100%; top: 0; cursor: w-resize; } .l-resize { left: $lineOffset; } .r-resize { right: $lineOffset; }
/* 上边和下边 */ .t-resize, .b-resize { width: 100%; height: 8px; left: 0; cursor: s-resize; } .t-resize { top: $lineOffset; } .b-resize { bottom: $lineOffset; } /* 四个角 */ .lt-resize, .rt-resize, .rb-resize, .lb-resize { width: 15px; height: 15px; z-index: 10; } .lt-resize, .lb-resize { left: $cornerOffset; } .lt-resize, .rt-resize { top: $cornerOffset; } .rt-resize, .rb-resize { right: $cornerOffset; } .rb-resize, .lb-resize { bottom: $cornerOffset; }
.lt-resize, .rb-resize { cursor: se-resize; } .rt-resize, .lb-resize { cursor: sw-resize; }}</style>

组件引用

<DraggableContainer  :width="400"  :height="400"  :min-height="300"  :min-width="300">  <div>能拖动我了</div></DraggableContainer>

总结

部分代码设计参考了著名第三方库vxe-modal的设计思路:vxe-modal

本文实现了拖拽移动和缩放的功能,同学们也可以根据需要往上面添加自己的改动。希望对您有所帮助!

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

拖拽 缩放 前端组件 JavaScript CSS
相关文章