手撸一个简易 Grid 拖拽布局

公众号:程序员白特,欢迎一起交流学习~

以下文章来源于稀土掘金技术社区 ,作者游仙好梦

最近有个需求需要实现自定义首页布局,需要将屏幕按照 6 列 4 行进行等分成多个格子,然后将组件可拖拽对应格子进行渲染展示。

对比一些已有的插件,发现想要实现产品的交互效果,没有现成可用的。本身功能并不是太过复杂,于是决定自己基于 vue 手撸一个简易的 Grid 拖拽布局。

概况

需要实现 Grid 拖拽布局,主要了解这两个东西就行

  • 拖放 API,关于拖放 API 介绍文章有很多 ,可以直接看 MDN 里拖放 API介绍,可以说很详细了。
  • Grid 布局, Grid 布局与 Flex 布局很相似,但是 Grid 像是二维布局,Flex 则为一维布局,Grid 布局远比 Flex 布局强大。MDN 关于网格布局介绍

需要实现主要包含:

  • 组件物料栏拖拽到布局容器
  • 布局容器 Grid 布局
  • 放置时是否重叠判断
  • 拖拽时样式
  • 放置后样式
  • 容器内二次拖拽

拖放操作实现

拖拽中主要使用到的事件如下

  • 被拖拽元素事件:
事件触发时刻
dragstart当用户开始拖拽一个元素或选中的文本时触发。
drag当拖拽元素或选中的文本时触发。
dragend当拖拽操作结束时触发
  • 放置容器事件:
事件触发时刻
dragenter当拖拽元素或选中的文本到一个可释放目标时触发。
dragleave当拖拽元素或选中的文本离开一个可释放目标时触发。
dragover当元素或选中的文本被拖到一个可释放目标上时触发。
drop当元素或选中的文本在可释放目标上被释放时触发。

可拖拽元素

让一个元素能够拖拽只需要给元素设置 「draggable=“true”」 即可拖拽,拖拽事件 API 提供了 「DataTransfer」 对象,可以用于设置拖拽数据信息,但是仅仅只能 「drop」 事件中获取到,但是我们需要在拖拽中就需要获取到拖拽信息,用来显示拖拽时样式,所以需要我们自己存储起来,以便读取。

需要处理主要是,在拖拽时将 将当前元素信息设置到 「dragStore」 中,结束时清空当前信息

<script setup lang="ts">  
  import { dragStore } from "./drag";  
  
  const props = defineProps<{  
    data: DragItem;  
    groupName?: string;  
  }>();  
  
  const onDragstart = (e) => dragStore.set(props.groupName, { ...props.data });  
  const onDragend = () => dragStore.remove(props.groupName);  
</script>  
<template>  
  <div class="drag-item__el" draggable="true" @dragstart="onDragstart" @dragend="onDragend"></div>  
</template>  

封装一个存储方法,然后通过配置相同 key ,可以在同时存在多个放置区域时候,区分开来。

class DragStore<T extends DragItemData> {  
  moveItem = new Map<string, DragItemData>();  
  
  set(key: string, data: T) {  
    this.moveItem.set(key, data);  
  }  
  
  remove(key: string) {  
    this.moveItem.delete(key);  
  }  
  
  get(key: string): undefined | DragItemData {  
    return this.moveItem.get(key);  
  }  
}  

可放置区域

首先时需要告诉浏览器当前区域是可以放置的,只需要在元素监听 「dragenter」「dragleave」「dragover」 事件即可,然后通过 「preventDefault」 来阻止浏览器默认行为。可以在这三个事件中处理判断当前位置是否可以放置等等。

示例:

<script setup lang="ts">  
  // 进入放置目标  
  const onDragenter = (e) => {  
    e.preventDefault();  
  };  
  
  // 在目标中移动  
  const onDragover = (e) => {  
    e.preventDefault();  
  };  
  
  // 离开目标  
  const onDragleave = (e) => {  
    e.preventDefault();  
  };  
