You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
zabbix/ui/js/class.dashboard.page.js

2103 lines
52 KiB

/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
const DASHBOARD_PAGE_STATE_INITIAL = 'initial';
const DASHBOARD_PAGE_STATE_ACTIVE = 'active';
const DASHBOARD_PAGE_STATE_INACTIVE = 'inactive';
const DASHBOARD_PAGE_STATE_DESTROYED = 'destroyed';
const DASHBOARD_PAGE_EVENT_EDIT = 'dashboard-page-edit';
const DASHBOARD_PAGE_EVENT_WIDGET_ADD = 'dashboard-page-widget-add';
const DASHBOARD_PAGE_EVENT_WIDGET_ADD_NEW = 'dashboard-page-widget-add-new';
const DASHBOARD_PAGE_EVENT_WIDGET_DELETE = 'dashboard-page-widget-delete';
const DASHBOARD_PAGE_EVENT_WIDGET_POSITION = 'dashboard-page-widget-position';
const DASHBOARD_PAGE_EVENT_WIDGET_EDIT = 'dashboard-page-widget-edit';
const DASHBOARD_PAGE_EVENT_WIDGET_ACTIONS = 'dashboard-page-widget-actions';
const DASHBOARD_PAGE_EVENT_WIDGET_COPY = 'dashboard-page-widget-copy';
const DASHBOARD_PAGE_EVENT_WIDGET_PASTE = 'dashboard-page-widget-paste';
const DASHBOARD_PAGE_EVENT_ANNOUNCE_WIDGETS = 'dashboard-page-announce-widgets';
const DASHBOARD_PAGE_EVENT_RESERVE_HEADER_LINES = 'dashboard-page-reserve-header-lines';
class CDashboardPage {
constructor(target, {
data,
dashboard,
cell_width,
cell_height,
max_columns,
max_rows,
widget_min_rows,
widget_max_rows,
widget_defaults,
is_editable,
is_edit_mode,
can_edit_dashboards,
time_period,
dynamic_hostid,
csrf_token = null,
unique_id
}) {
this._target = document.createElement('div');
this._dashboard_grid = target;
this._data = {
dashboard_pageid: data.dashboard_pageid,
name: data.name,
display_period: data.display_period
};
this._dashboard = {
templateid: dashboard.templateid,
dashboardid: dashboard.dashboardid
};
this._cell_width = cell_width;
this._cell_height = cell_height;
this._max_columns = max_columns;
this._max_rows = max_rows;
this._widget_min_rows = widget_min_rows;
this._widget_max_rows = widget_max_rows;
this._widget_defaults = widget_defaults;
this._is_editable = is_editable;
this._is_edit_mode = is_edit_mode;
this._can_edit_dashboards = can_edit_dashboards;
this._time_period = time_period;
this._dynamic_hostid = dynamic_hostid;
this._csrf_token = csrf_token;
this._unique_id = unique_id;
this._init();
}
_init() {
this._state = DASHBOARD_PAGE_STATE_INITIAL;
this._widgets = new Map();
this._grid_min_rows = 0;
this._grid_pad_rows = 2;
this._is_unsaved = false;
if (this._is_edit_mode) {
this._initWidgetDragging();
this._initWidgetResizing();
}
this._initWidgetPlaceholder();
}
// Logical state control methods.
getState() {
return this._state;
}
start() {
this._state = DASHBOARD_PAGE_STATE_INACTIVE;
this._registerEvents();
for (const widget of this._widgets.keys()) {
widget.start();
}
}
activate() {
this._state = DASHBOARD_PAGE_STATE_ACTIVE;
this._resizeGrid();
this._activateEvents();
for (const widget of this._widgets.keys()) {
this._dashboard_grid.appendChild(widget.getView());
this._activateWidget(widget);
}
if (this._is_edit_mode) {
this._activateWidgetDragging();
this._activateWidgetResizing();
}
this.resetWidgetPlaceholder();
}
_activateWidget(widget) {
widget.activate();
widget
.on(WIDGET_EVENT_ACTIONS, this._events.widgetActions)
.on(WIDGET_EVENT_EDIT, this._events.widgetEdit)
.on(WIDGET_EVENT_ENTER, this._events.widgetEnter)
.on(WIDGET_EVENT_LEAVE, this._events.widgetLeave)
.on(WIDGET_EVENT_COPY, this._events.widgetCopy)
.on(WIDGET_EVENT_PASTE, this._events.widgetPaste)
.on(WIDGET_EVENT_DELETE, this._events.widgetDelete);
}
deactivate() {
this._state = DASHBOARD_PAGE_STATE_INACTIVE;
for (const widget of this._widgets.keys()) {
this._dashboard_grid.removeChild(widget.getView());
this._deactivateWidget(widget);
}
this._deactivateEvents();
this._deactivateWidgetPlaceholder();
if (this._is_edit_mode) {
this._deactivateWidgetDragging();
this._deactivateWidgetResizing();
}
}
_deactivateWidget(widget) {
widget.deactivate();
widget
.off(WIDGET_EVENT_ACTIONS, this._events.widgetActions)
.off(WIDGET_EVENT_EDIT, this._events.widgetEdit)
.off(WIDGET_EVENT_ENTER, this._events.widgetEnter)
.off(WIDGET_EVENT_LEAVE, this._events.widgetLeave)
.off(WIDGET_EVENT_COPY, this._events.widgetCopy)
.off(WIDGET_EVENT_PASTE, this._events.widgetPaste)
.off(WIDGET_EVENT_DELETE, this._events.widgetDelete);
}
destroy() {
if (this._state === DASHBOARD_PAGE_STATE_ACTIVE) {
this.deactivate();
}
if (this._state !== DASHBOARD_PAGE_STATE_INACTIVE) {
throw new Error('Unsupported state change.');
}
this._state = DASHBOARD_PAGE_STATE_DESTROYED;
for (const widget of this._widgets.keys()) {
widget.destroy();
}
this._widgets.clear();
}
// External events management methods.
isEditMode() {
return this._is_edit_mode;
}
setEditMode() {
this._is_edit_mode = true;
for (const widget of this._widgets.keys()) {
widget.setEditMode();
}
this._initWidgetDragging();
this._initWidgetResizing();
if (this._state === DASHBOARD_PAGE_STATE_ACTIVE) {
this._resizeGrid();
this._activateWidgetDragging();
this._activateWidgetResizing();
this.resetWidgetPlaceholder();
}
}
setDynamicHost(dynamic_hostid) {
if (this._dynamic_hostid != dynamic_hostid) {
this._dynamic_hostid = dynamic_hostid;
for (const widget of this._widgets.keys()) {
if (widget.supportsDynamicHosts() && this._dynamic_hostid != widget.getDynamicHost()) {
widget.setDynamicHost(this._dynamic_hostid);
}
}
}
}
setTimePeriod(time_period) {
this._time_period = time_period;
for (const widget of this._widgets.keys()) {
widget.setTimePeriod(this._time_period);
}
}
isUserInteracting() {
for (const widget of this._widgets.keys()) {
if (widget.isUserInteracting()) {
return true;
}
}
return false;
}
announceWidgets(dashboard_pages) {
let widgets = [];
for (const dashboard_page of dashboard_pages) {
widgets = widgets.concat(Array.from(dashboard_page._widgets.keys()));
}
for (const widget of widgets) {
widget.announceWidgets(widgets);
}
}
isUnsaved() {
return this._is_unsaved;
}
// Data interface methods.
getUniqueId() {
return this._unique_id;
}
getDashboardPageId() {
return this._data.dashboard_pageid;
}
getName() {
return this._data.name;
}
setName(name) {
this._data.name = name;
}
getDisplayPeriod() {
return this._data.display_period;
}
setDisplayPeriod(display_period) {
this._data.display_period = display_period;
}
getWidgets() {
return [...this._widgets.keys()];
}
getWidget(unique_id) {
for (const widget of this._widgets.keys()) {
if (widget.getUniqueId() === unique_id) {
return widget;
}
}
return null;
}
addWidget({type, name, view_mode, fields, widgetid, pos, is_new, rf_rate, unique_id}) {
let widget;
if (type in this._widget_defaults) {
widget = this._createWidget(eval(this._widget_defaults[type].js_class), {
type,
name,
view_mode,
fields,
defaults: this._widget_defaults[type],
widgetid,
pos,
is_new,
rf_rate,
unique_id
});
}
else {
widget = this._createInaccessibleWidget({widgetid, pos, unique_id});
}
this._doAddWidget(widget);
if (widgetid === null) {
this._is_unsaved = true;
}
return widget;
}
addPastePlaceholderWidget({type, name, view_mode, pos, unique_id}) {
const paste_placeholder_widget = this._createPastePlaceholderWidget({type, name, view_mode, pos, unique_id});
this._doAddWidget(paste_placeholder_widget);
return paste_placeholder_widget;
}
_doAddWidget(widget) {
this._widgets.set(widget, {});
if (!this._isHelperWidget(widget)) {
this.fire(DASHBOARD_PAGE_EVENT_ANNOUNCE_WIDGETS);
}
if (this._state !== DASHBOARD_PAGE_STATE_INITIAL) {
widget.start();
}
if (this._state === DASHBOARD_PAGE_STATE_ACTIVE) {
const pos = widget.getPos();
this._resizeGrid(pos.y + pos.height + this._grid_pad_rows);
this._dashboard_grid.appendChild(widget.getView());
this._activateWidget(widget);
}
}
deleteWidget(widget, {is_batch_mode = false} = {}) {
if (widget.getState() === WIDGET_STATE_ACTIVE) {
this._dashboard_grid.removeChild(widget.getView());
this._deactivateWidget(widget);
}
if (widget.getState() !== WIDGET_STATE_INITIAL) {
widget.destroy();
}
this._widgets.delete(widget);
if (!is_batch_mode) {
if (!this._isHelperWidget(widget)) {
this.fire(DASHBOARD_PAGE_EVENT_ANNOUNCE_WIDGETS);
}
this._resizeGrid();
}
this._is_unsaved = true;
}
replaceWidget(widget, widget_data) {
this.deleteWidget(widget, {is_batch_mode: true});
return this.addWidget(widget_data);
}
_createWidget(widget_class, {type, name, view_mode, fields, defaults, widgetid, pos, is_new, rf_rate, unique_id}) {
return new widget_class({
type,
name,
view_mode,
fields,
defaults,
widgetid,
pos,
is_new,
rf_rate,
dashboard: {
templateid: this._dashboard.templateid,
dashboardid: this._dashboard.dashboardid
},
dashboard_page: {
unique_id: this._unique_id
},
cell_width: this._cell_width,
cell_height: this._cell_height,
min_rows: this._widget_min_rows,
is_editable: this._is_editable,
is_edit_mode: this._is_edit_mode,
can_edit_dashboards: this._can_edit_dashboards,
time_period: this._time_period,
dynamic_hostid: this._dynamic_hostid,
csrf_token: this._csrf_token,
unique_id
});
}
_createInaccessibleWidget({widgetid, pos, unique_id}) {
return this._createWidget(CWidgetInaccessible, {
type: 'inaccessible',
view_mode: ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER,
fields: {},
defaults: {
name: t('Inaccessible widget')
},
widgetid,
pos,
is_new: false,
rf_rate: 0,
unique_id
});
}
_createPastePlaceholderWidget({type, name, view_mode, pos, unique_id}) {
return this._createWidget(CWidgetPastePlaceholder, {
type: 'paste-placeholder',
name,
view_mode,
fields: {},
defaults: this._widget_defaults[type],
widgetid: null,
pos,
is_new: false,
rf_rate: 0,
unique_id
});
}
_isHelperWidget(widget) {
return widget instanceof CWidgetPastePlaceholder;
}
getDataCopy() {
const data = {
name: this._data.name,
display_period: this._data.display_period,
widgets: [],
dashboard: {
templateid: this._dashboard.templateid
}
};
for (const widget of this._widgets.keys()) {
if (!this._isHelperWidget(widget)) {
data.widgets.push(widget.getDataCopy({is_single_copy: false}));
}
}
return data;
}
save() {
const data = {
dashboard_pageid: this._data.dashboard_pageid ?? undefined,
name: this._data.name,
display_period: this._data.display_period,
widgets: []
};
for (const widget of this._widgets.keys()) {
if (!this._isHelperWidget(widget)) {
data.widgets.push(widget.save());
}
}
return data;
}
// Dashboard page view methods.
findFreePos({width, height}) {
const pos = {x: 0, y: 0, width: width, height: height};
const max_column = this._max_columns - pos.width;
const max_row = this._max_rows - pos.height;
let found = false;
let x, y;
for (y = 0; !found; y++) {
if (y > max_row) {
return null;
}
for (x = 0; x <= max_column && !found; x++) {
pos.x = x;
pos.y = y;
found = this._isPosFree(pos);
}
}
return pos;
}
accommodatePos(pos, {reverse_x = false, reverse_y = false} = {}) {
let pos_variants = [];
let pos_x = this._accommodatePosX({
...pos,
y: reverse_y ? pos.y + pos.height - this._widget_min_rows : pos.y,
height: this._widget_min_rows
}, {reverse: reverse_x});
pos_x = {...pos_x, y: pos.y, height: pos.height};
if (reverse_x) {
for (let x = pos_x.x, width = pos_x.width; width >= 1; x++, width--) {
pos_variants.push(this._accommodatePosY({...pos_x, x, width}, {reverse: reverse_y}));
}
}
else {
for (let width = pos_x.width; width >= 1; width--) {
pos_variants.push(this._accommodatePosY({...pos_x, width}, {reverse: reverse_y}));
}
}
let pos_best = null;
let pos_best_value = null;
for (const pos_variant of pos_variants) {
const delta_x = Math.abs(reverse_x ? pos_variant.x - pos.x : pos_variant.width - pos.width);
const delta_y = Math.abs(reverse_y ? pos_variant.y - pos.y : pos_variant.height - pos.height);
const value = Math.sqrt(Math.pow(delta_x, 2) + Math.pow(delta_y, 2));
if (pos_best === null
|| (pos_best.width == 1 && pos_variant.width > 1)
|| ((pos_best.width > 1 === pos_variant.width > 1) && value < pos_best_value)) {
pos_best = {...pos_variant};
pos_best_value = value;
}
}
return pos_best;
}
_accommodatePosX(pos, {reverse = false} = {}) {
const max_pos = {...pos};
if (reverse) {
for (let x = pos.x + pos.width - 1, width = 1; x >= pos.x; x--, width++) {
if (!this._isPosFree({...max_pos, x, width})) {
break;
}
max_pos.x = x;
max_pos.width = width;
}
}
else {
for (let width = 1; width <= pos.width; width++) {
if (!this._isPosFree({...max_pos, width})) {
break;
}
max_pos.width = width;
}
}
return max_pos;
}
_accommodatePosY(pos, {reverse = false} = {}) {
const max_pos = {...pos};
if (reverse) {
for (let y = pos.y + pos.height - 1, height = 1; y >= pos.y; y--, height++) {
if (!this._isPosFree({...max_pos, y, height})) {
break;
}
max_pos.y = y;
max_pos.height = height;
}
}
else {
for (let height = this._widget_min_rows; height <= pos.height; height++) {
if (!this._isPosFree({...max_pos, height})) {
break;
}
max_pos.height = height;
}
}
return max_pos;
}
promiseScrollIntoView(pos) {
const wrapper = document.querySelector('.wrapper');
const wrapper_offset_top = wrapper.scrollTop + this._dashboard_grid.getBoundingClientRect().y;
const margin = 5;
const pos_top = wrapper_offset_top + pos.y * this._cell_height - margin;
const pos_height = pos.height * this._cell_height + margin * 2;
const wrapper_scroll_top_min = Math.max(0, pos_top + Math.min(0, pos_height - wrapper.clientHeight));
const wrapper_scroll_top_max = pos_top;
this._resizeGrid(pos.y + pos.height + this._grid_pad_rows);
return new Promise((resolve) => {
let scroll_to = null;
if (wrapper.scrollTop < wrapper_scroll_top_min) {
scroll_to = wrapper_scroll_top_min;
}
else if (wrapper.scrollTop > wrapper_scroll_top_max) {
scroll_to = wrapper_scroll_top_max;
}
else {
return resolve();
}
const start_scroll = wrapper.scrollTop;
const start_time = Date.now();
const end_time = start_time + 300;
const animate = () => {
const time = Date.now();
if (time <= end_time) {
const progress = (time - start_time) / (end_time - start_time);
const smooth_progress = 0.5 + Math.sin(Math.PI * (progress - 0.5)) / 2;
wrapper.scrollTop = parseFloat(start_scroll + (scroll_to - start_scroll) * smooth_progress);
requestAnimationFrame(animate);
}
else {
wrapper.scrollTop = scroll_to;
resolve();
}
};
requestAnimationFrame(animate);
});
}
_isPosEqual(pos_1, pos_2) {
return (pos_1.x == pos_2.x && pos_1.y == pos_2.y && pos_1.width == pos_2.width && pos_1.height == pos_2.height);
}
_isPosOverlapping(pos_1, pos_2) {
return (
pos_1.x < (pos_2.x + pos_2.width) && (pos_1.x + pos_1.width) > pos_2.x
&& pos_1.y < (pos_2.y + pos_2.height) && (pos_1.y + pos_1.height) > pos_2.y
);
}
_isPosFree(pos) {
for (const widget of this._widgets.keys()) {
if (this._isPosOverlapping(pos, widget.getPos())) {
return false;
}
}
return true;
}
_isDataPosFree(pos, {except_widgets = null} = {}) {
for (const [widget, data] of this._widgets) {
if (except_widgets !== null && except_widgets.has(widget)) {
continue;
}
if (this._isPosOverlapping(data.pos, pos)) {
return false;
}
}
return true;
}
_resizeGrid(min_rows = null) {
if (min_rows === 0) {
this._grid_min_rows = 0;
}
else if (min_rows !== null) {
this._grid_min_rows = Math.max(this._grid_min_rows, Math.min(this._max_rows, min_rows));
}
let num_rows = Math.max(this._grid_min_rows, this._getNumOccupiedRows());
if (!this._is_edit_mode && num_rows === 0) {
this._dashboard_grid.style.height = '';
return;
}
let height = this._cell_height * num_rows;
if (this._is_edit_mode) {
let min_height = window.innerHeight - document.querySelector('.wrapper > footer').clientHeight
- this._dashboard_grid.getBoundingClientRect().y;
let element = this._dashboard_grid;
do {
min_height -= parseFloat(getComputedStyle(element).paddingBottom);
element = element.parentElement;
}
while (!element.classList.contains('wrapper'));
height = Math.min(Math.max(height, min_height), this._cell_height * this._max_rows);
}
this._dashboard_grid.style.height = `${height}px`;
}
_getNumOccupiedRows() {
let num_rows = 0;
for (const widget of this._widgets.keys()) {
const pos = widget.getPos();
num_rows = Math.max(num_rows, pos.y + pos.height);
}
return num_rows;
}
_leaveWidgets({except_widget = null} = {}) {
for (const widget of this._widgets.keys()) {
if (widget !== except_widget) {
widget.leave();
}
}
}
blockInteraction() {
if (this._dashboard_grid.querySelector('.dashboard-grid-widget-blocker') !== null) {
return;
}
const widget_blocker = document.createElement('div');
widget_blocker.classList.add('dashboard-grid-widget-blocker');
this._dashboard_grid.prepend(widget_blocker);
}
unblockInteraction() {
const widget_blocker = this._dashboard_grid.querySelector('.dashboard-grid-widget-blocker');
if (widget_blocker !== null) {
widget_blocker.remove();
}
}
// Widget placeholder methods.
_initWidgetPlaceholder() {
this._widget_placeholder = new CDashboardWidgetPlaceholder(this._cell_width, this._cell_height);
this._widget_placeholder_pos = null;
this._widget_placeholder_clicked_pos = null;
this._widget_placeholder_is_active = false;
this._widget_placeholder_is_edit_mode = null;
this._widget_placeholder_move_animation_frame = null;
this._dashboard_grid.appendChild(this._widget_placeholder.getNode());
const getGridEventPos = (e, {width, height}) => {
const rect = this._dashboard_grid.getBoundingClientRect();
const x = Math.round((e.pageX - rect.x) / rect.width * this._max_columns - width / 2);
const y = Math.round((e.pageY - rect.y) / this._cell_height - height / 2);
return {
x: Math.max(0, Math.min(this._max_columns - width, x)),
y: Math.max(0, Math.min(this._max_rows - height, y)),
width: width,
height: height
}
};
const move = (e) => {
if (this._widget_placeholder_clicked_pos !== null) {
let event_pos = getGridEventPos(e, {width: 1, height: 1});
let reverse_x = false;
let reverse_y = false;
const delta_x = event_pos.x - this._widget_placeholder_clicked_pos.x;
this._widget_placeholder_pos = {};
if (this._widget_placeholder_clicked_pos.width == 2) {
if (delta_x <= 0) {
this._widget_placeholder_clicked_pos.width = 1;
}
else if (delta_x >= 1) {
this._widget_placeholder_clicked_pos.x++;
this._widget_placeholder_clicked_pos.width = 1;
}
}
if (delta_x > 0) {
this._widget_placeholder_pos.x = this._widget_placeholder_clicked_pos.x;
this._widget_placeholder_pos.width = Math.max(
this._widget_placeholder_clicked_pos.width,
event_pos.x - this._widget_placeholder_clicked_pos.x + 1
);
}
else {
this._widget_placeholder_pos.x = event_pos.x;
this._widget_placeholder_pos.width = this._widget_placeholder_clicked_pos.x
- event_pos.x + this._widget_placeholder_clicked_pos.width;
reverse_x = true;
}
if (event_pos.y >= this._widget_placeholder_clicked_pos.y) {
this._widget_placeholder_pos.y = this._widget_placeholder_clicked_pos.y;
this._widget_placeholder_pos.height = Math.max(
this._widget_placeholder_clicked_pos.height,
event_pos.y - this._widget_placeholder_clicked_pos.y + 1
);
this._widget_placeholder_pos.height = Math.min(this._widget_max_rows,
this._widget_placeholder_pos.height
);
}
else {
this._widget_placeholder_pos.y = event_pos.y;
this._widget_placeholder_pos.height = this._widget_placeholder_clicked_pos.y
- event_pos.y + this._widget_placeholder_clicked_pos.height;
reverse_y = true;
const delta_y = this._widget_placeholder_pos.height - this._widget_max_rows;
if (delta_y > 0) {
this._widget_placeholder_pos.y += delta_y;
this._widget_placeholder_pos.height -= delta_y;
}
}
this._widget_placeholder_pos = this.accommodatePos(
this._widget_placeholder_pos, {reverse_x, reverse_y}
);
this._resizeGrid(this._widget_placeholder_pos.y + this._widget_placeholder_pos.height
+ this._grid_pad_rows
);
this._widget_placeholder
.setState(WIDGET_PLACEHOLDER_STATE_RESIZING)
.showAtPosition(this._widget_placeholder_pos);
}
else {
if (this._widget_placeholder_pos !== null && this._widget_placeholder.getNode().contains(e.target)) {
return;
}
if (e.target !== this._dashboard_grid) {
this._widget_placeholder.hide();
return;
}
this._widget_placeholder_pos = null;
const event_pos_1x1 = getGridEventPos(e, {width: 1, height: 1});
if (this._isPosFree(event_pos_1x1)) {
let event_pos = getGridEventPos(e, {width: 2, height: this._widget_min_rows});
for (const width of [2, 1]) {
for (const offset_y of [0, -1, 1]) {
for (const offset_x of [0, -1, 1]) {
const pos = {
x: event_pos.x + offset_x,
y: event_pos.y + offset_y,
width: width,
height: this._widget_min_rows
};
if (pos.x < 0 || pos.x + pos.width > this._max_columns
|| pos.y < 0 || pos.y + pos.height > this._max_rows) {
continue;
}
if (this._isPosOverlapping(pos, event_pos_1x1) && this._isPosFree(pos)) {
this._widget_placeholder_pos = pos;
break;
}
}
if (this._widget_placeholder_pos !== null) {
break;
}
}
if (this._widget_placeholder_pos !== null) {
break;
}
}
}
if (this._widget_placeholder_pos !== null) {
this._resizeGrid(this._widget_placeholder_pos.y + this._widget_placeholder_pos.height
+ this._grid_pad_rows
);
this._widget_placeholder
.setState(WIDGET_PLACEHOLDER_STATE_POSITIONING)
.showAtPosition(this._widget_placeholder_pos);
this._leaveWidgets();
}
else {
this._widget_placeholder.hide();
}
}
};
this._widget_placeholder_events = {
addNewWidget: () => {
if (!this._is_edit_mode) {
this.setEditMode();
this.fire(DASHBOARD_PAGE_EVENT_EDIT);
}
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_ADD_NEW);
},
mouseDown: (e) => {
if (e.button != 0) {
return;
}
if (this._widget_placeholder_pos === null) {
return;
}
e.preventDefault();
this.blockInteraction();
this._widget_placeholder_clicked_pos = this._widget_placeholder_pos;
this._widget_placeholder
.setState(WIDGET_PLACEHOLDER_STATE_RESIZING)
.showAtPosition(this._widget_placeholder_clicked_pos);
document.addEventListener('mouseup', this._widget_placeholder_events.mouseUp);
document.addEventListener('mousemove', this._widget_placeholder_events.mouseMove);
this._dashboard_grid.removeEventListener('mousemove', this._widget_placeholder_events.mouseMove);
},
mouseUp: (e) => {
this._deactivateWidgetPlaceholder({do_hide: false});
this.unblockInteraction();
const new_widget_pos = {...this._widget_placeholder_pos};
if (new_widget_pos.width == 2 && new_widget_pos.height == this._widget_min_rows) {
delete new_widget_pos.width;
delete new_widget_pos.height;
}
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_ADD, {
placeholder: this._widget_placeholder.getNode(),
new_widget_pos,
mouse_event: e
});
},
mouseMove: (e) => {
if (this._widget_placeholder_move_animation_frame !== null) {
cancelAnimationFrame(this._widget_placeholder_move_animation_frame);
}
this._widget_placeholder_move_animation_frame = requestAnimationFrame(() => {
this._widget_placeholder_move_animation_frame = null;
move(e);
});
},
mouseLeave: () => {
if (this._widget_placeholder_move_animation_frame !== null) {
cancelAnimationFrame(this._widget_placeholder_move_animation_frame);
this._widget_placeholder_move_animation_frame = null;
}
if (this._widget_placeholder_clicked_pos === null) {
this.resetWidgetPlaceholder();
}
},
scroll: (e) => {
if (e.target.scrollTop == 0) {
this._resizeGrid(0);
}
}
}
}
resetWidgetPlaceholder() {
if (this._widget_placeholder_is_active && this._widget_placeholder_is_edit_mode != this._is_edit_mode) {
this._deactivateWidgetPlaceholder();
}
if (!this._widget_placeholder_is_active) {
this._activateWidgetPlaceholder();
}
this._widget_placeholder_pos = null;
this._widget_placeholder_clicked_pos = null;
if (this._is_editable && this._widgets.size == 0) {
this._widget_placeholder
.setState(WIDGET_PLACEHOLDER_STATE_ADD_NEW)
.showAtDefaultPosition();
}
else {
this._widget_placeholder.hide();
}
}
_activateWidgetPlaceholder() {
this._widget_placeholder.on(WIDGET_PLACEHOLDER_EVENT_ADD_NEW_WIDGET,
this._widget_placeholder_events.addNewWidget
);
if (this._is_edit_mode) {
this._widget_placeholder.on('mousedown', this._widget_placeholder_events.mouseDown);
this._dashboard_grid.addEventListener('mousemove', this._widget_placeholder_events.mouseMove);
this._dashboard_grid.addEventListener('mouseleave', this._widget_placeholder_events.mouseLeave);
document.querySelector('.wrapper').addEventListener('scroll', this._widget_placeholder_events.scroll);
}
this._widget_placeholder_is_active = true;
this._widget_placeholder_is_edit_mode = this._is_edit_mode;
}
_deactivateWidgetPlaceholder({do_hide = true} = {}) {
this._widget_placeholder.off(WIDGET_PLACEHOLDER_EVENT_ADD_NEW_WIDGET,
this._widget_placeholder_events.addNewWidget
);
if (this._widget_placeholder_is_edit_mode) {
if (this._widget_placeholder_move_animation_frame !== null) {
cancelAnimationFrame(this._widget_placeholder_move_animation_frame);
this._widget_placeholder_move_animation_frame = null;
}
this._widget_placeholder.off('mousedown', this._widget_placeholder_events.mouseDown);
this._dashboard_grid.removeEventListener('mousemove', this._widget_placeholder_events.mouseMove);
this._dashboard_grid.removeEventListener('mouseleave', this._widget_placeholder_events.mouseLeave);
document.querySelector('.wrapper').removeEventListener('scroll', this._widget_placeholder_events.scroll);
document.removeEventListener('mousemove', this._widget_placeholder_events.mouseMove);
document.removeEventListener('mouseup', this._widget_placeholder_events.mouseUp);
}
if (do_hide) {
this._widget_placeholder.hide();
}
this._widget_placeholder_is_active = false;
this._widget_placeholder_is_edit_mode = null;
}
// Widget dragging methods.
_initWidgetDragging() {
const widget_helper = document.createElement('div');
widget_helper.classList.add('dashboard-grid-widget-placeholder');
widget_helper.append(document.createElement('div'));
let move_animation_frame = null;
let drag_widget = null;
let drag_pos = null;
let drag_rel_x = null;
let drag_rel_y = null;
const getGridPos = ({x, y, width, height}) => {
const rect = this._dashboard_grid.getBoundingClientRect();
const pos_x = parseInt(x / rect.width * this._max_columns + 0.5);
const pos_y = parseInt(y / this._cell_height + 0.5);
return {
x: Math.max(0, Math.min(this._max_columns - width, pos_x)),
y: Math.max(0, Math.min(this._max_rows - height, pos_y)),
width,
height
};
};
const showWidgetHelper = (pos) => {
if (widget_helper.parentNode === null) {
this._dashboard_grid.prepend(widget_helper);
}
widget_helper.style.left = `${this._cell_width * pos.x}%`;
widget_helper.style.top = `${this._cell_height * pos.y}px`;
widget_helper.style.width = `${this._cell_width * pos.width}%`;
widget_helper.style.height = `${this._cell_height * pos.height}px`;
};
const hideWidgetHelper = () => {
widget_helper.remove();
};
const pullWidgetsUp = (widgets, max_delta) => {
do {
let widgets_below = [];
for (const widget of widgets) {
const data = this._widgets.get(widget);
for (let y = Math.max(0, data.pos.y - max_delta); y < data.pos.y; y++) {
const test_pos = {...data.pos, y};
if (this._isDataPosFree(test_pos, {except_widgets: new Set([drag_widget, widget])})) {
for (const [w, w_data] of this._widgets) {
if (w === widget || w === drag_widget) {
continue;
}
if (this._isPosOverlapping(w_data.pos, {...data.pos, height: data.pos.height + 1})) {
widgets_below.push(w);
}
}
data.pos = test_pos;
break;
}
}
}
widgets = widgets_below;
}
while (widgets.length > 0);
};
const relocateWidget = (widget, pos) => {
if (pos.y + pos.height > this._max_rows) {
return false;
}
for (const [w, data] of this._widgets) {
if (w === widget || w === drag_widget) {
continue;
}
if (this._isPosOverlapping(data.pos, pos)) {
const test_pos = {...data.pos, y: pos.y + pos.height};
if (!relocateWidget(w, test_pos)) {
return false;
}
data.pos = test_pos;
}
}
return true;
};
const allocatePos = (widget, pos) => {
for (const [w, w_data] of this._widgets) {
w_data.pos = w_data.original_pos;
}
const data = this._widgets.get(widget);
const original_pos = data.original_pos;
let widgets_below = [];
for (const [w, w_data] of this._widgets) {
if (w === widget || w === drag_widget) {
continue;
}
if (this._isPosOverlapping(w_data.pos, {...original_pos, height: original_pos.height + 1})) {
widgets_below.push(w);
}
}
pullWidgetsUp(widgets_below, original_pos.height);
const result = relocateWidget(widget, pos);
for (const [w, w_data] of this._widgets) {
if (result && w !== widget) {
w.setPos(w_data.pos);
}
delete w_data.pos;
}
return result;
};
const move = (x, y) => {
const grid_rect = this._dashboard_grid.getBoundingClientRect();
const widget_pos = drag_widget.getPos();
const widget_view = drag_widget.getView();
const widget_view_rect = widget_view.getBoundingClientRect();
const pos_left = Math.max(0, Math.min(grid_rect.width - widget_view_rect.width, x + drag_rel_x));
const pos_top = Math.max(0, Math.min(grid_rect.height - widget_view_rect.height,
y + drag_rel_y + document.querySelector('.wrapper').scrollTop
));
widget_view.style.left = `${pos_left}px`;
widget_view.style.top = `${pos_top}px`;
const pos = getGridPos({
x: pos_left,
y: pos_top,
width: widget_pos.width,
height: widget_pos.height
});
if (!this._isPosEqual(pos, drag_pos)) {
if (allocatePos(drag_widget, pos)) {
drag_pos = pos;
drag_widget.setPos(drag_pos, {is_managed: true});
showWidgetHelper(drag_pos);
this._resizeGrid(drag_pos.y + drag_pos.height + this._grid_pad_rows);
}
}
};
this._widget_dragging_events = {
mouseDown: (e) => {
if (e.button != 0) {
return;
}
drag_widget = null;
for (const widget of this._widgets.keys()) {
const widget_view = widget.getView();
if (widget_view.querySelector(`.${widget.getCssClass('header')}`).contains(e.target)
&& !widget_view.querySelector(`.${widget.getCssClass('actions')}`).contains(e.target)) {
drag_widget = widget;
break;
}
}
if (drag_widget === null) {
return;
}
e.preventDefault();
this.blockInteraction();
this._deactivateWidgetPlaceholder();
for (const [widget, data] of this._widgets) {
data.original_pos = widget.getPos();
}
drag_widget.setDragging(true);
drag_pos = drag_widget.getPos();
const widget_view = drag_widget.getView();
const widget_view_computed_style = getComputedStyle(widget_view);
drag_rel_x = parseFloat(widget_view_computed_style.left) - e.pageX;
drag_rel_y = parseFloat(widget_view_computed_style.top) - e.pageY
- document.querySelector('.wrapper').scrollTop;
document.addEventListener('mouseup', this._widget_dragging_events.mouseUp, {passive: false});
document.addEventListener('mousemove', this._widget_dragging_events.mouseMove);
this._is_unsaved = true;
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_POSITION);
},
mouseUp: () => {
if (move_animation_frame !== null) {
cancelAnimationFrame(move_animation_frame);
}
drag_widget.setDragging(false);
drag_widget.setPos(drag_pos);
hideWidgetHelper();
move_animation_frame = null;
drag_widget = null;
drag_pos = null;
drag_rel_x = null;
drag_rel_y = null;
for (const data of this._widgets.values()) {
delete data.original_position;
}
document.removeEventListener('mouseup', this._widget_dragging_events.mouseUp);
document.removeEventListener('mousemove', this._widget_dragging_events.mouseMove);
this.unblockInteraction();
this.resetWidgetPlaceholder();
},
mouseMove: (e) => {
if (move_animation_frame !== null) {
cancelAnimationFrame(move_animation_frame);
}
move_animation_frame = requestAnimationFrame(() => {
move_animation_frame = null;
move(e.pageX, e.pageY);
});
}
};
}
_activateWidgetDragging() {
this._dashboard_grid.addEventListener('mousedown', this._widget_dragging_events.mouseDown, {passive: false});
}
_deactivateWidgetDragging() {
this._dashboard_grid.removeEventListener('mousedown', this._widget_dragging_events.mouseDown);
document.removeEventListener('mouseup', this._widget_dragging_events.mouseUp);
document.removeEventListener('mousemove', this._widget_dragging_events.mouseMove);
}
// Widget resizing methods.
_initWidgetResizing() {
let move_animation_frame = null;
let grid_rect = null;
let grid_cell_width = null;
let resize_widget = null;
let resize_sides = null;
let resize_pos = null;
let resize_pos_tested = null;
let resize_dim = null;
let resize_rel_x = null;
let resize_rel_y = null;
const axes_dim = {
x: 'width',
y: 'height'
};
const axes_dim_min = {
x: 1,
y: this._widget_min_rows
};
const axes_max = {
x: this._max_columns,
y: this._max_rows
};
const updateWidgetContainerPosition = () => {
const widget_view = resize_widget.getView();
const widget_view_container = widget_view.querySelector(`.${resize_widget.getCssClass('container')}`);
if (resize_sides.right) {
widget_view_container.style.right = 'auto';
widget_view_container.style.width = `${resize_pos.width * grid_cell_width}px`;
}
else if (resize_sides.left) {
widget_view_container.style.left = 'auto';
widget_view_container.style.width = `${resize_pos.width * grid_cell_width}px`;
}
if (resize_sides.bottom) {
widget_view_container.style.bottom = 'auto';
widget_view_container.style.height = `${resize_pos.height * this._cell_height}px`;
}
else if (resize_sides.top) {
widget_view_container.style.top = 'auto';
widget_view_container.style.height = `${resize_pos.height * this._cell_height}px`;
}
};
const getResizeSteps = (source_pos, target_pos) => {
let resize_steps = [];
for (const axis of ['x', 'y']) {
if (source_pos[axis] != target_pos[axis]) {
const distance = target_pos[axis] - source_pos[axis];
resize_steps.push({
operations: [
{property: axis, direction: Math.sign(distance)},
{property: axes_dim[axis], direction: -Math.sign(distance)},
],
count: Math.abs(distance)
});
}
else if (source_pos[axes_dim[axis]] != target_pos[axes_dim[axis]]) {
const distance = target_pos[axes_dim[axis]] - source_pos[axes_dim[axis]];
resize_steps.push({
operations: [
{property: axes_dim[axis], direction: Math.sign(distance)}
],
count: Math.abs(distance)
});
}
}
return resize_steps;
};
const getRunAwaySpec = (source_pos, target_pos) => {
if (source_pos.x + source_pos.width <= target_pos.x) {
return {axis: 'x', direction: 1};
}
else if (source_pos.x >= target_pos.x + target_pos.width) {
return {axis: 'x', direction: -1};
}
else if (source_pos.y + source_pos.height <= target_pos.y) {
return {axis: 'y', direction: 1};
}
else if (source_pos.y >= target_pos.y + target_pos.height) {
return {axis: 'y', direction: -1};
}
throw new Error('Source position must not overlap with the target position.');
};
const runAway = (widget, axis, direction, {do_squash = true} = {}) => {
const data = this._widgets.get(widget);
data.pos[axis] += direction;
if (data.pos[axis] < 0 || data.pos[axis] + data.pos[axes_dim[axis]] > axes_max[axis]) {
data.pos[axis] -= direction;
if (data.pos[axes_dim[axis]] > axes_dim_min[axis] && do_squash) {
data.pos[axes_dim[axis]]--;
if (direction == 1) {
data.pos[axis]++;
}
return true;
}
return false;
}
const original_positions = new Map();
let overlapping_widgets = [];
for (const [w, w_data] of this._widgets) {
if (w === resize_widget || w === widget) {
continue;
}
original_positions.set(w, {...w_data.pos});
if (this._isPosOverlapping(data.pos, w_data.pos)) {
overlapping_widgets.push(w);
}
}
if (overlapping_widgets.length == 0) {
return true;
}
let has_ran_away = true;
for (const w of overlapping_widgets) {
if (!runAway(w, axis, direction, {do_squash: false})) {
has_ran_away = false;
break;
}
}
if (has_ran_away) {
return true;
}
for (const [w, w_data] of this._widgets) {
if (w === resize_widget || w === widget) {
continue;
}
w_data.pos = {...original_positions.get(w)};
}
if (!do_squash) {
return false;
}
if (data.pos[axes_dim[axis]] > axes_dim_min[axis]) {
data.pos[axes_dim[axis]]--;
if (direction == -1) {
data.pos[axis]++;
}
return true;
}
has_ran_away = true;
for (const w of overlapping_widgets) {
if (!runAway(w, axis, direction, {do_squash: true})) {
has_ran_away = false;
break;
}
}
if (has_ran_away) {
return true;
}
for (const [w, w_data] of this._widgets) {
if (w === resize_widget || w === widget) {
continue;
}
w_data.pos = {...original_positions.get(w)};
}
return false;
};
const runBack = () => {
const relocated_widgets = new Map();
for (const [w, w_data] of this._widgets) {
if (w === resize_widget) {
continue;
}
if (!this._isPosEqual(w_data.pos, w_data.original_pos)) {
relocated_widgets.set(w, w_data);
}
}
let has_ran_back;
do {
has_ran_back = false;
for (const [w, w_data] of relocated_widgets) {
const pos = {...w_data.pos};
pos.x += Math.sign(w_data.original_pos.x - pos.x);
if (pos.width < w_data.original_pos.width) {
pos.width++;
}
pos.y += Math.sign(w_data.original_pos.y - pos.y);
if (pos.height < w_data.original_pos.height) {
pos.height++;
}
if (!this._isPosEqual(pos, w_data.pos)) {
if (this._isDataPosFree(pos, {except_widgets: new Set([w])})) {
w_data.pos = pos;
has_ran_back = true;
}
}
}
}
while (has_ran_back);
};
const resize = (target_resize_pos) => {
if (this._isPosEqual(target_resize_pos, resize_pos)
|| this._isPosEqual(target_resize_pos, resize_pos_tested)) {
return false;
}
resize_pos_tested = target_resize_pos;
let best_resize_pos = resize_pos;
for (const [w, w_data] of this._widgets) {
if (w !== resize_widget) {
w_data.best_pos = {...w.getPos()};
w_data.pos = {...w_data.best_pos};
}
}
for (const resize_step of getResizeSteps(resize_pos, target_resize_pos)) {
for (let i = 0; i < resize_step.count; i++) {
const step_resize_pos = {...best_resize_pos};
for (const operation of resize_step.operations) {
step_resize_pos[operation.property] += operation.direction;
}
let can_relocate = true;
for (const [w, w_data] of this._widgets) {
if (w === resize_widget) {
continue;
}
if (this._isPosOverlapping(step_resize_pos, w_data.pos)) {
const run_away_spec = getRunAwaySpec(best_resize_pos, w_data.pos);
can_relocate = runAway(w, run_away_spec.axis, run_away_spec.direction);
if (!can_relocate) {
break;
}
}
}
if (!can_relocate) {
for (const [w, w_data] of this._widgets) {
if (w !== resize_widget) {
w_data.pos = {...w_data.best_pos};
}
}
break;
}
best_resize_pos = step_resize_pos;
for (const [w, w_data] of this._widgets) {
if (w !== resize_widget) {
w_data.best_pos = {...w_data.pos};
}
}
}
}
const can_relocate = !this._isPosEqual(best_resize_pos, resize_pos);
if (can_relocate) {
resize_pos = best_resize_pos;
resize_pos_tested = best_resize_pos;
this._widgets.get(resize_widget).pos = {...best_resize_pos};
for (const [w, w_data] of this._widgets) {
if (w !== resize_widget) {
w_data.pos = w_data.best_pos;
}
}
runBack();
}
delete this._widgets.get(resize_widget).pos;
for (const [w, w_data] of this._widgets) {
if (w === resize_widget) {
continue;
}
if (can_relocate) {
const pos = w.getPos();
w.setPos(w_data.pos);
if (w_data.pos.width != pos.width || w_data.pos.height != pos.height) {
w.resize();
}
}
delete w_data.best_pos;
delete w_data.pos;
}
return can_relocate;
};
const move = (x, y) => {
const rel_x = x - resize_rel_x;
const rel_y = y - resize_rel_y + document.querySelector('.wrapper').scrollTop;
const dim = {...resize_dim};
if (resize_sides.right) {
dim.x = resize_dim.x;
dim.width = Math.max(
grid_cell_width,
Math.min(grid_rect.width - dim.x, resize_dim.width + rel_x)
);
}
else if (resize_sides.left) {
dim.x = Math.max(
0,
Math.min(
resize_dim.x + resize_dim.width - grid_cell_width,
resize_dim.x + rel_x
)
);
dim.width = resize_dim.width + resize_dim.x - dim.x;
}
if (resize_sides.bottom) {
dim.y = resize_dim.y;
dim.height = Math.max(
this._cell_height * this._widget_min_rows,
Math.min(
this._cell_height * this._max_rows - dim.y,
this._cell_height * this._widget_max_rows,
resize_dim.height + rel_y
)
);
}
else if (resize_sides.top) {
dim.y = Math.max(
0,
Math.min(
resize_dim.y + resize_dim.height - this._cell_height * this._widget_min_rows,
resize_dim.y + rel_y
),
resize_dim.y + resize_dim.height - this._cell_height * this._widget_max_rows
);
dim.height = resize_dim.height + resize_dim.y - dim.y;
}
const resize_pos_width = Math.floor((dim.width + 0.49) / grid_cell_width);
const resize_pos_height = Math.floor((dim.height + 0.49) / this._cell_height);
const target_resize_pos = {
x: resize_sides.left ? resize_pos.x + resize_pos.width - resize_pos_width : resize_pos.x,
y: resize_sides.top ? resize_pos.y + resize_pos.height - resize_pos_height : resize_pos.y,
width: resize_pos_width,
height: resize_pos_height
};
if (resize(target_resize_pos)) {
updateWidgetContainerPosition();
resize_widget.setPos(resize_pos, {is_managed: true});
resize_widget.resize();
this._resizeGrid(resize_pos.y + resize_pos.height + this._grid_pad_rows);
}
const widget_view = resize_widget.getView();
widget_view.style.left = `${dim.x}px`;
widget_view.style.top = `${dim.y}px`;
widget_view.style.width = `${dim.width}px`;
widget_view.style.height = `${dim.height}px`;
};
this._widget_resizing_events = {
mouseDown: (e) => {
if (e.button != 0) {
return;
}
resize_widget = null;
for (const widget of this._widgets.keys()) {
const widget_view = widget.getView();
if (widget_view.contains(e.target)) {
const resize_handle = e.target.closest(`.${widget.getCssClass('resize_handle')}`);
if (resize_handle === null) {
return false;
}
resize_widget = widget;
resize_sides = resize_widget.getResizeHandleSides(resize_handle);
break;
}
}
if (resize_widget === null) {
return;
}
e.preventDefault();
this.blockInteraction();
this._deactivateWidgetPlaceholder();
for (const [widget, data] of this._widgets) {
data.original_pos = widget.getPos();
}
grid_rect = this._dashboard_grid.getBoundingClientRect();
grid_cell_width = grid_rect.width * this._cell_width / 100;
resize_widget.setResizing(true);
resize_pos = resize_widget.getPos();
resize_pos_tested = resize_pos;
updateWidgetContainerPosition();
const widget_view = resize_widget.getView();
const widget_view_computed_style = getComputedStyle(widget_view);
resize_rel_x = e.pageX;
resize_rel_y = e.pageY + document.querySelector('.wrapper').scrollTop;
resize_dim = {
x: parseFloat(widget_view_computed_style.left),
y: parseFloat(widget_view_computed_style.top),
width: parseFloat(widget_view_computed_style.width),
height: parseFloat(widget_view_computed_style.height)
};
document.addEventListener('mouseup', this._widget_resizing_events.mouseUp, {passive: false});
document.addEventListener('mousemove', this._widget_resizing_events.mouseMove);
this._is_unsaved = true;
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_POSITION);
},
mouseUp: () => {
if (move_animation_frame !== null) {
cancelAnimationFrame(move_animation_frame);
}
resize_widget.setResizing(false);
resize_widget.setPos(resize_pos);
const widget_view = resize_widget.getView();
const widget_view_container = widget_view.querySelector(`.${resize_widget.getCssClass('container')}`);
widget_view_container.style.top = null;
widget_view_container.style.left = null;
widget_view_container.style.right = null;
widget_view_container.style.bottom = null;
widget_view_container.style.width = null;
widget_view_container.style.height = null;
move_animation_frame = null;
grid_rect = null;
grid_cell_width = null;
resize_widget = null;
resize_sides = null;
resize_pos = null;
resize_pos_tested = null;
resize_dim = null;
resize_rel_x = null;
resize_rel_y = null;
for (const data of this._widgets.values()) {
delete data.original_position;
}
document.removeEventListener('mouseup', this._widget_resizing_events.mouseUp);
document.removeEventListener('mousemove', this._widget_resizing_events.mouseMove);
this.unblockInteraction();
this.resetWidgetPlaceholder();
},
mouseMove: (e) => {
if (move_animation_frame !== null) {
cancelAnimationFrame(move_animation_frame);
}
move_animation_frame = requestAnimationFrame(() => {
move_animation_frame = null;
move(e.pageX, e.pageY);
});
}
};
}
_activateWidgetResizing() {
this._dashboard_grid.addEventListener('mousedown', this._widget_resizing_events.mouseDown, {passive: false});
}
_deactivateWidgetResizing() {
this._dashboard_grid.removeEventListener('mousedown', this._widget_resizing_events.mouseDown);
document.removeEventListener('mouseup', this._widget_resizing_events.mouseUp);
document.removeEventListener('mousemove', this._widget_resizing_events.mouseMove);
}
// Internal events management methods.
_registerEvents() {
this._events = {
widgetActions: (e) => {
const widget = e.detail.target;
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_ACTIONS, {
widget,
mouse_event: e.detail.mouse_event
});
},
widgetEdit: (e) => {
const widget = e.detail.target;
if (!this._is_edit_mode) {
this.setEditMode();
this.fire(DASHBOARD_PAGE_EVENT_EDIT);
}
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_EDIT, {widget});
},
widgetEnter: (e) => {
const widget = e.detail.target;
if (this._is_edit_mode) {
const pos = widget.getPos();
this._resizeGrid(pos.y + pos.height + this._grid_pad_rows);
this.resetWidgetPlaceholder();
}
if (!widget.isEntered()) {
widget.enter();
this._leaveWidgets({except_widget: widget});
}
if (widget.getPos().y == 0) {
const num_lines = widget.getNumHeaderLines();
if (num_lines != this._events_data.last_num_reserved_header_lines) {
this._events_data.last_num_reserved_header_lines = num_lines;
this.fire(DASHBOARD_PAGE_EVENT_RESERVE_HEADER_LINES, {num_lines});
}
}
},
widgetLeave: (e) => {
const widget = e.detail.target;
if (widget.isEntered()) {
widget.leave();
}
if (this._events_data.last_num_reserved_header_lines > 0) {
this._events_data.last_num_reserved_header_lines = 0;
this.fire(DASHBOARD_PAGE_EVENT_RESERVE_HEADER_LINES, {num_lines: 0});
}
},
widgetCopy: (e) => {
const widget = e.detail.target;
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_COPY, {widget});
},
widgetPaste: (e) => {
const widget = e.detail.target;
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_PASTE, {widget});
},
widgetDelete: (e) => {
const widget = e.detail.target;
this.deleteWidget(widget);
this.fire(DASHBOARD_PAGE_EVENT_WIDGET_DELETE);
},
dashboardGridResize: () => {
if (this._events_data.dashboard_grid_resize_first_time) {
this._events_data.dashboard_grid_resize_first_time = false;
this._events_data.dashboard_grid_resize_width = this._dashboard_grid.clientWidth;
return;
}
if (this._dashboard_grid.clientWidth == this._events_data.dashboard_grid_resize_width) {
return;
}
this._events_data.dashboard_grid_resize_width = this._dashboard_grid.clientWidth;
if (this._events_data.dashboard_grid_resize_timeout_id != null) {
clearTimeout(this._events_data.dashboard_grid_resize_timeout_id);
}
this._events_data.dashboard_grid_resize_timeout_id = setTimeout(() => {
this._events_data.dashboard_grid_resize_timeout_id = null;
this._resizeGrid();
if (this._is_edit_mode) {
this._widget_placeholder.resize();
}
for (const widget of this._widgets.keys()) {
widget.resize();
}
}, 200);
}
};
this._events_data = {
last_num_reserved_header_lines: 0,
dashboard_grid_resize_timeout_id: null,
dashboard_grid_resize_first_time: true,
dashboard_grid_resize_width: null
};
}
_activateEvents() {
this._events_data.dashboard_grid_resize_observer = new ResizeObserver(this._events.dashboardGridResize);
this._events_data.dashboard_grid_resize_observer.observe(this._dashboard_grid);
}
_deactivateEvents() {
this._events_data.dashboard_grid_resize_observer.disconnect();
if (this._events_data.dashboard_grid_resize_timeout_id != null) {
clearTimeout(this._events_data.dashboard_grid_resize_timeout_id);
}
}
/**
* Attach event listener to dashboard page events.
*
* @param {string} type
* @param {function} listener
* @param {Object|false} options
*
* @returns {CDashboardPage}
*/
on(type, listener, options = false) {
this._target.addEventListener(type, listener, options);
return this;
}
/**
* Detach event listener from dashboard page events.
*
* @param {string} type
* @param {function} listener
* @param {Object|false} options
*
* @returns {CDashboardPage}
*/
off(type, listener, options = false) {
this._target.removeEventListener(type, listener, options);
return this;
}
/**
* Dispatch dashboard page event.
*
* @param {string} type
* @param {Object} detail
* @param {Object} options
*
* @returns {boolean}
*/
fire(type, detail = {}, options = {}) {
return this._target.dispatchEvent(new CustomEvent(type, {...options, detail: {target: this, ...detail}}));
}
}