├── .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 | [](https://nodei.co/npm/angular-contextmenu/)
4 |
5 | [](http://travis-ci.org/ds82/angular-contextmenu) []() [](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 | 
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 | User |
89 | Domains |
90 | |
91 |
92 |
93 |
94 |
95 | |
96 | {{row.email}} |
97 | {{row.domains.join(', ')}} |
98 |
99 |
100 | |
101 |
102 |
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 | {{ row.id }} |
85 | {{ row.first_name }} |
86 | {{ row.last_name }} |
87 | {{ row.email }} |
88 | {{ row.country }} |
89 | {{ row.ip_address }} |
90 |
91 |
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 | {{ row.id }} |
107 | {{ row.first_name }} |
108 | {{ row.last_name }} |
109 | {{ row.email }} |
110 | {{ row.country }} |
111 | {{ row.ip_address }} |
112 |
113 |
114 |
115 |
second table
116 |
117 |
118 |
119 |
120 | {{ row.id }} |
121 | {{ row.first_name }} |
122 | {{ row.last_name }} |
123 | {{ row.email }} |
124 | {{ row.country }} |
125 | {{ row.ip_address }} |
126 |
127 |
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 | {{ row.id }} |
85 | {{ row.first_name }} |
86 | {{ row.last_name }} |
87 | {{ row.email }} |
88 | {{ row.country }} |
89 | {{ row.ip_address }} |
90 |
91 |
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 | {{ row.id }} |
70 | {{ row.first_name }} |
71 | {{ row.last_name }} |
72 | {{ row.email }} |
73 | {{ row.country }} |
74 | {{ row.ip_address }} |
75 |
76 |
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 |
--------------------------------------------------------------------------------