#angular #angular-material
#angular #angular-material
Вопрос:
Я пытался разобраться, как использовать matMenu в качестве контекстного меню, запускаемого, когда кто-то щелкает правой кнопкой мыши по одному из моих элементов.
Это мое меню:
<mat-menu #contextMenu="matMenu">
<button mat-menu-item>
<mat-icon>table_rows</mat-icon>
<span>Select Whole Row</span>
<span>⌘→</span>
</button>
<button mat-menu-item>
<mat-icon>functions</mat-icon>
<span>Insert Subtotal</span>
<span>⌃S</span>
</button>
</mat-menu>
Я хочу иметь возможность запускать меню при щелчке правой кнопкой мыши по этому элементу:
<div tabindex=0 *ngIf="!hasRowFocus"
class="display-cell" (keydown)="onSelectKeyDown($event)"
(click)=selectCellClick($event)
(dblclick)=selectCellDblClick($event)
(contextmenu)="openContextMenu()"
[ngClass]='selectClass'
(mouseover)="mouseover()"
(mouseout)="mouseout()"
#cellSelect >
<div (dragenter) = "mouseDragEnter($event)" (dragleave) = "mouseDragLeave($event)">{{templateDisplayValue}}</div>
</div>
Однако, согласно документации, мне нужно указать, что у этого div должна быть [matMenuTriggerFor]
директива, так что, когда openContextMenu()
он запускается щелчком правой кнопки мыши, я могу вызвать get ссылку на элемент триггера, а затем вызвать triggerElement.trigger()
, чтобы вызвать меню.
Проблема в том, что, похоже, настройка [matMenuTriggerFor]
автоматически подключается к событию щелчка, а не к событию щелчка правой кнопкой мыши, поэтому каждый раз, когда я щелкаю левой кнопкой мыши по элементу, открывается контекстное меню, что не является желаемым поведением.
Я видел обходные пути, подобные этому, в Stackblitz, который создает скрытый div в качестве элемента триггера, но требует указания координат x amp; y для местоположения элемента меню, который кажется неоптимальным.
Есть ли способ вызвать меню, щелкнув правой кнопкой мыши по элементу ввода, без необходимости создавать фиктивный элемент для размещения директивы matMenuTriggerFor?
Комментарии:
1. расширить MatMenuTrigger и переопределить декораторы?
Ответ №1:
К счастью, код для matMenuTriggerFor
размещен здесь: https://github.com/angular/components/blob/master/src/material/menu/menu-trigger.ts
Если вы посмотрите на класс в нижней части файла MatMenuTrigger
, то легко переопределить тот же базовый класс, проделать несколько трюков и выполнить нашу собственную matContextMenuTriggerFor
директиву:
import { Directive, HostListener, Input } from "@angular/core";
import { MatMenuPanel, _MatMenuTriggerBase } from "@angular/material/menu";
import { fromEvent, merge } from "rxjs";
// @Directive declaration styled same as matMenuTriggerFor
// with different selector and exportAs.
@Directive({
selector: `[matContextMenuTriggerFor]`,
host: {
'class': 'mat-menu-trigger',
},
exportAs: 'matContextMenuTrigger',
})
export class MatContextMenuTrigger extends _MatMenuTriggerBase {
// Duplicate the code for the matMenuTriggerFor binding
// using a new property and the public menu accessors.
@Input('matContextMenuTriggerFor')
get menu_again() {
return this.menu;
}
set menu_again(menu: MatMenuPanel) {
this.menu = menu;
}
// Make sure to ignore the original binding
// to allow separate menus on each button.
@Input('matMenuTriggerFor')
set ignoredMenu(value: any) { }
// Override _handleMousedown, and call super._handleMousedown
// with a new MouseEvent having button numbers 2 and 0 reversed.
_handleMousedown(event: MouseEvent): void {
return super._handleMousedown(new MouseEvent(event.type, Object.assign({}, event, { button: event.button === 0 ? 2 : event.button === 2 ? 0 : event.button })));
}
// Override _handleClick to make existing binding to clicks do nothing.
_handleClick(event: MouseEvent): void { }
// Create a place to store the host element.
private hostElement: EventTarget | null = null;
// Listen for contextmenu events (right-clicks), then:
// 1) Store the hostElement for use in later events.
// 2) Prevent browser default action.
// 3) Call super._handleClick to open the menu as expected.
@HostListener('contextmenu', ['$event'])
_handleContextMenu(event: MouseEvent): void {
this.hostElement = event.target;
if (event.shiftKey) return; // Hold a shift key to open original context menu. Delete this line if not desired behavior.
event.preventDefault();
super._handleClick(event);
}
// The complex logic below is to handle submenus and hasBackdrop===false well.
// Listen for click and contextmenu (right-click) events on entire document.
// If this menu is open, one of the following conditional actions.
// 1) If the click came from the overlay backdrop, close the menu and prevent default.
// 2) If the click came inside the overlay container, it was on a menu. If it was
// a contextmenu event, prevent default and re-dispatch it as a click.
// 3) If the event did not come from our host element, close the menu.
private contextListenerSub = merge(
fromEvent(document, "contextmenu"),
fromEvent(document, "click"),
).subscribe(event => {
if (this.menuOpen) {
if (event.target) {
const target = event.target as HTMLElement;
if (target.classList.contains("cdk-overlay-backdrop")) {
event.preventDefault();
this.closeMenu();
} else {
let inOverlay = false;
document.querySelectorAll(".cdk-overlay-container").forEach(e => {
if (e.contains(target))
inOverlay = true;
});
if (inOverlay) {
if (event.type === "contextmenu") {
event.preventDefault();
event.target?.dispatchEvent(new MouseEvent("click", event));
}
} else
if (target !== this.hostElement)
this.closeMenu();
}
}
}
});
// When destroyed, stop listening for the contextmenu events above,
// null the host element reference, then call super.
ngOnDestroy() {
this.contextListenerSub.unsubscribe();
this.hostElement = null;
return super.ngOnDestroy();
}
}
Добавьте MatContextMenuTrigger
директиву в declarations
список в вашем файле модуля, тогда это будет похоже на использование обычного:
<div [matContextMenuTriggerFor]="myContextMenu">
Right click here to open context menu.
</div>
<mat-menu #myContextMenu="matMenu">
My context menu.
</mat-menu>
И у вас могут быть отдельные меню как для щелчка левой кнопкой мыши, так и для щелчка правой кнопкой мыши:
<div [matMenuTriggerFor]="myNormalMenu" [matContextMenuTriggerFor]="myContextMenu">
Left click here to open normal menu.<br>
Right click here to open context menu.
</div>
<mat-menu #myNormalMenu="matMenu">
My normal menu.
</mat-menu>
<mat-menu #myContextMenu="matMenu">
My context menu.
</mat-menu>
Ответ №2:
Может быть, вы можете использовать css pointer-events: none;
и обернуть [matMenuTriggerFor]="contextMenu"
внутри списка следующим образом
<p>Right-click on the items below to show the context menu:</p>
<mat-list>
<mat-list-item *ngFor="let item of items" (contextmenu)="onContextMenu($event, item)">
<div [matMenuTriggerFor]="contextMenu" style="pointer-events: none;">{{ item.name }}</div>
</mat-list-item>
</mat-list>
<mat-menu #contextMenu="matMenu">
<ng-template matMenuContent let-item="item">
<button mat-menu-item (click)="onContextMenuAction1(item)">Action 1</button>
<button mat-menu-item (click)="onContextMenuAction2(item)">Action 2</button>
</ng-template>
</mat-menu>
Вот демонстрация
Но это не является предпочтительным. Я думаю, вам следует использовать overlay заменить на menu
Комментарии:
1. Спасибо, это полезно, но я не могу отключить события моего указателя, потому что (щелчок) и (наведение курсора) и некоторые другие события также являются триггерами в моем приложении для элемента, для которого мне нужно это контекстное меню. Однако я смог адаптировать ваше решение для этой цели, добавив фиктивный элемент, который программно нажимает на триггер при нажатии контекстного меню.
Ответ №3:
Похоже, что невозможно отключить запуск (click)
от запуска MatMenuTrigger
, не отключив события мыши полностью, вы не можете этого сделать в моем приложении, потому что тот же элемент, которому требуется контекстное меню, также должен реагировать на события наведения курсора мыши и перетаскивания. Но подход с фиктивным элементом может быть упрощен и, похоже, не требует предоставления позиционной информации, как в следующем примере, адаптированном из примера ttQuants stackblitz в его полезном ответе.
export class ContextMenuExample {
items = [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
];
@ViewChildren(MatMenuTrigger)
contextMenuTriggers: QueryList<MatMenuTrigger>;
onContextMenu(event: MouseEvent, item: Item, idx : number) {
event.preventDefault();
this.contextMenuTriggers.toArray()[idx].openMenu();
}
onContextMenuAction1(item: Item) {
alert(`Click on Action 1 for ${item.name}`);
}
onContextMenuAction2(item: Item) {
alert(`Click on Action 2 for ${item.name}`);
}
}
export interface Item {
id: number;
name: string;
}
Шаблон:
<p>Right-click on the items below to show the context menu:</p>
<mat-list>
<mat-list-item *ngFor="let item of items; let idx=index">
<div (contextmenu)="onContextMenu($event, item, idx)">{{item.name}}
<div [matMenuTriggerFor]="contextMenu" style="visibility: hidden;
pointer-events: none;">{{ item.name }}
</div>
</div>
</mat-list-item>
</mat-list>
<mat-menu #contextMenu="matMenu">
<ng-template matMenuContent let-item="item">
<button mat-menu-item (click)="onContextMenuAction1(item)">Action 1</button>
<button mat-menu-item (click)="onContextMenuAction2(item)">Action 2</button>
</ng-template>
</mat-menu>
Комментарии:
1. Это не работает. Меню не закрывается, если щелкнуть правой кнопкой мыши в другом месте…
Ответ №4:
Вот решение, которое я нашел. Контекстное меню откроется в положении курсора.
Создайте компонент с именем ContextMenuComponent
import { Component, HostBinding } from '@angular/core';
import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'app-context-menu',
template: '<ng-content></ng-content>',
styles: ['']
})
export class ContextMenuComponent extends MatMenuTrigger {
@HostBinding('style.position') private position = 'fixed';
@HostBinding('style.pointer-events') private events = 'none';
@HostBinding('style.left') private x: string;
@HostBinding('style.top') private y: string;
// Intercepts the global context menu event
public open({ x, y }: MouseEvent, data?: any) {
// Pass along the context data to support lazily-rendered content
if(!!data) { this.menuData = data; }
// Adjust the menu anchor position
this.x = x 'px';
this.y = y 'px';
// Opens the menu
this.openMenu();
// prevents default
return false;
}
}
Используйте контекстное меню, как показано ниже, там, где вы хотите
<app-context-menu [matMenuTriggerFor]="main" #menu>
<mat-menu #main="matMenu">
<ng-template matMenuContent let-name="name">
<button mat-menu-item>{{ name }}</button>
<button mat-menu-item>Menu item 1</button>
<button mat-menu-item>Menu item 2</button>
<button mat-menu-item [matMenuTriggerFor]="sub">
Others...
</button>
</ng-template>
</mat-menu>
<mat-menu #sub="matMenu">
<button mat-menu-item>Menu item 3</button>
<button mat-menu-item>Menu item 4</button>
</mat-menu>
</app-context-menu>
<section fxLayout="column" fxLayoutAlign="center center" (contextmenu)="menu.open($event, { name: 'Stack Overflow'} )">
<p>Try to right click and see how it works!</p>
<p>You do whatever you want here...</p>
</section>