Angular 2: Implement a custom context menu

后端 未结 1 1792
猫巷女王i
猫巷女王i 2021-02-02 18:26

I\'m implementing an Angular 2 attribute directive to allow me to add a custom context menu to an element like this:

Hello worl

相关标签:
1条回答
  • 2021-02-02 18:46

    Here is what I think is a good way to do it.
    You need 1 service, 1 component and 1 directive.

    Here is a plunker

    Explanation:

    The service ContextMenuService:

    • provides a subject of type {event:MouseEvent,obj:any[]} to be subscribed to by ContextMenuHolderComponent, and to receive values from ContextMenuDirective

    Code:

    import {Injectable} from 'angular2/core';
    import {Subject} from 'rxjs/Rx';
    
    @Injectable()
    export class ContextMenuService{
    
        public show:Subject<{event:MouseEvent,obj:any[]}> = new Subject<{event:MouseEvent,obj:any[]}>();
    }
    

    And add it to the list of providers in bootstrap()

    bootstrap(AppComponent,[ContextMenuService]);
    

    The Component ContextMenuHolderComponent:

    • This component is added inside the root component. e.g. AppComponent and it has a fixed position.
    • It subscribes to the subject in ContextMenuService to receive:

      1. menu items of type {title:string,subject:Subject}[], the subject is used to send the clicked on value inside the menu
      2. MouseEvent object
    • It has a (document:click) event listener to close the menu on clicks outside the menu.

    code:

    @Component({
      selector:'context-menu-holder',
      styles:[
        '.container{width:150px;background-color:#eee}',
        '.link{}','.link:hover{background-color:#abc}',
        'ul{margin:0px;padding:0px;list-style-type: none}'
      ],
      host:{
        '(document:click)':'clickedOutside()'
      },
      template:
      `<div [ngStyle]="locationCss" class="container">
          <ul>
              <li (click)="link.subject.next(link.title)" class="link" *ngFor="#link of links">
                  {{link.title}}
              </li>
          </ul>
        </div>
      `
    })
    class ContextMenuHolderComponent{
      links = [];
      isShown = false;
      private mouseLocation :{left:number,top:number} = {left:0;top:0};
      constructor(private _contextMenuService:ContextMenuService){
        _contextMenuService.show.subscribe(e => this.showMenu(e.event,e.obj));
      }
      // the css for the container div
      get locationCss(){
        return {
          'position':'fixed',
          'display':this.isShown ? 'block':'none',
          left:this.mouseLocation.left + 'px',
          top:this.mouseLocation.top + 'px',
        };
      }
      clickedOutside(){
        this.isShown= false; // hide the menu
      }
    
      // show the menu and set the location of the mouse
      showMenu(event,links){
        this.isShown = true;
        this.links = links;
        this.mouseLocation = {
          left:event.clientX,
          top:event.clientY
        }
      }
    }
    

    And add it to the root component:

    @Component({
        selector: 'my-app',
        directives:[ContextMenuHolderComponent,ChildComponent],
        template: `
        <context-menu-holder></context-menu-holder>
        <div>Whatever contents</div>
        <child-component></child-component>
        `
    })
    export class AppComponent { }
    

    The last one, ContextMenuDirective:

    • It adds a contextmenu event to the host element.
    • Accept an input of a list of items to be passed to ContextMenuHolderComponent.

    Code:

    @Directive({
      selector:'[context-menu]',
      host:{'(contextmenu)':'rightClicked($event)'}
    })
    class ContextMenuDirective{
      @Input('context-menu') links;
      constructor(private _contextMenuService:ContextMenuService){
      }
      rightClicked(event:MouseEvent){
        this._contextMenuService.show.next({event:event,obj:this.links});
        event.preventDefault(); // to prevent the browser contextmenu
      }
    }
    

    That's it. All you need to do now is attach the [context-menu] directive to an element and bind it to a list of items. For example:

    @Component({
      selector:'child-component',
      directives:[ContextMenuDirective],
      template:`
      <div [context-menu]="links" >right click here ... {{firstRightClick}}</div>
      <div [context-menu]="anotherLinks">Also right click here...{{secondRightClick}}</div>
      `
    })
    class ChildComponent{
      firstRightClick; secondRightClick;
      links;
      anotherLinks;
      constructor(){
        this.links = [
          {title:'a',subject:new Subject()},
          {title:'b',subject:new Subject()},
          {title:'b',subject:new Subject()}
        ];
        this.anotherLinks = [
          {title:'link 1',subject:new Subject()},
          {title:'link 2',subject:new Subject()},
          {title:'link 3',subject:new Subject()}
        ];
      }
    
      // subscribe to subjects
      ngOnInit(){
        this.links.forEach(l => l.subject.subscribe(val=> this.firstCallback(val)));
        this.anotherLinks.forEach(l => l.subject.subscribe(val=> this.secondCallback(val)))
      }
      firstCallback(val){
        this.firstRightClick = val;
      }
      secondCallback(val){
        this.secondRightClick = val;
      }
    }
    
    0 讨论(0)
提交回复
热议问题