├── .gitattributes ├── .gitignore ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── clickoutside.directive.js ├── example ├── .gitignore ├── README.md ├── app.js ├── css │ └── styles.css ├── index.html └── js │ └── controllers │ └── app.controller.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 3 | * text=auto 4 | 5 | # 6 | # The above will handle all files NOT found below 7 | # 8 | 9 | # These files are text and should be normalized 10 | *.php text 11 | *.css text 12 | *.js text 13 | *.htm text 14 | *.html text 15 | *.xml text 16 | *.txt text 17 | *.ini text 18 | *.inc text 19 | *.md text 20 | .htaccess text 21 | 22 | 23 | # These files are binary and should be left untouched 24 | # (binary is a macro for -text -diff) 25 | *.png binary 26 | *.jpg binary 27 | *.jpeg binary 28 | *.gif binary 29 | *.ico binary 30 | *.mov binary 31 | *.mp4 binary 32 | *.mp3 binary 33 | *.flv binary 34 | *.fla binary 35 | *.swf binary 36 | *.gz binary 37 | *.zip binary 38 | *.7z binary 39 | *.ttf binary 40 | 41 | # Documents 42 | *.doc diff=astextplain 43 | *.DOC diff=astextplain 44 | *.docx diff=astextplain 45 | *.DOCX diff=astextplain 46 | *.dot diff=astextplain 47 | *.DOT diff=astextplain 48 | *.pdf diff=astextplain 49 | *.PDF diff=astextplain 50 | *.rtf diff=astextplain 51 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Laravel 2 | /bootstrap/compiled.php 3 | .env.*.php 4 | .env.php 5 | 6 | # PUPHET 7 | /puphpet/* 8 | !/puphpet/config.yaml 9 | !/puphpet/README.md 10 | 11 | # Sublime Text IDE 12 | *.tmlanguage.cache 13 | *.tmPreferences.cache 14 | *.stTheme.cache 15 | *.sublime-workspace 16 | *.sublime-project 17 | sftp-config.json 18 | 19 | # angular-seed 20 | /angular-seed 21 | 22 | # MISC 23 | /vendor 24 | composer.phar 25 | .DS_Store 26 | Thumbs.db 27 | _notes 28 | .vagrant 29 | /*.lnk 30 | /bower_components 31 | /node_modules 32 | npm-debug.log 33 | 34 | # Brackets IDE 35 | .ftppass 36 | .brackets.json 37 | .grunt* 38 | 39 | # PhpStorm IDE 40 | .idea 41 | 42 | # Binaries 43 | *.exe 44 | *.app 45 | *.rar 46 | *.zip -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | bump: { 7 | options: { 8 | files: ['package.json', 'bower.json'], 9 | updateConfigs: [], 10 | commit: true, 11 | commitMessage: 'Bumped to release v%VERSION%', 12 | commitFiles: ['package.json', 'bower.json'], 13 | createTag: true, 14 | tagName: 'v%VERSION%', 15 | tagMessage: 'Version %VERSION%', 16 | push: false, 17 | pushTo: 'upstream', 18 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d', 19 | globalReplace: false, 20 | prereleaseName: false, 21 | regExp: false 22 | } 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-bump'); 27 | 28 | /**\/ 29 | $ grunt bump 30 | >> Version bumped to 0.0.2 31 | >> Committed as "Release v0.0.2" 32 | >> Tagged as "v0.0.2" 33 | >> Pushed to origin 34 | 35 | $ grunt bump:patch 36 | >> Version bumped to 0.0.3 37 | >> Committed as "Release v0.0.3" 38 | >> Tagged as "v0.0.3" 39 | >> Pushed to origin 40 | 41 | $ grunt bump:minor 42 | >> Version bumped to 0.1.0 43 | >> Committed as "Release v0.1.0" 44 | >> Tagged as "v0.1.0" 45 | >> Pushed to origin 46 | 47 | $ grunt bump:major 48 | >> Version bumped to 1.0.0 49 | >> Committed as "Release v1.0.0" 50 | >> Tagged as "v1.0.0" 51 | >> Pushed to origin 52 | 53 | $ grunt bump:patch 54 | >> Version bumped to 1.0.1 55 | >> Committed as "Release v1.0.1" 56 | >> Tagged as "v1.0.1" 57 | >> Pushed to origin 58 | 59 | $ grunt bump:git 60 | >> Version bumped to 1.0.1-ge96c 61 | >> Committed as "Release v1.0.1-ge96c" 62 | >> Tagged as "v1.0.1-ge96c" 63 | >> Pushed to origin 64 | 65 | $ grunt bump:prepatch 66 | >> Version bumped to 1.0.2-0 67 | >> Committed as "Release v1.0.2-0" 68 | >> Tagged as "v1.0.2-0" 69 | >> Pushed to origin 70 | 71 | $ grunt bump:prerelease 72 | >> Version bumped to 1.0.2-1 73 | >> Committed as "Release v1.0.2-1" 74 | >> Tagged as "v1.0.2-1" 75 | >> Pushed to origin 76 | 77 | $ grunt bump:patch # (major, minor or patch) will do this 78 | >> Version bumped to 1.0.2 79 | >> Committed as "Release v1.0.2" 80 | >> Tagged as "v1.0.2" 81 | >> Pushed to origin 82 | 83 | $ grunt bump:preminor 84 | >> Version bumped to 1.1.0-0 85 | >> Committed as "Release v1.1.0-0" 86 | >> Tagged as "v1.1.0-0" 87 | >> Pushed to origin 88 | 89 | $ grunt bump 90 | >> Version bumped to 1.1.0 91 | >> Committed as "Release v1.1.0" 92 | >> Tagged as "v1.1.0" 93 | >> Pushed to origin 94 | 95 | $ grunt bump:premajor (with prerelaseName set to 'rc' in options) 96 | >> Version bumped to 2.0.0-rc.0 97 | >> Committed as "Release v2.0.0-rc.0" 98 | >> Tagged as "v2.0.0-rc.0" 99 | >> Pushed to origin 100 | 101 | $ grunt bump 102 | >> Version bumped to 2.0.0 103 | >> Committed as "Release v2.0.0" 104 | >> Tagged as "v2.0.0" 105 | >> Pushed to origin 106 | 107 | $ grunt bump:prerelease # from a released version `prerelease` defaults to prepatch 108 | >> Version bumped to 2.0.1-rc.0 109 | >> Committed as "Release v2.0.1-rc.0" 110 | >> Tagged as "v2.0.1-rc.0" 111 | >> Pushed to origin 112 | /**/ 113 | }; 114 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Adam Jowett 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 | # angular-click-outside 2 | 3 | An Angular directive to detect a click outside of an elements scope. Great for closing dialogues, drawers and off screen menu's etc. 4 | 5 | ### Recent changes 6 | 7 | - Shortened Bower description to remove Bower warning on install (thanks [@jcubic](https://github.com/jcubic)) 8 | - Thanks to [@Lorelei](https://github.com/Lorelei) for the pull request to pass the event back in the callback function 9 | - Thanks to [@CosticaPuntaru](https://github.com/CosticaPuntaru) for the improvement to now allow the directive to no longer need an id on the element for this to work 10 | - Added basic ngdocs documentation 11 | - Removed the addition of the element id to the classes array as now it no longer needs to be checked 12 | 13 | ### Roadmap 14 | 15 | - Addition of outside-if attribute. Ability to restrict click outside registering to defined elements (opposite of outside-if-not) rather than anywhere outside an element 16 | - Look into the worth of converting directive to Angular 1 component 17 | - Conversion of directive to Angular 2 component 18 | 19 | ### Installation 20 | 21 | There are two easy ways to install the clickoutside directive: 22 | 23 | #### Manual download 24 | 25 | Download the `clickoutside.directive.js` file, and include it in your `index.html` file with something like: 26 | 27 | ```html 28 | 29 | ``` 30 | 31 | Also be sure to include the module in your `app.js` file with: 32 | 33 | #### npm 34 | ======= 35 | ```javascript 36 | angular.module('yourAppName', ['angular-click-outside']) 37 | ``` 38 | 39 | #### Bower 40 | ======= 41 | ```shell 42 | npm install @iamadamjowett/angular-click-outside 43 | ``` 44 | 45 | ### Usage 46 | ======= 47 | ```shell 48 | bower install angular-click-outside --save 49 | ``` 50 | 51 | ### Usage 52 | 53 | The directive will work with either ID's or classes, however, be wary of using classes as quite often some unwanted elements may have the same class, and so will be excluded/included unintentionally. 54 | 55 | If you are sure that you want to exclude/include all elements with a class however the directive will work just fine as it looks through the classNames as well as looking at the given ID list. 56 | 57 | General though ID's will suffice, but instances of dynamically inserted list items may require the use of classes. 58 | 59 | Add the directive via the `click-outside` attribute, and give exceptions via the `outside-if-not` attribute. 60 | 61 | Basic example: 62 | 63 | ```html 64 | 67 | ``` 68 | 69 | This is of little use though without a callback function to do something with that click: 70 | 71 | ```html 72 | 75 | ``` 76 | 77 | Where `closeThis()` is the function assigned to the scope via the controller such as: 78 | 79 | ```javascript 80 | angular 81 | .module('myApp') 82 | .controller('MenuController', ['$scope', MenuController]); 83 | 84 | function MenuController($scope) { 85 | $scope.closeThis = function () { 86 | console.log('closing'); 87 | } 88 | } 89 | ``` 90 | 91 | ```html 92 | 93 |
94 | 97 |
98 | ``` 99 | 100 | ### Adding Exceptions 101 | 102 | You can also add exceptions via the `outside-if-not` tag, which executes the callback function, but only if the ID's or classes listed aren't clicked. 103 | 104 | In this case `closeThis()` will be called only if clicked outside _and_ `#my-button` wasn't clicked as well (note `.my-button` also would be an exception). This can be great for things like slide in menus that might have a button outside of the menu scope that triggers it: 105 | 106 | ```html 107 | 108 | 111 | ``` 112 | 113 | You can have more than one exception by comma delimiting a list such as: 114 | 115 | ```html 116 | 117 | 120 | 121 | ``` 122 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-click-outside", 3 | "version": "2.11.0", 4 | "main": "clickoutside.directive.js", 5 | "homepage": "https://github.com/IamAdamJowett/angular-click-outside", 6 | "authors": [ 7 | "IamAdamJowett" 8 | ], 9 | "description": "An angular directive to detect a click outside of an elements scope, e.g. clicking outside of slide in menus.", 10 | "keywords": [ 11 | "angular", 12 | "click", 13 | "outside" 14 | ], 15 | "license": "MIT", 16 | "ignore": [], 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/IamAdamJowett/angular-click-outside" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /clickoutside.directive.js: -------------------------------------------------------------------------------- 1 | /*global angular, navigator*/ 2 | 3 | (function() { 4 | 'use strict'; 5 | 6 | angular 7 | .module('angular-click-outside', []) 8 | .directive('clickOutside', [ 9 | '$document', '$parse', '$timeout', 10 | clickOutside 11 | ]); 12 | 13 | /** 14 | * @ngdoc directive 15 | * @name angular-click-outside.directive:clickOutside 16 | * @description Directive to add click outside capabilities to DOM elements 17 | * @requires $document 18 | * @requires $parse 19 | * @requires $timeout 20 | **/ 21 | function clickOutside($document, $parse, $timeout) { 22 | return { 23 | restrict: 'A', 24 | link: function($scope, elem, attr) { 25 | 26 | // postpone linking to next digest to allow for unique id generation 27 | $timeout(function() { 28 | var classList = (attr.outsideIfNot !== undefined) ? attr.outsideIfNot.split(/[ ,]+/) : [], 29 | fn; 30 | 31 | function eventHandler(e) { 32 | var i, 33 | element, 34 | r, 35 | id, 36 | classNames, 37 | l; 38 | 39 | // check if our element already hidden and abort if so 40 | if (angular.element(elem).hasClass("ng-hide")) { 41 | return; 42 | } 43 | 44 | // if there is no click target, no point going on 45 | if (!e || !e.target) { 46 | return; 47 | } 48 | 49 | // loop through the available elements, looking for classes in the class list that might match and so will eat 50 | for (element = e.target; element; element = element.parentNode) { 51 | // check if the element is the same element the directive is attached to and exit if so (props @CosticaPuntaru) 52 | if (element === elem[0]) { 53 | return; 54 | } 55 | 56 | // now we have done the initial checks, start gathering id's and classes 57 | id = element.id, 58 | classNames = element.className, 59 | l = classList.length; 60 | 61 | // Unwrap SVGAnimatedString classes 62 | if (classNames && classNames.baseVal !== undefined) { 63 | classNames = classNames.baseVal; 64 | } 65 | 66 | // if there are no class names on the element clicked, skip the check 67 | if (classNames || id) { 68 | 69 | // loop through the elements id's and classnames looking for exceptions 70 | for (i = 0; i < l; i++) { 71 | //prepare regex for class word matching 72 | r = new RegExp('\\b' + classList[i] + '\\b'); 73 | 74 | // check for exact matches on id's or classes, but only if they exist in the first place 75 | if ((id !== undefined && r.test(id)) || (classNames && r.test(classNames))) { 76 | // now let's exit out as it is an element that has been defined as being ignored for clicking outside 77 | return; 78 | } 79 | } 80 | } 81 | } 82 | 83 | // if we have got this far, then we are good to go with processing the command passed in via the click-outside attribute 84 | $timeout(function() { 85 | fn = $parse(attr['clickOutside']); 86 | fn($scope, { event: e }); 87 | }); 88 | } 89 | 90 | // if the devices has a touchscreen, listen for this event 91 | if (_hasTouch()) { 92 | $document.on('touchstart', function () { 93 | setTimeout(eventHandler) 94 | }); 95 | } 96 | 97 | // still listen for the click event even if there is touch to cater for touchscreen laptops 98 | $document.on('click', eventHandler); 99 | 100 | // when the scope is destroyed, clean up the documents event handlers as we don't want it hanging around 101 | $scope.$on('$destroy', function() { 102 | if (_hasTouch()) { 103 | $document.off('touchstart', eventHandler); 104 | } 105 | 106 | $document.off('click', eventHandler); 107 | }); 108 | 109 | /** 110 | * @description Private function to attempt to figure out if we are on a touch device 111 | * @private 112 | **/ 113 | function _hasTouch() { 114 | // works on most browsers, IE10/11 and Surface 115 | return 'ontouchstart' in window || navigator.maxTouchPoints; 116 | }; 117 | }); 118 | } 119 | }; 120 | } 121 | })(); 122 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Sublime Text IDE 2 | *.tmlanguage.cache 3 | *.tmPreferences.cache 4 | *.stTheme.cache 5 | *.sublime-workspace 6 | *.sublime-project 7 | sftp-config.json 8 | 9 | # MISC 10 | .DS_Store 11 | Thumbs.db 12 | _notes 13 | /*.lnk 14 | /bower_components 15 | /node_modules 16 | 17 | # Binaries 18 | *.exe 19 | *.app 20 | *.rar 21 | *.zip -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Be sure to run the following command from within this example folder for the example to run 4 | 5 | ``` 6 | bower install angular 7 | ``` -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 'use strict'; 3 | 4 | angular.module('exampleApp', ['angular-click-outside']); -------------------------------------------------------------------------------- /example/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | box-sizing: border-box; 3 | font-family: sans-serif; 4 | font-size: 16px; 5 | } 6 | 7 | #exclusion-zone { 8 | background-color: #009900; 9 | color: #FFF; 10 | } 11 | 12 | #non-exclusion-zone { 13 | background-color: #990000; 14 | color: #FFF; 15 | } 16 | 17 | ul { 18 | width: 130px; 19 | } 20 | 21 | ul li { 22 | margin-bottom: 1em; 23 | } 24 | 25 | .zones { 26 | background-color: #dfdfdf; 27 | padding: 10px; 28 | width: 130px; 29 | } 30 | 31 | .spacer { 32 | display: block; 33 | height: 1.5em; 34 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Example App 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

