Browse Source

UI: Add new event tbcontextmenu

pull/11735/head
Vladyslav_Prykhodko 2 years ago
parent
commit
12d9604439
  1. 4
      ui-ngx/package.json
  2. 3
      ui-ngx/src/app/app.component.ts
  3. 27
      ui-ngx/src/app/core/utils.ts
  4. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  5. 27
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  6. 16
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  7. 35
      ui-ngx/src/app/shared/directives/context-menu.directive.ts
  8. 1
      ui-ngx/src/app/shared/directives/public-api.ts
  9. 74
      ui-ngx/src/app/shared/models/jquery-event.models.ts
  10. 3
      ui-ngx/src/app/shared/shared.module.ts
  11. 3
      ui-ngx/src/typings/jquery.typings.d.ts
  12. 10
      ui-ngx/yarn.lock

4
ui-ngx/package.json

@ -59,7 +59,7 @@
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",
"html2canvas": "^1.4.1",
"jquery": "^3.6.3",
"jquery": "^3.7.1",
"jquery.terminal": "^2.35.3",
"js-beautify": "1.14.7",
"json-schema-defaults": "^0.4.0",
@ -127,7 +127,7 @@
"@types/flowjs": "^2.13.9",
"@types/jasmine": "~3.10.2",
"@types/jasminewd2": "^2.0.10",
"@types/jquery": "^3.5.16",
"@types/jquery": "^3.5.30",
"@types/js-beautify": "^1.13.3",
"@types/leaflet": "1.8.0",
"@types/leaflet-polylinedecorator": "1.6.4",

3
ui-ngx/src/app/app.component.ts

