diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index b644b9726bae3993d8d02160a5c863cdefc69e9b..a5c37849b8ad179e4353d9d9ba7b6d4e995e6df4 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -15,6 +15,7 @@ module.exports = { "/examples/Disabled.md", "/examples/DisabledItems.md", "/examples/Responsive.md", + "/examples/MultipleItemsY.md", ], }, ], diff --git a/docs/examples/MultipleItemsY.md b/docs/examples/MultipleItemsY.md new file mode 100644 index 0000000000000000000000000000000000000000..718f2b0b8cee338caaf859907a6aebc918363f3e --- /dev/null +++ b/docs/examples/MultipleItemsY.md @@ -0,0 +1,14 @@ +# Example 9: Multiple Items per field + +Grid with multiple items per slot (overlaps only in y direction possible, +width of every item has to be `1`). The items are placed in the grid automatically. + +To allow this behaviour, the grid component has to receive the prop `multiple-items-y` with the value `true`. + +<ClientOnly> +<script setup> +import Example9MultipleItemsY from "../../example/src/Example9MultipleItemsY.vue"; +</script> + +<Example9MultipleItemsY /> +</ClientOnly> diff --git a/docs/guide/index.md b/docs/guide/index.md index 1932522e53ec42d3df4ba5b8df043c860adc8fc1..9b6d2f8c8630a57e5970d2ba3ca7daeddea3ce7f 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -244,3 +244,17 @@ Examples for such a mechanism can be found in Examples 4 and 5. ::: To handle movements from one grid to another, the attributes `gridId` and `originGridId` of the event will help. + +## Overlapping grid items + +If two items overlap, the one which is defined later in the array will be on top of the other one. +They cannot be dragged. + +If you want to have overlapping items which can be dragged, there exists the `multipleItemsY` prop. +If this is set to `true`, items can be dragged on top of each other. The items will be stacked horizontally. +This is useful for e.g. a calendar-like frontend. + +::: danger +If `multipleItemsY` is set to `true`, the `w` property of all items has to be set to `1`. +Otherwise, this will lead to unexpected behaviour. +::: diff --git a/example/src/App.vue b/example/src/App.vue index ed093f14b5ee732a8b1829600355e0b01ba25bbf..ce591a590d57fe854baa967df333168d9ec526a0 100644 --- a/example/src/App.vue +++ b/example/src/App.vue @@ -7,6 +7,7 @@ import Example5Colors from "./Example5Colors.vue"; import Example6Disabled from "./Example6Disabled.vue"; import Example7DisabledItems from "./Example7DisabledItems.vue"; import Example8Responsive from "./Example8Responsive.vue"; +import Example9MultipleItemsY from "./Example9MultipleItemsY.vue"; </script> <template> @@ -88,6 +89,13 @@ import Example8Responsive from "./Example8Responsive.vue"; <h2>Example 8:</h2> <p>The grid is responsive. Try resizing it below:</p> <example8-responsive></example8-responsive> + + <h2>Example 9:</h2> + <p> + Grid with multiple items per slot (overlaps only in y direction possible, + width of every item has to be <kbd>1</kbd>) + </p> + <example9-multiple-items-y /> </div> </template> diff --git a/example/src/Example9MultipleItemsY.vue b/example/src/Example9MultipleItemsY.vue new file mode 100644 index 0000000000000000000000000000000000000000..44b0567fd065f5bd7c65c52eb54aacd3b9b1e451 --- /dev/null +++ b/example/src/Example9MultipleItemsY.vue @@ -0,0 +1,62 @@ +<script> +import { defineComponent } from "vue"; + +export default defineComponent({ + name: "Example9MultipleItemsY", + data() { + return { + items: [ + { x: 1, y: 4, w: 1, h: 1, key: "item 1" }, + { x: 1, y: 1, w: 1, h: 1, key: "item 2" }, + { x: 1, y: 2, w: 1, h: 2, key: "item 3" }, + { x: 1, y: 3, w: 1, h: 1, key: "item 4" }, + { x: 1, y: 4, w: 1, h: 2, key: "item 5" }, + { x: 1, y: 2, w: 1, h: 3, key: "item 6" }, + { x: 2, y: 2, w: 1, h: 3, key: "item 7" }, + { x: 2, y: 1, w: 1, h: 3, key: "item 8" }, + { x: 2, y: 4, w: 1, h: 2, key: "item 9" }, + { x: 2, y: 7, w: 1, h: 3, key: "item 10" }, + { x: 2, y: 3, w: 1, h: 1, key: "item 11" }, + ], + }; + }, +}); +</script> + +<template> + <div class="parent"> + <drag-grid + v-model="items" + :cols="2" + :rows="8" + class="grid" + multiple-items-y + > + <template #item="{ key }"> + <div class="item"> + {{ key }} + </div> + </template> + </drag-grid> + </div> +</template> + +<style scoped> +.parent { + min-height: 600px; +} + +.grid { + border: #97fa56 2px solid; + width: clamp(20vw, 500px, 80vw); + aspect-ratio: 1; +} + +.item { + background: #325442; + color: #97fa56; + border: #97fa56 2px dotted; + width: 100%; + height: 100%; +} +</style> diff --git a/src/DragContainer.vue b/src/DragContainer.vue index 25eaaeb9bb24374795c6c0ab5706f724f4759bd4..6d62d98015af4a17403187a6d57ac8d8d24fe642 100644 --- a/src/DragContainer.vue +++ b/src/DragContainer.vue @@ -79,6 +79,16 @@ export default { required: true, }, disabled: Boolean, + numSiblings: { + type: Number, + required: false, + default: 1, + }, + siblingIndex: { + type: Number, + required: false, + default: 0, + }, }, computed: { isInGrid() { @@ -102,6 +112,12 @@ export default { cursor() { return this.disabled ? "auto" : "grab"; }, + width() { + return 100 / this.numSiblings + "%"; + }, + left() { + return (100 / this.numSiblings) * this.siblingIndex + "%"; + }, dataTransferString() { return JSON.stringify(this.dataTransfer); }, @@ -128,5 +144,8 @@ export default { calc(1px * v-bind(offsetY)) ); z-index: v-bind(zIndex); + width: v-bind(width); + left: v-bind(left); + position: relative; } </style> diff --git a/src/DragGrid.vue b/src/DragGrid.vue index 025bdfe0a1e092b9edb1015accf1975f50157c62..49bba443ca14a52e17969e06ffe7792a7d37c47e 100644 --- a/src/DragGrid.vue +++ b/src/DragGrid.vue @@ -22,13 +22,15 @@ </div> <DragContainer - v-for="item in value" + v-for="item in items" :key="item.key" :drag-i-d="item.key" :x="getInt('x', item)" :y="getInt('y', item)" :w="getInt('w', item)" :h="getInt('h', item)" + :num-siblings="item.numSiblings" + :sibling-index="item.siblingIndex" :data="getObject('data', item)" :context="context" :grid-id="gridId" @@ -127,6 +129,11 @@ export default { required: false, default: false, }, + multipleItemsY: { + type: Boolean, + required: false, + default: false, + }, }, methods: { positionAllowed(x, y, key) { @@ -142,17 +149,19 @@ export default { 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 (!this.multipleItemsY) { + for (let item of this.value) { + if (key === item.key) continue; if ( - y >= this.getInt("y", item) && - y < this.getInt("y", item) + this.getInt("h", item) + x >= this.getInt("x", item) && + x < this.getInt("x", item) + this.getInt("w", item) ) { - return false; + if ( + y >= this.getInt("y", item) && + y < this.getInt("y", item) + this.getInt("h", item) + ) { + return false; + } } } } @@ -299,6 +308,52 @@ export default { context: this.context, }; }, + items() { + if (!this.multipleItemsY) return this.value; + + if (this.value.some((item) => item.w > 1)) { + console.warn( + "You are using multipleItemsY but some items have a width greater than 1.", + "This is not supported and will lead to unexpected behaviour." + ); + } + + // calculate numSiblings for each field + // First dimension: the columns + let xSiblings = []; + this.value.forEach((item) => { + for (let i = 0; i < item.h; i++) { + if (!xSiblings[item.x]) xSiblings[item.x] = []; + if (xSiblings[item.x][item.y + i] === undefined) { + xSiblings[item.x][item.y + i] = 0; + } + xSiblings[item.x][item.y + i]++; + } + }); + + let xSiblingsCopy = structuredClone(xSiblings); + + return this.value.map((item) => { + let numSiblings = xSiblings[item.x] + .filter((i, index) => { + return index >= item.y && index < item.y + item.h; + }) + .reduce((a, b) => Math.max(a, b), 0); + for (let i = item.y; i < item.h + item.y; i++) { + xSiblingsCopy[item.x][i]--; + } + let offset = xSiblingsCopy[item.x] + .filter((i, index) => { + return index >= item.y && index < item.y + item.h; + }) + .reduce((a, b) => Math.max(a, b), 0); + return { + ...item, + numSiblings: numSiblings, + siblingIndex: offset, + }; + }); + }, }, }; </script>