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">
@@ -57,32 +55,11 @@ - - -
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(); } }