├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── dist └── .gitignore ├── example ├── app.js ├── data.js ├── ng-v1.2x.html ├── ng-v1.3x-two-menus.html ├── ng-v1.3x.html └── ng-v1.4x.html ├── karma.conf.js ├── package.json ├── screen.png ├── spec ├── .jshintrc └── contextmenu.spec.js ├── src ├── directive │ ├── container.js │ ├── contextmenu.js │ └── item.js ├── index.js └── service │ └── service.js └── style ├── contextmenu.less └── variables.less /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireOperatorBeforeLineBreak": true, 12 | "maximumLineLength": { 13 | "value": 100, 14 | "allowComments": true, 15 | "allowRegex": true 16 | }, 17 | "validateIndentation": 2, 18 | "validateQuoteMarks": "'", 19 | 20 | "disallowMultipleLineStrings": true, 21 | "disallowMixedSpacesAndTabs": true, 22 | "disallowTrailingWhitespace": true, 23 | "disallowSpaceAfterPrefixUnaryOperators": true, 24 | "disallowMultipleVarDecl": "exceptUndefined", 25 | "disallowKeywordsOnNewLine": ["else"], 26 | 27 | "requireSpaceAfterKeywords": [ 28 | "if", 29 | "else", 30 | "for", 31 | "while", 32 | "do", 33 | "switch", 34 | "return", 35 | "try", 36 | "catch" 37 | ], 38 | "requireSpaceBeforeBinaryOperators": [ 39 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 40 | "&=", "|=", "^=", "+=", 41 | 42 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 43 | "|", "^", "&&", "||", "===", "==", ">=", 44 | "<=", "<", ">", "!=", "!==" 45 | ], 46 | "requireSpaceAfterBinaryOperators": true, 47 | "requireSpacesInConditionalExpression": true, 48 | "requireSpaceBeforeBlockStatements": true, 49 | "requireSpacesInForStatement": true, 50 | "requireLineFeedAtFileEnd": true, 51 | "requireSpacesInFunctionExpression": { 52 | "beforeOpeningCurlyBrace": true 53 | }, 54 | "disallowSpacesInAnonymousFunctionExpression": { 55 | "beforeOpeningRoundBrace": true 56 | }, 57 | "disallowSpacesInsideObjectBrackets": "all", 58 | "disallowSpacesInsideArrayBrackets": "all", 59 | "disallowSpacesInsideParentheses": true, 60 | 61 | "disallowMultipleLineBreaks": true, 62 | "disallowNewlineBeforeBlockStatements": true 63 | } 64 | 65 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "indent": true, 4 | "quotmark": "single", 5 | "maxparams": 4, 6 | "maxdepth": 3, 7 | "maxstatements": 20, 8 | "maxlen": 120, 9 | "curly": true, 10 | "indent": true, 11 | 12 | "undef": true, 13 | "unused": true, 14 | "globals": { 15 | }, 16 | "predef": [ 17 | "window", 18 | "define", 19 | "document", 20 | "console", 21 | "angular" 22 | ], 23 | "node": true, 24 | "globalstrict": true 25 | } 26 | 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | spec/ 2 | src/ 3 | style/ 4 | 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | message=chore(release): %s 2 | save-exact=true 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | language: node_js 8 | node_js: 9 | - '4' 10 | - '6' 11 | before_install: 12 | before_script: 13 | - npm prune 14 | script: 15 | - npm run install-scope 16 | - npm run test-travis 17 | after_success: 18 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' 19 | - python travis_after_all.py 20 | - 'export $(cat .to_export_back) &> /dev/null' 21 | - npm run semantic-release 22 | branches: 23 | except: 24 | - "/^v\\d+\\.\\d+\\.\\d+$/" 25 | env: 26 | - TEST_SCOPE=1.2 27 | - TEST_SCOPE=1.3 28 | - TEST_SCOPE=1.4 29 | - TEST_SCOPE=1.5 30 | - TEST_SCOPE=1.6 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dennis 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-contextmenu 2 | 3 | [![npm](https://nodei.co/npm/angular-contextmenu.png?downloads=true&stars=true)](https://nodei.co/npm/angular-contextmenu/) 4 | 5 | [![Build Status](https://secure.travis-ci.org/ds82/angular-contextmenu.svg)](http://travis-ci.org/ds82/angular-contextmenu) [![npm](https://img.shields.io/npm/v/angular-contextmenu.svg)]() [![Greenkeeper badge](https://badges.greenkeeper.io/ds82/angular-contextmenu.svg)](https://greenkeeper.io/) 6 | 7 | [demo]: http://ds82.github.io/angular-contextmenu/ 8 | [jsbin]: http://jsbin.com/hodul/13/edit?html,js,output 9 | [jquery]: http://jquery.com 10 | [bootstrap]: http://getbootstrap.com 11 | [angular]: http://angularjs.org 12 | [download]: https://github.com/ds82/angular-contextmenu/releases 13 | 14 | > An [angular] module that utilizes [bootstrap]'s dropdown as contextmenu 15 | 16 | With this simple angular module you can use bootstrap dropdown menus as 17 | contextmenus on various elements (e.g. tables). 18 | 19 | Try the **[DEMO][demo]** or have a look at [jsbin]. 20 | 21 | ![](screen.png) 22 | 23 | ## Why? 24 | 25 | Yes, there are already a few angular modules which add contextmenus similar to this. *But* competition isn't a bad thing right? 26 | 27 | ## Todos 28 | 29 | * remove jquery & bootstrap dependency 30 | * add more tests 31 | * add travis integration 32 | * document code 33 | 34 | ## How to use 35 | 36 | Install via npm.. 37 | ``` 38 | npm install angular-contextmenu 39 | ``` 40 | 41 | Install via bower.. 42 | ``` 43 | bower install angular-contextmenu 44 | ``` 45 | 46 | or [download] from github. 47 | 48 | * get the contextmenu.(js|css) files from dist/ 49 | * make them available on your page 50 | * make angular load the module: 51 | ```js 52 | var app = angular.module('app', [ 53 | 'ngResource', 54 | 'ngRoute', 55 | 'io.dennis.contextmenu' 56 | ]); 57 | ``` 58 | * define the contextmenu in your template 59 | ```html 60 | 61 | 82 | ``` 83 | * link it to your html element 84 | ```html 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 102 |
 UserDomains 
94 | 95 | {{row.email}}{{row.domains.join(', ')}} 99 | 100 |
103 | ``` 104 | 105 | ## example 106 | 107 | You can find an example with angular v1.2 and angular v1.3 in the example folder. 108 | 109 | ## dev 110 | 111 | You can use npm to build/bundle the module: 112 | 113 | ``` 114 | npm install 115 | npm run build 116 | ``` 117 | 118 | If you want a watcher and auto-rebuild you can use nodemon for this: 119 | 120 | ``` 121 | npm install -g nodaemon 122 | nodemon --ignore dist --exec "npm run build" 123 | ``` 124 | 125 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-contextmenu", 3 | "homepage": "https://github.com/ds82/angular-contextmenu.git", 4 | "description": "A contextmenu for angular & bootstrap apps", 5 | "main": [ 6 | "dist/contextmenu.js", 7 | "dist/style.css" 8 | ], 9 | "keywords": [ 10 | "ng", 11 | "angular", 12 | "twbs", 13 | "bootstrap", 14 | "contextmenu" 15 | ], 16 | "authors": [ 17 | "Dennis Sänger " 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "app/thirdparty", 25 | "test", 26 | "tests", 27 | "spec", 28 | "style" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | 'use strict'; 3 | 4 | angular.module('app', [ 5 | 'io.dennis.contextmenu' 6 | ]).controller('MainCtrl', Main); 7 | 8 | Main.$inject = []; 9 | function Main() { 10 | var vm = this; 11 | vm.DATA = window.DATA; 12 | vm.version = angular.version; 13 | vm.remove = remove; 14 | 15 | function remove(what) { 16 | window.alert('REMOVE ' + what); 17 | } 18 | } 19 | })(window); 20 | -------------------------------------------------------------------------------- /example/data.js: -------------------------------------------------------------------------------- 1 | window.DATA = [{"id":1,"first_name":"Nancy","last_name":"Mitchell","email":"nmitchell0@ucoz.com","country":"Poland","ip_address":"191.208.130.87"}, 2 | {"id":2,"first_name":"Arthur","last_name":"Castillo","email":"acastillo1@wufoo.com","country":"France","ip_address":"65.10.13.121"}, 3 | {"id":3,"first_name":"Alan","last_name":"Perry","email":"aperry2@shop-pro.jp","country":"Poland","ip_address":"100.159.84.212"}, 4 | {"id":4,"first_name":"Evelyn","last_name":"Cooper","email":"ecooper3@upenn.edu","country":"Brazil","ip_address":"189.164.152.197"}, 5 | {"id":5,"first_name":"Susan","last_name":"Morales","email":"smorales4@baidu.com","country":"United States","ip_address":"254.54.36.111"}, 6 | {"id":6,"first_name":"Kimberly","last_name":"Lawrence","email":"klawrence5@deviantart.com","country":"Nigeria","ip_address":"35.201.132.152"}, 7 | {"id":7,"first_name":"Jerry","last_name":"Martinez","email":"jmartinez6@msn.com","country":"Singapore","ip_address":"125.59.3.192"}, 8 | {"id":8,"first_name":"Harry","last_name":"Rogers","email":"hrogers7@behance.net","country":"Russia","ip_address":"149.177.152.206"}, 9 | {"id":9,"first_name":"Phyllis","last_name":"Dean","email":"pdean8@hao123.com","country":"Indonesia","ip_address":"198.13.47.155"}, 10 | {"id":10,"first_name":"Larry","last_name":"Parker","email":"lparker9@vkontakte.ru","country":"Greece","ip_address":"98.64.145.124"}, 11 | {"id":11,"first_name":"Kevin","last_name":"Jacobs","email":"kjacobsa@senate.gov","country":"Indonesia","ip_address":"41.12.169.33"}, 12 | {"id":12,"first_name":"Shawn","last_name":"Ramirez","email":"sramirezb@pagesperso-orange.fr","country":"Ukraine","ip_address":"13.225.43.176"}, 13 | {"id":13,"first_name":"William","last_name":"Woods","email":"wwoodsc@imgur.com","country":"China","ip_address":"224.31.92.0"}, 14 | {"id":14,"first_name":"Melissa","last_name":"Arnold","email":"marnoldd@blinklist.com","country":"Indonesia","ip_address":"215.12.164.101"}, 15 | {"id":15,"first_name":"Arthur","last_name":"Burns","email":"aburnse@cisco.com","country":"Sweden","ip_address":"184.119.48.17"}, 16 | {"id":16,"first_name":"Patrick","last_name":"Stevens","email":"pstevensf@blogspot.com","country":"Indonesia","ip_address":"121.140.46.227"}, 17 | {"id":17,"first_name":"Brian","last_name":"Larson","email":"blarsong@theglobeandmail.com","country":"Brazil","ip_address":"101.51.148.175"}, 18 | {"id":18,"first_name":"Joseph","last_name":"Holmes","email":"jholmesh@acquirethisname.com","country":"Brazil","ip_address":"14.226.198.152"}, 19 | {"id":19,"first_name":"Dennis","last_name":"Bryant","email":"dbryanti@reddit.com","country":"Maldives","ip_address":"252.37.184.145"}, 20 | {"id":20,"first_name":"Kelly","last_name":"Alvarez","email":"kalvarezj@pcworld.com","country":"Indonesia","ip_address":"97.8.125.30"}, 21 | {"id":21,"first_name":"Sara","last_name":"Bishop","email":"sbishopk@pen.io","country":"China","ip_address":"154.23.62.237"}, 22 | {"id":22,"first_name":"Jeremy","last_name":"Henry","email":"jhenryl@army.mil","country":"Turkmenistan","ip_address":"110.123.42.145"}, 23 | {"id":23,"first_name":"Deborah","last_name":"Gordon","email":"dgordonm@prlog.org","country":"Russia","ip_address":"84.246.247.83"}, 24 | {"id":24,"first_name":"Cheryl","last_name":"Barnes","email":"cbarnesn@wiley.com","country":"Argentina","ip_address":"32.159.93.224"}, 25 | {"id":25,"first_name":"James","last_name":"Wheeler","email":"jwheelero@umn.edu","country":"Russia","ip_address":"105.39.140.210"}, 26 | {"id":26,"first_name":"Jerry","last_name":"Stephens","email":"jstephensp@washingtonpost.com","country":"Brazil","ip_address":"228.137.23.224"}, 27 | {"id":27,"first_name":"Justin","last_name":"Hunt","email":"jhuntq@trellian.com","country":"China","ip_address":"222.43.155.29"}, 28 | {"id":28,"first_name":"Sandra","last_name":"Warren","email":"swarrenr@youku.com","country":"Russia","ip_address":"167.220.221.26"}, 29 | {"id":29,"first_name":"John","last_name":"Garcia","email":"jgarcias@blogger.com","country":"Ukraine","ip_address":"145.219.121.151"}, 30 | {"id":30,"first_name":"Steve","last_name":"Hawkins","email":"shawkinst@weather.com","country":"Brazil","ip_address":"81.154.46.254"}, 31 | {"id":31,"first_name":"Diane","last_name":"Cox","email":"dcoxu@imgur.com","country":"Indonesia","ip_address":"191.160.69.60"}, 32 | {"id":32,"first_name":"Michael","last_name":"Peterson","email":"mpetersonv@hibu.com","country":"China","ip_address":"183.41.62.243"}, 33 | {"id":33,"first_name":"Judy","last_name":"Wallace","email":"jwallacew@diigo.com","country":"China","ip_address":"183.107.210.30"}, 34 | {"id":34,"first_name":"Carolyn","last_name":"Matthews","email":"cmatthewsx@va.gov","country":"Colombia","ip_address":"151.57.49.42"}, 35 | {"id":35,"first_name":"Nancy","last_name":"Willis","email":"nwillisy@nps.gov","country":"China","ip_address":"162.109.253.122"}, 36 | {"id":36,"first_name":"Joshua","last_name":"Arnold","email":"jarnoldz@xing.com","country":"United States","ip_address":"191.9.195.250"}, 37 | {"id":37,"first_name":"Shawn","last_name":"Marshall","email":"smarshall10@cdc.gov","country":"Uruguay","ip_address":"136.184.22.82"}, 38 | {"id":38,"first_name":"Paula","last_name":"Webb","email":"pwebb11@sphinn.com","country":"Russia","ip_address":"49.178.160.57"}, 39 | {"id":39,"first_name":"Joseph","last_name":"Gomez","email":"jgomez12@sun.com","country":"Russia","ip_address":"225.23.168.44"}, 40 | {"id":40,"first_name":"Lisa","last_name":"Cole","email":"lcole13@guardian.co.uk","country":"China","ip_address":"22.237.62.176"}, 41 | {"id":41,"first_name":"Janet","last_name":"Richardson","email":"jrichardson14@fotki.com","country":"China","ip_address":"43.112.168.174"}, 42 | {"id":42,"first_name":"Annie","last_name":"Jacobs","email":"ajacobs15@rakuten.co.jp","country":"Yemen","ip_address":"16.89.53.230"}, 43 | {"id":43,"first_name":"Michelle","last_name":"Rose","email":"mrose16@miitbeian.gov.cn","country":"Indonesia","ip_address":"159.18.139.155"}, 44 | {"id":44,"first_name":"Kathy","last_name":"Coleman","email":"kcoleman17@mysql.com","country":"Philippines","ip_address":"19.110.175.196"}, 45 | {"id":45,"first_name":"Rebecca","last_name":"Watkins","email":"rwatkins18@ebay.com","country":"Kiribati","ip_address":"95.63.225.104"}, 46 | {"id":46,"first_name":"Larry","last_name":"Ross","email":"lross19@bloglines.com","country":"Indonesia","ip_address":"21.175.246.165"}, 47 | {"id":47,"first_name":"Fred","last_name":"Hamilton","email":"fhamilton1a@state.tx.us","country":"Afghanistan","ip_address":"179.151.28.250"}, 48 | {"id":48,"first_name":"Philip","last_name":"Stevens","email":"pstevens1b@theguardian.com","country":"Cuba","ip_address":"24.154.255.160"}, 49 | {"id":49,"first_name":"Daniel","last_name":"Ruiz","email":"druiz1c@cafepress.com","country":"United States","ip_address":"195.47.83.217"}, 50 | {"id":50,"first_name":"Laura","last_name":"White","email":"lwhite1d@kickstarter.com","country":"Russia","ip_address":"193.34.136.106"}] 51 | -------------------------------------------------------------------------------- /example/ng-v1.2x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ng-contextmenu angular v1.2x example 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 48 | 49 | 50 | 51 |
52 | 53 | 59 | 60 |

data table example

61 | 62 | 63 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
{{ row.id }}{{ row.first_name }}{{ row.last_name }}{{ row.email }}{{ row.country }}{{ row.ip_address }}
92 | 93 |
94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /example/ng-v1.3x-two-menus.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ng-contextmenu angular v1.3x example with two menu's 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 51 | 52 | 53 | 54 |
55 | 56 | 62 | 63 | 64 | 81 | 82 | 83 | 100 | 101 |

first table

102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
{{ row.id }}{{ row.first_name }}{{ row.last_name }}{{ row.email }}{{ row.country }}{{ row.ip_address }}
114 | 115 |

second table

116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
{{ row.id }}{{ row.first_name }}{{ row.last_name }}{{ row.email }}{{ row.country }}{{ row.ip_address }}
128 | 129 | 130 |
131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /example/ng-v1.3x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ng-contextmenu angular v1.3x example 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 48 | 49 | 50 | 51 |
52 | 53 | 59 | 60 |

data table example

61 | 62 | 63 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
{{ row.id }}{{ row.first_name }}{{ row.last_name }}{{ row.email }}{{ row.country }}{{ row.ip_address }}
92 | 93 |
94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /example/ng-v1.4x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ng-contextmenu angular v{{main.version.full}} example 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 44 | 45 |

data table example

46 | 47 | 48 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
{{ row.id }}{{ row.first_name }}{{ row.last_name }}{{ row.email }}{{ row.country }}{{ row.ip_address }}
77 | 78 |
79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | plugins: [ 10 | 'karma-jasmine', 11 | 'karma-browserify', 12 | 'karma-chrome-launcher', 13 | 'karma-firefox-launcher', 14 | 'karma-phantomjs-launcher', 15 | ], 16 | 17 | // frameworks to use 18 | frameworks: ['browserify', 'jasmine'], 19 | 20 | // list of files / patterns to load in the browser 21 | files: [ 22 | require.resolve('jquery'), 23 | 'node_modules/angular/angular.js', 24 | 'node_modules/angular-mocks/angular-mocks.js', 25 | 'src/index.js', 26 | 'spec/*.spec.js' 27 | ], 28 | 29 | // list of files to exclude 30 | exclude: [ 31 | ], 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | 'src/*.js': ['browserify'], 37 | 'spec/*.spec.js': ['browserify'] 38 | }, 39 | 40 | browserify: { 41 | debug: true, 42 | bundleDelay: 1500 43 | // transform: ['brfs'] 44 | }, 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | // reporters: ['progress', 'html'], 50 | reporters: ['progress'], 51 | 52 | // the default configuration 53 | htmlReporter: { 54 | outputDir: 'build/karma_html', 55 | templatePath: 'node_modules/karma-html-reporter/jasmine_template.html' 56 | }, 57 | 58 | // web server port 59 | port: 9876, 60 | 61 | // enable / disable colors in the output (reporters and logs) 62 | colors: true, 63 | 64 | // level of logging 65 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 66 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 67 | logLevel: config.LOG_INFO, 68 | 69 | // enable / disable watching file and executing tests whenever any file changes 70 | autoWatch: true, 71 | autoWatchBatchDelay: 250, 72 | 73 | // start these browsers 74 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 75 | browsers: ['Chrome'], 76 | 77 | // Continuous Integration mode 78 | // if true, Karma captures browsers, runs the tests and exits 79 | singleRun: false 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-contextmenu", 3 | "description": "a contextmenu helper service for angular & bootstrap", 4 | "main": "dist/contextmenu.js", 5 | "version": "0.0.0-dev", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "test": "karma start karma.conf.js --single-run", 9 | "test-travis": "karma start karma.conf.js --single-run --browsers PhantomJS", 10 | "build": "npm run less && npm run browserify", 11 | "watch:build": "watchify src/index.js -o dist/contextmenu.js -v", 12 | "browserify": "browserify src/index.js --standalone ng-contextmenu | derequire > dist/contextmenu.js", 13 | "less": "lessc style/contextmenu.less > dist/style.css", 14 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 15 | "install-scope": "TEST_SCOPE=${TEST_SCOPE:-1.6} npm install angular@$TEST_SCOPE angular-mocks@$TEST_SCOPE" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ds82/angular-contextmenu.git" 20 | }, 21 | "keywords": [ 22 | "ng", 23 | "angular", 24 | "twbs", 25 | "bootstrap", 26 | "contextmenu" 27 | ], 28 | "author": "Dennis Sänger ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/ds82/angular-contextmenu/issues" 32 | }, 33 | "homepage": "https://github.com/ds82/angular-contextmenu", 34 | "devDependencies": { 35 | "browserify": "14.3.0", 36 | "derequire": "2.0.6", 37 | "jasmine-core": "2.6.1", 38 | "jquery": "3.2.1", 39 | "karma": "1.7.0", 40 | "karma-browserify": "5.1.1", 41 | "karma-chrome-launcher": "2.1.1", 42 | "karma-cli": "1.0.1", 43 | "karma-firefox-launcher": "1.0.1", 44 | "karma-jasmine": "1.1.0", 45 | "karma-phantomjs-launcher": "1.0.4", 46 | "less": "2.7.2", 47 | "semantic-release": "6.3.6", 48 | "watchify": "3.9.0" 49 | }, 50 | "dependencies": {} 51 | } 52 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ds82/angular-contextmenu/7d30da4684ffd5cd33562fbace6ad8da8ef9df48/screen.png -------------------------------------------------------------------------------- /spec/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "indent": true, 4 | "quotmark": "single", 5 | "maxparams": 4, 6 | "maxdepth": 3, 7 | "maxstatements": 20, 8 | "maxlen": 120, 9 | "curly": true, 10 | "indent": true, 11 | 12 | "undef": true, 13 | "unused": true, 14 | "globals": { 15 | }, 16 | "predef": [ 17 | "window", 18 | "define", 19 | "document", 20 | "console", 21 | "angular", 22 | "jasmine", 23 | "describe", 24 | "it", 25 | "beforeEach", 26 | "inject", 27 | "spyOn", 28 | "expect" 29 | ], 30 | "node": true, 31 | "globalstrict": true 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /spec/contextmenu.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('io.dennis.contextmenu', function() { 4 | var mock = angular.mock; 5 | var ae = angular.element; 6 | 7 | describe('directive:contextmenu', function() { 8 | var mockWindow; 9 | var $injector, $compile, $rootScope, $timeout; 10 | var $scope; 11 | var elementSpy; 12 | 13 | beforeEach(mock.module('io.dennis.contextmenu', function($provide) { 14 | mockWindow = { 15 | innerWidth: 1024, 16 | innerHeight: 768 17 | }; 18 | $provide.value('$window', mockWindow); 19 | })); 20 | beforeEach(inject(function(_$injector_, _$compile_, _$rootScope_, _$timeout_) { 21 | $injector = _$injector_; 22 | $compile = _$compile_; 23 | $rootScope = _$rootScope_; 24 | $timeout = _$timeout_; 25 | 26 | var configService = $injector.get('$contextmenu'); 27 | configService.set('DEBOUNCE_BROADCAST_TIME', 0); 28 | 29 | $scope = $rootScope.$new(); 30 | })); 31 | 32 | describe('with angular.element() spy', function() { 33 | 34 | beforeEach(function() { 35 | elementSpy = spyOn(angular, 'element'); 36 | }); 37 | 38 | afterEach(function() { 39 | elementSpy.and.callThrough(); 40 | }); 41 | 42 | it('should register $window.(click|contextmenu|scroll)', function() { 43 | var $window = $injector.get('$window'); 44 | var $windowElementStub = jasmine.createSpyObj('we', ['on']); 45 | 46 | var onEventFn; 47 | $windowElementStub.on.and.callFake(function(event, fn) { 48 | onEventFn = fn; 49 | }); 50 | 51 | elementSpy.and.callFake(function(_element) { 52 | return (_element === $window) ? 53 | $windowElementStub : ae(_element); 54 | }); 55 | 56 | var html = '
'; 57 | var element = angular.element(html); 58 | $compile(element)($scope); 59 | 60 | expect($windowElementStub.on).toHaveBeenCalledWith( 61 | 'contextmenu scroll click', jasmine.any(Function) 62 | ); 63 | 64 | }); 65 | 66 | it('should broadcast contextmenu.close on ', function(done) { 67 | var $window = $injector.get('$window'); 68 | var $windowElementStub = jasmine.createSpyObj('we', ['on']); 69 | 70 | $windowElementStub.on.and.callFake(onRegisterEvent); 71 | 72 | elementSpy.and.callFake(function(_element) { 73 | return (_element === $window) ? 74 | $windowElementStub : ae(_element); 75 | }); 76 | 77 | var broadcastSpy = spyOn($rootScope, '$broadcast'); 78 | 79 | var html = '
'; 80 | var element = angular.element(html); 81 | $compile(element)($scope); 82 | 83 | function onRegisterEvent(event, fn) { 84 | fn(); 85 | expect(broadcastSpy).toHaveBeenCalledWith('contextmenu.close'); 86 | done(); 87 | } 88 | 89 | }); 90 | }); 91 | 92 | it('should be able to register two independant menus', function() { 93 | var html = '
'; 94 | html += '
'; 95 | 96 | var element = angular.element(html); 97 | $compile(element)($scope); 98 | 99 | $rootScope.$apply(); 100 | expect($scope.some.menu).not.toEqual($scope.other.menu); 101 | }); 102 | 103 | it('should close other menus before opening current', function(done) { 104 | var html = '
'; 105 | html += '
'; 106 | 107 | var element = angular.element(html); 108 | var compiled = $compile(element)($scope); 109 | $rootScope.$apply(); 110 | 111 | var first = ae(compiled['0']); 112 | var second = ae(compiled['1']); 113 | 114 | first.controller('contextmenu').open(); 115 | 116 | setTimeout(function() { 117 | second.controller('contextmenu').open(); 118 | 119 | expect(first.hasClass('ng-hide')).toEqual(true); 120 | done(); 121 | }, 1); 122 | }); 123 | 124 | describe('', function() { 125 | 126 | var $element; 127 | 128 | beforeEach(function() { 129 | var html = '
'; 130 | var element = angular.element(html); 131 | var compiled = $compile(element)($scope); 132 | $rootScope.$apply(); 133 | $element = ae(compiled['0']); 134 | }); 135 | 136 | it('should open upwards if below the page mid', function() { 137 | var belowPageMid = (mockWindow.innerHeight / 2) + 1; 138 | $element.controller('contextmenu').open(null, 0, belowPageMid); 139 | $timeout.flush(); 140 | expect($element.hasClass('dropup')).toEqual(true); 141 | }); 142 | 143 | it('should not stick out of the viewport', function() { 144 | var html = '
'; 145 | var element = angular.element(html); 146 | var compiled = $compile(element)($scope); 147 | $rootScope.$apply(); 148 | $element = ae(compiled['0']); 149 | 150 | var setX = mockWindow.innerWidth; 151 | $element.controller('contextmenu').open(null, setX, 0); 152 | $timeout.flush(); 153 | 154 | var elementX = parseFloat($element.css('left')) 155 | expect(elementX < setX + mockWindow.innerWidth).toEqual(true); 156 | }); 157 | 158 | }); 159 | 160 | 161 | }); 162 | 163 | }); 164 | -------------------------------------------------------------------------------- /src/directive/container.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('io.dennis.contextmenu') 4 | .directive('contextmenuContainer', Container); 5 | 6 | function Container() { 7 | return { 8 | scope: { 9 | contextmenu: '=contextmenuContainer' 10 | }, 11 | restrict: 'A', 12 | controller: ['$scope', ContainerCtrl] 13 | }; 14 | 15 | } 16 | 17 | function ContainerCtrl($scope) { 18 | var pub = this; 19 | pub.get = get; 20 | 21 | function get() { 22 | return $scope.contextmenu; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/directive/contextmenu.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('io.dennis.contextmenu') 4 | .provider('$contextmenu', ContextmenuProvider) 5 | .directive('contextmenu', Contextmenu); 6 | 7 | var config = { 8 | DEBOUNCE_BROADCAST_TIME: 200 9 | }; 10 | var contextmenuConfig = new ContextmenuConfig(); 11 | 12 | function ContextmenuConfig() { 13 | this.set = function(key, value) { 14 | if (config[key]) { 15 | config[key] = value; 16 | } 17 | return config[key]; 18 | }; 19 | 20 | this.get = function(key) { 21 | return config[key]; 22 | }; 23 | } 24 | 25 | function ContextmenuProvider() { 26 | this.$get = function() { 27 | return contextmenuConfig; 28 | }; 29 | } 30 | 31 | Contextmenu.$inject = [ 32 | '$window', 33 | '$rootScope', 34 | 'ContextmenuService' 35 | ]; 36 | 37 | var canBroadcast = true; 38 | var broadcastClose; 39 | 40 | function Contextmenu($window, $rootScope, $contextmenu) { 41 | 42 | broadcastClose = (function($rootScope) { 43 | return function _broadcastClose() { 44 | if (canBroadcast) { 45 | $rootScope.$broadcast('contextmenu.close'); 46 | canBroadcast = false; 47 | setTimeout(function() { 48 | canBroadcast = true; 49 | }, config.DEBOUNCE_BROADCAST_TIME); 50 | } 51 | } 52 | })($rootScope); 53 | 54 | var $windowElement = angular.element($window); 55 | $windowElement.on('contextmenu scroll click', broadcastClose); 56 | 57 | return { 58 | scope: { 59 | contextmenu: '=' 60 | }, 61 | restrict: 'A', 62 | controller: CotextmenuCtrl, 63 | link: link, 64 | priority: 100 65 | }; 66 | 67 | function link(scope, element, attrs, ctrl) { 68 | scope.contextmenu = $contextmenu.$get(); 69 | scope.contextmenu.setMenu(ctrl); 70 | ctrl.setElement(element); 71 | } 72 | } 73 | 74 | CotextmenuCtrl.$inject = ['$scope', '$window', '$rootScope', '$timeout']; 75 | function CotextmenuCtrl($scope, $window, $rootScope, $timeout) { 76 | var pub = this; 77 | var $element; 78 | $scope.$on('contextmenu.close', close); 79 | 80 | pub.open = open; 81 | pub.close = close; 82 | pub.setElement = setElement; 83 | 84 | function open(item, x, y) { 85 | broadcastClose(); 86 | 87 | $element 88 | .toggleClass('open', true) 89 | .toggleClass('dropup', isDropup(y)) 90 | .css('visibility', 'hidden') 91 | .toggleClass('ng-hide', false); 92 | 93 | $timeout(function() { 94 | var width = $element.children().width(); 95 | 96 | x = (x + width > $window.innerWidth) ? 97 | $window.innerWidth - (width + 5) : x; 98 | 99 | $element.css({ 100 | top: y + 'px', 101 | left: x + 'px', 102 | visibility: 'visible' 103 | }); 104 | }); 105 | } 106 | 107 | function close() { 108 | $element.toggleClass('ng-hide', true); 109 | } 110 | 111 | function setElement(element) { 112 | $element = element; 113 | } 114 | 115 | function isDropup(y) { 116 | var mid = $window.innerHeight / 2; 117 | return (y > mid); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/directive/item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('io.dennis.contextmenu') 4 | .directive('contextmenuItem', Item); 5 | 6 | Item.$inject = []; 7 | 8 | function Item() { 9 | 10 | /*global DocumentTouch:false */ 11 | var isTouch = !!(('ontouchstart' in window) || 12 | window.DocumentTouch && document instanceof DocumentTouch); 13 | 14 | return { 15 | scope: { 16 | item: '=contextmenuItem' 17 | }, 18 | require: '^contextmenuContainer', 19 | restrict: 'A', 20 | link: link 21 | }; 22 | 23 | function link(scope, element, attrs, ctrl) { 24 | var iam = mk(scope.item, element); 25 | 26 | return (isTouch) ? 27 | registerTouch(iam, scope, ctrl) : 28 | registerMouse(iam, scope, ctrl); 29 | } 30 | 31 | function registerTouch(iam, scope, ctrl) { 32 | iam.element.on('click', function(ev) { 33 | ev.preventDefault(); 34 | ev.stopPropagation(); 35 | ctrl.get().open(iam, ev.clientX, ev.clientY); 36 | scope.$apply(); 37 | return false; 38 | }); 39 | } 40 | 41 | function registerMouse(iam, scope, ctrl) { 42 | iam.element.on('click', function(ev) { 43 | var multi = ev.ctrlKey || ev.metaKey; 44 | ev.preventDefault(); 45 | ev.stopPropagation(); 46 | 47 | ctrl.get().toggle(iam, multi); 48 | scope.$apply(); 49 | }); 50 | 51 | iam.element.on('contextmenu', function(ev) { 52 | // don't show context menu if user holds down ctrl || cmd key 53 | if (ev.ctrlKey || ev.metaKey) { return; } 54 | 55 | ev.preventDefault(); 56 | ev.stopPropagation(); 57 | ev.stopImmediatePropagation(); 58 | 59 | ctrl.get().open(iam, ev.clientX, ev.clientY); 60 | scope.$apply(); 61 | 62 | return false; 63 | }); 64 | } 65 | 66 | function mk(item, element) { 67 | return {item: item, element: element}; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('io.dennis.contextmenu', []); 4 | 5 | require('./service/service'); 6 | 7 | require('./directive/contextmenu'); 8 | require('./directive/container'); 9 | require('./directive/item'); 10 | -------------------------------------------------------------------------------- /src/service/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('io.dennis.contextmenu') 4 | .service('ContextmenuService', ContextmenuProvider); 5 | 6 | function ContextmenuProvider() { 7 | var self = this; 8 | 9 | self.$get = function() { 10 | return new Contextmenu(); 11 | }; 12 | } 13 | 14 | function Contextmenu() { 15 | var pub = this; 16 | 17 | var selected = []; 18 | var menu; 19 | 20 | init(); 21 | 22 | function init() { 23 | pub.menu = menu; 24 | pub.selected = selected; 25 | 26 | pub.setMenu = setMenu; 27 | pub.add = add; 28 | pub.remove = remove; 29 | pub.isSelected = isSelected; 30 | pub.get = get; 31 | pub.num = getNumberOf; 32 | pub.open = open; 33 | pub.close = close; 34 | pub.toggle = toggle; 35 | pub.clear = clear; 36 | pub.listOfIds = getListOfIds; 37 | } 38 | 39 | function setMenu(ctrl) { 40 | menu = ctrl; 41 | } 42 | 43 | function add(entry) { 44 | if (!isSelected(entry)) { 45 | selected.unshift(entry); 46 | toggleSelected(entry.element, true); 47 | } 48 | pub.item = selected[0].item; 49 | } 50 | 51 | function remove(entry) { 52 | var index = selected.indexOf(entry); 53 | if (index > -1) { 54 | selected.splice(index, 1); 55 | } 56 | toggleSelected(entry.element, false); 57 | } 58 | 59 | function isSelected(entry) { 60 | return (selected.indexOf(entry) > -1); 61 | } 62 | 63 | function get() { 64 | return selected[0]; 65 | } 66 | 67 | function getNumberOf() { 68 | return selected.length || 0; 69 | } 70 | 71 | function open(entry, x, y) { 72 | x = x || 0; 73 | y = y || 0; 74 | 75 | if (!isSelected(entry)) { 76 | clear(); 77 | } 78 | add(entry); 79 | menu.open.apply(null, arguments); 80 | } 81 | 82 | function close() { 83 | menu.close.apply(null, arguments); 84 | } 85 | 86 | function toggle(entry, multi) { 87 | multi = multi || false; 88 | var isEntrySelected = isSelected(entry); 89 | 90 | if (isEntrySelected) { 91 | remove(entry); 92 | 93 | } else { 94 | 95 | if (!multi) { clear(); } 96 | add(entry); 97 | } 98 | } 99 | 100 | function clear() { 101 | angular.forEach(selected, function(entry) { 102 | toggleSelected(entry.element, false); 103 | }); 104 | selected = []; 105 | } 106 | 107 | function getListOfIds(limit, path) { 108 | path = path || 'item.id'; 109 | limit = Math.min(limit || selected.length, selected.length); 110 | var list = selected.slice(0, limit).map(function(entry) { 111 | return safeGet(entry, path, ''); 112 | }); 113 | var asString = list.join(', '); 114 | return (limit < selected.length) ? asString + '..' : asString; 115 | } 116 | 117 | function toggleSelected(element, forceState) { 118 | element.toggleClass('selected', forceState); 119 | } 120 | 121 | function safeGet(obj, path, _default) { 122 | 123 | if (!obj) { 124 | return _default; 125 | } 126 | 127 | if (!path || !String(path).length) { 128 | return obj; 129 | } 130 | 131 | var keys = (angular.isArray(path)) ? path : path.split('.'); 132 | var next = keys.shift(); 133 | return get(obj[next], keys, _default); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /style/contextmenu.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | 3 | .contextmenu { 4 | position: fixed; 5 | display: block; 6 | z-index: 5000; 7 | 8 | li>a:hover { 9 | background-color: @btn-primary-bg; 10 | color: @btn-primary-color; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /style/variables.less: -------------------------------------------------------------------------------- 1 | @brand-primary: #428bca; 2 | 3 | @btn-primary-color: #fff; 4 | @btn-primary-bg: @brand-primary; 5 | --------------------------------------------------------------------------------