<template> <div @dragover.prevent="handleDragOver" @drop.prevent="handleDrop" class="grid" > <slot name="highlight"> <div class="highlight" ref="highlight"></div> </slot> <DragContainer v-for="item in value" :key="item.key" :drag-i-d="item.key" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :data="item.data" :context="context" > <slot v-bind="item" name="item"> <dl> <dt>Key</dt> <dd>{{ item.key }}</dd> <dt>Position</dt> <dd>{{ item.x }}, {{ item.y }}</dd> <dt>Size</dt> <dd>{{ item.w }} × {{ item.h }}</dd> <dt>Data</dt> <dd>{{ item.data }}</dd> </dl> </slot> </DragContainer> <slot></slot> </div> </template> <script> import DragContainer from "./DragContainer.vue"; import { v4 as uuidv4 } from "uuid"; export default { name: "DragGrid", components: { DragContainer, }, emits: ["input"], props: { rows: { type: Number, required: true, }, cols: { type: Number, required: true, }, posValidation: { type: Function, required: false, }, validateElement: { type: Function, required: false, }, value: { type: Array, required: true, }, context: { type: String, required: false, default: uuidv4, }, }, methods: { positionAllowed(x, y, key) { if (x < 0 || y < 0) return false; if (x > this.cols) return false; if (y > this.rows) return false; for (let item of this.value) { if (key === item.key) continue; if (x >= item.x && x < item.x + item.w) { if (y >= item.y && y < item.y + item.h) { return false; } } } if (this.posValidation) return this.posValidation(x, y, key); return true; }, handleDragOver(event) { let data = event.dataTransfer.getData("vueDrag/gridItem"); let element = JSON.parse(data); let coords = this.getCoords(event.layerX, event.layerY); if (element.context !== this.context) { this.$refs.highlight.style.display = "none"; return; } let newPositionValid = true; for (let x = coords.x; x < coords.x + element.w; x++) { for (let y = coords.y; y < coords.y + element.h; y++) { newPositionValid = this.positionAllowed(x, y, element.key); if (!newPositionValid) break; } if (!newPositionValid) break; } if (!newPositionValid) { this.$refs.highlight.style.display = "none"; return; } this.$refs.highlight.style.display = "block"; this.$refs.highlight.style.gridColumnStart = coords.x + ""; this.$refs.highlight.style.gridRowStart = coords.y + ""; this.$refs.highlight.style.gridColumnEnd = "span " + element.w; this.$refs.highlight.style.gridRowEnd = "span " + element.h; }, handleDrop(event) { this.$refs.highlight.style.display = "none"; let data = event.dataTransfer.getData("vueDrag/gridItem"); let element = JSON.parse(data); if (this.validateElement) this.validateElement(element); if (element.context !== this.context) { return; } let coords = this.getCoords(event.layerX, event.layerY); let newPositionValid = true; for (let x = coords.x; x < coords.x + element.w; x++) { for (let y = coords.y; y < coords.y + element.h; y++) { newPositionValid = this.positionAllowed(x, y, element.key); if (!newPositionValid) break; } if (!newPositionValid) break; } if (!newPositionValid) return; let valueCopy = structuredClone(this.value); let index = valueCopy.findIndex((i) => { return i.key === element.key; }); if (index >= 0) valueCopy.splice(index, 1); element.x = coords.x; element.y = coords.y; valueCopy.push(element); this.$emit("input", valueCopy); }, getCoords(x, y) { return { x: Math.ceil(x / (this.$el.offsetWidth / this.cols)), y: Math.ceil(y / (this.$el.offsetHeight / this.rows)), }; }, }, }; </script> <style scoped> .highlight { background: darkgrey; border: grey dashed 2px; display: none; transition: all 2s ease-in-out; z-index: -1; pointer-events: none; user-select: none; } .grid { display: grid; grid-template-columns: repeat(v-bind(cols), 1fr); grid-template-rows: repeat(v-bind(rows), 1fr); width: 100%; height: 100%; min-width: 100px; min-height: 100px; gap: 1em; } </style>