thingsboard/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts

413 lines
12 KiB
TypeScript
Raw Normal View History

///
2024-01-09 10:46:16 +02:00
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentRef,
2023-03-02 10:25:06 +02:00
ElementRef,
EventEmitter,
HostBinding,
Input,
OnChanges,
2023-03-02 10:25:06 +02:00
OnDestroy,
OnInit,
2023-03-02 10:25:06 +02:00
Output,
Renderer2,
SimpleChanges,
ViewChild,
ViewContainerRef,
2023-03-02 10:25:06 +02:00
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { SafeStyle } from '@angular/platform-browser';
2024-02-01 19:16:37 +02:00
import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2';
2024-02-01 19:16:37 +02:00
import { UtilsService } from '@core/services/utils.service';
2024-06-13 20:09:59 +03:00
import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
export enum WidgetComponentActionType {
MOUSE_DOWN,
CLICKED,
CONTEXT_MENU,
EDIT,
EXPORT,
REMOVE,
REPLACE_REFERENCE_WITH_WIDGET_COPY,
}
export class WidgetComponentAction {
event: MouseEvent;
actionType: WidgetComponentActionType;
}
// @dynamic
@Component({
selector: 'tb-widget-container',
templateUrl: './widget-container.component.html',
styleUrls: ['./widget-container.component.scss'],
2023-03-02 10:25:06 +02:00
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
2024-06-13 20:09:59 +03:00
export class WidgetContainerComponent extends PageComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
@HostBinding('class')
widgetContainerClass = 'tb-widget-container';
@ViewChild('tbWidgetElement', {static: true})
tbWidgetElement: ElementRef;
@Input()
gridsterItem: GridsterItemComponent;
@Input()
widget: DashboardWidget;
@Input()
dashboardStyle: {[klass: string]: any};
@Input()
backgroundImage: SafeStyle | string;
@Input()
isEdit: boolean;
2024-06-13 20:09:59 +03:00
@Input()
isEditingWidget: boolean;
2024-02-01 19:16:37 +02:00
@Input()
isPreview: boolean;
@Input()
isMobile: boolean;
@Input()
dashboardWidgets: DashboardWidgets;
@Input()
isEditActionEnabled: boolean;
@Input()
isExportActionEnabled: boolean;
@Input()
isRemoveActionEnabled: boolean;
@Input()
disableWidgetInteraction = false;
@Output()
widgetFullscreenChanged: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output()
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
2024-06-13 20:09:59 +03:00
hovered = false;
isReferenceWidget = false;
2024-06-13 20:09:59 +03:00
get widgetEditActionsEnabled(): boolean {
return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen;
}
private cssClass: string;
2024-06-13 20:09:59 +03:00
private editWidgetActionsTooltip: ITooltipsterInstance;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
2024-06-13 20:09:59 +03:00
private container: ViewContainerRef,
private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) {
super(store);
}
ngOnInit(): void {
this.widget.widgetContext.containerChangeDetector = this.cd;
const cssString = this.widget.widget.config.widgetCss;
if (isNotEmptyStr(cssString)) {
2024-02-01 19:16:37 +02:00
this.cssClass =
this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString);
}
2024-06-13 20:09:59 +03:00
$(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));
const dashboardContentElement = this.widget.widgetContext.dashboardContentElement;
if (dashboardContentElement) {
this.initEditWidgetActionTooltip(dashboardContentElement);
2024-08-16 18:08:38 +03:00
}
}
ngAfterViewInit(): void {
this.widget.widgetContext.$widgetElement = $(this.tbWidgetElement.nativeElement);
}
2024-06-13 20:09:59 +03:00
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) {
2024-02-01 19:16:37 +02:00
this.utils.clearCssElement(this.renderer, this.cssClass);
}
if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
2024-06-13 20:09:59 +03:00
this.editWidgetActionsTooltip.destroy();
}
}
isHighlighted(widget: DashboardWidget) {
return this.dashboardWidgets.isHighlighted(widget);
}
isNotHighlighted(widget: DashboardWidget) {
return this.dashboardWidgets.isNotHighlighted(widget);
}
onFullscreenChanged(expanded: boolean) {
if (expanded) {
this.renderer.addClass(this.tbWidgetElement.nativeElement, this.cssClass);
} else {
this.renderer.removeClass(this.tbWidgetElement.nativeElement, this.cssClass);
}
this.widgetFullscreenChanged.emit(expanded);
}
onMouseDown(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.MOUSE_DOWN
});
}
onClicked(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.CLICKED
});
}
onContextMenu(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.CONTEXT_MENU
});
}
onEdit(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.EDIT
});
}
onReplaceReferenceWithWidgetCopy(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.REPLACE_REFERENCE_WITH_WIDGET_COPY
});
}
onExport(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.EXPORT
});
}
onRemove(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.REMOVE
});
}
2024-06-13 20:09:59 +03:00
updateEditWidgetActionsTooltipState() {
if (this.editWidgetActionsTooltip) {
if (this.widgetEditActionsEnabled) {
this.editWidgetActionsTooltip.enable();
} else {
this.editWidgetActionsTooltip.disable();
}
}
}
2024-08-16 18:08:38 +03:00
private initEditWidgetActionTooltip(parent: HTMLElement) {
let componentRef: ComponentRef<EditWidgetActionsTooltipComponent>;
2024-06-13 20:09:59 +03:00
from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({
2024-08-16 18:08:38 +03:00
parent: $(parent),
2024-06-13 20:09:59 +03:00
delay: this.widget.selected ? [0, 10000000] : [0, 100],
distance: 2,
2024-08-16 17:42:55 +03:00
zIndex: 151,
2024-06-13 20:09:59 +03:00
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();
2024-08-16 18:08:38 +03:00
const container = parent.getBoundingClientRect();
2024-08-16 17:42:55 +03:00
position.coord.left = clientRect.right - position.size.width - container.left;
position.coord.top = position.coord.top - container.top;
2024-06-13 20:09:59 +03:00
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();
},
functionBefore: () => {
this.widget.isReference = this.dashboardUtils.isReferenceWidget(
this.widget.widgetContext.dashboard.stateController.dashboardCtrl.dashboardCtx.getDashboard(), this.widget.widgetId);
componentRef.instance.cd.detectChanges();
2024-06-13 20:09:59 +03:00
}
});
this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
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();
2024-06-13 20:09:59 +03:00
});
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));
2024-06-13 20:09:59 +03:00
});
}
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-action-container">
<div class="tb-widget-reference-panel tb-primary-fill" *ngIf="container.widget.isReference">
{{ 'widget.reference' | translate }}
<button mat-icon-button class="tb-mat-16"
color="primary"
[fxShow]="container.isEditActionEnabled"
(click)="container.onReplaceReferenceWithWidgetCopy($event)"
matTooltip="{{ 'widget.replace-reference-with-widget-copy' | translate }}"
matTooltipPosition="above">
<tb-icon matButtonIcon>mdi:file-replace-outline</tb-icon>
</button>
</div>
<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>
</div>`,
2024-06-13 20:09:59 +03:00
styles: [],
encapsulation: ViewEncapsulation.None
})
export class EditWidgetActionsTooltipComponent implements AfterViewInit {
@Input()
container: WidgetContainerComponent;
@Output()
viewInited = new EventEmitter();
constructor(public element: ElementRef<HTMLElement>,
public cd: ChangeDetectorRef) {
2024-06-13 20:09:59 +03:00
}
ngAfterViewInit() {
this.viewInited.emit();
}
}