From 12d96044395e711c81652fb84c80704219b73fc7 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 25 Sep 2024 16:55:11 +0300 Subject: [PATCH] UI: Add new event tbcontextmenu --- ui-ngx/package.json | 4 +- ui-ngx/src/app/app.component.ts | 3 + ui-ngx/src/app/core/utils.ts | 27 ------- .../dashboard/dashboard.component.html | 2 +- .../dashboard/dashboard.component.ts | 27 +++---- .../widget/widget-container.component.ts | 16 ++-- .../directives/context-menu.directive.ts | 35 +++++++++ .../src/app/shared/directives/public-api.ts | 1 + .../app/shared/models/jquery-event.models.ts | 74 +++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 3 + ui-ngx/src/typings/jquery.typings.d.ts | 3 + ui-ngx/yarn.lock | 10 +-- 12 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 ui-ngx/src/app/shared/directives/context-menu.directive.ts create mode 100644 ui-ngx/src/app/shared/models/jquery-event.models.ts diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 74c29bf00e..c37706b5e0 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 9ac5bc79a1..dcc33dcbd9 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/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() { diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 9076be76f1..2d13ed4133 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -895,30 +895,3 @@ export const camelCase = (str: string): string => { export const convertKeysToCamelCase = (obj: Record): Record => { 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); - } - }); -}; 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 f1fdae62f7..2490d16310 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 @@ -27,7 +27,7 @@ [class.center-vertical]="centerVertical" [class.center-horizontal]="centerHorizontal" (mousedown)="onDashboardMouseDown($event)" - (contextmenu)="openDashboardContextMenu($event)"> + (tbcontextmenu)="openDashboardContextMenu($event)">
; @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((_, 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(); } 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 e70a0d63ad..2480597218 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 @@ -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(); } diff --git a/ui-ngx/src/app/shared/directives/context-menu.directive.ts b/ui-ngx/src/app/shared/directives/context-menu.directive.ts new file mode 100644 index 0000000000..ada727cfe2 --- /dev/null +++ b/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(); + + constructor(private el: ElementRef) { + $(this.el.nativeElement).on('tbcontextmenu', (e: TbContextMenuEvent) => this.tbcontextmenu.emit(e)); + } + + ngOnDestroy() { + $(this.el.nativeElement).off('tbcontextmenu'); + } +} diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts index 6f25320d90..7e0902e223 100644 --- a/ui-ngx/src/app/shared/directives/public-api.ts +++ b/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'; diff --git a/ui-ngx/src/app/shared/models/jquery-event.models.ts b/ui-ngx/src/app/shared/models/jquery-event.models.ts new file mode 100644 index 0000000000..7852f35736 --- /dev/null +++ b/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'); + } + } + }; +}; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 130e272c21..309a3e2bf3 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/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, diff --git a/ui-ngx/src/typings/jquery.typings.d.ts b/ui-ngx/src/typings/jquery.typings.d.ts index d457729ace..a058373022 100644 --- a/ui-ngx/src/typings/jquery.typings.d.ts +++ b/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; } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 51bfe8f56e..cc6762eb57 100644 --- a/ui-ngx/yarn.lock +++ b/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==