</script>  
<template>  
  <div @dragenter="onDragenter($event)" @dragover="onDragover($event)" @dragleave="onDragleave($event)" @drop="onDrop($event)"></div>  
</template>  

上面的代码已经可以让,元素可以拖拽,然后当元素拖到可防止区域时候,可以看到鼠标样式会变为可放置样式了。

Grid 布局

我们是需要进行 Grid 拖拽布局,所以先对上面放置容器进行改造,首先就是需要将容器进行格子划分区域显示。

计算 Grid 格子大小

我这里直接使用了 「@vueuse/core」「useElementSize」 的 hooks 去获取容器元素大小变动,也可以自己通过 「ResizeObserver」 去监听元素变动,然后根据设置列数、行数、间隔去计算单个格子大小。

import { useElementSize } from "@vueuse/core";  
  
/**  
 * 容器等分尺寸  
 * @param {*} target 容器 HTML  
 * @param {*} column 列数  
 * @param {*} row 行数  
 * @param {*} gap 间隔  
 * @returns  
 */  
export const useBoxSize = (target: Ref<HTMLElement | undefined>, column: number, row: number, gap: number) => {  
  const { width, height } = useElementSize(target);  
  return computed(() => ({  
    width: (width.value - (column - 1) * gap) / column,  
    height: (height.value - (row - 1) * gap) / row,  
  }));  
};  

设置 Grid 样式

根据列数和行数循环生成格子数,「rowCount」、**「columnCount」**为行数和列数。

<div class="drop-content__drop-container" @dragenter="onDragenter($event)" @dragover="onDragover($event)" @dragleave="onDragleave($event)" @drop="onDrop($event)">  
  <template v-for="x in rowCount">  
    <div class="bg-column" v-for="y in columnCount" :key="`${x}-${y}`"></div>  
  </template>  
</div>  

设置 Grid 样式,下面变量中 「gap」 为格子间隔,「repeat」 是 Grid 用来重复设置相同值的,「grid-template-columns: repeat(2,100px)」 等效于 「grid-template-columns: 100px 100px」。因为我们只需在容器里监听拖拽放置事件,所以我们还需要将 所有的 「bg-column」 事件去掉,设置 「pointer-events: none」 即可。

.drop-content__drop-container {  
  display: grid;  
  row-gap: v-bind("gap+'px'");  
  column-gap: v-bind("gap+'px'");  
  grid-template-columns: repeat(v-bind("columnCount"), v-bind("boxSize.width+'px'"));  
  grid-template-rows: repeat(v-bind("rowCount"), v-bind("boxSize.height+'px'"));  
  .bg-column {  
    background-color: #fff;  
    border-radius: 6px;  
    pointer-events: none;  
  }  
}  

效果如下:

放置元素

放置元素时我们需要先计算出元素在 Grid 位置信息等,这样才知道元素应该放置那哪个地方。

拖拽位置计算

当元素拖拽进容器中时,我们可以通过 「offsetX」、**「offsetY」**两个数据获取当前鼠标距离容器左上角位置距离,我们可以根据这两个值计算出对应的在 Grid 中做坐标。

计算方式:

// 计算 x 坐标  
const getX = (num) => parseInt(num / (boxSizeWidth + gap));  
// 计算 y 坐标  
const getY = (num) => parseInt(num / (boxSizeHeight + gap));  

需要注意的是上面计算坐标是 0,0 开始的,而 Grid 是 1,1 开始的。

获取拖拽信息

我们在进入容器时,通过上面封装 「dragData」 来获取当前拖拽元素信息,获取它尺寸信息等等。

// 拖拽中的元素  
const current = reactive({  
  show: <boolean>false,  
  id: <undefined | number>undefined,  
  column: <number>0, // 宽  
  row: <number>0, // 高  
  x: <number>0, // 列  
  y: <number>0, // 行  
});  
  
