refactor(Dashboard Grid): updates replace logic when moving widgets and fixes position detection between mousemove and mousedown

This commit is contained in:
SimonGschnell
2025-04-11 12:00:38 +02:00
parent 90ce3e0263
commit a0a2ac41bf
5 changed files with 119 additions and 39 deletions
+36 -7
View File
@@ -40,15 +40,30 @@ export default {
"loading",
"item_data",
"place",
"setup",
],
computed: {
maxHeight(){
return this.setup?.height?.max;
},
maxWidth(){
if (Object.prototype.toString.call(this.setup?.width) == "[object Number]"){
return this.setup?.width;
}
return this.setup?.width?.max;
},
minHeight() {
return this.setup?.height?.min;
},
minWidth() {
return this.setup?.width?.min;
},
isResizeable(){
return this.maxWidth >1 || this.maxHeight >1;
},
isPinned(){
return this.place?.pinned ? true : false;
},
isResizeable() {
if (!this.widget) return false;
return this.widget.setup.width.max || this.widget.setup.height.max;
},
ready() {
return this.component && this.arguments !== null;
},
@@ -138,7 +153,7 @@ export default {
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</div>
<div v-else-if="!hidden || editMode" class="dashboard-item card overflow-hidden h-100 position-relative" :class="arguments && arguments.className ? arguments.className : ''">
<div v-else-if="!hidden || editMode" :id="widgetID" class="dashboard-item card overflow-hidden h-100 position-relative" :class="arguments && arguments.className ? arguments.className : ''">
<div v-if="widget" class="card-header d-flex ps-0 pe-2 align-items-center">
<Transition>
<span v-if="editMode && !isPinned" drag-action="move" class="col-auto mx-2 px-2 cursor-move"><i class="fa-solid fa-grip-vertical"></i></span>
@@ -188,8 +203,22 @@ export default {
</template>
</bs-modal>
<height-transition>
<div v-if="editMode && isResizeable && !isPinned" class="card-footer d-flex justify-content-end p-0">
<span drag-action="resize" class="col-auto px-1 cursor-nw-resize"><i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i></span>
<div v-if="editMode && isResizeable && !isPinned " class="card-footer d-flex justify-content-end p-0">
<template v-if="maxWidth < 2">
<span drag-action="resize" class="col-auto px-1 cursor-ns-resize">
<i class="fa-solid fa-up-down pe-2"></i>
</span>
</template>
<template v-else-if="maxHeight < 2">
<span drag-action="resize" class="col-auto px-1 cursor-ew-resize">
<i class="fa-solid fa-left-right pe-2"></i>
</span>
</template>
<template v-else>
<span drag-action="resize" class="col-auto px-1 cursor-nw-resize">
<i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i>
</span>
</template>
</div>
</height-transition>
</div>`,
+21 -4
View File
@@ -1,4 +1,5 @@
import BsConfirm from "../Bootstrap/Confirm.js";
import SectionModal from "../Bootstrap/Alert.js";
import DropGrid from '../Drop/Grid.js'
import DashboardItem from "./Item.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
@@ -39,6 +40,7 @@ export default {
configOpened: false,
gridWidth: 1,
gridHeight: null,
draggedItem:null,
}
},
provide() {
@@ -50,6 +52,13 @@ export default {
}
},
computed: {
computedWidgetsSetup(){
if(!this.widgetsSetup) return {};
return this.widgetsSetup.reduce((acc, setup)=>{
acc[setup.widget_id] = setup.setup;
return acc;
},{})
},
editModeIsActive() {
return (this.editMode || this.adminMode) && !this.configOpened
},
@@ -81,6 +90,9 @@ export default {
},
methods: {
showSectionInformation(){
SectionModal.popup(`this is the information for the section ${name}`);
},
handleConfigOpened() {
this.configOpened = true
},
@@ -173,12 +185,17 @@ export default {
});
},
template: `
<h4 v-if="items.length>0 && editMode" class=" mb-0">
<i @click="showSectionInformation(name)" class="fa-solid fa-circle-info section-info" ></i>
{{name}}:
</h4>
<div class="dashboard-section position-relative pb-3 border-bottom" ref="container" :style="getSectionStyle">
<drop-grid v-model:cols="gridWidth" :items="items" :active="editModeIsActive" :resize-limit="checkResizeLimit" :margin-for-extra-row=".01" @rearrange-items="updatePositions" @gridHeight="gridHeight=$event" >
<drop-grid v-model:cols="gridWidth" :items="items" :active="editModeIsActive" :resize-limit="checkResizeLimit" :margin-for-extra-row=".01" @draggedItem="draggedItem=$event" @rearrange-items="updatePositions" @gridHeight="gridHeight=$event" >
<template #default="item">
<div v-if="item.placeholder" class="empty-tile-hover" @click="$emit('widgetAdd', name, { widget: 1, config: {}, place: {[gridWidth]: {x:item.x,y:item.y,w:1,h:1}}, custom: 1 })"></div>
<div v-else-if="item.widgetid == draggedItem?.data.widgetid" class="draggedItem" ></div>
<dashboard-item
v-if="!item.placeholder"
v-else
:id="item.widget"
:widgetID="item.id"
:width="item.w"
@@ -190,6 +207,7 @@ export default {
:hidden="item.hidden"
:editMode="editMode"
:place="item.place[gridWidth]"
:setup="computedWidgetsSetup[item.widget]"
@change="saveConfig($event, item)"
@remove="removeWidget(item, $event)"
@config-opened="handleConfigOpened"
@@ -197,7 +215,6 @@ export default {
@pinItem="updatePositions($event,true)"
@unPinItem="updatePositions">
</dashboard-item>
<div v-else class="empty-tile-hover" @click="$emit('widgetAdd', name, { widget: 1, config: {}, place: {[gridWidth]: {x:item.x,y:item.y,w:1,h:1}}, custom: 1 })"></div>
</template>
+34 -10
View File
@@ -6,6 +6,7 @@ import GridLogic from '../../composables/GridLogic.js';
const MODE_IDLE = 0;
const MODE_MOVE = 1;
const MODE_RESIZE = 2;
const MODE_MOUSE_DOWN = 3;
export default {
name: 'Grid',
@@ -29,6 +30,7 @@ export default {
"rearrangeItems",
"newItem",
"gridHeight",
"draggedItem",
],
data() {
return {
@@ -45,7 +47,6 @@ export default {
draggedOffset: [0,0],
draggedItem: null,
draggedNode: null,
draggedItemIcon: null,
additionalRow: null,
reorderedItems:[],
clonedWidget:null,
@@ -377,11 +378,13 @@ export default {
return;
this.mode = MODE_MOVE;
this.draggedItem = item;
this.$emit('draggedItem',item);
this.draggedNode = evt.target;
//clones the widget for the drag Image
let clone = evt.target.cloneNode(true);
clone.style.zIndex = 20;
clone.style.zIndex = 5;
clone.classList.add("widgetClone");
this.$refs.container.appendChild(clone);
this.clonedWidget = clone;
@@ -400,6 +403,7 @@ export default {
if (!this.active)
return this.dragCancel();
this.checkPinnedWidgetAnimation();
if (this.updateCursor(evt)) {
switch(this.mode) {
case MODE_MOVE: {
@@ -442,14 +446,9 @@ export default {
this.positionUpdates = null;
this.draggedOffset = [0,0],
this.draggedItem = null;
this.$emit('draggedItem',null);
this.draggedNode = null;
if(this.draggedItemIcon){
this.draggedItemIcon.style.top = '-999px';
this.draggedItemIcon.style.left = '-999px';
this.draggedItemIcon = null;
}
},
dragEnd() {
let widgetClones = document.getElementsByClassName("widgetClone");
@@ -486,11 +485,33 @@ export default {
this.$emit('newItem', this.x, this.y);
},
updateCursorOnMouseMove(evt){
if(this.mode == MODE_IDLE)
if(this.mode == MODE_IDLE){
this.updateCursor(evt);
}
}
},
checkPinnedWidgetAnimation(){
let itemAtPosition = this.items.filter(item => item.x == this.x && item.y == this.y).pop();
if (itemAtPosition && itemAtPosition.place[this.cols] && itemAtPosition.place[this.cols].pinned) {
let pinnedWidget = document.getElementById(itemAtPosition.widgetid);
let test = pinnedWidget.querySelector("[pinned='true']");
if (!test.classList.contains("denied-dragging-animation")) {
test.classList.add("denied-dragging-animation");
}
} else {
Array.from(document.getElementsByClassName("denied-dragging-animation"))?.forEach(ele => {
ele.classList.remove("denied-dragging-animation");
})
}
},
mouseDown(){
this.mode = MODE_MOUSE_DOWN;
},
mouseUp() {
this.mode = MODE_IDLE;
},
},
template: `
<p style="position:fixed; top:200px; left:300px; z-index:200;">{{x}}-{{y}}</p>
<div
ref="container"
class="drop-grid position-relative h-0"
@@ -503,10 +524,13 @@ export default {
@mouseleave="mouseLeave">
<TransitionGroup tag="div">
<grid-item
ref="gridItems"
v-for="(item,index) in (mode == 0 && active ? placedItems_withPlaceholders : placedItems)"
:key="item.data.id"
:item="item"
@start-move="startMove"
@mouse-down="mouseDown"
@mouse-up="mouseUp"
@start-resize="startResize"
@dragging="dragging"
@end-drag="dragCancel"
+5
View File
@@ -1,4 +1,5 @@
export default {
name:'GridItem',
components: {
},
inject: {
@@ -8,6 +9,8 @@ export default {
active: Boolean
},
emits: [
"mouseDown",
"mouseUp",
"startMove",
"startResize",
"dragging",
@@ -26,6 +29,7 @@ export default {
},
methods: {
registerDragAction(evt) {
this.$emit('mouseDown', evt);
if (evt.target.hasAttribute('drag-action')) {
this.dragAction = evt.target.getAttribute('drag-action');
} else {
@@ -64,6 +68,7 @@ export default {
template: `
<div class="drop-grid-item"
@mousedown="registerDragAction"
@mouseup="$emit('mouseUp', $event)"
@touchstart.prevent="touchStart"
@touchend="touchDragEnd"
@dragstart="tryDragStart($event, item)"
+23 -18
View File
@@ -96,7 +96,7 @@ class GridLogic {
}
}
move(item, x, y) {
if (item.data.place[this.w].pinned)
if (item.data.place[this.w]?.pinned)
return [];
if (item.x == x && item.y == y)
return [];
@@ -116,6 +116,8 @@ class GridLogic {
prefer = DIR_RIGHT;
}
const originalFrame = [...item.frame];
const currItem = {...item};
currItem.x = x;
currItem.y = y;
@@ -123,27 +125,27 @@ class GridLogic {
let occupiers = this.getItemsInFrame(currItem.frame);
// does not update if the target conatins pinned widgets
if (occupiers.some(frame => this.data[frame]?.data.place[this.w].pinned)) {
if (occupiers.some(frame => this.data[frame]?.data.place[this.w]?.pinned)) {
return [];
}
// checks if target contains widget with the same high and width
let replace = occupiers
.map(occupier => this.data[occupier])
.filter( occupier => {
return occupier.data.w == item.w && occupier.data.h == item.h
});
// replaces positions of widget and target widget if they have same height and width
if(replace.length > 0){
let replaceUpdate =[];
replaceUpdate[replace[0].index] = { index: replace[0].index, x:item.x, y:item.y };
replaceUpdate[item.index] = { index: item.index, x: replace[0].x, y: replace[0].y};
//update Grid and dataGrid
replace[0].frame.forEach(f => this.grid[f] = item.index)
item.frame.forEach(f => this.grid[f] = replace[0].index);
this.data[replace[0].index] = item;
this.data[item.index] = replace[0];
let occupiersData = occupiers.map(occupier => this.data[occupier]);
let occupiersFrame = occupiersData.map(occupier => occupier.frame).flat();
if (!occupiersFrame.some(frame => !currItem.frame.includes(frame)) && !occupiersFrame.some(frame => originalFrame.includes(frame))){
let replaceUpdate = [];
let newOccupierFrames = [];
for(let f of originalFrame){
if(newOccupierFrames.includes(f)){
continue;
}
let occ = occupiersData.shift();
if(occ){
newOccupierFrames = [...newOccupierFrames, ...this.getItemFrame({ ...occ, ...this.getSingleFramePosition(f) })];
replaceUpdate[occ.index] = { index: occ.index, ...this.getSingleFramePosition(f)}
}
}
replaceUpdate[item.index] = { index: item.index, x, y };
return replaceUpdate;
}
@@ -257,6 +259,9 @@ class GridLogic {
frame.push(i + item.x + (j + item.y) * this.w);
return frame;
}
getSingleFramePosition(frame){
return { x: frame % this.w, y: Math.floor(frame / this.w)};
}
debug() {
return this.grid;
}