├── .bowerrc ├── .codeclimate.yml ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── .yo-rc.json ├── LICENSE ├── README.md ├── app ├── index.html ├── scripts │ └── app.js └── styles │ └── app.css ├── bower.json ├── dist ├── angular-notification-icons.css ├── angular-notification-icons.js ├── angular-notification-icons.min.css └── angular-notification-icons.min.js ├── gulp ├── build.js ├── inject.js ├── scripts.js ├── server.js ├── styles.js ├── unit-tests.js └── watch.js ├── gulpfile.js ├── karma.conf.js ├── package-lock.json ├── package.json └── src ├── angular-notification-icons.directive.js ├── angular-notification-icons.modules.js ├── less ├── angular-notification-icons.less └── prefixer.less ├── template └── notification-icon.html └── test ├── .jshintrc └── angular-notification-icons.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "json": "bower.json" 4 | } 5 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # Save as .codeclimate.yml (note leading .) in project root directory 2 | languages: 3 | Ruby: true 4 | JavaScript: true 5 | PHP: true 6 | Python: true 7 | # exclude_paths: 8 | - "dist/*" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .sass-cache/ 4 | .tmp/ 5 | npm-debug.log 6 | build/ 7 | 8 | .DS_Store 9 | 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 3 | "browser": true, // Standard browser globals e.g. `window`, `document`. 4 | "eqeqeq": true, // Require triple equals i.e. `===`. 5 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 6 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 7 | "quotmark": "single", // Define quotes to string values. 8 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 9 | "undef": true, // Require all non-global variables be declared before they are used. 10 | "unused": true, // Warn unused variables. 11 | "strict": true, // Require `use strict` pragma in every file. 12 | "trailing": true, // Prohibit trailing whitespaces. 13 | "devel": true, // Allow development statements e.g. `console.log();`. 14 | "noempty": true, // Prohibit use of empty blocks. 15 | "globals": { 16 | "angular": false 17 | } 18 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | - bower_components 6 | install: 7 | - npm install 8 | - node_modules/.bin/bower install 9 | script: npm run ci 10 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-gulp-angular": { 3 | "version": "0.11.0", 4 | "props": { 5 | "angularVersion": "~1.3.4", 6 | "angularModules": [ 7 | { 8 | "key": "animate", 9 | "module": "ngAnimate" 10 | } 11 | ], 12 | "jQuery": { 13 | "key": "none" 14 | }, 15 | "resource": { 16 | "key": "none", 17 | "module": null 18 | }, 19 | "router": { 20 | "key": "none", 21 | "module": null 22 | }, 23 | "ui": { 24 | "key": "none", 25 | "module": null 26 | }, 27 | "cssPreprocessor": { 28 | "key": "less", 29 | "extension": "less" 30 | }, 31 | "jsPreprocessor": { 32 | "key": "none", 33 | "extension": "js", 34 | "srcExtension": "js" 35 | }, 36 | "htmlPreprocessor": { 37 | "key": "none", 38 | "extension": "html" 39 | }, 40 | "bootstrapComponents": { 41 | "name": null, 42 | "version": null, 43 | "key": null, 44 | "module": null 45 | }, 46 | "foundationComponents": { 47 | "name": null, 48 | "version": null, 49 | "key": null, 50 | "module": null 51 | }, 52 | "paths": { 53 | "src": "app", 54 | "dist": "dist", 55 | "e2e": "e2e", 56 | "tmp": ".tmp" 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jacob Meacham 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-notification-icons 2 | Add notification popups to any element http://jemonjam.com/angular-notification-icons 3 | 4 | [![Build Status](https://travis-ci.org/jacob-meacham/angular-notification-icons.svg?branch=develop)](https://travis-ci.org/jacob-meacham/angular-notification-icons) 5 | [![Coverage Status](https://coveralls.io/repos/jacob-meacham/angular-notification-icons/badge.svg?branch=develop)](https://coveralls.io/r/jacob-meacham/angular-notification-icons?branch=develop) 6 | [![Code Climate](https://codeclimate.com/github/jacob-meacham/grunt-lcov-merge/badges/gpa.svg)](https://codeclimate.com/github/jacob-meacham/grunt-lcov-merge) 7 | 8 | ![demo](http://i.imgur.com/F9qc7Uw.gif) 9 | 10 | ```js 11 | 12 | 13 | 14 | ``` 15 | 16 | ## Getting Started 17 | ### 1. Install components 18 | 19 | Bower: 20 | ```shell 21 | bower install angular-notification-icons --save 22 | ``` 23 | 24 | npm: 25 | ```shell 26 | npm install angular-notification-icons --save 27 | ``` 28 | 29 | ### 2. Add css and scripts 30 | Bower: 31 | ```html 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | npm: 40 | ```html 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### 3. Add a dependency to your app 49 | ``` 50 | angular.module('MyApp', ['angular-notification-icons', 'ngAnimate']); // ngAnimate is only required if you want animations 51 | ``` 52 | 53 | ### 4. Add a notification-icon element around any other element 54 | ```html 55 | 56 | ... 57 | 58 | ``` 59 | 60 | angular-notification-icons is an angular directive that adds a notification popup on top of any element. The counter is tied to a scope variable and updating the count is as easy as updating the scope variable. angular-notification-icons comes with a number of canned animations and a default style, but it is easy to add your own styles or custom animations. angular-notification-icons can also optionally listen for DOM events and clear the count on a DOM event. 61 | 62 | angular-notification-icons has been tested with angular 1.3.x, 1.4.x, and 1.5.x. It will probably work with most recent versions of angular and angular-animate. 63 | 64 | ## Demo App 65 | To run the demo app, run `npm install`, `bower install` and then `gulp serve`. 66 | 67 | ## Webpack and ES6 68 | angular-notification-icons can be used in a webpack Angular application. To the top of your main application, add: 69 | ``` 70 | import angular from 'angular' 71 | import 'angular-animate' 72 | import 'angular-notification-icons' 73 | import 'angular-notification-icons/dist/angular-notification-icons.css' 74 | // Can also use less with a less-loader: 75 | // import 'angular-notification-icons/src/less/angular-notification-icons.less' 76 | ``` 77 | 78 | ## Basic Usage 79 | 80 | ### Counter 81 | The only required attribute for angular-notification-icons is 'count'. This uses two-way binding to bind a scope variable to the isolate scope. This makes updating the count very simple, since your controller has full control over how it's set. 82 | ```html 83 | 84 | ... 85 | 86 | ``` 87 | When myScopeVariable is <= 0, the notification icon will not be visible. Once myScopeVariable > 0, the notification will show up. 88 | 89 | [Live Demo](http://jemonjam.com/angular-notification-icons#basic) 90 | 91 | ### Built-in Animations 92 | angular-notification-icons comes with a few prebuilt animations for your use. Note that these are only available if you are using the ngAnimate module 93 | 94 | * bounce 95 | * fade 96 | * grow 97 | * shake 98 | 99 | There are three separate animation events: appear, update, and disappear. Appear is triggered when the counter goes from 0 to non-zero. Update is trigger when the counter increments or decrements but does not go to or from zero. Disappear is triggered when the counter goes from non-zero to zero. The default animation for appear and update is grow, and there is no default set for disappear. A common case is to use the same animation for appear and update, and you can use the 'animation' attribute for this case. 100 | 101 | ```html 102 | 103 | ... 104 | 105 | ``` 106 | This will create a notification that bounces when appearing and when the counter is updated. All three animation events can also be set explicitly: 107 | 108 | ```html 109 | 110 | ... 111 | 112 | ``` 113 | This will create a notification that bounces when appearing, shakes when updates, and fades away when disappearing. Because all of these attributes do not use two-way binding, if you're using a variable for the animation, you'll want to use {{myVariable}} when binding. 114 | 115 | [Live Demo](http://jemonjam.com/angular-notification-icons#animations) 116 | 117 | ### DOM Events 118 | angular-notification-icons can respond to DOM events to clear the counter. This clears the scope variable and runs an $apply. Your controller can $watch the variable if you want to react to clearing the counter. 119 | 120 | ```html 121 | 122 | ... 123 | 124 | ``` 125 | Will cause the count to be cleared upon click. Any DOM event name is valid as a clear-trigger. Because clear-trigger does not use two-way binding, if you're using a variable as the trigger, you'll want to use {{myVariable}} when binding. 126 | 127 | [Live Demo](http://jemonjam.com/angular-notification-icons#dom-events) 128 | 129 | ## Customizing 130 | angular-notification-icons was designed to be very simple to customize so that it fits the feel of your app. 131 | 132 | ### Adding Custom Style 133 | Adding custom style is done via CSS. When the directive is created, it adds a few elements to the DOM 134 | ```html 135 | 136 |
137 |
138 |
139 | 140 |
141 |
142 |
143 |
144 | ``` 145 | 146 | You can add styling at any level. For instance, if you just want to change the look of the notifaction icon, you can add to your app's css: 147 | 148 | ```css 149 | .angular-notification-icons-icon { 150 | left: -10px; 151 | background: yellow; 152 | color: black; 153 | width: 30px; 154 | height: 30px; 155 | font-weight: bolder; 156 | font-size: 1.2em; 157 | } 158 | ``` 159 | Which will make the notification icon appear on the left with a yellow background and bold, larger text. 160 | [Live Demo](http://jemonjam.com/angular-notification-icons#custom-style) 161 | 162 | ### Adding Custom Animations 163 | Adding a custom animation is as simple as adding custom styles. angular-notification-icons uses the standard [angular-animate](https://docs.angularjs.org/guide/animations) module for providing animations. This means that you can use either CSS keyframes or CSS transitions to build animations. 164 | 165 | ```css 166 | .angular-notification-icons-icon.my-custom-animation { 167 | transition:0.5s linear all; 168 | } 169 | 170 | .angular-notification-icons-icon.my-custom-animation-add { 171 | background: black; 172 | color: white; 173 | } 174 | 175 | .angular-notification-icons-icon.my-custom-animation-add-active { 176 | background: yellow; 177 | color: black; 178 | } 179 | 180 | .angular-notification-icons-icon.my-custom-keyframe-animation { 181 | animation: custom_keyframe_animation 0.5s; 182 | } 183 | @keyframes custom_keyframe_animation { 184 | from { 185 | opacity: 0; 186 | } 187 | to { 188 | opacity: 1; 189 | } 190 | } 191 | ``` 192 | 193 | Adding your animation is as simple as specifying it by name on the directive 194 | ```js 195 | 196 | ... 197 | 198 | ``` 199 | [Live Demo](http://jemonjam.com/angular-notification-icons#custom-style) 200 | 201 | ## Advanced Usage 202 | 203 | ### hideCount 204 | If you don't want the count number appear, you can hide the count using the 'hide-count' attribute 205 | ```html 206 | 207 | ... 208 | 209 | ``` 210 | When myCount > 0, the notification icon will be visible, but the number will be hidden. When myCount <= 0, the icon will be hidden as normal. 211 | 212 | [Live Demo](http://jemonjam.com/angular-notification-icons#hide-count) 213 | 214 | ### alwaysShow 215 | If you *always* want the count number to appear, even when 0 or negative, you can add the 'always-show' attribute 216 | ```html 217 | 218 | ... 219 | 220 | ``` 221 | 222 | [Live Demo](http://jemonjam.com/angular-notification-icons#always-show) 223 | 224 | 225 | ### Pill shape 226 | When the number of notifications grows large enough, the icon changes to a pill shape. This is achieved by adding the css class wide-icon to the icon's div. By default, the shape transitions to a pill once the count is greater than or equal to 100, but is configurable via the attribute 'wide-threshold'. 227 | ```html 228 | 229 | ... 230 | 231 | ``` 232 | This will change the shape to a pill once myCount >= 10. 233 | 234 | [Live Demo](http://jemonjam.com/angular-notification-icons#pill) 235 | 236 | ## Helping Out 237 | Pull requests are gladly accepted! Be sure to run `npm run build` to ensure that your PR is lint-free and all the tests pass. 238 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | angularNotifications 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 |
27 | 31 | 32 |
33 |

Getting Started

34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 |
47 |
48 |
49 | 50 |
51 |

Default Animations

52 |
53 |
54 | 55 | 56 | 57 | Grow 58 |
59 | 60 |
61 | 62 | 63 | 64 | Bounce 65 |
66 | 67 |
68 | 69 | 70 | 71 | Shake 72 |
73 | 74 |
75 | 76 | 77 | 78 | Bounce on disappear 79 |
80 |
81 | 82 |
83 | 84 | 85 | 86 |
87 | 88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 |
108 |
109 | 110 |
111 |

Custom style and animations

112 | 113 | 114 | 115 | 116 |
117 | 118 | 119 | 120 |
121 | 122 |
123 |
124 |
125 | JS 126 |
127 | 128 | 129 | 130 |
131 |
132 |
133 | CSS 134 |
135 | .custom-style .angular-notification-icons-icon { 136 | left: -10px; 137 | background: yellow; 138 | color: black; 139 | width: 30px; 140 | height: 30px; 141 | font-weight: bolder; 142 | font-size: 1.2em; 143 | } 144 | 145 | .angular-notification-icons-icon.my-custom-animation { 146 | transition:0.5s linear all; 147 | } 148 | 149 | .angular-notification-icons-icon.my-custom-animation-add { 150 | background: black; 151 | color: white; 152 | } 153 | 154 | .angular-notification-icons-icon.my-custom-animation-add-active { 155 | background: yellow; 156 | color: black; 157 | } 158 |
159 |
160 |
161 |
162 |
163 |
164 | 165 |
166 |

DOM Events

167 |
168 |
169 | 170 | 171 | 172 |
173 |
174 | 175 | 176 | 177 |
178 |
179 | 180 |
181 | 182 |
183 | 184 |
185 |
186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 |
194 |
195 |
196 |
197 | 198 |
199 |

Hidden Counter

200 | 201 | 202 | 203 | 204 |
205 | 206 | 207 | 208 |
209 | 210 |
211 |
212 | 213 | 214 | 215 |
216 |
217 |
218 |
219 | 220 |
221 |

Always Show

222 | 223 | 224 | 225 | 226 |
227 | 228 | 229 | 230 |
231 | 232 |
233 |
234 | 235 | 236 | 237 |
238 |
239 |
240 |
241 | 242 |
243 |

Pill Shape

244 |
245 |
246 | 247 | 248 | 249 |
250 |
251 | 252 |
253 | 254 | 255 | 256 |
257 |
258 |
259 | 260 |
261 |

Usage

262 |

1. Install bower component

263 |
264 | bower install angular-notification-icons --save 265 |
266 | 267 |

2. Add css and script

268 |
269 |
270 | 271 |

3. Add a dependency to your app

272 |
273 | angular.module('MyApp', ['angular-notification-icons']); 274 |
275 | 276 |

4. Add a notification-icon element around any other element

277 |
278 | 279 | ... 280 | 281 |
282 |
283 |
284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = angular.module('angular-notification-icons.demo', ['ngRoute', 'hljs', 'angular-notification-icons', 'ngAnimate', 'angular-notification-icons.demo.controllers']); 4 | 5 | // Pre-define modules 6 | angular.module('angular-notification-icons.demo.controllers', []); 7 | 8 | app.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 9 | $routeProvider. 10 | when('/', { 11 | controller: 'DemoController', 12 | controllerAs: 'vm' 13 | }) 14 | .otherwise({ 15 | redirectTo: '/' 16 | }); 17 | 18 | $locationProvider.html5Mode(true); 19 | }]); 20 | 21 | var DemoController = function($interval) { 22 | var vm = this; 23 | 24 | var autoCounter = function(index, start, steps, delay) { 25 | var numIterations = 0; 26 | vm.autoPending[index] = start; 27 | $interval(function() { 28 | if (++numIterations > steps) { 29 | vm.autoPending[index] = start; 30 | numIterations = 0; 31 | } else { 32 | vm.autoPending[index] = vm.autoPending[index] + 1; 33 | } 34 | }, delay); 35 | }; 36 | 37 | vm.autoPending = [0, 0]; 38 | autoCounter(0, 1, 4, 1000); 39 | autoCounter(1, 98, 4, 1000); 40 | 41 | vm.scriptSrc = '\n\n' + 42 | '\n' + 43 | '\n' + 44 | ''; 45 | }; 46 | 47 | var GifController = function($interval) { 48 | var vm = this; 49 | 50 | vm.autoPending = [0, 0, 0, 0]; 51 | 52 | $interval(function() { 53 | vm.autoPending[0] = 1; 54 | }, 5000, 1); 55 | $interval(function() { 56 | vm.autoPending[1] = 1; 57 | }, 5800, 1); 58 | $interval(function() { 59 | vm.autoPending[2] = 1; 60 | }, 6600, 1); 61 | $interval(function() { 62 | vm.autoPending[3] = 1; 63 | }, 7400, 1); 64 | 65 | $interval(function() { 66 | vm.autoPending[0] = 2; 67 | vm.autoPending[1] = 2; 68 | vm.autoPending[2] = 2; 69 | vm.autoPending[3] = 2; 70 | }, 8400, 1); 71 | }; 72 | 73 | 74 | angular.module('angular-notification-icons.demo.controllers') 75 | .controller('DemoController', ['$interval', DemoController]) 76 | .controller('GifController', ['$interval', GifController]); -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | notification-icon { 2 | padding: 10px; 3 | } 4 | 5 | .page-header { 6 | padding-bottom: 9px; 7 | margin: 40px 0 20px; 8 | border-bottom: 1px solid #eee; 9 | } 10 | 11 | .page-header h1 { 12 | font-size: 48px; 13 | margin-bottom: 5px; 14 | } 15 | 16 | .page-header a { 17 | font-weight: bold; 18 | } 19 | 20 | .custom-style .angular-notifications-icon { 21 | left: -10px; 22 | background: yellow; 23 | color: black; 24 | width: 30px; 25 | height: 30px; 26 | font-weight: bolder; 27 | font-size: 1.2em; 28 | } 29 | 30 | .animations .row { 31 | text-align: center; 32 | } 33 | 34 | .animations notification-icon { 35 | display: table; 36 | margin: 0 auto; 37 | } 38 | 39 | .buttons { 40 | padding-top: 20px; 41 | } 42 | 43 | .code { 44 | padding-top: 30px; 45 | padding-bottom: 10px; 46 | } 47 | 48 | .angular-notifications-icon.my-custom-animation { 49 | transition:0.5s linear all; 50 | } 51 | 52 | .angular-notifications-icon.my-custom-animation-add { 53 | background: black; 54 | color: white; 55 | } 56 | 57 | .angular-notifications-icon.my-custom-animation-add-active { 58 | background: yellow; 59 | color: black; 60 | } 61 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-notification-icons", 3 | "description": "angular directive to add notification popups to any element", 4 | "authors": [{ "name": "jemonjam", "homepage": "http://jemonjam.com"}], 5 | "keywords": ["angular", "angular-plugin", "directive", "notification"], 6 | "license": "MIT", 7 | "main": [ 8 | "dist/angular-notification-icons.js", 9 | "dist/angular-notification-icons.css" 10 | ], 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules/", 14 | "bower_components/", 15 | "gulp/", 16 | "app/", 17 | "src/*.js", 18 | "src/test/", 19 | "src/template/", 20 | "karma.conf.js", 21 | "gulpfile.js", 22 | "package.json" 23 | ], 24 | 25 | "dependencies": { 26 | "angular": ">1.3.0" 27 | }, 28 | 29 | "devDependencies": { 30 | "angular-mocks": "1.4.7", 31 | "angular-route": "1.4.7", 32 | "angular-animate": "1.4.7", 33 | "jquery": "2.1.3", 34 | "bootstrap": "4.0.0", 35 | "angular-bootstrap": "0.12.1", 36 | "fontawesome": "~4.3.0", 37 | "angular-highlightjs": "0.4.1" 38 | }, 39 | "resolutions": { 40 | "angular": "~1.4.7" 41 | }, 42 | "moduleType": [ 43 | "amd", 44 | "globals", 45 | "node" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /dist/angular-notification-icons.css: -------------------------------------------------------------------------------- 1 | .angular-notifications-container { 2 | position: relative; 3 | display: table-cell; 4 | width: 100%; 5 | } 6 | .angular-notifications-icon.overlay { 7 | position: absolute; 8 | z-index: 1; 9 | } 10 | .angular-notifications-icon { 11 | right: -10px; 12 | top: -10px; 13 | border-radius: 50%; 14 | text-align: center; 15 | padding-left: 5px; 16 | padding-right: 5px; 17 | padding-top: 3px; 18 | min-width: 25px; 19 | height: 25px; 20 | line-height: 1.15; 21 | background: red; 22 | color: white; 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | -ms-user-select: none; 26 | user-select: none; 27 | cursor: default; 28 | } 29 | .angular-notifications-icon.wide-icon { 30 | border-radius: 30% / 50%; 31 | background-clip: padding-box; 32 | } 33 | .angular-notifications-icon.fade { 34 | animation: notification-fade-in 0.5s; 35 | } 36 | .angular-notifications-icon.bounce { 37 | animation: notification-bounce 0.5s; 38 | } 39 | .angular-notifications-icon.grow { 40 | animation: notification-grow 0.5s; 41 | } 42 | .angular-notifications-icon.shake { 43 | animation: notification-shake 0.5s; 44 | } 45 | @keyframes notification-fade-in { 46 | from { 47 | opacity: 0; 48 | } 49 | to { 50 | opacity: 1; 51 | } 52 | } 53 | @keyframes notification-bounce { 54 | 30% { 55 | transform: perspective(1px) translate(0, -8px); 56 | } 57 | 60% { 58 | transform: perspective(1px) translate(0, 0); 59 | } 60 | 80% { 61 | transform: perspective(1px) translate(0, -5px); 62 | } 63 | to { 64 | transform: perspective(1px) translate(0, 0); 65 | } 66 | } 67 | @keyframes notification-grow { 68 | 30% { 69 | transform: scale(1.2); 70 | } 71 | 60% { 72 | transform: scale(0.8); 73 | } 74 | to { 75 | transform: scale(1); 76 | } 77 | } 78 | @keyframes notification-shake { 79 | 20% { 80 | transform: rotate(20deg); 81 | } 82 | 40% { 83 | transform: rotate(-20deg); 84 | } 85 | 60% { 86 | transform: rotate(10deg); 87 | } 88 | 80% { 89 | transform: rotate(-10deg); 90 | } 91 | to { 92 | transform: rotate(0); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /dist/angular-notification-icons.js: -------------------------------------------------------------------------------- 1 | ;(function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['angular'], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(require('angular')); 6 | } else { 7 | root.returnExports = factory(root.angular); 8 | } 9 | }(this, function(angular) { 10 | 'use strict'; 11 | 12 | angular.module('angular-notification-icons', ['angular-notification-icons.tpls']); 13 | angular.module('angular-notification-icons.tpls', []); 14 | 15 | angular.module("angular-notification-icons.tpls").run(["$templateCache", function($templateCache) {$templateCache.put("template/notification-icon.html","
\n
{{notification.count}}
\n
\n \n
\n
");}]); 16 | /* global angular */ 17 | 18 | 'use strict'; 19 | 20 | var NotificationDirectiveController = function($scope, $animate, $q) { 21 | var self = this; 22 | self.visible = false; 23 | self.wideThreshold = self.wideThreshold || 100; 24 | self.alwaysShow = self.alwaysShow || false; 25 | 26 | var animationPromise; 27 | var animationSet = { 28 | appear: self.appearAnimation || self.animation || 'grow', 29 | update: self.updateAnimation || self.animation || 'grow', 30 | disappear: self.disappearAnimation 31 | }; 32 | 33 | self.getElement = function(element) { 34 | return angular.element(element[0].querySelector('.angular-notifications-icon')); 35 | }; 36 | 37 | self.init = function(element) { 38 | self.$element = self.getElement(element); 39 | if (self.clearTrigger) { 40 | element.on(self.clearTrigger, function() { 41 | self.count = 0; 42 | $scope.$apply(); 43 | }); 44 | } 45 | }; 46 | 47 | var handleAnimation = function(animationClass) { 48 | if (animationClass) { 49 | if (animationPromise) { 50 | $animate.cancel(animationPromise); 51 | } 52 | 53 | // Can't chain because the chained promise doesn't have a cancel function. 54 | animationPromise = $animate.addClass(self.$element, animationClass); 55 | animationPromise.then(function() { 56 | self.$element.removeClass(animationClass); 57 | return $q.when(true); 58 | }); 59 | 60 | return animationPromise; 61 | } 62 | 63 | return $q.when(false); 64 | }; 65 | 66 | var appear = function() { 67 | self.visible = true; 68 | handleAnimation(animationSet.appear); 69 | }; 70 | 71 | var clear = function() { 72 | handleAnimation(animationSet.disappear).then(function(needsDigest) { 73 | self.visible = false; 74 | if (needsDigest) { 75 | $scope.$apply(); 76 | } 77 | }); 78 | }; 79 | 80 | var update = function() { 81 | handleAnimation(animationSet.update); 82 | }; 83 | 84 | $scope.$watch(function() { return self.count; }, function() { 85 | if (self.visible === false && (self.alwaysShow || self.count > 0)) { 86 | appear(); 87 | } else if (!self.alwaysShow && self.visible === true && self.count <= 0) { 88 | // Only clear if we're not always showing 89 | clear(); 90 | } else { 91 | update(); 92 | } 93 | 94 | // Use more of a pill shape if the count is high enough. 95 | if (Math.abs(self.count) >= self.wideThreshold) { 96 | self.$element.addClass('wide-icon'); 97 | } else { 98 | self.$element.removeClass('wide-icon'); 99 | } 100 | }); 101 | }; 102 | 103 | var notificationDirective = function() { 104 | return { 105 | restrict: 'EA', 106 | scope: { 107 | count: '=', 108 | hideCount: '@', 109 | alwaysShow: '@', 110 | animation: '@', 111 | appearAnimation: '@', 112 | disappearAnimation: '@', 113 | updateAnimation: '@', 114 | clearTrigger: '@', 115 | wideThreshold: '@' 116 | }, 117 | controller: 'NotificationDirectiveController', 118 | controllerAs: 'notification', 119 | bindToController: true, 120 | transclude: true, 121 | templateUrl: 'template/notification-icon.html', 122 | link: function(scope, element, attrs, ctrl) { 123 | ctrl.init(element); 124 | } 125 | }; 126 | }; 127 | 128 | angular.module('angular-notification-icons') 129 | .controller('NotificationDirectiveController', ['$scope', '$animate', '$q', NotificationDirectiveController]) 130 | .directive('notificationIcon', notificationDirective); 131 | 132 | return { NotificationDirectiveController: NotificationDirectiveController, notificationDirective: notificationDirective }; 133 | })); 134 | -------------------------------------------------------------------------------- /dist/angular-notification-icons.min.css: -------------------------------------------------------------------------------- 1 | .angular-notifications-container{position:relative;display:table-cell;width:100%}.angular-notifications-icon.overlay{position:absolute;z-index:1}.angular-notifications-icon{right:-10px;top:-10px;border-radius:50%;text-align:center;padding-left:5px;padding-right:5px;padding-top:3px;min-width:25px;height:25px;line-height:1.15;background:red;color:#fff;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.angular-notifications-icon.wide-icon{border-radius:30%/50%;background-clip:padding-box}.angular-notifications-icon.fade{animation:notification-fade-in .5s}.angular-notifications-icon.bounce{animation:notification-bounce .5s}.angular-notifications-icon.grow{animation:notification-grow .5s}.angular-notifications-icon.shake{animation:notification-shake .5s}@keyframes notification-fade-in{0%{opacity:0}to{opacity:1}}@keyframes notification-bounce{30%{transform:perspective(1px) translate(0,-8px)}60%,to{transform:perspective(1px) translate(0,0)}80%{transform:perspective(1px) translate(0,-5px)}}@keyframes notification-grow{30%{transform:scale(1.2)}60%{transform:scale(.8)}to{transform:scale(1)}}@keyframes notification-shake{20%{transform:rotate(20deg)}40%{transform:rotate(-20deg)}60%{transform:rotate(10deg)}80%{transform:rotate(-10deg)}to{transform:rotate(0)}} -------------------------------------------------------------------------------- /dist/angular-notification-icons.min.js: -------------------------------------------------------------------------------- 1 | !function(n,i){"function"==typeof define&&define.amd?define(["angular"],i):"object"==typeof exports?module.exports=i(require("angular")):n.returnExports=i(n.angular)}(this,function(n){"use strict";n.module("angular-notification-icons",["angular-notification-icons.tpls"]),n.module("angular-notification-icons.tpls",[]),n.module("angular-notification-icons.tpls").run(["$templateCache",function(n){n.put("template/notification-icon.html",'
\n
{{notification.count}}
\n
\n \n
\n
')}]);var i=function(i,t,e){var o=this;o.visible=!1,o.wideThreshold=o.wideThreshold||100,o.alwaysShow=o.alwaysShow||!1;var a,r={appear:o.appearAnimation||o.animation||"grow",update:o.updateAnimation||o.animation||"grow",disappear:o.disappearAnimation};o.getElement=function(i){return n.element(i[0].querySelector(".angular-notifications-icon"))},o.init=function(n){o.$element=o.getElement(n),o.clearTrigger&&n.on(o.clearTrigger,function(){o.count=0,i.$apply()})};var c=function(n){return n?(a&&t.cancel(a),a=t.addClass(o.$element,n),a.then(function(){return o.$element.removeClass(n),e.when(!0)}),a):e.when(!1)},l=function(){o.visible=!0,c(r.appear)},u=function(){c(r.disappear).then(function(n){o.visible=!1,n&&i.$apply()})},s=function(){c(r.update)};i.$watch(function(){return o.count},function(){o.visible===!1&&(o.alwaysShow||o.count>0)?l():!o.alwaysShow&&o.visible===!0&&o.count<=0?u():s(),Math.abs(o.count)>=o.wideThreshold?o.$element.addClass("wide-icon"):o.$element.removeClass("wide-icon")})},t=function(){return{restrict:"EA",scope:{count:"=",hideCount:"@",alwaysShow:"@",animation:"@",appearAnimation:"@",disappearAnimation:"@",updateAnimation:"@",clearTrigger:"@",wideThreshold:"@"},controller:"NotificationDirectiveController",controllerAs:"notification",bindToController:!0,transclude:!0,templateUrl:"template/notification-icon.html",link:function(n,i,t,e){e.init(i)}}};return n.module("angular-notification-icons").controller("NotificationDirectiveController",["$scope","$animate","$q",i]).directive("notificationIcon",t),{NotificationDirectiveController:i,notificationDirective:t}}); -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | // TODO: why no templateCache in $? 6 | var $ = require('gulp-load-plugins')({ 7 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 8 | }); 9 | var templateCache = require('gulp-angular-templatecache'); 10 | 11 | module.exports = function(options) { 12 | gulp.task('clean', function (done) { 13 | $.del([options.dist + '/', options.tmp + '/'], done); 14 | }); 15 | 16 | // TODO: Combine all of these pipes 17 | gulp.task('copy:templates', function() { 18 | return gulp.src(options.src + '/**/*.html') 19 | .pipe(templateCache({module: 'angular-notification-icons.tpls'})) 20 | .pipe(gulp.dest(options.tmp + '/templateCache')); 21 | }); 22 | 23 | gulp.task('build', ['scripts:jshint', 'copy:templates'], function() { 24 | gulp.src([options.src + '/**/*.js', options.tmp + '/templateCache/*.js', '!' + options.src + '/**/*.spec.js']) 25 | .pipe($.angularFilesort()).on('error', options.errorHandler('AngularFilesort')) 26 | .pipe($.concat('angular-notification-icons.js')) 27 | .pipe($.umd({ 28 | dependencies: function(file) { 29 | return [ 30 | { 31 | name: 'angular' 32 | } 33 | ] 34 | }, 35 | exports: function(file) { 36 | return '{ NotificationDirectiveController: NotificationDirectiveController, notificationDirective: notificationDirective }' 37 | }, 38 | namespace: function(file) { 39 | return 'returnExports' 40 | } 41 | })) 42 | .pipe(gulp.dest(options.dist)) 43 | .pipe($.uglify()) 44 | .pipe($.rename('angular-notification-icons.min.js')) 45 | .pipe(gulp.dest(options.dist)); 46 | 47 | return gulp.src(options.src + '/less/angular-notification-icons.less') 48 | .pipe($.less()).on('error', options.errorHandler('Less')) 49 | .pipe($.autoprefixer()).on('error', options.errorHandler('Autoprefixer')) 50 | .pipe($.rename('angular-notification-icons.css')) 51 | .pipe(gulp.dest(options.dist)) 52 | .pipe($.csso()) 53 | .pipe($.rename('angular-notification-icons.min.css')) 54 | .pipe(gulp.dest(options.dist)); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | var wiredep = require('wiredep').stream; 8 | 9 | module.exports = function(options) { 10 | var inject = function(srcDir, _injectOptions) { 11 | return function() { 12 | if (!_injectOptions) { 13 | _injectOptions = {}; 14 | } 15 | 16 | var injectOptions = { 17 | ignorePath: [options.tmp + '/serve'], 18 | addRootSlash: false 19 | }; 20 | injectOptions.srcStyles = _injectOptions.srcStyles || options.tmp + '/serve/**/*.css'; 21 | injectOptions.srcJs = _injectOptions.srcJs || srcDir + '/**/*.js'; 22 | 23 | var injectStyles = gulp.src(injectOptions.srcStyles, { read: false }); 24 | 25 | var injectScripts = gulp.src([ 26 | options.app + '/**/*.js', 27 | injectOptions.srcJs, 28 | '!' + srcDir + '/**/*.spec.js' 29 | ]) 30 | .pipe($.angularFilesort()).on('error', options.errorHandler('AngularFilesort')); 31 | 32 | return gulp.src(options.app + '/*.html') 33 | .pipe($.inject(injectStyles, injectOptions)) 34 | .pipe($.inject(injectScripts, injectOptions)) 35 | .pipe(wiredep(options.wiredep)) 36 | .pipe(gulp.dest(options.tmp + '/serve')); 37 | }; 38 | }; 39 | gulp.task('inject', ['scripts', 'styles'], inject(options.src)); 40 | gulp.task('inject:dist', inject(options.dist, {srcStyles: options.dist + '/angular-notification-icons.css', srcJs: options.dist + '/angular-notification-icons.js'})); 41 | gulp.task('inject:dist:min', inject(options.dist, {srcStyles: options.dist + '/angular-notification-icons.min.css', srcJs: options.dist + '/angular-notification-icons.min.js'})); 42 | }; 43 | -------------------------------------------------------------------------------- /gulp/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var browserSync = require('browser-sync'); 5 | 6 | var $ = require('gulp-load-plugins')(); 7 | 8 | module.exports = function(options) { 9 | gulp.task('scripts:jshint', function() { 10 | return gulp.src([options.src + '/**/*.js', options.app + '/**/*.js']) 11 | .pipe($.jshint()) 12 | .pipe($.jshint.reporter('jshint-stylish')) 13 | .pipe($.jshint.reporter('fail')); 14 | }); 15 | 16 | gulp.task('scripts', ['scripts:jshint'], function () { 17 | return gulp.src([options.src + '/**/*.js', options.app + '/**/*.js']) 18 | .pipe(browserSync.reload({ stream: true })) 19 | .pipe($.size()); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var browserSync = require('browser-sync'); 5 | var browserSyncSpa = require('browser-sync-spa'); 6 | 7 | var util = require('util'); 8 | 9 | module.exports = function(options) { 10 | 11 | function browserSyncInit(baseDir, browser) { 12 | browser = browser === undefined ? 'default' : browser; 13 | 14 | var routes = null; 15 | if(baseDir === options.app || (util.isArray(baseDir) && baseDir.indexOf(options.app) !== -1)) { 16 | routes = { 17 | '/bower_components': 'bower_components', 18 | '/app': 'app', 19 | '/src': options.src, 20 | '/dist': options.dist, 21 | '/template': options.src + '/template' 22 | }; 23 | } 24 | 25 | var server = { 26 | baseDir: baseDir, 27 | routes: routes 28 | }; 29 | 30 | browserSync.instance = browserSync.init({ 31 | startPath: '/', 32 | server: server, 33 | browser: browser 34 | }); 35 | } 36 | 37 | browserSync.use(browserSyncSpa({ 38 | selector: '[ng-app]'// Only needed for angular apps 39 | })); 40 | 41 | gulp.task('serve', ['watch'], function () { 42 | browserSyncInit([options.tmp + '/serve', options.app]); 43 | }); 44 | 45 | gulp.task('serve:dist', ['build', 'watch:dist'], function () { 46 | browserSyncInit([options.tmp + '/serve', options.app]); 47 | }); 48 | 49 | gulp.task('serve:dist:min', ['build', 'watch:dist:min'], function () { 50 | browserSyncInit([options.tmp + '/serve', options.app]); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /gulp/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var browserSync = require('browser-sync'); 5 | 6 | var $ = require('gulp-load-plugins')(); 7 | 8 | module.exports = function(options) { 9 | gulp.task('styles', function () { 10 | var lessOptions = { 11 | options: [ 12 | 'bower_components', 13 | options.src 14 | ] 15 | }; 16 | 17 | return gulp.src(options.src + '/**/*.less') 18 | .pipe($.sourcemaps.init()) 19 | .pipe($.less(lessOptions)).on('error', options.errorHandler('Less')) 20 | .pipe($.autoprefixer()).on('error', options.errorHandler('Autoprefixer')) 21 | .pipe($.sourcemaps.write()) 22 | .pipe(gulp.dest(options.tmp + '/serve/')) 23 | .pipe(browserSync.reload({ stream: true })); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /gulp/unit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var wiredep = require('wiredep'); 6 | var karma = require('karma'); 7 | var concat = require('concat-stream'); 8 | var _ = require('lodash'); 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | module.exports = function(options) { 12 | function listFiles(callback) { 13 | var wiredepOptions = _.extend({}, options.wiredep, { 14 | dependencies: true, 15 | devDependencies: true 16 | }); 17 | var bowerDeps = wiredep(wiredepOptions); 18 | 19 | var specFiles = [ 20 | options.src + '/**/*.spec.js' 21 | ]; 22 | 23 | var htmlFiles = [ 24 | options.src + '/**/*.html', 25 | ]; 26 | 27 | var srcFiles = [ 28 | options.src + '/**/*.js' 29 | ].concat(specFiles.map(function(file) { 30 | return '!' + file; 31 | })); 32 | 33 | 34 | gulp.src(srcFiles) 35 | .pipe($.angularFilesort()).on('error', options.errorHandler('AngularFilesort')) 36 | .pipe(concat(function(files) { 37 | callback(bowerDeps.js 38 | .concat(_.pluck(files, 'path')) 39 | .concat(htmlFiles) 40 | .concat(specFiles)); 41 | })); 42 | } 43 | 44 | function runTests (singleRun, done) { 45 | listFiles(function(files) { 46 | karma.server.start({ 47 | configFile: __dirname + '/../karma.conf.js', 48 | files: files, 49 | singleRun: singleRun, 50 | autoWatch: !singleRun 51 | }, done); 52 | }); 53 | } 54 | 55 | gulp.task('test', ['scripts'], function(done) { 56 | runTests(true, done); 57 | }); 58 | gulp.task('test:auto', ['watch'], function(done) { 59 | runTests(false, done); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var browserSync = require('browser-sync'); 5 | 6 | function isOnlyChange(event) { 7 | return event.type === 'changed'; 8 | } 9 | 10 | module.exports = function(options) { 11 | var watch = function(srcDir, watchOptions) { 12 | return function() { 13 | if (!watchOptions) { 14 | watchOptions = {}; 15 | } 16 | 17 | watchOptions.watchSrcLess = watchOptions.watchSrcLess || true; 18 | watchOptions.watchSrcHtml = watchOptions.watchSrcHtml || true; 19 | watchOptions.srcJs = watchOptions.srcJs || '/**/*.js'; 20 | 21 | gulp.watch([options.app + '/*.html', 'bower.json'], ['inject']); 22 | 23 | if (watchOptions.watchSrcLess) { 24 | gulp.watch([ 25 | srcDir + '/**/*.less' 26 | ], function(event) { 27 | if(isOnlyChange(event)) { 28 | gulp.start('styles'); 29 | } else { 30 | gulp.start('inject'); 31 | } 32 | }); 33 | } 34 | 35 | gulp.watch([srcDir + watchOptions.srcJs, options.app + '/**/*.js'], function(event) { 36 | if(isOnlyChange(event)) { 37 | gulp.start('scripts'); 38 | } else { 39 | gulp.start('inject'); 40 | } 41 | }); 42 | 43 | var htmlToWatch = [options.app + '/*.html']; 44 | if (watchOptions.watchSrcHtml) { 45 | htmlToWatch.push(srcDir + '/**/*.html'); 46 | } 47 | 48 | return gulp.watch(htmlToWatch, function(event) { 49 | browserSync.reload(event.path); 50 | }); 51 | }; 52 | }; 53 | 54 | gulp.task('watch', ['inject'], watch(options.src)); 55 | gulp.task('watch:dist', ['inject:dist'], watch(options.dist, {watchSrcLess: false, watchSrcHtml: false, srcJs: 'angular-notification-icons.js'})); 56 | gulp.task('watch:dist:min', ['inject:dist:min'], watch(options.dist, {watchSrcLess: false, watchSrcHtml: false, srcJs: 'angular-notification-icons.min.js'})); 57 | }; 58 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var gutil = require('gulp-util'); 5 | var wrench = require('wrench'); 6 | var coveralls = require('gulp-coveralls'); 7 | 8 | var options = { 9 | app: 'app', 10 | src: 'src', 11 | dist: 'dist', 12 | tmp: '.tmp', 13 | root: '.', 14 | errorHandler: function(title) { 15 | return function(err) { 16 | gutil.log(gutil.colors.red('[' + title + ']'), err.toString()); 17 | this.emit('end'); 18 | }; 19 | }, 20 | wiredep: { 21 | directory: 'bower_components', 22 | devDependencies: true 23 | } 24 | }; 25 | 26 | wrench.readdirSyncRecursive('./gulp').filter(function(file) { 27 | return (/\.js$/i).test(file); 28 | }).map(function(file) { 29 | require('./gulp/' + file)(options); 30 | }); 31 | 32 | gulp.task('default', ['clean'], function() { 33 | gulp.start('build'); 34 | }); 35 | 36 | gulp.task('ci', ['build', 'test'], function() { 37 | return gulp.src('build/**/lcov.info') 38 | .pipe(coveralls()); 39 | }); 40 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(config) { 4 | 5 | var configuration = { 6 | autoWatch : false, 7 | colors: true, 8 | 9 | frameworks: ['mocha', 'chai', 'sinon', 'sinon-chai'], 10 | 11 | browsers : ['PhantomJS'], 12 | 13 | reporters: ['mocha', 'coverage'], 14 | 15 | preprocessors: { 16 | 'src/**/*.js' : 'coverage', 17 | 'src/**/*.html': ['ng-html2js'] 18 | }, 19 | 20 | coverageReporter: { 21 | type: 'lcov', 22 | dir: 'build/coverage/client' 23 | }, 24 | 25 | ngHtml2JsPreprocessor: { 26 | stripPrefix: 'src/', 27 | moduleName: 'angular-notifications.tpls' 28 | }, 29 | }; 30 | 31 | config.set(configuration); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-notification-icons", 3 | "version": "1.0.0", 4 | "description": "angular directive to add notification popups to any element", 5 | "author": { 6 | "name": "jemonjam", 7 | "url": "http://jemonjam.com" 8 | }, 9 | "license": "MIT", 10 | "main": "dist/angular-notification-icons.js", 11 | "files": [ 12 | "dist", 13 | "src", 14 | "LICENSE" 15 | ], 16 | "scripts": { 17 | "start": "gulp", 18 | "run": "gulp serve", 19 | "test": "gulp test", 20 | "build": "gulp build", 21 | "ci": "gulp ci" 22 | }, 23 | "bugs": "https://github.com/jacob-meacham/angular-notification-icons/issues", 24 | "repository": "https://github.com/jacob-meacham/angular-notification-icons", 25 | "keywords": [ 26 | "angular", 27 | "angular-plugin", 28 | "directive", 29 | "notification" 30 | ], 31 | "peerDependencies": { 32 | "angular": ">1.3.0" 33 | }, 34 | "engines": { 35 | "node" : "<=5.4.0" 36 | }, 37 | "devDependencies": { 38 | "bower": "^1.7.9", 39 | "browser-sync": "~2.1.4", 40 | "browser-sync-spa": "~1.0.2", 41 | "chai": "^3.5.0", 42 | "chalk": "~0.5.1", 43 | "concat-stream": "~1.4.7", 44 | "del": "~1.1.1", 45 | "gulp": "~3.8.10", 46 | "gulp-angular-filesort": "~1.0.4", 47 | "gulp-angular-templatecache": "^1.6.0", 48 | "gulp-autoprefixer": "~2.1.0", 49 | "gulp-concat": "^2.5.2", 50 | "gulp-coveralls": "^0.1.4", 51 | "gulp-csso": "~1.0.0", 52 | "gulp-filter": "~2.0.2", 53 | "gulp-flatten": "~0.0.4", 54 | "gulp-inject": "~1.1.1", 55 | "gulp-jshint": "^1.10.0", 56 | "gulp-less": "~3.0.0", 57 | "gulp-load-plugins": "~0.8.0", 58 | "gulp-minify-html": "~0.1.7", 59 | "gulp-ng-annotate": "~0.5.2", 60 | "gulp-protractor": "~0.0.12", 61 | "gulp-rename": "~1.2.0", 62 | "gulp-replace": "~0.5.0", 63 | "gulp-rev": "~3.0.1", 64 | "gulp-rev-replace": "~0.3.1", 65 | "gulp-size": "~1.2.0", 66 | "gulp-sourcemaps": "~1.3.0", 67 | "gulp-uglify": "~1.1.0", 68 | "gulp-umd": "^0.2.0", 69 | "gulp-useref": "~1.1.0", 70 | "gulp-util": "~3.0.2", 71 | "http-proxy": "~1.8.0", 72 | "jshint-stylish": "^1.0.2", 73 | "karma": "~0.12.31", 74 | "karma-chai": "0.1.0", 75 | "karma-chrome-launcher": "0.1.8", 76 | "karma-coverage": "0.3.1", 77 | "karma-mocha": "0.1.10", 78 | "karma-mocha-reporter": "1.0.2", 79 | "karma-ng-html2js-preprocessor": "0.1.2", 80 | "karma-phantomjs-launcher": "0.1.4", 81 | "karma-sinon": "1.0.4", 82 | "karma-sinon-chai": "0.3.0", 83 | "lodash": "~3.2.0", 84 | "main-bower-files": "~2.5.0", 85 | "merge-stream": "~0.1.7", 86 | "mocha": "^3.0.2", 87 | "protractor": "~1.7.0", 88 | "require-dir": "~0.1.0", 89 | "sinon": "^1.17.5", 90 | "uglify-save-license": "~0.4.1", 91 | "wiredep": "~2.2.0", 92 | "wrench": "~1.5.8" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/angular-notification-icons.directive.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | 3 | 'use strict'; 4 | 5 | var NotificationDirectiveController = function($scope, $animate, $q) { 6 | var self = this; 7 | self.visible = false; 8 | self.wideThreshold = self.wideThreshold || 100; 9 | self.alwaysShow = self.alwaysShow || false; 10 | 11 | var animationPromise; 12 | var animationSet = { 13 | appear: self.appearAnimation || self.animation || 'grow', 14 | update: self.updateAnimation || self.animation || 'grow', 15 | disappear: self.disappearAnimation 16 | }; 17 | 18 | self.getElement = function(element) { 19 | return angular.element(element[0].querySelector('.angular-notifications-icon')); 20 | }; 21 | 22 | self.init = function(element) { 23 | self.$element = self.getElement(element); 24 | if (self.clearTrigger) { 25 | element.on(self.clearTrigger, function() { 26 | self.count = 0; 27 | $scope.$apply(); 28 | }); 29 | } 30 | }; 31 | 32 | var handleAnimation = function(animationClass) { 33 | if (animationClass) { 34 | if (animationPromise) { 35 | $animate.cancel(animationPromise); 36 | } 37 | 38 | // Can't chain because the chained promise doesn't have a cancel function. 39 | animationPromise = $animate.addClass(self.$element, animationClass); 40 | animationPromise.then(function() { 41 | self.$element.removeClass(animationClass); 42 | return $q.when(true); 43 | }); 44 | 45 | return animationPromise; 46 | } 47 | 48 | return $q.when(false); 49 | }; 50 | 51 | var appear = function() { 52 | self.visible = true; 53 | handleAnimation(animationSet.appear); 54 | }; 55 | 56 | var clear = function() { 57 | handleAnimation(animationSet.disappear).then(function(needsDigest) { 58 | self.visible = false; 59 | if (needsDigest) { 60 | $scope.$apply(); 61 | } 62 | }); 63 | }; 64 | 65 | var update = function() { 66 | handleAnimation(animationSet.update); 67 | }; 68 | 69 | $scope.$watch(function() { return self.count; }, function() { 70 | if (self.visible === false && (self.alwaysShow || self.count > 0)) { 71 | appear(); 72 | } else if (!self.alwaysShow && self.visible === true && self.count <= 0) { 73 | // Only clear if we're not always showing 74 | clear(); 75 | } else { 76 | update(); 77 | } 78 | 79 | // Use more of a pill shape if the count is high enough. 80 | if (Math.abs(self.count) >= self.wideThreshold) { 81 | self.$element.addClass('wide-icon'); 82 | } else { 83 | self.$element.removeClass('wide-icon'); 84 | } 85 | }); 86 | }; 87 | 88 | var notificationDirective = function() { 89 | return { 90 | restrict: 'EA', 91 | scope: { 92 | count: '=', 93 | hideCount: '@', 94 | alwaysShow: '@', 95 | animation: '@', 96 | appearAnimation: '@', 97 | disappearAnimation: '@', 98 | updateAnimation: '@', 99 | clearTrigger: '@', 100 | wideThreshold: '@' 101 | }, 102 | controller: 'NotificationDirectiveController', 103 | controllerAs: 'notification', 104 | bindToController: true, 105 | transclude: true, 106 | templateUrl: 'template/notification-icon.html', 107 | link: function(scope, element, attrs, ctrl) { 108 | ctrl.init(element); 109 | } 110 | }; 111 | }; 112 | 113 | angular.module('angular-notification-icons') 114 | .controller('NotificationDirectiveController', ['$scope', '$animate', '$q', NotificationDirectiveController]) 115 | .directive('notificationIcon', notificationDirective); 116 | -------------------------------------------------------------------------------- /src/angular-notification-icons.modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('angular-notification-icons', ['angular-notification-icons.tpls']); 4 | angular.module('angular-notification-icons.tpls', []); 5 | -------------------------------------------------------------------------------- /src/less/angular-notification-icons.less: -------------------------------------------------------------------------------- 1 | @import "./prefixer.less"; 2 | 3 | .angular-notifications-container { 4 | position: relative; 5 | display: table-cell; 6 | width: 100%; 7 | } 8 | 9 | .angular-notifications-icon.overlay { 10 | position: absolute; 11 | z-index: 1; 12 | } 13 | 14 | .angular-notifications-icon { 15 | right: -10px; 16 | top: -10px; 17 | border-radius: 50%; 18 | text-align: center; 19 | padding-left: 5px; 20 | padding-right: 5px; 21 | padding-top: 3px; 22 | min-width: 25px; 23 | height: 25px; 24 | line-height: 1.15; 25 | background: red; 26 | color: white; 27 | user-select: none; 28 | cursor: default; 29 | } 30 | 31 | .angular-notifications-icon.wide-icon { 32 | .border-radius(~"30% / 50%"); 33 | } 34 | 35 | .angular-notifications-icon.fade { 36 | .animation(notification-fade-in 0.5s); 37 | } 38 | 39 | .angular-notifications-icon.bounce { 40 | .animation(notification-bounce 0.5s); 41 | } 42 | 43 | .angular-notifications-icon.grow { 44 | .animation(notification-grow 0.5s); 45 | } 46 | 47 | .angular-notifications-icon.shake { 48 | .animation(notification-shake 0.5s); 49 | } 50 | 51 | @-webkit-keyframes notification-fade-in {.fade-frames;} 52 | @-moz-keyframes notification-fade-in {.fade-frames;} 53 | @-ms-keyframes notification-fade-in {.fade-frames;} 54 | @-o-keyframes notification-fade-in {.fade-frames;} 55 | @keyframes notification-fade-in {.fade-frames;} 56 | .fade-frames () { 57 | from { 58 | opacity: 0; 59 | } 60 | 61 | to { 62 | opacity: 1; 63 | } 64 | } 65 | 66 | @-webkit-keyframes notification-bounce {.bounce-frames;} 67 | @-moz-keyframes notification-bounce {.bounce-frames;} 68 | @-ms-keyframes notification-bounce {.bounce-frames;} 69 | @-o-keyframes notification-bounce {.bounce-frames;} 70 | @keyframes notification-bounce {.bounce-frames;} 71 | .bounce-frames () { 72 | 30% { 73 | .translate(0, -8px); 74 | } 75 | 76 | 60% { 77 | .translate(0, 0); 78 | } 79 | 80 | 80% { 81 | .translate(0, -5px); 82 | } 83 | 84 | to { 85 | .translate(0, 0); 86 | } 87 | } 88 | 89 | @-webkit-keyframes notification-grow {.grow-frames;} 90 | @-moz-keyframes notification-grow {.grow-frames;} 91 | @-ms-keyframes notification-grow {.grow-frames;} 92 | @-o-keyframes notification-grow {.grow-frames;} 93 | @keyframes notification-grow {.grow-frames;} 94 | .grow-frames () { 95 | 30% { 96 | .scale(1.2); 97 | } 98 | 99 | 60% { 100 | .scale(0.8); 101 | } 102 | 103 | to { 104 | transform: scale(1); 105 | } 106 | } 107 | 108 | @-webkit-keyframes notification-shake {.shake-frames;} 109 | @-moz-keyframes notification-shake {.shake-frames;} 110 | @-ms-keyframes notification-shake {.shake-frames;} 111 | @-o-keyframes notification-shake {.shake-frames;} 112 | @keyframes notification-shake {.shake-frames;} 113 | .shake-frames () { 114 | 20% { 115 | .rotate(20deg); 116 | } 117 | 118 | 40% { 119 | .rotate(-20deg); 120 | } 121 | 122 | 60% { 123 | .rotate(10deg); 124 | } 125 | 126 | 80% { 127 | .rotate(-10deg); 128 | } 129 | 130 | to { 131 | .rotate(0); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/less/prefixer.less: -------------------------------------------------------------------------------- 1 | // Credit to LESS Elements for the motivation and 2 | // to CSS3Please.com for implementation. 3 | // 4 | // Copyright (c) 2012 Joel Sutherland 5 | // MIT Licensed: 6 | // http://www.opensource.org/licenses/mit-license.php 7 | // 8 | //--------------------------------------------------- 9 | // This is a stripped down version of LESS-Prefixer (http://lessprefixer.com/) 10 | 11 | .animation(@args) { 12 | -webkit-animation: @args; 13 | -moz-animation: @args; 14 | -ms-animation: @args; 15 | -o-animation: @args; 16 | animation: @args; 17 | } 18 | 19 | .border-radius(@args) { 20 | -webkit-border-radius: @args; 21 | border-radius: @args; 22 | 23 | background-clip: padding-box; 24 | } 25 | 26 | .transform(@args) { 27 | -webkit-transform: @args; 28 | -moz-transform: @args; 29 | -ms-transform: @args; 30 | -o-transform: @args; 31 | transform: @args; 32 | } 33 | 34 | .rotate(@deg) { 35 | .transform(rotate(@deg)); 36 | } 37 | 38 | .scale(@factor) { 39 | .transform(scale(@factor)); 40 | } 41 | 42 | .translate(@x, @y) { 43 | .transform(perspective(1px) translate(@x, @y)); 44 | } -------------------------------------------------------------------------------- /src/template/notification-icon.html: -------------------------------------------------------------------------------- 1 |
2 |
{{notification.count}}
3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /src/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 3 | "browser": true, // Standard browser globals e.g. `window`, `document`. 4 | "eqeqeq": true, // Require triple equals i.e. `===`. 5 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 6 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 7 | "quotmark": "single", // Define quotes to string values. 8 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 9 | "undef": true, // Require all non-global variables be declared before they are used. 10 | "unused": true, // Warn unused variables. 11 | "strict": true, // Require `use strict` pragma in every file. 12 | "trailing": true, // Prohibit trailing whitespaces. 13 | "devel": true, // Allow development statements e.g. `console.log();`. 14 | "noempty": true, // Prohibit use of empty blocks. 15 | "expr": true, // Used for Chai 16 | "globals": { // Globals variables. 17 | "angular": false, 18 | 19 | /* MOCHA */ 20 | "describe": false, 21 | "it": false, 22 | "before": false, 23 | "beforeEach": false, 24 | "after": false, 25 | "afterEach": false, 26 | 27 | /* KARMA */ 28 | "inject": false, 29 | "sinon": false, 30 | "expect": false 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/angular-notification-icons.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('angular-notifications-icon', function() { 4 | beforeEach(module('angular-notification-icons', 'angular-notifications.tpls')); 5 | 6 | describe('NotificationDirectiveController', function() { 7 | var $controller; 8 | var scope; 9 | var $animate; 10 | var $q; 11 | 12 | var sandbox; 13 | var element; 14 | var angularElement; 15 | 16 | beforeEach(function() { 17 | sandbox = sinon.sandbox.create(); 18 | 19 | element = { name: 'foo' }; 20 | angularElement = { }; 21 | }); 22 | 23 | afterEach(function() { 24 | sandbox.restore(); 25 | }); 26 | 27 | beforeEach(inject(function(_$controller_, $rootScope, _$animate_, _$q_) { 28 | $controller = _$controller_; 29 | scope = {$scope: $rootScope.$new()}; 30 | $animate = _$animate_; 31 | $q = _$q_; 32 | })); 33 | 34 | var spyOn = function(object, functionName) { 35 | if (!object[functionName]) { 36 | var spy = sandbox.spy(); 37 | object[functionName] = spy; 38 | return spy; 39 | } 40 | 41 | return sandbox.spy(object, functionName); 42 | }; 43 | 44 | it('should start with reasonable defaults', function() { 45 | var ctrl = $controller('NotificationDirectiveController', scope); 46 | ctrl.visible.should.be.false; 47 | ctrl.wideThreshold.should.eql(100); 48 | }); 49 | 50 | it('should initialize with an element', function() { 51 | var jqElement = [angularElement]; 52 | angularElement.querySelector = function() { }; 53 | var selectorSpy = spyOn(angularElement, 'querySelector'); 54 | 55 | var ctrl = $controller('NotificationDirectiveController', scope); 56 | ctrl.init(jqElement); 57 | selectorSpy.firstCall.args.should.eql(['.angular-notifications-icon']); 58 | }); 59 | 60 | it('should set a clear trigger if specified', function() { 61 | var clearTriggerFn; 62 | angularElement.on = {}; 63 | sandbox.stub(angularElement, 'on', function(trigger, callback) { 64 | trigger.should.eql('mouseover'); 65 | clearTriggerFn = callback; 66 | }); 67 | 68 | var deferred = $q.defer(); 69 | spyOn(element, 'removeClass'); 70 | sandbox.stub($animate, 'addClass').returns(deferred.promise); 71 | 72 | var ctrl = $controller('NotificationDirectiveController', scope, {clearTrigger: 'mouseover'}); 73 | sandbox.stub(ctrl, 'getElement').returns(element); 74 | ctrl.init(angularElement); 75 | 76 | ctrl.count = 10; 77 | clearTriggerFn(); 78 | ctrl.count.should.eql(0); 79 | }); 80 | 81 | it('should appear if not visible and count goes above 0', function() { 82 | var deferred = $q.defer(); 83 | var removeClassSpy = spyOn(element, 'removeClass'); 84 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 85 | 86 | var ctrl = $controller('NotificationDirectiveController', scope); 87 | sandbox.stub(ctrl, 'getElement').returns(element); 88 | ctrl.init(angularElement); 89 | scope.$scope.self = ctrl; 90 | 91 | ctrl.count = 1; 92 | scope.$scope.$digest(); 93 | 94 | // Element should now be visible 95 | ctrl.visible.should.eql(true); 96 | addClassStub.should.have.been.calledWith(element, 'grow'); // Default animation was played 97 | 98 | // Class was removed after animation finished 99 | deferred.resolve(); 100 | scope.$scope.$apply(); 101 | removeClassSpy.secondCall.args.should.eql(['grow']); 102 | }); 103 | 104 | it('should play appear animation if set', function() { 105 | var deferred = $q.defer(); 106 | var removeClassSpy = spyOn(element, 'removeClass'); 107 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 108 | 109 | var ctrl = $controller('NotificationDirectiveController', scope, {appearAnimation: 'some-animation'}); 110 | sandbox.stub(ctrl, 'getElement').returns(element); 111 | ctrl.init(angularElement); 112 | scope.$scope.self = ctrl; 113 | 114 | ctrl.count = 1; 115 | scope.$scope.$digest(); 116 | 117 | // Element should now be visible 118 | ctrl.visible.should.eql(true); 119 | addClassStub.should.have.been.calledWith(element, 'some-animation'); 120 | 121 | // Class was removed after animation finished 122 | deferred.resolve(); 123 | scope.$scope.$apply(); 124 | removeClassSpy.secondCall.args.should.eql(['some-animation']); 125 | }); 126 | 127 | it('should update if visible and the count changes', function() { 128 | var deferred = $q.defer(); 129 | var removeClassSpy = spyOn(element, 'removeClass'); 130 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 131 | 132 | var ctrl = $controller('NotificationDirectiveController', scope); 133 | sandbox.stub(ctrl, 'getElement').returns(element); 134 | ctrl.init(angularElement); 135 | scope.$scope.self = ctrl; 136 | 137 | ctrl.count = 1; 138 | ctrl.visible = true; 139 | scope.$scope.$digest(); 140 | 141 | addClassStub.should.have.been.calledWith(element, 'grow'); // Default animation was played 142 | 143 | // Class was removed after animation finished 144 | deferred.resolve(); 145 | scope.$scope.$apply(); 146 | removeClassSpy.secondCall.args.should.eql(['grow']); 147 | }); 148 | 149 | it('should play update animation if set', function() { 150 | var deferred = $q.defer(); 151 | var removeClassSpy = spyOn(element, 'removeClass'); 152 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 153 | 154 | var ctrl = $controller('NotificationDirectiveController', scope, {updateAnimation: 'some-animation'}); 155 | sandbox.stub(ctrl, 'getElement').returns(element); 156 | ctrl.init(angularElement); 157 | scope.$scope.self = ctrl; 158 | 159 | ctrl.count = 1; 160 | ctrl.visible = true; 161 | scope.$scope.$digest(); 162 | 163 | addClassStub.should.have.been.calledWith(element, 'some-animation'); 164 | 165 | // Class was removed after animation finished 166 | deferred.resolve(); 167 | scope.$scope.$apply(); 168 | removeClassSpy.secondCall.args.should.eql(['some-animation']); 169 | }); 170 | 171 | it('should set the appear animation based on the \'animation\' property', function() { 172 | var deferred = $q.defer(); 173 | var removeClassSpy = spyOn(element, 'removeClass'); 174 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 175 | 176 | var ctrl = $controller('NotificationDirectiveController', scope, {animation: 'some-animation'}); 177 | sandbox.stub(ctrl, 'getElement').returns(element); 178 | ctrl.init(angularElement); 179 | scope.$scope.self = ctrl; 180 | 181 | ctrl.count = 1; 182 | scope.$scope.$digest(); 183 | 184 | // Element should now be visible 185 | ctrl.visible.should.eql(true); 186 | addClassStub.should.have.been.calledWith(element, 'some-animation'); 187 | 188 | // Class was removed after animation finished 189 | deferred.resolve(); 190 | scope.$scope.$apply(); 191 | removeClassSpy.secondCall.args.should.eql(['some-animation']); 192 | }); 193 | 194 | it('should set the update animation based on the \'animation\' property', function() { 195 | var deferred = $q.defer(); 196 | var removeClassSpy = spyOn(element, 'removeClass'); 197 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 198 | 199 | var ctrl = $controller('NotificationDirectiveController', scope, {animation: 'some-animation'}); 200 | sandbox.stub(ctrl, 'getElement').returns(element); 201 | ctrl.init(angularElement); 202 | scope.$scope.self = ctrl; 203 | 204 | ctrl.count = 1; 205 | ctrl.visible = true; 206 | scope.$scope.$digest(); 207 | 208 | addClassStub.should.have.been.calledWith(element, 'some-animation'); 209 | 210 | // Class was removed after animation finished 211 | deferred.resolve(); 212 | scope.$scope.$apply(); 213 | removeClassSpy.secondCall.args.should.eql(['some-animation']); 214 | }); 215 | 216 | it('should disappear if visible and the count goes to or below 0', function() { 217 | var ctrl = $controller('NotificationDirectiveController', scope, {}); 218 | spyOn(element, 'removeClass'); 219 | sandbox.stub(ctrl, 'getElement').returns(element); 220 | ctrl.init(angularElement); 221 | scope.$scope.self = ctrl; 222 | 223 | ctrl.count = 0; 224 | ctrl.visible = true; 225 | scope.$scope.$digest(); 226 | 227 | ctrl.visible.should.eql(false); 228 | }); 229 | 230 | it('should play disappear animation if set', function() { 231 | var deferred = $q.defer(); 232 | var removeClassSpy = spyOn(element, 'removeClass'); 233 | var addClassStub = sandbox.stub($animate, 'addClass').returns(deferred.promise); 234 | 235 | var ctrl = $controller('NotificationDirectiveController', scope, {disappearAnimation: 'some-animation'}); 236 | sandbox.stub(ctrl, 'getElement').returns(element); 237 | ctrl.init(angularElement); 238 | scope.$scope.self = ctrl; 239 | 240 | ctrl.count = 0; 241 | ctrl.visible = true; 242 | scope.$scope.$digest(); 243 | 244 | addClassStub.should.have.been.calledWith(element, 'some-animation'); 245 | 246 | // Class was removed after animation finished 247 | deferred.resolve(); 248 | scope.$scope.$apply(); 249 | removeClassSpy.secondCall.args.should.eql(['some-animation']); 250 | }); 251 | 252 | it('should interrupt an animation that is already playing', function() { 253 | var deferred = $q.defer(); 254 | spyOn(element, 'removeClass'); 255 | sandbox.stub($animate, 'addClass').returns(deferred.promise); 256 | 257 | var cancelStub = sandbox.stub($animate, 'cancel'); 258 | 259 | var ctrl = $controller('NotificationDirectiveController', scope); 260 | sandbox.stub(ctrl, 'getElement').returns(element); 261 | ctrl.init(angularElement); 262 | scope.$scope.self = ctrl; 263 | 264 | ctrl.count = 1; 265 | scope.$scope.$digest(); 266 | 267 | ctrl.count = 2; 268 | scope.$scope.$digest(); 269 | 270 | cancelStub.should.have.callCount(1); 271 | }); 272 | 273 | it('should add .wide-icon if the count is higher than the wide threshold', function() { 274 | var deferred = $q.defer(); 275 | var addClassSpy = spyOn(element, 'addClass'); 276 | sandbox.stub($animate, 'addClass').returns(deferred.promise); 277 | 278 | var ctrl = $controller('NotificationDirectiveController', scope, {wideThreshold: 10}); 279 | sandbox.stub(ctrl, 'getElement').returns(element); 280 | ctrl.init(angularElement); 281 | scope.$scope.self = ctrl; 282 | 283 | ctrl.count = 11; 284 | scope.$scope.$digest(); 285 | 286 | addClassSpy.should.have.been.calledWith('wide-icon'); 287 | }); 288 | 289 | it('should remove .wide-icon if the count is lower than the wide threshold', function() { 290 | var deferred = $q.defer(); 291 | var removeClassSpy = spyOn(element, 'removeClass'); 292 | sandbox.stub($animate, 'addClass').returns(deferred.promise); 293 | 294 | var ctrl = $controller('NotificationDirectiveController', scope, {wideThreshold: 10}); 295 | sandbox.stub(ctrl, 'getElement').returns(element); 296 | ctrl.init(angularElement); 297 | scope.$scope.self = ctrl; 298 | 299 | ctrl.count = 1; 300 | scope.$scope.$digest(); 301 | 302 | removeClassSpy.firstCall.args.should.eql(['wide-icon']); 303 | }); 304 | }); 305 | 306 | describe('notificationIcon directive', function() { 307 | var $compile; 308 | var $scope; 309 | 310 | beforeEach(inject(function(_$compile_, $rootScope) { 311 | $compile = _$compile_; 312 | $scope = $rootScope.$new(); 313 | })); 314 | 315 | it('should compile with a count', function() { 316 | var element = $compile('
')($scope); 317 | $scope.$digest(); 318 | 319 | var ctrl = element.isolateScope().notification; 320 | ctrl.count.should.eql(10); 321 | 322 | element.find('.inner-test-dev').should.exist; 323 | }); 324 | }); 325 | }); --------------------------------------------------------------------------------