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",
"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",

View File

@ -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() {

View File

@ -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"

View File

@ -58,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',
@ -187,13 +188,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
dashboardMenuPosition = { x: '0px', y: '0px' };
dashboardContextMenuEvent: MouseEvent;
dashboardContextMenuEvent: TbContextMenuEvent;
@ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger;
widgetMenuPosition = { x: '0px', y: '0px' };
widgetContextMenuEvent: MouseEvent;
widgetContextMenuEvent: TbContextMenuEvent;
dashboardWidgets = new DashboardWidgets(this,
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) {
const items = this.callbacks.prepareDashboardContextMenu($event);
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) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference);
if (items && items.length) {

View File

@ -45,6 +45,7 @@ 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;
event: MouseEvent | TbContextMenuEvent;
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('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;
if (dashboardContentElement) {
this.initEditWidgetActionTooltip(dashboardContentElement);
@ -180,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) {
@ -219,7 +223,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
});
}
onContextMenu(event: MouseEvent) {
onContextMenu(event: TbContextMenuEvent) {
if (event) {
event.stopPropagation();
}

View File

@ -185,7 +185,7 @@
matTooltipPosition="above">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</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"
[style.left]="ruleChainMenuPosition.x"
[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 { MatDrawer } from '@angular/material/sidenav';
import { HttpStatusCode } from '@angular/common/http';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import Timeout = NodeJS.Timeout;
@Component({
@ -131,7 +132,7 @@ export class RuleChainPageComponent extends PageComponent
ruleChainMenuPosition = { x: '0px', y: '0px' };
contextMenuEvent: MouseEvent;
contextMenuEvent: TbContextMenuEvent;
ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors;
ruleNodeTypesLibraryArray = ruleNodeTypesLibrary;
@ -657,7 +658,7 @@ export class RuleChainPageComponent extends PageComponent
this.validate();
}
openRuleChainContextMenu($event: MouseEvent) {
openRuleChainContextMenu($event: TbContextMenuEvent) {
if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) {
const x = $event.clientX;
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 './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 { 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,

View File

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

View File

@ -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==