├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── build.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── multiple │ │ ├── multiple-menu.component.ts │ │ └── multiple.component.ts │ ├── package.json │ ├── simple │ │ ├── animated-menu.component.ts │ │ ├── simple-menu.component.ts │ │ └── simple.component.ts │ └── submenu │ │ ├── submenu-menu.component.ts │ │ ├── submenu.component.ts │ │ └── subsubmenu-menu.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── lib │ ├── context-menu-injector.ts │ ├── context-menu-trigger.directive.ts │ ├── context-menu.module.ts │ ├── context-menu.service.ts │ ├── context-submenu-trigger.directive.ts │ ├── menu.component.ts │ ├── package.json │ └── public_api.ts ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── vercel.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@5 4 | browser-tools: circleci/browser-tools@1 5 | jobs: 6 | test: 7 | docker: 8 | - image: cimg/node:current-browsers 9 | environment: 10 | CHROME_BIN: '/usr/bin/google-chrome' 11 | steps: 12 | - browser-tools/install-chrome 13 | - checkout 14 | - node/install-packages 15 | - run: 16 | name: ghpages 17 | command: npm run ghpages 18 | - run: 19 | name: lint 20 | command: npm run lint 21 | release: 22 | executor: 23 | name: node/default 24 | tag: 'current' 25 | steps: 26 | - checkout 27 | - node/install-packages 28 | - run: npm run build 29 | - run: cd dist && npx semantic-release 30 | 31 | workflows: 32 | version: 2 33 | test_and_release: 34 | # Run the test jobs first, then the release only when all the test jobs are successful 35 | jobs: 36 | - test 37 | - release: 38 | filters: 39 | branches: 40 | only: 41 | - master 42 | context: 43 | - npm 44 | requires: 45 | - test 46 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:@angular-eslint/recommended", 15 | "plugin:@angular-eslint/template/process-inline-templates" 16 | ], 17 | "rules": { 18 | "@typescript-eslint/no-explicit-any": "off" 19 | } 20 | }, 21 | { 22 | "files": [ 23 | "*.html" 24 | ], 25 | "extends": [ 26 | "plugin:@angular-eslint/template/recommended" 27 | ], 28 | "rules": {} 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "printWidth": 100, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Scott Cooper 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-rightclick 2 | 3 | [![npm](https://badge.fury.io/js/%40ctrl%2Fngx-rightclick.svg)](https://www.npmjs.org/package/@ctrl/ngx-rightclick) 4 | [![CircleCI](https://circleci.com/gh/scttcper/ngx-rightclick.svg?style=svg)](https://circleci.com/gh/scttcper/ngx-rightclick) 5 | 6 | > Context Menu Service for Angular 7 | 8 | Demo: https://ngx-rightclick.vercel.app 9 | 10 | ## Install 11 | 12 | if you don't already have `@angular/cdk` that needs to be installed too 13 | 14 | ```sh 15 | npm install @ctrl/ngx-rightclick 16 | ``` 17 | 18 | ## Dependencies 19 | 20 | Latest version available for each version of Angular 21 | 22 | | ngx-rightclick | Angular | 23 | | -------------- | ------- | 24 | | 3.0.1 | 9.x | 25 | | 4.0.0 | 10 < 15 | 26 | | 15.0.0 | >= 15.* | 27 | 28 | ## Use 29 | 30 | Import and Add to NgModule 31 | 32 | ```ts 33 | import { ContextMenuModule } from '@ctrl/ngx-rightclick'; 34 | ``` 35 | 36 | Add context menu directive to element and pass the menu component to be shown. **Important** the menu component must also be added as to entryComponents in your NgModule. [See here](https://github.com/scttcper/ngx-rightclick/blob/2d9d0430e1e762e202d39dbad79da6bdaea1db23/src/app/app.module.ts#L47-L53) 37 | 38 | ```ts 39 | // show.component.ts 40 | @Component({ 41 | template: ` 42 |
Right Click
43 | `, 44 | }) 45 | export class ShowComponent { 46 | // menu component imported and assigned locally 47 | menu = SimpleMenuComponent; 48 | } 49 | ``` 50 | 51 | ```ts 52 | // my-menu.component.ts 53 | import { Component } from '@angular/core'; 54 | 55 | import { MenuComponent, ContextMenuService, MenuPackage } from '@ctrl/ngx-rightclick'; 56 | 57 | @Component({ 58 | selector: 'simple-menu', 59 | // add your menu html 60 | template: `Download`, 61 | }) 62 | export class SimpleMenuComponent extends MenuComponent { 63 | // this module does not have animations, set lazy false 64 | lazy = false; 65 | 66 | constructor(public menuPackage: MenuPackage, public contextMenuService: ContextMenuService) { 67 | super(menuPackage, contextMenuService); 68 | // grab any required menu context passed via menuContext input 69 | console.log(menuPackage.context); 70 | } 71 | 72 | handleClick() { 73 | // IMPORTANT! tell the menu to close, anything passed in here is given to (menuAction) 74 | this.contextMenuService.closeAll(); 75 | } 76 | } 77 | ``` 78 | 79 | Last step add css somewhere in your global styles 80 | 81 | ```css 82 | .cdk-overlay-container { 83 | position: fixed; 84 | z-index: 1000; 85 | pointer-events: none; 86 | top: 0; 87 | left: 0; 88 | width: 100%; 89 | height: 100%; 90 | } 91 | .ngx-contextmenu.cdk-overlay-pane { 92 | position: absolute; 93 | pointer-events: auto; 94 | box-sizing: border-box; 95 | } 96 | 97 | // not required but can help with mobile right click 98 | .target { 99 | user-select: none; 100 | } 101 | ``` 102 | 103 | ## [Inputs] 104 | 105 | | name | type | description | 106 | | ------------------------------------------ | ----------- | ------------------------------------------------------- | 107 | | contextMenuTrigger / contextSubmenuTrigger | `component` | the menu or submenu to be shown | 108 | | menuContext | `any` | passed to the menu component via MenuPackage | 109 | | holdToDisplay | `number` | `default: 1000` ms pressing down on mobile to show menu | 110 | 111 | ## (Ouput) 112 | 113 | | name | type | description | 114 | | ---------- | ------ | --------------------------------------------------- | 115 | | menuAction | `any` | whatever is passed to `ContextMenuService.closeAll` | 116 | | menuClose | `void` | triggered whenever a menu or submenu is closed | 117 | 118 | ## Submenu 119 | 120 | Use the `contextSubmenuTrigger` directive as you would the contextMenuTrigger inside your menu. 121 | 122 | ## Other Options 123 | 124 | [ngx-contextmenu](https://github.com/isaacplmann/ngx-contextmenu) 125 | Find the Angular Component of your dreams on [angular.parts](https://angular.parts/) 126 | 127 | ## License 128 | 129 | MIT 130 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-rightclick": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "src/styles.scss" 34 | ], 35 | "scripts": [], 36 | "vendorChunk": true, 37 | "extractLicenses": false, 38 | "buildOptimizer": false, 39 | "sourceMap": true, 40 | "optimization": false, 41 | "namedChunks": true 42 | }, 43 | "configurations": { 44 | "production": { 45 | "fileReplacements": [ 46 | { 47 | "replace": "src/environments/environment.ts", 48 | "with": "src/environments/environment.prod.ts" 49 | } 50 | ], 51 | "optimization": true, 52 | "outputHashing": "all", 53 | "sourceMap": false, 54 | "namedChunks": false, 55 | "extractLicenses": true, 56 | "vendorChunk": false, 57 | "buildOptimizer": true, 58 | "budgets": [ 59 | { 60 | "type": "initial", 61 | "maximumWarning": "500kb", 62 | "maximumError": "1mb" 63 | }, 64 | { 65 | "type": "anyComponentStyle", 66 | "maximumWarning": "2kb", 67 | "maximumError": "4kb" 68 | } 69 | ] 70 | } 71 | }, 72 | "defaultConfiguration": "" 73 | }, 74 | "serve": { 75 | "builder": "@angular-devkit/build-angular:dev-server", 76 | "options": { 77 | "browserTarget": "ngx-rightclick:build" 78 | }, 79 | "configurations": { 80 | "production": { 81 | "browserTarget": "ngx-rightclick:build:production" 82 | } 83 | } 84 | }, 85 | "extract-i18n": { 86 | "builder": "@angular-devkit/build-angular:extract-i18n", 87 | "options": { 88 | "browserTarget": "ngx-rightclick:build" 89 | } 90 | }, 91 | "test": { 92 | "builder": "@angular-devkit/build-angular:karma", 93 | "options": { 94 | "main": "src/test.ts", 95 | "polyfills": "src/polyfills.ts", 96 | "tsConfig": "tsconfig.spec.json", 97 | "karmaConfig": "karma.conf.js", 98 | "assets": [ 99 | "src/favicon.ico", 100 | "src/assets" 101 | ], 102 | "styles": [ 103 | "src/styles.scss" 104 | ], 105 | "scripts": [] 106 | } 107 | }, 108 | "lint": { 109 | "builder": "@angular-eslint/builder:lint", 110 | "options": { 111 | "lintFilePatterns": [ 112 | "src/**/*.ts", 113 | "src/**/*.html" 114 | ] 115 | } 116 | } 117 | } 118 | } 119 | }, 120 | "cli": { 121 | "analytics": false, 122 | "schematicCollections": [ 123 | "@angular-eslint/schematics" 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import { copyFileSync } from 'fs'; 2 | import { build } from 'ng-packagr'; 3 | import { join } from 'path'; 4 | 5 | 6 | async function main() { 7 | // make common 8 | await build({ 9 | project: join(process.cwd(), 'src/lib/package.json'), 10 | }); 11 | 12 | copyFileSync('README.md', join(process.cwd(), 'dist/README.md')); 13 | copyFileSync('LICENSE', join(process.cwd(), 'dist/LICENSE')); 14 | } 15 | 16 | main() 17 | .then(() => console.log('success')) 18 | .catch((e) => { 19 | console.error(e); 20 | process.exit(1); 21 | }); 22 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/zzz'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | customLaunchers: { 30 | ChromeCI: { 31 | base: `${process.env['TRAVIS'] ? 'ChromeHeadless' : 'Chrome'}`, 32 | flags: process.env['TRAVIS'] ? ['--no-sandbox'] : [], 33 | }, 34 | }, 35 | singleRun: false, 36 | restartOnFileChange: true, 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ctrl/ngx-rightclick", 3 | "version": "0.0.0-placeholder", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "prebuild": "del-cli dist", 9 | "build": "ts-node -O '{\"module\": \"commonjs\"}' build.ts", 10 | "ghpages": "ng build --configuration production --no-progress", 11 | "test": "ng test --watch=false --browsers=ChromeCI", 12 | "test:ci": "ng test --watch=false --code-coverage --no-progress --browsers=ChromeCI", 13 | "lint": "ng lint ngx-rightclick", 14 | "lint:fix": "ng lint ngx-rightclick" 15 | }, 16 | "private": true, 17 | "devDependencies": { 18 | "@angular-devkit/build-angular": "15.1.1", 19 | "@angular-eslint/builder": "15.2.0", 20 | "@angular-eslint/eslint-plugin": "15.2.0", 21 | "@angular-eslint/eslint-plugin-template": "15.2.0", 22 | "@angular-eslint/schematics": "15.2.0", 23 | "@angular-eslint/template-parser": "15.2.0", 24 | "@angular/animations": "15.1.0", 25 | "@angular/cdk": "15.1.0", 26 | "@angular/cli": "15.1.1", 27 | "@angular/common": "15.1.0", 28 | "@angular/compiler": "15.1.0", 29 | "@angular/compiler-cli": "15.1.0", 30 | "@angular/core": "15.1.0", 31 | "@angular/forms": "15.1.0", 32 | "@angular/language-service": "15.1.0", 33 | "@angular/material": "15.1.0", 34 | "@angular/platform-browser": "15.1.0", 35 | "@angular/platform-browser-dynamic": "15.1.0", 36 | "@angular/router": "15.1.0", 37 | "@ctrl/ngx-github-buttons": "6.0.3", 38 | "@types/jasmine": "~3.6.0", 39 | "@types/node": "14.0.22", 40 | "@typescript-eslint/eslint-plugin": "5.48.1", 41 | "@typescript-eslint/parser": "5.48.1", 42 | "bootstrap": "4.5.0", 43 | "codelyzer": "^6.0.2", 44 | "core-js": "3.6.5", 45 | "del-cli": "3.0.1", 46 | "eslint": "^8.31.0", 47 | "jasmine-core": "~4.5.0", 48 | "karma": "6.4.1", 49 | "karma-chrome-launcher": "~3.1.0", 50 | "karma-coverage-istanbul-reporter": "3.0.3", 51 | "karma-jasmine": "~4.0.0", 52 | "karma-jasmine-html-reporter": "^1.5.0", 53 | "ng-packagr": "15.1.1", 54 | "rxjs": "6.6.0", 55 | "ts-node": "8.10.2", 56 | "tslib": "^2.0.0", 57 | "typescript": "4.9.4", 58 | "zone.js": "~0.11.4" 59 | }, 60 | "repository": "scttcper/ngx-rightclick", 61 | "release": { 62 | "branch": "master" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