@ -32,6 +32,7 @@ import { AuthService } from '@core/auth/auth.service';
import { svgIcons, svgIconsUrl } from '@shared/models/icon.models';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { SETTINGS_KEY } from '@core/settings/settings.effects';
import { initCustomJQueryEvents } from '@shared/models/jquery-event.models';
@Component({
selector: 'tb-root',
@ -74,6 +75,8 @@ export class AppComponent implements OnInit {
this.setupTranslate();
this.setupAuth();
initCustomJQueryEvents();
}
setupTranslate() {

27
ui-ngx/src/app/core/utils.ts

@ -895,30 +895,3 @@ export const camelCase = (str: string): string => {
export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => {
return _.mapKeys(obj, (value, key) => _.camelCase(key));
};
export const isIOSDevice = (): boolean =>
/iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
export const onLongPress = (element: HTMLElement, callback: (event: TouchEvent) => void) => {
let timeoutId: Timeout;
$(element).on('touchstart', (e) => {
timeoutId = setTimeout(() => {
timeoutId = null;
e.stopPropagation();
callback(e.originalEvent);
}, 500);
});
$(element).on('contextmenu', (e) => e.preventDefault());
$(element).on('touchend', (e) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
});
$(element).on('touchmove', (e) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
});
};

2
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -27,7 +27,7 @@
[class.center-vertical]="centerVertical"
[class.center-horizontal]="centerHorizontal"
(mousedown)="onDashboardMouseDown($event)"
(contextmenu)="openDashboardContextMenu($event)">
(tbcontextmenu)="openDashboardContextMenu($event)">
<div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="dashboardMenuPosition.x"
[style.top]="dashboardMenuPosition.y"

27
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -19,7 +19,6 @@ import {
ChangeDetectionStrategy,
Component,
DoCheck,
ElementRef,
Input,
IterableDiffers,
KeyValueDiffers,
@ -46,7 +45,7 @@ import {
} from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { animatedScroll, deepClone, isDefined, isIOSDevice, onLongPress } from '@app/core/utils';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
@ -59,6 +58,7 @@ import { WidgetComponentAction, WidgetComponentActionType } from '@home/componen
import { TbPopoverComponent } from '@shared/components/popover.component';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
@Component({
selector: 'tb-dashboard',
@ -183,19 +183,18 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
isMobileSize = false;
@ViewChild('gridster', {static: true}) gridster: GridsterComponent;
@ViewChild('gridsterParent', {static: true, read: ElementRef}) gridsterParent: ElementRef<HTMLElement>;
@ViewChild('dashboardMenuTrigger', {static: true}) dashboardMenuTrigger: MatMenuTrigger;
dashboardMenuPosition = { x: '0px', y: '0px' };
dashboardContextMenuEvent: MouseEvent | TouchEvent;
dashboardContextMenuEvent: TbContextMenuEvent;
@ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger;
widgetMenuPosition = { x: '0px', y: '0px' };
widgetContextMenuEvent: MouseEvent | TouchEvent;
widgetContextMenuEvent: TbContextMenuEvent;
dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((_, item) => item),
@ -272,10 +271,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
);
this.updateWidgets();
if (isIOSDevice()) {
onLongPress(this.gridsterParent.nativeElement, (event) => this.openDashboardContextMenu(event));
}
}
ngOnDestroy(): void {
@ -401,30 +396,30 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
}
}
openDashboardContextMenu($event: MouseEvent | TouchEvent) {
openDashboardContextMenu($event: TbContextMenuEvent) {
if (this.callbacks && this.callbacks.prepareDashboardContextMenu) {
const items = this.callbacks.prepareDashboardContextMenu($event);
if (items && items.length) {
$event.preventDefault();
$event.stopPropagation();
this.dashboardContextMenuEvent = $event;
this.dashboardMenuPosition.x = ('touches' in $event ? $event.changedTouches[0].clientX : $event.clientX) + 'px';
this.dashboardMenuPosition.y = ('touches' in $event ? $event.changedTouches[0].clientY : $event.clientY) + 'px';
this.dashboardMenuPosition.x = $event.clientX + 'px';
this.dashboardMenuPosition.y = $event.clientY + 'px';
this.dashboardMenuTrigger.menuData = { items };
this.dashboardMenuTrigger.openMenu();
}
}
}
private openWidgetContextMenu($event: MouseEvent | TouchEvent, widget: DashboardWidget) {
private openWidgetContextMenu($event: TbContextMenuEvent, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference);
const items = this.callbacks.prepareWidgetContextMenu($event as any, widget.widget, widget.isReference);
if (items && items.length) {
$event.preventDefault();
$event.stopPropagation();
this.widgetContextMenuEvent = $event;
this.widgetMenuPosition.x = ('touches' in $event ? $event.changedTouches[0].clientX : $event.clientX) + 'px';
this.widgetMenuPosition.y = ('touches' in $event ? $event.changedTouches[0].clientY : $event.clientY) + 'px';
this.widgetMenuPosition.x = $event.clientX + 'px';
this.widgetMenuPosition.y = $event.clientY + 'px';
this.widgetMenuTrigger.menuData = { items, widget: widget.widget };
this.widgetMenuTrigger.openMenu();
}

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

@ -39,12 +39,13 @@ import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-compon
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { SafeStyle } from '@angular/platform-browser';
import { isIOSDevice, isNotEmptyStr, onLongPress } from '@core/utils';
import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service';
import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@ -57,7 +58,7 @@ export enum WidgetComponentActionType {
}
export class WidgetComponentAction {
event: MouseEvent | TouchEvent;
event: MouseEvent | TbContextMenuEvent;
actionType: WidgetComponentActionType;
}
@ -151,11 +152,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
$(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
if (isIOSDevice()) {
onLongPress(this.gridsterItem.el, (event) => this.onContextMenu(event));
} else {
$(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
}
$(this.gridsterItem.el).on('tbcontextmenu', (e: TbContextMenuEvent) => this.onContextMenu(e));
const dashboardContentElement = this.widget.widgetContext.dashboardContentElement;
if (dashboardContentElement) {
this.initEditWidgetActionTooltip(dashboardContentElement);
@ -184,6 +181,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
this.editWidgetActionsTooltip.destroy();
}
$(this.gridsterItem.el).off('mousedown');
$(this.gridsterItem.el).off('click');
$(this.gridsterItem.el).off('tbcontextmenu');
}
isHighlighted(widget: DashboardWidget) {
@ -223,7 +223,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
});
}
onContextMenu(event: MouseEvent | TouchEvent) {
onContextMenu(event: TbContextMenuEvent) {
if (event) {
event.stopPropagation();
}

35
ui-ngx/src/app/shared/directives/context-menu.directive.ts

@ -0,0 +1,35 @@
///
/// 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 { Directive, ElementRef, EventEmitter, OnDestroy, Output } from '@angular/core';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
@Directive({
selector: '[tbcontextmenu]'
})
export class ContextMenuDirective implements OnDestroy {
@Output()
tbcontextmenu = new EventEmitter<TbContextMenuEvent>();
constructor(private el: ElementRef) {
$(this.el.nativeElement).on('tbcontextmenu', (e: TbContextMenuEvent) => this.tbcontextmenu.emit(e));
}
ngOnDestroy() {
$(this.el.nativeElement).off('tbcontextmenu');
}
}

1
ui-ngx/src/app/shared/directives/public-api.ts

@ -16,3 +16,4 @@
export * from './truncate-with-tooltip.directive';
export * from './ellipsis-chip-list.directive';
export * from './context-menu.directive';

74
ui-ngx/src/app/shared/models/jquery-event.models.ts

@ -0,0 +1,74 @@
///
/// 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 Timeout = NodeJS.Timeout;
export interface TbContextMenuEvent extends Event {
clientX: number;
clientY: number;
}
const isIOSDevice = (): boolean =>
/iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
export const initCustomJQueryEvents = () => {
$.event.special.tbcontextmenu = {
setup(this: HTMLElement) {
const el = $(this);
if (isIOSDevice()) {
let timeoutId: Timeout;
el.on('touchstart', (e) => {
e.stopPropagation();
timeoutId = setTimeout(() => {
timeoutId = null;
e.stopPropagation();
const touch = e.originalEvent.changedTouches[0];
const event = $.Event('tbcontextmenu', {
clientX: touch.clientX,
clientY: touch.clientY
});
el.trigger(event, e);
}, 500);
});
el.on('touchend touchmove', () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
});
} else {
el.on('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
const event = $.Event('tbcontextmenu', {
clientX: e.originalEvent.clientX,
clientY: e.originalEvent.clientY
});
el.trigger(event, e);
});
}
},
teardown(this: HTMLElement) {
const el = $(this);
if (isIOSDevice()) {
el.off('touchstart touchend touchmove');
} else {
el.off('contextmenu');
}
}
};
};

3
ui-ngx/src/app/shared/shared.module.ts

@ -68,6 +68,7 @@ import { NgxHmCarouselModule } from 'ngx-hm-carousel';
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
import { UserMenuComponent } from '@shared/components/user-menu.component';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { ContextMenuDirective } from '@shared/directives/context-menu.directive';
import { NospacePipe } from '@shared/pipe/nospace.pipe';
import { TranslateModule } from '@ngx-translate/core';
import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
@ -371,6 +372,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
LedLightComponent,
MarkdownEditorComponent,
TruncateWithTooltipDirective,
ContextMenuDirective,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -633,6 +635,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
LedLightComponent,
MarkdownEditorComponent,
TruncateWithTooltipDirective,
ContextMenuDirective,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

3
ui-ngx/src/typings/jquery.typings.d.ts

@ -14,6 +14,9 @@
/// limitations under the License.
///
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
interface JQuery {
terminal(options?: any): any;
on(events: 'tbcontextmenu', handler: (e: TbContextMenuEvent) => void): this;
}

10
ui-ngx/yarn.lock

@ -3052,10 +3052,10 @@
dependencies:
"@types/jasmine" "*"
"@types/jquery@*", "@types/jquery@^3.5.16", "@types/jquery@^3.5.29":
version "3.5.29"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.29.tgz#3c06a1f519cd5fc3a7a108971436c00685b5dcea"
integrity sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==
"@types/jquery@*", "@types/jquery@^3.5.29", "@types/jquery@^3.5.30":
version "3.5.30"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.30.tgz#888d584cbf844d3df56834b69925085038fd80f7"
integrity sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==
dependencies:
"@types/sizzle" "*"
@ -7377,7 +7377,7 @@ jquery.terminal@^2.35.3:
optionalDependencies:
fsevents "^2.3.2"
jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.6.3, jquery@^3.7.1:
jquery@>=1.9.1, jquery@^3.5.0, jquery@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==

Loading…
Cancel
Save