diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
index 62ca4db37c..a76f991f46 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
@@ -48,15 +48,16 @@
[stateController]="dashboardCtx.stateController"
[dashboardTimewindow]="dashboardCtx.dashboardTimewindow"
[isEdit]="isEdit"
+ [isEditingWidget]="isEditingWidget"
[autofillHeight]="autoFillHeight"
[mobileAutofillHeight]="mobileAutoFillHeight"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
[isMobile]="isMobile"
[isMobileDisabled]="isMobileDisabled"
[disableWidgetInteraction]="isEdit"
- [isEditActionEnabled]="isEdit"
- [isExportActionEnabled]="isEdit && !widgetEditMode"
- [isRemoveActionEnabled]="isEdit && !widgetEditMode"
+ [isEditActionEnabled]="isEdit && !isEditingWidget"
+ [isExportActionEnabled]="isEdit && !widgetEditMode && !isEditingWidget"
+ [isRemoveActionEnabled]="isEdit && !widgetEditMode && !isEditingWidget"
[callbacks]="this"
[ignoreLoading]="layoutCtx.ignoreLoading"
[parentDashboard]="parentDashboard"
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
index 8d297bd34a..6856e458d6 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
+++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
@@ -73,6 +73,7 @@
[dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage"
[isEdit]="isEdit"
+ [isEditingWidget]="isEditingWidget"
[isPreview]="isPreview"
[isMobile]="isMobileSize"
[isEditActionEnabled]="isEditActionEnabled"
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
index 60fa74e09b..8b51d691a5 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
+++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
@@ -117,6 +117,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
isEdit: boolean;
+ @Input()
+ isEditingWidget: boolean;
+
@Input()
isPreview: boolean;
@@ -248,8 +251,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemCols: 8,
defaultItemRows: 6,
displayGrid: this.displayGrid,
- resizable: {enabled: this.isEdit},
- draggable: {enabled: this.isEdit},
+ resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
+ draggable: {enabled: this.isEdit && !this.isEditingWidget},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
@@ -300,7 +303,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
updateMobileOpts = true;
} else if (['outerMargin', 'margin', 'columns'].includes(propName)) {
updateLayoutOpts = true;
- } else if (propName === 'isEdit') {
+ } else if (['isEdit', 'isEditingWidget'].includes(propName)) {
updateEditingOpts = true;
} else if (['widgets', 'widgetLayouts'].includes(propName)) {
updateWidgets = true;
@@ -580,8 +583,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
}
private updateEditingOpts() {
- this.gridsterOpts.resizable.enabled = this.isEdit;
- this.gridsterOpts.draggable.enabled = this.isEdit;
+ this.gridsterOpts.resizable.enabled = this.isEdit && !this.isEditingWidget;
+ this.gridsterOpts.draggable.enabled = this.isEdit && !this.isEditingWidget;
}
public notifyGridsterOptionsChanged() {
diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index 5390a915d3..f1a4623511 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts
@@ -124,7 +124,10 @@ import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-dow
import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
import { AlarmDurationPredicateValueComponent } from '@home/components/profile/alarm/alarm-duration-predicate-value.component';
import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
-import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
+import {
+ EditWidgetActionsTooltipComponent,
+ WidgetContainerComponent
+} from '@home/components/widget/widget-container.component';
import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snmp/snmp-device-profile-transport.module';
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module';
@@ -207,6 +210,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
EntityAliasDialogComponent,
DashboardComponent,
WidgetContainerComponent,
+ EditWidgetActionsTooltipComponent,
WidgetComponent,
WidgetConfigComponent,
WidgetPreviewComponent,
@@ -345,6 +349,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
EntityAliasDialogComponent,
DashboardComponent,
WidgetContainerComponent,
+ EditWidgetActionsTooltipComponent,
WidgetComponent,
WidgetConfigComponent,
WidgetPreviewComponent,
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
index 7235b300bc..ee0780b11d 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
@@ -26,12 +26,10 @@
'mat-elevation-z4': widget.dropShadow,
'tb-overflow-visible': widgetComponent.widgetContext?.overflowVisible,
'tb-has-timewindow': widget.hasTimewindow,
- 'tb-edit': isEdit
+ 'tb-edit': isEdit || isEditingWidget,
+ 'tb-hover': hovered
}"
- [ngStyle]="widget.style"
- (mousedown)="onMouseDown($event)"
- (click)="onClicked($event)"
- (contextmenu)="onContextMenu($event)">
+ [ngStyle]="widget.style">
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
index 3143ab1ee2..7b6d8e59f0 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
@@ -102,6 +102,12 @@ div.tb-widget {
padding: 0 !important;
margin: 0 !important;
line-height: 20px;
+ &.mat-mdc-button-base {
+ .mat-mdc-button-touch-target {
+ height: 32px;
+ width: 32px;
+ }
+ }
.mat-icon {
width: 20px;
@@ -141,7 +147,69 @@ div.tb-widget {
opacity: .5;
}
+ &.tb-hover {
+ &:not(.tb-highlighted) {
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+ }
+ }
+
&.tb-edit {
cursor: pointer;
}
}
+
+gridster-item:hover {
+ .tb-widget-container {
+ div.tb-widget {
+ &.tb-edit {
+ &:not(.tb-highlighted) {
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+ }
+ }
+ }
+ }
+}
+
+.tooltipster-sidetip.tb-widget-edit-actions-tooltip {
+ .tooltipster-box {
+ user-select: none;
+ background: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.38);
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
+ .tooltipster-content {
+ padding: 4px 8px;
+ font-size: 12px;
+ line-height: 12px;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.76);
+ .tb-widget-actions-panel {
+ display: flex;
+ flex-direction: row;
+ place-content: center flex-start;
+ align-items: center;
+ gap: 8px;
+ }
+ }
+ }
+ .tooltipster-arrow {
+ .tooltipster-arrow-uncropped {
+ .tooltipster-arrow-background {
+ border-width: 12px;
+ }
+ }
+ }
+ &.tooltipster-top {
+ .tooltipster-arrow {
+ bottom: -1px;
+ .tooltipster-arrow-uncropped {
+ .tooltipster-arrow-border {
+ border-top-color: rgba(0, 0, 0, 0.38);
+ }
+ .tooltipster-arrow-background {
+ border-top-color: #fff;
+ left: -2px;
+ }
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
index 81c206391a..1546c4f92a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
@@ -22,12 +22,12 @@ import {
ElementRef,
EventEmitter,
HostBinding,
- Input,
+ Input, OnChanges,
OnDestroy,
OnInit,
Output,
- Renderer2,
- ViewChild,
+ Renderer2, SimpleChanges,
+ ViewChild, ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
@@ -38,6 +38,8 @@ import { SafeStyle } from '@angular/platform-browser';
import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service';
+import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
+import { from } from 'rxjs';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@@ -61,7 +63,7 @@ export class WidgetComponentAction {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class WidgetContainerComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
+export class WidgetContainerComponent extends PageComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@HostBinding('class')
widgetContainerClass = 'tb-widget-container';
@@ -84,6 +86,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
@Input()
isEdit: boolean;
+ @Input()
+ isEditingWidget: boolean;
+
@Input()
isPreview: boolean;
@@ -111,11 +116,20 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
@Output()
widgetComponentAction: EventEmitter
= new EventEmitter();
+ hovered = false;
+
+ get widgetEditActionsEnabled(): boolean {
+ return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen;
+ }
+
private cssClass: string;
+ private editWidgetActionsTooltip: ITooltipsterInstance;
+
constructor(protected store: Store,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
+ private container: ViewContainerRef,
private utils: UtilsService) {
super(store);
}
@@ -127,16 +141,34 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
this.cssClass =
this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString);
}
+ $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
+ $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
+ $(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
+ this.initEditWidgetActionTooltip();
}
ngAfterViewInit(): void {
this.widget.widgetContext.$widgetElement = $(this.tbWidgetElement.nativeElement);
}
+ ngOnChanges(changes: SimpleChanges) {
+ for (const propName of Object.keys(changes)) {
+ const change = changes[propName];
+ if (!change.firstChange && change.currentValue !== change.previousValue) {
+ if (['isEditActionEnabled', 'isRemoveActionEnabled', 'isExportActionEnabled'].includes(propName)) {
+ this.updateEditWidgetActionsTooltipState();
+ }
+ }
+ }
+ }
+
ngOnDestroy(): void {
if (this.cssClass) {
this.utils.clearCssElement(this.renderer, this.cssClass);
}
+ if (this.editWidgetActionsTooltip) {
+ this.editWidgetActionsTooltip.destroy();
+ }
}
isHighlighted(widget: DashboardWidget) {
@@ -198,4 +230,141 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
});
}
+ updateEditWidgetActionsTooltipState() {
+ if (this.editWidgetActionsTooltip) {
+ if (this.widgetEditActionsEnabled) {
+ this.editWidgetActionsTooltip.enable();
+ } else {
+ this.editWidgetActionsTooltip.disable();
+ }
+ }
+ }
+
+ private initEditWidgetActionTooltip() {
+ from(import('tooltipster')).subscribe(() => {
+ $(this.gridsterItem.el).tooltipster({
+ delay: this.widget.selected ? [0, 10000000] : [0, 100],
+ distance: 2,
+ zIndex: 151,
+ arrow: false,
+ theme: ['tb-widget-edit-actions-tooltip'],
+ interactive: true,
+ trigger: 'custom',
+ triggerOpen: {
+ mouseenter: true
+ },
+ triggerClose: {
+ mouseleave: true
+ },
+ side: ['top'],
+ trackOrigin: true,
+ trackerInterval: 25,
+ content: '',
+ functionPosition: (instance, helper, position) => {
+ const clientRect = helper.origin.getBoundingClientRect();
+ position.coord.left = clientRect.right - position.size.width;
+ position.target = clientRect.right;
+ return position;
+ },
+ functionReady: (_instance, helper) => {
+ const tooltipEl = $(helper.tooltip);
+ tooltipEl.on('mouseenter', () => {
+ this.hovered = true;
+ this.cd.markForCheck();
+ });
+ tooltipEl.on('mouseleave', () => {
+ this.hovered = false;
+ this.cd.markForCheck();
+ });
+ },
+ functionAfter: () => {
+ this.hovered = false;
+ this.cd.markForCheck();
+ }
+ });
+ this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
+ const componentRef = this.container.createComponent(EditWidgetActionsTooltipComponent);
+ componentRef.instance.container = this;
+ componentRef.instance.viewInited.subscribe(() => {
+ if (this.editWidgetActionsTooltip.status().open) {
+ this.editWidgetActionsTooltip.reposition();
+ }
+ });
+ this.editWidgetActionsTooltip.on('destroyed', () => {
+ componentRef.destroy();
+ });
+ const parentElement = componentRef.instance.element.nativeElement;
+ const content = parentElement.firstChild;
+ parentElement.removeChild(content);
+ parentElement.style.display = 'none';
+ this.editWidgetActionsTooltip.content(content);
+ this.updateEditWidgetActionsTooltipState();
+ this.widget.onSelected((selected) =>
+ this.updateEditWidgetActionsTooltipSelectedState(selected));
+ });
+ }
+
+ private updateEditWidgetActionsTooltipSelectedState(selected: boolean) {
+ if (this.editWidgetActionsTooltip) {
+ if (selected) {
+ this.editWidgetActionsTooltip.option('delay', [0, 10000000]);
+ this.editWidgetActionsTooltip.option('triggerClose', {
+ mouseleave: false
+ });
+ if (this.widgetEditActionsEnabled) {
+ this.editWidgetActionsTooltip.open();
+ }
+ } else {
+ this.editWidgetActionsTooltip.option('delay', [0, 100]);
+ this.editWidgetActionsTooltip.option('triggerClose', {
+ mouseleave: true
+ });
+ this.editWidgetActionsTooltip.close();
+ }
+ }
+ }
+
+}
+
+@Component({
+ template: `
+
+
+
+
`,
+ styles: [],
+ encapsulation: ViewEncapsulation.None
+})
+export class EditWidgetActionsTooltipComponent implements AfterViewInit {
+
+ @Input()
+ container: WidgetContainerComponent;
+
+ @Output()
+ viewInited = new EventEmitter();
+
+ constructor(public element: ElementRef) {
+ }
+
+ ngAfterViewInit() {
+ this.viewInited.emit();
+ }
}
diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
index d74373f734..f05fa0ac3a 100644
--- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
+++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
@@ -240,11 +240,20 @@ export class DashboardWidgets implements Iterable {
highlightWidget(widgetId: string): DashboardWidget {
const widget = this.findWidgetById(widgetId);
if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) {
- this.highlightedMode = true;
+ let detectChanges = false;
+ if (!this.highlightedMode) {
+ this.highlightedMode = true;
+ detectChanges = true;
+ }
widget.highlighted = true;
+ widget.selected = false;
this.dashboardWidgets.forEach((dashboardWidget) => {
if (dashboardWidget !== widget) {
dashboardWidget.highlighted = false;
+ dashboardWidget.selected = false;
+ if (detectChanges) {
+ dashboardWidget.widgetContext?.detectContainerChanges();
+ }
}
});
return widget;
@@ -330,6 +339,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private highlightedValue = false;
private selectedValue = false;
+ private selectedCallback: (selected: boolean) => void = () => {};
isFullscreen = false;
@@ -399,6 +409,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
}
}
+ onSelected(selectedCallback: (selected: boolean) => void) {
+ this.selectedCallback = selectedCallback;
+ }
+
get selected() {
return this.selectedValue;
}
@@ -406,6 +420,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
set selected(selected: boolean) {
if (this.selectedValue !== selected) {
this.selectedValue = selected;
+ this.selectedCallback(selected);
this.widgetContext.detectContainerChanges();
}
}