UI: Add support long tap in iOS device (show widget/dashboard context menu)

This commit is contained in:
Vladyslav_Prykhodko 2024-09-24 13:13:29 +03:00
parent a9670b6830
commit e87ef55634
4 changed files with 52 additions and 14 deletions

View File

@ -25,6 +25,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { serverErrorCodesTranslations } from '@shared/models/constants'; import { serverErrorCodesTranslations } from '@shared/models/constants';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import Timeout = NodeJS.Timeout;
const varsRegex = /\${([^}]*)}/g; const varsRegex = /\${([^}]*)}/g;
@ -894,3 +895,30 @@ export const camelCase = (str: string): string => {
export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => { export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => {
return _.mapKeys(obj, (value, key) => _.camelCase(key)); 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);
}
});
};

View File

@ -22,7 +22,7 @@
<mat-spinner color="warn" mode="indeterminate" diameter="100"> <mat-spinner color="warn" mode="indeterminate" diameter="100">
</mat-spinner> </mat-spinner>
</div> </div>
<div id="gridster-parent" <div id="gridster-parent" #gridsterParent
fxFlex class="tb-dashboard-content" [class.autofill-height]="isAutofillHeight()" fxFlex class="tb-dashboard-content" [class.autofill-height]="isAutofillHeight()"
[class.center-vertical]="centerVertical" [class.center-vertical]="centerVertical"
[class.center-horizontal]="centerHorizontal" [class.center-horizontal]="centerHorizontal"

View File

@ -19,6 +19,7 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
DoCheck, DoCheck,
ElementRef,
Input, Input,
IterableDiffers, IterableDiffers,
KeyValueDiffers, KeyValueDiffers,
@ -45,7 +46,7 @@ import {
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs'; import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; import { animatedScroll, deepClone, isDefined, isIOSDevice, onLongPress } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
@ -186,18 +187,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
isMobileSize = false; isMobileSize = false;
@ViewChild('gridster', {static: true}) gridster: GridsterComponent; @ViewChild('gridster', {static: true}) gridster: GridsterComponent;
@ViewChild('gridsterParent', {static: true, read: ElementRef}) gridsterParent: ElementRef<HTMLElement>;
@ViewChild('dashboardMenuTrigger', {static: true}) dashboardMenuTrigger: MatMenuTrigger; @ViewChild('dashboardMenuTrigger', {static: true}) dashboardMenuTrigger: MatMenuTrigger;
dashboardMenuPosition = { x: '0px', y: '0px' }; dashboardMenuPosition = { x: '0px', y: '0px' };
dashboardContextMenuEvent: MouseEvent; dashboardContextMenuEvent: MouseEvent | TouchEvent;
@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: MouseEvent | TouchEvent;
dashboardWidgets = new DashboardWidgets(this, dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((_, item) => item), this.differs.find([]).create<Widget>((_, item) => item),
@ -281,6 +283,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
); );
this.updateWidgets(); this.updateWidgets();
if (isIOSDevice()) {
onLongPress(this.gridsterParent.nativeElement, (event) => this.openDashboardContextMenu(event));
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -406,30 +412,30 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} }
} }
openDashboardContextMenu($event: MouseEvent) { openDashboardContextMenu($event: MouseEvent | TouchEvent) {
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) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
this.dashboardContextMenuEvent = $event; this.dashboardContextMenuEvent = $event;
this.dashboardMenuPosition.x = $event.clientX + 'px'; this.dashboardMenuPosition.x = ('touches' in $event ? $event.changedTouches[0].clientX : $event.clientX) + 'px';
this.dashboardMenuPosition.y = $event.clientY + 'px'; this.dashboardMenuPosition.y = ('touches' in $event ? $event.changedTouches[0].clientY : $event.clientY) + 'px';
this.dashboardMenuTrigger.menuData = { items }; this.dashboardMenuTrigger.menuData = { items };
this.dashboardMenuTrigger.openMenu(); this.dashboardMenuTrigger.openMenu();
} }
} }
} }
private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { private openWidgetContextMenu($event: MouseEvent | TouchEvent, 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) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
this.widgetContextMenuEvent = $event; this.widgetContextMenuEvent = $event;
this.widgetMenuPosition.x = $event.clientX + 'px'; this.widgetMenuPosition.x = ('touches' in $event ? $event.changedTouches[0].clientX : $event.clientX) + 'px';
this.widgetMenuPosition.y = $event.clientY + 'px'; this.widgetMenuPosition.y = ('touches' in $event ? $event.changedTouches[0].clientY : $event.clientY) + 'px';
this.widgetMenuTrigger.menuData = { items, widget: widget.widget }; this.widgetMenuTrigger.menuData = { items, widget: widget.widget };
this.widgetMenuTrigger.openMenu(); this.widgetMenuTrigger.openMenu();
} }

View File

@ -39,7 +39,7 @@ import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-compon
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { SafeStyle } from '@angular/platform-browser'; import { SafeStyle } from '@angular/platform-browser';
import { isNotEmptyStr } from '@core/utils'; import { isIOSDevice, isNotEmptyStr, onLongPress } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2'; import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { from } from 'rxjs'; import { from } from 'rxjs';
@ -57,7 +57,7 @@ export enum WidgetComponentActionType {
} }
export class WidgetComponentAction { export class WidgetComponentAction {
event: MouseEvent; event: MouseEvent | TouchEvent;
actionType: WidgetComponentActionType; actionType: WidgetComponentActionType;
} }
@ -151,7 +151,11 @@ 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)); if (isIOSDevice()) {
onLongPress(this.gridsterItem.el, (event) => this.onContextMenu(event));
} else {
$(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
}
const dashboardContentElement = this.widget.widgetContext.dashboardContentElement; const dashboardContentElement = this.widget.widgetContext.dashboardContentElement;
if (dashboardContentElement) { if (dashboardContentElement) {
this.initEditWidgetActionTooltip(dashboardContentElement); this.initEditWidgetActionTooltip(dashboardContentElement);
@ -213,7 +217,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}); });
} }
onContextMenu(event: MouseEvent) { onContextMenu(event: MouseEvent | TouchEvent) {
this.widgetComponentAction.emit({ this.widgetComponentAction.emit({
event, event,
actionType: WidgetComponentActionType.CONTEXT_MENU actionType: WidgetComponentActionType.CONTEXT_MENU