Skip to content
Snippets Groups Projects
DragGrid.vue 4.61 KiB
Newer Older
<template>
Julian's avatar
Julian committed
  <div
    @dragover.prevent="handleDragOver"
    @drop.prevent="handleDrop"
    class="grid"
  >
    <slot name="highlight">
      <div class="highlight" ref="highlight"></div>
    </slot>
Julian's avatar
Julian committed
    <DragContainer
      v-for="item in value"
Julian's avatar
Julian committed
      :key="item.key"
Julian's avatar
Julian committed
      :drag-i-d="item.key"
Julian's avatar
Julian committed
      :x="item.x"
      :y="item.y"
      :w="item.w"
      :h="item.h"
Julian's avatar
Julian committed
      :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>
Julian's avatar
Julian committed
    </DragContainer>
    <slot></slot>
  </div>
</template>

<script>
Julian's avatar
Julian committed
import DragContainer from "./DragContainer.vue";
import { v4 as uuidv4 } from "uuid";
export default {
Julian's avatar
Julian committed
  name: "DragGrid",
Julian's avatar
Julian committed
  components: {
    DragContainer,
  },
  emits: ["input"],
  props: {
    rows: {
      type: Number,
Julian's avatar
Julian committed
      required: true,
    },
    cols: {
      type: Number,
Julian's avatar
Julian committed
      required: true,
    },
    posValidation: {
      type: Function,
      required: false,
    },
Julian's avatar
Julian committed
    validateElement: {
      type: Function,
      required: false,
    },
    value: {
      type: Array,
      required: true,
    },
    context: {
      type: String,
      required: false,
      default: uuidv4,
    },
  },
  methods: {
    positionAllowed(x, y, key) {
Julian's avatar
Julian committed
      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;
    },
Julian's avatar
Julian committed
    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;
        }
Julian's avatar
Julian committed
        if (!newPositionValid) break;
      }

      if (!newPositionValid) {
        this.$refs.highlight.style.display = "none";
        return;
      }

Julian's avatar
Julian committed
      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) {
Julian's avatar
Julian committed
      this.$refs.highlight.style.display = "none";
Julian's avatar
Julian committed
      let data = event.dataTransfer.getData("vueDrag/gridItem");
      let element = JSON.parse(data);
Julian's avatar
Julian committed

      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;
        }
Julian's avatar
Julian committed
        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);
Julian's avatar
Julian committed
      element.x = coords.x;
      element.y = coords.y;
      valueCopy.push(element);
      this.$emit("input", valueCopy);
Julian's avatar
Julian committed
    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>
  background: darkgrey;
  border: grey dashed 2px;
Julian's avatar
Julian committed
  display: none;
  transition: all 2s ease-in-out;
  z-index: -1;
  pointer-events: none;
  user-select: none;
Julian's avatar
Julian committed

.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;
}
Julian's avatar
Julian committed
</style>