-
-
-
-
-
-
-
-
-
- {{ widget ? 'Config for ' + widget.setup.name : '' }}
-
-
-
-
-
-
-
-
-
-
-
-
- `,
+
+
+
+
+
+ {{ widgetTemplate ? 'Config for ' + widgetTemplate.setup.name : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
};
diff --git a/public/js/components/Dashboard/Section.js b/public/js/components/Dashboard/Section.js
index 817cc52a3..db91915d5 100644
--- a/public/js/components/Dashboard/Section.js
+++ b/public/js/components/Dashboard/Section.js
@@ -1,9 +1,12 @@
import BsConfirm from "../Bootstrap/Confirm.js";
import DropGrid from '../Drop/Grid.js'
import DashboardItem from "./Item.js";
-import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
import WidgetIcon from "./Widget/WidgetIcon.js"
+import dragClick from '../../directives/dragClick.js';
+
+import ObjectUtils from "../../helpers/ObjectUtils.js";
+
export default {
name: 'Section',
components: {
@@ -11,8 +14,11 @@ export default {
DashboardItem,
WidgetIcon,
},
+ directives: {
+ dragClick
+ },
inject: {
- widgetsSetup:{
+ widgetsSetup: {
type: Array,
default: [],
},
@@ -39,9 +45,8 @@ export default {
configOpened: false,
gridWidth: 1,
gridHeight: null,
- draggedItem:null,
- additionalRow:false,
- }
+ additionalRow: false
+ };
},
provide() {
return {
@@ -49,22 +54,40 @@ export default {
this.editModeIsActive
),
sectionName: Vue.computed(() => this.name),
- }
+ };
},
computed: {
- computedWidgetsSetup(){
- if(!this.widgetsSetup) return {};
- return this.widgetsSetup.reduce((acc, setup)=>{
- acc[setup.widget_id] = setup.setup;
+ sectionNameTranslation() {
+ switch (this.name) {
+ case "general":
+ return this.$p.t('dashboard', this.name);
+ case "custom":
+ return this.$p.t('dashboard', this.name);
+ default:
+ return this.name;
+ }
+ },
+ showSectionInformation() {
+ switch (this.name) {
+ case "general":
+ return this.$p.t('dashboard', 'dashboardGeneralSectionDescription');
+ case "custom":
+ return this.$p.t('dashboard', 'dashboardCustomSectionDescription');
+ default:
+ return this.$p.t('dashboard', 'dashboardSectionDescription', [this.name]);
+ }
+ },
+ indexedWidgetsTemplates() {
+ if (!this.widgetsSetup)
+ return {};
+ return this.widgetsSetup.reduce((acc, setup) => {
+ acc[setup.widget_id] = setup;
return acc;
- },{})
+ }, {});
},
editModeIsActive() {
return (this.editMode || this.adminMode) && !this.configOpened
},
- getSectionStyle() {
- return 'margin-bottom: 8px;';
- },
items() {
// reuses the nearest placement of the widget from another viewport
/* const computeNearestPlace = (item, gridWidth) =>{
@@ -85,76 +108,55 @@ export default {
if(!item?.widgetid && item?.id){
item.widgetid = item.id;
}
- return { ...item, reorder: false, ...(item.place[this.gridWidth] || { reorder: true, ...{ x: 0, y: 0, w: 1, h: 1 } })};
+
+ let weight = 5;
+ if (!item.source)
+ weight = 6;
+ else if (item.source == 'general')
+ weight = 4;
+
+ let placement = item.place[this.gridWidth];
+ if (!placement) {
+ weight -= 3;
+ placement = {};
+ }
+
+ return { ...item, ...placement, weight };
});
- return placedItems;
-
- },
-
+
+ if (this.editModeIsActive)
+ return placedItems;
+ return placedItems.filter(item => !item.hidden);
+ }
+ },
+ watch: {
+ items() {
+ this.additionalRow = false;
+ }
},
methods: {
- sectionNameTranslation(){
- switch(this.name){
- case "general":
- return this.$p.t('dashboard',this.name);
- break;
- case "custom":
- return this.$p.t('dashboard',this.name);
- break;
- default:
- return this.name;
- break;
- }
- },
- showSectionInformation(){
- if (this.name == "general"){
- return this.$p.t('dashboard', 'dashboardGeneralSectionDescription');
- }
- else if(this.name == "custom"){
- return this.$p.t('dashboard', 'dashboardCustomSectionDescription');
- }
- else{
- return this.$p.t('dashboard', 'dashboardSectionDescription', [this.name]);
- }
- },
handleConfigOpened() {
this.configOpened = true
},
handleConfigClosed() {
this.configOpened = false
},
- checkResizeLimit(item, w, h) {
- // NOTE(chris): widgets needs to be loaded for this to work
- let widget = this.widgetState[item.widget];
- if (widget) {
- let minmaxW = { ...widget.setup.width };
- if (minmaxW.max)
- minmaxW.min = minmaxW.min || 1;
- else
- minmaxW = { min: minmaxW, max: minmaxW };
- if (w < minmaxW.min)
- w = minmaxW.min;
- if (w > minmaxW.max)
- w = minmaxW.max;
-
- let minmaxH = { ...widget.setup.height };
- if (minmaxH.max)
- minmaxH.min = minmaxH.min || 1;
- else
- minmaxH = { min: minmaxH, max: minmaxH };
- if (h < minmaxH.min)
- h = minmaxH.min;
- if (h > minmaxH.max)
- h = minmaxH.max;
- }
- return [w, h];
- },
removeWidget(item, revert) {
if (item.custom) {
- BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', this.name, item.id));
+ BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', item.id, this.name));
} else {
let update = {};
update[item.id] = { hidden: !revert };
+
+ if (!revert) {
+ // NOTE(chris): move to last line
+ update[item.id].place = [];
+ let y = this.gridHeight;
+ if (this.additionalRow)
+ y--;
+ update[item.id].place[this.gridWidth] = { x: 0, y };
+ }
+
this.updatePreset(update);
}
},
@@ -163,49 +165,42 @@ export default {
payload[item.id] = { config };
this.updatePreset(payload);
},
- updatePositions(updated, pinned=false) {
+ updatePositions(updated) {
let result = {};
updated.forEach(update => {
-
- let item = {...update.item};
- if (!item.placeholder) {
- if (!item.place[this.gridWidth])
- item.place[this.gridWidth] = {x: 0, y: 0, w: 1, h: 1};
- delete item.x;
- delete item.y;
- delete item.w;
- delete item.h;
- delete item.place[this.gridWidth].pinned;
- if (update.x !== undefined)
- item.place[this.gridWidth].x = update.x;
- if (update.y !== undefined)
- item.place[this.gridWidth].y = update.y;
- if (update.w !== undefined)
- item.place[this.gridWidth].w = update.w;
- if (update.h !== undefined)
- item.place[this.gridWidth].h = update.h;
- if (pinned){
- item.place[this.gridWidth].pinned = true;
- }
+ let item = structuredClone(ObjectUtils.deepToRaw(update.item));
- result[item.id] = item;
+ if (!item.placeholder) {
+ if (!item.place[this.gridWidth])
+ item.place[this.gridWidth] = { x: 0, y: 0, w: 1, h: 1 };
+
+ delete item.x;
+ delete item.y;
+ delete item.w;
+ delete item.h;
+ delete item.pinned;
+ delete item.weight;
+
+ if (update.x !== undefined)
+ item.place[this.gridWidth].x = update.x;
+ if (update.y !== undefined)
+ item.place[this.gridWidth].y = update.y;
+ if (update.w !== undefined)
+ item.place[this.gridWidth].w = update.w;
+ if (update.h !== undefined)
+ item.place[this.gridWidth].h = update.h;
+ if (update.pinned !== undefined)
+ item.place[this.gridWidth].pinned = update.pinned;
+
+ result[item.id] = item;
}
});
this.updatePreset(result);
},
updatePreset(update) {
- let payload = {};
- payload[this.name] = update;
- this.$emit('widgetUpdate', this.name, payload);
+ this.$emit('widgetUpdate', update, this.name);
}
},
- setup() {
- const { state: widgetState } = useCachedWidgetLoader();
-
- return {
- widgetState
- };
- },
mounted() {
let self = this;
let cont = self.$refs.container;
@@ -215,44 +210,62 @@ export default {
self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size'));
});
},
- template: `
-
-
-
- {{sectionNameTranslation()}}:
-
-
-
+ template: /* html */`
+
+
+
+ {{ sectionNameTranslation }}:
+
+
+
-
+
-
-
+ @pin-item="updatePositions"
+ @un-pin-item="updatePositions"
+ >
-
- `
+ `
}
/*
diff --git a/public/js/components/DashboardWidget/LvPlan.js b/public/js/components/DashboardWidget/LvPlan.js
index c82db2e0c..9000ff9c3 100644
--- a/public/js/components/DashboardWidget/LvPlan.js
+++ b/public/js/components/DashboardWidget/LvPlan.js
@@ -11,9 +11,6 @@ export default {
mixins: [
AbstractWidget
],
- inject: [
- "timezone"
- ],
methods: {
getPromiseFunc(start, end) {
return [
@@ -27,6 +24,6 @@ export default {
},
template: /*html*/`
-
+
`
}
\ No newline at end of file
diff --git a/public/js/components/Drop/Grid.js b/public/js/components/Drop/Grid.js
index a7bea64e3..1cecf0031 100644
--- a/public/js/components/Drop/Grid.js
+++ b/public/js/components/Drop/Grid.js
@@ -1,12 +1,9 @@
-// TODO(chris): Comments
-
import GridItem from './Grid/Item.js';
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',
@@ -17,135 +14,122 @@ export default {
cols: Number,
items: Array,
itemsSetup: Object,
- resizeLimit: Function,
active: {
type: Boolean,
default: true
},
- marginForExtraRow: {
- type: Number,
- default: 0
- },
- additionalRow:{
+ additionalRow: {
type: Boolean,
- default: false,
+ default: false
}
},
emits: [
"rearrangeItems",
- "newItem",
- "gridHeight",
- "draggedItem",
- "update:additionalRow"
+ "gridHeight"
],
data() {
return {
+ // gridlogic
+ grid: null,
+ tempPositionUpdates: null,
+ correctedPositionUpdates: null,
+ // dragging
+ mode: MODE_IDLE,
+ draggedOffset: [0, 0],
+ draggedItem: null,
+ overwriteRows: null,
+ clonedWidget: null, // ghost image
+ // tile coordinates while dragging
x: -1,
y: -1,
- clientX:0,
- clientY: 0,
- mode: MODE_IDLE,
- grid: null,
- dragGrid: null,
- permUpdates: [],
- positionUpdates: null,
- fixedPositionUpdates: null,
- draggedOffset: [0,0],
- draggedItem: null,
- draggedNode: null,
- reorderedItems:[],
- clonedWidget:null,
- }
- },
- inject:{
- sectionName: {
- type: String,
- default: '',
- },
+ // mouse coordinates while dragging
+ clientX: 0,
+ clientY: 0
+ };
},
computed: {
- additionalRowComputed: {
- get() {
- return this.additionalRow;
- },
- set(value) {
- this.$emit('update:additionalRow', value);
- }
- },
- items_hashmap() {
- let items = {};
- this.items.forEach(item => {
- if (this.reorderedItems.length > 0 && this.needsReordering(item)){
- let rearrangedPosition = this.reorderedItems.filter(widget => widget.data.widgetid == item.widgetid)?.pop();
- if (rearrangedPosition) {
- item.x = rearrangedPosition.x;
- item.y = rearrangedPosition.y;
- }
- }
- items[`x${item.x}y${item.y}`] = item;
- });
- return items
- },
- items_placeholders(){
- let placeholders = [];
- let col_max = this.cols;
- let rows_max = this.rows;
-
- // occupied hashmap to keep track of the occupied cells
- let occupied = {};
-
- for (let y = 0; y < rows_max; y++) {
- for (let x = 0; x < col_max; x++) {
- // skip current position if it was registered as occupied
- if (Object.keys(occupied).length && occupied[`x${x}y${y}`]) {
- continue;
- }
- let current_item = this.items_hashmap[`x${x}y${y}`];
- if (current_item) {
- //calculate the occupied cells from the width and the height from the items
- let width = current_item.w;
- let height = current_item.h;
- let max_x = x + width - 1;
- let max_y = y + height - 1;
- if(x != max_x || y != max_y){
- for (let occupied_y = y; occupied_y <= max_y; occupied_y++) {
- for (let occupied_x = x; occupied_x <= max_x; occupied_x++) {
- if (occupied_x != x || occupied_y != y) {
- occupied[`x${occupied_x}y${occupied_y}`]=true;
- }
- }
- }
- }
- }
- else {
- placeholders.push({ x: x, y: y, w: 1, h: 1, placeholder: true,
- data: { id: 'placeholder_' + String(placeholders.length).padStart(4, "0") } });
- }
- }
- }
- return placeholders;
- },
- placedItems_withPlaceholders() {
- return [...this.placedItems, ...this.items_placeholders];
- },
+ // gridlogic
rows() {
- if (this.additionalRowComputed) {
- return this.grid ? (this.grid.h+1) : 1;
- }
- return this.grid ? this.grid.h : 1;
+ const gridH = this.grid?.h || 1;
+
+ if (this.overwriteRows !== null && this.overwriteRows > gridH)
+ return this.overwriteRows;
+ if (this.additionalRow)
+ return this.grid ? gridH + 1 : 1;
+
+ return gridH;
},
gridStyle() {
- const addH = this.active ? this.marginForExtraRow : 0;
return {
- '--fhc-dg-row-height': 100/(this.rows + addH) + '%',
+ '--fhc-dg-row-height': 100/this.rows + '%',
'--fhc-dg-col-width': 100/this.cols + '%',
- '--fhc-dg-item-padding-horizontal': '0.25%',
- '--fhc-dg-item-padding-top': '0.5%',
- 'padding-bottom': 100 * (this.rows + addH)/this.cols + '%'
- }
+ '--fhc-dg-item-padding':
+ 'var(--fhc-dg-item-py, var(--fhc-dg-item-p, .25%))' +
+ ' ' +
+ 'var(--fhc-dg-item-px, var(--fhc-dg-item-p, .25%))',
+ 'padding-bottom': 100 * this.rows/this.cols + '%'
+ };
},
- indexedItems() {
+ // dragging
+ sizeLimits() {
+ return Object.fromEntries(Object.entries(this.itemsSetup).map(([type, { setup }]) => {
+ const result = {}; // work on a copy
+ if (setup.height === undefined)
+ result.height = { min: 1, max: undefined };
+ else if (Number.isInteger(setup.height))
+ result.height = { min: setup.height, max: setup.height };
+ else
+ result.height = {
+ min: setup.height.min ?? 1,
+ max: setup.height.max
+ };
+
+ if (setup.width === undefined)
+ result.width = { min: 1, max: undefined };
+ else if (Number.isInteger(setup.width))
+ result.width = { min: setup.width, max: setup.width };
+ else
+ result.width = {
+ min: setup.width.min ?? 1,
+ max: setup.width.max
+ };
+
+ return [type, result];
+ }));
+ },
+ // item pipeline
+ placeholders() { // empty tiles
+ let placeholders = this.grid.getFreeSlots().map((item, index) => {
+ return {
+ x: item.x,
+ y: item.y,
+ h: 1,
+ w: 1,
+ placeholder: true,
+ data: {
+ id: 'placeholder_' + index
+ }
+ };
+ });
+
+ if (this.additionalRow) {
+ for (var x = 0; x < this.cols; x++)
+ placeholders.push({
+ x,
+ y: this.grid.h,
+ h: 1,
+ w: 1,
+ placeholder: true,
+ data: {
+ id: 'placeholder_' + placeholders.length
+ }
+ });
+ }
+
+ return placeholders;
+ },
+ indexedItems() { // indexed
return this.items.map(
(item, index) => {
return {
@@ -154,207 +138,135 @@ export default {
y: item.y,
w: item.w,
h: item.h,
+ pinned: item.pinned,
weight: item.weight || 0,
data: item
}
}
);
},
- prePlacedItems() {
- if (!this.fixedPositionUpdates)
+ prePlacedItems() { // indexed & corrected
+ if (!this.correctedPositionUpdates)
return this.indexedItems;
return this.indexedItems.map(item => {
- if (!this.fixedPositionUpdates[item.index])
+ if (!this.correctedPositionUpdates[item.index])
return item;
return {
index: item.index,
weight: item.weight,
data: item.data,
- x: this.fixedPositionUpdates[item.index].x === undefined ? item.x : this.fixedPositionUpdates[item.index].x,
- y: this.fixedPositionUpdates[item.index].y === undefined ? item.y : this.fixedPositionUpdates[item.index].y,
- w: this.fixedPositionUpdates[item.index].w === undefined ? item.w : this.fixedPositionUpdates[item.index].w,
- h: this.fixedPositionUpdates[item.index].h === undefined ? item.h : this.fixedPositionUpdates[item.index].h
+ x: this.correctedPositionUpdates[item.index].x === undefined ? item.x : this.correctedPositionUpdates[item.index].x,
+ y: this.correctedPositionUpdates[item.index].y === undefined ? item.y : this.correctedPositionUpdates[item.index].y,
+ w: this.correctedPositionUpdates[item.index].w === undefined ? item.w : this.correctedPositionUpdates[item.index].w,
+ h: this.correctedPositionUpdates[item.index].h === undefined ? item.h : this.correctedPositionUpdates[item.index].h,
+ pinned: item.pinned
};
});
},
- placedItems() {
- if (!this.positionUpdates)
+ placedItems() { // indexed & corrected & dragging
+ if (!this.tempPositionUpdates)
return this.prePlacedItems;
- let mappedPlacedItems= this.prePlacedItems.map(item => {
- if (!this.positionUpdates[item.index] )
+ return this.prePlacedItems.map(item => {
+ if (!this.tempPositionUpdates[item.index])
return item;
- let height_diff = this.positionUpdates[item.index]?.h - item.h;
- let width_diff = this.positionUpdates[item.index]?.w - item.w;
+
return {
- resize: this.positionUpdates[item.index]?.resize,
index: item.index,
weight: item.weight,
data: item.data,
- x: this.positionUpdates[item.index].x === undefined ? item.x : this.positionUpdates[item.index].x,
- y: this.positionUpdates[item.index].y === undefined ? item.y : this.positionUpdates[item.index].y,
- w: width_diff>0?item.w:this.positionUpdates[item.index].w === undefined ? item.w : this.positionUpdates[item.index].w,
- h: height_diff > 0 ?item.h:this.positionUpdates[item.index].h === undefined ? item.h : this.positionUpdates[item.index].h
-
+ x: this.tempPositionUpdates[item.index].x === undefined ? item.x : this.tempPositionUpdates[item.index].x,
+ y: this.tempPositionUpdates[item.index].y === undefined ? item.y : this.tempPositionUpdates[item.index].y,
+ w: this.tempPositionUpdates[item.index].w === undefined ? item.w : this.tempPositionUpdates[item.index].w,
+ h: this.tempPositionUpdates[item.index].h === undefined ? item.h : this.tempPositionUpdates[item.index].h,
+ pinned: item.pinned
};
});
+ },
+ currentItems() { // final items with classes
+ if (this.mode == MODE_IDLE && this.active)
+ return [ ...this.placedItems, ...this.placeholders ];
- let temporaryResizeItems = [];
- mappedPlacedItems.forEach(item=>{
- if(item.resize){
- let newItem = {
- ...item,
- w:this.positionUpdates[item.index].w === undefined ? item.w : this.positionUpdates[item.index].w,
- h:this.positionUpdates[item.index].h === undefined ? item.h : this.positionUpdates[item.index].h,
- resizeOverlay:true,
- blank:true,
- };
- temporaryResizeItems.push(newItem)
+ if (this.mode != MODE_IDLE && this.draggedItem) {
+ // add classes to dragged item
+ const draggedItemIndex = this.placedItems.findIndex(item => item.index == this.draggedItem.index);
+ const modifiedDraggedItem = {
+ ...this.placedItems[draggedItemIndex],
+ classes: []
+ };
+
+ if (this.mode == MODE_MOVE) {
+ modifiedDraggedItem.classes.push('drop-grid-item-move');
}
- })
- return [...mappedPlacedItems, ...temporaryResizeItems];
- },
- showEmptyTileHover() {
- if (!this.active || !this.grid || this.mode != MODE_IDLE || this.x < 0 || this.y < 0 || this.x >= this.cols || this.y >= this.rows)
- return false;
- return this.grid.isFreeSlot(this.x, this.y);
- },
- widgetSetup(){
- if (!this.widgetsSetup)
- return;
- return this.widgetsSetup.reduce((acc, ele) => {
- acc[ele.widget_id] =ele;
- return acc;
- } ,{});
- },
+ if (this.mode == MODE_RESIZE) {
+ modifiedDraggedItem.classes.push('drop-grid-item-resize');
+ if (this.draggedItem.oversized)
+ modifiedDraggedItem.classes.push('drop-grid-item-oversized')
+ else if (this.tempPositionUpdates?.length)
+ modifiedDraggedItem.classes.push('drop-grid-item-sizechanged')
+ }
+
+ let currentItems = this.placedItems.toSpliced(draggedItemIndex, 1, modifiedDraggedItem);
+
+ if (!this.tempPositionUpdates?.length && this.draggedItem.blockers) {
+ this.draggedItem.blockers.forEach(index => {
+ const blockerIndex = this.placedItems.findIndex(item => item.index == index);
+ const modifiedBlocker = {
+ ...this.placedItems[blockerIndex],
+ classes: ['drop-grid-item-blocker']
+ };
+ currentItems.splice(blockerIndex, 1, modifiedBlocker);
+ });
+ }
+
+ return currentItems;
+ }
+
+ return this.placedItems;
+ }
},
watch: {
active(active) {
- if (!active)
+ if (!active && this.mode != MODE_IDLE)
this.dragCancel();
},
- cols() {
- this.dragCancel();
+ cols(value, oldValue) {
+ if (value == oldValue)
+ return;
+
+ this.reinitGrid();
},
rows: {
- handler(value) {
+ handler(value, oldValue) {
+ if (value == oldValue)
+ return;
+
this.$emit('gridHeight', value);
},
immediate: true
},
indexedItems: {
handler(value) {
- this.dragCancel();
-
- const updated = this.createNewGrid(value);
-
- this.fixedPositionUpdates = updated;
- if (updated.length)
- this.$emit('rearrangeItems', updated.filter(v => v));
+ this.reinitGrid();
},
immediate: true,
deep: true
}
},
methods: {
- needsReordering(item){
- if (!item?.data?.place[this.cols]){
- return true;
- }
- return false;
- },
- toggleDraggedItemOverlay(condition){
- if(!this.draggedNode)
- return;
- if(condition){
- this.draggedNode.firstElementChild.classList.add("dashboard-item-overlay");
- }else{
- this.draggedNode.firstElementChild.classList.remove("dashboard-item-overlay");
- }
- },
- dragging(event){
- if(this.mode == MODE_MOVE){
- this.toggleDraggedItemOverlay(true);
-
- const containerRect = this.$refs.container.getBoundingClientRect();
- const clonedWidgetRect = this.clonedWidget.getBoundingClientRect();
-
- let desiredTop = this.clientY - 20;
- let desiredLeft = this.clientX - 15;
-
- const minTop = 0;
- const maxTop = containerRect.height - clonedWidgetRect.height;
- const minLeft = 0;
- const maxLeft = containerRect.width - clonedWidgetRect.width;
-
- const constrainedTop = Math.max(minTop, Math.min(maxTop, desiredTop));
- const constrainedLeft = Math.max(minLeft, Math.min(maxLeft, desiredLeft));
-
- this.clonedWidget.style.top = `${constrainedTop}px`;
- this.clonedWidget.style.left = `${constrainedLeft}px`;
- }
- },
- createNewGrid(items) {
- this.grid = new GridLogic(this.cols);
- const result = [];
- let sortedItems = [...items].sort((a, b) => {
- if(this.needsReordering(a) && this.needsReordering(b)){
- return 0;
- }
- else if(this.needsReordering(a)){
- return 999;
- }
- else if(this.needsReordering(b)){
- return -999;
- }
-
- return a.weight > b.weight;
- });
- let reorderedItems = [];
- sortedItems.forEach(item => {
- let freeSlots = this.grid.getFreeSlots();
-
- if(this.needsReordering(item)){
- let firstFreeSlot = freeSlots.shift();
- if (!firstFreeSlot) {
- item.x = 0;
- item.y = this.grid.h;
- }else{
- item.x = firstFreeSlot.x;
- item.y = firstFreeSlot.y;
- }
- reorderedItems.push(item);
-
- }
- if (item.x + item.w > this.cols) {
- let targetW = this.cols-item.x,
- targetX = undefined;
- if (this.resizeLimit) {
- [targetW] = this.resizeLimit(item.data, targetW, item.h);
- }
- if (targetW < 1)
- targetW = 1;
- if (targetW > this.cols)
- targetW = this.cols;
- if (item.x + targetW > this.cols) {
- targetX = this.cols - targetW;
- }
- if (targetW == item.w)
- targetW = undefined;
- result[item.index] = {
- item: item.data,
- x: targetX,
- w: targetW
- };
- }
- item.frame = this.grid.getItemFrame(item);
- this.convertGridResultToUpdate(this.grid.add(item), result, items);
- });
- this.reorderedItems = reorderedItems;
- this.grid.clearWeights();
- return result;
+ // helpers
+ reinitGrid() {
+ if (this.mode != MODE_IDLE)
+ this.dragCancel();
+
+ const updated = this.createNewGrid(this.indexedItems);
+
+ this.correctedPositionUpdates = updated;
+ if (updated.length)
+ updated = updated.filter(v => v);
+ if (updated.length)
+ this.$emit('rearrangeItems', updated);
},
convertGridResultToUpdate(input, output, baseArray) {
-
if (!input)
return;
if (!baseArray)
@@ -374,19 +286,128 @@ export default {
output[item.index] = result;
});
},
- mouseLeave() {
- /* if (this.mode == MODE_IDLE) {
- this.x = -1;
- this.y = -1;
+ // gridlogic
+ createNewGrid(items) {
+ this.grid = new GridLogic(this.cols);
+ const result = [];
+ const sortedItems = [...items].sort((a, b) => a.weight > b.weight);
+ sortedItems.forEach(item => {
+ const target = { ...item };
+
+ if (target.x === undefined && target.y == undefined) {
+ target.x = 0;
+ target.y = 0;
+ const setup = this.sizeLimits[target.data.widget];
+ target.w = setup.width.min;
+ target.h = setup.height.min;
+ }
+
+ if (target.x + target.w > this.cols) {
+ let targetW = this.cols - target.x,
+ targetX = undefined;
+
+ [ targetW ] = this.cropSizeToAllowed(target.data.widget, targetW, target.h);
+
+ if (targetW > this.cols)
+ targetW = this.cols;
+ if (target.x + targetW > this.cols) {
+ targetX = this.cols - targetW;
+ }
+ if (targetW == target.w)
+ targetW = undefined;
+
+ if (targetX !== undefined)
+ target.x = targetX;
+ if (targetW !== undefined)
+ target.w = targetW;
+ }
+
+ target.frame = this.grid.getItemFrame(target);
+ this.convertGridResultToUpdate(this.grid.add(target), result, items);
- } */
+ ['x', 'y', 'h', 'w'].forEach(prop => {
+ if (target[prop] === item[prop])
+ return;
+
+ if (!result[target.index])
+ result[target.index] = { item: target.data };
+
+ if (result[target.index][prop] === undefined)
+ result[target.index][prop] = target[prop];
+ });
+ });
+ this.grid.clearWeights();
+ return result;
+ },
+ _updateCorrectedPositions(updated) {
+ updated.forEach((item, index) => {
+ if (!this.correctedPositionUpdates[index])
+ this.correctedPositionUpdates[index] = item;
+ else
+ this.correctedPositionUpdates[index] = {...this.correctedPositionUpdates[index], ...item};
+ });
+ let additionalUpdates = this.createNewGrid(this.prePlacedItems);
+ if (additionalUpdates.length) {
+ // NOTE(chris): this should never happen but it's here for safety
+ additionalUpdates.forEach((item, index) => updated[index] = item);
+ return this._updateCorrectedPositions(updated);
+ }
+ return updated;
+ },
+ // dragging
+ _dragStart(evt, item) {
+ if (evt.dataTransfer) {
+ evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
+ evt.dataTransfer.dropEffect = 'move';
+ evt.dataTransfer.effectAllowed = 'move';
+ }
+ },
+ startMove(evt, item) {
+ if (!this.active)
+ return;
+
+ // workaround for chrome fireing event dragend when styles are manipulated during dragging
+ setTimeout(() => {
+ this.mode = MODE_MOVE;
+ this.updateCursor(evt);
+ this.draggedItem = item;
+
+ //clones the widget for the drag Image
+
+ // NOTE(chris): this is the element that follows the mouse while dragging
+ // equivalent to the ghost image
+ let clone = evt.target.closest(".drop-grid-item")?.cloneNode(true);
+
+ clone.style.zIndex = 5;
+ clone.classList.add("widgetClone");
+ this.$refs.container.appendChild(clone);
+ const hiddenWidget = clone.querySelector("[style='display: none;']");
+ if (hiddenWidget)
+ hiddenWidget.style.removeProperty("display");
+ this.clonedWidget = clone;
+
+ this.draggedOffset = [item.x - this.x, item.y - this.y];
+ }, 0);
+
+ this._dragStart(evt, item);
+ },
+ startResize(evt, item) {
+ if (!this.active)
+ return;
+
+ // workaround for chrome fireing event dragend when styles are manipulated during dragging
+ setTimeout(() => {
+ this.mode = MODE_RESIZE;
+ this.draggedItem = item;
+ }, 0);
+
+ this._dragStart(evt);
},
updateCursor(evt) {
if (!this.active) {
this.x = this.y = -1;
return false;
}
- const addH = this.active ? this.marginForExtraRow : 0;
const rect = this.$refs.container.getBoundingClientRect();
if (!evt.clientX && !evt.clientY && evt.touches){
@@ -397,7 +418,7 @@ export default {
this.clientX = (evt.clientX - rect.left);
this.clientY = (evt.clientY - rect.top);
const gridX = Math.floor(this.cols * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
- const gridY = Math.floor((this.rows + addH) * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
+ const gridY = Math.floor(this.rows * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.x == gridX && this.y == gridY)
return false;
@@ -407,65 +428,19 @@ export default {
return true;
},
- _dragStart(evt, item) {
- if (evt.dataTransfer) {
- evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
- evt.dataTransfer.dropEffect = 'move';
- evt.dataTransfer.effectAllowed = 'move';
- }
- },
- startMove(evt, item) {
-
- if (!this.active)
- return;
-
- this.mode = MODE_MOVE;
-
- this.draggedItem = item;
-
- this.$emit('draggedItem', item);
- // workaround for chrome fireing event dragend when styles are manipulated during dragging
- setTimeout(() => {
- this.draggedNode = evt.target.closest(".drop-grid-item");
- //clones the widget for the drag Image
-
- let clone = evt.target.closest(".drop-grid-item")?.cloneNode(true);
-
- clone.style.zIndex = 5;
- clone.classList.add("widgetClone");
- this.$refs.container.appendChild(clone);
- const hiddenWidget = clone.querySelector("[style='display: none;']");
- hiddenWidget.style.removeProperty("display");
- this.clonedWidget = clone;
- }, 0);
-
- this.draggedOffset = [item.x - this.x, item.y - this.y];
- this._dragStart(evt, item);
- },
- startResize(evt, item) {
- if (!this.active)
- return;
- this.mode = MODE_RESIZE;
- this.draggedItem = item;
- this.$emit('draggedItem', item);
- this._dragStart(evt);
- },
dragOver(evt) {
if ((this.y + 1) > this.rows && (this.mode == MODE_MOVE || this.mode == MODE_RESIZE)) {
this.dragCancel();
-
}
if (!this.active)
return this.dragCancel();
- this.checkPinnedWidgetAnimation();
- if(this.mode == MODE_RESIZE){
- this.checkWidgetSizeLimitAnimation();
- }
+
if (this.updateCursor(evt)) {
+ const targetCoordinates = {};
+ const dragGrid = new GridLogic(this.grid);
+
switch(this.mode) {
case MODE_MOVE: {
- evt.preventDefault();
- this.dragGrid = new GridLogic(this.grid);
let x = this.x + this.draggedOffset[0];
let y = this.y + this.draggedOffset[1];
if (x < 0) {
@@ -479,216 +454,165 @@ export default {
this.draggedOffset[1] += y;
y = 0;
}
- this.positionUpdates= this.dragGrid.move(this.draggedItem, x, y);
+
+ targetCoordinates.x = x;
+ targetCoordinates.y = y;
+ targetCoordinates.w = this.draggedItem.w;
+ targetCoordinates.h = this.draggedItem.h;
+
+ this.tempPositionUpdates = dragGrid.move(this.draggedItem, x, y);
+ this.overwriteRows = dragGrid.h;
break;
}
case MODE_RESIZE: {
- evt.preventDefault();
- this.dragGrid = new GridLogic(this.grid);
- let w = Math.min(this.cols - this.draggedItem.x, Math.max(1, this.x - this.draggedItem.x + 1));
- let h = Math.max(1, this.y - this.draggedItem.y + 1);
- if (this.resizeLimit)
- [w, h] = this.resizeLimit(this.draggedItem.data, w, h);
- this.positionUpdates = this.dragGrid.resize(this.draggedItem, w, h);
+ const targetW = this.x - this.draggedItem.x + 1;
+ const targetH = this.y - this.draggedItem.y + 1;
+ let w = Math.min(this.cols - this.draggedItem.x, targetW);
+ let h = targetH;
+
+ [ w, h ] = this.cropSizeToAllowed(this.draggedItem.data.widget, w, h);
+
+ // check resize limits
+ this.draggedItem.oversized = (w !== targetW || h !== targetH);
+ if (this.draggedItem.oversized)
+ [ w, h ] = [ this.draggedItem.w, this.draggedItem.h ];
+
+ targetCoordinates.x = this.draggedItem.x;
+ targetCoordinates.y = this.draggedItem.y;
+ targetCoordinates.w = w;
+ targetCoordinates.h = h;
+
+ this.tempPositionUpdates = dragGrid.resize(this.draggedItem, w, h);
+ this.overwriteRows = dragGrid.h;
break;
}
}
+ // check for blocking pinned widgets
+ if (!this.tempPositionUpdates?.length) {
+ const frame = this.grid.getItemFrame(targetCoordinates);
+ const itemsAtPosition = this.grid.getItemsInFrame(frame);
+ this.draggedItem.blockers = itemsAtPosition.filter(index => this.indexedItems[index].pinned);
+ }
}
+ if (this.tempPositionUpdates?.length)
+ evt.preventDefault();
},
- dragCancel() {
- this.removeWidgetClones();
- this.additionalRowComputed = false;
- this.toggleDraggedItemOverlay(false);
+ _cleanupDragging() {
this.mode = MODE_IDLE;
- this.positionUpdates = null;
- this.draggedOffset = [0,0],
- this.draggedItem = null;
- this.$emit('draggedItem',null);
- this.draggedNode = null;
+ this.overwriteRows = null;
- },
- dragEnd() {
- this.removeWidgetClones();
- this.toggleDraggedItemOverlay(false);
-
- if (this.mode == MODE_IDLE){
- return;
+ if (this.draggedItem) {
+ const draggedItem = this.indexedItems.find(item => item.index == this.draggedItem.index);
+ delete draggedItem.classes;
+ this.draggedItem = null;
}
- // clean up unused classes
- let draggedItemNode = document.getElementById(this.draggedItem.data.widgetid);
- draggedItemNode.classList.remove("border-danger");
- Array.from(document.getElementsByClassName("denied-dragging-animation"))?.forEach(ele => {
- ele.classList.remove("denied-dragging-animation");
- })
-
- //if (!this.active || this.x < 0 || this.y < 0 || this.x >= this.cols)
- //return this.dragCancel();
-
- this.mode = MODE_IDLE;
- let updated = [];
- this.convertGridResultToUpdate(this.positionUpdates, updated);
- updated = this._updateFixedPositions(updated);
- if (updated.length)
- this.$emit('rearrangeItems', updated.filter(v => v));
-
- this.draggedItem = null;
- this.draggedNode = null;
- this.$emit('draggedItem', null);
- },
- _updateFixedPositions(updated) {
- updated.forEach((item, index) => {
- if (!this.fixedPositionUpdates[index])
- this.fixedPositionUpdates[index] = item;
- else
- this.fixedPositionUpdates[index] = {...this.fixedPositionUpdates[index], ...item};
- });
- let additionalUpdates = this.createNewGrid(this.prePlacedItems);
- if (additionalUpdates.length) {
- // NOTE(chris): this should never happen but it's here for safety
- additionalUpdates.forEach((item, index) => updated[index] = item);
- return this._updateFixedPositions(updated);
- }
- return updated;
- },
- emptyTileClicked() {
- this.additionalRowComputed = false;
- this.$emit('newItem', this.x, this.y);
- },
- updateCursorOnMouseMove(evt){
- if(this.mode == MODE_IDLE){
- this.updateCursor(evt);
- }
- },
- checkPinnedWidgetAnimation(){
- let itemAtPosition=[];
- switch(this.mode){
- case MODE_RESIZE:
- for (let x = this.draggedItem.x; x <= this.x; x++) {
- for (let y = this.draggedItem.y; y <= this.y; y++) {
- this.items.forEach(item => {
- if (item.x == x && item.y == y) {
- itemAtPosition.push(item);
- }
- });
- }
- }
- break;
- case MODE_MOVE:
- itemAtPosition = this.items.filter(item=>item.x == this.x && item.y == this.y);
- break;
- }
-
- Array.from(document.getElementsByClassName("denied-dragging-animation"))?.forEach(ele => {
- ele.classList.remove("denied-dragging-animation");
- })
-
- itemAtPosition.forEach(item=>{
- if (item.place[this.cols] && item.place[this.cols].pinned) {
- let pinnedWidget = document.getElementById(item.widgetid);
- let pinNode = pinnedWidget.querySelector("[pinned='true']");
- if (!pinNode.classList.contains("denied-dragging-animation")) {
- pinNode.classList.add("denied-dragging-animation");
- }
- }
- })
- },
- checkWidgetSizeLimitAnimation() {
-
- let draggedItemSetup = this.itemsSetup[this.draggedItem.data.widget];
- let draggedItemMaxWidth = draggedItemSetup.width.max ?? draggedItemSetup.width;
- let draggedItemMinWidth = draggedItemSetup.width.min ?? draggedItemSetup.width;
- let draggedItemMaxHeight = draggedItemSetup.height.max ?? draggedItemSetup.height;
- let draggedItemMinHeight = draggedItemSetup.height.min ?? draggedItemSetup.height;
- let draggedItemNode = document.getElementById(this.draggedItem.data.widgetid);
-
- let width_after_resize = this.x - this.draggedItem.x + 1;
- let height_after_resize = this.y - this.draggedItem.y + 1;
- if(
- (width_after_resize > 0 && (width_after_resize > draggedItemMaxWidth
- || width_after_resize < draggedItemMinWidth)
- )
- ||
- (height_after_resize > 0 && (height_after_resize > draggedItemMaxHeight
- || height_after_resize < draggedItemMinHeight)
- )
- ){
- draggedItemNode.classList.add("border-danger");
- }else{
- draggedItemNode.classList.remove("border-danger");
- }
- },
- removeWidgetClones(){
+ // removeWidgetClones
let widgetClones = Array.from(document.getElementsByClassName("widgetClone"));
for (let i = 0; i < widgetClones.length; i++) {
this.$refs.container.removeChild(widgetClones[i]);
}
},
- mouseDown(){
- this.mode = MODE_MOUSE_DOWN;
+ dragCancel() {
+ if (this.mode == MODE_IDLE) {
+ return;
+ }
+
+ this.tempPositionUpdates = null;
+ this.draggedOffset = [0,0];
+ this._cleanupDragging();
},
- mouseUp() {
- this.mode = MODE_IDLE;
+ dragEnd() {
+ if (this.mode == MODE_IDLE) {
+ return;
+ }
+
+ let updated = [];
+ this.convertGridResultToUpdate(this.tempPositionUpdates, updated);
+ updated = this._updateCorrectedPositions(updated);
+ if (updated.length)
+ this.$emit('rearrangeItems', updated.filter(v => v));
+
+ this._cleanupDragging();
},
+ moveGhostImage(event) {
+ if (this.mode == MODE_MOVE) {
+ const containerRect = this.$refs.container.getBoundingClientRect();
+ const clonedWidgetRect = this.clonedWidget.getBoundingClientRect();
+
+ let desiredTop = this.clientY - 20;
+ let desiredLeft = this.clientX - 15;
+
+ const minTop = 0;
+ const maxTop = containerRect.height - clonedWidgetRect.height;
+ const minLeft = 0;
+ const maxLeft = containerRect.width - clonedWidgetRect.width;
+
+ const constrainedTop = Math.max(minTop, Math.min(maxTop, desiredTop));
+ const constrainedLeft = Math.max(minLeft, Math.min(maxLeft, desiredLeft));
+
+ this.clonedWidget.style.top = `${constrainedTop}px`;
+ this.clonedWidget.style.left = `${constrainedLeft}px`;
+ }
+ },
+ cropSizeToAllowed(type, w, h) {
+ if (w < 1)
+ w = 1;
+ if (h < 1)
+ h = 1;
+
+ const setup = this.sizeLimits[type];
+
+ if (!setup)
+ return [w, h];
+
+ if (w < setup.width.min)
+ w = setup.width.min;
+ if (h < setup.height.min)
+ h = setup.height.min;
+ if (setup.width.max && w > setup.width.max)
+ w = setup.width.max;
+ if (setup.height.max && h > setup.height.max)
+ h = setup.height.max;
+
+ return [w, h];
+ }
},
- template: `
-
-
+ @dragover="dragOver"
+ @drop="dragEnd"
+ >
+
+ padding: 'var(--fhc-dg-item-padding)'
+ }"
+ @start-move="startMove"
+ @start-resize="startResize"
+ @drag="moveGhostImage"
+ @dragend="dragCancel"
+ >
-
+
-
-
`
+ `
}
-
-/*
-OLD VERSION - ON HOVER
-
-
-
-*/
\ No newline at end of file
diff --git a/public/js/components/Drop/Grid/Item.js b/public/js/components/Drop/Grid/Item.js
index bd256a1fb..d0f2b8774 100644
--- a/public/js/components/Drop/Grid/Item.js
+++ b/public/js/components/Drop/Grid/Item.js
@@ -1,88 +1,26 @@
export default {
name:'GridItem',
- components: {
- },
- inject: {
- },
props: {
- item: Object,
- active: Boolean
+ item: Object
},
emits: [
- "mouseDown",
- "mouseUp",
"startMove",
- "startResize",
- "dragging",
- "endDrag",
- "dropDrag",
- "item",
- "touchStart",
- "touchEnd",
+ "startResize"
],
- data() {
- return {
- dragAction: '',
- dragging: false
- }
- },
- computed: {
- },
methods: {
- registerDragAction(evt) {
- this.$emit('mouseDown', evt);
- if (evt.target.hasAttribute('drag-action')) {
- this.dragAction = evt.target.getAttribute('drag-action');
- } else {
- let parent = evt.target.closest('[drag-action]');
- if (parent) {
- this.dragAction = parent.getAttribute('drag-action');
- } else {
- this.dragAction = '';
- }
- }
- },
- tryDragStart(evt, item) {
- let dragAction = this.dragAction || evt.target.getAttribute('drag-action');
+ tryDragStart(evt) {
+ let dragAction = evt.target.getAttribute('drag-action');
if (dragAction) {
- this.dragging = true;
if (dragAction == 'move')
- return this.$emit('startMove', evt, item);
+ return this.$emit('startMove', evt, this.item);
else if (dragAction == 'resize')
- return this.$emit('startResize', evt, item);
- }
- //evt.preventDefault();
- },
- touchDragEnd(evt) {
- if (!this.dragging)
- return;
- this.dragging = false;
- this.$emit('touchEnd', evt);
- },
- touchStart(event){
- this.$emit('touchStart', event);
- this.registerDragAction(event);
- this.tryDragStart(event, this.item);
- },
- touchMove(event){
- if(this.dragging){
- event.preventDefault();
- this.$emit('dragging', event);
+ return this.$emit('startResize', evt, this.item);
}
}
},
- template: `
-
+ template: /* html */`
+
- `
+ `
}
diff --git a/public/js/composables/Dashboard/CachedWidgetLoader.js b/public/js/composables/Dashboard/CachedWidgetLoader.js
deleted file mode 100644
index 4bc2d4992..000000000
--- a/public/js/composables/Dashboard/CachedWidgetLoader.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import ApiWidget from "../../api/factory/dashboard/widget.js";
-
-const promises = Vue.ref([]);
-const stateRef = Vue.ref([]);
-const state = Vue.readonly(stateRef);
-
-export function useCachedWidgetLoader() {
- const $api = Vue.inject('$api');
- const $fhcAlert = Vue.inject('$fhcAlert');
-
- function load(id) {
- if (state.value[id])
- return Promise.resolve(state.value[id]);
-
- if (!promises.value[id])
- promises.value[id] = new Promise((resolve, reject) => {
- $api
- .call(ApiWidget.get(id))
- .then(res => {
- stateRef.value[id] = res.data;
- promises.value[id] = undefined;
- resolve(state.value[id]);
- })
- .catch($fhcAlert.handleSystemError);
- });
-
- return promises.value[id];
- }
-
- return {
- state,
- actions: {
- load
- }
- };
-}
\ No newline at end of file
diff --git a/public/js/composables/GridLogic.js b/public/js/composables/GridLogic.js
index ff8ae0c39..9f5b96ba6 100644
--- a/public/js/composables/GridLogic.js
+++ b/public/js/composables/GridLogic.js
@@ -1,4 +1,10 @@
-// TODO(chris): Comments
+/**
+ * This class arranges rectangular items on a grid with a defined width and
+ * a potential infinite height. It calculates repositioning of already placed
+ * items if a new item would overlap one or more of said placed items.
+ * This can be manipulated by adding weights to the items or by defining an
+ * item as pinned.
+ */
const DIR_UP = 0;
const DIR_LEFT = 1;
@@ -23,33 +29,23 @@ class GridLogic {
const i = y*this.w + x;
return !this.grid[i] && this.grid[i] !== 0;
}
- getMaxY(){
- return this.data.reduce((acc, item) => {
- if (item?.y > acc) {
- acc = item.y;
- }
- return acc;
- }, 0);
- }
getFreeSlots() {
const freeSlots = [];
- let biggestY = this.getMaxY();
- let totalSpaces = this.w * (biggestY+1);
- for(let i=0; i < totalSpaces; i++){
- if (!this.grid[i] && this.grid[i] !== 0){
- this.grid[i] = undefined;
- }
- }
- for(let i =0; i < this.grid.length; i++){
- if (!this.grid[i] && this.grid[i] !== 0){
+ let i = this.w * this.h;
+
+ while (i--) {
+ if (!this.grid[i] && this.grid[i] !== 0) {
let x = i % this.w;
let y = Math.floor(i / this.w);
freeSlots.push({x, y});
}
}
+
return freeSlots;
}
add(item, prefer) {
+ if (!item.frame)
+ item.frame = this.getItemFrame(item);
let occupiers = this.getItemsInFrame(item.frame);
if (!occupiers.length) {
item.frame.forEach(f => this.grid[f] = item.index);
@@ -61,6 +57,14 @@ class GridLogic {
item.frame.forEach(f => intermGrid.grid[f] = -1);
+ intermGrid.data.forEach(currItem => {
+ if (currItem.pinned) {
+ if (!currItem.frame)
+ currItem.frame = intermGrid.getItemFrame(currItem);
+ currItem.frame.forEach(f => intermGrid.grid[f] = -1);
+ }
+ });
+
const possiblities = intermGrid.tryMoving(occupiers, prefer);
if (possiblities.length) {
const bestOption = possiblities.sort((a,b) => {
@@ -83,7 +87,9 @@ class GridLogic {
result[move.index] = {
index: currItem.index,
x: currItem.x,
- y: currItem.y
+ y: currItem.y,
+ w: currItem.w,
+ h: currItem.h
};
});
item.frame.forEach(f => this.grid[f] = item.index);
@@ -91,12 +97,12 @@ class GridLogic {
return result;
} else {
- console.error('FATAL', "can't arrange item on grid");
+ return null;
}
}
}
move(item, x, y) {
- if (item.data.place[this.w]?.pinned)
+ if (item.pinned)
return [];
if (item.x == x && item.y == y)
return [];
@@ -116,8 +122,6 @@ class GridLogic {
prefer = DIR_RIGHT;
}
- const originalFrame = Array.isArray(item.frame) ? [...item.frame] : [item.frame];
-
const currItem = {...item};
currItem.x = x;
currItem.y = y;
@@ -125,33 +129,60 @@ 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]?.pinned)) {
return [];
}
-
- // checks if target contains widget with the same high and width
- 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 };
+
+ // checks if target contains moving widgets start position
+ // so swapping should be avoided
+ const targetAndItemOverlap = this.getItemFrame(item).some(frame => currItem.frame.includes(frame))
+ if (!targetAndItemOverlap) {
+
+ // checks if target contains widget with the same high and width
+ // so swapping is possible
+ const occupiersFrame = occupiers.map(occupier => this.data[occupier].frame).flat();
+ const occupiersInsideMovingItem = occupiersFrame.every(frame => currItem.frame.includes(frame));
+
+ if (occupiersInsideMovingItem) {
+ // every slot of all items in the target zone is inside said zone
+ const replaceUpdate = [];
- return replaceUpdate;
+ const diffX = item.x - x;
+ const diffY = item.y - y;
+
+ occupiers.forEach(occupier => {
+ const data = { ...this.data[occupier] };
+ data.x += diffX;
+ data.y += diffY;
+ data.frame = this.getItemFrame(data);
+ this.remove(data);
+ this.add(data);
+ replaceUpdate[occupier] = {
+ index: data.index,
+ x: data.x,
+ y: data.y,
+ w: data.w,
+ h: data.h
+ };
+ });
+
+ this.add({ ...item, x, y });
+ replaceUpdate[item.index] = {
+ index: item.index,
+ x,
+ y,
+ w: item.w,
+ h: item.h
+ };
+
+ return replaceUpdate;
+ }
}
const updates = this.add(currItem, prefer);
- updates[item.index] = {index: item.index, x, y};
+ if (updates)
+ updates[item.index] = { index: item.index, x, y, w: item.w, h: item.h };
+
return updates;
}
resize(item, w, h) {
@@ -166,7 +197,7 @@ class GridLogic {
const updates = this.add(currItem);
if(updates)
- updates[item.index] = {index: item.index, w, h, x:item.x, y:item.y, resize:true};
+ updates[item.index] = { index: item.index, w, h, x: item.x, y: item.y };
return updates;
}
@@ -205,13 +236,13 @@ class GridLogic {
let targetframe;
switch(dir) {
case DIR_UP:
- if (this.data[index].data?.place[this.w]?.pinned || this.data[index].y - amount < 0)
+ if (this.data[index].pinned || this.data[index].y - amount < 0)
return false;
targetframe = this.data[index].frame.map(i => i-this.w*amount);
move.y = -amount;
break;
case DIR_DOWN:
- if (this.data[index].data?.place[this.w]?.pinned)
+ if (this.data[index].pinned)
return false;
if (this.data[index].y + this.data[index].h + amount > this.h)
cost += .4;
@@ -219,13 +250,13 @@ class GridLogic {
move.y = amount;
break;
case DIR_LEFT:
- if (this.data[index].data?.place[this.w]?.pinned || this.data[index].x - amount < 0)
+ if (this.data[index].pinned || this.data[index].x - amount < 0)
return false;
targetframe = this.data[index].frame.map(i => i-amount);
move.x = -amount;
break;
case DIR_RIGHT:
- if (this.data[index].data?.place[this.w]?.pinned || this.data[index].x + this.data[index].w + amount > this.w)
+ if (this.data[index].pinned || this.data[index].x + this.data[index].w + amount > this.w)
return false;
targetframe = this.data[index].frame.map(i => i+amount);
move.x = amount;
@@ -262,9 +293,6 @@ 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;
}
diff --git a/public/js/composables/Renderers.js b/public/js/composables/Renderers.js
new file mode 100644
index 000000000..c250c27b5
--- /dev/null
+++ b/public/js/composables/Renderers.js
@@ -0,0 +1,50 @@
+import ApiRenderers from '../api/factory/renderers.js';
+
+/**
+ * @return object { renderers: Object }
+ */
+export function useRenderers() {
+ /* Result Vars */
+ const renderers = Vue.ref(null);
+
+ /* Helper Vars */
+ const $api = Vue.inject('$api');
+ const $fhcAlert = Vue.inject('$fhcAlert');
+
+ /* Main Logic */
+ $api
+ .call(ApiRenderers.loadRenderers())
+ .then(res => {
+ const head = document.head;
+ for (const rendertype of Object.keys(res.data)) {
+ const renderersForType = {};
+ for (const name of Object.keys(res.data[rendertype])) {
+ const rendererUrl = res.data[rendertype][name];
+ if (rendererUrl.substr(-4) == ".css") {
+ // add to head
+ if (!head.querySelector(`link[href="${rendererUrl}"]`)) {
+ var link = document.createElement("link");
+ link.type = "text/css";
+ link.rel = "stylesheet";
+ link.href = rendererUrl;
+ head.appendChild(link);
+ }
+ } else {
+ renderersForType[name] = Vue.markRaw(
+ Vue.defineAsyncComponent(() => import(rendererUrl))
+ );
+ }
+ }
+ if (Object.keys(renderersForType).length) {
+ if (renderers.value === null)
+ renderers.value = {};
+ renderers.value[rendertype] = renderersForType;
+ }
+ }
+ })
+ .catch($fhcAlert.handleSystemErrors);
+
+ return {
+ renderers
+ };
+}
\ No newline at end of file
diff --git a/public/js/directives/dragClick.js b/public/js/directives/dragClick.js
index 5603eb93f..9e6048c9a 100644
--- a/public/js/directives/dragClick.js
+++ b/public/js/directives/dragClick.js
@@ -1,5 +1,12 @@
import { bindDragEnterLeave } from '../helpers/DragAndDrop.js';
+import { enableDragDropTouch } from "../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
+
+if (!document.dragDropTouchActive) {
+ enableDragDropTouch();
+ document.dragDropTouchActive = true;
+}
+
export default {
mounted(el, binding) {
const delay = parseInt(binding.arg) || 300;
diff --git a/public/js/directives/draggable.js b/public/js/directives/draggable.js
index 64747c15a..d534b9703 100644
--- a/public/js/directives/draggable.js
+++ b/public/js/directives/draggable.js
@@ -1,5 +1,12 @@
import { setTransferData, convertToValidDragObject } from '../helpers/DragAndDrop.js';
+import { enableDragDropTouch } from "../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
+
+if (!document.dragDropTouchActive) {
+ enableDragDropTouch();
+ document.dragDropTouchActive = true;
+}
+
const EFFECTS = [
'none',
'copy',
diff --git a/public/js/directives/drop.js b/public/js/directives/drop.js
index 4c50e1b8e..4e14c5296 100644
--- a/public/js/directives/drop.js
+++ b/public/js/directives/drop.js
@@ -1,5 +1,12 @@
import { getValidTransferData, eventHasTypes, bindDragEnterLeave } from '../helpers/DragAndDrop.js';
+import { enableDragDropTouch } from "../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
+
+if (!document.dragDropTouchActive) {
+ enableDragDropTouch();
+ document.dragDropTouchActive = true;
+}
+
const EFFECTS = [
'move',
'copy',
diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php
index 2632548d6..007cd04dd 100644
--- a/system/dbupdate_3.4.php
+++ b/system/dbupdate_3.4.php
@@ -96,6 +96,7 @@ require_once('dbupdate_3.4/71566_studienordnungsdokument_neuer_organisationseinh
require_once('dbupdate_3.4/70376_lohnguide.php');
require_once('dbupdate_3.4/76150_perm_other_lv_plan.php');
require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php');
+require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '
Pruefe Tabellen und Attribute!
';
diff --git a/system/dbupdate_3.4/68530_Dashboard_Cleanup.php b/system/dbupdate_3.4/68530_Dashboard_Cleanup.php
new file mode 100644
index 000000000..918711211
--- /dev/null
+++ b/system/dbupdate_3.4/68530_Dashboard_Cleanup.php
@@ -0,0 +1,91 @@
+,
+ *
+ * Description:
+ * Cleanup Dashboard DB data
+ */
+if (! defined('DB_NAME')) exit('No direct script access allowed');
+
+// Cleanup presets
+if ($result = @$db->db_query("
+ SELECT 1
+ FROM dashboard.tbl_dashboard_preset
+ WHERE preset ? COALESCE(funktion_kurzbz, 'general')
+ OR preset ? 'custom'
+ LIMIT 1
+")) {
+ if ($db->db_num_rows($result)) {
+ $qry = "
+ UPDATE dashboard.tbl_dashboard_preset
+ SET preset = COALESCE(preset->COALESCE(funktion_kurzbz, 'general'), preset->'custom')->'widgets'
+ WHERE preset ? COALESCE(funktion_kurzbz, 'general')
+ OR preset ? 'custom'
+ ";
+
+ $result = $db->db_query($qry);
+
+ if (!$result) {
+ echo '
dashboard.tbl_dashboard_preset '.$db->db_last_error().'';
+ } else {
+ $affected_rows = $db->db_affected_rows($result);
+ echo 'dashboard.tbl_dashboard_preset: ' . $affected_rows . ' rows migrated
';
+ }
+ }
+}
+
+// Cleanup user overrides
+if ($result = @$db->db_query("
+ SELECT 1
+ FROM dashboard.tbl_dashboard_benutzer_override
+ WHERE EXISTS (
+ SELECT 1
+ FROM jsonb_each(override)
+ WHERE value ? 'widgets'
+ LIMIT 1
+ ) AND override <> '[]'::jsonb
+ LIMIT 1
+")) {
+ if ($db->db_num_rows($result)) {
+ $qry = "
+ UPDATE dashboard.tbl_dashboard_benutzer_override
+ SET override = COALESCE((
+ SELECT json_object_agg(key, value) FROM (
+ SELECT value->'widgets' AS widgets
+ FROM jsonb_each(override)
+ WHERE jsonb_typeof(value->'widgets') = 'object'
+ ) x, jsonb_each(widgets)
+ ), '[]')
+ WHERE EXISTS (
+ SELECT 1
+ FROM jsonb_each(override)
+ WHERE value ? 'widgets'
+ LIMIT 1
+ ) AND override <> '[]'::jsonb
+ ";
+
+ $result = $db->db_query($qry);
+
+ if (!$result) {
+ echo '
dashboard.tbl_dashboard_benutzer_override '.$db->db_last_error().'';
+ } else {
+ $affected_rows = $db->db_affected_rows($result);
+ echo 'dashboard.tbl_dashboard_benutzer_override: ' . $affected_rows . ' rows migrated
';
+ }
+ }
+}
diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php
index ac351c9e7..f19cf9fbd 100644
--- a/system/phrasesupdate.php
+++ b/system/phrasesupdate.php
@@ -47649,6 +47649,66 @@ array(
)
)
),
+ array(
+ 'app' => 'core',
+ 'category' => 'dashboard',
+ 'phrase' => 'widgetFromGeneralSection',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dieses Widget ist für das Dashboard vordefiniert',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This is a predefined widget of this dashboard',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'dashboard',
+ 'phrase' => 'widgetFromCustomSection',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dieses Widget wurde von Ihnen hinzugefügt',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This widget has been added by you',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'dashboard',
+ 'phrase' => 'widgetFromFunktionSection',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Dieses Widget wurde hinzugefügt weil Sie der "{0}" Funktion zugewiesen wurden',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'This widget has been added because you have been assigned to the "{0}" function',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
// CIS4 phrases from legacy code end
// FHC4 Phrases Abschlusspruefung
array(