Skip to content

Commit

Permalink
feat(submenus): Add submenus
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacplmann committed Jun 12, 2017
1 parent 30e574b commit a122cf3
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 37 deletions.
41 changes: 41 additions & 0 deletions README.md
Expand Up @@ -75,6 +75,43 @@ public isMenuItemType1(item: any): boolean {
}
```

## Sub-menus

You can specify sub-menus like this:

```html
<ul>
<li *ngFor="let item of items" [contextMenu]="basicMenu" [contextMenuSubject]="item">Right Click: {{item?.name}}</li>
</ul>
<context-menu>
<ng-template contextMenuItem [subMenu]="saySubMenu">
Say...
</ng-template>
<context-menu #saySubMenu>
<ng-template contextMenuItem (execute)="showMessage('Hi, ' + $event.item.name)">
...hi!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Hola, ' + $event.item.name)">
...hola!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Salut, ' + $event.item.name)">
...salut!
</ng-template>
</context-menu>
<ng-template contextMenuItem divider="true"></ng-template>
<ng-template contextMenuItem let-item (execute)="showMessage($event.item.name + ' said: ' + $event.item.otherProperty)">
Bye, {{item?.name}}
</ng-template>
<ng-template contextMenuItem passive="true">
Input something: <input type="text">
</ng-template>
</context-menu>
```

Notes:
1. The sub `<context-menu>` can not be placed inside the `<ng-template>` that references it.
2. Sub-menus may be nested as deeply as you wish.

## Upgrade from angular2-contextmenu 0.x

1. Change `package.json` to reference `ngx-contextmenu` instead of `angular2-contextmenu`
Expand Down Expand Up @@ -253,6 +290,10 @@ You can optionally set focus on the context menu whenever it opens. This enable
export class AppModule {}
```

### Keyboard navigation

If you have `autoFocus` enabled, you can use keyboard shortcuts to navigate the context menu. `tab` - focus next menu item, `shift-tab` - focus previous menu item, `enter` - execute menu item or open sub menu, `esc` - close current context menu.

## Disable Context Menu

