Skip to content
Snippets Groups Projects

Resolve "Implement basic functionality"

Merged Julian requested to merge 1-implement-basic-functionality into main
1 file
+ 37
10
Compare changes
  • Side-by-side
  • Inline
src/DragGrid.vue 0 → 100644
+ 335
0
<template>
<interact
droppable
@dropmove="disabled || loading ? undefined : handleDragOver($event)"
@drop.prevent="disabled || loading ? undefined : handleDrop($event)"
@dragleave="$refs.highlightContainer.style.display = 'none'"
class="grid"
>
<template v-if="loading">
<template v-for="loader in cols * rows">
<div :key="loader">
<slot name="loader"></slot>
</div>
</template>
</template>
<template v-else>
<div class="highlight-container" ref="highlightContainer">
<slot name="highlight">
<div class="highlight"></div>
</slot>
</div>
<DragContainer
v-for="item in value"
:key="item.key"
:drag-i-d="item.key"
:x="getInt('x', item)"
:y="getInt('y', item)"
:w="getInt('w', item)"
:h="getInt('h', item)"
:data="getObject('data', item)"
:context="context"
:grid-id="gridId"
:disabled="disabled || loading || item.disabled"
>
<slot v-bind="transformItem(item)" :raw-item="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>
<template v-for="disabledField in disabledFields">
<GridItem
class="disabledField"
:x="disabledField.x"
:y="disabledField.y"
:key="disabledField.x + '|' + disabledField.y"
>
<slot name="disabledField"></slot>
</GridItem>
</template>
<slot></slot>
</template>
</interact>
</template>
<script>
import DragContainer from "./DragContainer.vue";
import { v4 as uuidv4 } from "uuid";
import GridItem from "./GridItem.vue";
export default {
name: "DragGrid",
components: {
GridItem,
DragContainer,
},
emits: ["input", "itemChanged"],
props: {
rows: {
type: Number,
required: true,
},
cols: {
type: Number,
required: true,
},
posValidation: {
type: Function,
required: false,
default: undefined,
},
validateElement: {
type: Function,
required: false,
default: undefined,
},
value: {
type: Array,
required: true,
},
context: {
type: String,
required: false,
default: uuidv4,
},
gridId: {
type: String,
required: false,
default: uuidv4,
},
noHighlight: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
disabledFields: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
positionAllowed(x, y, key) {
if (x < 0 || y < 0) return false;
if (x > this.cols) return false;
if (y > this.rows) return false;
if (
this.disabledFields.filter((field) => field.x === x && field.y === y)
.length > 0
) {
// Field is disabled
return false;
}
for (let item of this.value) {
if (key === item.key) continue;
if (
x >= this.getInt("x", item) &&
x < this.getInt("x", item) + this.getInt("w", item)
) {
if (
y >= this.getInt("y", item) &&
y < this.getInt("y", item) + this.getInt("h", item)
) {
return false;
}
}
}
if (this.posValidation) return this.posValidation(x, y, key);
return true;
},
handleDragOver(event) {
let data = event.relatedTarget.dataset.transfer;
if (!data) return;
let element = JSON.parse(data);
let coords = this.getCoords(
event.dragEvent.client.x - element.mouseX,
event.dragEvent.client.y - element.mouseY
);
if (element.context !== this.context || this.noHighlight) {
this.$refs.highlightContainer.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.highlightContainer.style.display = "none";
return;
}
this.$refs.highlightContainer.style.display = "block";
this.$refs.highlightContainer.style.gridColumnStart = coords.x + "";
this.$refs.highlightContainer.style.gridRowStart = coords.y + "";
this.$refs.highlightContainer.style.gridColumnEnd = "span " + element.w;
this.$refs.highlightContainer.style.gridRowEnd = "span " + element.h;
},
handleDrop(event) {
this.$refs.highlightContainer.style.display = "none";
let data = event.relatedTarget.dataset.transfer;
if (!data) return;
let element = JSON.parse(data);
if (this.validateElement) this.validateElement(element);
if (element.context !== this.context) {
return;
}
let coords = this.getCoords(
event.dragEvent.client.x - element.mouseX,
event.dragEvent.client.y - element.mouseY
);
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;
element.x = coords.x;
element.y = coords.y;
try {
let valueCopy = structuredClone(this.value);
let index = valueCopy.findIndex((i) => {
return i.key === element.key;
});
if (index >= 0) valueCopy.splice(index, 1);
let elementCopy = structuredClone(element);
elementCopy.context = undefined;
elementCopy.originGridId = undefined;
elementCopy.mouseX = undefined;
elementCopy.mouseY = undefined;
valueCopy.push(elementCopy);
this.$emit("input", valueCopy);
} catch (e) {
if (e.code === DOMException.DATA_CLONE_ERR) {
// We use functions for properties → we can't clone; only emit `item-changed` event
console.debug(
"Grid couldn't be cloned, please listen to the `item-changed` event and handle changes yourself."
);
} else {
throw e;
}
}
element.gridId = this.gridId;
this.$emit("itemChanged", element);
},
clamp: (min, num, max) => Math.min(Math.max(num, min), max),
getCoords(x, y) {
let rect = this.$el.getBoundingClientRect();
return {
x: this.clamp(
1,
Math.ceil((x - rect.x) / (rect.width / this.cols)),
this.cols
),
y: this.clamp(
1,
Math.ceil((y - rect.y) / (rect.height / this.rows)),
this.rows
),
};
},
getInt(property, item) {
let val = item[property] || 1;
return val instanceof Function ? val(this.gridData) : parseInt(val);
},
getObject(property, item) {
let val = item[property] || {};
return val instanceof Function ? val(this.gridData) : val;
},
transformItem(item) {
let newItem = { key: item.key };
newItem.x = this.getInt("x", item);
newItem.y = this.getInt("y", item);
newItem.w = this.getInt("w", item);
newItem.h = this.getInt("h", item);
newItem.data = this.getObject("data", item);
return newItem;
},
},
computed: {
gridData() {
return {
gridId: this.gridId,
context: this.context,
};
},
},
};
</script>
<style scoped>
.highlight {
background: darkgrey;
border: grey dashed 2px;
width: 100%;
height: 100%;
}
.highlight-container {
display: none;
transition: all 2s ease-in-out;
pointer-events: none;
user-select: none;
width: 100%;
height: 100%;
}
.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;
touch-action: none;
isolation: isolate;
}
</style>
Loading