// 进入放置目标  
const onDragenter = (e) => {  
  e.preventDefault();  
  const dragData = dragStore.get(props.groupName);  
  if (dragData) {  
    current.column = dragData.column;  
    current.row = dragData.row;  
    current.x = getX(e.offsetX);  
    current.y = getY(e.offsetY);  
    current.show = true;  
  }  
};  
  
// 在目标中移动  
const onDragover = (e) => {  
  e.preventDefault();  
  const dragData = dragStore.get(props.groupName);  
  if (dragData) {  
    current.x = getX(e.offsetX);  
    current.y = getY(e.offsetY);  
  }  
};  
  
const onDragleave = (e) => {  
  e.preventDefault();  
  current.show = false;  
  current.id = undefined;  
};  

在 drop 事件中,我们将当前拖拽元素存放起来,list 会存放每一次拖拽进来元素信息。

const list = ref([]);  
  
// 放置在目标上  
const onDrop = async (e) => {  
  e.preventDefault();  
  current.show = false;  
  const item = dragStore.get(props.groupName);  
  
  list.value.push({  
    ...item,  
    x: current.x,  
    y: current.y,  
    id: new Date().getTime(),  
  });  
};  

计算碰撞

在上面还需要计算当前拖拽的位置是否可以放置,需要处理是否包含在容器内,是否与其他已放置元素存在重叠等等。

计算是否在容器内

这个是比较好计算的,只需要当前拖拽位置左上角坐标 >= 容器左上角的坐标,然后右下角的坐标 <= 容器的右下角的坐标,就是在容器内的。

代码实现:

/**  
 * 判断是否在当前四边形内  
 * @param {*} p1 父容器  
 * @param {*} p2  
 *  对应是 左上角坐标 和 右下角坐标  
 *  [0,0,1,1]  => 左上角坐标 0,0  右下角 1,1  
 */  
export const booleanWithin = (p1: [number, number, number, number], p2: [number, number, number, number]) => {  
  return p1[0] <= p2[0] && p1[1] <= p2[1] && p1[2] >= p2[2] && p1[3] >= p2[3];  
};  
计算是否与现有的相交

两个矩形相交情况有很多种,计算比较麻烦,但是我们可以计算他们不相交,然后在取反方式判断是否相交。

不相交情况只有四种,假设有 p1、p2 连个矩形,它们不相交的情况只有四种:

  • p1 在 p2 左边
  • p1 在 p2 右边
  • p1 在 p2 上边
  • p1 在 p2 下边

代码实现:

/**  
 * 判断是两四边形是否相交  
 * @param {*} p1 父容器  
 * @param {*} p2  
 *  对应是 左上角坐标 和 右下角坐标  
 *  [0,0,1,1]  => 左上角坐标 0,0  右下角 1,1  
 */  
export const booleanIntersects = (p1: [number, number, number, number], p2: [number, number, number, number]) => {  
  return !(p1[2] <= p2[0] || p2[2] <= p1[0] || p1[3] <= p2[1] || p2[3] <= p1[1]);  
};  

在放置前判断

可以通过计算属性去计算,在后面拖拽中处理样式也可以用到。修改 「drop」 中方法,然后在 「drop」 中根据 「isPutDown」 是否有效。

// 是否可以放置  
const isPutDown = computed(() => {  
  const currentXy = [current.x, current.y, current.x + current.column, current.y + current.row];  
  return (  
    booleanWithin([0, 0, columnCount.value, rowCount.value], currentXy) && //  
    list.value.every((item) => item.id === current.id || !booleanIntersects([item.x, item.y, item.x + item.column, item.y + item.row], currentXy))  
  );  
});  

拖拽时样式

上处理了基本拖放数据处理逻辑,为了更好的交互,我们可以在拖拽中显示元素预占位信息,更加直观的显示元素占位大小,类似这样:

我们可以根据上面 「current」 中信息去计算大小信息,还可以根据 「isPutDown」 去判断当前位置是否可以放置,用来显示不同交互效果。