ngx-rightclick

6 |

Context Menu Service for Angular

7 | 8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |

Examples:

17 | 18 | 29 |
30 |
31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styles: [ 7 | ` 8 | .selected { 9 | background-color: blue; 10 | } 11 | `, 12 | ], 13 | }) 14 | export class AppComponent { 15 | items = [ 16 | { name: 'swag', selected: false }, 17 | { name: 'bonus', selected: false }, 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { GhButtonModule } from '@ctrl/ngx-github-buttons'; 5 | import { RouterModule, Routes } from '@angular/router'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { ContextMenuModule } from '../lib/context-menu.module'; 9 | import { SimpleComponent } from './simple/simple.component'; 10 | import { SimpleMenuComponent } from './simple/simple-menu.component'; 11 | import { AnimatedMenuComponent } from './simple/animated-menu.component'; 12 | import { MultipleMenuComponent } from './multiple/multiple-menu.component'; 13 | import { MultipleComponent } from './multiple/multiple.component'; 14 | import { SubmenuComponent } from './submenu/submenu.component'; 15 | import { SubmenuMenuComponent } from './submenu/submenu-menu.component'; 16 | import { SubSubmenuMenuComponent } from './submenu/subsubmenu-menu.component'; 17 | 18 | const routes: Routes = [ 19 | { path: 'simple', component: SimpleComponent }, 20 | { path: 'multiple', component: MultipleComponent }, 21 | { path: 'submenu', component: SubmenuComponent }, 22 | { path: '**', redirectTo: 'simple' }, 23 | ]; 24 | 25 | @NgModule({ 26 | declarations: [ 27 | AppComponent, 28 | SimpleComponent, 29 | SimpleMenuComponent, 30 | AnimatedMenuComponent, 31 | MultipleMenuComponent, 32 | MultipleComponent, 33 | SubmenuComponent, 34 | SubmenuMenuComponent, 35 | SubSubmenuMenuComponent, 36 | ], 37 | imports: [ 38 | BrowserModule, 39 | ContextMenuModule, 40 | BrowserAnimationsModule, 41 | GhButtonModule, 42 | RouterModule.forRoot(routes, { useHash: true }), 43 | ], 44 | bootstrap: [AppComponent] 45 | }) 46 | export class AppModule {} 47 | -------------------------------------------------------------------------------- /src/app/multiple/multiple-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | } from '@angular/animations'; 8 | import { Component } from '@angular/core'; 9 | 10 | import { ContextMenuService } from '../../lib/context-menu.service'; 11 | import { MenuPackage } from '../../lib/context-menu-injector'; 12 | import { MenuComponent } from '../../lib/menu.component'; 13 | 14 | @Component({ 15 | selector: 'animated-menu', 16 | template: ` 17 | 28 | `, 29 | animations: [ 30 | trigger('menu', [ 31 | state('enter', style({ opacity: 1 })), 32 | state('exit, void', style({ opacity: 0 })), 33 | transition('* => *', animate(100)), 34 | ]), 35 | ], 36 | }) 37 | export class MultipleMenuComponent extends MenuComponent { 38 | item: any; 39 | constructor( 40 | public menuPackage: MenuPackage, 41 | public contextMenuService: ContextMenuService, 42 | ) { 43 | super(menuPackage, contextMenuService); 44 | this.item = menuPackage.context; 45 | } 46 | 47 | add() { 48 | this.item.count += 1; 49 | // tell the menu to close 50 | this.contextMenuService.closeAll(`added one ${this.item.name}`); 51 | } 52 | remove() { 53 | let msg = `${this.item.name} cannot be removed`; 54 | if (this.item.count > 0) { 55 | this.item.count -= 1; 56 | msg = `removed one ${this.item.name}`; 57 | } 58 | // tell the menu to close 59 | this.contextMenuService.closeAll(msg); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/multiple/multiple.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { MultipleMenuComponent } from './multiple-menu.component'; 4 | 5 | @Component({ 6 | selector: 'simple-component', 7 | template: ` 8 |
9 |
17 | {{ item.name }} ({{ item.count }}) 18 |
19 |
20 |
{{ msg }}
21 | `, 22 | }) 23 | export class MultipleComponent { 24 | messages: string[] = []; 25 | menu = MultipleMenuComponent; 26 | items = [ 27 | { name: 'banana', count: 0 }, 28 | { name: 'apple', count: 0 }, 29 | { name: 'orange', count: 0 }, 30 | ]; 31 | handleClose(msg: string) { 32 | this.messages.unshift(msg); 33 | this.messages.splice(10, this.messages.length); 34 | } 35 | 36 | onBeforeOpen($event: any) { 37 | const { menuContext, preventOpen } = $event; 38 | if (menuContext.name === 'orange') { 39 | preventOpen(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-rightclick", 3 | "private": true, 4 | "description_1": "This is a special package.json file that is not used by package managers.", 5 | "description_2": "It is used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size.", 6 | "description_3": "It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.", 7 | "description_4": "To learn more about this file see: https://angular.io/config/app-package-json.", 8 | "sideEffects": false 9 | } 10 | -------------------------------------------------------------------------------- /src/app/simple/animated-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | } from '@angular/animations'; 8 | import { Component } from '@angular/core'; 9 | 10 | import { ContextMenuService } from '../../lib/context-menu.service'; 11 | import { MenuPackage } from '../../lib/context-menu-injector'; 12 | import { MenuComponent } from '../../lib/menu.component'; 13 | 14 | @Component({ 15 | selector: 'animated-menu', 16 | template: ` 17 | 32 | `, 33 | animations: [ 34 | trigger('menu', [ 35 | state( 36 | 'enter', 37 | style({ opacity: 1, marginTop: '0px', visibility: 'visible' }), 38 | ), 39 | state('exit, void', style({ opacity: 0, marginTop: '-15px' })), 40 | transition('* => *', animate('120ms ease-in')), 41 | ]), 42 | ], 43 | }) 44 | export class AnimatedMenuComponent extends MenuComponent { 45 | constructor( 46 | public menuPackage: MenuPackage, 47 | public contextMenuService: ContextMenuService, 48 | ) { 49 | super(menuPackage, contextMenuService); 50 | } 51 | 52 | handleClick(msg: string) { 53 | // tell the menu to close 54 | this.contextMenuService.closeAll(msg); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/simple/simple-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | } from '@angular/animations'; 8 | import { Component } from '@angular/core'; 9 | 10 | import { ContextMenuService } from '../../lib/context-menu.service'; 11 | import { MenuPackage } from '../../lib/context-menu-injector'; 12 | import { MenuComponent } from '../../lib/menu.component'; 13 | 14 | @Component({ 15 | selector: 'simple-menu', 16 | template: ` 17 | 32 | `, 33 | animations: [ 34 | trigger('menu', [ 35 | state('enter', style({ opacity: 1 })), 36 | state('exit, void', style({ opacity: 0 })), 37 | transition('* => *', animate(0)), 38 | ]), 39 | ], 40 | }) 41 | export class SimpleMenuComponent extends MenuComponent { 42 | // this module does not have animations, set lazy false 43 | lazy = false; 44 | 45 | constructor( 46 | public menuPackage: MenuPackage, 47 | public contextMenuService: ContextMenuService, 48 | ) { 49 | super(menuPackage, contextMenuService); 50 | } 51 | 52 | handleClick(msg: string) { 53 | // tell the menu to close 54 | this.contextMenuService.closeAll(msg); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/simple/simple.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { SimpleMenuComponent } from './simple-menu.component'; 3 | import { AnimatedMenuComponent } from './animated-menu.component'; 4 | 5 | @Component({ 6 | selector: 'simple-component', 7 | template: ` 8 |

Right click to see menu

9 |
10 |
13 | Basic Menu 14 |
15 |
18 | Animated Menu 19 |
20 |
21 |

{{ message }}

22 | `, 23 | }) 24 | 25 | export class SimpleComponent { 26 | menu = SimpleMenuComponent; 27 | animatedMenu = AnimatedMenuComponent; 28 | messages: string[] = []; 29 | 30 | handleClose(msg: string) { 31 | this.messages.unshift(msg); 32 | this.messages.splice(10, this.messages.length); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/submenu/submenu-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | } from '@angular/animations'; 8 | import { Component } from '@angular/core'; 9 | 10 | import { ContextMenuService } from '../../lib/context-menu.service'; 11 | import { MenuPackage } from '../../lib/context-menu-injector'; 12 | import { MenuComponent } from '../../lib/menu.component'; 13 | import { SubSubmenuMenuComponent } from './subsubmenu-menu.component'; 14 | 15 | @Component({ 16 | selector: 'animated-menu', 17 | template: ` 18 | 39 | `, 40 | animations: [ 41 | trigger('menu', [ 42 | state('enter', style({ opacity: 1 })), 43 | state('exit, void', style({ opacity: 0 })), 44 | transition('* => *', animate(100)), 45 | ]), 46 | ], 47 | }) 48 | export class SubmenuMenuComponent extends MenuComponent { 49 | submenu = SubSubmenuMenuComponent; 50 | constructor( 51 | public menuPackage: MenuPackage, 52 | public contextMenuService: ContextMenuService, 53 | ) { 54 | super(menuPackage, contextMenuService); 55 | } 56 | 57 | handleClick(msg: string) { 58 | // tell the menu to close 59 | this.contextMenuService.closeAll(msg); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/submenu/submenu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { SubmenuMenuComponent } from './submenu-menu.component'; 4 | 5 | @Component({ 6 | selector: 'simple-component', 7 | template: ` 8 |
9 |
14 | Right Click 15 |
16 |
17 |

{{ message }}

18 | `, 19 | }) 20 | export class SubmenuComponent { 21 | menu = SubmenuMenuComponent; 22 | messages: string[] = []; 23 | 24 | handleClose(msg: string) { 25 | this.messages.unshift(msg); 26 | this.messages.splice(10, this.messages.length); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/submenu/subsubmenu-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | state, 4 | style, 5 | transition, 6 | trigger, 7 | } from '@angular/animations'; 8 | import { Component } from '@angular/core'; 9 | 10 | import { ContextMenuService } from '../../lib/context-menu.service'; 11 | import { MenuPackage } from '../../lib/context-menu-injector'; 12 | import { MenuComponent } from '../../lib/menu.component'; 13 | 14 | @Component({ 15 | selector: 'animated-menu', 16 | template: ` 17 | 30 | `, 31 | animations: [ 32 | trigger('menu', [ 33 | state('enter', style({ opacity: 1 })), 34 | state('exit, void', style({ opacity: 0 })), 35 | transition('* => *', animate(100)), 36 | ]), 37 | ], 38 | }) 39 | export class SubSubmenuMenuComponent extends MenuComponent { 40 | constructor( 41 | public menuPackage: MenuPackage, 42 | public contextMenuService: ContextMenuService, 43 | ) { 44 | super(menuPackage, contextMenuService); 45 | } 46 | 47 | handleClick(action: string) { 48 | // tell the menu to close 49 | this.contextMenuService.closeAll(action); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-rightclick/ed9e2d647730c8f84476b710499f924a9fae79c1/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-rightclick/ed9e2d647730c8f84476b710499f924a9fae79c1/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ngx-rightclick 8 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/context-menu-injector.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '@angular/core'; 2 | 3 | import { ActiveContextMenuSub } from './context-menu.service'; 4 | 5 | export class MenuPackage { 6 | constructor(public menu: ActiveContextMenuSub, public context: any) {} 7 | } 8 | 9 | export class MenuInjector implements Injector { 10 | menuContext: MenuPackage; 11 | constructor( 12 | private activeContextMenu: ActiveContextMenuSub, 13 | private parentInjector: Injector, 14 | private context: any, 15 | ) { 16 | this.menuContext = new MenuPackage(activeContextMenu, context); 17 | } 18 | 19 | get(token: any, notFoundValue?: T, flags?: any): T | MenuPackage { 20 | if (token === MenuPackage) { 21 | return this.menuContext; 22 | } 23 | return this.parentInjector.get(token, notFoundValue, flags); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/context-menu-trigger.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | HostListener, 4 | Input, 5 | Output, 6 | EventEmitter, 7 | OnDestroy, 8 | OnInit, 9 | } from '@angular/core'; 10 | import { Subscription } from 'rxjs'; 11 | 12 | import { ContextMenuService, ActiveContextMenu } from './context-menu.service'; 13 | 14 | @Directive({ 15 | selector: '[contextMenuTrigger]', 16 | exportAs: 'contextMenuTrigger', 17 | }) 18 | export class ContextMenuTriggerDirective implements OnDestroy, OnInit { 19 | @Input() contextMenuTrigger: any; 20 | @Input() menuContext: any; 21 | @Input() holdToDisplay = 1000; 22 | @Output() menuAction = new EventEmitter(); 23 | @Output() menuClose = new EventEmitter(); 24 | @Output() beforeOpen = new EventEmitter(); 25 | 26 | menu?: ActiveContextMenu; 27 | visible = false; 28 | private mouseDownTimeoutId: any; 29 | private sub!: Subscription; 30 | 31 | @HostListener('contextmenu', ['$event']) 32 | handleMenu($event: MouseEvent) { 33 | let preventOpen = false; 34 | this.beforeOpen.emit({ 35 | menuContext: this.menuContext, 36 | event: $event, 37 | preventOpen: () => { 38 | preventOpen = true; 39 | }, 40 | }); 41 | 42 | if (preventOpen) { 43 | return; 44 | } 45 | 46 | $event.preventDefault(); 47 | this.menu = this.contextMenuService.show( 48 | $event, 49 | this.contextMenuTrigger, 50 | this.menuContext, 51 | this.menuClose, 52 | this.menuAction, 53 | ); 54 | this.visible = true; 55 | } 56 | 57 | @HostListener('touchstart', ['$event']) 58 | handleMouseDown($event: any) { 59 | if (this.holdToDisplay >= 0) { 60 | $event.stopPropagation(); 61 | $event.clientY = $event.touches[0].clientY; 62 | $event.clientX = $event.touches[0].clientX; 63 | 64 | this.mouseDownTimeoutId = setTimeout( 65 | () => this.handleMenu($event), 66 | this.holdToDisplay, 67 | ); 68 | } 69 | } 70 | 71 | @HostListener('touchend') 72 | handleMouseUp() { 73 | clearTimeout(this.mouseDownTimeoutId); 74 | } 75 | 76 | constructor(private contextMenuService: ContextMenuService) {} 77 | 78 | ngOnInit() { 79 | this.sub = this.menuClose.subscribe(() => (this.visible = false)); 80 | } 81 | 82 | ngOnDestroy() { 83 | this.sub.unsubscribe(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/context-menu.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { OverlayModule } from '@angular/cdk/overlay'; 3 | import { PortalModule } from '@angular/cdk/portal'; 4 | 5 | import { ContextMenuTriggerDirective } from './context-menu-trigger.directive'; 6 | import { ContextSubmenuTriggerDirective } from './context-submenu-trigger.directive'; 7 | import { MenuComponent } from './menu.component'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | ContextMenuTriggerDirective, 12 | ContextSubmenuTriggerDirective, 13 | MenuComponent, 14 | ], 15 | exports: [ContextMenuTriggerDirective, ContextSubmenuTriggerDirective], 16 | imports: [PortalModule, OverlayModule], 17 | }) 18 | export class ContextMenuModule {} 19 | -------------------------------------------------------------------------------- /src/lib/context-menu.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Overlay, 3 | OverlayRef, 4 | ScrollStrategyOptions, 5 | } from '@angular/cdk/overlay'; 6 | import { ComponentPortal } from '@angular/cdk/portal'; 7 | import { ElementRef, Injectable, Injector, EventEmitter } from '@angular/core'; 8 | 9 | import { BehaviorSubject } from 'rxjs'; 10 | import { filter, take } from 'rxjs/operators'; 11 | 12 | import { MenuInjector } from './context-menu-injector'; 13 | 14 | export interface ActiveContextMenuSub { 15 | id: number; 16 | isTriggerHovered: BehaviorSubject; 17 | isMenuHovered: BehaviorSubject; 18 | submenu: boolean; 19 | } 20 | export interface ActiveContextMenu extends ActiveContextMenuSub { 21 | overlayRef: OverlayRef; 22 | component: any; 23 | menuClose: EventEmitter; 24 | menuAction: EventEmitter; 25 | } 26 | 27 | @Injectable({ providedIn: 'root' }) 28 | export class ContextMenuService { 29 | menus: ActiveContextMenu[] = []; 30 | id = 0; 31 | 32 | constructor( 33 | private overlay: Overlay, 34 | private scrollStrategy: ScrollStrategyOptions, 35 | private injector: Injector, 36 | ) {} 37 | 38 | /** 39 | * 40 | * @param $event triggering event 41 | * @param menuComponent the component to be shown 42 | * @param submenu is a menu within a menu 43 | * @param level if submenu, what level 44 | */ 45 | show( 46 | $event: MouseEvent, 47 | menuComponent: any, 48 | context: any, 49 | menuClose: EventEmitter, 50 | menuAction: EventEmitter, 51 | submenu = false, 52 | level?: number, 53 | ): ActiveContextMenu { 54 | let target: any; 55 | if (!submenu) { 56 | this.closeAll(); 57 | target = { 58 | getBoundingClientRect: (): DOMRect => new DOMRect($event.clientX, $event.clientY, 0, 0) 59 | }; 60 | } else { 61 | // close other submenus 62 | this.closeAll(undefined, level); 63 | target = $event.target; 64 | } 65 | const el = new ElementRef(target); 66 | const positionStrategy = this.overlay 67 | .position() 68 | .flexibleConnectedTo(el) 69 | .withFlexibleDimensions(false); 70 | 71 | if (!submenu) { 72 | positionStrategy.withPositions([ 73 | { 74 | originX: 'start', 75 | originY: 'bottom', 76 | overlayX: 'start', 77 | overlayY: 'top', 78 | }, 79 | { 80 | originX: 'start', 81 | originY: 'top', 82 | overlayX: 'start', 83 | overlayY: 'bottom', 84 | }, 85 | { 86 | originX: 'end', 87 | originY: 'top', 88 | overlayX: 'start', 89 | overlayY: 'top', 90 | }, 91 | { 92 | originX: 'start', 93 | originY: 'top', 94 | overlayX: 'end', 95 | overlayY: 'top', 96 | }, 97 | { 98 | originX: 'end', 99 | originY: 'center', 100 | overlayX: 'start', 101 | overlayY: 'center', 102 | }, 103 | { 104 | originX: 'start', 105 | originY: 'center', 106 | overlayX: 'end', 107 | overlayY: 'center', 108 | }, 109 | ]); 110 | } else { 111 | positionStrategy.withPositions([ 112 | { 113 | originX: 'end', 114 | originY: 'top', 115 | overlayX: 'start', 116 | overlayY: 'top', 117 | }, 118 | { 119 | originX: 'start', 120 | originY: 'top', 121 | overlayX: 'end', 122 | overlayY: 'top', 123 | }, 124 | { 125 | originX: 'end', 126 | originY: 'bottom', 127 | overlayX: 'start', 128 | overlayY: 'bottom', 129 | }, 130 | { 131 | originX: 'start', 132 | originY: 'bottom', 133 | overlayX: 'end', 134 | overlayY: 'bottom', 135 | }, 136 | ]); 137 | } 138 | const t: ActiveContextMenuSub = { 139 | submenu, 140 | id: this.id++, 141 | isMenuHovered: new BehaviorSubject(false), 142 | isTriggerHovered: new BehaviorSubject(false), 143 | }; 144 | const menuInjector = new MenuInjector(t, this.injector, context); 145 | const componentPortal = new ComponentPortal( 146 | menuComponent, 147 | undefined, 148 | menuInjector, 149 | ); 150 | const overlayRef = this.overlay.create({ 151 | positionStrategy, 152 | panelClass: 'ngx-contextmenu', 153 | scrollStrategy: this.scrollStrategy.close(), 154 | }); 155 | const component = overlayRef.attach(componentPortal); 156 | const res = { overlayRef, component, ...t, menuClose, menuAction }; 157 | this.menus.push(res); 158 | return res; 159 | } 160 | getCurrentLevel() { 161 | return this.menus.length; 162 | } 163 | closeAll(context?: any, idx = 0) { 164 | for (let index = idx; index < this.menus.length; index++) { 165 | const menu = this.menus[index]; 166 | this.destroyMenu(menu, context); 167 | } 168 | this.menus.splice(idx, this.menus.length); 169 | } 170 | destroyMenu(menu: ActiveContextMenu, context?: any) { 171 | menu.component.instance._state = 'exit'; 172 | if (menu.component.instance.lazy) { 173 | menu.component.instance._animationDone 174 | .pipe( 175 | filter((event: any) => event.toState === 'exit'), 176 | take(1), 177 | ) 178 | .subscribe(() => { 179 | menu.overlayRef.detach(); 180 | menu.overlayRef.dispose(); 181 | }); 182 | } else { 183 | menu.overlayRef.detach(); 184 | menu.overlayRef.dispose(); 185 | } 186 | if (context) { 187 | menu.menuAction.next(context); 188 | } 189 | menu.menuClose.next(); 190 | } 191 | close(menu: ActiveContextMenu, menuIndex: number, context?: any) { 192 | this.destroyMenu(menu, context); 193 | this.menus.splice(menuIndex, 1); 194 | } 195 | checkOutsideClick($event: MouseEvent) { 196 | for (const m of this.menus) { 197 | const clickedInside = m.component.location.nativeElement.contains( 198 | $event.target, 199 | ); 200 | if (clickedInside) { 201 | $event.preventDefault(); 202 | $event.stopPropagation(); 203 | return; 204 | } 205 | } 206 | this.closeAll(); 207 | } 208 | closeSubMenu(id: number): void { 209 | const menuIndex = this.menus.findIndex(n => n.id === id); 210 | if (menuIndex === -1 || menuIndex !== this.menus.length - 1) { 211 | return; 212 | } 213 | // make sure we can close the current menu 214 | const menu = this.menus[menuIndex]; 215 | if (menu.isMenuHovered.getValue() || menu.isTriggerHovered.getValue()) { 216 | return; 217 | } 218 | // close all menus up if possible 219 | for (let index = this.menus.length - 1; index >= 1; index--) { 220 | const m = this.menus[index]; 221 | if (!m.isMenuHovered.getValue() && !m.isTriggerHovered.getValue()) { 222 | this.close(m, index); 223 | } else { 224 | return; 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/lib/context-submenu-trigger.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | HostListener, 4 | Input, 5 | OnDestroy, 6 | Output, 7 | EventEmitter, 8 | } from '@angular/core'; 9 | 10 | import { ActiveContextMenu, ContextMenuService } from './context-menu.service'; 11 | 12 | @Directive({ selector: '[contextSubmenuTrigger]' }) 13 | export class ContextSubmenuTriggerDirective implements OnDestroy { 14 | @Input() hoverDelay = 500; 15 | @Input() openDelay = 200; 16 | @Input() contextSubmenuTrigger?: any; 17 | @Input() menuContext?: any; 18 | @Output() menuAction = new EventEmitter(); 19 | @Output() menuClose = new EventEmitter(); 20 | menu?: ActiveContextMenu; 21 | opentimer: any; 22 | closetimer: any; 23 | visible = false; 24 | level = 1; 25 | 26 | constructor(private contextMenuService: ContextMenuService) { 27 | // get current level 28 | setTimeout(() => (this.level = this.contextMenuService.getCurrentLevel())); 29 | } 30 | 31 | @HostListener('click', ['$event']) 32 | handleSubMenuClick($event: MouseEvent) { 33 | $event.preventDefault(); 34 | $event.stopPropagation(); 35 | clearTimeout(this.opentimer); 36 | clearTimeout(this.closetimer); 37 | this.menu = this.contextMenuService.show( 38 | $event, 39 | this.contextSubmenuTrigger, 40 | this.menuContext, 41 | this.menuClose, 42 | this.menuAction, 43 | true, 44 | this.level, 45 | ); 46 | this.visible = true; 47 | } 48 | 49 | @HostListener('mouseover', ['$event']) 50 | handleSubMenuEnter($event: MouseEvent) { 51 | if (this.menu) { 52 | this.menu.isTriggerHovered.next(true); 53 | } 54 | clearTimeout(this.closetimer); 55 | this.opentimer = setTimeout(() => { 56 | this.menu = this.contextMenuService.show( 57 | $event, 58 | this.contextSubmenuTrigger, 59 | this.menuContext, 60 | this.menuClose, 61 | this.menuAction, 62 | true, 63 | this.level, 64 | ); 65 | this.visible = true; 66 | this.opentimer = null; 67 | }, this.openDelay); 68 | } 69 | 70 | /** 71 | * submenu hides after cursor has exited for a period of time 72 | */ 73 | @HostListener('mouseout') 74 | handleSubMenuExit() { 75 | clearTimeout(this.opentimer); 76 | if (this.menu) { 77 | this.menu.isTriggerHovered.next(false); 78 | } 79 | this.closetimer = setTimeout(() => { 80 | if (this.menu) { 81 | this.menu.isTriggerHovered.next(false); 82 | this.contextMenuService.closeSubMenu(this.menu.id); 83 | this.menu = undefined; 84 | } 85 | this.visible = false; 86 | }, this.hoverDelay); 87 | } 88 | 89 | /** 90 | * if overwritten make sure to clear timeouts 91 | */ 92 | ngOnDestroy() { 93 | clearTimeout(this.opentimer); 94 | clearTimeout(this.closetimer); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostListener } from '@angular/core'; 2 | import { AnimationEvent } from '@angular/animations'; 3 | 4 | import { ContextMenuService } from './context-menu.service'; 5 | import { MenuPackage } from './context-menu-injector'; 6 | import { Subject } from 'rxjs'; 7 | 8 | @Component({ 9 | selector: 'app-menu', 10 | template: ``, 11 | }) 12 | export class MenuComponent { 13 | /** State of the dialog animation. */ 14 | _state: 'void' | 'enter' | 'exit' = 'enter'; 15 | _animationDone = new Subject(); 16 | /** set lazy to False if you do not have animations */ 17 | lazy = true; 18 | closetimer: any; 19 | 20 | constructor( 21 | public menuPackage: MenuPackage, 22 | public contextMenuService: ContextMenuService, 23 | ) {} 24 | 25 | @HostListener('mouseover') 26 | handleMouseover() { 27 | if (!this.menuPackage.menu.submenu) { 28 | return; 29 | } 30 | this.menuPackage.menu.isMenuHovered.next(true); 31 | clearTimeout(this.closetimer); 32 | } 33 | 34 | @HostListener('mouseleave') 35 | handleMouseleave() { 36 | if (!this.menuPackage.menu.submenu) { 37 | return; 38 | } 39 | this.menuPackage.menu.isMenuHovered.next(false); 40 | this.closetimer = setTimeout(() => { 41 | this.contextMenuService.closeSubMenu(this.menuPackage.menu.id); 42 | }, 500); 43 | } 44 | 45 | @HostListener('document:click', ['$event']) 46 | handleWindowClick($event: MouseEvent) { 47 | this.contextMenuService.checkOutsideClick($event); 48 | } 49 | 50 | /** Callback that is invoked when the menu animation completes. */ 51 | @HostListener('@menu.done', ['$event']) 52 | _onAnimationDone(event: AnimationEvent) { 53 | this._animationDone.next(event); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "name": "@ctrl/ngx-rightclick", 4 | "version": "0.0.0-placeholder", 5 | "description": "GitHub Buttons for Angular. Star, Like, Follow and more", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "peerDependencies": { 10 | "@angular/core": ">=10.0.0-0", 11 | "@angular/cdk": ">=10.0.0-0" 12 | }, 13 | "author": "Scott Cooper (https://github.com/scttcper)", 14 | "repository": "scttcper/ngx-rightclick", 15 | "homepage": "https://ngx-rightclick.vercel.app", 16 | "license": "MIT", 17 | "keywords": ["ngx", "contextmenu", "rightclick", "menu", "context", "angular"], 18 | "ngPackage": { 19 | "lib": { 20 | "entryFile": "public_api.ts" 21 | }, 22 | "dest": "../../dist" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './context-menu.module'; 2 | export * from './context-menu-injector'; 3 | export * from './menu.component'; 4 | export * from './context-submenu-trigger.directive'; 5 | export * from './context-menu-trigger.directive'; 6 | export * from './context-menu.service'; 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/functions'; 2 | 3 | // Global settings 4 | 5 | $enable-caret: true; 6 | $enable-rounded: true; 7 | $enable-shadows: true; 8 | $enable-gradients: true; 9 | $enable-transitions: true; 10 | $enable-grid-classes: true; 11 | $enable-print-styles: true; 12 | 13 | $blue: #5e72e4 !default; 14 | $indigo: #5603ad !default; 15 | $purple: #8965e0 !default; 16 | $pink: #f3a4b5 !default; 17 | $red: #f5365c !default; 18 | $orange: #fb6340 !default; 19 | $yellow: #ffd600 !default; 20 | $green: #2dce89 !default; 21 | $teal: #11cdef !default; 22 | $cyan: #2bffc6 !default; 23 | 24 | $default: $blue; 25 | $primary: $indigo; 26 | $secondary: #fff !default; 27 | $success: $green !default; 28 | $info: $pink !default; 29 | $warning: $orange !default; 30 | $danger: $red !default; 31 | 32 | @import 'bootstrap/scss/variables'; 33 | @import 'bootstrap/scss/mixins'; 34 | @import 'bootstrap/scss/root'; 35 | @import 'bootstrap/scss/reboot'; 36 | @import 'bootstrap/scss/type'; 37 | // @import "~bootstrap/scss/images"; 38 | // @import "~bootstrap/scss/code"; 39 | @import 'bootstrap/scss/grid'; 40 | // @import "~bootstrap/scss/tables"; 41 | // @import "~bootstrap/scss/forms"; 42 | // @import "~bootstrap/scss/buttons"; 43 | // @import "~bootstrap/scss/transitions"; 44 | @import 'bootstrap/scss/dropdown'; 45 | // @import "~bootstrap/scss/button-group"; 46 | // @import "~bootstrap/scss/input-group"; 47 | // @import "~bootstrap/scss/custom-forms"; 48 | @import 'bootstrap/scss/nav'; 49 | @import 'bootstrap/scss/navbar'; 50 | // @import "~bootstrap/scss/card"; 51 | // @import "~bootstrap/scss/breadcrumb"; 52 | // @import "~bootstrap/scss/pagination"; 53 | // @import "~bootstrap/scss/badge"; 54 | @import 'bootstrap/scss/jumbotron'; 55 | // @import "~bootstrap/scss/alert"; 56 | // @import "~bootstrap/scss/progress"; 57 | // @import "~bootstrap/scss/media"; 58 | // @import "~bootstrap/scss/list-group"; 59 | // @import "~bootstrap/scss/close"; 60 | // @import "~bootstrap/scss/modal"; 61 | // @import "~bootstrap/scss/tooltip"; 62 | // @import "~bootstrap/scss/popover"; 63 | // @import "~bootstrap/scss/carousel"; 64 | @import 'bootstrap/scss/utilities'; 65 | @import 'bootstrap/scss/print'; 66 | 67 | body { 68 | -webkit-font-smoothing: antialiased; 69 | text-rendering: optimizeLegibility; 70 | } 71 | 72 | @include media-breakpoint-down(md) { 73 | h1 { 74 | font-size: 2rem; 75 | } 76 | } 77 | .jumbotron { 78 | background: linear-gradient(150deg, #5603ad 15%, #8f6ed5 70%, rgb(195, 107, 253) 94%); 79 | } 80 | 81 | .cdk-overlay-container { 82 | position: fixed; 83 | z-index: 1000; 84 | pointer-events: none; 85 | top: 0; 86 | left: 0; 87 | width: 100%; 88 | height: 100%; 89 | } 90 | .ngx-contextmenu.cdk-overlay-pane { 91 | position: absolute; 92 | pointer-events: auto; 93 | box-sizing: border-box; 94 | } 95 | 96 | .target { 97 | user-select: none; 98 | } 99 | 100 | .dropdown-submenu { 101 | position: relative; 102 | } 103 | 104 | .dropdown-submenu { 105 | position: relative; 106 | } 107 | 108 | .dropdown-submenu button::after { 109 | transform: rotate(-90deg); 110 | position: absolute; 111 | right: 6px; 112 | top: 0.8em; 113 | } 114 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | /* tslint:disable:ordered-imports */ 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "target": "ES2022", 18 | "module": "es2020", 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ], 23 | "useDefineForClassFields": false 24 | }, 25 | "angularCompilerOptions": { 26 | "strictInjectionParameters": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------