If you need to disable the context menu, you can pass a `boolean` to the `[disabled]` input:
Expand Down
42 changes: 39 additions & 3 deletions src/demo/app.component.html
Expand Up @@ -64,12 +64,48 @@ <h3>Enabled and Visible as Functions</h3>
<div style="position:absolute; top: 20px;">
<div style="position:fixed;top:200px;left:-200px">
<context-menu #basicMenu [disabled]="disableBasicMenu" style="pointer-events:all">
<ng-template contextMenuItem (execute)="showMessage('Hi, ' + $event.item.name)">
Say hi!
<ng-template contextMenuItem [subMenu]="saySubMenu">
Say...
</ng-template>
<ng-template contextMenuItem let-item (execute)="showMessage($event.event)">
<context-menu #saySubMenu>
<ng-template contextMenuItem [subMenu]="sayHiSubMenu">
...hi!
</ng-template>
<context-menu #sayHiSubMenu>
<ng-template contextMenuItem (execute)="showMessage('Hi, ' + $event.item.name)">
...hi!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Hola, ' + $event.item.name)">
...hola!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Salut, ' + $event.item.name)">
...salut!
</ng-template>
</context-menu>
<ng-template contextMenuItem (execute)="showMessage('Hola, ' + $event.item.name)">
...hola!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Salut, ' + $event.item.name)">
...salut!
</ng-template>
</context-menu>
<ng-template contextMenuItem let-item [subMenu]="byeSubMenu">
Bye, {{item?.name}}
</ng-template>
<context-menu #byeSubMenu>
<ng-template contextMenuItem (execute)="showMessage('Bye, ' + $event.item.name)">
...bye!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Ciao, ' + $event.item.name)">
...ciao!
</ng-template>
<ng-template contextMenuItem (execute)="showMessage('Au revoir, ' + $event.item.name)">
...au revoir!
</ng-template>
</context-menu>
<ng-template contextMenuItem (execute)="showMessage('Simple')">
Simple
</ng-template>
<ng-template contextMenuItem passive="true">
Input something: <input type="text">
</ng-template>
Expand Down
42 changes: 37 additions & 5 deletions src/lib/contextMenu.component.ts
@@ -1,13 +1,16 @@
import { ContextMenuContentComponent } from './contextMenuContent.component';
import { ContextMenuItemDirective } from './contextMenu.item.directive';
import { CONTEXT_MENU_OPTIONS, IContextMenuOptions } from './contextMenu.options';
import { ContextMenuService, IContextMenuClickEvent } from './contextMenu.service';
import { ContextMenuInjectorService } from './contextMenuInjector.service';
import {
ChangeDetectorRef,
Component,
ComponentRef,
ContentChildren,
ElementRef,
EventEmitter,
HostListener,
Inject,
Input,
OnDestroy,
Expand Down Expand Up @@ -35,17 +38,17 @@ export interface MouseLocation {
template: ``,
})
export class ContextMenuComponent implements OnDestroy {
@Input() public autoFocus = false;
@Input() public useBootstrap4 = false;
@Input() public disabled = false;
@Output() public close: EventEmitter<any> = new EventEmitter<any>();
@Output() public open: EventEmitter<any> = new EventEmitter<any>();
@ContentChildren(ContextMenuItemDirective) public menuItems: QueryList<ContextMenuItemDirective>;
@ViewChild('menu') public menuElement: ElementRef;
public visibleMenuItems: ContextMenuItemDirective[] = [];
public contextMenuContent: ContextMenuContentComponent;

public links: ILinkConfig[] = [];
public isShown = false;
public isOpening = false;
public item: any;
public event: MouseEvent;
private mouseLocation: MouseLocation = { left: '0px', top: '0px' };
Expand All @@ -59,9 +62,18 @@ export class ContextMenuComponent implements OnDestroy {
private contextMenuInjector: ContextMenuInjectorService,
) {
if (options) {
this.autoFocus = options.autoFocus;
this.useBootstrap4 = options.useBootstrap4;
}
this.subscription.add(_contextMenuService.show.subscribe(menuEvent => this.onMenuEvent(menuEvent)));
this.subscription.add(_contextMenuService.triggerClose.subscribe(contextMenuContent => {
if (!contextMenuContent) {
this.contextMenuInjector.destroyAll();
} else {
this.destroySubMenus(contextMenuContent);
this.contextMenuInjector.destroy(contextMenuContent);
}
}));
this.subscription.add(_contextMenuService.close.subscribe(event => this.close.emit(event)));
}

Expand All @@ -75,23 +87,43 @@ export class ContextMenuComponent implements OnDestroy {
return;
}
const { contextMenu, event, item } = menuEvent;
this.contextMenuInjector.destroyAll();
if (!menuEvent.parentContextMenu) {
this.contextMenuInjector.destroyAll();
} else {
this.destroySubMenus(menuEvent.parentContextMenu);
}

if (contextMenu && contextMenu !== this) {
return;
}
this.event = event;
this.item = item;
this.setVisibleMenuItems();
setTimeout(() => {
this.contextMenuInjector.create({
this.setVisibleMenuItems();
this.contextMenuContent = this.contextMenuInjector.create({
menuItems: this.visibleMenuItems,
item: this.item,
event: this.event,
parentContextMenu: menuEvent.parentContextMenu,
});
this.open.next(menuEvent);
});
}

public destroySubMenus(parent: ContextMenuContentComponent): void {
const cmContents: ComponentRef<ContextMenuContentComponent>[] = this.contextMenuInjector.getByType(this.contextMenuInjector.type);
cmContents.filter(content => content.instance.parentContextMenu === parent)
.forEach(comp => {
this.destroySubMenus(comp.instance);
this.contextMenuInjector.destroy(comp);
});
}

@HostListener('window:keydown.Escape')
public destroyLeafMenu(): void {
this._contextMenuService.destroyLeafMenu();
}

public isMenuItemVisible(menuItem: ContextMenuItemDirective): boolean {
return this.evaluateIfFunction(menuItem.visible);
}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/contextMenu.item.directive.ts
@@ -1,3 +1,4 @@
import { ContextMenuComponent } from './contextMenu.component';
import { Directive, Input, Output, EventEmitter, TemplateRef } from '@angular/core';

@Directive({
Expand All @@ -6,9 +7,10 @@ import { Directive, Input, Output, EventEmitter, TemplateRef } from '@angular/co
/* tslint:enable:directive-selector-type */
})
export class ContextMenuItemDirective {
@Input() public divider: boolean = false;
@Input() public passive: boolean = false;
@Input() public subMenu: ContextMenuComponent;
@Input() public divider = false;
@Input() public enabled: boolean | ((item: any) => boolean) = true;
@Input() public passive = false;
@Input() public visible: boolean | ((item: any) => boolean) = true;
@Output() public execute: EventEmitter<{ event: Event, item: any }> = new EventEmitter<{ event: Event, item: any }>();

Expand Down
27 changes: 26 additions & 1 deletion src/lib/contextMenu.service.ts
@@ -1,15 +1,40 @@
import { ContextMenuComponent } from './';
import { Injectable } from '@angular/core';
import { ContextMenuContentComponent } from './contextMenuContent.component';
import { ContextMenuInjectorService } from './contextMenuInjector.service';
import { ComponentRef, Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

export interface IContextMenuClickEvent {
contextMenu?: ContextMenuComponent;
event: MouseEvent;
parentContextMenu?: ContextMenuContentComponent;
item: any;
}

@Injectable()
export class ContextMenuService {
public isDestroyingLeafMenu = false;

public show: Subject<IContextMenuClickEvent> = new Subject<IContextMenuClickEvent>();
public triggerClose: Subject<ContextMenuContentComponent> = new Subject();
public close: Subject<Event> = new Subject();

constructor(private contextMenuInjector: ContextMenuInjectorService) {}

public destroyLeafMenu(): void {
if (this.isDestroyingLeafMenu) {
return;
}
this.isDestroyingLeafMenu = true;
setTimeout(() => {
const cmContents: ComponentRef<ContextMenuContentComponent>[] = this.contextMenuInjector.getByType(this.contextMenuInjector.type);
if (cmContents.length > 1) {
cmContents[cmContents.length - 2].instance.focus();
}
if (cmContents.length > 0) {
this.contextMenuInjector.destroy(cmContents[cmContents.length - 1]);
}
this.isDestroyingLeafMenu = false;
});
}
}

0 comments on commit a122cf3

Please sign in to comment.