@perfectmemory/ngx-contextmenu
Version:
A context menu component for Angular
957 lines (943 loc) • 37.9 kB
JavaScript
import * as i1 from '@angular/cdk/overlay';
import { OverlayModule, FullscreenOverlayContainer, OverlayContainer } from '@angular/cdk/overlay';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { EventEmitter, Output, Input, Optional, Directive, Injectable, DOCUMENT, HostListener, HostBinding, ViewChildren, Inject, ChangeDetectionStrategy, Component, ElementRef, ContentChildren, ViewEncapsulation, NgModule } from '@angular/core';
import { FocusKeyManager } from '@angular/cdk/a11y';
import { Subscription } from 'rxjs';
import { ComponentPortal } from '@angular/cdk/portal';
const evaluateIfFunction = (value, item) => {
if (value instanceof Function) {
return value(item);
}
return !!value;
};
class ContextMenuItemDirective {
/**
* Is this menu item disabled
*/
set disabled(disabled) {
this.#disabled = disabled;
}
get disabled() {
return (this.passive ||
this.divider ||
evaluateIfFunction(this.#disabled, this.value));
}
#disabled;
constructor(template) {
this.template = template;
/**
* True to make this menu item a divider
*/
this.divider = false;
/**
* Is this menu item passive (for title)
*/
this.passive = false;
/**
* Is this menu item visible
*/
this.visible = true;
/**
* Emits event and item
*/
this.execute = new EventEmitter();
this.#disabled = false;
}
/**
* @internal
*/
triggerExecute(event, value) {
if (evaluateIfFunction(this.#disabled, value)) {
return;
}
this.execute.emit({ event, value });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuItemDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: ContextMenuItemDirective, isStandalone: false, selector: "[contextMenuItem]", inputs: { subMenu: "subMenu", divider: "divider", disabled: "disabled", passive: "passive", visible: "visible" }, outputs: { execute: "execute" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[contextMenuItem]',
standalone: false,
}]
}], ctorParameters: () => [{ type: i0.TemplateRef, decorators: [{
type: Optional
}] }], propDecorators: { subMenu: [{
type: Input
}], divider: [{
type: Input
}], disabled: [{
type: Input
}], passive: [{
type: Input
}], visible: [{
type: Input
}], execute: [{
type: Output
}] } });
class ContextMenuContentItemDirective {
get nativeElement() {
return this.elementRef.nativeElement;
}
constructor(elementRef) {
this.elementRef = elementRef;
}
/**
* @implements FocusableOption
*/
focus(origin) {
this.elementRef.nativeElement.focus();
}
/**
* @implements FocusableOption
*/
get disabled() {
return evaluateIfFunction(this.contextMenuContentItem?.disabled, this.contextMenuContentItem?.value);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuContentItemDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: ContextMenuContentItemDirective, isStandalone: false, selector: "[contextMenuContentItem]", inputs: { contextMenuContentItem: "contextMenuContentItem" }, host: { classAttribute: "ngx-context-menu-item" }, exportAs: ["contextMenuContentItem"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuContentItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[contextMenuContentItem]',
exportAs: 'contextMenuContentItem',
host: {
class: 'ngx-context-menu-item',
},
standalone: false,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { contextMenuContentItem: [{
type: Input
}] } });
class ContextMenuOverlaysService {
constructor() {
/**
* Emits when all context menus are closed
*/
this.allClosed = new EventEmitter();
this.stack = [];
}
/**
* Add an item to the stack
*/
push(value) {
this.stack.push(value);
}
/**
* Clear the whole stack
*/
closeAll() {
this.stack.forEach((item) => this.dispose(item));
this.stack = [];
this.allClosed.emit();
}
isEmpty() {
return this.stack.length === 0;
}
dispose(overlayRef) {
overlayRef.detach();
overlayRef.dispose();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuOverlaysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuOverlaysService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuOverlaysService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* For testing purpose only
*/
const TESTING_WRAPPER = {
FocusKeyManager,
};
class ContextMenuContentComponent {
// TODO: should be private but issue in spec with NullInjectorError: No provider for ElementRef!
constructor(elementRef, document, contextMenuOverlaysService) {
this.elementRef = elementRef;
this.document = document;
this.contextMenuOverlaysService = contextMenuOverlaysService;
/**
* The list of `IContextMenuItemDirective` that represent each menu items
*/
this.menuDirectives = [];
/**
* A CSS class to apply a theme to the the menu
*/
this.menuClass = '';
/**
* Emit when a menu item is selected
*/
this.execute = new EventEmitter();
/**
* Emit when all menus is closed
*/
this.close = new EventEmitter();
/**
* Accessibility
*
* @internal
*/
this.role = 'menu';
/**
* Accessibility
*
* @internal
*/
this.ariaOrientation = 'vertical';
this.subscription = new Subscription();
}
/**
* @internal
*/
ngAfterViewInit() {
this.setupDirectives();
this.activeElement = this.document.activeElement;
this.elementRef.nativeElement.focus();
}
/**
* @internal
*/
ngOnDestroy() {
this.activeElement?.focus();
this.subscription.unsubscribe();
this.focusKeyManager?.destroy();
}
/**
* @internal
*/
onKeyArrowDownOrUp(event) {
this.focusKeyManager?.onKeydown(event);
}
/**
* @internal
*/
onKeyArrowRight(event) {
this.openCloseActiveItemSubMenu(this.dir !== 'rtl', event);
}
/**
* @internal
*/
onKeyArrowLeft(event) {
this.openCloseActiveItemSubMenu(this.dir === 'rtl', event);
}
/**
* @internal
*/
onKeyEnterOrSpace(event) {
if (!this.focusKeyManager?.activeItem) {
return;
}
this.onMenuItemSelect(this.focusKeyManager.activeItem, event);
}
/**
* @internal
*/
onClickOrRightClick(event) {
if (event.type === 'click' && event.button === 2) {
return;
}
if (this.elementRef.nativeElement.contains(event.target)) {
return;
}
this.contextMenuOverlaysService.closeAll();
}
/**
* @internal
*/
hideSubMenus() {
this.menuDirectives.forEach((menuDirective) => {
menuDirective.subMenu?.hide();
});
}
/**
* @internal
*/
stopEvent(event) {
event.stopPropagation();
}
/**
* @internal
*/
isMenuItemDisabled(menuItem) {
return evaluateIfFunction(menuItem.disabled, this.value);
}
/**
* @internal
*/
isMenuItemVisible(menuItem) {
return evaluateIfFunction(menuItem.contextMenuContentItem?.visible, this.value);
}
/**
* @internal
*/
openSubMenu(subMenu, event) {
if (!subMenu) {
return;
}
if (this.focusKeyManager?.activeItemIndex === null) {
return;
}
const anchorContextMenuContentItem = this.contextMenuContentItems?.toArray()[this.focusKeyManager?.activeItemIndex ?? 0];
const anchorElement = anchorContextMenuContentItem &&
anchorContextMenuContentItem.nativeElement;
if (anchorElement && event instanceof KeyboardEvent) {
subMenu.show({
anchoredTo: 'element',
anchorElement,
value: this.value,
parentContextMenu: this,
});
return;
}
if (subMenu.isOpen) {
return;
}
if (event.currentTarget) {
subMenu.show({
anchoredTo: 'element',
anchorElement: event.currentTarget,
value: this.value,
parentContextMenu: this,
});
return;
}
subMenu.show({
anchoredTo: 'position',
x: event.clientX,
y: event.clientY,
value: this.value,
});
}
/**
* @internal
*/
onMenuItemSelect(menuContentItem, event) {
this.cancelEvent(event);
this.openSubMenu(menuContentItem.contextMenuContentItem?.subMenu, event);
if (!menuContentItem.contextMenuContentItem?.subMenu) {
this.triggerExecute(menuContentItem, event);
}
}
triggerExecute(menuItem, event) {
menuItem.contextMenuContentItem?.triggerExecute(event, this.value);
}
setupDirectives() {
this.menuDirectives.forEach((menuDirective) => {
menuDirective.value = this.value;
this.subscription.add(menuDirective.execute.subscribe((event) => {
this.execute.emit({ ...event, menuDirective });
}));
});
this.focusKeyManager = new TESTING_WRAPPER.FocusKeyManager(this.contextMenuContentItems).withWrap();
}
openCloseActiveItemSubMenu(open, event) {
if (open) {
this.openActiveItemSubMenu(event);
return;
}
this.closeActiveItemSubMenu(event);
}
openActiveItemSubMenu(event) {
if (this.focusKeyManager?.activeItemIndex === null) {
return;
}
this.cancelEvent(event);
if (this.focusKeyManager?.activeItem) {
this.openSubMenu(this.focusKeyManager.activeItem?.contextMenuContentItem?.subMenu, event);
}
}
closeActiveItemSubMenu(event) {
this.hideSubMenus();
if (!this.focusKeyManager?.activeItemIndex) {
return;
}
this.close.emit();
this.cancelEvent(event);
}
cancelEvent(event) {
if (!event || !event.target) {
return;
}
const target = event.target;
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) ||
target.isContentEditable) {
return;
}
event.preventDefault();
event.stopPropagation();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuContentComponent, deps: [{ token: i0.ElementRef }, { token: DOCUMENT }, { token: ContextMenuOverlaysService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: ContextMenuContentComponent, isStandalone: false, selector: "context-menu-content", inputs: { menuDirectives: "menuDirectives", value: "value", dir: "dir", parentContextMenu: "parentContextMenu", menuClass: "menuClass" }, outputs: { execute: "execute", close: "close" }, host: { attributes: { "tabindex": "0", "role": "dialog" }, listeners: { "keydown.ArrowDown": "onKeyArrowDownOrUp($event)", "keydown.ArrowUp": "onKeyArrowDownOrUp($event)", "keydown.ArrowRight": "onKeyArrowRight($event)", "keydown.ArrowLeft": "onKeyArrowLeft($event)", "window:keydown.Enter": "onKeyEnterOrSpace($event)", "window:keydown.Space": "onKeyEnterOrSpace($event)", "document:click": "onClickOrRightClick($event)" }, properties: { "attr.dir": "this.dir", "class": "this.menuClass", "attr.role": "this.role", "aria-orientation": "this.ariaOrientation" }, classAttribute: "ngx-contextmenu" }, viewQueries: [{ propertyName: "contextMenuContentItems", predicate: ContextMenuContentItemDirective, descendants: true, read: ContextMenuContentItemDirective }], ngImport: i0, template: "@for (menuDirective of menuDirectives; track menuDirective; let i = $index) {\n@if (menuDirective.divider) {\n<hr\n [contextMenuContentItem]=\"menuDirective\"\n role=\"separator\"\n (mouseenter)=\"hideSubMenus()\"\n/>\n} @if (!menuDirective.divider && !menuDirective.passive) {\n<button\n #buttonContextMenuContentItem=\"contextMenuContentItem\"\n [contextMenuContentItem]=\"menuDirective\"\n type=\"button\"\n role=\"menuitem\"\n [attr.aria-haspopup]=\"!!menuDirective.subMenu ? 'menu' : null\"\n [class.ngx-contextmenu--parent-menu]=\"!!menuDirective.subMenu\"\n [attr.disabled]=\"isMenuItemDisabled(menuDirective) ? 'disabled' : null\"\n (click)=\"onMenuItemSelect(buttonContextMenuContentItem, $event)\"\n (mouseenter)=\"\n hideSubMenus();\n openSubMenu(\n buttonContextMenuContentItem.contextMenuContentItem?.subMenu,\n $event\n )\n \"\n>\n <ng-template\n [ngTemplateOutlet]=\"menuDirective.template\"\n [ngTemplateOutletContext]=\"{ $implicit: value }\"\n ></ng-template>\n</button>\n} @if (!menuDirective.divider && menuDirective.passive) {\n<span\n [contextMenuContentItem]=\"menuDirective\"\n class=\"ngx-contextmenu-item--passive\"\n role=\"menuitem\"\n [attr.aria-disabled]=\"isMenuItemDisabled(menuDirective)\"\n (click)=\"stopEvent($event)\"\n (contextmenu)=\"stopEvent($event)\"\n [class.disabled]=\"isMenuItemDisabled(menuDirective)\"\n (mouseenter)=\"isMenuItemDisabled(menuDirective) ? null : hideSubMenus()\"\n>\n <ng-template\n [ngTemplateOutlet]=\"menuDirective.template\"\n [ngTemplateOutletContext]=\"{ $implicit: value }\"\n ></ng-template>\n</span>\n} }\n", dependencies: [{ kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: ContextMenuContentItemDirective, selector: "[contextMenuContentItem]", inputs: ["contextMenuContentItem"], exportAs: ["contextMenuContentItem"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuContentComponent, decorators: [{
type: Component,
args: [{ selector: 'context-menu-content', changeDetection: ChangeDetectionStrategy.OnPush, host: {
tabindex: '0',
role: 'dialog',
class: 'ngx-contextmenu',
}, standalone: false, template: "@for (menuDirective of menuDirectives; track menuDirective; let i = $index) {\n@if (menuDirective.divider) {\n<hr\n [contextMenuContentItem]=\"menuDirective\"\n role=\"separator\"\n (mouseenter)=\"hideSubMenus()\"\n/>\n} @if (!menuDirective.divider && !menuDirective.passive) {\n<button\n #buttonContextMenuContentItem=\"contextMenuContentItem\"\n [contextMenuContentItem]=\"menuDirective\"\n type=\"button\"\n role=\"menuitem\"\n [attr.aria-haspopup]=\"!!menuDirective.subMenu ? 'menu' : null\"\n [class.ngx-contextmenu--parent-menu]=\"!!menuDirective.subMenu\"\n [attr.disabled]=\"isMenuItemDisabled(menuDirective) ? 'disabled' : null\"\n (click)=\"onMenuItemSelect(buttonContextMenuContentItem, $event)\"\n (mouseenter)=\"\n hideSubMenus();\n openSubMenu(\n buttonContextMenuContentItem.contextMenuContentItem?.subMenu,\n $event\n )\n \"\n>\n <ng-template\n [ngTemplateOutlet]=\"menuDirective.template\"\n [ngTemplateOutletContext]=\"{ $implicit: value }\"\n ></ng-template>\n</button>\n} @if (!menuDirective.divider && menuDirective.passive) {\n<span\n [contextMenuContentItem]=\"menuDirective\"\n class=\"ngx-contextmenu-item--passive\"\n role=\"menuitem\"\n [attr.aria-disabled]=\"isMenuItemDisabled(menuDirective)\"\n (click)=\"stopEvent($event)\"\n (contextmenu)=\"stopEvent($event)\"\n [class.disabled]=\"isMenuItemDisabled(menuDirective)\"\n (mouseenter)=\"isMenuItemDisabled(menuDirective) ? null : hideSubMenus()\"\n>\n <ng-template\n [ngTemplateOutlet]=\"menuDirective.template\"\n [ngTemplateOutletContext]=\"{ $implicit: value }\"\n ></ng-template>\n</span>\n} }\n" }]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: Document, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: ContextMenuOverlaysService }], propDecorators: { menuDirectives: [{
type: Input
}], value: [{
type: Input
}], dir: [{
type: Input
}, {
type: HostBinding,
args: ['attr.dir']
}], parentContextMenu: [{
type: Input
}], menuClass: [{
type: HostBinding,
args: ['class']
}, {
type: Input
}], execute: [{
type: Output
}], close: [{
type: Output
}], contextMenuContentItems: [{
type: ViewChildren,
args: [ContextMenuContentItemDirective, {
read: ContextMenuContentItemDirective,
}]
}], role: [{
type: HostBinding,
args: ['attr.role']
}], ariaOrientation: [{
type: HostBinding,
args: ['aria-orientation']
}], onKeyArrowDownOrUp: [{
type: HostListener,
args: ['keydown.ArrowDown', ['$event']]
}, {
type: HostListener,
args: ['keydown.ArrowUp', ['$event']]
}], onKeyArrowRight: [{
type: HostListener,
args: ['keydown.ArrowRight', ['$event']]
}], onKeyArrowLeft: [{
type: HostListener,
args: ['keydown.ArrowLeft', ['$event']]
}], onKeyEnterOrSpace: [{
type: HostListener,
args: ['window:keydown.Enter', ['$event']]
}, {
type: HostListener,
args: ['window:keydown.Space', ['$event']]
}], onClickOrRightClick: [{
type: HostListener,
args: ['document:click', ['$event']]
}] } });
const getPositionsToXY = (dir = 'ltr') => {
if (dir === 'ltr') {
return [
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom',
},
{
originX: 'end',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'center',
overlayX: 'start',
overlayY: 'center',
},
{
originX: 'start',
originY: 'center',
overlayX: 'end',
overlayY: 'center',
},
];
}
return [
{
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom',
},
{
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'center',
overlayX: 'end',
overlayY: 'center',
},
{
originX: 'end',
originY: 'center',
overlayX: 'start',
overlayY: 'center',
},
];
};
const getPositionsToAnchorElement = (dir = 'ltr') => {
if (dir === 'ltr') {
return [
{
originX: 'end',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'bottom',
overlayX: 'start',
overlayY: 'bottom',
},
{
originX: 'start',
originY: 'bottom',
overlayX: 'end',
overlayY: 'bottom',
},
];
}
else {
return [
{
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top',
},
{
originX: 'end',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'bottom',
overlayX: 'end',
overlayY: 'bottom',
},
{
originX: 'end',
originY: 'bottom',
overlayX: 'start',
overlayY: 'bottom',
},
];
}
};
class ContextMenuComponent {
/**
* Returns true if the context menu is opened, false otherwise
*/
get isOpen() {
return this.#isOpen;
}
#isOpen;
constructor(overlay, scrollStrategy, contextMenuOverlaysService) {
this.overlay = overlay;
this.scrollStrategy = scrollStrategy;
this.contextMenuOverlaysService = contextMenuOverlaysService;
/**
* A CSS class to apply to the context menu overlay, ideal for theming and custom styling
*/
this.menuClass = '';
/**
* Disable the whole context menu
*/
this.disabled = false;
/**
* Emit when the menu is opened
*/
this.open = new EventEmitter();
/**
* Emit when the menu is closed
*/
this.close = new EventEmitter();
/**
* @internal
*/
this.visibleMenuItems = [];
this.subscriptions = new Subscription();
this.#isOpen = false;
}
/**
* @internal
*/
ngOnInit() {
const subscription = this.contextMenuOverlaysService.allClosed.subscribe(() => {
this.#isOpen = false;
});
this.subscriptions.add(subscription);
}
/**
* @internal
*/
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
/**
* @internal
*/
show(event) {
if (this.disabled) {
return;
}
this.value = event.value;
this.setVisibleMenuItems();
this.openContextMenu({
...event,
menuItemDirectives: this.visibleMenuItems,
menuClass: this.menuClass,
dir: this.dir,
});
this.open.next(event);
}
/**
* @internal
*/
hide() {
this.contextMenuContentComponent?.menuDirectives.forEach((menuDirective) => {
menuDirective.subMenu?.hide();
});
this.overlayRef?.detach();
this.overlayRef?.dispose();
this.#isOpen = false;
}
/**
* @internal
*/
openContextMenu(context) {
let positionStrategy;
if (context.anchoredTo === 'position') {
positionStrategy = this.overlay
.position()
.flexibleConnectedTo({
x: context.x,
y: context.y,
})
.withPositions(getPositionsToXY(context.dir));
}
else {
const { anchorElement, parentContextMenu } = context;
positionStrategy = this.overlay
.position()
.flexibleConnectedTo(new ElementRef(anchorElement))
.withPositions(getPositionsToAnchorElement(parentContextMenu.dir));
}
this.overlayRef = this.overlay.create({
positionStrategy,
panelClass: 'ngx-contextmenu-overlay',
scrollStrategy: this.scrollStrategy.close(),
});
this.contextMenuOverlaysService.push(this.overlayRef);
this.attachContextMenu(context);
this.#isOpen = true;
}
attachContextMenu(context) {
const { value, menuItemDirectives } = context;
const contextMenuContentRef = this.overlayRef?.attach(new ComponentPortal(ContextMenuContentComponent));
const contextMenuContentComponent = contextMenuContentRef?.instance;
if (!contextMenuContentComponent) {
return;
}
this.contextMenuContentComponent = contextMenuContentComponent;
contextMenuContentComponent.value = value;
contextMenuContentComponent.menuDirectives = menuItemDirectives;
contextMenuContentComponent.menuClass = this.getMenuClass(context);
contextMenuContentComponent.dir = this.getDir(context);
const closeSubscription = contextMenuContentComponent.close.subscribe(() => {
this.overlayRef?.detach();
this.overlayRef?.dispose();
});
const executeSubscription = contextMenuContentComponent.execute.subscribe(() => {
this.contextMenuOverlaysService.closeAll();
});
contextMenuContentRef.onDestroy(() => {
this.close.emit();
closeSubscription.unsubscribe();
executeSubscription.unsubscribe();
});
contextMenuContentRef.changeDetectorRef.detectChanges();
}
getMenuClass(event) {
return (event.menuClass ||
(event.anchoredTo === 'element' && event?.parentContextMenu?.menuClass) ||
'');
}
getDir(event) {
return (event.dir ||
(event.anchoredTo === 'element' && event?.parentContextMenu?.dir) ||
undefined);
}
isMenuItemVisible(menuItem) {
return evaluateIfFunction(menuItem.visible, this.value);
}
setVisibleMenuItems() {
this.visibleMenuItems =
this.menuItems?.filter((menuItem) => this.isMenuItemVisible(menuItem)) ??
[];
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuComponent, deps: [{ token: i1.Overlay }, { token: i1.ScrollStrategyOptions }, { token: ContextMenuOverlaysService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.3", type: ContextMenuComponent, isStandalone: false, selector: "context-menu", inputs: { menuClass: "menuClass", disabled: "disabled", dir: "dir" }, outputs: { open: "open", close: "close" }, queries: [{ propertyName: "menuItems", predicate: ContextMenuItemDirective }], ngImport: i0, template: '', isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuComponent, decorators: [{
type: Component,
args: [{
encapsulation: ViewEncapsulation.None,
selector: 'context-menu',
template: '',
standalone: false,
}]
}], ctorParameters: () => [{ type: i1.Overlay }, { type: i1.ScrollStrategyOptions }, { type: ContextMenuOverlaysService }], propDecorators: { menuClass: [{
type: Input
}], disabled: [{
type: Input
}], dir: [{
type: Input
}], open: [{
type: Output
}], close: [{
type: Output
}], menuItems: [{
type: ContentChildren,
args: [ContextMenuItemDirective]
}] } });
class ContextMenuDirective {
/**
* Return true if the context menu is opened, false otherwise
*/
get isOpen() {
return this.contextMenu?.isOpen ?? false;
}
constructor(elementRef, contextMenuOverlaysService) {
this.elementRef = elementRef;
this.contextMenuOverlaysService = contextMenuOverlaysService;
/**
* The directive must have a tabindex for being accessible
*/
this.tabindex = '0';
}
/**
* @internal
*/
onContextMenu(event) {
if (!this.canOpen()) {
return;
}
this.closeAll();
this.contextMenu?.show({
anchoredTo: 'position',
x: event.clientX,
y: event.clientY,
value: this.contextMenuValue,
});
event.preventDefault();
event.stopPropagation();
}
/**
* @internal
*/
onKeyArrowEscape() {
this.close();
}
/**
* Programmatically open the context menu
*/
open(event) {
if (!this.canOpen()) {
return;
}
if (event instanceof MouseEvent) {
this.onContextMenu(event);
return;
}
const { x, y, height } = this.elementRef.nativeElement.getBoundingClientRect();
this.contextMenu?.show({
anchoredTo: 'position',
x,
y: y + height,
value: this.contextMenuValue,
});
}
/**
* Programmatically close the context menu
*/
close() {
this.contextMenu?.hide();
}
closeAll() {
this.contextMenuOverlaysService.closeAll();
}
canOpen() {
return (this.contextMenu && !this.contextMenu.disabled) ?? false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuDirective, deps: [{ token: i0.ElementRef }, { token: ContextMenuOverlaysService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: ContextMenuDirective, isStandalone: false, selector: "[contextMenu]", inputs: { contextMenuValue: "contextMenuValue", contextMenu: "contextMenu", tabindex: "tabindex" }, host: { attributes: { "role": "button", "attr.aria-haspopup": "menu" }, listeners: { "contextmenu": "onContextMenu($event)", "window:contextmenu": "onKeyArrowEscape()", "window:keydown.Escape": "onKeyArrowEscape()" }, properties: { "attr.tabindex": "this.tabindex", "attr.aria-expanded": "this.isOpen" } }, exportAs: ["ngxContextMenu"], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuDirective, decorators: [{
type: Directive,
args: [{
selector: '[contextMenu]',
exportAs: 'ngxContextMenu',
host: {
role: 'button',
'attr.aria-haspopup': 'menu',
},
standalone: false,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: ContextMenuOverlaysService }], propDecorators: { contextMenuValue: [{
type: Input
}], contextMenu: [{
type: Input
}], tabindex: [{
type: Input
}, {
type: HostBinding,
args: ['attr.tabindex']
}], isOpen: [{
type: HostBinding,
args: ['attr.aria-expanded']
}], onContextMenu: [{
type: HostListener,
args: ['contextmenu', ['$event']]
}], onKeyArrowEscape: [{
type: HostListener,
args: ['window:contextmenu']
}, {
type: HostListener,
args: ['window:keydown.Escape']
}] } });
class ContextMenuModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuModule, declarations: [ContextMenuComponent,
ContextMenuContentComponent,
ContextMenuContentItemDirective,
ContextMenuDirective,
ContextMenuItemDirective], imports: [CommonModule, OverlayModule], exports: [ContextMenuDirective,
ContextMenuComponent,
ContextMenuItemDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuModule, providers: [
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
], imports: [CommonModule, OverlayModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuModule, decorators: [{
type: NgModule,
args: [{
declarations: [
ContextMenuComponent,
ContextMenuContentComponent,
ContextMenuContentItemDirective,
ContextMenuDirective,
ContextMenuItemDirective,
],
providers: [
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
],
exports: [
ContextMenuDirective,
ContextMenuComponent,
ContextMenuItemDirective,
],
imports: [CommonModule, OverlayModule],
}]
}] });
/**
* Programmatically open a ContextMenuComponent to a X/Y position
*/
class ContextMenuService {
constructor(contextMenuOverlaysService) {
this.contextMenuOverlaysService = contextMenuOverlaysService;
}
/**
* Show the given `ContextMenuComponent` at a specified X/Y position
*/
show(contextMenu, options = { x: 0, y: 0 }) {
contextMenu.show({
anchoredTo: 'position',
value: options.value,
x: options.x,
y: options.y,
});
}
/**
* Close all open `ContextMenuComponent`
*/
closeAll() {
this.contextMenuOverlaysService.closeAll();
}
/**
* Return true if any `ContextMenuComponent` is open
*/
hasOpenMenu() {
return !this.contextMenuOverlaysService.isEmpty();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuService, deps: [{ token: ContextMenuOverlaysService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: ContextMenuService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: ContextMenuOverlaysService }] });
/*
* Public API Surface of @perfectmemory/ngx-contextmenu
*/
/**
* Generated bundle index. Do not edit.
*/
export { ContextMenuComponent, ContextMenuDirective, ContextMenuItemDirective, ContextMenuModule, ContextMenuService };
//# sourceMappingURL=perfectmemory-ngx-contextmenu.mjs.map