diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..00b7967d0c255e059cea692bc9bbb3d527c6ead3 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Vue Draggable Grid + +A library for Vue2 which allows drag&drop functionality in a grid context. + +## Installation + +```bash +npm install vue-draggable-grid +``` + +## Usage + +Include the library in your project: + +```javascript +import draggableGrid from "vue-draggable-grid"; + +Vue.use(draggableGrid); + +// Now create your app as usual +``` + +An example usage could look like this: + +```vue +<template> + <drag-grid v-model="items" :cols="4" :rows="4"> + <template #item="item"> + {{ item.data.text }} + </template> + </drag-grid> +</template> + +<script> +export default { + name: "YourComponent", + data() { + return { + items: [ + { + x: 1, + y: 3, + w: 2, + h: 2, + key: "item1", + data: { text: "Hello world 1" }, + }, + { + x: 2, + y: 2, + w: 2, + h: 1, + key: "item2", + data: { text: "Hello world 2" }, + }, + { + x: 3, + y: 1, + w: 1, + h: 1, + key: "item3", + data: { text: "Hello world 3" }, + }, + ], + }; + }, +}; +</script> +``` + +For more examples visit the [examples folder](./example), or read the [documentation](./docs). + +Repository: [https://edugit.org/AlekSIS/libs/vue-draggable-grid/](https://edugit.org/AlekSIS/libs/vue-draggable-grid/) + +Built by [Julian Leucker](https://edugit.org/ZugBahnHof). diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index a5c37849b8ad179e4353d9d9ba7b6d4e995e6df4..fc286e0694242f13b2f07c45e5ecbe70661a0c17 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -16,6 +16,7 @@ module.exports = { "/examples/DisabledItems.md", "/examples/Responsive.md", "/examples/MultipleItemsY.md", + "/examples/Animals.md", ], }, ], diff --git a/docs/examples/Animals.md b/docs/examples/Animals.md new file mode 100644 index 0000000000000000000000000000000000000000..69a162210b2d15e77464158ce596bede157739ff --- /dev/null +++ b/docs/examples/Animals.md @@ -0,0 +1,14 @@ +# Example 10: Animals + +This example shows, how one can find out, if the user is dragging an item, +which item is being dragged and which grid is being dragged over. This is +an example for the proper `isDraggedOver` and the +`containerDragStart` and `containerDragEnd` events. + +<ClientOnly> +<script setup> +import ExampleAOverlay from "../../example/src/ExampleAOverlay.vue"; +</script> + +<ExampleAOverlay /> +</ClientOnly> diff --git a/example/src/App.vue b/example/src/App.vue index ce591a590d57fe854baa967df333168d9ec526a0..007744da04efc98c89e331300e1ce71812b08ca2 100644 --- a/example/src/App.vue +++ b/example/src/App.vue @@ -8,6 +8,7 @@ import Example6Disabled from "./Example6Disabled.vue"; import Example7DisabledItems from "./Example7DisabledItems.vue"; import Example8Responsive from "./Example8Responsive.vue"; import Example9MultipleItemsY from "./Example9MultipleItemsY.vue"; +import ExampleAOverlay from "./ExampleAOverlay.vue"; </script> <template> @@ -96,6 +97,15 @@ import Example9MultipleItemsY from "./Example9MultipleItemsY.vue"; width of every item has to be <kbd>1</kbd>) </p> <example9-multiple-items-y /> + + <h2>Example 10:</h2> + <p> + This example shows, how one can find out, if the user is dragging an item, + which item is being dragged and which grid is being dragged over. This is + an example for the proper <kbd>isDraggedOver</kbd> and the + <kbd>containerDragStart</kbd> and <kbd>containerDragEnd</kbd> events. + </p> + <example-a-overlay /> </div> </template> diff --git a/example/src/ExampleAOverlay.vue b/example/src/ExampleAOverlay.vue new file mode 100644 index 0000000000000000000000000000000000000000..a951b7bb323d79392a0d3709332b51f8ced64074 --- /dev/null +++ b/example/src/ExampleAOverlay.vue @@ -0,0 +1,323 @@ +<script> +import DragGrid from "../../src/DragGrid.vue"; + +function initialState() { + return { + items: [ + { + key: "key1", + x: 1, + y: 5, + w: 1, + h: 1, + data: { name: "Cat", grid: false }, + }, + { + key: "key2", + x: 4, + y: 3, + w: 1, + h: 1, + data: { name: "Penguin", grid: false }, + }, + { + key: "key3", + x: 2, + y: 3, + w: 1, + h: 1, + data: { name: "Dog", grid: false }, + }, + { + key: "key4", + x: 5, + y: 4, + w: 1, + h: 1, + data: { name: "Chinchilla", grid: false }, + }, + { + key: "key5", + x: 5, + y: 1, + w: 1, + h: 1, + data: { name: "Iguana", grid: false }, + }, + { + key: "key6", + x: 3, + y: 4, + w: 1, + h: 1, + data: { name: "Axolotl", grid: true }, + disabled: true, + }, + ], + overlays: [ + { key: "overlay1", x: 2, y: 2, animal: "Dog" }, + { key: "overlay2", x: 3, y: 5, animal: "Iguana" }, + { key: "overlay3", x: 4, y: 2, animal: "Chinchilla" }, + { key: "overlay4", x: 2, y: 4, animal: "Penguin" }, + { key: "overlay5", x: 4, y: 4, animal: "Cat" }, + ], + dragged: { + key1: false, + key2: false, + key3: false, + key4: false, + key5: false, + key6: false, + }, + showOverlayOnlyOnDragover: false, + showAllHighlights: false, + }; +} + +export default { + name: "ExampleAOverlay", + components: { + DragGrid, + }, + data() { + return initialState(); + }, + methods: { + handleItemChanged(element, gridId) { + if (gridId === "second-grid" && element.originGridId === "second-grid") { + // Disable movement inside the second grid + return; + } + + let overlay = this.overlays.find((o) => o.animal === element.data.name); + this.items.map((item) => { + if (item.key === element.key) { + item.data.grid = gridId === "ov-grid"; + item.x = element.x; + item.y = element.y; + item.disabled = + overlay && overlay.x === element.x && overlay.y === element.y; + } + return item; + }); + if (overlay && overlay.x === element.x && overlay.y === element.y) { + this.overlays = this.overlays.filter((o) => o.key !== overlay.key); + } + }, + reset() { + Object.assign(this.$data, initialState()); + }, + handleContainerDrag(element, type) { + this.dragged[element.key] = type === "start"; + }, + }, + computed: { + gridOneItems() { + return this.items.filter((item) => item.data.grid); + }, + gridTwoItems() { + return this.items + .filter((item) => !item.data.grid) + .map((item) => ({ + ...item, + x: () => 0, + y: () => 0, + disabled: false, + })); + }, + computedOverlays() { + if (this.showAllHighlights) { + return Object.values(this.dragged).some((obj) => !!obj) + ? this.overlays + : []; + } + return this.overlays.filter( + (o) => + this.dragged[ + ((a) => (a ? a.key : a))( + // Get the key of the dragged item or null + this.items.find((item) => item.data.name === o.animal), + ) + ], + ); + }, + }, +}; +</script> + +<template> + <div> + <drag-grid + :value="gridOneItems" + :cols="5" + :rows="5" + id="ov-grid" + context="animals" + grid-id="ov-grid" + @itemChanged="handleItemChanged($event, 'ov-grid')" + @containerDragStart="handleContainerDrag($event, 'start')" + @containerDragEnd="handleContainerDrag($event, 'end')" + > + <template #item="{ rawItem }"> + <div + :class="{ + container: true, + disabled: rawItem.disabled, + dragged: dragged[rawItem.key], + }" + > + <span>{{ rawItem.data.name }}</span> + </div> + </template> + <template #default="{ isDraggedOver }"> + <div + class="container goal" + v-for="goal in computedOverlays" + v-show="isDraggedOver || !showOverlayOnlyOnDragover" + :key="goal.key" + :style="{ + gridColumn: goal.x + ' / span 1', + gridRow: goal.y + ' / span 1', + }" + > + {{ goal.animal }} here! + </div> + </template> + </drag-grid> + <drag-grid + :value="gridTwoItems" + :cols="1" + :rows="5" + id="second-grid" + context="animals" + grid-id="second-grid" + no-highlight + @itemChanged="handleItemChanged($event, 'second-grid')" + @containerDragStart="handleContainerDrag($event, 'start')" + @containerDragEnd="handleContainerDrag($event, 'end')" + > + <template #item="{ rawItem }"> + <div + :class="{ + container: true, + disabled: rawItem.disabled, + dragged: dragged[rawItem.key], + }" + > + <span>{{ rawItem.data.name }}</span> + </div> + </template> + </drag-grid> + <span> + <button @click="reset">Reset</button><br /> + <button @click="showOverlayOnlyOnDragover = !showOverlayOnlyOnDragover"> + Show overlay + {{ showOverlayOnlyOnDragover ? "always" : "only on dragover" }}</button + ><br /> + <button @click="showAllHighlights = !showAllHighlights"> + {{ + showAllHighlights + ? "Only show overlay for dragged item" + : "Show overlay for all items" + }} + </button> + </span> + </div> +</template> + +<style scoped> +#ov-grid, +#second-grid { + max-width: 600px; + border-radius: 1em; + border: 0.2em solid #e0e0e0; + box-shadow: 0.2em 0.2em 0 0.1em #bdc3c7; + padding: 0.3em 0.4em 0.4em 0.3em; +} + +#second-grid { + width: fit-content; + height: 100%; +} + +div { + display: flex; + flex-direction: row; + gap: 1em; + align-items: stretch; +} + +.container { + background: #3498db; + color: #ecf0f1; + width: 100%; + height: 100%; + user-select: none; + text-align: center; + border-radius: 0.6em; + box-shadow: #2980b9 0.1em 0.1em 0 0.1em; + min-height: 3rem; + display: flex; + justify-content: center; + align-items: center; +} + +.container.disabled { + background: #bdc3c7; + box-shadow: #95a5a6 0.1em 0.1em 0 0.1em; +} + +.container.goal { + background: #2ecc71; + box-shadow: #27ae60 0.1em 0.1em 0 0.1em; + animation: breathe 2s infinite; +} + +@keyframes breathe { + 0% { + background: #2ecc71; + box-shadow: #27ae60 0.1em 0.1em 0 0.1em; + } + 50% { + background: #a3d9a5; + box-shadow: #2ecc71 0.1em 0.1em 0 0.1em; + } + 100% { + background: #2ecc71; + box-shadow: #27ae60 0.1em 0.1em 0 0.1em; + } +} + +.container.dragged { + animation: glare 2s infinite; + color: #222; + background: #f1c40f; + box-shadow: #f39c12 0.1em 0.1em 0 0.1em; +} + +@keyframes glare { + 0% { + background: #f1c40f; + box-shadow: #f39c12 0.1em 0.1em 0 0.1em; + } + 50% { + background: #f7dd73; + box-shadow: #f1c40f 0.1em 0.1em 0 0.1em; + } + 100% { + background: #f1c40f; + box-shadow: #f39c12 0.1em 0.1em 0 0.1em; + } +} + +button { + margin-top: 1em; + padding: 0.5em 1em; + border-radius: 0.5em; + border: 0.2em solid #e0e0e0; + background: #ecf0f1; + color: #3498db; + font-weight: bold; + cursor: pointer; +} +</style> diff --git a/package.json b/package.json index b24d5af01774a76ec186e92c54320481dd03abbf..074a5ac2cdbd31788f96f8ca76cce341e908504e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-draggable-grid", - "version": "0.2.0", + "version": "0.4.0", "scripts": { "build": "vite build", "example:build": "vite build example", diff --git a/src/DragContainer.vue b/src/DragContainer.vue index 6d62d98015af4a17403187a6d57ac8d8d24fe642..7f588839c0c6b2407834ef0f509e9e758aee2e9b 100644 --- a/src/DragContainer.vue +++ b/src/DragContainer.vue @@ -15,6 +15,7 @@ <script> export default { name: "DragContainer", + emits: ["dragstart", "dragmove", "dragend"], methods: { handleDragStart(event) { if (this.isDisabled) return; @@ -33,15 +34,18 @@ export default { }; // Now the element is on top of everything else inside the grid (not on top of other grids though) this.zIndex = 999999; + this.$emit("dragstart", this.dataTransfer); }, handleDragEnd() { this.offsetX = 0; this.offsetY = 0; this.zIndex = "auto"; + this.$emit("dragend", this.dataTransfer); }, handleDragMove(event) { this.offsetX += event.dx; this.offsetY += event.dy; + this.$emit("dragmove", this.dataTransfer); }, }, props: { diff --git a/src/DragGrid.vue b/src/DragGrid.vue index 1572374d6b64265cfe91c8ef0b489833d905ce61..c5e18ea21076747b760afadee24de2621f7bb567 100644 --- a/src/DragGrid.vue +++ b/src/DragGrid.vue @@ -35,6 +35,8 @@ :context="context" :grid-id="gridId" :disabled="disabled || loading || item.disabled" + @dragstart="emitContainerDragStart" + @dragend="emitContainerDragEnd" > <slot v-bind="transformItem(item)" :raw-item="item" name="item"> <dl> @@ -59,7 +61,7 @@ <slot name="disabledField" :is-dragged-over="isDraggedOver"></slot> </GridItem> </template> - <slot></slot> + <slot :is-dragged-over="isDraggedOver"></slot> </template> </interact> </template> @@ -75,7 +77,7 @@ export default { GridItem, DragContainer, }, - emits: ["input", "itemChanged"], + emits: ["input", "itemChanged", "containerDragStart", "containerDragEnd"], data() { return { isDraggedOver: false, @@ -312,6 +314,12 @@ export default { newItem.data = this.getObject("data", item); return newItem; }, + emitContainerDragStart(dataTransfer) { + this.$emit("containerDragStart", dataTransfer); + }, + emitContainerDragEnd(dataTransfer) { + this.$emit("containerDragEnd", dataTransfer); + }, }, computed: { gridData() {