├── .travis.yml ├── .vscode └── settings.json ├── .eslintrc.json ├── gulpfile.js ├── src ├── contextMenu.options.ts ├── contextMenu.service.ts ├── contextMenu.attach.directive.ts ├── contextMenu.item.directive.ts └── contextMenu.component.ts ├── .editorconfig ├── .npmignore ├── demo ├── index.ts ├── index.html ├── app.module.ts ├── app.component.ts └── app.component.html ├── typings.json ├── gulp-tasks └── lint.js ├── tsconfig.json ├── LICENSE ├── .gitignore ├── angular2-contextmenu.ts ├── .config └── bundle-system.js ├── tslint.json ├── package.json ├── webpack.config.js ├── CHANGELOG.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "node" 6 | 7 | sudo: false 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.check.workspaceVersion": false, 3 | "vsicons.presets.angular": false 4 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/eslint-config-valorsoft/.eslintrc.json", 3 | "env": { 4 | "node": true 5 | } 6 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | 5 | require('require-dir')('./gulp-tasks'); 6 | 7 | gulp.task('default', () => { 8 | gulp.start('lint'); 9 | }); 10 | -------------------------------------------------------------------------------- /src/contextMenu.options.ts: -------------------------------------------------------------------------------- 1 | import { OpaqueToken } from '@angular/core'; 2 | 3 | export interface IContextMenuOptions { 4 | useBootstrap4: boolean; 5 | } 6 | export const CONTEXT_MENU_OPTIONS = new OpaqueToken('CONTEXT_MENU_OPTIONS'); 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # @AngularClass 2 | # http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | insert_final_newline = false 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | gulp-tasks 3 | logs 4 | 5 | # typings 6 | typings 7 | 8 | # testing 9 | karma.conf.js 10 | test.bundle.js 11 | coverage 12 | 13 | # demo build 14 | demo 15 | demo-build 16 | webpack.config.js 17 | 18 | #typescript sources 19 | *.ts 20 | *.js.map 21 | !*.d.ts 22 | /src/**/*.ts 23 | !/src/**/*.d.ts 24 | 25 | compiled 26 | -------------------------------------------------------------------------------- /demo/index.ts: -------------------------------------------------------------------------------- 1 | // require('./styles.css'); 2 | 3 | import { AppModule } from './app.module'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | function main(): any { 7 | return platformBrowserDynamic().bootstrapModule(AppModule); 8 | } 9 | 10 | document.addEventListener('DOMContentLoaded', () => main()); 11 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "moment": "registry:npm/moment#2.10.5+20160211003958", 4 | "webpack": "registry:npm/webpack#1.12.9+20160219013405" 5 | }, 6 | "devDependencies": {}, 7 | "ambientDependencies": { 8 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", 9 | "jasmine": "registry:dt/jasmine#2.2.0+20160317120654", 10 | "require": "registry:dt/require#2.1.20+20160316155526" 11 | } 12 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular2 Context Menu Demo 4 | 5 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /gulp-tasks/lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const tslint = require('gulp-tslint'); 5 | const gitignore = require('gitignore-to-glob')(); 6 | 7 | gitignore.push('**/*.ts'); 8 | 9 | gulp.task('tslint', () => 10 | gulp 11 | .src(gitignore) 12 | .pipe(tslint()) 13 | .pipe(tslint.report('prose', { 14 | emitError: true, 15 | summarizeFailureOutput: true, 16 | reportLimit: 50 17 | })) 18 | ); 19 | 20 | gulp.task('lint', ['tslint']); 21 | -------------------------------------------------------------------------------- /src/contextMenu.service.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenuComponent } from '../angular2-contextmenu'; 2 | import { Injectable } from '@angular/core'; 3 | import { Subject } from 'rxjs/Rx'; 4 | 5 | export interface IContextMenuClickEvent { 6 | actions?: any[]; 7 | contextMenu?: ContextMenuComponent; 8 | event: MouseEvent; 9 | item: any; 10 | } 11 | 12 | @Injectable() 13 | export class ContextMenuService { 14 | public show: Subject = new Subject(); 15 | } 16 | -------------------------------------------------------------------------------- /demo/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { ContextMenuModule } from '../angular2-contextmenu'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | bootstrap: [AppComponent], 9 | declarations: [AppComponent], 10 | imports: [ 11 | BrowserModule, 12 | CommonModule, 13 | ContextMenuModule, 14 | ], 15 | providers: [/* TODO: Providers go here */], 16 | }) 17 | export class AppModule { } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "jsx": "react", 8 | "listFiles": false, 9 | "noLib": false, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noImplicitAny": true, 13 | "preserveConstEnums": true, 14 | "removeComments": false, 15 | "sourceMap": false, 16 | "target": "es5" 17 | }, 18 | "exclude": [ 19 | "node_modules" 20 | ], 21 | "files": [ 22 | "./typings/browser.d.ts", 23 | "./angular2-contextmenu.ts" 24 | ], 25 | "angularCompilerOptions": { 26 | "genDir": "compiled" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/contextMenu.attach.directive.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenuComponent } from './contextMenu.component'; 2 | import { ContextMenuService } from './contextMenu.service'; 3 | import { Directive, HostListener, Input } from '@angular/core'; 4 | 5 | @Directive({ 6 | selector: '[contextMenu]', 7 | }) 8 | export class ContextMenuAttachDirective { 9 | @Input() public contextMenuSubject: any; 10 | @Input() public contextMenu: ContextMenuComponent; 11 | 12 | constructor(private contextMenuService: ContextMenuService) { } 13 | 14 | @HostListener('contextmenu', ['$event']) 15 | public onContextMenu(event: MouseEvent): void { 16 | this.contextMenuService.show.next({ 17 | contextMenu: this.contextMenu, 18 | event, 19 | item: this.contextMenuSubject, 20 | }); 21 | event.preventDefault(); 22 | event.stopPropagation(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/contextMenu.item.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, Output, EventEmitter, TemplateRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | /* tslint:disable:directive-selector-type */ 5 | selector: 'template[contextMenuItem]', 6 | /* tslint:enable:directive-selector-type */ 7 | }) 8 | export class ContextMenuItemDirective { 9 | @Input() public divider: boolean = false; 10 | @Input() public passive: boolean = false; 11 | @Input() public enabled: boolean | ((item: any) => boolean) = true; 12 | @Input() public visible: boolean | ((item: any) => boolean) = true; 13 | @Output() public execute: EventEmitter<{ event: Event, item: any }> = new EventEmitter<{ event: Event, item: any }>(); 14 | 15 | constructor(public template: TemplateRef<{ item: any }>) { } 16 | 17 | public evaluateIfFunction(value: any, item: any): any { 18 | if (value instanceof Function) { 19 | return value(item); 20 | } 21 | return value; 22 | } 23 | 24 | public triggerExecute(item: any, $event?: MouseEvent): void { 25 | if (!this.evaluateIfFunction(this.enabled, item)) { 26 | return; 27 | } 28 | this.execute.emit({ event: $event, item }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | 41 | # type script artifacts 42 | /typings 43 | 44 | # WebStorm 45 | .idea 46 | 47 | # ignore build and dist for now 48 | /bundles 49 | /demo-build 50 | /dist 51 | /coverage 52 | /ts 53 | 54 | # ignore incline compiling 55 | /demo/**/*.js 56 | /demo/**/*.js.map 57 | /demo/**/*.d.ts 58 | /src/**/*.js 59 | /src/**/*.js.map 60 | /src/**/*.d.ts 61 | /compiled 62 | *.metadata.json 63 | angular2-contextmenu.js 64 | angular2-contextmenu.d.ts 65 | angular2-contextmenu.js.map 66 | /logs 67 | -------------------------------------------------------------------------------- /angular2-contextmenu.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_OPTIONS, IContextMenuOptions } from './src/contextMenu.options'; 2 | import { ModuleWithProviders, NgModule } from '@angular/core'; 3 | import { CommonModule } from '@angular/common'; 4 | import { ContextMenuComponent } from './src/contextMenu.component'; 5 | import { ContextMenuItemDirective } from './src/contextMenu.item.directive'; 6 | import { ContextMenuService } from './src/contextMenu.service'; 7 | import { ContextMenuAttachDirective } from './src/contextMenu.attach.directive'; 8 | 9 | export * from './src/contextMenu.component'; 10 | export * from './src/contextMenu.service'; 11 | 12 | @NgModule({ 13 | declarations: [ 14 | ContextMenuAttachDirective, 15 | ContextMenuComponent, 16 | ContextMenuItemDirective, 17 | ], 18 | exports: [ 19 | ContextMenuAttachDirective, 20 | ContextMenuComponent, 21 | ContextMenuItemDirective, 22 | ], 23 | imports: [ 24 | CommonModule, 25 | ], 26 | providers: [ 27 | ContextMenuService, 28 | ], 29 | }) 30 | export class ContextMenuModule { 31 | public static forRoot(options: IContextMenuOptions): ModuleWithProviders { 32 | return { 33 | ngModule: ContextMenuModule, 34 | providers: [ 35 | { 36 | provide: CONTEXT_MENU_OPTIONS, 37 | useValue: options, 38 | }, 39 | ], 40 | }; 41 | } 42 | } 43 | export default ContextMenuModule; 44 | -------------------------------------------------------------------------------- /demo/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { ContextMenuService, ContextMenuComponent } from '../angular2-contextmenu'; 3 | 4 | @Component({ 5 | selector: 'angular2-context-menu-demo', 6 | styles: [` 7 | .dashboardContainer { 8 | width: 100%; 9 | height: 100%; 10 | position: fixed; 11 | } 12 | 13 | .componentsContainer { 14 | position: fixed; 15 | bottom: 0; 16 | top: 100px; 17 | width: 100%; } 18 | 19 | .componentContainer { 20 | overflow: auto; 21 | position: absolute; } 22 | `], 23 | templateUrl: './app.component.html', 24 | }) 25 | export class AppComponent { 26 | 27 | public items: any[] = [ 28 | { 29 | name: 'John', 30 | otherProperty: 'Foo', 31 | layout: { 32 | height: 90, 33 | left: 0, 34 | top: 0, 35 | width: 98, 36 | }, 37 | actions: [{ 38 | enabled: true, 39 | execute: (item: any): void => console.log(item), 40 | html: (item: any): string => `John custom: ${item.name}`, 41 | visible: true, 42 | }, { 43 | divider: true, 44 | visible: true, 45 | }, { 46 | enabled: true, 47 | execute: (item: any): void => console.log(item), 48 | html: (item: any): string => `John custom: ${item.name}`, 49 | visible: true, 50 | }], 51 | }, 52 | { 53 | name: 'Joe', 54 | otherProperty: 'Bar', 55 | layout: { 56 | height: 90, 57 | left: 98, 58 | top: 0, 59 | width: 98, 60 | }, 61 | actions: [{ 62 | enabled: true, 63 | execute: (item: any): void => console.log(item), 64 | html: (item: any): string => `Joe something: ${item.name}`, 65 | visible: true, 66 | }], 67 | }, 68 | ]; 69 | public outsideValue: string = 'something'; 70 | 71 | @ViewChild('basicMenu') public basicMenu: ContextMenuComponent; 72 | @ViewChild('enableAndVisible') public enableAndVisible: ContextMenuComponent; 73 | @ViewChild('withFunctions') public withFunctions: ContextMenuComponent; 74 | 75 | constructor(private contextMenuService: ContextMenuService) { } 76 | 77 | public onContextMenu($event: MouseEvent, item: any): void { 78 | this.contextMenuService.show.next({ event: $event, item: item }); 79 | $event.preventDefault(); 80 | } 81 | 82 | public showMessage(message: string): void { 83 | console.log(message); 84 | } 85 | 86 | public onlyJohn(item: any): boolean { 87 | return item.name === 'John'; 88 | } 89 | 90 | public onlyJoe(item: any): boolean { 91 | return item.name === 'Joe'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.config/bundle-system.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | /*eslint no-console: 0, no-sync: 0*/ 5 | 6 | // System.js bundler 7 | // simple and yet reusable system.js bundler 8 | // bundles, minifies and gzips 9 | 10 | const fs = require('fs'); 11 | const del = require('del'); 12 | const path = require('path'); 13 | const zlib = require('zlib'); 14 | const async = require('async'); 15 | const Builder = require('systemjs-builder'); 16 | 17 | const pkg = require('../package.json'); 18 | const name = pkg.name; 19 | const targetFolder = path.resolve('./bundles'); 20 | 21 | async.waterfall([ 22 | cleanBundlesFolder, 23 | getSystemJsBundleConfig, 24 | buildSystemJs({minify: false, sourceMaps: true, mangle: false}), 25 | getSystemJsBundleConfig, 26 | buildSystemJs({minify: true, sourceMaps: true, mangle: false}), 27 | gzipSystemJsBundle 28 | ], err => { 29 | if (err) { 30 | throw err; 31 | } 32 | }); 33 | 34 | function getSystemJsBundleConfig(cb) { 35 | const config = { 36 | baseURL: '..', 37 | transpiler: 'typescript', 38 | typescriptOptions: { 39 | module: 'cjs' 40 | }, 41 | map: { 42 | typescript: path.resolve('node_modules/typescript/lib/typescript.js'), 43 | '@angular/core': path.resolve('node_modules/@angular/core/index.js'), 44 | '@angular/common': path.resolve('node_modules/@angular/common/index.js'), 45 | '@angular/compiler': path.resolve('node_modules/@angular/compiler/index.js'), 46 | '@angular/platform-browser': path.resolve('node_modules/@angular/platform-browser/index.js'), 47 | '@angular/platform-browser-dynamic': path.resolve('node_modules/@angular/platform-browser-dynamic/'), 48 | rxjs: path.resolve('node_modules/rxjs') 49 | }, 50 | paths: { 51 | '*': '*.js' 52 | } 53 | }; 54 | 55 | config.meta = ['@angular/common','@angular/compiler','@angular/core', 56 | '@angular/platform-browser','@angular/platform-browser-dynamic', 'rxjs'].reduce((memo, currentValue) => { 57 | memo[path.resolve(`node_modules/${currentValue}/*`)] = {build: false}; 58 | return memo; 59 | }, {}); 60 | config.meta.moment = {build: false}; 61 | return cb(null, config); 62 | } 63 | 64 | function cleanBundlesFolder(cb) { 65 | return del(targetFolder) 66 | .then(paths => { 67 | console.log('Deleted files and folders:\n', paths.join('\n')); 68 | cb(); 69 | }); 70 | } 71 | 72 | function buildSystemJs(options) { 73 | return (config, cb) => { 74 | const minPostFix = options && options.minify ? '.min' : ''; 75 | const fileName = `${name}${minPostFix}.js`; 76 | const dest = path.resolve(__dirname, targetFolder, fileName); 77 | const builder = new Builder(); 78 | 79 | console.log('Bundling system.js file:', fileName, options); 80 | builder.config(config); 81 | return builder 82 | .bundle([name, name].join('/'), dest, options) 83 | .then(() => cb()) 84 | .catch(cb); 85 | }; 86 | } 87 | 88 | function gzipSystemJsBundle(cb) { 89 | const files = fs 90 | .readdirSync(path.resolve(targetFolder)) 91 | .map(file => path.resolve(targetFolder, file)) 92 | .filter(file => fs.statSync(file).isFile()) 93 | .filter(file => path.extname(file) !== 'gz'); 94 | 95 | return async.eachSeries(files, (file, gzipcb) => { 96 | process.nextTick(() => { 97 | console.log('Gzipping ', file); 98 | const gzip = zlib.createGzip({level: 9}); 99 | const inp = fs.createReadStream(file); 100 | const out = fs.createWriteStream(`${file}.gz`); 101 | 102 | inp.on('end', () => gzipcb()); 103 | inp.on('error', err => gzipcb(err)); 104 | return inp.pipe(gzip).pipe(out); 105 | }); 106 | }, cb); 107 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/tslint-microsoft-contrib", "node_modules/codelyzer"], 3 | "rules": { 4 | "align": [ 5 | true, 6 | "parameters", 7 | "arguments", 8 | "statements" 9 | ], 10 | "ban": false, 11 | "class-name": true, 12 | "comment-format": [ 13 | true, 14 | "check-space" 15 | ], 16 | "curly": true, 17 | "eofline": true, 18 | "forin": true, 19 | "indent": [ 20 | true, 21 | "spaces" 22 | ], 23 | "jsdoc-format": true, 24 | "label-position": true, 25 | "label-undefined": true, 26 | "max-line-length": [ 27 | true, 28 | 140 29 | ], 30 | "member-access": true, 31 | "member-ordering": [ 32 | true, 33 | "public-before-private", 34 | "static-before-instance", 35 | "variables-before-functions" 36 | ], 37 | "no-any": false, 38 | "no-arg": true, 39 | "no-banned-terms": true, 40 | "no-bitwise": true, 41 | "no-conditional-assignment": true, 42 | "no-consecutive-blank-lines": true, 43 | "no-console": [ 44 | true, 45 | "debug", 46 | "info", 47 | "time", 48 | "timeEnd", 49 | "trace" 50 | ], 51 | "no-construct": true, 52 | "no-constructor-vars": false, 53 | "no-debugger": false, 54 | "no-duplicate-case": true, 55 | "no-duplicate-key": true, 56 | "no-duplicate-variable": true, 57 | "no-empty": true, 58 | "no-empty-interfaces": true, 59 | "no-eval": true, 60 | "no-function-constructor-with-string-args": true, 61 | "no-for-in": true, 62 | "no-inferrable-types": false, 63 | "no-internal-module": true, 64 | "no-null-keyword": true, 65 | "no-require-imports": false, 66 | "no-reserved-keywords": true, 67 | "no-shadowed-variable": true, 68 | "no-sparse-arrays": true, 69 | "no-string-literal": true, 70 | "no-switch-case-fall-through": true, 71 | "no-trailing-whitespace": true, 72 | "no-unreachable": true, 73 | "no-unused-expression": true, 74 | "no-unused-variable": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "no-var-requires": false, 78 | "object-literal-sort-keys": true, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-finally", 85 | "check-whitespace" 86 | ], 87 | "quotemark": [ 88 | true, 89 | "single", 90 | "jsx-single", 91 | "avoid-escape" 92 | ], 93 | "radix": true, 94 | "semicolon": [true, "always"], 95 | "switch-default": true, 96 | "trailing-comma": [ 97 | true, { 98 | "multiline": "always", 99 | "singleline": "never" 100 | } 101 | ], 102 | "triple-equals": [ 103 | true, 104 | "allow-null-check" 105 | ], 106 | "typedef": [ 107 | true, 108 | "call-signature", 109 | "parameter", 110 | "property-declaration", 111 | "member-variable-declaration" 112 | ], 113 | "typedef-whitespace": [ 114 | true, { 115 | "call-signature": "nospace", 116 | "index-signature": "nospace", 117 | "parameter": "nospace", 118 | "property-declaration": "nospace", 119 | "variable-declaration": "nospace" 120 | }, { 121 | "call-signature": "space", 122 | "index-signature": "space", 123 | "parameter": "space", 124 | "property-declaration": "space", 125 | "variable-declaration": "space" 126 | } 127 | ], 128 | "use-strict": false, 129 | "variable-name": [ 130 | true, 131 | "check-format", 132 | "allow-leading-underscore", 133 | "ban-keywords" 134 | ], 135 | "whitespace": [ 136 | true, 137 | "check-branch", 138 | "check-decl", 139 | "check-operator", 140 | "check-separator", 141 | "check-type" 142 | ], 143 | "use-isnan": true, 144 | "jquery-deferred-must-complete": true, 145 | "mocha-avoid-only": true, 146 | "directive-selector-name": [true, "camelCase"], 147 | "component-selector-name": [true, "kebab-case"], 148 | "directive-selector-type": [true, "attribute"], 149 | "component-selector-type": [true, "element"], 150 | "use-input-property-decorator": true, 151 | "use-output-property-decorator": true, 152 | "use-host-property-decorator": true, 153 | "no-attribute-parameter-decorator": true, 154 | "no-input-rename": true, 155 | "no-output-rename": true, 156 | "no-forward-ref": false, 157 | "use-life-cycle-interface": true, 158 | "use-pipe-transform-interface": true, 159 | "component-class-suffix": true, 160 | "directive-class-suffix": true 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /demo/app.component.html: -------------------------------------------------------------------------------- 1 |

