营销网站的优势是什么意思,海南的网站建设公司,做的好的电商网站,网站设计十大品牌公众号#xff1a;程序员白特#xff0c;欢迎一起交流学习~ 以下文章来源于稀土掘金技术社区 #xff0c;作者游仙好梦 最近有个需求需要实现自定义首页布局#xff0c;需要将屏幕按照 6 列 4 行进行等分成多个格子#xff0c;然后将组件可拖拽对应格子进行渲染展示。 对比… 公众号程序员白特欢迎一起交流学习~ 以下文章来源于稀土掘金技术社区 作者游仙好梦 最近有个需求需要实现自定义首页布局需要将屏幕按照 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 langts 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 classdrag-item__el draggabletrue dragstartonDragstart dragendonDragend/div
/template 封装一个存储方法然后通过配置相同 key 可以在同时存在多个放置区域时候区分开来。
class DragStoreT extends DragItemData { moveItem new Mapstring, 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 langts // 进入放置目标 const onDragenter (e) { e.preventDefault(); }; // 在目标中移动 const onDragover (e) { e.preventDefault(); }; // 离开目标 const onDragleave (e) { e.preventDefault(); };
/script
template div dragenteronDragenter($event) dragoveronDragover($event) dragleaveonDragleave($event) droponDrop($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: RefHTMLElement | 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 classdrop-content__drop-container dragenteronDragenter($event) dragoveronDragover($event) dragleaveonDragleave($event) droponDrop($event) template v-forx in rowCount div classbg-column v-fory 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(gappx); column-gap: v-bind(gappx); grid-template-columns: repeat(v-bind(columnCount), v-bind(boxSize.widthpx)); grid-template-rows: repeat(v-bind(rowCount), v-bind(boxSize.heightpx)); .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: booleanfalse, id: undefined | numberundefined, column: number0, // 宽 row: number0, // 高 x: number0, // 列 y: number0, // 行
}); // 进入放置目标
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」 去除了所以我们需要给每个子元素都加上去。然后给子元素添加 「draggabletrue」然后处理拖拽事件基本上和上面处理方式一样在 「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?fileREADME.md