UNPKG

@hoso/ngx-contextmenu

Version:

An Angular component to show a context menu on an arbitrary component

581 lines (568 loc) 29.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/cdk/overlay'), require('@angular/common'), require('@angular/core'), require('@angular/cdk/portal'), require('rxjs'), require('@angular/cdk/a11y'), require('rxjs/operators')) : typeof define === 'function' && define.amd ? define('@hoso/ngx-contextmenu', ['exports', '@angular/cdk/overlay', '@angular/common', '@angular/core', '@angular/cdk/portal', 'rxjs', '@angular/cdk/a11y', 'rxjs/operators'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.hoso = global.hoso || {}, global.hoso['ngx-contextmenu'] = {}), global.ng.cdk.overlay, global.ng.common, global.ng.core, global.ng.cdk.portal, global.rxjs, global.ng.cdk.a11y, global.rxjs.operators)); }(this, (function (exports, overlay, common, core, portal, rxjs, a11y, operators) { 'use strict'; var CONTEXT_MENU_OPTIONS = new core.InjectionToken('CONTEXT_MENU_OPTIONS'); var ARROW_LEFT_KEYCODE = 37; var ContextMenuContentComponent = /** @class */ (function () { function ContextMenuContentComponent(changeDetector, elementRef, options) { this.changeDetector = changeDetector; this.elementRef = elementRef; this.options = options; this.menuItems = []; this.isLeaf = false; this.execute = new core.EventEmitter(); this.openSubMenu = new core.EventEmitter(); this.closeLeafMenu = new core.EventEmitter(); this.closeAllMenus = new core.EventEmitter(); this.autoFocus = false; this.useBootstrap4 = false; this.subscription = new rxjs.Subscription(); if (options) { this.autoFocus = options.autoFocus; this.useBootstrap4 = options.useBootstrap4; } } ContextMenuContentComponent.prototype.ngOnInit = function () { var _this = this; this.menuItems.forEach(function (menuItem) { menuItem.currentItem = _this.item; _this.subscription.add(menuItem.execute.subscribe(function (event) { return _this.execute.emit(Object.assign(Object.assign({}, event), { menuItem: menuItem })); })); }); var queryList = new core.QueryList(); queryList.reset(this.menuItems); this._keyManager = new a11y.ActiveDescendantKeyManager(queryList).withWrap(); }; ContextMenuContentComponent.prototype.ngAfterViewInit = function () { var _this = this; if (this.autoFocus) { setTimeout(function () { return _this.focus(); }); } this.overlay.updatePosition(); }; ContextMenuContentComponent.prototype.ngOnDestroy = function () { this.subscription.unsubscribe(); }; ContextMenuContentComponent.prototype.focus = function () { if (this.autoFocus) { this.menuElement.nativeElement.focus(); } }; ContextMenuContentComponent.prototype.stopEvent = function ($event) { $event.stopPropagation(); }; ContextMenuContentComponent.prototype.isMenuItemEnabled = function (menuItem) { return this.evaluateIfFunction(menuItem && menuItem.enabled); }; ContextMenuContentComponent.prototype.isMenuItemVisible = function (menuItem) { return this.evaluateIfFunction(menuItem && menuItem.visible); }; ContextMenuContentComponent.prototype.evaluateIfFunction = function (value) { if (value instanceof Function) { return value(this.item); } return value; }; ContextMenuContentComponent.prototype.isDisabled = function (link) { return link.enabled && !link.enabled(this.item); }; ContextMenuContentComponent.prototype.onKeyEvent = function (event) { if (!this.isLeaf) { return; } this._keyManager.onKeydown(event); }; ContextMenuContentComponent.prototype.keyboardOpenSubMenu = function (event) { if (!this.isLeaf) { return; } this.cancelEvent(event); var menuItem = this.menuItems[this._keyManager.activeItemIndex]; if (menuItem) { this.onOpenSubMenu(menuItem); } }; ContextMenuContentComponent.prototype.keyboardMenuItemSelect = function (event) { if (!this.isLeaf) { return; } this.cancelEvent(event); var menuItem = this.menuItems[this._keyManager.activeItemIndex]; if (menuItem) { this.onMenuItemSelect(menuItem, event); } }; ContextMenuContentComponent.prototype.onCloseLeafMenu = function (event) { if (!this.isLeaf) { return; } this.cancelEvent(event); this.closeLeafMenu.emit({ exceptRootMenu: event.keyCode === ARROW_LEFT_KEYCODE, event: event }); }; ContextMenuContentComponent.prototype.closeMenu = function (event) { if (event.type === 'click' && event.button === 2) { return; } this.closeAllMenus.emit({ event: event }); }; ContextMenuContentComponent.prototype.onOpenSubMenu = function (menuItem, event) { var anchorElementRef = this.menuItemElements.toArray()[this._keyManager.activeItemIndex]; var anchorElement = anchorElementRef && anchorElementRef.nativeElement; this.openSubMenu.emit({ anchorElement: anchorElement, contextMenu: menuItem.subMenu, event: event, item: this.item, parentContextMenu: this }); }; ContextMenuContentComponent.prototype.onMenuItemSelect = function (menuItem, event) { event.preventDefault(); event.stopPropagation(); this.onOpenSubMenu(menuItem, event); if (!menuItem.subMenu) { menuItem.triggerExecute(this.item, event); } }; ContextMenuContentComponent.prototype.cancelEvent = function (event) { if (!event) { return; } var target = event.target; if (['INPUT', 'TEXTAREA', 'SELECT'].indexOf(target.tagName) > -1 || target.isContentEditable) { return; } event.preventDefault(); event.stopPropagation(); }; return ContextMenuContentComponent; }()); ContextMenuContentComponent.decorators = [ { type: core.Component, args: [{ selector: 'context-menu-content', template: "\n <div\n class=\"dropdown open show ngx-contextmenu\"\n [ngClass]=\"menuClass\"\n tabindex=\"0\"\n >\n <ul\n #menu\n class=\"dropdown-menu show\"\n style=\"position: static; float: none;\"\n tabindex=\"0\"\n >\n <li\n #li\n *ngFor=\"let menuItem of menuItems; let i = index\"\n [class.disabled]=\"!isMenuItemEnabled(menuItem)\"\n [class.divider]=\"menuItem.divider\"\n [class.dropdown-divider]=\"useBootstrap4 && menuItem.divider\"\n [class.active]=\"menuItem.isActive && isMenuItemEnabled(menuItem)\"\n [attr.role]=\"menuItem.divider ? 'separator' : undefined\"\n >\n <a\n *ngIf=\"!menuItem.divider && !menuItem.passive\"\n href\n [class.dropdown-item]=\"useBootstrap4\"\n [class.active]=\"menuItem.isActive && isMenuItemEnabled(menuItem)\"\n [class.disabled]=\"useBootstrap4 && !isMenuItemEnabled(menuItem)\"\n [class.hasSubMenu]=\"!!menuItem.subMenu\"\n (click)=\"onMenuItemSelect(menuItem, $event)\"\n (mouseenter)=\"onOpenSubMenu(menuItem, $event)\"\n >\n <ng-template\n [ngTemplateOutlet]=\"menuItem.template\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-template>\n </a>\n\n <span\n (click)=\"stopEvent($event)\"\n (contextmenu)=\"stopEvent($event)\"\n class=\"passive\"\n *ngIf=\"!menuItem.divider && menuItem.passive\"\n [class.dropdown-item]=\"useBootstrap4\"\n [class.disabled]=\"useBootstrap4 && !isMenuItemEnabled(menuItem)\"\n >\n <ng-template\n [ngTemplateOutlet]=\"menuItem.template\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-template>\n </span>\n </li>\n </ul>\n </div>\n ", styles: ["\n .passive {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: @line-height-base;\n white-space: nowrap;\n }\n .hasSubMenu:before {\n content: '\u25B6';\n float: right;\n }\n "] },] } ]; /** @nocollapse */ ContextMenuContentComponent.ctorParameters = function () { return [ { type: core.ChangeDetectorRef }, { type: core.ElementRef }, { type: undefined, decorators: [{ type: core.Optional }, { type: core.Inject, args: [CONTEXT_MENU_OPTIONS,] }] } ]; }; ContextMenuContentComponent.propDecorators = { menuItems: [{ type: core.Input }], item: [{ type: core.Input }], event: [{ type: core.Input }], parentContextMenu: [{ type: core.Input }], menuClass: [{ type: core.Input }], overlay: [{ type: core.Input }], isLeaf: [{ type: core.Input }], execute: [{ type: core.Output }], openSubMenu: [{ type: core.Output }], closeLeafMenu: [{ type: core.Output }], closeAllMenus: [{ type: core.Output }], menuElement: [{ type: core.ViewChild, args: ['menu', { static: true },] }], menuItemElements: [{ type: core.ViewChildren, args: ['li',] }], onKeyEvent: [{ type: core.HostListener, args: ['window:keydown.ArrowDown', ['$event'],] }, { type: core.HostListener, args: ['window:keydown.ArrowUp', ['$event'],] }], keyboardOpenSubMenu: [{ type: core.HostListener, args: ['window:keydown.ArrowRight', ['$event'],] }], keyboardMenuItemSelect: [{ type: core.HostListener, args: ['window:keydown.Enter', ['$event'],] }, { type: core.HostListener, args: ['window:keydown.Space', ['$event'],] }], onCloseLeafMenu: [{ type: core.HostListener, args: ['window:keydown.Escape', ['$event'],] }, { type: core.HostListener, args: ['window:keydown.ArrowLeft', ['$event'],] }], closeMenu: [{ type: core.HostListener, args: ['document:click', ['$event'],] }, { type: core.HostListener, args: ['document:contextmenu', ['$event'],] }] }; var ContextMenuService = /** @class */ (function () { function ContextMenuService(overlay, scrollStrategy) { this.overlay = overlay; this.scrollStrategy = scrollStrategy; this.isDestroyingLeafMenu = false; this.show = new rxjs.Subject(); this.triggerClose = new rxjs.Subject(); this.close = new rxjs.Subject(); this.overlays = []; this.fakeElement = { getBoundingClientRect: function () { return ({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, }); } }; } ContextMenuService.prototype.openContextMenu = function (context) { var anchorElement = context.anchorElement, event = context.event, parentContextMenu = context.parentContextMenu; if (!parentContextMenu) { var mouseEvent_1 = event; this.fakeElement.getBoundingClientRect = function () { return ({ bottom: mouseEvent_1.clientY, height: 0, left: mouseEvent_1.clientX, right: mouseEvent_1.clientX, top: mouseEvent_1.clientY, width: 0, }); }; this.closeAllContextMenus({ eventType: 'cancel', event: event }); var positionStrategy = this.overlay.position().connectedTo(new core.ElementRef(anchorElement || this.fakeElement), { originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }) .withFallbackPosition({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }) .withFallbackPosition({ originX: 'end', originY: 'top' }, { overlayX: 'start', overlayY: 'top' }) .withFallbackPosition({ originX: 'start', originY: 'top' }, { overlayX: 'end', overlayY: 'top' }) .withFallbackPosition({ originX: 'end', originY: 'center' }, { overlayX: 'start', overlayY: 'center' }) .withFallbackPosition({ originX: 'start', originY: 'center' }, { overlayX: 'end', overlayY: 'center' }); this.overlays = [this.overlay.create({ positionStrategy: positionStrategy, panelClass: 'ngx-contextmenu', scrollStrategy: this.scrollStrategy.close(), })]; this.attachContextMenu(this.overlays[0], context); } else { var positionStrategy = this.overlay.position().connectedTo(new core.ElementRef(event ? event.target : anchorElement), { originX: 'end', originY: 'top' }, { overlayX: 'start', overlayY: 'top' }) .withFallbackPosition({ originX: 'start', originY: 'top' }, { overlayX: 'end', overlayY: 'top' }) .withFallbackPosition({ originX: 'end', originY: 'bottom' }, { overlayX: 'start', overlayY: 'bottom' }) .withFallbackPosition({ originX: 'start', originY: 'bottom' }, { overlayX: 'end', overlayY: 'bottom' }); var newOverlay = this.overlay.create({ positionStrategy: positionStrategy, panelClass: 'ngx-contextmenu', scrollStrategy: this.scrollStrategy.close(), }); this.destroySubMenus(parentContextMenu); this.overlays = this.overlays.concat(newOverlay); this.attachContextMenu(newOverlay, context); } }; ContextMenuService.prototype.attachContextMenu = function (overlay, context) { var _this = this; var event = context.event, item = context.item, menuItems = context.menuItems, menuClass = context.menuClass; var contextMenuContent = overlay.attach(new portal.ComponentPortal(ContextMenuContentComponent)); contextMenuContent.instance.event = event; contextMenuContent.instance.item = item; contextMenuContent.instance.menuItems = menuItems; contextMenuContent.instance.overlay = overlay; contextMenuContent.instance.isLeaf = true; contextMenuContent.instance.menuClass = menuClass; overlay.contextMenu = contextMenuContent.instance; var subscriptions = new rxjs.Subscription(); subscriptions.add(contextMenuContent.instance.execute.asObservable() .subscribe(function (executeEvent) { return _this.closeAllContextMenus(Object.assign({ eventType: 'execute' }, executeEvent)); })); subscriptions.add(contextMenuContent.instance.closeAllMenus.asObservable() .subscribe(function (closeAllEvent) { return _this.closeAllContextMenus(Object.assign({ eventType: 'cancel' }, closeAllEvent)); })); subscriptions.add(contextMenuContent.instance.closeLeafMenu.asObservable() .subscribe(function (closeLeafMenuEvent) { return _this.destroyLeafMenu(closeLeafMenuEvent); })); subscriptions.add(contextMenuContent.instance.openSubMenu.asObservable() .subscribe(function (subMenuEvent) { _this.destroySubMenus(contextMenuContent.instance); if (!subMenuEvent.contextMenu) { contextMenuContent.instance.isLeaf = true; return; } contextMenuContent.instance.isLeaf = false; _this.show.next(subMenuEvent); })); contextMenuContent.onDestroy(function () { menuItems.forEach(function (menuItem) { return menuItem.isActive = false; }); subscriptions.unsubscribe(); }); contextMenuContent.changeDetectorRef.detectChanges(); }; ContextMenuService.prototype.closeAllContextMenus = function (closeEvent) { if (this.overlays) { this.close.next(closeEvent); this.overlays.forEach(function (overlay, index) { overlay.detach(); overlay.dispose(); }); } this.overlays = []; }; ContextMenuService.prototype.getLastAttachedOverlay = function () { var overlay = this.overlays[this.overlays.length - 1]; while (this.overlays.length > 1 && overlay && !overlay.hasAttached()) { overlay.detach(); overlay.dispose(); this.overlays = this.overlays.slice(0, -1); overlay = this.overlays[this.overlays.length - 1]; } return overlay; }; ContextMenuService.prototype.destroyLeafMenu = function (_a) { var _this = this; var _b = _a === void 0 ? {} : _a, exceptRootMenu = _b.exceptRootMenu, event = _b.event; if (this.isDestroyingLeafMenu) { return; } this.isDestroyingLeafMenu = true; setTimeout(function () { var overlay = _this.getLastAttachedOverlay(); if (_this.overlays.length > 1 && overlay) { overlay.detach(); overlay.dispose(); } if (!exceptRootMenu && _this.overlays.length > 0 && overlay) { _this.close.next({ eventType: 'cancel', event: event }); overlay.detach(); overlay.dispose(); } var newLeaf = _this.getLastAttachedOverlay(); if (newLeaf) { newLeaf.contextMenu.isLeaf = true; } _this.isDestroyingLeafMenu = false; }); }; ContextMenuService.prototype.destroySubMenus = function (contextMenu) { var overlay = contextMenu.overlay; var index = this.overlays.indexOf(overlay); this.overlays.slice(index + 1).forEach(function (subMenuOverlay) { subMenuOverlay.detach(); subMenuOverlay.dispose(); }); }; ContextMenuService.prototype.isLeafMenu = function (contextMenuContent) { var overlay = this.getLastAttachedOverlay(); return contextMenuContent.overlay === overlay; }; return ContextMenuService; }()); ContextMenuService.decorators = [ { type: core.Injectable } ]; /** @nocollapse */ ContextMenuService.ctorParameters = function () { return [ { type: overlay.Overlay }, { type: overlay.ScrollStrategyOptions } ]; }; var ContextMenuAttachDirective = /** @class */ (function () { function ContextMenuAttachDirective(contextMenuService) { this.contextMenuService = contextMenuService; } ContextMenuAttachDirective.prototype.onContextMenu = function (event) { if (!this.contextMenu.disabled) { this.contextMenuService.show.next({ contextMenu: this.contextMenu, event: event, item: this.contextMenuSubject, }); event.preventDefault(); event.stopPropagation(); } }; return ContextMenuAttachDirective; }()); ContextMenuAttachDirective.decorators = [ { type: core.Directive, args: [{ selector: '[contextMenu]', },] } ]; /** @nocollapse */ ContextMenuAttachDirective.ctorParameters = function () { return [ { type: ContextMenuService } ]; }; ContextMenuAttachDirective.propDecorators = { contextMenuSubject: [{ type: core.Input }], contextMenu: [{ type: core.Input }], onContextMenu: [{ type: core.HostListener, args: ['contextmenu', ['$event'],] }] }; var ContextMenuItemDirective = /** @class */ (function () { function ContextMenuItemDirective(template, elementRef) { this.template = template; this.elementRef = elementRef; this.divider = false; this.enabled = true; this.passive = false; this.visible = true; this.execute = new core.EventEmitter(); this.isActive = false; } Object.defineProperty(ContextMenuItemDirective.prototype, "disabled", { get: function () { return this.passive || this.divider || !this.evaluateIfFunction(this.enabled, this.currentItem); }, enumerable: false, configurable: true }); ContextMenuItemDirective.prototype.evaluateIfFunction = function (value, item) { if (value instanceof Function) { return value(item); } return value; }; ContextMenuItemDirective.prototype.setActiveStyles = function () { this.isActive = true; }; ContextMenuItemDirective.prototype.setInactiveStyles = function () { this.isActive = false; }; ContextMenuItemDirective.prototype.triggerExecute = function (item, $event) { if (!this.evaluateIfFunction(this.enabled, item)) { return; } this.execute.emit({ event: $event, item: item }); }; return ContextMenuItemDirective; }()); ContextMenuItemDirective.decorators = [ { type: core.Directive, args: [{ /* tslint:disable:directive-selector-type */ selector: '[contextMenuItem]', },] } ]; /** @nocollapse */ ContextMenuItemDirective.ctorParameters = function () { return [ { type: core.TemplateRef }, { type: core.ElementRef } ]; }; ContextMenuItemDirective.propDecorators = { subMenu: [{ type: core.Input }], divider: [{ type: core.Input }], enabled: [{ type: core.Input }], passive: [{ type: core.Input }], visible: [{ type: core.Input }], execute: [{ type: core.Output }] }; var ContextMenuComponent = /** @class */ (function () { function ContextMenuComponent(_contextMenuService, changeDetector, elementRef, options) { var _this = this; this._contextMenuService = _contextMenuService; this.changeDetector = changeDetector; this.elementRef = elementRef; this.options = options; this.menuClass = ""; this.autoFocus = false; this.useBootstrap4 = false; this.disabled = false; this.close = new core.EventEmitter(); this.open = new core.EventEmitter(); this.visibleMenuItems = []; this.links = []; this.subscription = new rxjs.Subscription(); if (options) { this.autoFocus = options.autoFocus; this.useBootstrap4 = options.useBootstrap4; } this.subscription.add(_contextMenuService.show.subscribe(function (menuEvent) { _this.onMenuEvent(menuEvent); })); } ContextMenuComponent.prototype.ngOnDestroy = function () { this.subscription.unsubscribe(); }; ContextMenuComponent.prototype.onMenuEvent = function (menuEvent) { var _this = this; if (this.disabled) { return; } var contextMenu = menuEvent.contextMenu, event = menuEvent.event, item = menuEvent.item; if (contextMenu && contextMenu !== this) { return; } this.event = event; this.item = item; this.setVisibleMenuItems(); this._contextMenuService.openContextMenu(Object.assign(Object.assign({}, menuEvent), { menuItems: this.visibleMenuItems, menuClass: this.menuClass })); this._contextMenuService.close.asObservable().pipe(operators.first()).subscribe(function (closeEvent) { return _this.close.emit(closeEvent); }); this.open.next(menuEvent); }; ContextMenuComponent.prototype.isMenuItemVisible = function (menuItem) { return this.evaluateIfFunction(menuItem.visible); }; ContextMenuComponent.prototype.setVisibleMenuItems = function () { var _this = this; this.visibleMenuItems = this.menuItems.filter(function (menuItem) { return _this.isMenuItemVisible(menuItem); }); }; ContextMenuComponent.prototype.evaluateIfFunction = function (value) { if (value instanceof Function) { return value(this.item); } return value; }; return ContextMenuComponent; }()); ContextMenuComponent.decorators = [ { type: core.Component, args: [{ encapsulation: core.ViewEncapsulation.None, selector: 'context-menu', template: " ", styles: ["\n .cdk-overlay-container {\n position: fixed;\n z-index: 1000;\n pointer-events: none;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n }\n .ngx-contextmenu.cdk-overlay-pane {\n position: absolute;\n pointer-events: auto;\n box-sizing: border-box;\n }\n "] },] } ]; /** @nocollapse */ ContextMenuComponent.ctorParameters = function () { return [ { type: ContextMenuService }, { type: core.ChangeDetectorRef }, { type: core.ElementRef }, { type: undefined, decorators: [{ type: core.Optional }, { type: core.Inject, args: [CONTEXT_MENU_OPTIONS,] }] } ]; }; ContextMenuComponent.propDecorators = { menuClass: [{ type: core.Input }], autoFocus: [{ type: core.Input }], useBootstrap4: [{ type: core.Input }], disabled: [{ type: core.Input }], close: [{ type: core.Output }], open: [{ type: core.Output }], menuItems: [{ type: core.ContentChildren, args: [ContextMenuItemDirective,] }], menuElement: [{ type: core.ViewChild, args: ['menu', { static: false },] }] }; var ContextMenuModule = /** @class */ (function () { function ContextMenuModule() { } ContextMenuModule.forRoot = function (options) { return { ngModule: ContextMenuModule, providers: [ ContextMenuService, { provide: CONTEXT_MENU_OPTIONS, useValue: options, }, { provide: overlay.OverlayContainer, useClass: overlay.FullscreenOverlayContainer }, ], }; }; return ContextMenuModule; }()); ContextMenuModule.decorators = [ { type: core.NgModule, args: [{ declarations: [ ContextMenuAttachDirective, ContextMenuComponent, ContextMenuContentComponent, ContextMenuItemDirective, ], entryComponents: [ ContextMenuContentComponent, ], exports: [ ContextMenuAttachDirective, ContextMenuComponent, ContextMenuItemDirective, ], imports: [ common.CommonModule, overlay.OverlayModule, ], },] } ]; /* * Public API Surface of ngx-contextmenu */ /** * Generated bundle index. Do not edit. */ exports.ContextMenuAttachDirective = ContextMenuAttachDirective; exports.ContextMenuComponent = ContextMenuComponent; exports.ContextMenuItemDirective = ContextMenuItemDirective; exports.ContextMenuModule = ContextMenuModule; exports.ContextMenuService = ContextMenuService; exports.ɵa = CONTEXT_MENU_OPTIONS; exports.ɵb = ContextMenuContentComponent; Object.defineProperty(exports, '__esModule', { value: true }); }))); //# sourceMappingURL=hoso-ngx-contextmenu.umd.js.map