【问题标题】:Using Angular Material matMenu as context menu使用 Angular Material matMenu 作为上下文菜单
【发布时间】:2021-02-03 18:41:06
【问题描述】:

我一直在试图弄清楚如何使用 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()被右键点击触发时,我可以调用获取对触发元素的引用,然后调用triggerElement.trigger() 生成菜单。

问题是设置[matMenuTriggerFor] 似乎自动连接到点击事件,而不是右键点击事件,所以每当我左单击元素时,上下文菜单会打开,这不是期望的行为。

我在Stackblitz 上看到过类似的解决方法,它创建一个隐藏的 div 作为触发元素,但需要为菜单元素的位置提供 x 和 y 坐标,这似乎不是最佳的。

有什么方法可以通过右键单击输入元素来触发菜单,而不必创建一个虚拟元素来承载 matMenuTriggerFor 指令?

【问题讨论】:

  • 扩展 MatMenuTrigger 并覆盖装饰器?

标签: angular angular-material


【解决方案1】:

也许您可以使用 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>

这里是demo

但这不是优选的。我认为你应该使用 overlay 替换菜单

【讨论】:

  • 谢谢,这很有帮助,但我不能禁用我的指针事件,因为(单击)和(悬停)以及其他一些事件也是我的应用程序中元素上的触发器,我需要这个上下文菜单。但是,我能够通过添加一个虚拟元素来调整您的解决方案,该元素在单击上下文菜单时以编程方式单击触发器。
【解决方案2】:

幸运的是,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>

【讨论】:

    【解决方案3】:

    似乎不可能断开(click) 与触发MatMenuTrigger 的连接,除非完全禁用鼠标事件,你不能在我的应用程序中这样做,因为需要上下文菜单的相同元素也需要响应单击悬停和拖动事件。但是虚拟元素方法可以简化,并且似乎不需要提供位置信息,如下例所示,改编自 ttQuants stackblitz example in his有用的答案。

    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>
    

    【讨论】:

    • 这不起作用。如果您右键单击其他地方,它不会关闭菜单...
    【解决方案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>
    

    【讨论】:

      猜你喜欢
      • 2013-09-11
      • 1970-01-01
      • 1970-01-01
      • 2013-01-12
      • 1970-01-01
      • 2021-03-26
      • 1970-01-01
      • 1970-01-01
      • 2016-10-01
      相关资源
      最近更新 更多