UNPKG

ng2-right-click-menu

Version:
661 lines (654 loc) 19.2 kB
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