UI: Improve edit widget toolbar.

This commit is contained in:
Igor Kulikov 2024-06-13 20:09:59 +03:00
parent e84447d28e
commit 9bd923a878
8 changed files with 280 additions and 41 deletions

View File

@ -48,15 +48,16 @@
[stateController]="dashboardCtx.stateController" [stateController]="dashboardCtx.stateController"
[dashboardTimewindow]="dashboardCtx.dashboardTimewindow" [dashboardTimewindow]="dashboardCtx.dashboardTimewindow"
[isEdit]="isEdit" [isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[autofillHeight]="autoFillHeight" [autofillHeight]="autoFillHeight"
[mobileAutofillHeight]="mobileAutoFillHeight" [mobileAutofillHeight]="mobileAutoFillHeight"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight" [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
[isMobile]="isMobile" [isMobile]="isMobile"
[isMobileDisabled]="isMobileDisabled" [isMobileDisabled]="isMobileDisabled"
[disableWidgetInteraction]="isEdit" [disableWidgetInteraction]="isEdit"
[isEditActionEnabled]="isEdit" [isEditActionEnabled]="isEdit && !isEditingWidget"
[isExportActionEnabled]="isEdit && !widgetEditMode" [isExportActionEnabled]="isEdit && !widgetEditMode && !isEditingWidget"
[isRemoveActionEnabled]="isEdit && !widgetEditMode" [isRemoveActionEnabled]="isEdit && !widgetEditMode && !isEditingWidget"
[callbacks]="this" [callbacks]="this"
[ignoreLoading]="layoutCtx.ignoreLoading" [ignoreLoading]="layoutCtx.ignoreLoading"
[parentDashboard]="parentDashboard" [parentDashboard]="parentDashboard"

View File

@ -73,6 +73,7 @@
[dashboardStyle]="dashboardStyle" [dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage" [backgroundImage]="backgroundImage"
[isEdit]="isEdit" [isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isPreview]="isPreview" [isPreview]="isPreview"
[isMobile]="isMobileSize" [isMobile]="isMobileSize"
[isEditActionEnabled]="isEditActionEnabled" [isEditActionEnabled]="isEditActionEnabled"

View File

@ -117,6 +117,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input() @Input()
isEdit: boolean; isEdit: boolean;
@Input()
isEditingWidget: boolean;
@Input() @Input()
isPreview: boolean; isPreview: boolean;
@ -248,8 +251,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemCols: 8, defaultItemCols: 8,
defaultItemRows: 6, defaultItemRows: 6,
displayGrid: this.displayGrid, displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit}, resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
draggable: {enabled: this.isEdit}, draggable: {enabled: this.isEdit && !this.isEditingWidget},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => { itemInitCallback: (item, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
@ -300,7 +303,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
updateMobileOpts = true; updateMobileOpts = true;
} else if (['outerMargin', 'margin', 'columns'].includes(propName)) { } else if (['outerMargin', 'margin', 'columns'].includes(propName)) {
updateLayoutOpts = true; updateLayoutOpts = true;
} else if (propName === 'isEdit') { } else if (['isEdit', 'isEditingWidget'].includes(propName)) {
updateEditingOpts = true; updateEditingOpts = true;
} else if (['widgets', 'widgetLayouts'].includes(propName)) { } else if (['widgets', 'widgetLayouts'].includes(propName)) {
updateWidgets = true; updateWidgets = true;
@ -580,8 +583,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} }
private updateEditingOpts() { private updateEditingOpts() {
this.gridsterOpts.resizable.enabled = this.isEdit; this.gridsterOpts.resizable.enabled = this.isEdit && !this.isEditingWidget;
this.gridsterOpts.draggable.enabled = this.isEdit; this.gridsterOpts.draggable.enabled = this.isEdit && !this.isEditingWidget;
} }
public notifyGridsterOptionsChanged() { public notifyGridsterOptionsChanged() {

View File

@ -124,7 +124,10 @@ import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-dow
import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component'; import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
import { AlarmDurationPredicateValueComponent } from '@home/components/profile/alarm/alarm-duration-predicate-value.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 { 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 { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snmp/snmp-device-profile-transport.module';
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module'; import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.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, EntityAliasDialogComponent,
DashboardComponent, DashboardComponent,
WidgetContainerComponent, WidgetContainerComponent,
EditWidgetActionsTooltipComponent,
WidgetComponent, WidgetComponent,
WidgetConfigComponent, WidgetConfigComponent,
WidgetPreviewComponent, WidgetPreviewComponent,
@ -345,6 +349,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
EntityAliasDialogComponent, EntityAliasDialogComponent,
DashboardComponent, DashboardComponent,
WidgetContainerComponent, WidgetContainerComponent,
EditWidgetActionsTooltipComponent,
WidgetComponent, WidgetComponent,
WidgetConfigComponent, WidgetConfigComponent,
WidgetPreviewComponent, WidgetPreviewComponent,

View File

@ -26,12 +26,10 @@
'mat-elevation-z4': widget.dropShadow, 'mat-elevation-z4': widget.dropShadow,
'tb-overflow-visible': widgetComponent.widgetContext?.overflowVisible, 'tb-overflow-visible': widgetComponent.widgetContext?.overflowVisible,
'tb-has-timewindow': widget.hasTimewindow, 'tb-has-timewindow': widget.hasTimewindow,
'tb-edit': isEdit 'tb-edit': isEdit || isEditingWidget,
'tb-hover': hovered
}" }"
[ngStyle]="widget.style" [ngStyle]="widget.style">
(mousedown)="onMouseDown($event)"
(click)="onClicked($event)"
(contextmenu)="onContextMenu($event)">
<div *ngIf="!!widgetComponent.widgetContext?.inited" <div *ngIf="!!widgetComponent.widgetContext?.inited"
class="tb-widget-header"> class="tb-widget-header">
<ng-container *ngIf="!widgetComponent.widgetContext?.embedTitlePanel"> <ng-container *ngIf="!widgetComponent.widgetContext?.embedTitlePanel">
@ -57,32 +55,11 @@
</button> </button>
<button mat-icon-button <button mat-icon-button
[fxShow]="!isEdit && widget.enableFullscreen" [fxShow]="!isEdit && widget.enableFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen" (click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen; updateEditWidgetActionsTooltipState()"
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"> matTooltipPosition="above">
<tb-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</tb-icon> <tb-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</tb-icon>
</button> </button>
<button mat-icon-button
[fxShow]="isEditActionEnabled && !widget.isFullscreen"
(click)="onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
[fxShow]="isExportActionEnabled && !widget.isFullscreen"
(click)="onExport($event)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<tb-icon>file_download</tb-icon>
</button>
<button mat-icon-button
[fxShow]="isRemoveActionEnabled && !widget.isFullscreen"
(click)="onRemove($event);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<tb-icon>close</tb-icon>
</button>
</div> </div>
</div> </div>
<div class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}"> <div class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">

View File

@ -102,6 +102,12 @@ div.tb-widget {
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
line-height: 20px; line-height: 20px;
&.mat-mdc-button-base {
.mat-mdc-button-touch-target {
height: 32px;
width: 32px;
}
}
.mat-icon { .mat-icon {
width: 20px; width: 20px;
@ -141,7 +147,69 @@ div.tb-widget {
opacity: .5; opacity: .5;
} }
&.tb-hover {
&:not(.tb-highlighted) {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
}
&.tb-edit { &.tb-edit {
cursor: pointer; 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;
}
}
}
}
}

View File

@ -22,12 +22,12 @@ import {
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Input, Input, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
Renderer2, Renderer2, SimpleChanges,
ViewChild, ViewChild, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -38,6 +38,8 @@ import { SafeStyle } from '@angular/platform-browser';
import { isNotEmptyStr } from '@core/utils'; import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2'; import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs';
export enum WidgetComponentActionType { export enum WidgetComponentActionType {
MOUSE_DOWN, MOUSE_DOWN,
@ -61,7 +63,7 @@ export class WidgetComponentAction {
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class WidgetContainerComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { export class WidgetContainerComponent extends PageComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@HostBinding('class') @HostBinding('class')
widgetContainerClass = 'tb-widget-container'; widgetContainerClass = 'tb-widget-container';
@ -84,6 +86,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
@Input() @Input()
isEdit: boolean; isEdit: boolean;
@Input()
isEditingWidget: boolean;
@Input() @Input()
isPreview: boolean; isPreview: boolean;
@ -111,11 +116,20 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
@Output() @Output()
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>(); widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
hovered = false;
get widgetEditActionsEnabled(): boolean {
return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen;
}
private cssClass: string; private cssClass: string;
private editWidgetActionsTooltip: ITooltipsterInstance;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private renderer: Renderer2, private renderer: Renderer2,
private container: ViewContainerRef,
private utils: UtilsService) { private utils: UtilsService) {
super(store); super(store);
} }
@ -127,16 +141,34 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
this.cssClass = this.cssClass =
this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString); 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 { ngAfterViewInit(): void {
this.widget.widgetContext.$widgetElement = $(this.tbWidgetElement.nativeElement); 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 { ngOnDestroy(): void {
if (this.cssClass) { if (this.cssClass) {
this.utils.clearCssElement(this.renderer, this.cssClass); this.utils.clearCssElement(this.renderer, this.cssClass);
} }
if (this.editWidgetActionsTooltip) {
this.editWidgetActionsTooltip.destroy();
}
} }
isHighlighted(widget: DashboardWidget) { 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: `<div class="tb-widget-actions-panel">
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isEditActionEnabled"
(click)="container.onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isExportActionEnabled"
(click)="container.onExport($event)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<tb-icon>file_download</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isRemoveActionEnabled"
(click)="container.onRemove($event);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<tb-icon>close</tb-icon>
</button>
</div>`,
styles: [],
encapsulation: ViewEncapsulation.None
})
export class EditWidgetActionsTooltipComponent implements AfterViewInit {
@Input()
container: WidgetContainerComponent;
@Output()
viewInited = new EventEmitter();
constructor(public element: ElementRef<HTMLElement>) {
}
ngAfterViewInit() {
this.viewInited.emit();
}
} }

View File

@ -240,11 +240,20 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
highlightWidget(widgetId: string): DashboardWidget { highlightWidget(widgetId: string): DashboardWidget {
const widget = this.findWidgetById(widgetId); const widget = this.findWidgetById(widgetId);
if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { 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.highlighted = true;
widget.selected = false;
this.dashboardWidgets.forEach((dashboardWidget) => { this.dashboardWidgets.forEach((dashboardWidget) => {
if (dashboardWidget !== widget) { if (dashboardWidget !== widget) {
dashboardWidget.highlighted = false; dashboardWidget.highlighted = false;
dashboardWidget.selected = false;
if (detectChanges) {
dashboardWidget.widgetContext?.detectContainerChanges();
}
} }
}); });
return widget; return widget;
@ -330,6 +339,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private highlightedValue = false; private highlightedValue = false;
private selectedValue = false; private selectedValue = false;
private selectedCallback: (selected: boolean) => void = () => {};
isFullscreen = false; isFullscreen = false;
@ -399,6 +409,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
} }
} }
onSelected(selectedCallback: (selected: boolean) => void) {
this.selectedCallback = selectedCallback;
}
get selected() { get selected() {
return this.selectedValue; return this.selectedValue;
} }
@ -406,6 +420,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
set selected(selected: boolean) { set selected(selected: boolean) {
if (this.selectedValue !== selected) { if (this.selectedValue !== selected) {
this.selectedValue = selected; this.selectedValue = selected;
this.selectedCallback(selected);
this.widgetContext.detectContainerChanges(); this.widgetContext.detectContainerChanges();
} }
} }