Skip to content

Commit

Permalink
feat(hotkey): Arrow key navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacplmann committed Jun 28, 2017
1 parent 4e1ccbb commit 16f9a8d
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 32 deletions.
9 changes: 6 additions & 3 deletions src/lib/contextMenu.component.ts
Expand Up @@ -102,9 +102,7 @@ export class ContextMenuComponent implements OnDestroy {
this.setVisibleMenuItems();
this.contextMenuContent = this.contextMenuInjector.create({
menuItems: this.visibleMenuItems,
item: this.item,
event: this.event,
parentContextMenu: menuEvent.parentContextMenu,
...menuEvent,
});
this.open.next(menuEvent);
});
Expand All @@ -124,6 +122,11 @@ export class ContextMenuComponent implements OnDestroy {
this._contextMenuService.destroyLeafMenu();
}

@HostListener('window:keydown.ArrowLeft')
public destroyLeafSubMenu(): void {
this._contextMenuService.destroyLeafMenu({ exceptRootMenu: true });
}

public isMenuItemVisible(menuItem: ContextMenuItemDirective): boolean {
return this.evaluateIfFunction(menuItem.visible);
}
Expand Down
17 changes: 15 additions & 2 deletions src/lib/contextMenu.service.ts
Expand Up @@ -9,6 +9,7 @@ export interface IContextMenuClickEvent {
event: MouseEvent;
parentContextMenu?: ContextMenuContentComponent;
item: any;
activeMenuItemIndex?: number;
}

