diff --git a/.angular-cli.json b/.angular-cli.json index 4a640df..7138974 100644 --- a/.angular-cli.json +++ b/.angular-cli.json @@ -26,7 +26,8 @@ "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" - } + }, + "showCircularDependencies": false } ], "e2e": { @@ -55,6 +56,9 @@ }, "defaults": { "styleExt": "css", - "component": {} + "component": {}, + "build": { + "showCircularDependencies": false + } } } diff --git a/README.md b/README.md index 69bbad8..042f0f5 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,105 @@ -### WIP ! - -# ng2-right-click-menu +## ng2-right-click-menu _Right click context menu for Angular 2+_ __DEMO__ https://msarsha.github.io/ng2-right-click-menu/ -## How to use +### Dependencies -- `npm install --save ng2-right-click-menu` -- import `ShContextMenuModule` into your app module +`@angular/cdk` -Add the `[sh-context]` directive to the desired element and bind an `IShContextMenuItem` array. +`@angular/cdk/overlay-prebuilt.css"` -Use the `[sh-data-context]` property to inject a context object of type `any`. +### Setup -````html -
- // content -
-```` +`npm install --save ng2-right-click-menu @angular/cdk` -### `[sh-context]` (`IShContextMenuItem[]`) +import `ShContextMenuModule` ````typescript - interface IShContextMenuItem { - label?: ((context: any) => string) | string; // as of version 0.0.11 this property is rendered as HTML - divider?: boolean; - onClick?($event: any): void; - visible?(context: any): boolean; - disabled?(context: any): boolean; - subMenu?: boolean; - subMenuItems?: IShContextMenuItem[]; -} -```` - -Example: +import {ShContextMenuModule} from 'ng2-right-click-menu' -````typescript - items: IShContextMenuItem[]; - - this.items = [ - { - label: 'Save', - onClick: this.clickEvent - }, - { - label: (context) => `Edit ${context.someVariable}`, - onClick: this.clickEvent - }, - { - label: 'Sub Menu', - subMenu: true, - subMenuItems: [ - { - label: 'Save', - onClick: this.clickEvent - }, - { - label: 'Edit', - onClick: this.clickEvent - }] - } - { - divider: true - }, - { - label: 'Remove', - disabled: ctx => { - return ctx.Two === 'Two'; - }, - onClick: this.clickEvent - }, - { - label: (context) => `Hide ${context.name}`, - onClick: this.clickEvent, - visible: ctx => { - return ctx.One === 'One'; - } - } - ]; - - clickEvent($event: any){ - console.log('clicked ', $event); - }; +@NgModule({ + //... + imports: [ShContextMenuModule] + //... +}) ```` -### Passing a function to the label option +import css file in your `styles.css`: -You can pass either a string or a function that returns a string (using the data context as a parameter) to the `label` option of menu items. Passing a function allows the label to contain dynamic content. - -### onBeforeMenuOpen (v0.0.14) - -The `onBeforeMenuOpen` event can be used to cancel the menu from opening and allow to modify the menu items that will be display by the current event. - -The `open()` callback is used to continue the context menu event and can be injected with the new modified `IShContextMenuItem` items array. (optional. if items array is not provided the original array defined by `[sh-context]` will be used.) - -````html -
- Click Me ! -
+````css + @import "~@angular/cdk/overlay-prebuilt.css"; ```` -component: +## Usage -````typescript -onBefore = (event: BeforeMenuEvent) => { - event.open([new items]); - }; -```` +#### Defining a Basic Menu Template -`BeforeMenuEvent` interface: -````typescript -interface BeforeMenuEvent { - event: MouseEvent; - items: IShContextMenuItem[]; - open(items?: IShContextMenuItem[]): void; -} -```` +The menu template is built using the `sh-context-menu` component as the menu wrapper, +and nested `ng-template` with the `shContextMenuItem` directive for every menu item: -### Options Object (v0.0.10) +The `shContextMenuItem` directive provide a template variable (`let-data`) that gives you access to the data object attached to the menu. ````html -
-```` - -````typescript - options: IShContextOptions = { - // set options - } + + +
+ Menu Item - {{data.label}} +
+
+
```` -The options object is of type `IShContextOptions` and currently support the following options: +#### Attaching Menu To An Element -Options | Type | Default | Description -:---:|:---:|:---:|:---| -rtl|boolean|false|right to left support -theme|string|light|menu color theme +Attaching works by using the `shAttachMenu` directive and providing the `#menu` (from the above example) template variable: -### Sub Menus (v0.0.9) +The object provided to the `[shMenuData]` input will be available as a template variable inside `ng-template`s with `shContextMenuItem` -Setting the `subMenu` property to `true` and the `subMenuItems` property to a `IShContextMenuItem[]` will render a sub menu. +```html +
Right Click Me
+``` -````typescript -{ - label: 'Sub Menu', - subMenu: true, - subMenuItems: [ - { - label: 'Save', - onClick: this.clickEvent - }, - { - label: 'Edit', - onClick: this.clickEvent - }] -} -```` - -#### The `onClick` handler +## Sub Menus -The `onClick` handler is a function that is being injected with `$event` parameter. +Sub menu is attached to the `shContextMenuItem` directive using the `[subMenu]` input. -The `$event` structure is: +The `[subMenu]` input is provided with a `sh-context-menu`'s template variable (just like attaching a menu to an element). -````typescript - { - menuItem: item, - dataContext: this.dataContext - } +````html + + +
+ Menu Item - {{data.label}} +
+
+ + +
+ Menu Item - {{data.label}} +
+
+
+
```` -Where the `menuItem` property is of type `IShContextMenuItem` and is the clicked menu item. - -And the `dataContext` is the object used on the `[sh-data-context]` binding. +## API +#### sh-context-menu -#### The `disabled` and `visible` handlers +Name | Type | Default | Description +:---:|:---:|:---:|:---: +[this]|any|null|the `this` context for input callbacks (visible) - typically the menu's host component -Both get injected with the object used on the `[sh-data-context]` binding +#### shContextMenuItem -And should return a `boolean` to indicate if the current `IShContextMenuItem` is disabled or visible. +Name | Type | Default | Description +:---:|:---:|:---:|:---: +[subMenu]|ShContextMenuComponent|null|sub menu +[divider]|boolean|false|render a divider +[closeOnClick]|boolean|true|should the menu close on click +[visible]|(event: ShContextMenuClickEvent) => boolean|null|function to determine if a item is visible +(click)|ShContextMenuClickEvent|null|click handler ### Setting up development env diff --git a/changelog.md b/changelog.md index 1d5e3d9..bcde96d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +### 1.0.0 - BREAKING CHANGES + +new api using ng-template [#fc09e66](https://github.com/msarsha/ng2-right-click-menu/commit/fc09e6687ce7ca376708386b8841d5336b3ac82a) + ### 0.0.16 z-index increased to bring menu to foreground [#fc09e66](https://github.com/msarsha/ng2-right-click-menu/commit/fc09e6687ce7ca376708386b8841d5336b3ac82a) diff --git a/lib/src/html.directive.ts b/lib/src/html.directive.ts deleted file mode 100644 index 179f314..0000000 --- a/lib/src/html.directive.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ElementRef } from '@angular/core'; -import { AfterContentInit } from '@angular/core'; -import { Input } from '@angular/core'; -import { Directive } from '@angular/core'; - -@Directive({ - selector: '[sh-html]' -}) -export class HtmlDirective implements AfterContentInit { - @Input('sh-html') content: String; - - constructor(private elmRef: ElementRef){} - - ngAfterContentInit(): void { - this.elmRef.nativeElement.insertAdjacentHTML('afterbegin', this.content); - } -} diff --git a/lib/src/index.ts b/lib/src/index.ts deleted file mode 100644 index b1f0ff3..0000000 --- a/lib/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ShContextMenuModule } from './sh-context-menu.module'; -export { IShContextMenuItem, IShContextOptions, BeforeMenuEvent } from './sh-context-menu.models'; diff --git a/lib/src/injector.service.ts b/lib/src/injector.service.ts deleted file mode 100644 index c44cfe0..0000000 --- a/lib/src/injector.service.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - ApplicationRef, ComponentFactoryResolver, ComponentRef, Injectable, - Injector, EmbeddedViewRef, Type -} from '@angular/core'; - -/** - * Injection service is a helper to append components - * dynamically to a known location in the DOM, most - * noteably for dialogs/tooltips appending to body. - * - * @export - * @class InjectionService - */ -@Injectable() -export class InjectionService { - private _container: ComponentRef; - - constructor( - private applicationRef: ApplicationRef, - private componentFactoryResolver: ComponentFactoryResolver, - private injector: Injector) { - } - - /** - * Gets the root view container to inject the component to. - * - * @returns {ComponentRef} - * - * @memberOf InjectionService - */ - getRootViewContainer(): ComponentRef { - if(this._container) return this._container; - - const rootComponents = this.applicationRef['components']; - if (rootComponents.length) return rootComponents[0]; - - throw new Error('View Container not found! ngUpgrade needs to manually set this via setRootViewContainer.'); - } - - /** - * Overrides the default root view container. This is useful for - * things like ngUpgrade that doesn't have a ApplicationRef root. - * - * @param {any} container - * - * @memberOf InjectionService - */ - setRootViewContainer(container:any): void { - this._container = container; - } - - /** - * Gets the html element for a component ref. - * - * @param {ComponentRef} componentRef - * @returns {HTMLElement} - * - * @memberOf InjectionService - */ - getComponentRootNode(componentRef: ComponentRef): HTMLElement { - return (componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement; - } - - /** - * Gets the root component container html element. - * - * @returns {HTMLElement} - * - * @memberOf InjectionService - */ - getRootViewContainerNode(): HTMLElement { - return this.getComponentRootNode(this.getRootViewContainer()); - } - - /** - * Projects the inputs onto the component - * - * @param {ComponentRef} component - * @param {*} options - * @returns {ComponentRef} - * - * @memberOf InjectionService - */ - projectComponentInputs(component: ComponentRef, options: any): ComponentRef { - if(options) { - const props = Object.getOwnPropertyNames(options); - for(const prop of props) { - component.instance[prop] = options[prop]; - } - } - - return component; - } - - /** - * Appends a component to a adjacent location - * - * @template T - * @param {Type} componentClass - * @param {*} [options={}] - * @param {Element} [location=this.getRootViewContainerNode()] - * @returns {ComponentRef} - * - * @memberOf InjectionService - */ - appendComponent( - componentClass: Type, - options: any = {}, - location: Element = this.getRootViewContainerNode()): ComponentRef { - - let componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass); - let componentRef = componentFactory.create(this.injector); - let appRef: any = this.applicationRef; - let componentRootNode = this.getComponentRootNode(componentRef); - - // project the options passed to the component instance - this.projectComponentInputs(componentRef, options); - - appRef.attachView(componentRef.hostView); - - componentRef.onDestroy(() => { - appRef.detachView(componentRef.hostView); - }); - - location.appendChild(componentRootNode); - - return componentRef; - } -} diff --git a/lib/src/sh-context-menu.component.ts b/lib/src/sh-context-menu.component.ts deleted file mode 100755 index 77ec010..0000000 --- a/lib/src/sh-context-menu.component.ts +++ /dev/null @@ -1,197 +0,0 @@ -import {Component, Input, Output, EventEmitter, OnInit, ElementRef, ViewChild, AfterContentInit} from "@angular/core"; -import {IShContextMenuItem, IShContextOptions} from "./sh-context-menu.models"; -import {ShContextService} from './sh-context-service'; - -export interface ShContextPosition { - top: number; - left: number; -} - -@Component({ - selector: 'sh-context-menu', - template: ` -
-
    -
  • -
    -
    -
    -
    -
    -
  • -
-
- `, - styles: [` - .sh-context--container { - font-family: sans-serif; - position: fixed; - background: #ececec; - min-width: 150px; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 3px; - box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1); - z-index: 9999; - color: black; - } - - .dark { - background: #383737 !important; - color: white !important; - } - - .sh-context--container ul { - list-style: none; - padding: 5px 0; - margin: 0; - } - - .sh-context--container ul li { - padding: 5px 10px 5px 15px; - transition: all 0.15s; - } - - .sh-context--container ul li.sh-context-divider { - height: 1px; - margin: 1px 1px 8px 1px; - overflow: hidden; - border-bottom: 1px solid #d0d0d0; - line-height: 10px; - } - - .sh-context--container ul li.sh-menu-item:hover { - cursor: pointer; - background: #4b8bec; - color: white; - } - - .sh-context--container.dark ul li.sh-menu-item:hover { - cursor: pointer; - background: #4b8bec; - color: white; - } - - .sh-context--container ul li.sh-menu-disabled { - color: #d0d0d0; - } - - .sh-context--container ul li.sh-menu-item.sh-menu-hidden { - display: none; - } - - .sh-context--container ul li.sh-menu-disabled:hover { - cursor: not-allowed; - color: #d0d0d0; - background: #ececec; - } - - .right-arrow { - float: right; - margin-left: 10px; - margin-top: 3px; - border-top: 6px solid transparent; - border-bottom: 6px solid transparent; - border-left: 8px solid black; - } - - .left-arrow { - float: left; - margin-right: 10px; - margin-top: 3px; - border-top: 6px solid transparent; - border-bottom: 6px solid transparent; - border-right: 8px solid black; - } - `] -}) -export class ShContextMenuComponent implements OnInit, AfterContentInit { - @Input() position: ShContextPosition; - @Input() items: IShContextMenuItem[]; - @Input() dataContext: any; - @Output() onClose = new EventEmitter(); - - options: IShContextOptions; - - @ViewChild('childRef') childRef: ElementRef; - - constructor(private ctxService: ShContextService) { - } - - ngOnInit(): void { - this.options = this.ctxService.getOptions(); - } - - ngAfterContentInit(): void { - if (this.options.rtl) - this.setRtlLocation(); - } - - getLabel(item: IShContextMenuItem): string { - if (typeof item.label === 'string') { - return item.label; - } else if (typeof item.label === 'function') { - return item.label(this.dataContext); - } - return ''; - } - - close() { - this.onClose.emit(); - } - - onClick(item: IShContextMenuItem) { - if (this.isItemDisabled(item)) - return; - - if (item.onClick) { - this.close(); - - // invoke the onClick handler with a timeout of 0, - // so that the menu gets a chance to be closed before (screen refresh) - this.invokeOnClickWithTimeOut(item); - } - } - - private invokeOnClickWithTimeOut(item: IShContextMenuItem) { - setTimeout(() => { - if (item.onClick) { - item.onClick({ - menuItem: item, - dataContext: this.dataContext - }); - } - }, 0); - } - - isItemDisabled(item: IShContextMenuItem) { - if (!item.disabled) - return false; - - return item.disabled(this.dataContext); - } - - isItemVisible(item: IShContextMenuItem) { - if (!item.visible) - return true; - - return item.visible(this.dataContext); - } - - setRtlLocation() { - const elmRect: ClientRect = - this.childRef.nativeElement.getClientRects()[0]; - - this.position.left = this.position.left - elmRect.width; - } - -} diff --git a/lib/src/sh-context-menu.directive.ts b/lib/src/sh-context-menu.directive.ts deleted file mode 100755 index 9b2161c..0000000 --- a/lib/src/sh-context-menu.directive.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {ShContextService} from './sh-context-service'; -import { - Directive, - Input, - HostListener, - ComponentRef, - Output, EventEmitter -} from "@angular/core"; - -import {ShContextOverlayComponent} from './sh-context-overlay.component'; -import {ShContextMenuComponent} from "./sh-context-menu.component"; -import {InjectionService} from "./injector.service"; -import {BeforeMenuEvent, IShContextMenuItem, IShContextOptions} from "./sh-context-menu.models"; - -@Directive({ - selector: '[sh-context]' -}) -export class ShContextMenuDirective { - @Input('sh-context') menuItems: IShContextMenuItem[]; - @Input('sh-data-context') dataContext: any; - @Input('sh-options') options: IShContextOptions; - - @Output('onBeforeMenuOpen') onBeforeMenuOpen = new EventEmitter(); - - ctxComponent: ComponentRef; - overlayComponent: ComponentRef; - - constructor(private ctxService: ShContextService, - private injectionService: InjectionService) { - } - - @HostListener('contextmenu', ['$event']) - onClick(event: MouseEvent): boolean | void { - this.options = this.ctxService.setOptions(this.options); - - this.closeMenu(); - - if ( this.contextMenuIsEmpty() ) { - return; - } - - if (this.onBeforeMenuOpen.observers.length > 0) { - this.onBeforeMenuOpen.emit({ - event: event, - items: this.menuItems, - open: (modifiedItems: IShContextMenuItem[] = this.menuItems) => this.createMenu(event, modifiedItems) - }); - } else { - this.createMenu(event); - } - - return false; - } - - private createMenu(event: MouseEvent, items: IShContextMenuItem[] = this.menuItems) { - this.overlayComponent = this.createOverlayComponent(); - this.ctxComponent = this.createContextComponent(); - - this.registerBindings(items); - this.registerEvents(); - this.setLocation(event); - } - - registerEvents() { - this.ctxComponent.instance.onClose.subscribe(() => { - this.closeMenu() - }); - - this.overlayComponent.instance.onClick.subscribe(() => { - this.closeMenu() - }); - } - - registerBindings(menuItems: IShContextMenuItem[]) { - this.ctxComponent.instance.items = menuItems; - this.ctxComponent.instance.dataContext = this.dataContext; - } - - createContextComponent(): ComponentRef { - return this.injectionService.appendComponent(ShContextMenuComponent); - } - - createOverlayComponent(): ComponentRef { - return this.injectionService.appendComponent(ShContextOverlayComponent); - } - - setLocation(event: MouseEvent) { - let {clientX, clientY} = event; - - this.ctxComponent.instance.position = { - top: clientY, - left: clientX - }; - } - - closeMenu() { - if (this.ctxComponent) - this.ctxComponent.destroy(); - - if (this.overlayComponent) - this.overlayComponent.destroy(); - } - - private contextMenuIsEmpty(): boolean { - return !this.menuItems || this.menuItems.length === 0; - } -} diff --git a/lib/src/sh-context-menu.models.ts b/lib/src/sh-context-menu.models.ts deleted file mode 100644 index a890fca..0000000 --- a/lib/src/sh-context-menu.models.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const ShContextDefaultOptions: IShContextOptions = { - rtl: false, - theme: 'light' -}; - -export interface IShContextMenuItem { - label?: ((context: any) => string) | string; - id?: string; - divider?: boolean; - onClick?($event: any): void; - visible?(context: any): boolean; - disabled?(context: any): boolean; - subMenu?: boolean; - subMenuItems?: IShContextMenuItem[]; - data?: any; -} - -export interface IShContextOptions { - rtl?: boolean; - theme?: 'light' | 'dark' -} - -export interface BeforeMenuEvent { - event: MouseEvent; - items: IShContextMenuItem[]; - open(items?: IShContextMenuItem[]): void; -} diff --git a/lib/src/sh-context-menu.module.ts b/lib/src/sh-context-menu.module.ts deleted file mode 100755 index 02ccb56..0000000 --- a/lib/src/sh-context-menu.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from "@angular/core"; -import { CommonModule } from "@angular/common"; - -import { HtmlDirective } from './html.directive'; -import { ShContextOverlayComponent } from './sh-context-overlay.component'; -import { ShContextMenuDirective } from "./sh-context-menu.directive"; -import { ShContextMenuComponent } from './sh-context-menu.component'; -import { ShContextSubMenuDirective } from './sh-context-sub-menu.directive'; -import { ShContextServiceModule } from './sh-context-service'; - - -@NgModule({ - declarations: [ - ShContextMenuDirective, - ShContextMenuComponent, - ShContextSubMenuDirective, - ShContextOverlayComponent, - HtmlDirective - ], - exports: [ShContextMenuDirective], - imports: [ - CommonModule, - ShContextServiceModule - ], - entryComponents: [ - ShContextMenuComponent, - ShContextOverlayComponent - ] -}) -export class ShContextMenuModule { } diff --git a/lib/src/sh-context-overlay.component.ts b/lib/src/sh-context-overlay.component.ts deleted file mode 100644 index e11f8c0..0000000 --- a/lib/src/sh-context-overlay.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EventEmitter } from '@angular/core'; -import { Output } from '@angular/core'; -import { Component } from '@angular/core'; - -@Component({ - selector: 'sh-context-overlay', - template: `
`, - styles: [` - .sh-context-overlay{ - position: fixed; - top:0; - bottom: 0; - left: 0; - right: 0; - z-index: 9998; - background-color: transparent; - } - `] -}) -export class ShContextOverlayComponent{ - @Output() onClick = new EventEmitter() -} diff --git a/lib/src/sh-context-service.ts b/lib/src/sh-context-service.ts deleted file mode 100644 index 7064745..0000000 --- a/lib/src/sh-context-service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable, NgModule } from '@angular/core'; -import {InjectionService} from "./injector.service"; -import {IShContextOptions, ShContextDefaultOptions} from "./sh-context-menu.models"; - -@Injectable() -export class ShContextService { - options: IShContextOptions; - - setOptions(opts: IShContextOptions): IShContextOptions { - this.options = Object.assign({}, ShContextDefaultOptions, opts); - return this.options; - } - - getOptions(): IShContextOptions{ - return this.options; - } -} - -@NgModule({ - providers: [ShContextService, InjectionService] -}) -export class ShContextServiceModule { } diff --git a/lib/src/sh-context-sub-menu.directive.ts b/lib/src/sh-context-sub-menu.directive.ts deleted file mode 100755 index 8b4c303..0000000 --- a/lib/src/sh-context-sub-menu.directive.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Directive, Output, ElementRef, EventEmitter, Input, HostListener, ViewContainerRef, ComponentFactoryResolver, ComponentRef, OnInit } from "@angular/core"; - -import { ShContextMenuComponent, ShContextPosition } from "./sh-context-menu.component"; -import { ShContextService } from "./sh-context-service"; -import {IShContextMenuItem, IShContextOptions} from "./sh-context-menu.models"; - -@Directive({ - selector: '[sh-context-sub-menu]' -}) -export class ShContextSubMenuDirective implements OnInit { - @Input('sh-context-sub-menu') menuItems: IShContextMenuItem[]; - @Input('sh-data-context') dataContext: any; - @Output() closeSubMenu = new EventEmitter(); - - options: IShContextOptions; - ctxComponent: ComponentRef; - - constructor( - private viewRef: ViewContainerRef, - private elmRef: ElementRef, - private resolver: ComponentFactoryResolver, - private ctxService: ShContextService - ) { } - - ngOnInit(): void { - this.options = this.ctxService.getOptions(); - } - - @HostListener('mouseover', ['$event']) - onMouseOver(event: MouseEvent) { - this.closeCurrent(); - this.ctxComponent = this.createContextComponent(); - - this.registerBindings(); - this.registerEvents(); - this.setLocation(); - - return false; - } - - registerEvents() { - this.ctxComponent.instance.onClose.subscribe(() => { - this.closeSubMenu.emit(); - }); - } - - registerBindings() { - this.ctxComponent.instance.items = this.menuItems; - this.ctxComponent.instance.dataContext = this.dataContext; - } - - createContextComponent(): ComponentRef { - let shContextMenuFactory = this.resolver.resolveComponentFactory(ShContextMenuComponent); - let shContextComponentRef = this.viewRef.createComponent(shContextMenuFactory); - - return shContextComponentRef; - } - - setLocation() { - const { top, left, width } = - this.elmRef.nativeElement.getClientRects()[0]; - - let position: ShContextPosition = { - top: top, - left: this.options.rtl ? left : left + width - }; - - this.ctxComponent.instance.position = position; - } - - closeMenu() { - this.closeSubMenu.emit(); - } - - closeCurrent() { - this.viewRef.clear(); - } -} diff --git a/package.json b/package.json index 68e5a21..72fa2ce 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "ng-packagr -p lib/package.json && cp README.md ./lib-build", - "build:win": "ng-packagr -p lib/package.json && copy README.md lib-build", + "build": "ng-packagr -p src/lib/package.json && cp README.md ./lib-build", + "build:win": "ng-packagr -p src/lib/package.json && copy README.md lib-build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" @@ -30,6 +30,7 @@ "private": true, "dependencies": { "@angular/animations": "^5.2.0", + "@angular/cdk": "^5.2.1", "@angular/common": "^5.2.0", "@angular/compiler": "^5.2.0", "@angular/core": "^5.2.0", @@ -58,10 +59,10 @@ "karma-coverage-istanbul-reporter": "^1.2.1", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", + "ng-packagr": "^1.6.0", "protractor": "~5.1.2", "ts-node": "~4.1.0", "tslint": "~5.9.1", - "typescript": "~2.5.3", - "ng-packagr": "^1.6.0" + "typescript": "~2.5.3" } } diff --git a/src/app/app.component.css b/src/app/app.component.css index 82541ef..bfc3947 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -1,20 +1,20 @@ -.box{ +.box { height: 180px; width: 180px; background-color: #e3e4c2; line-height: 140px; text-align: center; - box-shadow: 6px 6px 15px 2px rgba(0,0,0,.3); + box-shadow: 6px 6px 15px 2px rgba(0, 0, 0, .3); display: inline-block; margin: 20px; } -.box.box-right{ +.box.box-right { float: right; width: 240px; } -i.menu-icon{ +i.menu-icon { margin-right: 3px; display: inline-block; color: green; diff --git a/src/app/app.component.html b/src/app/app.component.html index 43cfee5..d3d3979 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,6 +1,85 @@ -
-

{{title}}

-
-
-

{{titleRtl}}

+
+

Menu One

+ + + + + + + +
+ two - {{item.label}} +
+
+
+
+ +
+ one - {{item.label}} +
+
+ + + + + +

Hi !

+
+
+ + + +
+ From Sub Menu - {{item.label}} +
+
+ + + +
+ one - {{item.label}} +
+
+ +
+ From Sub Menu - {{item.label}} +
+
+ + + + From Sub Menu Two - {{item.label}} + + +
+
+ + +{{itemVisible}} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5600a9d..1a9953c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,5 @@ -import {Component, ViewEncapsulation} from '@angular/core'; -import {IShContextMenuItem, IShContextOptions, BeforeMenuEvent} from 'ng2-right-click-menu'; +import {Component, Input, ViewEncapsulation} from '@angular/core'; +import {ShContextMenuComponent} from '../lib/src/sh-context-menu.component'; @Component({ selector: 'app-root', @@ -7,129 +7,57 @@ import {IShContextMenuItem, IShContextOptions, BeforeMenuEvent} from 'ng2-right- templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent { +export class AppComponent { title = 'Right Click Me'; - titleRtl = 'Right Click Me (RTL)'; - items: IShContextMenuItem[]; - itemsRtl: IShContextMenuItem[]; - dataCtxOne: any; - dataCtxTwo: any; - options: IShContextOptions; + items: any[]; - constructor() { - this.dataCtxOne = { - One: 'One' - }; - - this.dataCtxTwo = { - Two: 'Two' - }; + thisContext = this; + itemVisible = false; + constructor() { this.items = [ { - label: ' Save Me On Your HD', - onClick: this.clickEvent, - }, - { - label: 'Edit', - onClick: this.clickEvent - }, - { - label: ' Sub Menu', - subMenu: true, - subMenuItems: [ - { - label: 'Save', - onClick: this.clickEvent - }, - { - label: 'Edit', - onClick: this.clickEvent - }, - { - label: 'Another Sub Menu', - subMenu: true, - subMenuItems: [ - { - label: 'Save', - onClick: this.clickEvent - }, - { - label: 'Edit', - onClick: this.clickEvent - }] - } - ] + label: 'Item One' }, { - divider: true - }, - { - label: 'Remove', - disabled: ctx => { - return ctx.Two === 'Two'; - }, - onClick: this.clickEvent - }, - { - label: 'Hidden', - onClick: this.clickEvent, - visible: ctx => { - return ctx.One === 'One'; - } + label: 'Item Two' } ]; + } - this.itemsRtl = [ - { - label: 'שמור', - onClick: this.clickEvent - }, - { - label: 'ערוך', - onClick: this.clickEvent - }, - { - label: 'תפריט נוסף', - subMenu: true, - subMenuItems: [ - { - label: 'שמור', - onClick: this.clickEvent - }, - { - label: 'ערוך', - onClick: this.clickEvent - }, - { - label: 'עוד תפריט נוסף', - subMenu: true, - subMenuItems: [ - { - label: 'שמור', - onClick: this.clickEvent - }, - { - label: 'ערוך', - onClick: this.clickEvent - }] - } - ] - } - ]; + onClick(event) { + console.log('clicked', this, event); + } - this.options = { - rtl: true, - theme: 'dark' - }; + isVisible(event) { + return true; } +} - onBefore = (event: BeforeMenuEvent) => { - console.log(event); - event.open([event.items[0]]); - }; +@Component({ + selector: 'my-menu', + template: ` +
+ from comp !! - {{item.label}} +
+
+ from comp !! - {{item.label}} +
+ ` +}) +// TODO: this is not possible now (because the use of TemplatePortal instead of ComponentPortal) +// should later define an interface for using a custom component as context menu +export class MyMenuComponent extends ShContextMenuComponent { +} - clickEvent = ($event: any) => { - console.log('clicked ', $event); - } +@Component({ + selector: 'my-content', + template: ` +
+ +
+ ` +}) +export class MyContentComponent { + @Input() item: any; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d7fb4cc..459e9a3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,12 +1,14 @@ import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; -import {AppComponent} from './app.component'; -import {ShContextMenuModule} from 'ng2-right-click-menu'; +import {AppComponent, MyContentComponent, MyMenuComponent} from './app.component'; +import {ShContextMenuModule} from '../lib/src/sh-context-menu.module'; @NgModule({ declarations: [ - AppComponent + AppComponent, + MyMenuComponent, + MyContentComponent ], imports: [ BrowserModule, diff --git a/lib/package.json b/src/lib/package.json similarity index 78% rename from lib/package.json rename to src/lib/package.json index 6807fa5..52cb498 100644 --- a/lib/package.json +++ b/src/lib/package.json @@ -1,8 +1,8 @@ { "name": "ng2-right-click-menu", - "version": "0.0.16", + "version": "1.0.0", "author": "Matan Sar-Shalom ", - "description": "Right click context menu for Angular 2+", + "description": "Right click context menu for Angular", "license": "MIT", "private": false, "keywords": [ @@ -13,7 +13,8 @@ "menu", "right", "click", - "menu" + "menu", + "contextmenu" ], "repository": { "type": "git", @@ -21,7 +22,8 @@ }, "peerDependencies": { "@angular/core": "^5.0.0", - "@angular/common": "^5.0.0" + "@angular/common": "^5.0.0", + "@angular/cdk": "^5.0.0" }, "ngPackage": { "$schema": "../node_modules/ng-packagr/ng-package.schema.json", diff --git a/src/lib/src/index.ts b/src/lib/src/index.ts new file mode 100644 index 0000000..27c1db5 --- /dev/null +++ b/src/lib/src/index.ts @@ -0,0 +1,5 @@ +export {ShContextMenuModule} from './sh-context-menu.module'; +export {ShContextMenuComponent} from './sh-context-menu.component'; +export {ShAttachMenuDirective} from './sh-attach-menu.directive'; +export {ShContextMenuItemDirective} from './sh-context-menu-item.directive'; +export {ShContextMenuService} from './sh-context-menu.service'; diff --git a/src/lib/src/sh-attach-menu.directive.ts b/src/lib/src/sh-attach-menu.directive.ts new file mode 100644 index 0000000..b797abc --- /dev/null +++ b/src/lib/src/sh-attach-menu.directive.ts @@ -0,0 +1,50 @@ +import {Directive, ElementRef, Input, OnDestroy, OnInit} from '@angular/core'; +import {ShContextMenuService} from './sh-context-menu.service'; +import {ShContextMenuComponent} from './sh-context-menu.component'; +import {Subscription} from 'rxjs/Subscription'; +import {fromEvent} from 'rxjs/observable/fromEvent'; +import {merge} from 'rxjs/observable/merge'; + +@Directive({ + selector: '[shAttachMenu]' +}) +export class ShAttachMenuDirective implements OnDestroy, OnInit { + @Input('shAttachMenu') menu: ShContextMenuComponent; + @Input('shMenuTriggers') triggers: string[]; + @Input('shMenuData') data: any; + sub: Subscription; + + constructor(private ctxService: ShContextMenuService, private elm: ElementRef) { + } + + ngOnInit(): void { + this.setupEvents(); + } + + private setupEvents() { + const observables = []; + + if (!this.triggers) { + observables.push(fromEvent(this.elm.nativeElement, 'contextmenu')); + } else { + this.triggers.forEach((t) => { + observables.push(fromEvent(this.elm.nativeElement, t)); + }); + } + + this.sub = merge(...observables).subscribe(this.openMenu.bind(this)); + } + + openMenu(event: MouseEvent) { + this.ctxService.openMenu({ + menu: this.menu, + mouseEvent: event, + targetElement: this.elm, + data: this.data + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } +} diff --git a/src/lib/src/sh-context-menu-item.directive.ts b/src/lib/src/sh-context-menu-item.directive.ts new file mode 100644 index 0000000..ff4a413 --- /dev/null +++ b/src/lib/src/sh-context-menu-item.directive.ts @@ -0,0 +1,42 @@ +import {Directive, EventEmitter, Input, Optional, Output, TemplateRef} from '@angular/core'; +import {ShContextMenuComponent} from './sh-context-menu.component'; +import {ShContextMenuClickEvent} from './sh-context-menu.models'; + +export class MenuItemContext { + $implicit: any; + + constructor() { + this.$implicit = {}; + } +} + +@Directive({ + selector: '[shContextMenuItem]' +}) +export class ShContextMenuItemDirective { + @Input() subMenu: ShContextMenuComponent; + @Input() divider: boolean; + @Input() visible: (event: ShContextMenuClickEvent) => boolean; + @Input() disabled: (event: ShContextMenuClickEvent) => boolean; + @Input() closeOnClick = true; + + @Output() click = new EventEmitter(); + + context: MenuItemContext = new MenuItemContext(); + + private _active: boolean; + + constructor(@Optional() public template: TemplateRef) { + } + + setNotActive() { + this._active = false; + if (this.subMenu) { + this.subMenu.setNotActive(); + } + } + + setActive() { + this._active = true; + } +} diff --git a/src/lib/src/sh-context-menu.component.ts b/src/lib/src/sh-context-menu.component.ts new file mode 100644 index 0000000..c5e4704 --- /dev/null +++ b/src/lib/src/sh-context-menu.component.ts @@ -0,0 +1,128 @@ +import { + Component, ContentChildren, ElementRef, Input, OnDestroy, QueryList, TemplateRef, ViewChild, ViewChildren, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import {ShContextMenuItemDirective} from './sh-context-menu-item.directive'; +import {ShContextMenuService} from './sh-context-menu.service'; +import {OverlayRef} from '@angular/cdk/overlay'; + +@Component({ + selector: 'sh-context-menu', + encapsulation: ViewEncapsulation.None, + styleUrls: ['sh-context-menu.css'], + template: ` + + +
+
+ + + +
+
+
+ ` +}) +export class ShContextMenuComponent implements OnDestroy { + @Input('this') thisContext: any; + + @ContentChildren(ShContextMenuItemDirective, {read: ShContextMenuItemDirective}) contentChildrenItems; + @ViewChildren(ShContextMenuItemDirective, {read: ShContextMenuItemDirective}) viewChildrenItems; + + @ViewChild('menuTemplate', {read: TemplateRef}) menuTemplate; + @ViewChild('menuContainer', {read: ViewContainerRef}) menuContainer; + + public overlayRef: OverlayRef; + private subActive: boolean; + + constructor(private ctxService: ShContextMenuService) { + this.contentChildrenItems = new QueryList(); + this.viewChildrenItems = new QueryList(); + } + + get menuItems(): QueryList { + // 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; + } + + onEnter($event: MouseEvent, item: ShContextMenuItemDirective, elm: HTMLElement) { + 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 setActive(item: ShContextMenuItemDirective) { + item.setActive(); + this.subActive = true; + } + + onClick(event: MouseEvent, item: ShContextMenuItemDirective) { + // 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 callWithContext(fn, fallbackContext, data, event) { + return fn.call(this.thisContext ? this.thisContext : fallbackContext, {data, event}); + } + + close(): void { + this.setNotActive(); + this.menuContainer.detach(); + this.overlayRef.detach(); + } + + ngOnDestroy(): void { + this.close(); + } + + setNotActive() { + this.subActive = false; + this.menuItems.forEach(i => i.setNotActive()); + } + + isVisible(item: ShContextMenuItemDirective) { + if (!item.visible) { + return true; + } + + return this.callWithContext(item.visible, this, item.context.$implicit, null); + } +} diff --git a/src/lib/src/sh-context-menu.css b/src/lib/src/sh-context-menu.css new file mode 100644 index 0000000..055beef --- /dev/null +++ b/src/lib/src/sh-context-menu.css @@ -0,0 +1,54 @@ +.sh-backdrop { + background-color: transparent; +} + +.sh-context-menu { + background: #ececec; + min-width: 150px; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1); + color: black; + padding: 5px 0; + margin: 0; +} + +.sh-context-menu--item { + padding: 5px 10px 5px 15px; + transition: all 0.15s; +} + +.sh-context-menu--item:hover, .sh-context-menu--item__sub-active { + background-color: #4b8bec; + color: white; + cursor: pointer; +} + +.sh-context-menu--item.sh-context-menu--item__divider:hover { + background-color: #ececec; + color: black; + cursor: default; +} + +.sh-context-menu--item__divider { + height: 1px; + margin: 1px 1px 8px 1px; + 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; + transform: translateY(-50%); + position: absolute; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 8px solid black; +} diff --git a/src/lib/src/sh-context-menu.models.ts b/src/lib/src/sh-context-menu.models.ts new file mode 100644 index 0000000..cea5449 --- /dev/null +++ b/src/lib/src/sh-context-menu.models.ts @@ -0,0 +1,18 @@ +import {ShContextMenuComponent} from './sh-context-menu.component'; +import {ElementRef} from '@angular/core'; + +export class ShContextMenuEvent { + menu: ShContextMenuComponent; + mouseEvent: MouseEvent; + targetElement: ElementRef; + data: any; +} + +export class ShContextSubMenuEvent extends ShContextMenuEvent { + parentMenu: ShContextMenuComponent; +} + +export class ShContextMenuClickEvent { + data: any; + event?: MouseEvent; +} diff --git a/src/lib/src/sh-context-menu.module.ts b/src/lib/src/sh-context-menu.module.ts new file mode 100755 index 0000000..4e73183 --- /dev/null +++ b/src/lib/src/sh-context-menu.module.ts @@ -0,0 +1,33 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {ShContextMenuComponent} from './sh-context-menu.component'; +import {ShAttachMenuDirective} from './sh-attach-menu.directive'; +import {ShContextMenuService} from './sh-context-menu.service'; +import {ShContextMenuItemDirective} from './sh-context-menu-item.directive'; +import {OverlayModule} from '@angular/cdk/overlay'; + +@NgModule({ + declarations: [ + ShAttachMenuDirective, + ShContextMenuComponent, + ShContextMenuItemDirective + ], + exports: [ + ShAttachMenuDirective, + ShContextMenuComponent, + ShContextMenuItemDirective + ], + providers: [ + ShContextMenuService + ], + imports: [ + CommonModule, + OverlayModule + ], + entryComponents: [ + ShContextMenuComponent + ] +}) +export class ShContextMenuModule { +} diff --git a/src/lib/src/sh-context-menu.service.ts b/src/lib/src/sh-context-menu.service.ts new file mode 100644 index 0000000..c20a8fb --- /dev/null +++ b/src/lib/src/sh-context-menu.service.ts @@ -0,0 +1,205 @@ +import {ElementRef, Injectable, OnDestroy} from '@angular/core'; +import {ShContextMenuComponent} from './sh-context-menu.component'; +import {CloseScrollStrategy, Overlay} from '@angular/cdk/overlay'; +import {TemplatePortal} from '@angular/cdk/portal'; +import {ConnectedPositionStrategy} from '@angular/cdk/overlay/typings/position/connected-position-strategy'; +import {ShContextMenuEvent, ShContextSubMenuEvent} from './sh-context-menu.models'; +import {OverlayRef} from '@angular/cdk/overlay'; +import {fromEvent} from 'rxjs/observable/fromEvent'; +import {Subscription} from 'rxjs/Subscription'; + +@Injectable() +export class ShContextMenuService implements OnDestroy { + activeOverlays: OverlayRef[] = []; + backDropSub: Subscription; + + activeMenu: ShContextMenuComponent; + + constructor(private overlay: Overlay) { + } + + openMenu(ctxEvent: ShContextMenuEvent) { + this.closeCurrentOverlays(); + const {menu, mouseEvent, targetElement, data} = ctxEvent; + + this.activeMenu = menu; + + mouseEvent.preventDefault(); + mouseEvent.stopPropagation(); + + this.overrideGetBoundingClientRect(targetElement, mouseEvent); + + const scrollStrategy = this.buildCloseScrollStrategy(); + const positionStrategy = this.buildConnectedPositionStrategy(targetElement); + + this.attachContextToItems(menu, data); + + const overlayRef = this.createAndAttachOverlay(positionStrategy, scrollStrategy, menu, true); + this.attachOverlayRef(menu, overlayRef); + + this.registerBackdropEvents(overlayRef); + } + + openSubMenu(ctxEvent: ShContextSubMenuEvent): any { + const {menu, mouseEvent, targetElement, data, parentMenu} = ctxEvent; + + mouseEvent.preventDefault(); + mouseEvent.stopPropagation(); + + const scrollStrategy = this.buildCloseScrollStrategy(); + const positionStrategy = this.buildConnectedPositionStrategyForSubMenu(targetElement); + const overlayRef = this.createAndAttachOverlay(positionStrategy, scrollStrategy, menu, false); + + this.attachContextToItems(menu, data); + this.attachThisContext(menu, parentMenu); + this.attachOverlayRef(menu, overlayRef); + } + + destroy() { + this.closeCurrentOverlays(); + this.backDropSub.unsubscribe(); + } + + ngOnDestroy(): void { + this.destroy(); + } + + closeSubMenus(menu: ShContextMenuComponent) { + const itemsWithSubMenus = menu + .menuItems + .filter(i => !!i.subMenu && !!i.subMenu.overlayRef); + + if (itemsWithSubMenus.length) { + itemsWithSubMenus.forEach(sm => this.closeSubMenus(sm.subMenu)); + + const overlayRefs = itemsWithSubMenus + .map(i => i.subMenu.overlayRef); + + overlayRefs.forEach(r => r.dispose()); + } + } + + private registerBackdropEvents(overlayRef: OverlayRef) { + const elm = overlayRef.backdropElement; + + this.backDropSub = fromEvent(elm, 'mousedown') + .subscribe(this.closeCurrentOverlays.bind(this)); + } + + private createAndAttachOverlay(positionStrategy: ConnectedPositionStrategy, + scrollStrategy: CloseScrollStrategy, + menu: ShContextMenuComponent, + hasBackdrop: boolean = true) { + + 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 + */ + const menuPortal = new TemplatePortal(menu.menuTemplate, menu.menuContainer); + overlayRef.attach(menuPortal); + + this.activeOverlays.push(overlayRef); + + return overlayRef; + } + + private buildCloseScrollStrategy() { + return this.overlay.scrollStrategies.close(); + } + + private buildConnectedPositionStrategy(elm: ElementRef): ConnectedPositionStrategy { + return this + .overlay + .position() + .connectedTo(elm, + {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'}); + } + + private buildConnectedPositionStrategyForSubMenu(elm: ElementRef): ConnectedPositionStrategy { + return this + .overlay + .position() + .connectedTo(elm, + {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'}); + } + + /* + we need to override getBoundingClientRect() to return the position of the menu. + this is done because @angular/cdk use this function internally to determine where the overlay should be positioned + https://github.com/angular/material2/blob/master/src/cdk/overlay/position/connected-position-strategy.ts#L288 + */ + private overrideGetBoundingClientRect(elm: ElementRef, event: MouseEvent) { + const {clientX, clientY} = event; + + elm.nativeElement.getBoundingClientRect = (): ClientRect => { + return { + bottom: clientY, + height: 0, + left: clientX, + right: clientX, + top: clientY, + width: 0 + }; + }; + } + + private closeCurrentOverlays() { + this.activeOverlays.forEach((o) => { + o.detach(); + o.dispose(); + }); + + this.activeOverlays = []; + + // TODO: create close subject and emit. + // subscribe in component + if (this.activeMenu) { + this.activeMenu.close(); + } + } + + private attachContextToItems(menu: ShContextMenuComponent, data: any) { + menu.menuItems.forEach(i => i.context.$implicit = data); + } + + private attachThisContext(menu: ShContextMenuComponent, parentMenu: ShContextMenuComponent) { + menu.thisContext = parentMenu.thisContext; + } + + private attachOverlayRef(menu: ShContextMenuComponent, overlayRef: OverlayRef) { + menu.overlayRef = overlayRef; + } +} diff --git a/src/polyfills.ts b/src/polyfills.ts index d68672f..6545292 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -34,7 +34,7 @@ // import 'core-js/es6/weak-map'; // import 'core-js/es6/set'; -/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +/** IE10 and IE11 requires the following for NgClass support click SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. /** IE10 and IE11 requires the following for the Reflect API. */ diff --git a/src/styles.css b/src/styles.css index 90d4ee0..c12f1b3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,4 @@ +@import "~@angular/cdk/overlay-prebuilt.css"; + + /* You can add global styles to this file, and also import other style files */ diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json index 51a1d68..3ac1166 100644 --- a/src/tsconfig.app.json +++ b/src/tsconfig.app.json @@ -3,7 +3,7 @@ "angularCompilerOptions": { "paths": { "ng2-right-click-menu": [ - "../lib/src/index.ts" + "./lib/src/index.ts" ] } }, diff --git a/tsconfig.json b/tsconfig.json index 475df5f..1ad142b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,7 @@ ], "paths": { "ng2-right-click-menu": [ - "../lib/src/index.ts" + "./lib/src/index.ts" ] } } diff --git a/tslint.json b/tslint.json index 9963d6c..aefac69 100644 --- a/tslint.json +++ b/tslint.json @@ -29,7 +29,7 @@ "interface-over-type-literal": true, "label-position": true, "max-line-length": [ - true, + false, 140 ], "member-access": false, @@ -118,13 +118,13 @@ "check-type" ], "directive-selector": [ - true, + false, "attribute", "app", "camelCase" ], "component-selector": [ - true, + false, "element", "app", "kebab-case" @@ -133,7 +133,7 @@ "use-input-property-decorator": true, "use-output-property-decorator": true, "use-host-property-decorator": true, - "no-input-rename": true, + "no-input-rename": false, "no-output-rename": true, "use-life-cycle-interface": true, "use-pipe-transform-interface": true, diff --git a/yarn.lock b/yarn.lock index 4f98f89..c67ff13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,12 @@ dependencies: tslib "^1.7.1" +"@angular/cdk@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-5.2.1.tgz#11500502deeaf42f88f92ca7d78d9a7642b07761" + dependencies: + tslib "^1.7.1" + "@angular/cli@1.6.5": version "1.6.5" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-1.6.5.tgz#9217c5d5c366292aca61fb0328c406bb5b0f2d76"