diff --git a/README.md b/README.md index f43cc81..e27a76b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,43 @@ public isMenuItemType1(item: any): boolean { } ``` +## Sub-menus + +You can specify sub-menus like this: + +```html + + + + Say... + + + + ...hi! + + + ...hola! + + + ...salut! + + + + + Bye, {{item?.name}} + + + Input something: + + +``` + +Notes: +1. The sub `` can not be placed inside the `` 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` @@ -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: diff --git a/src/demo/app.component.html b/src/demo/app.component.html index 388ff74..f70f24b 100644 --- a/src/demo/app.component.html +++ b/src/demo/app.component.html @@ -64,12 +64,48 @@

Enabled and Visible as Functions

- - Say hi! + + Say... - + + + ...hi! + + + + ...hi! + + + ...hola! + + + ...salut! + + + + ...hola! + + + ...salut! + + + Bye, {{item?.name}} + + + ...bye! + + + ...ciao! + + + ...au revoir! + + + + Simple + Input something: diff --git a/src/lib/contextMenu.component.ts b/src/lib/contextMenu.component.ts index 4339a55..9642a02 100644 --- a/src/lib/contextMenu.component.ts +++ b/src/lib/contextMenu.component.ts @@ -1,3 +1,4 @@ +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'; @@ -5,9 +6,11 @@ import { ContextMenuInjectorService } from './contextMenuInjector.service'; import { ChangeDetectorRef, Component, + ComponentRef, ContentChildren, ElementRef, EventEmitter, + HostListener, Inject, Input, OnDestroy, @@ -35,6 +38,7 @@ 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 = new EventEmitter(); @@ -42,10 +46,9 @@ export class ContextMenuComponent implements OnDestroy { @ContentChildren(ContextMenuItemDirective) public menuItems: QueryList; @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' }; @@ -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))); } @@ -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[] = 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); } diff --git a/src/lib/contextMenu.item.directive.ts b/src/lib/contextMenu.item.directive.ts index 05c2da7..ed8dd9a 100644 --- a/src/lib/contextMenu.item.directive.ts +++ b/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({ @@ -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 }>(); diff --git a/src/lib/contextMenu.service.ts b/src/lib/contextMenu.service.ts index 80679ac..e518f9e 100644 --- a/src/lib/contextMenu.service.ts +++ b/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 = new Subject(); + public triggerClose: Subject = new Subject(); public close: Subject = new Subject(); + + constructor(private contextMenuInjector: ContextMenuInjectorService) {} + + public destroyLeafMenu(): void { + if (this.isDestroyingLeafMenu) { + return; + } + this.isDestroyingLeafMenu = true; + setTimeout(() => { + const cmContents: ComponentRef[] = 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; + }); + } } diff --git a/src/lib/contextMenuContent.component.ts b/src/lib/contextMenuContent.component.ts index eb6bf36..951db9d 100644 --- a/src/lib/contextMenuContent.component.ts +++ b/src/lib/contextMenuContent.component.ts @@ -1,5 +1,3 @@ -import { Subscription } from 'rxjs/Subscription'; -import { ContextMenuInjectorService } from './contextMenuInjector.service'; import { ContextMenuItemDirective } from './contextMenu.item.directive'; import { CONTEXT_MENU_OPTIONS, IContextMenuOptions } from './contextMenu.options'; import { ContextMenuService } from './contextMenu.service'; @@ -8,16 +6,15 @@ import { ChangeDetectorRef, Component, ElementRef, - EventEmitter, HostListener, Inject, Input, Optional, - Output, Renderer, ViewChild } from '@angular/core'; -import { OnInit, OnDestroy } from '@angular/core'; +import { OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs/Subscription'; export interface ILinkConfig { click: (item: any, $event?: MouseEvent) => void; @@ -41,17 +38,21 @@ export interface MouseLocation { font-weight: normal; line-height: @line-height-base; white-space: nowrap; - }` + }`, + `.hasSubMenu:after { + content: "\u25B6"; + float: right; + }`, ], template: - `