@Injectable()
Expand All @@ -21,7 +22,7 @@ export class ContextMenuService {

constructor(private contextMenuInjector: ContextMenuInjectorService) {}

public destroyLeafMenu(): void {
public destroyLeafMenu({exceptRootMenu}: {exceptRootMenu?: boolean} = {}): void {
if (this.isDestroyingLeafMenu) {
return;
}
Expand All @@ -31,10 +32,22 @@ export class ContextMenuService {
if (cmContents && cmContents.length > 1) {
cmContents[cmContents.length - 2].instance.focus();
}
if (cmContents && cmContents.length > 0) {
if (cmContents && cmContents.length > (exceptRootMenu ? 1 : 0)) {
this.contextMenuInjector.destroy(cmContents[cmContents.length - 1]);
}
this.isDestroyingLeafMenu = false;
});
}

public getLeafMenu(): ContextMenuContentComponent {
const cmContents: ComponentRef<ContextMenuContentComponent>[] = this.contextMenuInjector.getByType(this.contextMenuInjector.type);
if (cmContents && cmContents.length > 0) {
return cmContents[cmContents.length - 1].instance;
}
return undefined;
}

public isLeafMenu(cmContent: ContextMenuContentComponent): boolean {
return this.getLeafMenu() === cmContent;
}
}
76 changes: 49 additions & 27 deletions src/lib/contextMenuContent.component.ts
Expand Up @@ -11,9 +11,10 @@ import {
Input,
Optional,
Renderer,
ViewChild
ViewChild,
ViewChildren,
} from '@angular/core';
import { OnDestroy, OnInit } from '@angular/core';
import { OnDestroy, OnInit, QueryList } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

export interface ILinkConfig {
Expand Down Expand Up @@ -46,11 +47,11 @@ export interface MouseLocation {
],
template:
`<div class="dropdown ngx-contextmenu" tabindex="0">
<ul #menu [ngStyle]="locationCss" class="dropdown-menu" tabindex="0">
<li *ngFor="let menuItem of menuItems; let i = index" [class.disabled]="!isMenuItemEnabled(menuItem)"
<ul #menu [ngStyle]="locationCss" class="dropdown-menu" tabindex="0">
<li #li *ngFor="let menuItem of menuItems; let i = index" [class.disabled]="!isMenuItemEnabled(menuItem)"
[class.divider]="menuItem.divider" [class.dropdown-divider]="useBootstrap4 && menuItem.divider"
[class.active]="i === activeMenuItemIndex && isMenuItemEnabled(menuItem)"
[attr.role]="menuItem.divider ? 'separator' : undefined" [id]="'item'+i">
[attr.role]="menuItem.divider ? 'separator' : undefined">
<a *ngIf="!menuItem.divider && !menuItem.passive" href [class.dropdown-item]="useBootstrap4"
[class.disabled]="useBootstrap4 && !isMenuItemEnabled(menuItem)" [class.hasSubMenu]="!!menuItem.subMenu"
(click)="onMenuItemSelect(menuItem, $event)" (mouseenter)="openSubMenu(menuItem, $event)">
Expand All @@ -72,9 +73,10 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView
@Input() public item: any;
@Input() public event: MouseEvent;
@Input() public parentContextMenu: ContextMenuContentComponent;
@Input() public activeMenuItemIndex = -1;
@ViewChild('menu') public menuElement: ElementRef;
@ViewChildren('li') public menuItemElements: QueryList<ElementRef>;

public activeMenuItemIndex = -1;
public autoFocus = false;
public useBootstrap4 = false;
public isShown = false;
Expand All @@ -97,6 +99,9 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView

ngOnInit(): void {
this.isOpening = true;
if (this.activeMenuItemIndex === undefined) {
this.activeMenuItemIndex = -1;
}
setTimeout(() => this.isOpening = false, 400);
if (this.menuItems) {
// Declarative context menu
Expand Down Expand Up @@ -174,11 +179,11 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView
}

public isMenuItemEnabled(menuItem: ContextMenuItemDirective): boolean {
return this.evaluateIfFunction(menuItem.enabled);
return this.evaluateIfFunction(menuItem && menuItem.enabled);
}

public isMenuItemVisible(menuItem: ContextMenuItemDirective): boolean {
return this.evaluateIfFunction(menuItem.visible);
return this.evaluateIfFunction(menuItem && menuItem.visible);
}

public evaluateIfFunction(value: any): any {
Expand All @@ -197,8 +202,8 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView
this.changeDetector.markForCheck();
}

@HostListener('document:click')
@HostListener('document:contextmenu')
@HostListener('window:click')
@HostListener('window:contextmenu')
public clickedOutside(): void {
if (!this.isOpening) {
this.hideMenu();
Expand All @@ -218,9 +223,9 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView
this.changeDetector.markForCheck();
}

@HostListener('document:keydown.ArrowUp', ['$event'])
@HostListener('window:keydown.ArrowDown', ['$event'])
public nextItem(): void {
if (!this.isShown) {
if (!this._contextMenuService.isLeafMenu(this)) {
return;
}
if (event) {
Expand All @@ -231,14 +236,15 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView
} else {
this.activeMenuItemIndex++;
}
if (!this.isMenuItemEnabled(this.menuItems[this.activeMenuItemIndex]) || this.menuItems[this.activeMenuItemIndex].divider) {
const menuItem = this.menuItems[this.activeMenuItemIndex];
if (!this.isMenuItemEnabled(menuItem) || menuItem.divider || menuItem.passive) {
this.nextItem();
}
}

@HostListener('document:keydown.ArrowDown', ['$event'])
@HostListener('window:keydown.ArrowUp', ['$event'])
public prevItem(event?: KeyboardEvent): void {
if (!this.isShown) {
if (!this._contextMenuService.isLeafMenu(this)) {
return;
}
if (event) {
Expand All @@ -249,47 +255,63 @@ export class ContextMenuContentComponent implements OnInit, OnDestroy, AfterView
} else {
this.activeMenuItemIndex--;
}
if (!this.isMenuItemEnabled(this.menuItems[this.activeMenuItemIndex]) || this.menuItems[this.activeMenuItemIndex].divider) {
const menuItem = this.menuItems[this.activeMenuItemIndex];
if (!this.isMenuItemEnabled(menuItem) || menuItem.divider || menuItem.passive) {
this.prevItem();
}
}

@HostListener('document:keydown.Enter', ['$event'])
public onEnter(event?: KeyboardEvent): void {
if (!this.isShown) {
@HostListener('window:keydown.ArrowRight', ['$event'])
public keyboardOpenSubMenu(event?: KeyboardEvent): void {
if (!this._contextMenuService.isLeafMenu(this)) {
return;
}
if (event) {
event.preventDefault();
}
if (this.activeMenuItemIndex >= 0) {
const menuItem = this.menuItems[this.activeMenuItemIndex];
const clickEvent = new MouseEvent('click');
// clickEvent.target = menuItem.elementRef.nativeElement;
} else {
this.hideMenu();
const menuItemElement = this.menuItemElements.toArray()[this.activeMenuItemIndex].nativeElement;
this.openSubMenu(menuItem, <any>event, menuItemElement, 0);
}
}

@HostListener('window:keydown.Enter', ['$event'])
@HostListener('window:keydown.Space', ['$event'])
public keyboardMenuItemSelect(event?: KeyboardEvent): void {
if (!this._contextMenuService.isLeafMenu(this)) {
return;
}
if (event) {
event.preventDefault();
}
if (this.activeMenuItemIndex >= 0) {
const menuItem = this.menuItems[this.activeMenuItemIndex];
const menuItemElement = this.menuItemElements.toArray()[this.activeMenuItemIndex].nativeElement;
this.onMenuItemSelect(menuItem, <any>event, menuItemElement, 0);
}
}

public openSubMenu(menuItem: ContextMenuItemDirective, event: MouseEvent): void {
public openSubMenu(menuItem: ContextMenuItemDirective, event: MouseEvent, target?: HTMLElement, activeMenuItemIndex?: number): void {
this._contextMenuService.triggerClose.next(this);
if (!menuItem.subMenu) {
return;
}
const rect = (<HTMLElement>event.target).getBoundingClientRect();
const rect = (target || <HTMLElement>event.target).getBoundingClientRect();
const newEvent = Object.assign({}, event, { clientX: rect.right, clientY: rect.top, view: event.view });
this._contextMenuService.show.next({
contextMenu: menuItem.subMenu,
item: this.item,
event: newEvent,
parentContextMenu: this,
activeMenuItemIndex,
});
}

public onMenuItemSelect(menuItem: ContextMenuItemDirective, event: MouseEvent): void {
public onMenuItemSelect(menuItem: ContextMenuItemDirective, event: MouseEvent, target?: HTMLElement, activeMenuItemIndex?: number): void {
event.preventDefault();
event.stopPropagation();
this.openSubMenu(menuItem, event);
this.openSubMenu(menuItem, event, target);
if (!menuItem.subMenu) {
menuItem.triggerExecute(this.item, event);
}
Expand Down

0 comments on commit 16f9a8d

Please sign in to comment.