ng2-right-click-menu
Version:
Right click context menu for Angular
661 lines (654 loc) • 19.2 kB
JavaScript
import { EventEmitter, Directive, TemplateRef, Optional, Input, Output, Injectable, QueryList, ElementRef, Component, ViewEncapsulation, ContentChildren, ViewChildren, ViewChild, ViewContainerRef, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { fromEvent, merge } from 'rxjs';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class MenuItemContext {
constructor() {
this.$implicit = {};
}
}
class ShContextMenuItemDirective {
/**
* @param {?} template
*/
constructor(template) {
this.template = template;
this.closeOnClick = true;
this.click = new EventEmitter();
this.context = new MenuItemContext();
}
/**
* @return {?}
*/
setNotActive() {
this._active = false;
if (this.subMenu) {
this.subMenu.setNotActive();
}
}
/**
* @return {?}
*/
setActive() {
this._active = true;
}
}
ShContextMenuItemDirective.decorators = [
{ type: Directive, args: [{
selector: '[shContextMenuItem]'
},] }
];
/** @nocollapse */
ShContextMenuItemDirective.ctorParameters = () => [
{ type: TemplateRef, decorators: [{ type: Optional }] }
];
ShContextMenuItemDirective.propDecorators = {
subMenu: [{ type: Input }],
divider: [{ type: Input }],
visible: [{ type: Input }],
disabled: [{ type: Input }],
closeOnClick: [{ type: Input }],
click: [{ type: Output }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class ShContextMenuService {
/**
* @param {?} overlay
*/
constructor(overlay) {
this.overlay = overlay;
this.activeOverlays = [];
}
/**
* @param {?} ctxEvent
* @return {?}
*/
openMenu(ctxEvent) {
this.closeCurrentOverlays();
const { menu, mouseEvent, data } = ctxEvent;
this.activeMenu = menu;
this.anchorElement = this.createAnchorElement();
/** @type {?} */
const scrollStrategy = this.buildScrollStrategy();
/** @type {?} */
const positionStrategy = this.buildPositionStrategy(this.anchorElement, mouseEvent);
this.attachContextToItems(menu, data);
/** @type {?} */
const overlayRef = this.createAndAttachOverlay(positionStrategy, scrollStrategy, menu, true);
this.attachOverlayRef(menu, overlayRef);
this.registerDetachEvents(overlayRef);
}
/**
* @param {?} ctxEvent
* @return {?}
*/
openSubMenu(ctxEvent) {
const { menu, mouseEvent, targetElement, data, parentMenu } = ctxEvent;
mouseEvent.preventDefault();
mouseEvent.stopPropagation();
/** @type {?} */
const scrollStrategy = this.buildScrollStrategy();
/** @type {?} */
const positionStrategy = this.buildPositionStrategyForSubMenu(targetElement);
/** @type {?} */
const overlayRef = this.createAndAttachOverlay(positionStrategy, scrollStrategy, menu, false);
this.attachContextToItems(menu, data);
this.attachThisContext(menu, parentMenu);
this.attachOverlayRef(menu, overlayRef);
}
/**
* @return {?}
*/
destroy() {
this.closeCurrentOverlays();
this.subs.unsubscribe();
}
/**
* @return {?}
*/
ngOnDestroy() {
this.destroy();
}
/**
* @param {?} menu
* @return {?}
*/
closeSubMenus(menu) {
/** @type {?} */
const itemsWithSubMenus = menu.menuItems.filter((/**
* @param {?} i
* @return {?}
*/
i => !!i.subMenu && !!i.subMenu.overlayRef));
if (itemsWithSubMenus.length) {
itemsWithSubMenus.forEach((/**
* @param {?} sm
* @return {?}
*/
sm => this.closeSubMenus(sm.subMenu)));
/** @type {?} */
const overlayRefs = itemsWithSubMenus.map((/**
* @param {?} i
* @return {?}
*/
i => i.subMenu.overlayRef));
overlayRefs.forEach((/**
* @param {?} r
* @return {?}
*/
r => r.dispose()));
}
}
/**
* @private
* @param {?} overlayRef
* @return {?}
*/
registerDetachEvents(overlayRef) {
this.subs = overlayRef
.backdropClick()
.subscribe(this.closeCurrentOverlays.bind(this));
this.subs.add(overlayRef.detachments().subscribe(this.closeCurrentOverlays.bind(this)));
}
/**
* @private
* @param {?} positionStrategy
* @param {?} scrollStrategy
* @param {?} menu
* @param {?=} hasBackdrop
* @return {?}
*/
createAndAttachOverlay(positionStrategy, scrollStrategy, menu, hasBackdrop = true) {
/** @type {?} */
const overlayRef = this.overlay.create({
positionStrategy,
scrollStrategy,
hasBackdrop: hasBackdrop,
backdropClass: 'sh-backdrop'
});
/*
TODO: try passing the TemplatePortal context (data)
and then injecting it to the *ngTemplateOutlet in the component template
*/
/** @type {?} */
const menuPortal = new TemplatePortal(menu.menuTemplate, menu.menuContainer);
overlayRef.attach(menuPortal);
this.activeOverlays.push(overlayRef);
return overlayRef;
}
/**
* @private
* @return {?}
*/
buildScrollStrategy() {
return this.overlay.scrollStrategies.reposition({ autoClose: true });
}
/**
* @private
* @param {?} ele
* @param {?} event
* @return {?}
*/
buildPositionStrategy(ele, event) {
const { x, y } = event;
return this.overlay
.position()
.flexibleConnectedTo(ele)
.withDefaultOffsetX(x)
.withDefaultOffsetY(y)
.withPositions(this.buildPositions())
.withFlexibleDimensions(false)
.withPush(true);
}
/**
* @private
* @param {?} elm
* @return {?}
*/
buildPositionStrategyForSubMenu(elm) {
return this.overlay
.position()
.flexibleConnectedTo(elm)
.withPositions(this.buildSubMenuPositions())
.withFlexibleDimensions(false)
.withPush(true);
}
/**
* @private
* @return {?}
*/
closeCurrentOverlays() {
if (this.anchorElement) {
this.anchorElement.remove();
}
this.activeOverlays.forEach((/**
* @param {?} o
* @return {?}
*/
o => {
o.detach();
o.dispose();
}));
this.activeOverlays = [];
// TODO: create close subject and emit.
// subscribe in component
if (this.activeMenu) {
this.activeMenu.close();
}
}
/**
* @private
* @param {?} menu
* @param {?} data
* @return {?}
*/
attachContextToItems(menu, data) {
menu.menuItems.forEach((/**
* @param {?} i
* @return {?}
*/
i => (i.context.$implicit = data)));
}
/**
* @private
* @param {?} menu
* @param {?} parentMenu
* @return {?}
*/
attachThisContext(menu, parentMenu) {
menu.thisContext = parentMenu.thisContext;
}
/**
* @private
* @param {?} menu
* @param {?} overlayRef
* @return {?}
*/
attachOverlayRef(menu, overlayRef) {
menu.overlayRef = overlayRef;
}
/**
* @private
* @return {?}
*/
createAnchorElement() {
/** @type {?} */
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.top = '0';
div.style.bottom = '0';
div.style.left = '0';
div.style.right = '0';
document.body.appendChild(div);
return div;
}
/**
* @private
* @return {?}
*/
buildSubMenuPositions() {
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'
}
];
}
/**
* @private
* @return {?}
*/
buildPositions() {
return [
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top'
},
{
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top'
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom'
},
{
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'bottom'
}
];
}
}
ShContextMenuService.decorators = [
{ type: Injectable }
];
/** @nocollapse */
ShContextMenuService.ctorParameters = () => [
{ type: Overlay }
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class ShContextMenuComponent {
/**
* @param {?} ctxService
*/
constructor(ctxService) {
this.ctxService = ctxService;
this.contentChildrenItems = new QueryList();
this.viewChildrenItems = new QueryList();
}
/**
* @return {?}
*/
get menuItems() {
// when using the ShContextMenuComponent as menu, the ContentChildren is the source
if (this.contentChildrenItems.length) {
return this.contentChildrenItems;
}
// when using a custom component as menu the ViewChildren is the source
return this.viewChildrenItems;
}
/**
* @param {?} $event
* @param {?} item
* @param {?} elm
* @return {?}
*/
onEnter($event, item, elm) {
this.ctxService.closeSubMenus(this);
this.setNotActive();
if (!item.subMenu) {
return;
}
this.setActive(item);
this.ctxService.openSubMenu({
data: item.context.$implicit,
targetElement: new ElementRef(elm),
menu: item.subMenu,
mouseEvent: $event,
parentMenu: this
});
}
/**
* @private
* @param {?} item
* @return {?}
*/
setActive(item) {
item.setActive();
this.subActive = true;
}
/**
* @param {?} event
* @param {?} item
* @return {?}
*/
onClick(event, item) {
// TODO: move click handling to service
if (item.divider) {
return;
}
if (!item.subMenu && item.closeOnClick) {
this.ctxService.destroy();
item.click.emit({
data: item.context.$implicit,
event
});
}
}
/**
* @private
* @param {?} fn
* @param {?} fallbackContext
* @param {?} data
* @param {?} event
* @return {?}
*/
callWithContext(fn, fallbackContext, data, event) {
return fn.call(this.thisContext ? this.thisContext : fallbackContext, {
data,
event
});
}
/**
* @return {?}
*/
close() {
this.setNotActive();
this.menuContainer.detach();
if (this.overlayRef) {
this.overlayRef.detach();
}
}
/**
* @return {?}
*/
ngOnDestroy() {
this.close();
}
/**
* @return {?}
*/
setNotActive() {
this.subActive = false;
this.menuItems.forEach((/**
* @param {?} i
* @return {?}
*/
i => i.setNotActive()));
}
/**
* @param {?} item
* @return {?}
*/
isVisible(item) {
if (!item.visible) {
return true;
}
return this.callWithContext(item.visible, this, item.context.$implicit, null);
}
}
ShContextMenuComponent.decorators = [
{ type: Component, args: [{
selector: 'sh-context-menu',
encapsulation: ViewEncapsulation.None,
template: `
<ng-container #menuContainer></ng-container>
<ng-template #menuTemplate>
<div class="sh-context-menu">
<div
*ngFor="let menuItem of menuItems"
#itemElement
[ngClass]="{
'sh-sub-anchor': menuItem.subMenu,
'sh-context-menu--item__divider': menuItem.divider,
'sh-context-menu--item__sub-active': subActive && menuItem.active
}"
class="sh-context-menu--item"
(mouseenter)="onEnter($event, menuItem, itemElement)"
(click)="onClick($event, menuItem)"
>
<ng-container *ngIf="!menuItem.divider || !isVisible(menuItem)">
<ng-content
*ngTemplateOutlet="menuItem.template; context: menuItem.context"
></ng-content>
</ng-container>
</div>
</div>
</ng-template>
`,
styles: [".sh-backdrop{background-color:transparent}.sh-context-menu{background:#ececec;min-width:150px;border:1px solid rgba(0,0,0,.2);border-radius:3px;box-shadow:0 0 10px 2px rgba(0,0,0,.1);color:#000;padding:5px 0;margin:0}.sh-context-menu--item{padding:5px 10px 5px 15px;-webkit-transition:.15s;transition:.15s}.sh-context-menu--item:hover,.sh-context-menu--item__sub-active{background-color:#4b8bec;color:#fff;cursor:pointer}.sh-context-menu--item.sh-context-menu--item__divider:hover{background-color:#ececec;color:#000;cursor:default}.sh-context-menu--item__divider{height:1px;margin:1px 1px 8px;overflow:hidden;border-bottom:1px solid #d0d0d0}.sh-context-menu--item.sh-sub-anchor{position:relative;min-width:160px}.sh-sub-anchor:after{content:'';top:50%;right:6px;-webkit-transform:translateY(-50%);transform:translateY(-50%);position:absolute;border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:8px solid #000}"]
}] }
];
/** @nocollapse */
ShContextMenuComponent.ctorParameters = () => [
{ type: ShContextMenuService }
];
ShContextMenuComponent.propDecorators = {
thisContext: [{ type: Input, args: ['this',] }],
contentChildrenItems: [{ type: ContentChildren, args: [ShContextMenuItemDirective, {
read: ShContextMenuItemDirective
},] }],
viewChildrenItems: [{ type: ViewChildren, args: [ShContextMenuItemDirective, {
read: ShContextMenuItemDirective
},] }],
menuTemplate: [{ type: ViewChild, args: ['menuTemplate', { read: TemplateRef, static: true },] }],
menuContainer: [{ type: ViewChild, args: ['menuContainer', { read: ViewContainerRef, static: true },] }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class ShAttachMenuDirective {
/**
* @param {?} ctxService
* @param {?} elm
*/
constructor(ctxService, elm) {
this.ctxService = ctxService;
this.elm = elm;
this.open = new EventEmitter();
}
/**
* @return {?}
*/
ngOnInit() {
this.setupEvents();
}
/**
* @private
* @return {?}
*/
setupEvents() {
/** @type {?} */
const observables = [];
if (!this.triggers) {
observables.push(fromEvent(this.elm.nativeElement, 'contextmenu'));
}
else {
this.triggers.forEach((/**
* @param {?} t
* @return {?}
*/
t => {
observables.push(fromEvent(this.elm.nativeElement, t));
}));
}
this.sub = merge(...observables).subscribe(this.openMenu.bind(this));
}
/**
* @param {?} event
* @return {?}
*/
openMenu(event) {
event.preventDefault();
event.stopPropagation();
/** @type {?} */
let preventOpen = false;
this.open.emit({
data: this.data,
mouseEvent: event,
preventOpen: (/**
* @return {?}
*/
() => {
preventOpen = true;
})
});
if (preventOpen)
return;
this.ctxService.openMenu({
menu: this.menu,
mouseEvent: event,
targetElement: this.elm,
data: this.data
});
}
/**
* @return {?}
*/
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe();
}
}
}
ShAttachMenuDirective.decorators = [
{ type: Directive, args: [{
selector: '[shAttachMenu]'
},] }
];
/** @nocollapse */
ShAttachMenuDirective.ctorParameters = () => [
{ type: ShContextMenuService },
{ type: ElementRef }
];
ShAttachMenuDirective.propDecorators = {
menu: [{ type: Input, args: ['shAttachMenu',] }],
triggers: [{ type: Input, args: ['shMenuTriggers',] }],
data: [{ type: Input, args: ['shMenuData',] }],
open: [{ type: Output }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class ShContextMenuModule {
}
ShContextMenuModule.decorators = [
{ type: NgModule, args: [{
declarations: [
ShAttachMenuDirective,
ShContextMenuComponent,
ShContextMenuItemDirective
],
exports: [
ShAttachMenuDirective,
ShContextMenuComponent,
ShContextMenuItemDirective
],
providers: [ShContextMenuService],
imports: [CommonModule, OverlayModule],
entryComponents: [ShContextMenuComponent]
},] }
];
export { ShAttachMenuDirective, ShContextMenuComponent, ShContextMenuItemDirective, ShContextMenuModule, ShContextMenuService, MenuItemContext as ɵa };
//# sourceMappingURL=ng2-right-click-menu.js.map