可以直接通过 Grid 的 grid-area 属性,快速计算出放置位置信息,应为我们上面计算的 x 、y 是从 0 开始的,所以这里需要 +1。

grid-area: `${y + 1} / ${x + 1} / ${y + row + 1}/ ${ x + column + 1 }`  

预览容器

在元素放置后,我们还需要根据 list 中数据,生成元素占位样式处理,我们可以拖拽容器上层在放置一个容器,专门用来显示放置后的样式,也是可以直接使用 Grid 布局去处理。

预览样式

样式基本上和 「drop-container」 样式抱持一致即可,需要注意的时需要为预览容器设置 「pointer-events: none」,避免遮挡了 「drop-container」 事件监听。

.drop-content__preview,  
.drop-content__drop-container {  
  // ...  
}  

每个元素位置信息计算方式,基本和拖拽时样式计算方式一致,直接通过 「grid-area」 去布局就可以了。

grid-area: `${y + 1} / ${x + 1} / ${y + row + 1}/ ${ x + column + 1 }`  

二次拖拽

当元素拖拽进来后,我们还需要对放置的元素支持继续拖拽。因为上面我们将预览事件通过 「pointer-events」 去除了,所以我们需要给每个子元素都加上去。然后给子元素添加 「draggable=true」,然后处理拖拽事件,基本上和上面处理方式一样,在 「dragstart」「dragend」 处理拖拽元素信息。

然后我们还需在 「onDrop」 进行一番修改,如果是二次拖拽时只需要修改坐标信息,修改原 「onDrop」 处理方式:

if (item.id) {  
  item.x = current.x;  
  item.y = current.y;  
} else {  
  list.value.push({  
    ...item,  
    x: current.x,  
    y: current.y,  
    id: new Date().getTime(),  
  });  
}  

位置偏移优化

当你对元素二次拖拽时,会发现元素会存在偏移问。比如你放置了一个 1x2 元素后,当你从下面拖拽,你会发现拖拽中的占位样式和你拖拽元素位置存在偏差。

效果如下图

出现这情况应为上面我们时根据鼠标位置为左上角进行计算的,所以会存在这种偏差问题,我们可在拖拽前计算出偏移量来校正位置。

我们可以在二次拖拽时,获取到鼠标在当前元素内位置信息

const onDragstart = (e) => {  
  const data = props.data;  
  data.offsetX = e.offsetX;  
  data.offsetY = e.offsetY;  
  dragStore.set(props.groupName, data);  
};  

「drop-container」 内计算 x、y 值时候减去偏移量,对 「onDragenter」「onDragover」 进行如下调整修改

current.x = getX(e.offsetX) - getX(dragData?.offsetX ?? 0);  
current.y = getY(e.offsetY) - getY(dragData?.offsetY ?? 0);  

拖拽元素优化

因为上面我们将预览元素添加了 「pointer-events: all」,所以在我们拖拽到现有元素上时,会挡住 「drop-container」 事件的触发,在二次拖拽时,比如将一个 2x2 元素我们需要往下移动一格时,会发现也会被自己挡住。

  • 预览元素遮挡问题,可以在拖拽时将其他元素都设置为 「none」,二次拖拽时要做自己设置为 「all」 否则会无法拖拽