Angular2 Context Menu Demo

2 |
3 |
4 |
5 |
6 |

Base Case

7 |
    8 |
  • Right Click: {{item?.name}}
  • 9 |
10 |

Enabled and Visible

11 |
    12 |
  • Right Click: {{item?.name}}
  • 13 |
14 | 15 | 18 | 21 | 22 | 25 | 26 |

Enabled and Visible as Functions

27 |
    28 |
  • Right Click: {{item?.name}}
  • 29 |
30 | 31 | 34 | 37 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | 53 | 56 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
68 | {{ item.name }} 69 |
70 | 71 | 74 | 77 | 78 |
79 |
80 | 91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-contextmenu", 3 | "version": "0.8.2", 4 | "description": "An Angular 2 component to show a context menu on an arbitrary component", 5 | "main": "angular2-contextmenu.js", 6 | "scripts": { 7 | "flow.install:typings": "./node_modules/.bin/typings install", 8 | "flow.compile": "npm run flow.install:typings && npm run flow.compile:common && npm run flow.compile:system ", 9 | "flow.compile:common": "./node_modules/.bin/ngc", 10 | "flow.compile:system": "./.config/bundle-system.js", 11 | "flow.copy:src": "./node_modules/.bin/cpy angular2-contextmenu.ts \"components/*.ts\" ts --parents", 12 | "flow.clean": "./node_modules/.bin/del bundles coverage demo-build typings \"components/**/*.+(js|d.ts|js.map)\" dist \"angular2-contextmenu.+(js|d.ts|js.map)\"", 13 | "flow.deploy:gh-pages": "npm run flow.build:prod && ./node_modules/.bin/gh-pages -d demo-build", 14 | "flow.eslint": "./node_modules/.bin/eslint --ignore-path .gitignore --ext js --fix . .config", 15 | "disabled.flow.tslint": "./node_modules/.bin/gulp lint", 16 | "flow.lint": "npm run flow.eslint && npm run flow.tslint", 17 | "flow.changelog": "./node_modules/.bin/conventional-changelog -i CHANGELOG.md -s -p angular -v", 18 | "flow.github-release": "./node_modules/.bin/conventional-github-releaser -p angular", 19 | "flow.build:prod": "NODE_ENV=production ./node_modules/.bin/webpack --progress --color", 20 | "flow.build:dev": "./node_modules/.bin/webpack --progress --color", 21 | "flow.serve:dev": "./node_modules/.bin/webpack-dev-server --inline --colors --display-error-details --display-cached", 22 | "flow.serve:prod": "NODE_ENV=production ./node_modules/.bin/webpack-dev-server --inline --colors --display-error-details --display-cached", 23 | "disabled.prepublish": "npm run flow.clean && npm run flow.compile", 24 | "disabled.postpublish": "npm run flow.deploy:gh-pages", 25 | "start": "npm run flow.serve:dev", 26 | "pretest": "npm run flow.lint", 27 | "test": "NODE_ENV=test ./node_modules/.bin/karma start", 28 | "disabled.preversion": "npm test", 29 | "version": "npm run flow.changelog && git add -A", 30 | "postversion": "git push && git push --tags" 31 | }, 32 | "typings": "angular2-contextmenu.d.ts", 33 | "keywords": [ 34 | "angular2", 35 | "contextmenu", 36 | "angular2-contextmenu", 37 | "ng2", 38 | "ng2-contextmenu" 39 | ], 40 | "author": "Isaac Mann ", 41 | "license": "MIT", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+ssh://git@github.com/isaacplmann/angular2-contextmenu.git" 45 | }, 46 | "devDependencies": { 47 | "@angular/common": "^2.4.1", 48 | "@angular/compiler": "^2.4.1", 49 | "@angular/compiler-cli": "^2.4.1", 50 | "@angular/core": "^2.4.1", 51 | "@angular/platform-browser": "^2.4.1", 52 | "@angular/platform-browser-dynamic": "^2.4.1", 53 | "@angular/platform-server": "^2.4.1", 54 | "angular2-template-loader": "^0.5.0", 55 | "async": "1.5.2", 56 | "bootstrap": "3.3.6", 57 | "codecov": "1.0.1", 58 | "codelyzer": "0.0.28", 59 | "compression-webpack-plugin": "0.3.1", 60 | "conventional-changelog-cli": "1.2.0", 61 | "conventional-github-releaser": "1.1.2", 62 | "copy-webpack-plugin": "3.0.0", 63 | "cpy-cli": "1.0.1", 64 | "del-cli": "0.2.0", 65 | "es6-promise": "3.2.1", 66 | "es6-shim": "0.35.1", 67 | "es7-reflect-metadata": "1.6.0", 68 | "eslint-config-valorsoft": "0.0.14", 69 | "exports-loader": "0.6.3", 70 | "file-loader": "0.8.5", 71 | "gh-pages": "0.11.0", 72 | "gitignore-to-glob": "0.2.1", 73 | "gulp": "3.9.1", 74 | "gulp-size": "2.1.0", 75 | "gulp-tslint": "5.0.0", 76 | "html-loader": "0.4.3", 77 | "html-webpack-plugin": "2.17.0", 78 | "istanbul-instrumenter-loader": "0.2.0", 79 | "jasmine": "2.4.1", 80 | "karma": "0.13.22", 81 | "karma-chrome-launcher": "1.0.1", 82 | "karma-coverage": "1.0.0", 83 | "karma-jasmine": "1.0.2", 84 | "karma-phantomjs-launcher": "1.0.0", 85 | "karma-sourcemap-loader": "0.3.7", 86 | "karma-spec-reporter": "0.0.26", 87 | "karma-webpack": "1.7.0", 88 | "lite-server": "2.2.0", 89 | "markdown-loader": "0.1.7", 90 | "marked": "0.3.5", 91 | "phantomjs-polyfill": "0.0.2", 92 | "phantomjs-prebuilt": "2.1.7", 93 | "prismjs": "1.5.0", 94 | "prismjs-loader": "0.0.3", 95 | "raw-loader": "0.5.1", 96 | "reflect-metadata": "0.1.2", 97 | "require-dir": "0.3.0", 98 | "rxjs": "^5.0.0-beta.12", 99 | "source-map-loader": "0.1.5", 100 | "systemjs-builder": "0.15.17", 101 | "ts-loader": "0.8.2", 102 | "tslint-config-valorsoft": "1.0.3", 103 | "tslint-microsoft-contrib": "^2.0.10", 104 | "typescript": "2.0.10", 105 | "typings": "0.8.1", 106 | "webpack": "1.13.1", 107 | "webpack-combine-loaders": "^2.0.0", 108 | "webpack-dev-server": "1.14.1", 109 | "zone.js": "^0.6.21" 110 | }, 111 | "dependencies": {} 112 | } 113 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const marked = require('marked'); 6 | const webpack = require('webpack'); 7 | const reqPrism = require('prismjs'); 8 | const CompressionPlugin = require('compression-webpack-plugin'); 9 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 10 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 11 | const combineLoaders = require('webpack-combine-loaders'); 12 | 13 | // marked renderer hack 14 | marked.Renderer.prototype.code = function renderCode(code, lang) { 15 | const out = this.options.highlight(code, lang); 16 | const classMap = this.options.langPrefix + lang; 17 | 18 | if (!lang) { 19 | return `
${out}\n
`; 20 | } 21 | return `
${out}\n
\n`; 22 | }; 23 | 24 | /*eslint no-process-env:0, camelcase:0*/ 25 | const isProduction = (process.env.NODE_ENV || 'development') === 'production'; 26 | const devtool = process.env.NODE_ENV === 'test' ? 'inline-source-map' : 'source-map'; 27 | const dest = 'demo-build'; 28 | const absDest = root(dest); 29 | 30 | const config = { 31 | // isProduction ? 'source-map' : 'evale', 32 | devtool, 33 | debug: false, 34 | 35 | verbose: true, 36 | displayErrorDetails: true, 37 | context: __dirname, 38 | stats: { 39 | colors: true, 40 | reasons: true 41 | }, 42 | 43 | resolve: { 44 | cache: false, 45 | root: __dirname, 46 | extensions: ['', '.ts', '.js', '.json'] 47 | }, 48 | 49 | entry: { 50 | angular2: [ 51 | // Angular 2 Deps 52 | 'es6-shim', 53 | 'es6-promise', 54 | 'zone.js', 55 | 'reflect-metadata', 56 | '@angular/common', 57 | '@angular/core' 58 | ], 59 | 'angular2-contextmenu': ['angular2-contextmenu'], 60 | 'angular2-contextmenu-demo': 'demo' 61 | }, 62 | 63 | output: { 64 | path: absDest, 65 | filename: '[name].js', 66 | sourceMapFilename: '[name].js.map', 67 | chunkFilename: '[id].chunk.js' 68 | }, 69 | 70 | // our Development Server configs 71 | devServer: { 72 | inline: true, 73 | colors: true, 74 | historyApiFallback: true, 75 | contentBase: dest, 76 | //publicPath: dest, 77 | outputPath: dest, 78 | watchOptions: {aggregateTimeout: 300, poll: 1000} 79 | }, 80 | 81 | markdownLoader: { 82 | langPrefix: 'language-', 83 | highlight(code, lang) { 84 | const language = !lang || lang === 'html' ? 'markup' : lang; 85 | const Prism = global.Prism || reqPrism; 86 | 87 | if (!Prism.languages[language]) { 88 | require(`prismjs/components/prism-${language}.js`); 89 | } 90 | return Prism.highlight(code, Prism.languages[language]); 91 | } 92 | }, 93 | module: { 94 | loaders: [ 95 | // support markdown 96 | {test: /\.md$/, loader: 'html?minimize=false!markdown'}, 97 | // Support for *.json files. 98 | {test: /\.json$/, loader: 'json'}, 99 | // Support for CSS as raw text 100 | {test: /\.css$/, loader: 'raw'}, 101 | // support for .html as raw text 102 | {test: /\.html$/, loader: 'raw'}, 103 | // Support for .ts files. 104 | { 105 | test: /\.ts$/, 106 | loader: combineLoaders([{ loader: 'ts' }, { loader: 'angular2-template-loader' }]), 107 | // query: { 108 | // compilerOptions: { 109 | // removeComments: true, 110 | // noEmitHelpers: false 111 | // } 112 | // }, 113 | exclude: [/\.(spec|e2e)\.ts$/] 114 | } 115 | ], 116 | noParse: [ 117 | /rtts_assert\/src\/rtts_assert/, 118 | /reflect-metadata/, 119 | /zone\.js\/dist\/zone-microtask/ 120 | ] 121 | }, 122 | 123 | plugins: [ 124 | //new Clean([dest]), 125 | new webpack.optimize.DedupePlugin(), 126 | new webpack.optimize.OccurenceOrderPlugin(true), 127 | new webpack.optimize.CommonsChunkPlugin({ 128 | name: 'angular2', 129 | minChunks: Infinity, 130 | filename: 'angular2.js' 131 | }), 132 | // static assets 133 | new CopyWebpackPlugin([{from: 'demo/favicon.ico', to: 'favicon.ico'}]), 134 | new CopyWebpackPlugin([{from: 'demo/assets', to: 'assets'}]), 135 | // generating html 136 | new HtmlWebpackPlugin({template: 'demo/index.html'}) 137 | ], 138 | pushPlugins() { 139 | if (!isProduction) { 140 | return; 141 | } 142 | const plugins = [ 143 | //production only 144 | new webpack.optimize.UglifyJsPlugin({ 145 | beautify: false, 146 | mangle: false, 147 | comments: false, 148 | compress: { 149 | screw_ie8: true 150 | //warnings: false, 151 | //drop_debugger: false 152 | } 153 | //verbose: true, 154 | //beautify: false, 155 | //quote_style: 3 156 | }), 157 | new CompressionPlugin({ 158 | asset: '{file}.gz', 159 | algorithm: 'gzip', 160 | regExp: /\.js$|\.html|\.css|.map$/, 161 | threshold: 10240, 162 | minRatio: 0.8 163 | }) 164 | ]; 165 | 166 | this 167 | .plugins 168 | .push 169 | .apply(plugins); 170 | } 171 | }; 172 | 173 | config.pushPlugins(); 174 | 175 | module.exports = config; 176 | 177 | function root(partialPath) { 178 | return path.join(__dirname, partialPath); 179 | } 180 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [0.8.2](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.8.1...v0.8.2) (2017-03-31) 3 | 4 | 5 | ### Documentation 6 | 7 | * **doc:** Point to ngx-contextmenu for upgrades. 8 | 9 | 10 | 11 | 12 | # [0.8.1](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.8.0...v0.8.1) (2017-02-28) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **position:** Default to 100x100 size if menuElement is undefined 18 | 19 | 20 | 21 | 22 | # [0.8.0](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.7...v0.8.0) (2017-02-27) 23 | 24 | 25 | ### Features 26 | 27 | * **passive:** Add a passive menuitem that will not close the menu when clicked 28 | 29 | 30 | 31 | 32 | # [0.7.7](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.6...v0.7.7) (2017-02-10) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **position:** Use getComputedStyle to calculate offsetParent style 38 | 39 | 40 | 41 | 42 | # [0.7.6](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.5...v0.7.6) (2017-02-06) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * **position:** Don't offset context menu when inside fixed positioned element 48 | 49 | 50 | 51 | 52 | # [0.7.5](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.4...v0.7.5) (2017-02-01) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **position:** Don't offset context menu when inside absolute positioned element 58 | 59 | 60 | 61 | 62 | # [0.7.4](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.3...v0.7.4) (2017-01-30) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * **position:** Recover when context menu offsetParent is undefined 68 | 69 | 70 | 71 | 72 | # [0.7.3](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.1...v0.7.3) (2017-01-27) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * **position:** Position contextmenu when parent element has 3d transform 78 | 79 | 80 | 81 | 82 | # 0.7.2 (2017-01-27) 83 | 84 | ### Bad build - ignore 85 | 86 | 87 | 88 | # [0.7.1](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.7.0...v0.7.1) (2017-01-18) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * **closing:** Close context menu with esc key in Safari 94 | 95 | 96 | 97 | 98 | # [0.7.0](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.6.0...v0.7.0) (2017-01-05) 99 | 100 | 101 | ### Features 102 | 103 | * **dividers:** Add appropriate divider class for bootstrap 4 104 | * **position:** Position context menu to the left or above the mouse location, if the menu goes outside the body element 105 | * **closing:** Hide the context menu on escape keydown or window scroll events 106 | 107 | 108 | 109 | 110 | # [0.6.0](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.5.3...v0.6.0) (2016-12-08) 111 | 112 | 113 | ### Features 114 | 115 | * **dividers:** Add the ability to create dividers in the context menu ([c050ae5](https://github.com/isaacplmann/angular2-contextmenu/commit/c050ae5)) 116 | 117 | 118 | 119 | 120 | ## [0.5.3](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.5.2...v0.5.3) (2016-12-02) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * **styles:** Remove invisible context menu items from the DOM instead of using [hidden] attribute ([1a7121e](https://github.com/isaacplmann/angular2-contextmenu/commit/1a7121e)) 126 | 127 | 128 | 129 | 130 | # [0.5.1](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.4.1...v0.5.1) (2016-10-20) 131 | - Add `forRoot` function to globally set `useBootstrap4` 132 | 133 | 134 | 135 | ## [0.4.1](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.4.1) (2016-10-12) 136 | - Fix bug `[enabled]` as a function not blocking execution 137 | 138 | 139 | 140 | # [0.4.0](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.4.0) (2016-10-11) 141 | - `[contextMenu]` and `[contextMenuSubject]` to automate wiring up the context menu 142 | - `[visible]` and `[enabled]` can be booleans or functions 143 | - Can have multiple context menus per component 144 | 145 | 146 | 147 | ## [0.2.1](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.2.1) (2016-09-12) 148 | - Fix type definition for `ContextMenuService` to make `actions` optional 149 | 150 | 151 | 152 | # [0.2.0](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.2.0) (2016-09-12) 153 | - Add `ContextMenuItemComponent` for declarative configuration 154 | 155 | 156 | 157 | ## [0.1.11](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.1.11) (2016-07-28) 158 | 159 | 160 | 161 | 162 | ## [0.1.10](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.1.10) (2016-07-28) 163 | 164 | 165 | 166 | 167 | ## [0.1.10](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.5...v0.1.10) (2016-07-26) 168 | 169 | 170 | 171 | 172 | ## [0.1.5](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.4...v0.1.5) (2016-05-25) 173 | 174 | 175 | 176 | 177 | ## [0.1.4](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.3...v0.1.4) (2016-05-25) 178 | 179 | 180 | 181 | 182 | ## [0.1.3](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.2...v0.1.3) (2016-05-25) 183 | 184 | 185 | 186 | 187 | ## [0.1.2](https://github.com/isaacplmann/angular2-contextmenu/compare/v0.1.1...v0.1.2) (2016-05-25) 188 | 189 | 190 | 191 | 192 | ## 0.1.1 (2016-05-25) 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /src/contextMenu.component.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenuItemDirective } from './contextMenu.item.directive'; 2 | import { CONTEXT_MENU_OPTIONS, IContextMenuOptions } from './contextMenu.options'; 3 | import { ContextMenuService, IContextMenuClickEvent } from './contextMenu.service'; 4 | import { 5 | AfterContentInit, 6 | ChangeDetectorRef, 7 | Component, 8 | ContentChildren, 9 | ElementRef, 10 | EventEmitter, 11 | HostListener, 12 | Inject, 13 | Input, 14 | Optional, 15 | Output, 16 | QueryList, 17 | ViewChild 18 | } from '@angular/core'; 19 | 20 | export interface ILinkConfig { 21 | click: (item: any, $event?: MouseEvent) => void; 22 | enabled?: (item: any) => boolean; 23 | html: (item: any) => string; 24 | } 25 | export interface MouseLocation { 26 | left?: string; 27 | marginLeft?: string; 28 | marginTop?: string; 29 | top?: string; 30 | } 31 | 32 | @Component({ 33 | selector: 'context-menu', 34 | styles: [ 35 | `.passive { 36 | display: block; 37 | padding: 3px 20px; 38 | clear: both; 39 | font-weight: normal; 40 | line-height: @line-height-base; 41 | white-space: nowrap; 42 | }` 43 | ], 44 | template: 45 | ` 71 | `, 72 | }) 73 | export class ContextMenuComponent implements AfterContentInit { 74 | @Input() public useBootstrap4: boolean = false; 75 | @Output() public close: EventEmitter = new EventEmitter(); 76 | @ContentChildren(ContextMenuItemDirective) public menuItems: QueryList; 77 | @ViewChild('menu') public menuElement: ElementRef; 78 | public visibleMenuItems: ContextMenuItemDirective[] = []; 79 | 80 | public links: ILinkConfig[] = []; 81 | public isShown: boolean = false; 82 | public isOpening: boolean = false; 83 | public item: any; 84 | private mouseLocation: MouseLocation = { left: '0px', top: '0px' }; 85 | constructor( 86 | private _contextMenuService: ContextMenuService, 87 | private changeDetector: ChangeDetectorRef, 88 | private elementRef: ElementRef, 89 | @Optional() 90 | @Inject(CONTEXT_MENU_OPTIONS) private options: IContextMenuOptions 91 | ) { 92 | if (options) { 93 | this.useBootstrap4 = options.useBootstrap4; 94 | } 95 | _contextMenuService.show.subscribe(menuEvent => this.onMenuEvent(menuEvent)); 96 | } 97 | 98 | stopEvent($event: MouseEvent) { 99 | $event.stopPropagation() 100 | } 101 | 102 | get locationCss(): any { 103 | return { 104 | 'position': 'fixed', 105 | 'display': this.isShown ? 'block' : 'none', 106 | left: this.mouseLocation.left, 107 | marginLeft: this.mouseLocation.marginLeft, 108 | marginTop: this.mouseLocation.marginTop, 109 | top: this.mouseLocation.top, 110 | }; 111 | } 112 | 113 | @HostListener('document:click') 114 | @HostListener('document:contextmenu') 115 | public clickedOutside(): void { 116 | if (!this.isOpening) { 117 | this.hideMenu(); 118 | } 119 | } 120 | 121 | public ngAfterContentInit(): void { 122 | this.menuItems.forEach(menuItem => { 123 | menuItem.execute.subscribe(() => this.hideMenu()); 124 | }); 125 | } 126 | 127 | public isMenuItemEnabled(menuItem: ContextMenuItemDirective): boolean { 128 | return this.evaluateIfFunction(menuItem.enabled); 129 | } 130 | 131 | public isMenuItemVisible(menuItem: ContextMenuItemDirective): boolean { 132 | return this.evaluateIfFunction(menuItem.visible); 133 | } 134 | 135 | public evaluateIfFunction(value: any): any { 136 | if (value instanceof Function) { 137 | return value(this.item); 138 | } 139 | return value; 140 | } 141 | 142 | public isDisabled(link: ILinkConfig): boolean { 143 | return link.enabled && !link.enabled(this.item); 144 | } 145 | 146 | public execute(link: ILinkConfig, $event?: MouseEvent): void { 147 | if (this.isDisabled(link)) { 148 | return; 149 | } 150 | this.hideMenu(); 151 | link.click(this.item, $event); 152 | } 153 | 154 | public onMenuEvent(menuEvent: IContextMenuClickEvent): void { 155 | let { actions, contextMenu, event, item } = menuEvent; 156 | if (contextMenu && contextMenu !== this) { 157 | this.hideMenu(); 158 | return; 159 | } 160 | this.isOpening = true; 161 | setTimeout(() => this.isOpening = false, 400); 162 | if (actions) { 163 | if (console && console.warn) { 164 | console.warn(`actions configuration object is deprecated and will be removed in version 1.x. 165 | See https://github.com/isaacplmann/angular2-contextmenu for the new declarative syntax.`); 166 | } 167 | } 168 | if (actions && actions.length > 0) { 169 | // Imperative context menu 170 | this.setVisibleMenuItems(); 171 | this.showMenu(); 172 | } else if (this.menuItems) { 173 | // Declarative context menu 174 | setTimeout(() => { 175 | this.setVisibleMenuItems(); 176 | if (this.visibleMenuItems.length > 0) { 177 | this.showMenu(); 178 | } else { 179 | this.hideMenu(); 180 | } 181 | setTimeout(() => { 182 | const menuWidth = this.menuElement ? this.menuElement.nativeElement.clientWidth : 100; 183 | const menuHeight = this.menuElement ? this.menuElement.nativeElement.clientHeight : 100; 184 | const bodyWidth = event.view.document.body.clientWidth; 185 | const bodyHeight = event.view.document.body.clientHeight; 186 | const distanceFromRight = bodyWidth - (event.clientX + menuWidth); 187 | const distanceFromBottom = bodyHeight - (event.clientY + menuHeight); 188 | let isMenuOutsideBody: boolean = false; 189 | if (distanceFromRight < 0 && event.clientX > bodyWidth / 2) { 190 | this.mouseLocation.marginLeft = '-' + menuWidth + 'px'; 191 | isMenuOutsideBody = true; 192 | } 193 | if (distanceFromBottom < 0 && event.clientY > bodyHeight / 2) { 194 | this.mouseLocation.marginTop = '-' + menuHeight + 'px'; 195 | isMenuOutsideBody = true; 196 | } 197 | if (isMenuOutsideBody) { 198 | this.showMenu(); 199 | } 200 | }); 201 | }); 202 | } else { 203 | this.hideMenu(); 204 | } 205 | this.links = actions; 206 | this.item = item; 207 | let adjustX = 0; 208 | let adjustY = 0; 209 | const offsetParent: HTMLElement = this.elementRef.nativeElement.offsetParent; 210 | if (offsetParent && offsetParent.tagName !== 'BODY') { 211 | const position = event.view.getComputedStyle(offsetParent).position; 212 | if (position !== 'absolute' && position !== 'fixed') { 213 | const { left, top } = offsetParent.getBoundingClientRect(); 214 | adjustX = -left; 215 | adjustY = -top; 216 | } 217 | } 218 | this.mouseLocation = { 219 | left: event.clientX + adjustX + 'px', 220 | top: event.clientY + adjustY + 'px', 221 | }; 222 | } 223 | 224 | public setVisibleMenuItems(): void { 225 | this.visibleMenuItems = this.menuItems.filter(menuItem => this.isMenuItemVisible(menuItem)); 226 | } 227 | 228 | public showMenu(): void { 229 | this.isShown = true; 230 | this.changeDetector.markForCheck(); 231 | } 232 | 233 | @HostListener('window:scroll') 234 | @HostListener('document:keydown', ['$event']) 235 | public hideMenu(event?: KeyboardEvent): void { 236 | if (event && (event.keyCode && event.keyCode !== 27 || event.key && event.key !== 'Escape')) { 237 | return; 238 | } 239 | if (this.isShown === true) { 240 | this.close.emit({}); 241 | } 242 | this.isShown = false; 243 | this.changeDetector.markForCheck(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated - use [ngx-contextmenu](https://github.com/isaacplmann/ngx-contextmenu) instead 2 | 3 | # angular2-contextmenu 4 | 5 | **This library is being moved to [ngx-contextmenu](https://github.com/isaacplmann/ngx-contextmenu). With the name change comes support for Angular 4 and removal of the old imperative syntax.** 6 | 7 | A context menu built with Angular 2 inspired by [ui.bootstrap.contextMenu](https://github.com/Templarian/ui.bootstrap.contextMenu). Bootstrap classes are included in the markup, but there is no explicit dependency on Bootstrap. [Demo](http://plnkr.co/edit/wpJXpEh4zNZ4uCxTURx2?p=preview) 8 | 9 | ## Installation 10 | 11 | - `npm install angular2-contextmenu` 12 | - import ContextMenuModule into your app module 13 | 14 | ## Usage 15 | 16 | ### Declarative vs. Imperative 17 | 18 | With version 0.2.0, there is a new declarative syntax that allows for quite a bit more flexibility and keeps html out of configuration objects. 19 | The older syntax is deprecated and will be removed in version 1.x. (I have no timeline on when I'll release 1.x, but wanted to give everyone advance warning.) 20 | 21 | ### Template 22 | 23 | ```html 24 |
    25 |
  • Right Click: {{item?.name}}
  • 26 |
27 | 28 | 31 | 32 | 35 | 38 | 39 | ``` 40 | 41 | ### Component Code 42 | 43 | ```js 44 | @Component({ 45 | ... 46 | }) 47 | export class MyContextMenuClass { 48 | public items = [ 49 | { name: 'John', otherProperty: 'Foo' }, 50 | { name: 'Joe', otherProperty: 'Bar' } 51 | ]; 52 | @ViewChild(ContextMenuComponent) public basicMenu: ContextMenuComponent; 53 | } 54 | ``` 55 | 56 | ## Context Menu Items 57 | 58 | - Each context menu item is a `