Open this dropdown menu which has some click outside exclusions applied

17 | 24 |
 
25 |
26 |
 
27 |
28 |
29 | 30 | 33 |
34 |
35 |
 
36 |
37 |
38 | 39 | 42 |
43 |
44 |
 
45 | This link should close the menu before heading off to Google 46 |
 
47 |
Click outsides will be ignored in here
48 |
 
49 | 50 |
 
51 |
This is not excluded and will register as a click outside of the menu and so the drop down menu will close
52 |
 
53 |
This file input should also be excluded
54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /example/js/controllers/app.controller.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | (function() { 3 | 'use strict'; 4 | 5 | angular.module('exampleApp') 6 | .controller('AppController', ['$scope', '$document', AppController]); 7 | 8 | function AppController($scope, $document) { 9 | $scope.closeThis = closeThis; 10 | 11 | $scope.data = { 12 | availableOptions: [{ 13 | id: '1', 14 | name: 'Option A' 15 | }, { 16 | id: '2', 17 | name: 'Option B' 18 | }, { 19 | id: '3', 20 | name: 'Option C' 21 | }], 22 | selectedOption: { 23 | id: '3', 24 | name: 'Option C' 25 | } //This sets the default value of the select in the ui 26 | }; 27 | 28 | function closeThis() { 29 | console.log('clicked outside'); 30 | $scope.showDropdown = false; 31 | } 32 | } 33 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iamadamjowett/angular-click-outside", 3 | "version": "2.11.0", 4 | "main": "clickoutside.directive.js", 5 | "author": { 6 | "name": "Adam Jowett", 7 | "email": "akuma.me@gmail.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:IamAdamJowett/angular-click-outside.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/IamAdamJowett/angular-click-outside/issues" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "grunt": "^0.4.5", 19 | "grunt-bump": "^0.3.2" 20 | } 21 | } 22 | --------------------------------------------------------------------------------