:style="{ pointerEvents: current.show && item.id !== current.id ? 'none' : 'all' }"`  
  • 二次拖拽时自己位置遮挡问题 我们可以在拖拽时增加标识,将自己通过 「transform」 移除到多拽容器外去
moveing.value  
  ? {  
      opacity: 0,  
      transform: `translate(-999999999px, -9999999999px)`,  
    }  
  : {};  

结语

到目前为止基本上的 Grid 拖拽布局大致实现了,已经满足基本业务需求了,当然有需要朋友还可以在上面增加支持拖拉调整大小、碰撞后自动调整位置等等。

完整源码在此,在线体验:

https://stackblitz.com/edit/vitejs-vite-rkwugn?file=README.md

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/510334.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java基础之流程控制语句(循环)

文章目录 Java基础之流程控制语句(循环)1.顺序结构2.分支结构if语句的第一种格式if语句的第二种格式if语句的第三种格式Switch语句格式Switch的其他知识点default的位置和省略case穿透Switch的新特性 3.循环结构循环的分类for 循环while 循环for循环 与 while循环 的对比 4.do.…

Java毕业设计 基于SSM学生选课系统

Java毕业设计 基于SSM学生选课系统 SSM jsp 学生选课系统 功能介绍 学生&#xff1a;登录 注册 全部课程 未选课程 已选课程 添加课程 删除课程 推荐课程 搜索 个人资料 修改信息 修改密码 我的选课 管理员&#xff1a; 登录 注册 课程管理 添加修改删除课程 课程查看 查看选…

新华三交换机配置开启Web图形界面

新华三交换机配置开启Web图形界面 1. 交换机桥接到电脑&#xff0c;电脑配置网络ip 2. 交换机配置ip&#xff0c;可ping通。 <H3c>sys System View: return to User View with CtrlZ.[H3c]inter vlan 1[H3c-Vlan-interface1]ip add 192.168.3.20 24[H3c-Vlan-interface1…

LLMs 可能在 2 年内彻底改变金融行业

在艾伦图灵研究所&#xff08;The Alan Turing Institute&#xff09;最新的一项研究中&#xff0c;我们看到了大型语言模型&#xff08;Large Language Models&#xff0c;LLMs&#xff09;的一种可能性。它有望通过检测欺诈行为、生成财务洞察以及自动化客户服务&#xff0c;…

android 使用ollvm混淆so

使用到的工具 ndk 21.4.7075529&#xff08;android studio上下载的&#xff09;cmake 3.10.2.4988404&#xff08;android studio上下载的&#xff09;llvm-9.0.1llvm-mingw-20230130-msvcrt-x86_64.zipPython 3.11.5 环境配置 添加cmake mingw环境变量如下图: 编译 下载…

在Windows中查找电脑序列号的几种方法,看看有没有适合你的

序言 Windows不会在其界面的任何位置显示PC的序列号,流行的系统信息工具也不会。但你通常可以通过简单的命令、在BIOS中查看或在硬件本身上查找PC的序列号。 运行WMIC命令 打开命令提示符或PowerShell窗口以开始。在Windows 11、Windows 10或Windows 8上,右键单击开始按钮…

蓝桥杯刷题第六天(昨天忘记发了)

今天想从不一样的角度来解题&#xff1a;从时间紧张暴力求解到思路阔达直接通过所有案例 暴力方法&#xff1a; 思路第一眼看到这个问题我就想到了第一个思路就是先用两个数组一个存石子数一个存颜色状态&#xff0c;每次遍历一遍看看有没有相邻石子颜色一样且为和最小的。 im…

64B66B编码详解

文章目录 前言一、64B66B的实现方式二、64B66B数据帧格式 前言 参考&#xff1a;https://blog.chinaaet.com/justlxy/p/5100064750 64B66B是我们在高速通信协议中的一种处理PHY层发送数据的编码。为何我们要编码&#xff0c;直接把原数据发送出去不行吗&#xff1f;答案是当然…

opengl草稿复习,承上启下(一)

目录 1、链接文件夹中的cpp 2、链接资源到输出目录 3、多编译目标 4、cmakelist添加库 4、添加glfw和glad 5、glfw运行 6、NDC、VBO、VAO 7、渐变三角形 8、渲染两个三角形 9、渲染两个三角形&#xff0c;同时基于原来颜色进行渐变 10、三角形渲染模块化 11、纹理渲…

26版SPSS操作教程(初级第十四章)

前言 #由于导师最近布置了学习SPSS这款软件的任务&#xff0c;因此想来平台和大家一起交流下学习经验&#xff0c;这期推送内容接上一次第十三章的学习笔记&#xff0c;希望能得到一些指正和帮助~ 粉丝及官方意见说明 #针对官方爸爸的意见说的推送缺乏操作过程的数据案例文件…

GT收发器PHY层设计(2)GT_module模块设计

文章目录 前言一、设计框图二、例化IP核端口三、common_reset_i模块四、gt_usrclk_source模块五、IBUFDS_GTE2和gtwizard_0_common模块六、顶层模块gt_module总结 前言 根据官方的example design设计一个自定义协议的高速PHY设计 一、设计框图 设计思路及代码思路参考FPGA奇哥…

mysql 数据库基本操作

mysql 数据库基本操作 1、创建五张表 – user 表&#xff1a;后台用户表 – product 表&#xff1a;产品表 – account 表&#xff1a;客户账户表 – product_account 表 : 客户购买表 – customer 表 &#xff1a; 客户表 2、创建表 SQL 语句&#xff1a; 注意&#xff1a…

简单而复杂的Python

Python是一种简单&复杂的编程语言。简单的时候可以到极致&#xff1a; print(hello world!)另一方面&#xff0c;Python 也具有许多复杂的语法特性&#xff0c;例如面向对象编程、装饰器、迭代器、生成器等等。这些特性使得 Python 适用于各种不同的编程任务和项目。 当我…

ROS中的栅格地图

目录 一、ROS中栅格地图格式 二、C节点发布地图 2.1 发布者发布地图的C实现 三、python节点发布地图 一、ROS中栅格地图格式 机器人导航所使用的地图数据&#xff0c;就是ROS导航软件包里的map_server 节点&#xff0c;在话题/map中发布的消息数据&#xff0c;消息类型是na…

智能锁也能用上GPT技术了?大扭力电机更配中国门?这家公司再次引领行业

智能锁也能用上GPT技术了&#xff1f;小小智能锁电机&#xff0c;竟然能拉动2.5吨SUV&#xff1f; 今日&#xff0c;中国智能锁领军品牌德施曼在北京举办「2024德施曼创新技术预沟通会」&#xff0c;德施曼技术研发中心总监桑胜伟揭秘了两项行业突破性技术——GPTfinger及龙霆…

vulnhub pWnOS v2.0通关

知识点总结&#xff1a; 1.通过模块来寻找漏洞 2.msf查找漏洞 3.通过网站源代码&#xff0c;查看模块信息 环境准备 攻击机&#xff1a;kali2023 靶机&#xff1a;pWnOS v2.0 安装地址&#xff1a;pWnOS: 2.0 (Pre-Release) ~ VulnHub 在安装网址中看到&#xff0c;该靶…

Deferred library xxx was not loaded

Deferred 延迟修饰词作用下的文件库尚未完成载入

Mysql的MHA

目录 一、MHA概念 1.1 MHA 的组成 1.2 MAH特性 1.3 MHA原理 1.4当主挂了以后会给哪个从服务器&#xff1f; 二、搭建MysqlMHA 2.1实验思路 2.2实验准备 2.3服务搭建 1. 关闭防火墙和selinux 2、修改 master&#xff08;192.168.91.103&#xff09;、Slave1&#xff0…

vue3鼠标向下滑动,导航条改变背景颜色和logo的封装

代码中使用了element-plus组件&#xff0c;需先安装 向下滑动前 向下滑动后&#xff08;改变了logo 字体 背景颜色&#xff09; <script lang"ts" setup> import router from /router; import { ArrowDown } from element-plus/icons-vue import { ref, …

课程15 光在晶体中的传播(三)(视频P53-55)

旋光与菲涅尔棱镜 旋光现象 说明从石英晶片出射的&#xff0c;依然是线偏振光&#xff0c;其偏振面却发生了旋转。 左旋石英与右旋石英 ![左旋石英与右旋石英![](https://img-blog.csdnimg.cn/direct/6ead0356c3c740169ba6e723863b743b.png) 旋光异构体 右旋石英表现出φR &a…