mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-18 23:42:17 +00:00
Basic VUE Components & Test WidgetController
This commit is contained in:
@@ -130,7 +130,7 @@ class Config extends Auth_Controller
|
||||
$this->terminateWithJsonError('override could not be saved');
|
||||
}
|
||||
|
||||
$this->outputJsonSuccess(array('msg' => 'override successfully stored.'));
|
||||
$this->outputJsonSuccess(array('msg' => 'override successfully stored.', 'data' => $override_decoded));
|
||||
}
|
||||
|
||||
public function removeWidgetFromUserOverride()
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
defined('BASEPATH') || exit('No direct script access allowed');
|
||||
/**
|
||||
* Description of Widget
|
||||
*
|
||||
* @author chris
|
||||
*/
|
||||
class Widget extends Auth_Controller
|
||||
{
|
||||
private $demoData = [
|
||||
[
|
||||
"id" => 1,
|
||||
"name" => 'Widget 1',
|
||||
"description" => 'Das ist ein Test Widget',
|
||||
"icon" => 'https://upload.wikimedia.org/wikipedia/commons/8/8a/Farben-Testbild.svg',
|
||||
"file" => 'DashboardWidget/Widget1.js',
|
||||
"arguments" => [
|
||||
"test" => 2
|
||||
],
|
||||
"size" => [
|
||||
"width" => [ "max" => 3 ],
|
||||
"height" => [ "max" => 3 ]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
array(
|
||||
'index' => 'dashboard/benutzer:r',
|
||||
'getWidgetsForDashboard' => 'dashboard/benutzer:rw',
|
||||
'addWidgetToUserOverride' => 'dashboard/benutzer:rw',
|
||||
'updateWidgetsToUserOverride' => 'dashboard/benutzer:rw',
|
||||
'removeWidgetFromUserOverride' => 'dashboard/benutzer:rw'
|
||||
)
|
||||
);
|
||||
|
||||
$this->load->library('dashboard/DashboardLib', null, 'DashboardLib');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$widget_id = $this->input->get('id');
|
||||
|
||||
foreach ($this->demoData as $widget) {
|
||||
if ($widget["id"] == $widget_id)
|
||||
return $this->outputJsonSuccess($widget);
|
||||
}
|
||||
return $this->outputJsonSuccess([
|
||||
"id" => 0,
|
||||
"name" => 'Widget Not Found',
|
||||
"file" => 'DashboardWidget/Default.js',
|
||||
"arguments" => [
|
||||
"className" => 'alert-danger',
|
||||
"title" => 'Widget Not Found',
|
||||
"msg" => 'The widget with the id ' . $widget_id . ' could not be found'
|
||||
],
|
||||
"size" => [
|
||||
"width" => 1,
|
||||
"height" => 1
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function getWidgetsForDashboard()
|
||||
{
|
||||
$db = $this->input->get('db');
|
||||
|
||||
$this->outputJsonSuccess($this->demoData);
|
||||
}
|
||||
|
||||
public function addWidgetToUserOverride()
|
||||
{
|
||||
$input = json_decode($this->input->raw_input_stream);
|
||||
$dashboard_kurzbz = $input->db;
|
||||
$uid = $input->uid;
|
||||
$funktion = $input->funktion;
|
||||
|
||||
$override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
|
||||
|
||||
$override_decoded = json_decode($override->override, true);
|
||||
$widgetid = isset($input->widgetid) ? array('widgetid' => $input->widgetid)
|
||||
: $this->DashboardLib->generateWidgetId($dashboard_kurzbz);
|
||||
|
||||
$override_decoded[$funktion][$widgetid['widgetid']] = $input->widget;
|
||||
|
||||
$override->override = json_encode($override_decoded);
|
||||
|
||||
$result = $this->DashboardLib->insertOrUpdateOverride($override);
|
||||
if( isError($result) ) {
|
||||
http_response_code(500);
|
||||
$this->terminateWithJsonError('override could not be saved');
|
||||
}
|
||||
|
||||
$this->outputJsonSuccess($widgetid['widgetid']);
|
||||
}
|
||||
|
||||
public function updateWidgetsToUserOverride()
|
||||
{
|
||||
$input = json_decode($this->input->raw_input_stream, true);
|
||||
$dashboard_kurzbz = $input['db'];
|
||||
$uid = $input['uid'];
|
||||
$funktion = $input['funktion'];
|
||||
|
||||
$override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
|
||||
|
||||
$override_decoded = json_decode($override->override, true);
|
||||
if ($override_decoded[$funktion])
|
||||
$override_decoded[$funktion] = array_replace_recursive($override_decoded[$funktion], $input['widgets']);
|
||||
else
|
||||
$override_decoded[$funktion] = $input['widgets'];
|
||||
|
||||
$override->override = json_encode($override_decoded);
|
||||
|
||||
$result = $this->DashboardLib->insertOrUpdateOverride($override);
|
||||
if( isError($result) ) {
|
||||
http_response_code(500);
|
||||
$this->terminateWithJsonError('override could not be saved');
|
||||
}
|
||||
|
||||
$this->outputJsonSuccess(['msg' => 'override successfully updated.']);
|
||||
}
|
||||
|
||||
public function removeWidgetFromUserOverride()
|
||||
{
|
||||
$input = json_decode($this->input->raw_input_stream);
|
||||
$dashboard_kurzbz = $input->db;
|
||||
$uid = $input->uid;
|
||||
$funktion = $input->funktion;
|
||||
$widgetid = $input->widgetid;
|
||||
|
||||
$override = $this->DashboardLib->getOverride($dashboard_kurzbz, $uid);
|
||||
if( empty($override) ) {
|
||||
http_response_code(404);
|
||||
$this->terminateWithJsonError('userconfig for dashboard '
|
||||
. $dashboard_kurzbz . ' not found.');
|
||||
}
|
||||
|
||||
$override_decoded = json_decode($override->override, true);
|
||||
|
||||
if( array_key_exists($widgetid, $override_decoded[$funktion]) )
|
||||
{
|
||||
unset($override_decoded[$funktion][$widgetid]);
|
||||
}
|
||||
else
|
||||
{
|
||||
http_response_code(404);
|
||||
$this->terminateWithJsonError('widgetid ' . $widgetid . ' not found in funktion ' . $funktion);
|
||||
}
|
||||
|
||||
$override->override = json_encode($override_decoded);
|
||||
$result = $this->DashboardLib->insertOrUpdateOverride($override, $uid);
|
||||
if( isError($result) )
|
||||
{
|
||||
http_response_code(500);
|
||||
$this->terminateWithJsonError('failed to remove widget');
|
||||
}
|
||||
$this->outputJsonSuccess(array('msg' => 'override successfully updated.'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.alert-danger .form-check-input:checked {
|
||||
border-color: #842029;
|
||||
background-color: #842029;
|
||||
}
|
||||
|
||||
.draganddropcontainer {
|
||||
grid-template-columns:repeat(4,1fr);
|
||||
gap: 1rem;
|
||||
place-items: stretch;
|
||||
place-content: stretch;
|
||||
}
|
||||
@media(max-width: 577px) {
|
||||
.draganddropcontainer {
|
||||
grid-template-columns:repeat(2,1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import DashboardSection from "./Section.js";
|
||||
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
|
||||
|
||||
export default {
|
||||
props: [
|
||||
"dashboard",
|
||||
"apiurl"
|
||||
],
|
||||
data: () => ({
|
||||
sections: [],
|
||||
widgets: [],
|
||||
isLoading: 0,
|
||||
tmpCreate: null,
|
||||
}),
|
||||
components: {
|
||||
DashboardSection
|
||||
},
|
||||
computed: {
|
||||
listReady() {
|
||||
return this.widgets.length && !this.isLoading;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
widgetAdd(section_name, widget) {
|
||||
if (!this.widgets.length) {
|
||||
axios.get(this.apiurl + '/Widget/getWidgetsForDashboard', {params:{
|
||||
db: this.dashboard
|
||||
}}).then(res => {
|
||||
//console.log(res.data.retval);
|
||||
this.widgets = res.data.retval;
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
}
|
||||
this.tmpCreate = {section_name,widget};
|
||||
this.listModal.show();
|
||||
},
|
||||
widgetCreate(widget) {
|
||||
this.isLoading = 1;
|
||||
this.tmpCreate.widget.widget = widget;
|
||||
axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
|
||||
db: this.dashboard,
|
||||
uid: 'ma0168',
|
||||
funktion_kurzbz: this.tmpCreate.section_name,
|
||||
widgets: [this.tmpCreate.widget]
|
||||
}).then(result => {
|
||||
let newId = 0;
|
||||
let sec = result.data.retval.data.widgets[this.tmpCreate.section_name];
|
||||
for (var i in sec) {
|
||||
newId = i;
|
||||
break;
|
||||
}
|
||||
console.log(newId);
|
||||
this.tmpCreate.widget.id = newId;
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == this.tmpCreate.section_name)
|
||||
section.widgets.push(this.tmpCreate.widget);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('ERROR: ', error);
|
||||
alert('ERROR: ' + error.response.data.retval);
|
||||
}).finally(() => {
|
||||
this.listModal.hide();
|
||||
this.isLoading = 0;
|
||||
});
|
||||
},
|
||||
widgetUpdate(section_name, payload) {
|
||||
payload = payload[section_name];
|
||||
for (var k in payload) {
|
||||
for (var i in this.sections) {
|
||||
if (this.sections[i].name == section_name) {
|
||||
for (var wid in this.sections[i].widgets) {
|
||||
if (this.sections[i].widgets[wid].id == k) {
|
||||
payload[k] = {...this.sections[i].widgets[wid], ...payload[k]};
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
payload[k].widgetid = k;
|
||||
}
|
||||
return axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
|
||||
db: this.dashboard,
|
||||
uid: 'ma0168',
|
||||
funktion_kurzbz: section_name,
|
||||
widgets: payload
|
||||
}).then(result => {
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == section_name) {
|
||||
section.widgets.forEach((widget, i) => {
|
||||
if (payload[widget.id]) {
|
||||
// TODO(chris): revert placement on failure
|
||||
delete payload[widget.id].place; // TODO(chris): find out why overwriting place bugs out
|
||||
section.widgets[i] = {...widget, ...payload[widget.id]};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('ERROR: ', error);
|
||||
alert('ERROR: ' + error.response.data.retval);
|
||||
});
|
||||
},
|
||||
widgetRemove(section_name, id) {
|
||||
axios.post(this.apiurl + '/Config/removeWidgetFromUserOverride', {
|
||||
db: this.dashboard,
|
||||
uid: 'ma0168',
|
||||
funktion_kurzbz: section_name,
|
||||
widgetid: id
|
||||
}).then(result => {
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == section_name)
|
||||
section.widgets = section.widgets.filter(widget => widget.id != id);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('ERROR: ', error);
|
||||
alert('ERROR: ' + error.response.data.retval);
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
CachedWidgetLoader.setPath(this.apiurl + '/Widget');
|
||||
axios.get(this.apiurl + '/Config', {params:{
|
||||
db: this.dashboard,
|
||||
uid: 'ma0168'
|
||||
}}).then(res => {
|
||||
//console.log(res.data.retval);
|
||||
for (var name in res.data.retval.widgets) {
|
||||
let widgets = [];
|
||||
for (var wid in res.data.retval.widgets[name]) {
|
||||
res.data.retval.widgets[name][wid].id = wid;
|
||||
widgets.push(res.data.retval.widgets[name][wid]);
|
||||
}
|
||||
this.sections.push({
|
||||
name: name,
|
||||
widgets: widgets
|
||||
});
|
||||
}
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
},
|
||||
mounted() {
|
||||
this.listModal = new bootstrap.Modal(this.$refs.widgetlist);
|
||||
},
|
||||
template: `<div class="core-dashboard">
|
||||
<dashboard-section v-for="section in sections" :key="section.name" :name="section.name" :widgets="section.widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section>
|
||||
<div ref="widgetlist" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create new widget</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div v-if="listReady" class="row">
|
||||
<div v-for="widget in widgets" :v-key="widget.id" class="col">
|
||||
<div class="card h-100" @click="widgetCreate(widget.id)">
|
||||
<img class="card-img-top" :src="widget.icon" :alt="'pictogram for ' + widget.name">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ widget.name }}</h5>
|
||||
<p class="card-text">{{ widget.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data: () => ({
|
||||
component: '',
|
||||
arguments: null,
|
||||
configModal: null,
|
||||
target: false,
|
||||
widget: null,
|
||||
tmpConfig: {},
|
||||
isLoading: false,
|
||||
hasConfig: true
|
||||
}),
|
||||
emits: [
|
||||
"change",
|
||||
"remove",
|
||||
"dragstart",
|
||||
"resizestart",
|
||||
],
|
||||
props: [
|
||||
"id",
|
||||
"config",
|
||||
"width",
|
||||
"height",
|
||||
"custom",
|
||||
"hidden",
|
||||
"editMode"
|
||||
],
|
||||
computed: {
|
||||
isResizeable() {
|
||||
if (!this.widget)
|
||||
return false;
|
||||
return this.widget.size.width.max || this.widget.size.height.max;
|
||||
},
|
||||
ready() {
|
||||
return this.component && this.arguments !== null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mouseDown(e) {
|
||||
this.target = e.target;
|
||||
},
|
||||
startDrag(e) {
|
||||
if (this.$refs.dragHandle.contains(this.target)) {
|
||||
this.$emit('dragstart', e);
|
||||
} else if (this.isResizeable && this.$refs.resizeHandle.contains(this.target)) {
|
||||
if (this.isResizeable)
|
||||
this.$emit('resizestart', e);
|
||||
else
|
||||
e.preventDefault();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
openConfig() {
|
||||
this.tmpConfig = {...this.arguments};
|
||||
this.configModal.show();
|
||||
},
|
||||
setConfig(hasConfig) {
|
||||
this.hasConfig = hasConfig;
|
||||
},
|
||||
changeConfig() {
|
||||
this.isLoading = true;
|
||||
let config = {...this.tmpConfig};
|
||||
this.sendChangeConfig(config);
|
||||
},
|
||||
changeConfigManually() {
|
||||
let config = {...this.arguments};
|
||||
this.sendChangeConfig(config);
|
||||
},
|
||||
sendChangeConfig(config) {
|
||||
for (var k in config) {
|
||||
if (this.widget.arguments[k] == config[k]) {
|
||||
delete config[k];
|
||||
}
|
||||
}
|
||||
this.$emit('change', config);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
config() {
|
||||
this.arguments = {...this.widget.arguments, ...this.config};
|
||||
this.tmpConfig = {...this.arguments};
|
||||
this.configModal.hide();
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.widget = await CachedWidgetLoader.loadWidget(this.id);
|
||||
let component = (await import('../' + this.widget.file)).default;
|
||||
this.$options.components['widget' + this.widget.id] = component;
|
||||
this.component = 'widget' + this.widget.id;
|
||||
this.arguments = {...this.widget.arguments, ...this.config};
|
||||
this.tmpConfig = {...this.arguments};
|
||||
},
|
||||
mounted() {
|
||||
this.configModal = new bootstrap.Modal(this.$refs.config);
|
||||
},
|
||||
template: `<div v-if="!hidden || editMode" :class="'dashboard-item card overflow-hidden ' + (arguments ? arguments.className : '')" @mousedown="mouseDown($event)" @dragstart="startDrag($event)" :draggable="!!editMode">
|
||||
<div v-if="editMode && widget" class="card-header d-flex">
|
||||
<span ref="dragHandle" class="col-auto pe-3"><i class="fa-solid fa-grip-vertical"></i></span>
|
||||
<span class="col">{{ widget.name }}</span>
|
||||
<a v-if="hasConfig" class="col-auto ps-1" href="#" @click.prevent="openConfig"><i class="fa-solid fa-gear"></i></a>
|
||||
<a v-if="custom" class="col-auto ps-1" href="#" @click.prevent="$emit('remove')">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</a>
|
||||
<div v-else class="col-auto ps-1 form-switch">
|
||||
<input class="form-check-input ms-0" type="checkbox" role="switch" id="flexSwitchCheckChecked" :checked="!hidden" @input="$emit('remove', hidden)">
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="ready" class="card-body overflow-hidden">
|
||||
<component :is="component" :config="arguments" :width="width" :height="height" @setConfig="setConfig" @change="changeConfigManually"></component>
|
||||
</div>
|
||||
<div v-else class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
|
||||
<div ref="config" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 v-if="widget" class="modal-title">Config for {{ widget.name }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<component v-if="ready && !isLoading" :is="component" :config="tmpConfig" @change="changeConfig" :configMode="true"></component>
|
||||
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="changeConfig">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="editMode && isResizeable" class="card-footer d-flex justify-content-end p-0">
|
||||
<span ref="resizeHandle" class="col-auto ps-1" @dragstart.prevent="$emit('resize')"><i class="fa-solid fa-up-right-and-down-left-from-center"></i></span>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
import DashboardItem from "./Item.js";
|
||||
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
|
||||
|
||||
// TODO(chris): handle overflow (moving outside the box)
|
||||
export default {
|
||||
data: () => ({
|
||||
gridWidth: 0,
|
||||
containerRect: {top:0,left:0},
|
||||
changeHeight: 1,
|
||||
movedObjects: [],
|
||||
editMode: 0,
|
||||
gridXLast: 0,
|
||||
gridYLast: 0,
|
||||
}),
|
||||
props: [
|
||||
"name",
|
||||
"widgets"
|
||||
],
|
||||
emits: [
|
||||
"widgetAdd",
|
||||
"widgetUpdate",
|
||||
"widgetRemove"
|
||||
],
|
||||
components: {
|
||||
DashboardItem
|
||||
},
|
||||
computed: {
|
||||
items() {
|
||||
this.widgets.forEach((item,i) => item.index = i);
|
||||
return this.widgets;
|
||||
},
|
||||
itemCoords() {
|
||||
if (!this.gridWidth)
|
||||
return [];
|
||||
let itemCoords = this.items.map(item => item.place[this.gridWidth] || this.createItemPlacement(item));
|
||||
// TODO(chris): verify positions & sizes
|
||||
let occupiers = [];
|
||||
let wrongPlacedItems = [];
|
||||
let gridWidth = this.gridWidth;
|
||||
this.items.forEach(item => {
|
||||
let x = item._x !== undefined ? item._x : itemCoords[item.index].x;
|
||||
let y = item._y !== undefined ? item._y : itemCoords[item.index].y;
|
||||
let w = item._w !== undefined ? item._w : itemCoords[item.index].w;
|
||||
let h = item._h !== undefined ? item._h : itemCoords[item.index].h;
|
||||
// TODO(chris): check with and height params here?
|
||||
for (var i = 0; i < w; i++) {
|
||||
for (var j = 0; j < h; j++) {
|
||||
var c = (y+j-1) * gridWidth + (x+i-1);
|
||||
// NOTE(chris): check for overlaping items
|
||||
if (occupiers[c] !== undefined) {
|
||||
//console.log('try to add ' + item.index + ' to ' + x + '/' + y + ', but ' + occupiers[c] + ' is already there');
|
||||
// NOTE(chris): remove possible other entries of this item
|
||||
for (var c2 = c; c2; c2--)
|
||||
if (occupiers[c2] == item.index)
|
||||
occupiers[c2] = undefined;
|
||||
wrongPlacedItems.push(item);
|
||||
return;
|
||||
}
|
||||
occupiers[c] = item.index;
|
||||
}
|
||||
}
|
||||
});
|
||||
wrongPlacedItems.forEach(item => {
|
||||
let w = item._w !== undefined ? item._w : itemCoords[item.index].w;
|
||||
let h = item._h !== undefined ? item._h : itemCoords[item.index].h;
|
||||
for (var c = 0; c < occupiers.length + gridWidth; c++) {
|
||||
if (occupiers[c] === undefined) {
|
||||
var occupied = false;
|
||||
for (var i = 0; i < w; i++) {
|
||||
for (var j = 0; j < h; j++) {
|
||||
if (occupiers[c + i + j * gridWidth] !== undefined) {
|
||||
i = w;
|
||||
occupied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!occupied) {
|
||||
item.place[gridWidth].x = c%gridWidth + 1;
|
||||
item.place[gridWidth].y = Math.floor(c/gridWidth) + 1;
|
||||
for (var i = 0; i < w; i++) {
|
||||
for (var j = 0; j < h; j++) {
|
||||
occupiers[c + i + j * gridWidth] = item.index;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return itemCoords;
|
||||
},
|
||||
gridHeight() {
|
||||
if (!this.gridWidth || !this.changeHeight)
|
||||
return 0;
|
||||
let minH = 0;
|
||||
this.itemCoords.forEach((item,i) => minH = Math.max(minH, (!this.editMode && this.items[i].hidden) ? 0 : item.y + item.h - 1));
|
||||
// TODO(chris): the extraline should only be present if all slots are occupied
|
||||
return minH + this.editMode;
|
||||
},
|
||||
gridOccupiers() {
|
||||
let occupiers = [];
|
||||
let gridWidth = this.gridWidth;
|
||||
this.items.forEach(item => {
|
||||
let x = item._x !== undefined ? item._x : this.itemCoords[item.index].x;
|
||||
let y = item._y !== undefined ? item._y : this.itemCoords[item.index].y;
|
||||
let w = item._w !== undefined ? item._w : this.itemCoords[item.index].w;
|
||||
let h = item._h !== undefined ? item._h : this.itemCoords[item.index].h;
|
||||
for (var i = 0; i < w; i++) {
|
||||
for (var j = 0; j < h; j++) {
|
||||
var c = (y+j-1) * gridWidth + (x+i-1);
|
||||
occupiers[c] = item.index;
|
||||
}
|
||||
}
|
||||
});
|
||||
return occupiers;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addWidget(evt) {
|
||||
if (evt.target != this.$refs.container || !this.editMode)
|
||||
return;
|
||||
const rect = this.containerRect;
|
||||
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth) + 1;
|
||||
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight) + 1;
|
||||
if (this.gridOccupiers[gridY * this.gridWidth + gridX] === undefined) {
|
||||
let widget = { widget: 1, config: {}, place: {}, custom: 1 };
|
||||
widget.place[this.gridWidth] = {
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
w: 1,
|
||||
h: 1
|
||||
};
|
||||
this.$emit('widgetAdd', this.name, widget);
|
||||
}
|
||||
},
|
||||
createItemPlacement(item) {
|
||||
// TODO(chris): create correct default placement if it is not there
|
||||
item.place[this.gridWidth] = {x:1,y:1,w:1,h:1};
|
||||
/*var freeList = [], nextId = 0;
|
||||
this.items.forEach(item => {
|
||||
if (!item.place[this.gridWidth]) {
|
||||
if (!this.gridWidth) {
|
||||
item.place[this.gridWidth] = {x:1,y:nextId++,w:1,h:1};
|
||||
} else {
|
||||
// TODO(chris): IMPLEMENT widths & heights
|
||||
if (freeList[nextId])
|
||||
while (freeList[++nextId]);
|
||||
freeList[nextId] = 1;
|
||||
item.place[this.gridWidth] = {x:(nextId%this.gridWidth)+1,y:Math.floor(nextId/this.gridWidth)+1,w:1,h:1};
|
||||
}
|
||||
}
|
||||
});*/
|
||||
return item.place[this.gridWidth];
|
||||
},
|
||||
startDrag(evt, item) {
|
||||
this.gridXLast = -1;
|
||||
this.gridYLast = -1;
|
||||
item._x = this.itemCoords[item.index].x;
|
||||
item._y = this.itemCoords[item.index].y;
|
||||
|
||||
evt.dataTransfer.dropEffect = 'move';
|
||||
evt.dataTransfer.effectAllowed = 'move';
|
||||
evt.dataTransfer.setData('itemAction', 'm');
|
||||
evt.dataTransfer.setData('itemId', item.index);
|
||||
evt.dataTransfer.setData('itemW', this.itemCoords[item.index].w);
|
||||
evt.dataTransfer.setData('itemH', this.itemCoords[item.index].h);
|
||||
},
|
||||
startResize(evt, item) {
|
||||
this.gridXLast = -1;
|
||||
this.gridYLast = -1;
|
||||
item._w = this.itemCoords[item.index].w;
|
||||
item._h = this.itemCoords[item.index].h;
|
||||
|
||||
evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
|
||||
evt.dataTransfer.dropEffect = 'move';
|
||||
evt.dataTransfer.effectAllowed = 'move';
|
||||
evt.dataTransfer.setData('itemAction', 'r');
|
||||
evt.dataTransfer.setData('itemId', item.index);
|
||||
evt.dataTransfer.setData('itemX', this.itemCoords[item.index].x);
|
||||
evt.dataTransfer.setData('itemY', this.itemCoords[item.index].y);
|
||||
},
|
||||
occupyFields(id, x, y, w, h) {
|
||||
var c;
|
||||
while ((c = this.movedObjects.pop())) {
|
||||
if (this.items[c]._y !== undefined) {
|
||||
this.items[c].place[this.gridWidth].y = this.items[c]._y;
|
||||
this.items[c]._y = undefined;
|
||||
}
|
||||
}
|
||||
var move = {};
|
||||
move[id] = this.items[id];
|
||||
this.getOccupiedItems(x,y,w,h,move);
|
||||
h = y + h;
|
||||
y = 0;
|
||||
for (x in move) {
|
||||
if (x != id) {
|
||||
c = move[x]._y !== undefined ? move[x]._y : this.itemCoords[x].y;
|
||||
if (c < h)
|
||||
y = Math.max(h-c, y);
|
||||
}
|
||||
}
|
||||
for (x in move) {
|
||||
if (x != id) {
|
||||
this.movedObjects.push(x);
|
||||
if (move[x]._y === undefined) {
|
||||
move[x]._y = this.itemCoords[x].y;
|
||||
}
|
||||
move[x].place[this.gridWidth].y = move[x]._y + y;
|
||||
}
|
||||
}
|
||||
},
|
||||
getOccupiedItems(x, y, w, h, move) {
|
||||
var i, j, c;
|
||||
for (i = 0; i < w; i++) {
|
||||
for (j = 0; j < h; j++) {
|
||||
c = (y+j-1) * this.gridWidth + (x+i-1);
|
||||
if (this.gridOccupiers[c] !== undefined && !move[this.gridOccupiers[c]]) {
|
||||
move[this.gridOccupiers[c]] = this.items[this.gridOccupiers[c]];
|
||||
c = this.itemCoords[this.gridOccupiers[c]];
|
||||
this.getOccupiedItems(c.x, c.y + 1, c.w, c.h, move);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDragOver(evt) {
|
||||
let id, x, y, w, h;
|
||||
const action = evt.dataTransfer.getData('itemAction');
|
||||
const rect = this.containerRect;
|
||||
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
|
||||
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
|
||||
|
||||
if (this.gridXLast == gridX && this.gridYLast == gridY)
|
||||
return;
|
||||
this.gridXLast = gridX;
|
||||
this.gridYLast = gridY;
|
||||
|
||||
if (action == 'm') {
|
||||
x = gridX + 1;
|
||||
y = gridY + 1;
|
||||
w = parseInt(evt.dataTransfer.getData('itemW'));
|
||||
h = parseInt(evt.dataTransfer.getData('itemH'));
|
||||
|
||||
if (x + w > this.gridWidth + 1)
|
||||
x = this.gridWidth + 1 - w;
|
||||
|
||||
id = evt.dataTransfer.getData('itemID');
|
||||
this.occupyFields(id, x, y, w, h);
|
||||
|
||||
this.itemCoords[id].x = x;
|
||||
this.itemCoords[id].y = y;
|
||||
} else if (action == 'r') {
|
||||
x = parseInt(evt.dataTransfer.getData('itemX'));
|
||||
y = parseInt(evt.dataTransfer.getData('itemY'));
|
||||
w = gridX + 2 - x;
|
||||
h = gridY + 2 - y;
|
||||
w = Math.max(1, w);
|
||||
h = Math.max(1, h);
|
||||
|
||||
if (x + w > this.gridWidth + 1)
|
||||
w = this.gridWidth + 1 - x;
|
||||
|
||||
id = evt.dataTransfer.getData('itemID');
|
||||
let widget = CachedWidgetLoader.getWidget(this.items[id].widget);
|
||||
if (widget) {
|
||||
let minmaxW = widget.size.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.size.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;
|
||||
}
|
||||
|
||||
this.occupyFields(id, x, y, w, h);
|
||||
|
||||
this.itemCoords[id].w = w;
|
||||
this.itemCoords[id].h = h;
|
||||
}
|
||||
},
|
||||
onDrop(evt) {
|
||||
let id = 0;
|
||||
let update = {};
|
||||
while ((id = this.movedObjects.pop())) {
|
||||
if (this.items[id]._y !== undefined) {
|
||||
if (this.itemCoords[id].y != this.items[id]._y) {
|
||||
update[this.items[id].id] = {place:{}};
|
||||
update[this.items[id].id].place[this.gridWidth] = {y:this.itemCoords[id].y};
|
||||
}
|
||||
this.items[id]._y = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
id = evt.dataTransfer.getData('itemId');
|
||||
|
||||
const action = evt.dataTransfer.getData('itemAction');
|
||||
update[this.items[id].id] = {place:{}};
|
||||
update[this.items[id].id].place[this.gridWidth] = {};
|
||||
|
||||
if (action == 'm') {
|
||||
if (this.items[id]._x !== undefined) {
|
||||
if (this.itemCoords[id].x != this.items[id]._x) {
|
||||
update[this.items[id].id].place[this.gridWidth].x = this.itemCoords[id].x;
|
||||
}
|
||||
this.items[id]._x = undefined;
|
||||
}
|
||||
if (this.items[id]._y !== undefined) {
|
||||
if (this.itemCoords[id].y != this.items[id]._y) {
|
||||
update[this.items[id].id].place[this.gridWidth].y = this.itemCoords[id].y;
|
||||
}
|
||||
this.items[id]._y = undefined;
|
||||
}
|
||||
} else if (action == 'r') {
|
||||
update[this.items[id].id].place[this.gridWidth].w = this.itemCoords[id].w;
|
||||
update[this.items[id].id].place[this.gridWidth].h = this.itemCoords[id].h;
|
||||
}
|
||||
|
||||
if (update[this.items[id].id].place[this.gridWidth].x === undefined &&
|
||||
update[this.items[id].id].place[this.gridWidth].y === undefined &&
|
||||
update[this.items[id].id].place[this.gridWidth].w === undefined &&
|
||||
update[this.items[id].id].place[this.gridWidth].h === undefined) {
|
||||
delete update[this.items[id].id].place[this.gridWidth];
|
||||
}
|
||||
|
||||
this.updatePreset(update);
|
||||
|
||||
// TODO(chris): find better way to trigger change for gridHeight
|
||||
this.changeHeight++
|
||||
},
|
||||
removeWidget(item, revert) {
|
||||
if (item.custom) {
|
||||
if (confirm('Are you sure you want to delete this widget?')) {
|
||||
this.$emit('widgetRemove', this.name, item.id);
|
||||
}
|
||||
} else {
|
||||
let update = {};
|
||||
update[item.id] = { hidden: !revert };
|
||||
this.updatePreset(update);
|
||||
}
|
||||
},
|
||||
saveConfig(config, item) {
|
||||
let payload = {};
|
||||
payload[item.id] = { config };console.log(payload);
|
||||
this.updatePreset(payload);
|
||||
},
|
||||
updatePreset(update) {
|
||||
let payload = {};
|
||||
payload[this.name] = update;
|
||||
this.$emit('widgetUpdate', this.name, payload);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let self = this;
|
||||
let cont = self.$refs.container;
|
||||
self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
|
||||
self.containerRect = cont.getBoundingClientRect();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
for (const child of cont.children) {
|
||||
child.style.display = 'none';
|
||||
}
|
||||
self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
|
||||
self.containerRect = cont.getBoundingClientRect();
|
||||
for (const child of cont.children) {
|
||||
child.style.display = '';
|
||||
}
|
||||
});
|
||||
},
|
||||
template: `<div class="dashboard-section">
|
||||
<h3 class="d-flex">
|
||||
<span class="col">{{name}}</span>
|
||||
<button class="col-auto btn" @click.prevent="editMode = editMode ? 0 : 1"><i class="fa-solid fa-gear"></i></button>
|
||||
</h3>
|
||||
<div class="position-relative" :style="'height:0;padding-bottom:' + (gridHeight * 100/gridWidth) + '%'">
|
||||
<div ref="container"
|
||||
class="position-absolute top-0 left-0 w-100 h-100 draganddropcontainer"
|
||||
:style="'display:grid;grid-template-rows:repeat('+gridHeight+',1fr)'"
|
||||
@click="addWidget($event)"
|
||||
@drop="onDrop($event, 1)"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragenter.prevent>
|
||||
|
||||
<dashboard-item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:id="item.widget"
|
||||
:config="item.config"
|
||||
:custom="item.custom"
|
||||
:hidden="item.hidden"
|
||||
:editMode="editMode"
|
||||
:x="itemCoords[item.index] ? itemCoords[item.index].x : -1"
|
||||
:y="itemCoords[item.index] ? itemCoords[item.index].y : -1"
|
||||
:style="itemCoords[item.index] ? {'grid-column-start':itemCoords[item.index].x,'grid-column-end':itemCoords[item.index].x+itemCoords[item.index].w,'grid-row-start':itemCoords[item.index].y,'grid-row-end':itemCoords[item.index].y+itemCoords[item.index].h} : {}"
|
||||
:width="itemCoords[item.index] ? itemCoords[item.index].w : 0"
|
||||
:height="itemCoords[item.index] ? itemCoords[item.index].h : 0"
|
||||
@dragstart="startDrag($event, item)"
|
||||
@resizestart="startResize($event, item)"
|
||||
@change="saveConfig($event, item)"
|
||||
@remove="removeWidget(item, $event)">
|
||||
</dashboard-item>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
props: [
|
||||
"config",
|
||||
"width",
|
||||
"height",
|
||||
"setConfig",
|
||||
"configMode"
|
||||
],
|
||||
emits: [
|
||||
"change" // TODO(chris): do we need this?
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import AbstractWidget from './Abstract';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
AbstractWidget
|
||||
],
|
||||
created() {
|
||||
this.$emit('setConfig', false)
|
||||
},
|
||||
template: `<div class="dashboard-widget-default">
|
||||
<h5 class="card-title">{{ config.title }}</h5>
|
||||
<p class="card-text">{{ config.msg }}</p>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
let __widgets = {};
|
||||
let __widgetsStarted = {};
|
||||
let __path = '';
|
||||
|
||||
export default {
|
||||
getWidget(id) {
|
||||
return __widgets[id];
|
||||
},
|
||||
loadWidget(id) {
|
||||
if (__widgets[id])
|
||||
return Promise.resolve(__widgets[id]);
|
||||
if (__widgetsStarted[id])
|
||||
return __widgetsStarted[id];
|
||||
if (!__path)
|
||||
return Promise.reject('Widget could not be loaded because there is no path yet!');
|
||||
|
||||
__widgetsStarted[id] = new Promise((resolve, reject) => {
|
||||
axios.get(__path, {params:{id}}).then(res => {
|
||||
__widgets[id] = res.data.retval;
|
||||
__widgetsStarted[id] = undefined;
|
||||
resolve(__widgets[id]);
|
||||
}).catch(error => reject(error.response.data.retval.error));
|
||||
});
|
||||
return __widgetsStarted[id];
|
||||
},
|
||||
setPath(path) {
|
||||
__path = path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user