Merge pull request #11735 from vvlladd28/improvement/dashboard/ios-support-context-menu

Add support long tap in iOS device (show widget/dashboard contexе menu)
This commit is contained in:
Igor Kulikov 2024-09-25 18:00:28 +03:00 committed by GitHub
commit 21fb4afb24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 149 additions and 18 deletions

View File

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

View File

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

View File

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

View File

@ -58,6 +58,7 @@ import { WidgetComponentAction, WidgetComponentActionType } from '@home/componen
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
@Component({ @Component({
selector: 'tb-dashboard', selector: 'tb-dashboard',
@ -187,13 +188,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
dashboardMenuPosition = { x: '0px', y: '0px' }; dashboardMenuPosition = { x: '0px', y: '0px' };
dashboardContextMenuEvent: MouseEvent; dashboardContextMenuEvent: TbContextMenuEvent;
@ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger; @ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger;
widgetMenuPosition = { x: '0px', y: '0px' }; widgetMenuPosition = { x: '0px', y: '0px' };
widgetContextMenuEvent: MouseEvent; widgetContextMenuEvent: TbContextMenuEvent;
dashboardWidgets = new DashboardWidgets(this, dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((_, item) => item), this.differs.find([]).create<Widget>((_, item) => item),
@ -395,7 +396,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} }
} }
openDashboardContextMenu($event: MouseEvent) { openDashboardContextMenu($event: TbContextMenuEvent) {
if (this.callbacks && this.callbacks.prepareDashboardContextMenu) { if (this.callbacks && this.callbacks.prepareDashboardContextMenu) {
const items = this.callbacks.prepareDashboardContextMenu($event); const items = this.callbacks.prepareDashboardContextMenu($event);
if (items && items.length) { if (items && items.length) {
@ -410,7 +411,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} }
} }
private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { private openWidgetContextMenu($event: TbContextMenuEvent, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference); const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference);
if (items && items.length) { if (items && items.length) {

View File

@ -45,6 +45,7 @@ import { UtilsService } from '@core/services/utils.service';
import { from } from 'rxjs'; import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
export enum WidgetComponentActionType { export enum WidgetComponentActionType {
MOUSE_DOWN, MOUSE_DOWN,
@ -57,7 +58,7 @@ export enum WidgetComponentActionType {
} }
export class WidgetComponentAction { export class WidgetComponentAction {
event: MouseEvent; event: MouseEvent | TbContextMenuEvent;
actionType: WidgetComponentActionType; actionType: WidgetComponentActionType;
} }
@ -151,7 +152,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
$(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
$(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; const dashboardContentElement = this.widget.widgetContext.dashboardContentElement;
if (dashboardContentElement) { if (dashboardContentElement) {
this.initEditWidgetActionTooltip(dashboardContentElement); this.initEditWidgetActionTooltip(dashboardContentElement);
@ -180,6 +181,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) { if (this.editWidgetActionsTooltip && !this.editWidgetActionsTooltip.status().destroyed) {
this.editWidgetActionsTooltip.destroy(); this.editWidgetActionsTooltip.destroy();
} }
$(this.gridsterItem.el).off('mousedown');
$(this.gridsterItem.el).off('click');
$(this.gridsterItem.el).off('tbcontextmenu');
} }
isHighlighted(widget: DashboardWidget) { isHighlighted(widget: DashboardWidget) {
@ -219,7 +223,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}); });
} }
onContextMenu(event: MouseEvent) { onContextMenu(event: TbContextMenuEvent) {
if (event) { if (event) {
event.stopPropagation(); event.stopPropagation();
} }

View File

@ -185,7 +185,7 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button> </button>
<div class="tb-absolute-fill tb-rulechain-graph" (contextmenu)="openRuleChainContextMenu($event)"> <div class="tb-absolute-fill tb-rulechain-graph" (tbcontextmenu)="openRuleChainContextMenu($event)">
<div #ruleChainMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed" <div #ruleChainMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="ruleChainMenuPosition.x" [style.left]="ruleChainMenuPosition.x"
[style.top]="ruleChainMenuPosition.y" [style.top]="ruleChainMenuPosition.y"

View File

@ -94,6 +94,7 @@ import { VersionControlComponent } from '@home/components/vc/version-control.com
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
import { MatDrawer } from '@angular/material/sidenav'; import { MatDrawer } from '@angular/material/sidenav';
import { HttpStatusCode } from '@angular/common/http'; import { HttpStatusCode } from '@angular/common/http';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -131,7 +132,7 @@ export class RuleChainPageComponent extends PageComponent
ruleChainMenuPosition = { x: '0px', y: '0px' }; ruleChainMenuPosition = { x: '0px', y: '0px' };
contextMenuEvent: MouseEvent; contextMenuEvent: TbContextMenuEvent;
ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors; ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
ruleNodeTypesLibraryArray = ruleNodeTypesLibrary; ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
@ -657,7 +658,7 @@ export class RuleChainPageComponent extends PageComponent
this.validate(); this.validate();
} }
openRuleChainContextMenu($event: MouseEvent) { openRuleChainContextMenu($event: TbContextMenuEvent) {
if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) { if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) {
const x = $event.clientX; const x = $event.clientX;
const y = $event.clientY; const y = $event.clientY;

View File

@ -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');
}
}

View File

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

View File

@ -0,0 +1,80 @@
///
/// 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;
ctrlKey: boolean;
metaKey: boolean;
}
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,
ctrlKey: false,
metaKey: false
});
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,
ctrlKey: e.originalEvent.ctrlKey,
metaKey: e.originalEvent.metaKey,
});
el.trigger(event, e);
});
}
},
teardown(this: HTMLElement) {
const el = $(this);
if (isIOSDevice()) {
el.off('touchstart touchend touchmove');
} else {
el.off('contextmenu');
}
}
};
};

View File

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

View File

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

View File

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