├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── index.js ├── karma.conf.js ├── karma.coverage.js ├── package-lock.json ├── package.json ├── test ├── directiveTemplateSpec.js ├── toasterContainerControllerSpec.js ├── toasterContainerSpec.js ├── toasterEventRegistrySpec.js └── toasterServiceSpec.js ├── toaster.css ├── toaster.js ├── toaster.min.css ├── toaster.min.js └── toaster.scss /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | components 3 | bower_components 4 | coverage 5 | 6 | # IntelliJ 7 | .idea/ 8 | *.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | before_install: 5 | - export CHROME_BIN=chromium-browser 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | before_script: 9 | - npm install karma 10 | - npm install karma-jasmine 11 | - npm install karma-chrome-launcher 12 | - npm install karma-coverage 13 | - npm install coveralls 14 | script: node_modules/karma/bin/karma start karma.coverage.js --single-run 15 | after_script: "cat ./coverage/lcov-report/lcov.info | ./node_modules/coveralls/bin/coveralls.js" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 jirikavi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AngularJS-Toaster 2 | ================= 3 | 4 | **AngularJS Toaster** is an AngularJS port of the **toastr** non-blocking notification jQuery library. It requires AngularJS v1.2.6 or higher and angular-animate for the CSS3 transformations. `angular-sanitize` is required if using the `html` bodyOutputType. 5 | 6 | [![Build Status](https://travis-ci.org/jirikavi/AngularJS-Toaster.svg)](https://travis-ci.org/jirikavi/AngularJS-Toaster) 7 | [![Coverage Status](https://coveralls.io/repos/jirikavi/AngularJS-Toaster/badge.svg?branch=master&service=github&bust=3.0.0)](https://coveralls.io/github/jirikavi/AngularJS-Toaster?branch=master) 8 | 9 | ### Current Version 3.0.0 10 | 11 | ## Angular Compatibility 12 | AngularJS-Toaster requires AngularJS v1.2.6 or higher and specifically targets AngularJS, not Angular 2-7, although it could be used via ngUpgrade. 13 | If you are looking for the Angular 2-7 port of AngularJS-Toaster, it is located [here](https://github.com/Stabzs/Angular2-Toaster). 14 | 15 | ## Demo 16 | - Simple demo using the current version is at http://plnkr.co/edit/Esrdbl5S6hcmhiVmSjiF?p=preview 17 | - Older versions are http://plnkr.co/edit/1poa9A or http://plnkr.co/edit/4qpHwp or http://plnkr.co/edit/lzYaZt (with version 0.4.5) 18 | - Older version with Angular 1.2.0 is placed at http://plnkr.co/edit/mejR4h 19 | - Older version with Angular 1.2.0-rc.2 is placed at http://plnkr.co/edit/iaC2NY 20 | - Older version with Angular 1.1.5 is placed at http://plnkr.co/mVR4P4 21 | 22 | ## Getting started 23 | 24 | Optionally: to install with bower, use: 25 | ``` 26 | bower install --save angularjs-toaster 27 | ``` 28 | or with npm : 29 | ``` 30 | npm install --save angularjs-toaster 31 | ``` 32 | * Link scripts: 33 | 34 | ```html 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | * Add toaster container directive: 42 | 43 | ```html 44 | 45 | ``` 46 | 47 | * Prepare the call of toaster method: 48 | 49 | ```js 50 | // Display an info toast with no title 51 | angular.module('main', ['toaster', 'ngAnimate']) 52 | .controller('myController', function($scope, toaster) { 53 | $scope.pop = function(){ 54 | toaster.pop('info', "title", "text"); 55 | }; 56 | }); 57 | ``` 58 | 59 | * Call controller method on button click: 60 | 61 | ```html 62 |
63 | 64 |
65 | ``` 66 | 67 | * Close the toast: 68 | 69 | The `toaster` service exposes a `clear` function that takes two parameters: 70 | 71 | - `toasterId`: the id of the toast container you would like to target 72 | - `toastId`: the id of the toast you would like to close 73 | 74 | The `toaster.pop()` function returns an object that contains both the toasterId and the toastId. 75 | This object can be passed directly into the `clear` function to close a toast: 76 | 77 | ```js 78 | var toastInstance = toaster.pop({type: 'info', body: 'Hello'}); 79 | toaster.clear(toastInstance); 80 | ``` 81 | 82 | You can also provide each argument individually: 83 | `toaster.clear(1, toastInstance.toastId);` 84 | 85 | The following rules apply to the parameters passed to the `clear` function. 86 | 87 | - If the `toasterId` is undefined, null, or does not exist AND a toaster container has 88 | defined an Id, no toasts will be cleared for that container. 89 | - If the `toasterId` is undefined or null, each toaster container without a defined Id will 90 | be affected. 91 | - If the `toasterId` is passed as `*`, all containers will be affected. 92 | - if the `toasterId` is passed as `*` and a `toastId` is not defined, all toasts in all 93 | containers will be removed. 94 | - If the `toastId` is undefined or null, any container that is targeted via the above rules 95 | will have all toasts removed from that container. 96 | - If the `toastId` is defined, any container that is targeted via the above rules will remove 97 | toasts that match the `toastId`. 98 | 99 | 100 | ### Timeout 101 | By default, toasts have a timeout setting of 5000, meaning that they are removed after 5000 102 | milliseconds. 103 | 104 | If the timeout is set to 0, the toast will be considered 105 | "sticky" and will not automatically dismiss. 106 | 107 | The timeout can be configured at three different levels: 108 | 109 | * Globally in the config for all toast types: 110 | ```html 111 | 112 | ``` 113 | 114 | * Per info-class type: 115 | By passing the time-out configuration as an object instead of a number, you can specify the global behavior an info-class type should have. 116 | ```html 117 | 119 | 120 | ``` 121 | If a type is not defined and specified, a timeout will not be applied, making the toast "sticky". 122 | 123 | * Per toast constructed via toaster.pop('success', "title", "text"): 124 | ```html 125 | toaster.pop({ 126 | type: 'error', 127 | title: 'Title text', 128 | body: 'Body text', 129 | timeout: 3000 130 | }); 131 | ``` 132 | 133 | ### Close Button 134 | 135 | The Close Button's visibility can be configured at three different levels: 136 | 137 | * Globally in the config for all toast types: 138 | ```html 139 | 140 | ``` 141 | 142 | * Per info-class type: 143 | By passing the close-button configuration as an object instead of a boolean, you can specify the global behavior an info-class type should have. 144 | ```html 145 | 147 | 148 | ``` 149 | If a type is not defined and specified, the default behavior for that type is false. 150 | 151 | * Per toast constructed via toaster.pop('success', "title", "text"): 152 | ```html 153 | toaster.pop({ 154 | type: 'error', 155 | title: 'Title text', 156 | body: 'Body text', 157 | showCloseButton: true 158 | }); 159 | ``` 160 | This option is given the most weight and will override the global configurations for that toast. However, it will not persist to other toasts of that type and does not alter or pollute the global configuration. 161 | 162 | ### Close Html 163 | 164 | The close button html can be overridden either globally or per toast call. 165 | 166 | - Globally: 167 | 168 | ```html 169 | 171 | ``` 172 | - Per toast: 173 | 174 | ```js 175 | toaster.pop({ 176 | type: 'error', 177 | title: 'Title text', 178 | body: 'Body text', 179 | showCloseButton: true, 180 | closeHtml: '' 181 | }); 182 | ``` 183 | 184 | 185 | ### Body Output Type 186 | The rendering of the body content is configurable at both the Global level, which applies to all toasts, and the individual toast level when passed as an argument to the toast. 187 | 188 | There are five types of body renderings: 'html', 'trustedHtml', 'template', 'templateWithData', 'directive'. 189 | 190 | - html: When using this configuration, the toast will bind the toast.html to `ng-bind-html`. If the `angular-sanitize` library is not loaded, an exception will be thrown. 191 | 192 | - trustedHtml: When using this configuration, the toast will parse the body content using 193 | `$sce.trustAsHtml(toast.body)`. It will bypass any sanitization. Only use this option if you own and trust the html content! 194 | 195 | - template: Will use the `toast.body` if passed as an argument, else it will fallback to the template bound to the `'body-template': 'toasterBodyTmpl.html'` configuration option. 196 | 197 | - templateWithData: 198 | - Will use the `toast.body` if passed as an argument, else it will fallback to the template bound to the `'body-template': 'toasterBodyTmpl.html'` configuration option. 199 | - Assigns any data associated with the template to the toast. 200 | 201 | - directive 202 | - Will use the `toast.body` argument to represent the name of a directive that you want to render as the toast's body, else it will fallback to the template bound to the `'body-template': 'toasterBodyTmpl.html'` configuration option. 203 | The directive name being passed to the `body` argument should appear as it exists in the markup, 204 | not camelCased as it would appear in the directive declaration (`cool-directive-name` instead of `coolDirectiveName`). The directive must be usable as an attribute. 205 | 206 | ```js 207 | // The toast pop call, passing in a directive name to be rendered 208 | toaster.pop({ 209 | type: 'info', 210 | body: 'bind-unsafe-html', 211 | bodyOutputType: 'directive' 212 | }); 213 | ``` 214 | 215 | ```js 216 | // The directive that will be dynamically rendered 217 | .directive('bindUnsafeHtml', [function () { 218 | return { 219 | template: "Orange directive text!" 220 | }; 221 | }]) 222 | ``` 223 | - Will use the `toast.directiveData` argument to accept data that will be bound to the directive's scope. The directive cannot use isolateScope and will 224 | throw an exception if isolateScope is detected. All data must be passed via the directiveData argument. 225 | 226 | ```js 227 | // The toast pop call, passing in a directive name to be rendered 228 | toaster.pop({ 229 | type: 'info', 230 | body: 'bind-name', 231 | bodyOutputType: 'directive', 232 | directiveData: { name: 'Bob' } 233 | }); 234 | ``` 235 | 236 | ```js 237 | // The directive that will be dynamically rendered 238 | .directive('bindName', [function () { 239 | return { 240 | template: "Hi {{directiveData.name}}!" 241 | }; 242 | }]) 243 | ``` 244 | 245 | There are additional documented use cases in these [tests](test/directiveTemplateSpec.js). 246 | 247 | All five options can be configured either globally for all toasts or individually per toast.pop() call. If the `body-output-type` option is configured on the toast, it will take precedence over the global configuration for that toast instance. 248 | 249 | - Globally: 250 | 251 | ```html 252 | 253 | ``` 254 | 255 | - Per toast: 256 | 257 | ```js 258 | toaster.pop({ 259 | type: 'error', 260 | title: 'Title text', 261 | body: 'Body text', 262 | bodyOutputType: 'trustedHtml' 263 | }); 264 | ``` 265 | 266 | ### On Show Callback 267 | An onShow callback function can be attached to each toast instance, with the toast passed as a parameter when invoked. The callback will be invoked upon toast add. 268 | 269 | ```js 270 | toaster.pop({ 271 | title: 'A toast', 272 | body: 'with an onShow callback', 273 | onShowCallback: function (toast) { 274 | toaster.pop({ 275 | title: 'A toast', 276 | body: 'invoked as an onShow callback' 277 | }); 278 | } 279 | }); 280 | ``` 281 | 282 | ### On Hide Callback 283 | An onHide callback function can be attached to each toast instance, with the toast passed as a parameter when invoked. The callback will be invoked upon toast removal. This can be used to chain toast calls. 284 | 285 | ```js 286 | toaster.pop({ 287 | title: 'A toast', 288 | body: 'with an onHide callback', 289 | onHideCallback: function (toast) { 290 | toaster.pop({ 291 | title: 'A toast', 292 | body: 'invoked as an onHide callback' 293 | }); 294 | } 295 | }); 296 | ``` 297 | 298 | ### Multiple Toaster Containers 299 | If desired, you can include multiple `` 300 | elements in your DOM. The library will register an event handler for every instance 301 | of the container that it identifies. By default, when there are multiple registered 302 | containers, each container will receive a toast notification and display it when a toast 303 | is popped. 304 | 305 | To target a specific container, you need to register that container with a unique `toaster-id`. 306 | 307 | ```html 308 | 310 | 311 | ``` 312 | 313 | This gives you the ability to specifically target a unique container rather than broadcasting 314 | new toast events to any containers that are currently registered. 315 | 316 | ```js 317 | vm.popContainerOne = function () { 318 | toaster.pop({ type: 'info', body: 'One', toasterId: 1 }); 319 | } 320 | 321 | vm.popContainerTwo = function () { 322 | toaster.pop({ type: 'info', body: 'Two', toasterId: 2 }); 323 | } 324 | ``` 325 | 326 | [This plnkr](http://plnkr.co/edit/4ICtcrpTSoAB9Vo5bRvN?p=preview) demonstrates this behavior 327 | and it is documented in these [tests](test/toasterContainerSpec.js#L430). 328 | 329 | 330 | ### Limit 331 | Limit is defaulted to 0, meaning that there is no maximum number of toasts that are defined 332 | before the toast container begins removing toasts when a new toast is added. 333 | 334 | To change this behavior, pass a "limit" option to the toast-container configuration: 335 | 336 | ```html 337 | 338 | ``` 339 | 340 | ### Dismiss on tap 341 | By default, the `tap-to-dismiss` option is set to true, meaning that if a toast is clicked anywhere 342 | on the toast body, the toast will be dismissed. This behavior can be overriden in the toast-container 343 | configuration so that if set to false, the toast will only be dismissed if the close button is defined 344 | and clicked: 345 | 346 | ```html 347 | 348 | ``` 349 | 350 | This configuration can also be overriden at the toast level via the `tapToDismiss` parameter: 351 | 352 | ```js 353 | toaster.pop({ type: 'info', body: 'One', tapToDismiss: true }); 354 | ``` 355 | 356 | The toast configuration will always take precedence over the toaster-container configuration. 357 | 358 | 359 | ### Newest Toasts on Top 360 | The `newest-on-top` option is defaulted to true, adding new toasts on top of other existing toasts. 361 | If changed to false via the toaster-container configuration, toasts will be added to the bottom of 362 | other existing toasts. 363 | 364 | ```html 365 | 366 | ``` 367 | 368 | ### Other Options 369 | 370 | ```html 371 | // Change display position 372 | 373 | ``` 374 | 375 | ### Animations 376 | Unlike toastr, this library relies on ngAnimate and CSS3 transformations for optional animations. To include and use animations, add a reference to angular-animate.min.js (as described in Getting started - Link scripts) and add ngAnimate as a dependency alongside toaster. 377 | 378 | ```js 379 | // Inject ngAnimate to enable animations 380 | angular.module('main', ['toaster', 'ngAnimate']); 381 | ``` 382 | If you do not want to use animations, you can safely remove the angular-animate.min.js reference as well as the injection of ngAnimate. Toasts will be displayed without animations. 383 | 384 | 385 | ### Common Issues 386 | - Toaster always shows up as "info" 387 | - Your `1.2.6", 16 | "angular-animate": ">1.2.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("./toaster.js"); 2 | module.exports = "toaster"; 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Oct 21 2015 12:37:04 GMT-0600 (Mountain Daylight Time) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.js', 19 | 'https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-sanitize.js', 20 | 'https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular-mocks.js', 21 | 'toaster.js', 22 | 'test/**/*Spec.js' 23 | ], 24 | 25 | 26 | // list of files to exclude 27 | exclude: [ 28 | ], 29 | 30 | 31 | // preprocess matching files before serving them to the browser 32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 33 | preprocessors: { 34 | }, 35 | 36 | 37 | // test results reporter to use 38 | // possible values: 'dots', 'progress' 39 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 40 | reporters: ['progress'], 41 | 42 | 43 | // web server port 44 | port: 9876, 45 | 46 | 47 | // enable / disable colors in the output (reporters and logs) 48 | colors: true, 49 | 50 | 51 | // level of logging 52 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 53 | logLevel: config.LOG_INFO, 54 | 55 | 56 | // enable / disable watching file and executing tests whenever any file changes 57 | autoWatch: true, 58 | 59 | 60 | // start these browsers 61 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 62 | browsers: ['Chrome'], 63 | 64 | plugins: [ 65 | 'karma-chrome-launcher', 66 | 'karma-coverage', 67 | 'karma-jasmine' 68 | ], 69 | 70 | // Continuous Integration mode 71 | // if true, Karma captures browsers, runs the tests and exits 72 | singleRun: false, 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /karma.coverage.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 3 | require("./karma.conf")(config); 4 | 5 | config.autoWatch = false; 6 | 7 | config.preprocessors = { 8 | 'toaster.js': ['coverage'] 9 | }; 10 | 11 | config.coverageReporter = { 12 | dir: 'coverage/', 13 | reporters: [ 14 | { type: 'html', subdir: 'html-report' }, 15 | { type: 'text-summary' } 16 | ] 17 | }; 18 | 19 | config.singleRun = true; 20 | 21 | config.reporters.push('coverage'); 22 | config.plugins.push('karma-coverage'); 23 | 24 | config.customLaunchers = { 25 | Chrome_travis_ci: { 26 | base: 'Chrome', 27 | flags: ['--no-sandbox'] 28 | } 29 | }; 30 | 31 | if (process.env.TRAVIS) { 32 | config.browsers = ['Chrome_travis_ci']; 33 | config.coverageReporter.reporters.push({ 34 | type: 'lcov', subdir: 'lcov-report' 35 | }); 36 | } 37 | 38 | config.set(config); 39 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularjs-toaster", 3 | "version": "3.0.0", 4 | "main": "index.js", 5 | "description": "AngularJS Toaster is a customized version of toastr non-blocking notification javascript library", 6 | "author": "Jiri Kavulak, Stabzs", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jirikavi/AngularJS-Toaster.git" 11 | }, 12 | "scripts": { 13 | "test": "karma start", 14 | "coverage": "karma start karma.coverage.js", 15 | "build": "uglifyjs --compress --mangle --output toaster.min.js --comments -- toaster.js" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "angular": ">1.2.6", 20 | "angular-animate": "~1.2.8", 21 | "angular-mocks": "^1.4.7", 22 | "coveralls": "2.13.1", 23 | "jasmine-core": "^2.3.4", 24 | "karma": "1.7.0", 25 | "karma-chrome-launcher": "^2.2.0", 26 | "karma-cli": "1.0.1", 27 | "karma-coverage": "^1.1.1", 28 | "karma-jasmine": "^1.1.0", 29 | "uglify-js": "3.4.9" 30 | }, 31 | "jspm": { 32 | "main": "toaster", 33 | "dependencies": { 34 | "css": "jspm:css@*" 35 | }, 36 | "shim": { 37 | "toaster": { 38 | "deps": [ 39 | "./toaster.css!" 40 | ] 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/directiveTemplateSpec.js: -------------------------------------------------------------------------------- 1 | /* global describe global it global beforeEach global angular global inject global expect */ 2 | 3 | 'use strict'; 4 | 5 | describe('directiveTemplate', function () { 6 | var toaster, scope, $compile; 7 | 8 | beforeEach(function () { 9 | createDirectives(); 10 | 11 | // load dependencies 12 | module('testApp'); 13 | module('toaster'); 14 | 15 | // inject the toaster service 16 | inject(function (_toaster_, _$rootScope_, _$compile_) { 17 | toaster = _toaster_; 18 | scope = _$rootScope_; 19 | $compile = _$compile_; 20 | }); 21 | }); 22 | 23 | it('should load and render the referenced directive template text', function () { 24 | var container = compileContainer(); 25 | pop({ type: 'info', body: 'bind-template-only', bodyOutputType: 'directive' }); 26 | 27 | expect(container[0].innerText).toBe('here is some great new text! It was brought in via directive!'); 28 | }); 29 | 30 | it('should bind directiveData to the directive template', function () { 31 | var container = compileContainer(); 32 | pop({ type: 'info', body: 'bind-template-with-data', bodyOutputType: 'directive', directiveData: { name: 'Bob' } }); 33 | 34 | expect(container[0].innerText).toBe('Hello Bob'); 35 | }); 36 | 37 | it('should parse type string directiveData to an object', function () { 38 | var container = compileContainer(); 39 | pop({ type: 'info', body: 'bind-template-with-data', bodyOutputType: 'directive', directiveData: '{ "name": "Bob" }' }); 40 | 41 | expect(container[0].innerText).toBe('Hello Bob'); 42 | }); 43 | 44 | it('should render type number directiveData', function () { 45 | var container = compileContainer(); 46 | pop({ type: 'info', body: 'bind-template-with-numeric-data', bodyOutputType: 'directive', directiveData: 2 }); 47 | 48 | expect(container[0].innerText).toBe('1 + 1 = 2'); 49 | }); 50 | 51 | it('should bind Attribute-restricted templates', function () { 52 | var container = compileContainer(); 53 | pop({ type: 'info', body: 'bind-template-only', bodyOutputType: 'directive', directiveData: { name: 'Bob' } }); 54 | 55 | expect(container[0].innerText).toBe('here is some great new text! It was brought in via directive!'); 56 | }); 57 | 58 | it('should bind unrestricted templates', function () { 59 | var container = compileContainer(); 60 | pop({ type: 'info', body: 'unrestricted-template', bodyOutputType: 'directive' }); 61 | 62 | expect(container[0].innerText).toBe('Unrestricted Template'); 63 | }); 64 | 65 | it('should not bind Element-only-restricted templates', function () { 66 | var hasError = false; 67 | var container = compileContainer(); 68 | 69 | try { 70 | pop({ type: 'info', body: 'element-template', bodyOutputType: 'directive' }); 71 | } catch(e) { 72 | var message = 'Directives must be usable as attributes. ' + 73 | 'Add "A" to the restrict option (or remove the option entirely). Occurred for directive element-template.'; 74 | 75 | expect(e.message).toBe(message); 76 | hasError = true; 77 | } 78 | 79 | expect(hasError).toBe(true); 80 | }); 81 | 82 | it('should not bind Class-only-restricted templates', function () { 83 | var hasError = false; 84 | var container = compileContainer(); 85 | 86 | try { 87 | pop({ type: 'info', body: 'class-template', bodyOutputType: 'directive' }); 88 | } catch(e) { 89 | var message = 'Directives must be usable as attributes. ' + 90 | 'Add "A" to the restrict option (or remove the option entirely). Occurred for directive class-template.'; 91 | 92 | expect(e.message).toBe(message); 93 | hasError = true; 94 | } 95 | 96 | expect(hasError).toBe(true); 97 | }); 98 | 99 | it('should throw an error if directiveName argument is not passed via body', function () { 100 | var container = compileContainer(); 101 | var hasError = false; 102 | 103 | expect(container[0].innerText).toBe(''); 104 | 105 | try { 106 | pop({ type: 'info', bodyOutputType: 'directive' }); 107 | } catch (e) { 108 | expect(e.message).toBe('A valid directive name must be provided via the toast body argument when using bodyOutputType: directive'); 109 | hasError = true; 110 | } 111 | 112 | expect(container[0].innerText).toBe(''); 113 | expect(hasError).toBe(true); 114 | }); 115 | 116 | it('should throw an error if directiveName argument is an empty string', function () { 117 | var container = compileContainer(); 118 | var hasError = false; 119 | 120 | expect(container[0].innerText).toBe(''); 121 | 122 | try { 123 | pop({ type: 'info', body: '', bodyOutputType: 'directive' }); 124 | } catch (e) { 125 | expect(e.message).toBe('A valid directive name must be provided via the toast body argument when using bodyOutputType: directive'); 126 | hasError = true; 127 | } 128 | 129 | expect(container[0].innerText).toBe(''); 130 | expect(hasError).toBe(true); 131 | }); 132 | 133 | it('should throw an error if the directive could not be found', function () { 134 | var hasError = false; 135 | 136 | compileContainer(); 137 | 138 | try { 139 | pop({ type: 'info', body: 'non-existent-directive', bodyOutputType: 'directive' }); 140 | } catch (e) { 141 | var message = 'non-existent-directive could not be found. ' + 142 | 'The name should appear as it exists in the markup,' + 143 | ' not camelCased as it would appear in the directive declaration,' + 144 | ' e.g. directive-name not directiveName.' 145 | 146 | expect(e.message).toBe(message); 147 | hasError = true; 148 | } 149 | 150 | expect(hasError).toBe(true); 151 | }); 152 | 153 | it('should throw an error if the directive uses isolate scope', function () { 154 | var hasError = false; 155 | compileContainer(); 156 | 157 | try { 158 | pop({ type: 'info', body: 'isolate-scope', bodyOutputType: 'directive' }); 159 | } catch (e) { 160 | var message = 'Cannot use a directive with an isolated scope.' + 161 | ' The scope must be either true or falsy (e.g. false/null/undefined). Occurred for directive isolate-scope.'; 162 | 163 | expect(e.message).toBe(message) 164 | hasError = true; 165 | } 166 | 167 | expect(hasError).toBe(true); 168 | }) 169 | 170 | 171 | function compileContainer() { 172 | var element = angular.element(''); 173 | $compile(element)(scope); 174 | scope.$digest(); 175 | 176 | return element; 177 | } 178 | 179 | function pop(params) { 180 | toaster.pop(params); 181 | 182 | // force new toast to be rendered 183 | scope.$digest(); 184 | } 185 | 186 | function createDirectives() { 187 | angular.module('testApp', []) 188 | .directive('bindTemplateOnly', function () { 189 | return { 190 | restrict: 'A', 191 | template: 'here is some great new text! It was brought in via directive!' 192 | } 193 | }) 194 | .directive('bindTemplateWithData', function () { 195 | return { template: 'Hello {{directiveData.name}}' } 196 | }) 197 | .directive('bindTemplateWithNumericData', function () { 198 | return { template: '1 + 1 = {{directiveData}}' } 199 | }) 200 | .directive('elementTemplate', function () { 201 | return { restrict: 'E', template: 'Element Template' } 202 | }) 203 | .directive('classTemplate', function () { 204 | return { restrict: 'C', template: 'Class Template' } 205 | }) 206 | .directive('unrestrictedTemplate', function () { 207 | return { template: 'Unrestricted Template' } 208 | }) 209 | .directive('isolateScope', function () { 210 | return { template: 'isolate scope template', scope: {}} 211 | }); 212 | } 213 | }) -------------------------------------------------------------------------------- /test/toasterContainerControllerSpec.js: -------------------------------------------------------------------------------- 1 | /* global describe global it global beforeEach global angular global jasmine global inject global expect global spyOn */ 2 | 3 | 'use strict'; 4 | 5 | var rootScope, toaster, $compile; 6 | 7 | describe('toasterContainer controller', function () { 8 | beforeEach(function () { 9 | module('toaster'); 10 | 11 | // inject the toaster service 12 | inject(function (_toaster_, _$rootScope_, _$compile_) { 13 | toaster = _toaster_; 14 | rootScope = _$rootScope_; 15 | $compile = _$compile_; 16 | }); 17 | }); 18 | 19 | it('should stop timer if config.mouseoverTimer is true', function () { 20 | var container = angular.element( 21 | ''); 22 | 23 | $compile(container)(rootScope); 24 | rootScope.$digest(); 25 | var scope = container.scope(); 26 | 27 | expect(scope.config.mouseoverTimer).toBe(true); 28 | 29 | toaster.pop({ type: 'info' }); 30 | 31 | rootScope.$digest(); 32 | 33 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 34 | 35 | scope.stopTimer(scope.toasters[0]); 36 | 37 | rootScope.$digest(); 38 | 39 | expect(scope.toasters[0].timeoutPromise).toBe(null); 40 | }); 41 | 42 | it('should do nothing if config.mouseoverTimer is true and stopTimer is called again', function () { 43 | var container = angular.element( 44 | ''); 45 | 46 | $compile(container)(rootScope); 47 | rootScope.$digest(); 48 | var scope = container.scope(); 49 | 50 | expect(scope.config.mouseoverTimer).toBe(true); 51 | 52 | toaster.pop({ type: 'info' }); 53 | 54 | rootScope.$digest(); 55 | 56 | scope.stopTimer(scope.toasters[0]); 57 | rootScope.$digest(); 58 | 59 | expect(scope.toasters[0].timeoutPromise).toBe(null); 60 | 61 | scope.stopTimer(scope.toasters[0]); 62 | rootScope.$digest(); 63 | 64 | expect(scope.toasters[0].timeoutPromise).toBe(null); 65 | }); 66 | 67 | it('should not stop timer if config.mouseoverTimer is false', function () { 68 | var container = angular.element( 69 | ''); 70 | 71 | $compile(container)(rootScope); 72 | rootScope.$digest(); 73 | var scope = container.scope(); 74 | 75 | expect(scope.config.mouseoverTimer).toBe(false); 76 | 77 | toaster.pop({ type: 'info' }); 78 | 79 | rootScope.$digest(); 80 | 81 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 82 | 83 | scope.stopTimer(scope.toasters[0]); 84 | 85 | rootScope.$digest(); 86 | 87 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 88 | }); 89 | 90 | it('should restart timer if config.mouseoverTimer is true and timeoutPromise is falsy', function () { 91 | var container = angular.element( 92 | ''); 93 | 94 | $compile(container)(rootScope); 95 | rootScope.$digest(); 96 | var scope = container.scope(); 97 | 98 | toaster.pop({ type: 'info' }); 99 | rootScope.$digest(); 100 | 101 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 102 | 103 | scope.stopTimer(scope.toasters[0]); 104 | expect(scope.toasters[0].timeoutPromise).toBe(null); 105 | 106 | scope.restartTimer(scope.toasters[0]); 107 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 108 | }); 109 | 110 | it('should not restart timer if config.mouseoverTimer is true and timeoutPromise is truthy', function () { 111 | var container = angular.element( 112 | ''); 113 | 114 | $compile(container)(rootScope); 115 | rootScope.$digest(); 116 | var scope = container.scope(); 117 | 118 | toaster.pop({ type: 'info' }); 119 | rootScope.$digest(); 120 | 121 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 122 | 123 | spyOn(scope, 'configureTimer').and.callThrough(); 124 | 125 | scope.restartTimer(scope.toasters[0]); 126 | expect(scope.toasters[0].timeoutPromise).toBeDefined(); 127 | expect(scope.configureTimer).not.toHaveBeenCalled(); 128 | }); 129 | 130 | it('should not restart timer and should remove toast if config.mouseoverTimer is not true and timeoutPromise is null', function () { 131 | var container = angular.element( 132 | ''); 133 | 134 | $compile(container)(rootScope); 135 | rootScope.$digest(); 136 | var scope = container.scope(); 137 | 138 | toaster.pop({ type: 'info' }); 139 | rootScope.$digest(); 140 | 141 | expect(scope.config.mouseoverTimer).toBe(2); 142 | 143 | scope.toasters[0].timeoutPromise = null; 144 | 145 | spyOn(scope, 'configureTimer').and.callThrough(); 146 | spyOn(scope, 'removeToast').and.callThrough(); 147 | 148 | scope.restartTimer(scope.toasters[0]); 149 | 150 | expect(scope.configureTimer).not.toHaveBeenCalled(); 151 | expect(scope.removeToast).toHaveBeenCalled(); 152 | expect(scope.toasters.length).toBe(0) 153 | }); 154 | 155 | it('should not restart timer or remove toast if config.mouseoverTimer is not true and timeoutPromise is not null', function () { 156 | var container = angular.element( 157 | ''); 158 | 159 | $compile(container)(rootScope); 160 | rootScope.$digest(); 161 | var scope = container.scope(); 162 | 163 | toaster.pop({ type: 'info' }); 164 | rootScope.$digest(); 165 | 166 | expect(scope.config.mouseoverTimer).toBe(2); 167 | 168 | spyOn(scope, 'configureTimer').and.callThrough(); 169 | spyOn(scope, 'removeToast').and.callThrough(); 170 | 171 | scope.restartTimer(scope.toasters[0]); 172 | 173 | expect(scope.configureTimer).not.toHaveBeenCalled(); 174 | expect(scope.removeToast).not.toHaveBeenCalled(); 175 | expect(scope.toasters.length).toBe(1) 176 | }); 177 | 178 | describe('click', function () { 179 | it('should do nothing if config.tap is not true and toast.showCloseButton is not true', function () { 180 | var container = angular.element( 181 | ''); 182 | 183 | $compile(container)(rootScope); 184 | rootScope.$digest(); 185 | var scope = container.scope(); 186 | 187 | spyOn(scope, 'removeToast').and.callThrough(); 188 | 189 | toaster.pop({ type: 'info' }); 190 | rootScope.$digest(); 191 | 192 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 193 | 194 | expect(scope.toasters.length).toBe(1); 195 | expect(scope.removeToast).not.toHaveBeenCalled(); 196 | }); 197 | 198 | it('should do nothing if config.tap is not true and toast.showCloseButton is true', function () { 199 | var container = angular.element( 200 | ''); 201 | 202 | $compile(container)(rootScope); 203 | rootScope.$digest(); 204 | var scope = container.scope(); 205 | 206 | spyOn(scope, 'removeToast').and.callThrough(); 207 | 208 | toaster.pop({ type: 'info' }); 209 | rootScope.$digest(); 210 | 211 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 212 | 213 | expect(scope.toasters.length).toBe(1); 214 | expect(scope.removeToast).not.toHaveBeenCalled(); 215 | }); 216 | 217 | it('should do nothing if config.tap is not true and isCloseButton is not true', function () { 218 | var container = angular.element( 219 | ''); 220 | 221 | $compile(container)(rootScope); 222 | rootScope.$digest(); 223 | var scope = container.scope(); 224 | 225 | spyOn(scope, 'removeToast').and.callThrough(); 226 | 227 | toaster.pop({ type: 'info' }); 228 | rootScope.$digest(); 229 | 230 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0], false); 231 | 232 | expect(scope.toasters.length).toBe(1); 233 | expect(scope.removeToast).not.toHaveBeenCalled(); 234 | }); 235 | 236 | it('should do nothing if config.tap is not true and toast.tap is not true and isCloseButton is not true', function () { 237 | var container = angular.element( 238 | ''); 239 | 240 | $compile(container)(rootScope); 241 | rootScope.$digest(); 242 | var scope = container.scope(); 243 | 244 | spyOn(scope, 'removeToast').and.callThrough(); 245 | 246 | toaster.pop({ type: 'info', tapToDismiss: false }); 247 | rootScope.$digest(); 248 | 249 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0], false); 250 | 251 | expect(scope.toasters.length).toBe(1); 252 | expect(scope.removeToast).not.toHaveBeenCalled(); 253 | }); 254 | 255 | it('should do nothing if config.tap is true and toast.tap is not true and isCloseButton is not true', function () { 256 | var container = angular.element( 257 | ''); 258 | 259 | $compile(container)(rootScope); 260 | rootScope.$digest(); 261 | var scope = container.scope(); 262 | 263 | spyOn(scope, 'removeToast').and.callThrough(); 264 | 265 | toaster.pop({ type: 'info', tapToDismiss: false }); 266 | rootScope.$digest(); 267 | 268 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0], false); 269 | 270 | expect(scope.toasters.length).toBe(1); 271 | expect(scope.removeToast).not.toHaveBeenCalled(); 272 | }); 273 | 274 | it('should remove toast if config.tap is false but toast.tap is true', function () { 275 | var container = angular.element( 276 | ''); 277 | 278 | $compile(container)(rootScope); 279 | rootScope.$digest(); 280 | var scope = container.scope(); 281 | 282 | spyOn(scope, 'removeToast').and.callThrough(); 283 | 284 | toaster.pop({ type: 'info', tapToDismiss: true }); 285 | rootScope.$digest(); 286 | 287 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 288 | 289 | expect(scope.toasters.length).toBe(0); 290 | expect(scope.removeToast).toHaveBeenCalled(); 291 | }); 292 | 293 | it('should remove toast if config.tap is false but toast.tap is true', function () { 294 | var container = angular.element( 295 | ''); 296 | 297 | $compile(container)(rootScope); 298 | rootScope.$digest(); 299 | var scope = container.scope(); 300 | 301 | spyOn(scope, 'removeToast').and.callThrough(); 302 | 303 | toaster.pop({ type: 'info', tapToDismiss: true }); 304 | rootScope.$digest(); 305 | 306 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 307 | 308 | expect(scope.toasters.length).toBe(0); 309 | expect(scope.removeToast).toHaveBeenCalled(); 310 | }); 311 | 312 | it('should remove toast if config.tap is true', function () { 313 | var container = angular.element( 314 | ''); 315 | 316 | $compile(container)(rootScope); 317 | rootScope.$digest(); 318 | var scope = container.scope(); 319 | 320 | spyOn(scope, 'removeToast').and.callThrough(); 321 | 322 | toaster.pop({ type: 'info' }); 323 | rootScope.$digest(); 324 | 325 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 326 | 327 | expect(scope.toasters.length).toBe(0); 328 | expect(scope.removeToast).toHaveBeenCalled(); 329 | }); 330 | 331 | it('should remove toast if config.tap is true and the click handler function returns true', function () { 332 | var container = angular.element( 333 | ''); 334 | 335 | $compile(container)(rootScope); 336 | rootScope.$digest(); 337 | var scope = container.scope(); 338 | 339 | spyOn(scope, 'removeToast').and.callThrough(); 340 | 341 | toaster.pop({ type: 'info', clickHandler: function (toast, isCloseButton) { return true; } }); 342 | rootScope.$digest(); 343 | 344 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 345 | 346 | expect(scope.toasters.length).toBe(0); 347 | expect(scope.removeToast).toHaveBeenCalled(); 348 | }); 349 | 350 | it('should not remove toast if config.tap is true and the click handler function does not return true', function () { 351 | var container = angular.element( 352 | ''); 353 | 354 | $compile(container)(rootScope); 355 | rootScope.$digest(); 356 | var scope = container.scope(); 357 | 358 | spyOn(scope, 'removeToast').and.callThrough(); 359 | 360 | toaster.pop({ type: 'info', clickHandler: function (toast, isCloseButton) { } }); 361 | rootScope.$digest(); 362 | 363 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 364 | 365 | expect(scope.toasters.length).toBe(1); 366 | expect(scope.removeToast).not.toHaveBeenCalled(); 367 | }); 368 | 369 | it('should remove toast if config.tap is true and the click handler exists on the parent returning true', function () { 370 | var container = angular.element( 371 | ''); 372 | 373 | $compile(container)(rootScope); 374 | rootScope.$digest(); 375 | var scope = container.scope(); 376 | scope.$parent.clickHandler = function () { return true; }; 377 | 378 | spyOn(scope, 'removeToast').and.callThrough(); 379 | 380 | toaster.pop({ type: 'info', clickHandler: 'clickHandler' }); 381 | rootScope.$digest(); 382 | 383 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 384 | 385 | expect(scope.toasters.length).toBe(0); 386 | expect(scope.removeToast).toHaveBeenCalled(); 387 | }); 388 | 389 | it('should not remove toast if config.tap is true and the click handler exists on the parent not returning true', function () { 390 | var container = angular.element( 391 | ''); 392 | 393 | $compile(container)(rootScope); 394 | rootScope.$digest(); 395 | var scope = container.scope(); 396 | scope.$parent.clickHandler = function () { }; 397 | 398 | spyOn(scope, 'removeToast').and.callThrough(); 399 | 400 | toaster.pop({ type: 'info', clickHandler: 'clickHandler' }); 401 | rootScope.$digest(); 402 | 403 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 404 | 405 | expect(scope.toasters.length).toBe(1); 406 | expect(scope.removeToast).not.toHaveBeenCalled(); 407 | }); 408 | 409 | it('should remove toast if config.tap is true and the click handler does not exist on the parent', function () { 410 | // TODO: this functionality seems counter-intuitive. 411 | // Need to identify use cases to see if this is actually correct. 412 | 413 | var container = angular.element( 414 | ''); 415 | 416 | $compile(container)(rootScope); 417 | rootScope.$digest(); 418 | var scope = container.scope(); 419 | 420 | spyOn(scope, 'removeToast').and.callThrough(); 421 | console.log = jasmine.createSpy("log"); 422 | 423 | toaster.pop({ type: 'info', clickHandler: 'clickHandler' }); 424 | rootScope.$digest(); 425 | 426 | scope.click({stopPropagation: function() {return true;}}, scope.toasters[0]); 427 | 428 | expect(scope.toasters.length).toBe(0); 429 | expect(scope.removeToast).toHaveBeenCalled(); 430 | 431 | expect(console.log).toHaveBeenCalledWith("TOAST-NOTE: Your click handler is not inside a parent scope of toaster-container."); 432 | }); 433 | }); 434 | }); -------------------------------------------------------------------------------- /test/toasterContainerSpec.js: -------------------------------------------------------------------------------- 1 | /* global describe global it global beforeEach global angular global jasmine global inject global expect global spyOn */ 2 | 3 | 'use strict'; 4 | 5 | var rootScope, toaster, $compile, $sanitize; 6 | 7 | describe('toasterContainer', function () { 8 | beforeEach(function () { 9 | module('toaster'); 10 | 11 | // inject the toaster service 12 | inject(function (_toaster_, _$rootScope_, _$compile_) { 13 | toaster = _toaster_; 14 | rootScope = _$rootScope_; 15 | $compile = _$compile_; 16 | }); 17 | }); 18 | 19 | it('should pop a toast via individual parameters', function () { 20 | var container = compileContainer(); 21 | var scope = container.scope(); 22 | 23 | toaster.pop('info', 'test', 'test'); 24 | 25 | expect(scope.toasters.length).toBe(1); 26 | }); 27 | 28 | it('should unsubscribe events on $destroy if handlers exist', function () { 29 | var toasterEventRegistry; 30 | 31 | inject(function (_toasterEventRegistry_) { 32 | toasterEventRegistry = _toasterEventRegistry_; 33 | }); 34 | 35 | var container = compileContainer(); 36 | var scope = container.scope(); 37 | 38 | spyOn(toasterEventRegistry, 'unsubscribeToNewToastEvent').and.callThrough(); 39 | spyOn(toasterEventRegistry, 'unsubscribeToClearToastsEvent').and.callThrough(); 40 | 41 | scope.$destroy(); 42 | 43 | expect(toasterEventRegistry.unsubscribeToNewToastEvent).toHaveBeenCalled(); 44 | expect(toasterEventRegistry.unsubscribeToClearToastsEvent).toHaveBeenCalled(); 45 | }); 46 | 47 | 48 | describe('addToast', function () { 49 | it('should default to icon-class config value if toast.type not found in icon-classes', function () { 50 | var toasterConfig; 51 | 52 | inject(function (_toasterConfig_) { 53 | toasterConfig = _toasterConfig_; 54 | }); 55 | 56 | compileContainer(); 57 | 58 | expect(toasterConfig['icon-class']).toBe('toast-info'); 59 | 60 | toaster.pop({ type: 'invalid' }); 61 | 62 | rootScope.$digest(); 63 | 64 | expect(toaster.toast.type).toBe('toast-info'); 65 | }); 66 | 67 | it('should allow subsequent duplicates if prevent-duplicates is not set', function () { 68 | var container = compileContainer(); 69 | var scope = container.scope(); 70 | 71 | expect(scope.toasters.length).toBe(0); 72 | 73 | toaster.pop({ type: 'info', title: 'title', body: 'body' }); 74 | toaster.pop({ type: 'info', title: 'title', body: 'body' }); 75 | 76 | rootScope.$digest(); 77 | 78 | expect(scope.toasters.length).toBe(2); 79 | }); 80 | 81 | it('should not allow subsequent duplicates if prevent-duplicates is true and body matches', function () { 82 | var container = angular.element( 83 | ''); 84 | 85 | $compile(container)(rootScope); 86 | rootScope.$digest(); 87 | 88 | var scope = container.scope(); 89 | 90 | expect(scope.toasters.length).toBe(0); 91 | 92 | toaster.pop({ type: 'info', title: 'title', body: 'body' }); 93 | toaster.pop({ type: 'info', title: 'title', body: 'body' }); 94 | 95 | expect(scope.toasters.length).toBe(1); 96 | }); 97 | 98 | it('should not allow subsequent duplicates if prevent-duplicates is true and id matches with unique bodies', function () { 99 | var container = angular.element( 100 | ''); 101 | 102 | $compile(container)(rootScope); 103 | rootScope.$digest(); 104 | 105 | var scope = container.scope(); 106 | 107 | expect(scope.toasters.length).toBe(0); 108 | 109 | var toastWrapper = toaster.pop({ type: 'info', title: 'title', body: 'body' }); 110 | toaster.pop({ type: 'info', title: 'title', body: 'body2', toastId: toastWrapper.toastId }); 111 | 112 | expect(scope.toasters.length).toBe(1); 113 | }); 114 | 115 | it('should allow subsequent duplicates if prevent-duplicates is true with unique toastId and body params', function () { 116 | var container = angular.element( 117 | ''); 118 | 119 | $compile(container)(rootScope); 120 | rootScope.$digest(); 121 | 122 | var scope = container.scope(); 123 | 124 | expect(scope.toasters.length).toBe(0); 125 | 126 | toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 1 }); 127 | toaster.pop({ type: 'info', title: 'title', body: 'body2', toastId: 2 }); 128 | 129 | rootScope.$digest(); 130 | 131 | expect(scope.toasters.length).toBe(2); 132 | }); 133 | 134 | it('should not allow subsequent duplicates if prevent-duplicates is true with identical toastId params', function () { 135 | var container = angular.element( 136 | ''); 137 | 138 | $compile(container)(rootScope); 139 | rootScope.$digest(); 140 | 141 | var scope = container.scope(); 142 | 143 | expect(scope.toasters.length).toBe(0); 144 | 145 | toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 1 }); 146 | toaster.pop({ type: 'info', title: 'title', body: 'body', toastId: 1 }); 147 | 148 | rootScope.$digest(); 149 | 150 | expect(scope.toasters.length).toBe(1); 151 | }); 152 | 153 | it('should not render the close button if showCloseButton is false', function () { 154 | var container = compileContainer(); 155 | 156 | toaster.pop({ type: 'info', body: 'With a close button' }); 157 | 158 | rootScope.$digest(); 159 | 160 | expect(container.find('button')[0]).toBeUndefined(); 161 | }); 162 | 163 | it('should use the default close html if toast.closeHtml is undefined', function () { 164 | var container = compileContainer(); 165 | 166 | toaster.pop({ type: 'info', body: 'With a close button', showCloseButton: true }); 167 | 168 | rootScope.$digest(); 169 | 170 | var buttons = container.find('button'); 171 | 172 | expect(buttons.length).toBe(1); 173 | expect(buttons[0].outerHTML).toBe(''); 174 | }); 175 | 176 | it('should use the toast.closeHtml argument if passed', function () { 177 | var container = compileContainer(); 178 | 179 | toaster.pop({ type: 'info', body: 'With a close button', showCloseButton: true, 180 | closeHtml: '' 181 | }); 182 | 183 | rootScope.$digest(); 184 | 185 | var buttons = container.find('button'); 186 | 187 | expect(buttons.length).toBe(1); 188 | expect(buttons[0].outerHTML).toBe(''); 189 | }); 190 | 191 | it('should render toast.closeHtml argument if not a button element', function () { 192 | var container = compileContainer(); 193 | 194 | toaster.pop({ type: 'info', body: 'With close text', showCloseButton: true, 195 | closeHtml: 'Close' 196 | }); 197 | 198 | rootScope.$digest(); 199 | 200 | var spans = container.find('span'); 201 | 202 | expect(spans.length).toBe(1); 203 | expect(spans[0].outerHTML).toBe('Close'); 204 | }); 205 | 206 | it('should show the close button if mergedConfig close-button is an object set to true for toast-info', function () { 207 | var container = angular.element( 208 | ''); 209 | 210 | $compile(container)(rootScope); 211 | rootScope.$digest(); 212 | 213 | toaster.pop({ type: 'info' }); 214 | 215 | rootScope.$digest(); 216 | 217 | var buttons = container.find('button'); 218 | 219 | expect(buttons.length).toBe(1); 220 | expect(buttons[0].outerHTML).toBe(''); 221 | }); 222 | 223 | it('should not render the close button if mergedConfig close-button type cannot be found', function () { 224 | var container = angular.element( 225 | ''); 226 | 227 | $compile(container)(rootScope); 228 | rootScope.$digest(); 229 | 230 | toaster.pop({ type: 'info' }); 231 | 232 | rootScope.$digest(); 233 | 234 | var buttons = container.find('button'); 235 | 236 | expect(buttons.length).toBe(0); 237 | expect(buttons[0]).toBeUndefined(); 238 | }); 239 | 240 | it('should not render the close button if mergedConfig close-button is not an object', function () { 241 | var container = angular.element( 242 | ''); 243 | 244 | $compile(container)(rootScope); 245 | rootScope.$digest(); 246 | 247 | toaster.pop({ type: 'info' }); 248 | 249 | rootScope.$digest(); 250 | 251 | var buttons = container.find('button'); 252 | 253 | expect(buttons.length).toBe(0); 254 | expect(buttons[0]).toBeUndefined(); 255 | }); 256 | 257 | it('should render \'trustedHtml\' bodyOutputType', function () { 258 | var container = compileContainer(); 259 | 260 | toaster.pop({ bodyOutputType: 'trustedHtml', body: '
Body
' }); 261 | 262 | rootScope.$digest(); 263 | 264 | var body = container.find('section'); 265 | 266 | expect(body.length).toBe(1); 267 | expect(body[0].outerHTML).toBe('
Body
'); 268 | }); 269 | 270 | it('default toast template exists', function () { 271 | inject(function($templateCache) { 272 | var template = $templateCache.get('angularjs-toaster/toast.html'); 273 | 274 | expect(template.length).toBeGreaterThan(0); 275 | }); 276 | }); 277 | 278 | it('should render template bodyOutputType when body is passed', function () { 279 | inject(function($templateCache) { 280 | $templateCache.put('/templatepath/template.html', '
Template
'); 281 | }); 282 | 283 | var container = compileContainer(); 284 | 285 | toaster.pop({ bodyOutputType: 'template', body: '/templatepath/template.html' }); 286 | 287 | rootScope.$digest(); 288 | 289 | expect(toaster.toast.body).toBe('/templatepath/template.html'); 290 | 291 | var body = container.find('section'); 292 | 293 | expect(body.length).toBe(1); 294 | expect(body[0].outerHTML).toBe('
Template
'); 295 | }); 296 | 297 | it('should render default template bodyOutputType when body is not passed', function () { 298 | inject(function($templateCache) { 299 | $templateCache.put('toasterBodyTmpl.html', '
Template
'); 300 | }); 301 | 302 | var container = compileContainer(); 303 | 304 | toaster.pop({ bodyOutputType: 'template' }); 305 | 306 | rootScope.$digest(); 307 | 308 | expect(toaster.toast.bodyTemplate).toBe('toasterBodyTmpl.html'); 309 | 310 | var body = container.find('section'); 311 | 312 | expect(body.length).toBe(1); 313 | expect(body[0].outerHTML).toBe('
Template
'); 314 | }); 315 | 316 | it('should render templateWithData bodyOutputType when body is passed', function () { 317 | inject(function($templateCache) { 318 | $templateCache.put('template.html', '
Template {{toaster.data}}
'); 319 | }); 320 | 321 | var container = compileContainer(); 322 | 323 | toaster.pop({ bodyOutputType: 'templateWithData', body: "{template: 'template.html', data: 123 }" }); 324 | 325 | rootScope.$digest(); 326 | 327 | var body = container.find('section'); 328 | 329 | expect(body.length).toBe(1); 330 | expect(body[0].outerHTML).toBe('
Template 123
'); 331 | }); 332 | 333 | it('should throw exception for default templateWithData bodyOutputType when body is not passed', function () { 334 | // TODO: If the default fallback template cannot be parsed to an object 335 | // composed of template and data, an exception is thrown. This seems to 336 | // be undesirable behavior. A clearer exception should be thrown, or better 337 | // handling should be handled, or the fallback option should be removed. 338 | inject(function($templateCache) { 339 | $templateCache.put('template.html', '
Template {{toaster.data}}
'); 340 | }); 341 | 342 | compileContainer(); 343 | var hasException = false; 344 | 345 | try { 346 | toaster.pop({ bodyOutputType: 'templateWithData' }); 347 | } catch (e) { 348 | expect(e.message).toBe("Cannot read property 'template' of undefined"); 349 | hasException = true; 350 | } 351 | 352 | expect(hasException).toBe(true); 353 | }); 354 | 355 | it('should remove first in toast if limit is met and newest-on-top is true', function () { 356 | var container = angular.element( 357 | ''); 358 | 359 | $compile(container)(rootScope); 360 | rootScope.$digest(); 361 | 362 | var scope = container.scope(); 363 | 364 | toaster.pop({ type: 'info', body: 'first' }); 365 | toaster.pop({ type: 'info', body: 'second' }); 366 | 367 | rootScope.$digest(); 368 | 369 | expect(scope.toasters.length).toBe(2); 370 | expect(scope.toasters[0].body).toBe('second'); 371 | expect(scope.toasters[1].body).toBe('first'); 372 | 373 | toaster.pop({ type: 'info', body: 'third' }); 374 | 375 | rootScope.$digest(); 376 | 377 | expect(scope.toasters.length).toBe(2); 378 | expect(scope.toasters[0].body).toBe('third'); 379 | expect(scope.toasters[1].body).toBe('second'); 380 | }); 381 | 382 | it('should remove last in toast if limit is met and newest-on-top is false', function () { 383 | var container = angular.element( 384 | ''); 385 | 386 | $compile(container)(rootScope); 387 | rootScope.$digest(); 388 | 389 | var scope = container.scope(); 390 | 391 | toaster.pop({ type: 'info', body: 'first' }); 392 | toaster.pop({ type: 'info', body: 'second' }); 393 | 394 | rootScope.$digest(); 395 | 396 | expect(scope.toasters.length).toBe(2); 397 | expect(scope.toasters[0].body).toBe('first'); 398 | expect(scope.toasters[1].body).toBe('second'); 399 | 400 | toaster.pop({ type: 'info', body: 'third' }); 401 | 402 | rootScope.$digest(); 403 | 404 | expect(scope.toasters.length).toBe(2); 405 | expect(scope.toasters[0].body).toBe('second'); 406 | expect(scope.toasters[1].body).toBe('third'); 407 | }); 408 | 409 | it('should invoke onShowCallback if it exists when toast is added', function () { 410 | compileContainer(); 411 | var mock = { 412 | callback : function () { } 413 | }; 414 | 415 | spyOn(mock, 'callback'); 416 | 417 | toaster.pop({ type: 'info', body: 'toast 1', onShowCallback: mock.callback }); 418 | 419 | rootScope.$digest(); 420 | 421 | expect(mock.callback).toHaveBeenCalled(); 422 | }); 423 | 424 | it('should not invoke onShowCallback if it does not exist when toast is added', function () { 425 | compileContainer(); 426 | var mock = { 427 | callback : function () { } 428 | }; 429 | 430 | spyOn(mock, 'callback'); 431 | 432 | toaster.pop({ type: 'info', body: 'toast 1' }); 433 | 434 | rootScope.$digest(); 435 | 436 | expect(mock.callback).not.toHaveBeenCalled(); 437 | }); 438 | 439 | it('should invoke pass toast instance to onShowCallback', function () { 440 | compileContainer(); 441 | var toastSetByCallback = null; 442 | 443 | function callback(t) { 444 | toastSetByCallback = t; 445 | } 446 | 447 | toaster.pop({ type: 'info', body: 'toast 1', onShowCallback: callback }); 448 | 449 | rootScope.$digest(); 450 | 451 | expect(toastSetByCallback).not.toBeNull(); 452 | }); 453 | }); 454 | 455 | 456 | describe('removeToast', function () { 457 | it('should not remove toast if toastId does not match a toastId', function () { 458 | var container = compileContainer(); 459 | var scope = container.scope(); 460 | 461 | var toast1 = toaster.pop({ type: 'info', body: 'toast 1' }); 462 | var toast2 = toaster.pop({ type: 'info', body: 'toast 2' }); 463 | 464 | rootScope.$digest(); 465 | 466 | expect(scope.toasters.length).toBe(2); 467 | expect(scope.toasters[1].toastId).toBe(toast1.toastId) 468 | expect(scope.toasters[0].toastId).toBe(toast2.toastId) 469 | 470 | scope.removeToast(3); 471 | 472 | rootScope.$digest(); 473 | 474 | expect(scope.toasters.length).toBe(2); 475 | }); 476 | 477 | it('should invoke onHideCallback if it exists when toast is removed', function () { 478 | var container = compileContainer(); 479 | var scope = container.scope(); 480 | 481 | var mock = { 482 | callback : function () { } 483 | }; 484 | 485 | spyOn(mock, 'callback'); 486 | 487 | var toast = toaster.pop({ type: 'info', body: 'toast 1', onHideCallback: mock.callback }); 488 | 489 | rootScope.$digest(); 490 | scope.removeToast(toast.toastId); 491 | rootScope.$digest(); 492 | 493 | expect(mock.callback).toHaveBeenCalled(); 494 | }); 495 | 496 | it('should invoke pass toast instance to onHideCallback', function () { 497 | var container = compileContainer(); 498 | var scope = container.scope(); 499 | 500 | var toastSetByCallback = null; 501 | 502 | function callback(t) { 503 | toastSetByCallback = t; 504 | } 505 | 506 | var toast = toaster.pop({ type: 'info', body: 'toast 1', onHideCallback: callback }); 507 | 508 | rootScope.$digest(); 509 | scope.removeToast(toast.toastId); 510 | rootScope.$digest(); 511 | 512 | expect(toastSetByCallback).not.toBeNull(); 513 | }); 514 | 515 | it('should invoke onHideCallback if toast is removed by limit', function () { 516 | var container = angular.element( 517 | ''); 518 | 519 | $compile(container)(rootScope); 520 | rootScope.$digest(); 521 | 522 | var scope = container.scope(); 523 | 524 | var mock = { 525 | callback : function () { } 526 | }; 527 | 528 | spyOn(mock, 'callback'); 529 | 530 | toaster.pop({ type: 'info', body: 'first', onHideCallback: mock.callback }); 531 | toaster.pop({ type: 'info', body: 'second' }); 532 | 533 | rootScope.$digest(); 534 | 535 | expect(scope.toasters.length).toBe(2); 536 | expect(scope.toasters[0].body).toBe('second'); 537 | expect(scope.toasters[1].body).toBe('first'); 538 | 539 | toaster.pop({ type: 'info', body: 'third' }); 540 | 541 | rootScope.$digest(); 542 | 543 | expect(scope.toasters.length).toBe(2); 544 | expect(scope.toasters[0].body).toBe('third'); 545 | expect(scope.toasters[1].body).toBe('second'); 546 | 547 | expect(mock.callback).toHaveBeenCalled(); 548 | }); 549 | }); 550 | 551 | 552 | describe('scope._onNewTest', function () { 553 | it('should not add toast if toasterId is passed to scope._onNewToast but toasterId is not set via config', function () { 554 | var container = compileContainer(); 555 | var scope = container.scope(); 556 | 557 | expect(scope.config.toasterId).toBeUndefined(); 558 | 559 | toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 }); 560 | 561 | rootScope.$digest(); 562 | 563 | expect(scope.toasters.length).toBe(0); 564 | }); 565 | 566 | it('should add toast if toasterId is passed to scope._onNewToast and toasterId is set via config', function () { 567 | var container = angular.element( 568 | ''); 569 | 570 | $compile(container)(rootScope); 571 | rootScope.$digest(); 572 | var scope = container.scope(); 573 | 574 | expect(scope.config.toasterId).toBe(1); 575 | 576 | toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 }); 577 | 578 | rootScope.$digest(); 579 | 580 | expect(scope.toasters.length).toBe(1); 581 | }); 582 | 583 | it('should add toasts to their respective container based on toasterId', function () { 584 | var container1 = angular.element( 585 | ''); 586 | var container2 = angular.element( 587 | ''); 588 | 589 | $compile(container1)(rootScope); 590 | $compile(container2)(rootScope); 591 | rootScope.$digest(); 592 | 593 | var scope1 = container1.scope(); 594 | var scope2 = container2.scope(); 595 | 596 | toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 }); 597 | toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2 }); 598 | 599 | rootScope.$digest(); 600 | 601 | expect(scope1.toasters.length).toBe(1); 602 | expect(scope2.toasters.length).toBe(1); 603 | }); 604 | }); 605 | 606 | describe('scope._onClearToasts', function (){ 607 | it('should remove all toasts from all containers if toasterId is *', function () { 608 | var container1 = angular.element( 609 | ''); 610 | var container2 = angular.element( 611 | ''); 612 | 613 | $compile(container1)(rootScope); 614 | $compile(container2)(rootScope); 615 | rootScope.$digest(); 616 | 617 | var scope1 = container1.scope(); 618 | var scope2 = container2.scope(); 619 | 620 | toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 }); 621 | toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2 }); 622 | 623 | rootScope.$digest(); 624 | 625 | expect(scope1.toasters.length).toBe(1); 626 | expect(scope2.toasters.length).toBe(1); 627 | 628 | toaster.clear('*'); 629 | 630 | rootScope.$digest(); 631 | 632 | expect(scope1.toasters.length).toBe(0); 633 | expect(scope2.toasters.length).toBe(0); 634 | }); 635 | 636 | it('should remove all toasts from all containers if config.toasterId and toastId are undefined', function () { 637 | var container1 = angular.element( 638 | ''); 639 | var container2 = angular.element( 640 | ''); 641 | 642 | $compile(container1)(rootScope); 643 | $compile(container2)(rootScope); 644 | rootScope.$digest(); 645 | 646 | var scope1 = container1.scope(); 647 | var scope2 = container2.scope(); 648 | 649 | toaster.pop({ type: 'info', body: 'toast 1' }); 650 | toaster.pop({ type: 'info', body: 'toast 2' }); 651 | 652 | rootScope.$digest(); 653 | 654 | // since there are two separate instances of the container 655 | // without a toasterId, both receive the newToast event 656 | expect(scope1.toasters.length).toBe(2); 657 | expect(scope2.toasters.length).toBe(2); 658 | 659 | toaster.clear(); 660 | 661 | rootScope.$digest(); 662 | 663 | expect(scope1.toasters.length).toBe(0); 664 | expect(scope2.toasters.length).toBe(0); 665 | }); 666 | 667 | it('should not remove by toasterId / toastId from the correct container if toast.toasterId is defined and toast.toastId is undefined', function () { 668 | var container1 = angular.element( 669 | ''); 670 | var container2 = angular.element( 671 | ''); 672 | 673 | $compile(container1)(rootScope); 674 | $compile(container2)(rootScope); 675 | rootScope.$digest(); 676 | 677 | var scope1 = container1.scope(); 678 | var scope2 = container2.scope(); 679 | 680 | // removeAllToasts explicitly looks for toast.uid, which is only set 681 | // if toastId is passed as a parameter 682 | toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1 }); 683 | toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2 }); 684 | toaster.pop({ type: 'info', body: 'toast 3', toasterId: 2 }); 685 | 686 | rootScope.$digest(); 687 | 688 | expect(scope1.toasters.length).toBe(1); 689 | expect(scope2.toasters.length).toBe(2); 690 | 691 | toaster.clear(2, 1); 692 | 693 | rootScope.$digest(); 694 | 695 | expect(scope1.toasters.length).toBe(1); 696 | expect(scope2.toasters.length).toBe(2); 697 | }); 698 | 699 | it('should remove by toasterId / toastId from the correct container if toasterId is defined and toastId is defined', function () { 700 | var container1 = angular.element( 701 | ''); 702 | var container2 = angular.element( 703 | ''); 704 | 705 | $compile(container1)(rootScope); 706 | $compile(container2)(rootScope); 707 | rootScope.$digest(); 708 | 709 | var scope1 = container1.scope(); 710 | var scope2 = container2.scope(); 711 | 712 | // removeAllToasts explicitly looks for toast.uid, which is only set 713 | // if toastId is passed as a parameter 714 | var toast1 = toaster.pop({ type: 'info', body: 'toast 1', toasterId: 1, toastId: 1 }); 715 | var toast2 = toaster.pop({ type: 'info', body: 'toast 2', toasterId: 2, toastId: 1 }); 716 | var toast3 = toaster.pop({ type: 'info', body: 'toast 3', toasterId: 2, toastId: 2 }); 717 | 718 | rootScope.$digest(); 719 | 720 | expect(scope1.toasters.length).toBe(1); 721 | expect(scope2.toasters.length).toBe(2); 722 | 723 | toaster.clear(2, toast2.toastId); 724 | 725 | rootScope.$digest(); 726 | 727 | expect(scope1.toasters.length).toBe(1); 728 | expect(scope2.toasters.length).toBe(1); 729 | }); 730 | }); 731 | }); 732 | 733 | 734 | describe('toastContainer -> addToast -> bodyOutputType.html with ngSanitize', function () { 735 | beforeEach(function () { 736 | module('ngSanitize'); 737 | module('toaster'); 738 | 739 | // inject the toaster service 740 | inject(function (_toaster_, _$rootScope_, _$compile_, _$sanitize_) { 741 | toaster = _toaster_; 742 | rootScope = _$rootScope_; 743 | $compile = _$compile_; 744 | $sanitize = _$sanitize_; 745 | }); 746 | }); 747 | 748 | it('should render \'html\' bodyOutputType if content is safe', function () { 749 | var container = compileContainer(); 750 | 751 | toaster.pop({ bodyOutputType: 'html', body: '
Body
' }); 752 | 753 | rootScope.$digest(); 754 | 755 | var body = container[0].querySelector('.toast-message div'); 756 | 757 | expect(body.innerHTML).toBe('
Body
'); 758 | }); 759 | 760 | it('should render \'html\' bodyOutputType removing cusotm element tags', function () { 761 | var container = compileContainer(); 762 | 763 | toaster.pop({ bodyOutputType: 'html', body: 'Body' }); 764 | 765 | rootScope.$digest(); 766 | 767 | var body = container[0].querySelector('.toast-message div'); 768 | 769 | expect(body.innerHTML).toBe('Body'); 770 | }); 771 | 772 | it('should render \'html\' bodyOutputType removing ids', function () { 773 | var container = compileContainer(); 774 | 775 | toaster.pop({ bodyOutputType: 'html', body: '
Body
' }); 776 | 777 | rootScope.$digest(); 778 | 779 | var body = container[0].querySelector('.toast-message div'); 780 | 781 | expect(body.innerHTML).toBe('
Body
'); 782 | }); 783 | 784 | it('should remove unsafe attributes if \'html\' bodyOutputType', function () { 785 | var container = compileContainer(); 786 | 787 | toaster.pop({ bodyOutputType: 'html', body: '

an html\n' + 788 | 'click here\n' + 789 | 'snippet

' }); 790 | 791 | rootScope.$digest(); 792 | 793 | var body = container[0].querySelector('em'); 794 | 795 | expect(body.outerHTML).toEqual('click here'); 796 | }); 797 | }); 798 | 799 | describe('toastContainer -> addToast -> bodyOutputType.html without ngSanitize', function () { 800 | beforeEach(function () { 801 | module('toaster'); 802 | 803 | // inject the toaster service 804 | inject(function (_toaster_, _$rootScope_, _$compile_) { 805 | toaster = _toaster_; 806 | rootScope = _$rootScope_; 807 | $compile = _$compile_; 808 | }); 809 | }); 810 | 811 | it('should throw exception \'html\' bodyOutputType if content is safe', function () { 812 | var container = compileContainer(); 813 | 814 | toaster.pop({ bodyOutputType: 'html', body: '
Body
' }); 815 | 816 | var wasError = false; 817 | try { 818 | rootScope.$digest(); 819 | } catch (e) { 820 | expect(e.message.includes('Attempting to use an unsafe value in a safe context')).toBeTruthy(); 821 | wasError = true; 822 | } 823 | 824 | expect(wasError).toEqual(true); 825 | expect(container[0].querySelector('div[ng-switch-when="html"]').innerHTML).toEqual(''); 826 | }); 827 | 828 | it('should throw exception \'html\' bodyOutputType if content is unsafe', function () { 829 | var container = compileContainer(); 830 | 831 | toaster.pop({ bodyOutputType: 'html', body: '

an html\n' + 832 | 'click here\n' + 833 | 'snippet

' }); 834 | 835 | var wasError = false; 836 | try { 837 | rootScope.$digest(); 838 | } catch (e) { 839 | expect(e.message.includes('Attempting to use an unsafe value in a safe context')).toBeTruthy(); 840 | wasError = true; 841 | } 842 | 843 | expect(wasError).toEqual(true); 844 | expect(container[0].querySelector('div[ng-switch-when="html"]').innerHTML).toEqual(''); 845 | }); 846 | }); 847 | 848 | describe('toasterContainer', function () { 849 | var $interval, $intervalSpy; 850 | 851 | inject(function (_$interval_) { 852 | $interval = _$interval_; 853 | }); 854 | 855 | beforeEach(function () { 856 | $intervalSpy = jasmine.createSpy('$interval', $interval); 857 | 858 | module('toaster', function ($provide) { 859 | $provide.value('$interval', $intervalSpy); 860 | }); 861 | 862 | // inject the toaster service 863 | inject(function (_toaster_, _$rootScope_, _$compile_) { 864 | toaster = _toaster_; 865 | rootScope = _$rootScope_; 866 | $compile = _$compile_; 867 | }); 868 | }); 869 | 870 | it('should use the toast.timeout argument if it is a valid number', function () { 871 | var container = compileContainer(); 872 | var scope = container.scope(); 873 | 874 | spyOn(scope, 'configureTimer').and.callThrough(); 875 | 876 | toaster.pop({ timeout: 2 }); 877 | 878 | expect(scope.configureTimer).toHaveBeenCalled(); 879 | expect(scope.configureTimer.calls.allArgs()[0][0].timeout).toBe(2); 880 | expect($intervalSpy.calls.first().args[1]).toBe(2) 881 | }); 882 | 883 | it('should not use the toast.timeout argument if not a valid number', function () { 884 | var container = compileContainer(); 885 | var scope = container.scope(); 886 | 887 | spyOn(scope, 'configureTimer').and.callThrough(); 888 | 889 | toaster.pop({ timeout: "2" }); 890 | 891 | expect(scope.configureTimer).toHaveBeenCalled(); 892 | expect(scope.configureTimer.calls.allArgs()[0][0].timeout).toBe("2"); 893 | expect($intervalSpy.calls.first().args[1]).toBe(5000); 894 | }); 895 | 896 | it('should call scope.removeToast when toast.timeoutPromise expires', function () { 897 | var container = compileContainer(); 898 | var scope = container.scope(); 899 | 900 | spyOn(scope, 'removeToast').and.callThrough(); 901 | 902 | toaster.pop({ timeout: 2 }); 903 | 904 | $intervalSpy.calls.first().args[0](); 905 | 906 | rootScope.$digest(); 907 | 908 | expect(scope.removeToast).toHaveBeenCalled(); 909 | }); 910 | 911 | it('should retrieve timeout by toast type if mergedConfig toast-timeout is an object', function () { 912 | var element = angular.element( 913 | ''); 914 | 915 | $compile(element)(rootScope); 916 | rootScope.$digest(); 917 | 918 | toaster.pop({ type: 'info' }); 919 | 920 | expect($intervalSpy.calls.first().args[1]).toBe(5); 921 | }); 922 | 923 | it('should not set a timeout if mergedConfig toast-timeout is an object and does not match toast type', function () { 924 | // TODO: this seems to be a bug in the toast-timeout configuration option. 925 | // It should fall back to a default value if the toast type configuration 926 | // does not match the target toast type or throw an exception to warn of an 927 | // invalid configuration. 928 | 929 | var element = angular.element( 930 | ''); 931 | 932 | $compile(element)(rootScope); 933 | rootScope.$digest(); 934 | 935 | toaster.pop({ type: 'warning' }); 936 | 937 | expect($intervalSpy.calls.all().length).toBe(0); 938 | }); 939 | }); 940 | 941 | 942 | function compileContainer() { 943 | var element = angular.element(''); 944 | $compile(element)(rootScope); 945 | rootScope.$digest(); 946 | 947 | return element; 948 | } -------------------------------------------------------------------------------- /test/toasterEventRegistrySpec.js: -------------------------------------------------------------------------------- 1 | /* global describe global it global beforeEach global angular global inject global expect */ 2 | 3 | 'use strict'; 4 | 5 | describe('toasterEventRegistry', function () { 6 | var toaster, toasterConfig, toasterEventRegistry, rootScope, $compile; 7 | 8 | beforeEach(function () { 9 | // load dependencies 10 | module('testApp'); 11 | module('toaster') 12 | 13 | // inject the toaster service 14 | inject(function (_toaster_, _toasterConfig_, _toasterEventRegistry_, _$rootScope_, _$compile_) { 15 | toaster = _toaster_; 16 | toasterConfig = _toasterConfig_; 17 | toasterEventRegistry = _toasterEventRegistry_; 18 | rootScope = _$rootScope_; 19 | $compile = _$compile_; 20 | }); 21 | }); 22 | 23 | it('unsubscribeToNewToastEvent will throw error if newToastEventSubscribers is empty and deregisterNewToast is not defined', function () { 24 | var hasError = false; 25 | 26 | try { 27 | toasterEventRegistry.unsubscribeToNewToastEvent(function () {}); 28 | } catch(e) { 29 | expect(e.message.indexOf(' is not a function')).toBeGreaterThan(-1); 30 | hasError = true; 31 | } 32 | 33 | expect(hasError).toBe(true); 34 | }); 35 | 36 | it('unsubscribeToNewToastEvent will not splice if index not found and will not throw error', function () { 37 | var hasError = false; 38 | var fakeHandler = function (fakeHandlerId) {}; 39 | toasterEventRegistry.subscribeToNewToastEvent(fakeHandler); 40 | 41 | try { 42 | toasterEventRegistry.unsubscribeToNewToastEvent(function () {}); 43 | } catch(e) { 44 | hasError = true; 45 | } 46 | 47 | expect(hasError).toBe(false); 48 | }); 49 | 50 | it('unsubscribeToClearToastsEvent will throw error if clearToastsEventSubscribers is empty and deregisterClearToasts is not defined', function () { 51 | var hasError = false; 52 | 53 | try { 54 | toasterEventRegistry.unsubscribeToClearToastsEvent(function () {}); 55 | } catch(e) { 56 | expect(e.message.indexOf(' is not a function')).toBeGreaterThan(-1); 57 | hasError = true; 58 | } 59 | 60 | expect(hasError).toBe(true); 61 | }); 62 | 63 | it('unsubscribeToClearToastsEvent will not splice if index not found and will not throw error', function () { 64 | var hasError = false; 65 | var fakeHandler = function (fakeHandlerId) {}; 66 | toasterEventRegistry.subscribeToClearToastsEvent(fakeHandler); 67 | 68 | try { 69 | toasterEventRegistry.unsubscribeToClearToastsEvent(function () {}); 70 | } catch(e) { 71 | hasError = true; 72 | } 73 | 74 | expect(hasError).toBe(false); 75 | }); 76 | }); -------------------------------------------------------------------------------- /test/toasterServiceSpec.js: -------------------------------------------------------------------------------- 1 | /* global describe global it global beforeEach global angular global inject global expect */ 2 | 3 | 'use strict'; 4 | 5 | describe('toasterService', function () { 6 | var toaster, toasterConfig, rootScope, $compile; 7 | 8 | beforeEach(function () { 9 | // load dependencies 10 | module('testApp'); 11 | module('toaster') 12 | 13 | // inject the toaster service 14 | inject(function (_toaster_, _toasterConfig_, _$rootScope_, _$compile_) { 15 | toaster = _toaster_; 16 | toasterConfig = _toasterConfig_; 17 | rootScope = _$rootScope_; 18 | $compile = _$compile_; 19 | }); 20 | }); 21 | 22 | 23 | it('should create an error method from error icon class', function () { 24 | var container = angular.element(''); 25 | 26 | $compile(container)(rootScope); 27 | rootScope.$digest(); 28 | var scope = container.scope(); 29 | 30 | expect(scope.toasters.length).toBe(0) 31 | 32 | expect(toasterConfig['icon-classes'].error).toBe('toast-error'); 33 | 34 | toaster.error('test', 'test'); 35 | 36 | rootScope.$digest(); 37 | 38 | expect(scope.toasters.length).toBe(1) 39 | expect(scope.toasters[0].type).toBe('toast-error'); 40 | }); 41 | 42 | it('should create an info method from info icon class', function () { 43 | var container = angular.element(''); 44 | 45 | $compile(container)(rootScope); 46 | rootScope.$digest(); 47 | var scope = container.scope(); 48 | 49 | expect(scope.toasters.length).toBe(0) 50 | 51 | expect(toasterConfig['icon-classes'].info).toBe('toast-info'); 52 | 53 | toaster.info('test', 'test'); 54 | 55 | rootScope.$digest(); 56 | 57 | expect(scope.toasters.length).toBe(1) 58 | expect(scope.toasters[0].type).toBe('toast-info'); 59 | }); 60 | 61 | it('should create an wait method from wait icon class', function () { 62 | var container = angular.element(''); 63 | 64 | $compile(container)(rootScope); 65 | rootScope.$digest(); 66 | var scope = container.scope(); 67 | 68 | expect(scope.toasters.length).toBe(0) 69 | 70 | expect(toasterConfig['icon-classes'].wait).toBe('toast-wait'); 71 | 72 | toaster.wait('test', 'test'); 73 | 74 | rootScope.$digest(); 75 | 76 | expect(scope.toasters.length).toBe(1) 77 | expect(scope.toasters[0].type).toBe('toast-wait'); 78 | }); 79 | 80 | it('should create an success method from success icon class', function () { 81 | var container = angular.element(''); 82 | 83 | $compile(container)(rootScope); 84 | rootScope.$digest(); 85 | var scope = container.scope(); 86 | 87 | expect(scope.toasters.length).toBe(0) 88 | 89 | expect(toasterConfig['icon-classes'].success).toBe('toast-success'); 90 | 91 | toaster.success('test', 'test'); 92 | 93 | rootScope.$digest(); 94 | 95 | expect(scope.toasters.length).toBe(1) 96 | expect(scope.toasters[0].type).toBe('toast-success'); 97 | }); 98 | 99 | it('should create an warning method from warning icon class', function () { 100 | var container = angular.element(''); 101 | 102 | $compile(container)(rootScope); 103 | rootScope.$digest(); 104 | var scope = container.scope(); 105 | 106 | expect(scope.toasters.length).toBe(0) 107 | 108 | expect(toasterConfig['icon-classes'].warning).toBe('toast-warning'); 109 | 110 | toaster.warning('test', 'test'); 111 | 112 | rootScope.$digest(); 113 | 114 | expect(scope.toasters.length).toBe(1) 115 | expect(scope.toasters[0].type).toBe('toast-warning'); 116 | }); 117 | 118 | it('should create a method from the icon class that takes an object', function () { 119 | var container = angular.element(''); 120 | 121 | $compile(container)(rootScope); 122 | rootScope.$digest(); 123 | var scope = container.scope(); 124 | 125 | expect(scope.toasters.length).toBe(0) 126 | 127 | expect(toasterConfig['icon-classes'].error).toBe('toast-error'); 128 | 129 | toaster.error({ title: 'test', body: 'test'}); 130 | 131 | rootScope.$digest(); 132 | 133 | expect(scope.toasters.length).toBe(1) 134 | expect(scope.toasters[0].type).toBe('toast-error'); 135 | }); 136 | 137 | it('should return a toast wrapper instance from pop', function () { 138 | var container = angular.element(''); 139 | 140 | $compile(container)(rootScope); 141 | rootScope.$digest(); 142 | 143 | var toast = toaster.pop('success', 'title', 'body'); 144 | expect(toast).toBeDefined(); 145 | expect(angular.isObject(toast)).toBe(true); 146 | expect(angular.isUndefined(toast.toasterId)).toBe(true); 147 | expect(toast.toastId).toBe(container.scope().toasters[0].toastId); 148 | }); 149 | 150 | it('should return a toast wrapper instance from each helper function', function () { 151 | var container = angular.element(''); 152 | 153 | $compile(container)(rootScope); 154 | rootScope.$digest(); 155 | 156 | var errorToast = toaster.error('title', 'body'); 157 | var infoToast = toaster.info('title', 'body'); 158 | var waitToast = toaster.wait('title', 'body'); 159 | var successToast = toaster.success('title', 'body'); 160 | var warningToast = toaster.warning('title', 'body'); 161 | 162 | expect(errorToast).toBeDefined(); 163 | expect(infoToast).toBeDefined(); 164 | expect(waitToast).toBeDefined(); 165 | expect(successToast).toBeDefined(); 166 | expect(warningToast).toBeDefined(); 167 | }); 168 | 169 | it('clear should take toast wrapper returned from pop', function () { 170 | var container = angular.element(''); 171 | 172 | $compile(container)(rootScope); 173 | rootScope.$digest(); 174 | var scope = container.scope(); 175 | 176 | var toast = toaster.pop('success', 'title', 'body'); 177 | expect(scope.toasters.length).toBe(1); 178 | toaster.clear(toast); 179 | expect(scope.toasters.length).toBe(0); 180 | }); 181 | 182 | it('clear should take individual arguments from toast wrapper returned from pop', function () { 183 | var container = angular.element(''); 184 | 185 | $compile(container)(rootScope); 186 | rootScope.$digest(); 187 | var scope = container.scope(); 188 | 189 | var toast = toaster.pop('success', 'title', 'body'); 190 | expect(scope.toasters.length).toBe(1); 191 | toaster.clear(toast.toasterId, toast.toastId); 192 | expect(scope.toasters.length).toBe(0); 193 | }); 194 | }); -------------------------------------------------------------------------------- /toaster.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Toastr 3 | * Version 2.0.1 4 | * Copyright 2012 John Papa and Hans Fjallemark. 5 | * All Rights Reserved. 6 | * Use, reproduction, distribution, and modification of this code is subject to the terms and 7 | * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Author: John Papa and Hans Fjallemark 10 | * Project: https://github.com/CodeSeven/toastr 11 | */ 12 | .toast-title { 13 | font-weight: bold; 14 | } 15 | .toast-message { 16 | -ms-word-wrap: break-word; 17 | word-wrap: break-word; 18 | } 19 | .toast-message a, 20 | .toast-message label { 21 | color: #ffffff; 22 | } 23 | .toast-message a:hover { 24 | color: #cccccc; 25 | text-decoration: none; 26 | } 27 | 28 | .toast-close-button { 29 | position: relative; 30 | right: -0.3em; 31 | top: -0.3em; 32 | float: right; 33 | font-size: 20px; 34 | font-weight: bold; 35 | color: #ffffff; 36 | -webkit-text-shadow: 0 1px 0 #ffffff; 37 | text-shadow: 0 1px 0 #ffffff; 38 | opacity: 0.8; 39 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 40 | filter: alpha(opacity=80); 41 | } 42 | .toast-close-button:hover, 43 | .toast-close-button:focus { 44 | color: #000000; 45 | text-decoration: none; 46 | cursor: pointer; 47 | opacity: 0.4; 48 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); 49 | filter: alpha(opacity=40); 50 | } 51 | 52 | /*Additional properties for button version 53 | iOS requires the button element instead of an anchor tag. 54 | If you want the anchor version, it requires `href="#"`.*/ 55 | button.toast-close-button { 56 | padding: 0; 57 | cursor: pointer; 58 | background: transparent; 59 | border: 0; 60 | -webkit-appearance: none; 61 | } 62 | .toast-top-full-width { 63 | top: 0; 64 | right: 0; 65 | width: 100%; 66 | } 67 | .toast-bottom-full-width { 68 | bottom: 0; 69 | right: 0; 70 | width: 100%; 71 | } 72 | .toast-top-left { 73 | top: 12px; 74 | left: 12px; 75 | } 76 | .toast-top-center { 77 | top: 12px; 78 | } 79 | .toast-top-right { 80 | top: 12px; 81 | right: 12px; 82 | } 83 | .toast-bottom-right { 84 | right: 12px; 85 | bottom: 12px; 86 | } 87 | .toast-bottom-center { 88 | bottom: 12px; 89 | } 90 | .toast-bottom-left { 91 | bottom: 12px; 92 | left: 12px; 93 | } 94 | .toast-center { 95 | top: 45%; 96 | } 97 | #toast-container { 98 | position: fixed; 99 | z-index: 999999; 100 | pointer-events: auto; 101 | /*overrides*/ 102 | 103 | } 104 | #toast-container.toast-center, 105 | #toast-container.toast-top-center, 106 | #toast-container.toast-bottom-center{ 107 | width: 100%; 108 | pointer-events: none; 109 | } 110 | #toast-container.toast-center > div, 111 | #toast-container.toast-top-center > div, 112 | #toast-container.toast-bottom-center > div{ 113 | margin-left: auto; 114 | margin-right: auto; 115 | pointer-events: auto; 116 | } 117 | #toast-container.toast-center > button, 118 | #toast-container.toast-top-center > button, 119 | #toast-container.toast-bottom-center > button{ 120 | pointer-events: auto; 121 | } 122 | #toast-container * { 123 | -moz-box-sizing: border-box; 124 | -webkit-box-sizing: border-box; 125 | box-sizing: border-box; 126 | } 127 | #toast-container > div { 128 | margin: 0 0 6px; 129 | padding: 15px 15px 15px 50px; 130 | width: 300px; 131 | -moz-border-radius: 3px 3px 3px 3px; 132 | -webkit-border-radius: 3px 3px 3px 3px; 133 | border-radius: 3px 3px 3px 3px; 134 | background-position: 15px center; 135 | background-repeat: no-repeat; 136 | -moz-box-shadow: 0 0 12px #999999; 137 | -webkit-box-shadow: 0 0 12px #999999; 138 | box-shadow: 0 0 12px #999999; 139 | color: #ffffff; 140 | opacity: 0.8; 141 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 142 | filter: alpha(opacity=80); 143 | } 144 | #toast-container > :hover { 145 | -moz-box-shadow: 0 0 12px #000000; 146 | -webkit-box-shadow: 0 0 12px #000000; 147 | box-shadow: 0 0 12px #000000; 148 | opacity: 1; 149 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 150 | filter: alpha(opacity=100); 151 | cursor: pointer; 152 | } 153 | #toast-container > .toast-info { 154 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; 155 | } 156 | #toast-container > .toast-wait { 157 | background-image: url("data:image/gif;base64,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") !important; 158 | } 159 | #toast-container > .toast-error { 160 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; 161 | } 162 | #toast-container > .toast-success { 163 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; 164 | } 165 | #toast-container > .toast-warning { 166 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; 167 | } 168 | #toast-container.toast-top-full-width > div, 169 | #toast-container.toast-bottom-full-width > div { 170 | width: 96%; 171 | margin-left: auto; 172 | margin-right: auto; 173 | } 174 | .toast { 175 | background-color: #030303; 176 | } 177 | .toast-success { 178 | background-color: #51a351; 179 | } 180 | .toast-error { 181 | background-color: #bd362f; 182 | } 183 | .toast-info { 184 | background-color: #2f96b4; 185 | } 186 | .toast-wait { 187 | background-color: #2f96b4; 188 | } 189 | .toast-warning { 190 | background-color: #f89406; 191 | } 192 | /*Responsive Design*/ 193 | @media all and (max-width: 240px) { 194 | #toast-container > div { 195 | padding: 8px 8px 8px 50px; 196 | width: 11em; 197 | } 198 | #toast-container .toast-close-button { 199 | right: -0.2em; 200 | top: -0.2em; 201 | } 202 | } 203 | @media all and (min-width: 241px) and (max-width: 480px) { 204 | #toast-container > div { 205 | padding: 8px 8px 8px 50px; 206 | width: 18em; 207 | } 208 | #toast-container .toast-close-button { 209 | right: -0.2em; 210 | top: -0.2em; 211 | } 212 | } 213 | @media all and (min-width: 481px) and (max-width: 768px) { 214 | #toast-container > div { 215 | padding: 15px 15px 15px 50px; 216 | width: 25em; 217 | } 218 | } 219 | 220 | /* 221 | * AngularJS-Toaster 222 | * Version 0.3 223 | */ 224 | :not(.no-enter)#toast-container > div.ng-enter, 225 | :not(.no-leave)#toast-container > div.ng-leave 226 | { 227 | -webkit-transition: 1000ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 228 | -moz-transition: 1000ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 229 | -ms-transition: 1000ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 230 | -o-transition: 1000ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 231 | transition: 1000ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; 232 | } 233 | 234 | :not(.no-enter)#toast-container > div.ng-enter.ng-enter-active, 235 | :not(.no-leave)#toast-container > div.ng-leave { 236 | opacity: 0.8; 237 | } 238 | 239 | :not(.no-leave)#toast-container > div.ng-leave.ng-leave-active, 240 | :not(.no-enter)#toast-container > div.ng-enter { 241 | opacity: 0; 242 | } -------------------------------------------------------------------------------- /toaster.js: -------------------------------------------------------------------------------- 1 | /* global angular */ 2 | /* 3 | * @license 4 | * AngularJS Toaster 5 | * Version: 3.0.0 6 | * 7 | * Copyright 2013-2019 Jiri Kavulak, Stabzs. 8 | * All Rights Reserved. 9 | * Use, reproduction, distribution, and modification of this code is subject to the terms and 10 | * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php 11 | * 12 | * Authors: Jiri Kavulak, Stabzs 13 | * Related to project of John Papa, Hans Fjällemark and Nguyễn Thiện Hùng (thienhung1989) 14 | */ 15 | (function(window, document) { 16 | 'use strict'; 17 | 18 | angular.module('toaster', []).constant( 19 | 'toasterConfig', { 20 | 'limit': 0, // limits max number of toasts 21 | 'tap-to-dismiss': true, 22 | 'close-button': false, 23 | 'close-html': '', 24 | 'newest-on-top': true, 25 | 'time-out': 5000, 26 | 'icon-classes': { 27 | error: 'toast-error', 28 | info: 'toast-info', 29 | wait: 'toast-wait', 30 | success: 'toast-success', 31 | warning: 'toast-warning' 32 | }, 33 | 'body-output-type': '', // Options: '', 'html', 'trustedHtml', 'template', 'templateWithData', 'directive' 34 | 'body-template': 'toasterBodyTmpl.html', 35 | 'icon-class': 'toast-info', 36 | 'position-class': 'toast-top-right', // Options (see CSS): 37 | // 'toast-top-full-width', 'toast-bottom-full-width', 'toast-center', 38 | // 'toast-top-left', 'toast-top-center', 'toast-top-right', 39 | // 'toast-bottom-left', 'toast-bottom-center', 'toast-bottom-right', 40 | 'title-class': 'toast-title', 41 | 'message-class': 'toast-message', 42 | 'prevent-duplicates': false, 43 | 'mouseover-timer-stop': true // stop timeout on mouseover and restart timer on mouseout 44 | } 45 | ).run(['$templateCache', function($templateCache) { 46 | $templateCache.put('angularjs-toaster/toast.html', 47 | '
' + 48 | '
' + 49 | '
' + 50 | '
{{toaster.title}}
' + 51 | '
' + 52 | '
' + 53 | '
' + 54 | '
' + 55 | '
' + 56 | '
' + 57 | '
{{toaster.body}}
' + 58 | '
' + 59 | '
' + 60 | '
'); 61 | } 62 | ]).service( 63 | 'toaster', [ 64 | '$rootScope', 'toasterConfig', function($rootScope, toasterConfig) { 65 | // http://stackoverflow.com/questions/26501688/a-typescript-guid-class 66 | var Guid = (function() { 67 | var Guid = {}; 68 | Guid.newGuid = function() { 69 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 70 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 71 | return v.toString(16); 72 | }); 73 | }; 74 | return Guid; 75 | }()); 76 | 77 | this.pop = function(type, title, body, timeout, bodyOutputType, clickHandler, toasterId, showCloseButton, toastId, onHideCallback) { 78 | if (angular.isObject(type)) { 79 | var params = type; // Enable named parameters as pop argument 80 | this.toast = { 81 | type: params.type, 82 | title: params.title, 83 | body: params.body, 84 | timeout: params.timeout, 85 | bodyOutputType: params.bodyOutputType, 86 | clickHandler: params.clickHandler, 87 | showCloseButton: params.showCloseButton, 88 | closeHtml: params.closeHtml, 89 | toastId: params.toastId, 90 | onShowCallback: params.onShowCallback, 91 | onHideCallback: params.onHideCallback, 92 | directiveData: params.directiveData, 93 | tapToDismiss: params.tapToDismiss 94 | }; 95 | toasterId = params.toasterId; 96 | } else { 97 | this.toast = { 98 | type: type, 99 | title: title, 100 | body: body, 101 | timeout: timeout, 102 | bodyOutputType: bodyOutputType, 103 | clickHandler: clickHandler, 104 | showCloseButton: showCloseButton, 105 | toastId: toastId, 106 | onHideCallback: onHideCallback 107 | }; 108 | } 109 | 110 | if (!this.toast.toastId || !this.toast.toastId.length) { 111 | this.toast.toastId = Guid.newGuid(); 112 | } 113 | 114 | $rootScope.$emit('toaster-newToast', toasterId, this.toast.toastId); 115 | 116 | return { 117 | toasterId: toasterId, 118 | toastId: this.toast.toastId 119 | }; 120 | }; 121 | 122 | this.clear = function(toasterId, toastId) { 123 | if (angular.isObject(toasterId)) { 124 | $rootScope.$emit('toaster-clearToasts', toasterId.toasterId, toasterId.toastId); 125 | } else { 126 | $rootScope.$emit('toaster-clearToasts', toasterId, toastId); 127 | } 128 | }; 129 | 130 | // Create one method per icon class, to allow to call toaster.info() and similar 131 | for (var type in toasterConfig['icon-classes']) { 132 | this[type] = createTypeMethod(type); 133 | } 134 | 135 | function createTypeMethod(toasterType) { 136 | return function(title, body, timeout, bodyOutputType, clickHandler, toasterId, showCloseButton, toastId, onHideCallback) { 137 | if (angular.isString(title)) { 138 | return this.pop( 139 | toasterType, 140 | title, 141 | body, 142 | timeout, 143 | bodyOutputType, 144 | clickHandler, 145 | toasterId, 146 | showCloseButton, 147 | toastId, 148 | onHideCallback); 149 | } else { // 'title' is actually an object with options 150 | return this.pop(angular.extend(title, { type: toasterType })); 151 | } 152 | }; 153 | } 154 | }] 155 | ).factory( 156 | 'toasterEventRegistry', [ 157 | '$rootScope', function($rootScope) { 158 | var deregisterNewToast = null, deregisterClearToasts = null, newToastEventSubscribers = [], clearToastsEventSubscribers = [], toasterFactory; 159 | 160 | toasterFactory = { 161 | setup: function() { 162 | if (!deregisterNewToast) { 163 | deregisterNewToast = $rootScope.$on( 164 | 'toaster-newToast', function(event, toasterId, toastId) { 165 | for (var i = 0, len = newToastEventSubscribers.length; i < len; i++) { 166 | newToastEventSubscribers[i](event, toasterId, toastId); 167 | } 168 | }); 169 | } 170 | 171 | if (!deregisterClearToasts) { 172 | deregisterClearToasts = $rootScope.$on( 173 | 'toaster-clearToasts', function(event, toasterId, toastId) { 174 | for (var i = 0, len = clearToastsEventSubscribers.length; i < len; i++) { 175 | clearToastsEventSubscribers[i](event, toasterId, toastId); 176 | } 177 | }); 178 | } 179 | }, 180 | 181 | subscribeToNewToastEvent: function(onNewToast) { 182 | newToastEventSubscribers.push(onNewToast); 183 | }, 184 | subscribeToClearToastsEvent: function(onClearToasts) { 185 | clearToastsEventSubscribers.push(onClearToasts); 186 | }, 187 | unsubscribeToNewToastEvent: function(onNewToast) { 188 | var index = newToastEventSubscribers.indexOf(onNewToast); 189 | if (index >= 0) { 190 | newToastEventSubscribers.splice(index, 1); 191 | } 192 | 193 | if (newToastEventSubscribers.length === 0) { 194 | deregisterNewToast(); 195 | deregisterNewToast = null; 196 | } 197 | }, 198 | unsubscribeToClearToastsEvent: function(onClearToasts) { 199 | var index = clearToastsEventSubscribers.indexOf(onClearToasts); 200 | if (index >= 0) { 201 | clearToastsEventSubscribers.splice(index, 1); 202 | } 203 | 204 | if (clearToastsEventSubscribers.length === 0) { 205 | deregisterClearToasts(); 206 | deregisterClearToasts = null; 207 | } 208 | } 209 | }; 210 | return { 211 | setup: toasterFactory.setup, 212 | subscribeToNewToastEvent: toasterFactory.subscribeToNewToastEvent, 213 | subscribeToClearToastsEvent: toasterFactory.subscribeToClearToastsEvent, 214 | unsubscribeToNewToastEvent: toasterFactory.unsubscribeToNewToastEvent, 215 | unsubscribeToClearToastsEvent: toasterFactory.unsubscribeToClearToastsEvent 216 | }; 217 | }] 218 | ) 219 | .directive('directiveTemplate', ['$compile', '$injector', function($compile, $injector) { 220 | return { 221 | restrict: 'A', 222 | scope: { 223 | directiveName: '@directiveName', 224 | directiveData: '=directiveData' 225 | }, 226 | replace: true, 227 | link: function(scope, elm, attrs) { 228 | scope.$watch('directiveName', function(directiveName) { 229 | if (angular.isUndefined(directiveName) || directiveName.length <= 0) 230 | throw new Error('A valid directive name must be provided via the toast body argument when using bodyOutputType: directive'); 231 | 232 | var directive; 233 | 234 | try { 235 | directive = $injector.get(attrs.$normalize(directiveName) + 'Directive'); 236 | } catch (e) { 237 | throw new Error(directiveName + ' could not be found. ' + 238 | 'The name should appear as it exists in the markup, not camelCased as it would appear in the directive declaration,' + 239 | ' e.g. directive-name not directiveName.'); 240 | } 241 | 242 | 243 | var directiveDetails = directive[0]; 244 | 245 | if (directiveDetails.scope !== true && directiveDetails.scope) { 246 | throw new Error('Cannot use a directive with an isolated scope. ' + 247 | 'The scope must be either true or falsy (e.g. false/null/undefined). ' + 248 | 'Occurred for directive ' + directiveName + '.'); 249 | } 250 | 251 | if (directiveDetails.restrict.indexOf('A') < 0) { 252 | throw new Error('Directives must be usable as attributes. ' + 253 | 'Add "A" to the restrict option (or remove the option entirely). Occurred for directive ' + 254 | directiveName + '.'); 255 | } 256 | 257 | if (scope.directiveData) 258 | scope.directiveData = angular.fromJson(scope.directiveData); 259 | 260 | var template = $compile('
')(scope); 261 | 262 | elm.append(template); 263 | }); 264 | } 265 | }; 266 | }]) 267 | .directive( 268 | 'toasterContainer', [ 269 | '$parse', '$rootScope', '$interval', '$sce', 'toasterConfig', 'toaster', 'toasterEventRegistry', 270 | function($parse, $rootScope, $interval, $sce, toasterConfig, toaster, toasterEventRegistry) { 271 | return { 272 | replace: true, 273 | restrict: 'EA', 274 | scope: true, // creates an internal scope for this directive (one per directive instance) 275 | link: function(scope, elm, attrs) { 276 | var mergedConfig; 277 | 278 | // Merges configuration set in directive with default one 279 | mergedConfig = angular.extend({}, toasterConfig, scope.$eval(attrs.toasterOptions)); 280 | 281 | scope.config = { 282 | toasterId: mergedConfig['toaster-id'], 283 | position: mergedConfig['position-class'], 284 | title: mergedConfig['title-class'], 285 | message: mergedConfig['message-class'], 286 | tap: mergedConfig['tap-to-dismiss'], 287 | closeButton: mergedConfig['close-button'], 288 | closeHtml: mergedConfig['close-html'], 289 | animation: mergedConfig['animation-class'], 290 | mouseoverTimer: mergedConfig['mouseover-timer-stop'] 291 | }; 292 | 293 | scope.$on( 294 | "$destroy", function() { 295 | toasterEventRegistry.unsubscribeToNewToastEvent(scope._onNewToast); 296 | toasterEventRegistry.unsubscribeToClearToastsEvent(scope._onClearToasts); 297 | } 298 | ); 299 | 300 | function setTimeout(toast, time) { 301 | toast.timeoutPromise = $interval( 302 | function() { 303 | scope.removeToast(toast.toastId); 304 | }, time, 1 305 | ); 306 | } 307 | 308 | scope.configureTimer = function(toast) { 309 | var timeout = angular.isNumber(toast.timeout) ? toast.timeout : mergedConfig['time-out']; 310 | if (typeof timeout === "object") timeout = timeout[toast.type]; 311 | if (timeout > 0) { 312 | setTimeout(toast, timeout); 313 | } 314 | }; 315 | 316 | function addToast(toast, toastId) { 317 | toast.type = mergedConfig['icon-classes'][toast.type]; 318 | if (!toast.type) { 319 | toast.type = mergedConfig['icon-class']; 320 | } 321 | 322 | if (mergedConfig['prevent-duplicates'] === true && scope.toasters.length) { 323 | if (scope.toasters[scope.toasters.length - 1].body === toast.body) { 324 | return; 325 | } else { 326 | var i, len, dupFound = false; 327 | for (i = 0, len = scope.toasters.length; i < len; i++) { 328 | if (scope.toasters[i].toastId === toastId) { 329 | dupFound = true; 330 | break; 331 | } 332 | } 333 | 334 | if (dupFound) return; 335 | } 336 | } 337 | 338 | 339 | // set the showCloseButton property on the toast so that 340 | // each template can bind directly to the property to show/hide 341 | // the close button 342 | var closeButton = mergedConfig['close-button']; 343 | 344 | // if toast.showCloseButton is a boolean value, 345 | // it was specifically overriden in the pop arguments 346 | if (typeof toast.showCloseButton === "boolean") { 347 | 348 | } else if (typeof closeButton === "boolean") { 349 | toast.showCloseButton = closeButton; 350 | } else if (typeof closeButton === "object") { 351 | var closeButtonForType = closeButton[toast.type]; 352 | 353 | if (typeof closeButtonForType !== "undefined" && closeButtonForType !== null) { 354 | toast.showCloseButton = closeButtonForType; 355 | } 356 | } else { 357 | // if an option was not set, default to false. 358 | toast.showCloseButton = false; 359 | } 360 | 361 | if (toast.showCloseButton) { 362 | toast.closeHtml = $sce.trustAsHtml(toast.closeHtml || scope.config.closeHtml); 363 | } 364 | 365 | // Set the toast.bodyOutputType to the default if it isn't set 366 | toast.bodyOutputType = toast.bodyOutputType || mergedConfig['body-output-type']; 367 | switch (toast.bodyOutputType) { 368 | case 'trustedHtml': 369 | toast.html = $sce.trustAsHtml(toast.body); 370 | break; 371 | case 'template': 372 | toast.bodyTemplate = toast.body || mergedConfig['body-template']; 373 | break; 374 | case 'templateWithData': 375 | var fcGet = $parse(toast.body || mergedConfig['body-template']); 376 | var templateWithData = fcGet(scope); 377 | toast.bodyTemplate = templateWithData.template; 378 | toast.data = templateWithData.data; 379 | break; 380 | case 'directive': 381 | toast.html = toast.body; 382 | break; 383 | } 384 | 385 | scope.configureTimer(toast); 386 | 387 | if (mergedConfig['newest-on-top'] === true) { 388 | scope.toasters.unshift(toast); 389 | if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) { 390 | removeToast(scope.toasters.length - 1); 391 | } 392 | } else { 393 | scope.toasters.push(toast); 394 | if (mergedConfig['limit'] > 0 && scope.toasters.length > mergedConfig['limit']) { 395 | removeToast(0); 396 | } 397 | } 398 | 399 | if (angular.isFunction(toast.onShowCallback)) { 400 | toast.onShowCallback(toast); 401 | } 402 | } 403 | 404 | scope.removeToast = function(toastId) { 405 | var i, len; 406 | for (i = 0, len = scope.toasters.length; i < len; i++) { 407 | if (scope.toasters[i].toastId === toastId) { 408 | removeToast(i); 409 | break; 410 | } 411 | } 412 | }; 413 | 414 | function removeToast(toastIndex) { 415 | var toast = scope.toasters[toastIndex]; 416 | 417 | // toast is always defined since the index always has a match 418 | if (toast.timeoutPromise) { 419 | $interval.cancel(toast.timeoutPromise); 420 | } 421 | scope.toasters.splice(toastIndex, 1); 422 | 423 | if (angular.isFunction(toast.onHideCallback)) { 424 | toast.onHideCallback(toast); 425 | } 426 | } 427 | 428 | function removeAllToasts(toastId) { 429 | for (var i = scope.toasters.length - 1; i >= 0; i--) { 430 | if (isUndefinedOrNull(toastId)) { 431 | removeToast(i); 432 | } else { 433 | if (scope.toasters[i].toastId == toastId) { 434 | removeToast(i); 435 | } 436 | } 437 | } 438 | } 439 | 440 | scope.toasters = []; 441 | 442 | function isUndefinedOrNull(val) { 443 | return angular.isUndefined(val) || val === null; 444 | } 445 | 446 | scope._onNewToast = function(event, toasterId, toastId) { 447 | // Compatibility: if toaster has no toasterId defined, and if call to display 448 | // hasn't either, then the request is for us 449 | if ((isUndefinedOrNull(scope.config.toasterId) && isUndefinedOrNull(toasterId)) || (!isUndefinedOrNull(scope.config.toasterId) && !isUndefinedOrNull(toasterId) && scope.config.toasterId == toasterId)) { 450 | addToast(toaster.toast, toastId); 451 | } 452 | }; 453 | scope._onClearToasts = function(event, toasterId, toastId) { 454 | // Compatibility: if toaster has no toasterId defined, and if call to display 455 | // hasn't either, then the request is for us 456 | if (toasterId == '*' || (isUndefinedOrNull(scope.config.toasterId) && isUndefinedOrNull(toasterId)) || (!isUndefinedOrNull(scope.config.toasterId) && !isUndefinedOrNull(toasterId) && scope.config.toasterId == toasterId)) { 457 | removeAllToasts(toastId); 458 | } 459 | }; 460 | 461 | toasterEventRegistry.setup(); 462 | 463 | toasterEventRegistry.subscribeToNewToastEvent(scope._onNewToast); 464 | toasterEventRegistry.subscribeToClearToastsEvent(scope._onClearToasts); 465 | }, 466 | controller: [ 467 | '$scope', '$element', '$attrs', function($scope, $element, $attrs) { 468 | // Called on mouseover 469 | $scope.stopTimer = function(toast) { 470 | if ($scope.config.mouseoverTimer === true) { 471 | if (toast.timeoutPromise) { 472 | $interval.cancel(toast.timeoutPromise); 473 | toast.timeoutPromise = null; 474 | } 475 | } 476 | }; 477 | 478 | // Called on mouseout 479 | $scope.restartTimer = function(toast) { 480 | if ($scope.config.mouseoverTimer === true) { 481 | if (!toast.timeoutPromise) { 482 | $scope.configureTimer(toast); 483 | } 484 | } else if (toast.timeoutPromise === null) { 485 | $scope.removeToast(toast.toastId); 486 | } 487 | }; 488 | 489 | $scope.click = function(event, toast, isCloseButton) { 490 | event.stopPropagation(); 491 | 492 | var tapToDismiss = typeof toast.tapToDismiss === "boolean" 493 | ? toast.tapToDismiss 494 | : $scope.config.tap; 495 | if (tapToDismiss === true || (toast.showCloseButton === true && isCloseButton === true)) { 496 | var removeToast = true; 497 | if (toast.clickHandler) { 498 | if (angular.isFunction(toast.clickHandler)) { 499 | removeToast = toast.clickHandler(toast, isCloseButton); 500 | } else if (angular.isFunction($scope.$parent.$eval(toast.clickHandler))) { 501 | removeToast = $scope.$parent.$eval(toast.clickHandler)(toast, isCloseButton); 502 | } else { 503 | console.log("TOAST-NOTE: Your click handler is not inside a parent scope of toaster-container."); 504 | } 505 | } 506 | if (removeToast) { 507 | $scope.removeToast(toast.toastId); 508 | } 509 | } 510 | }; 511 | }], 512 | templateUrl: 'angularjs-toaster/toast.html' 513 | }; 514 | }] 515 | ); 516 | })(window, document); 517 | -------------------------------------------------------------------------------- /toaster.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Toastr 3 | * Version 2.0.1 4 | * Copyright 2012 John Papa and Hans Fjallemark. 5 | * All Rights Reserved. 6 | * Use, reproduction, distribution, and modification of this code is subject to the terms and 7 | * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Author: John Papa and Hans Fjallemark 10 | * Project: https://github.com/CodeSeven/toastr 11 | */ 12 | .toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#fff;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-center{top:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-center{bottom:12px}.toast-bottom-left{bottom:12px;left:12px}.toast-center{top:45%}#toast-container{position:fixed;z-index:999999;pointer-events:auto}#toast-container.toast-bottom-center,#toast-container.toast-center,#toast-container.toast-top-center{width:100%;pointer-events:none}#toast-container.toast-bottom-center>div,#toast-container.toast-center>div,#toast-container.toast-top-center>div{margin-left:auto;margin-right:auto;pointer-events:auto}#toast-container.toast-bottom-center>button,#toast-container.toast-center>button,#toast-container.toast-top-center>button{pointer-events:auto}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px 3px 3px 3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-wait{background-image:url(data:image/gif;base64,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)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info,.toast-wait{background-color:#2f96b4}.toast-warning{background-color:#f89406}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}}:not(.no-enter)#toast-container>div.ng-enter,:not(.no-leave)#toast-container>div.ng-leave{-webkit-transition:1s cubic-bezier(.25,.25,.75,.75) all;-moz-transition:1s cubic-bezier(.25,.25,.75,.75) all;-ms-transition:1s cubic-bezier(.25,.25,.75,.75) all;-o-transition:1s cubic-bezier(.25,.25,.75,.75) all;transition:1s cubic-bezier(.25,.25,.75,.75) all}:not(.no-enter)#toast-container>div.ng-enter.ng-enter-active,:not(.no-leave)#toast-container>div.ng-leave{opacity:.8}:not(.no-enter)#toast-container>div.ng-enter,:not(.no-leave)#toast-container>div.ng-leave.ng-leave-active{opacity:0} -------------------------------------------------------------------------------- /toaster.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * AngularJS Toaster 4 | * Version: 3.0.0 5 | * 6 | * Copyright 2013-2019 Jiri Kavulak, Stabzs. 7 | * All Rights Reserved. 8 | * Use, reproduction, distribution, and modification of this code is subject to the terms and 9 | * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * Authors: Jiri Kavulak, Stabzs 12 | * Related to project of John Papa, Hans Fjällemark and Nguyễn Thiện Hùng (thienhung1989) 13 | */ 14 | !function(t,e){"use strict";angular.module("toaster",[]).constant("toasterConfig",{limit:0,"tap-to-dismiss":!0,"close-button":!1,"close-html":'',"newest-on-top":!0,"time-out":5e3,"icon-classes":{error:"toast-error",info:"toast-info",wait:"toast-wait",success:"toast-success",warning:"toast-warning"},"body-output-type":"","body-template":"toasterBodyTmpl.html","icon-class":"toast-info","position-class":"toast-top-right","title-class":"toast-title","message-class":"toast-message","prevent-duplicates":!1,"mouseover-timer-stop":!0}).run(["$templateCache",function(t){t.put("angularjs-toaster/toast.html",'
{{toaster.title}}
{{toaster.body}}
')}]).service("toaster",["$rootScope","toasterConfig",function(d,t){var m={newGuid:function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){var e=16*Math.random()|0;return("x"==t?e:3&e|8).toString(16)})}};for(var e in this.pop=function(t,e,o,s,a,i,n,r,c,l){if(angular.isObject(t)){var u=t;this.toast={type:u.type,title:u.title,body:u.body,timeout:u.timeout,bodyOutputType:u.bodyOutputType,clickHandler:u.clickHandler,showCloseButton:u.showCloseButton,closeHtml:u.closeHtml,toastId:u.toastId,onShowCallback:u.onShowCallback,onHideCallback:u.onHideCallback,directiveData:u.directiveData,tapToDismiss:u.tapToDismiss},n=u.toasterId}else this.toast={type:t,title:e,body:o,timeout:s,bodyOutputType:a,clickHandler:i,showCloseButton:r,toastId:c,onHideCallback:l};return this.toast.toastId&&this.toast.toastId.length||(this.toast.toastId=m.newGuid()),d.$emit("toaster-newToast",n,this.toast.toastId),{toasterId:n,toastId:this.toast.toastId}},this.clear=function(t,e){angular.isObject(t)?d.$emit("toaster-clearToasts",t.toasterId,t.toastId):d.$emit("toaster-clearToasts",t,e)},t["icon-classes"])this[e]=o(e);function o(l){return function(t,e,o,s,a,i,n,r,c){return angular.isString(t)?this.pop(l,t,e,o,s,a,i,n,r,c):this.pop(angular.extend(t,{type:l}))}}}]).factory("toasterEventRegistry",["$rootScope",function(t){var e,o=null,s=null,i=[],n=[];return{setup:(e={setup:function(){o||(o=t.$on("toaster-newToast",function(t,e,o){for(var s=0,a=i.length;s")(a);i.append(s)})}}}]).directive("toasterContainer",["$parse","$rootScope","$interval","$sce","toasterConfig","toaster","toasterEventRegistry",function(d,t,i,m,o,a,n){return{replace:!0,restrict:"EA",scope:!0,link:function(c,t,e){var l;function u(t){var e=c.toasters[t];e.timeoutPromise&&i.cancel(e.timeoutPromise),c.toasters.splice(t,1),angular.isFunction(e.onHideCallback)&&e.onHideCallback(e)}function s(t){return angular.isUndefined(t)||null===t}l=angular.extend({},o,c.$eval(e.toasterOptions)),c.config={toasterId:l["toaster-id"],position:l["position-class"],title:l["title-class"],message:l["message-class"],tap:l["tap-to-dismiss"],closeButton:l["close-button"],closeHtml:l["close-html"],animation:l["animation-class"],mouseoverTimer:l["mouseover-timer-stop"]},c.$on("$destroy",function(){n.unsubscribeToNewToastEvent(c._onNewToast),n.unsubscribeToClearToastsEvent(c._onClearToasts)}),c.configureTimer=function(t){var e,o,s=angular.isNumber(t.timeout)?t.timeout:l["time-out"];"object"==typeof s&&(s=s[t.type]),0l.limit&&u(c.toasters.length-1)):(c.toasters.push(t),0l.limit&&u(0)),angular.isFunction(t.onShowCallback)&&t.onShowCallback(t)}(a.toast,o)},c._onClearToasts=function(t,e,o){("*"==e||s(c.config.toasterId)&&s(e)||!s(c.config.toasterId)&&!s(e)&&c.config.toasterId==e)&&function(t){for(var e=c.toasters.length-1;0<=e;e--)s(t)?u(e):c.toasters[e].toastId==t&&u(e)}(o)},n.setup(),n.subscribeToNewToastEvent(c._onNewToast),n.subscribeToClearToastsEvent(c._onClearToasts)},controller:["$scope","$element","$attrs",function(a,t,e){a.stopTimer=function(t){!0===a.config.mouseoverTimer&&t.timeoutPromise&&(i.cancel(t.timeoutPromise),t.timeoutPromise=null)},a.restartTimer=function(t){!0===a.config.mouseoverTimer?t.timeoutPromise||a.configureTimer(t):null===t.timeoutPromise&&a.removeToast(t.toastId)},a.click=function(t,e,o){if(t.stopPropagation(),!0===("boolean"==typeof e.tapToDismiss?e.tapToDismiss:a.config.tap)||!0===e.showCloseButton&&!0===o){var s=!0;e.clickHandler&&(angular.isFunction(e.clickHandler)?s=e.clickHandler(e,o):angular.isFunction(a.$parent.$eval(e.clickHandler))?s=a.$parent.$eval(e.clickHandler)(e,o):console.log("TOAST-NOTE: Your click handler is not inside a parent scope of toaster-container.")),s&&a.removeToast(e.toastId)}}}],templateUrl:"angularjs-toaster/toast.html"}}])}(window,document); -------------------------------------------------------------------------------- /toaster.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Toastr 3 | * Version 2.0.1 4 | * Copyright 2012 John Papa and Hans Fjällemark. 5 | * All Rights Reserved. 6 | * Use, reproduction, distribution, and modification of this code is subject to the terms and 7 | * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Author: John Papa and Hans Fjällemark 10 | * Project: https://github.com/CodeSeven/toastr 11 | * 12 | * 13 | * SCSS File 14 | * Author: Damian Szymczuk 15 | * GitHub: https://github.com/dszymczuk 16 | * 17 | */ 18 | 19 | 20 | /* Variables */ 21 | $textColor: #ffffff !default; 22 | $textColorHover: #cccccc !default; 23 | $closeButton: #ffffff !default; 24 | $closeButtonHover: #000000 !default; 25 | 26 | $fontSize: 20px !default; 27 | 28 | $toast: #030303 !default; 29 | $toastSuccess: #51a351 !default; 30 | $toastError: #bd362f !default; 31 | $toastInfo: #2f96b4 !default; 32 | $toastWarning: #f89406 !default; 33 | 34 | 35 | $toastPositionFullWidthTop: 0 !default; 36 | $toastPositionFullWidthBottom: 0 !default; 37 | 38 | $toastPossitionTop: 12px !default; 39 | $toastPossitionLeft: 12px !default; 40 | $toastPossitionRight: 12px !default; 41 | $toastPossitionBottom: 12px !default; 42 | 43 | $toastContainerColor: #ffffff !default; 44 | $toastContainerShadowColor: #999999 !default; 45 | $toastContainerShadowColorHover: #000000 !default; 46 | 47 | 48 | .toast-title { 49 | font-weight: bold; 50 | } 51 | 52 | .toast-message { 53 | -ms-word-wrap: break-word; 54 | word-wrap: break-word; 55 | a, label { 56 | color: $textColor; 57 | } 58 | a:hover { 59 | color: $textColorHover; 60 | text-decoration: none; 61 | } 62 | } 63 | 64 | .toast-close-button { 65 | position: relative; 66 | right: -0.3em; 67 | top: -0.3em; 68 | float: right; 69 | font-size: $fontSize; 70 | font-weight: bold; 71 | color: $closeButton; 72 | -webkit-text-shadow: 0 1px 0 $closeButton; 73 | text-shadow: 0 1px 0 $closeButton; 74 | opacity: 0.8; 75 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 76 | filter: alpha(opacity = 80); 77 | &:hover, &:focus { 78 | color: $closeButtonHover; 79 | text-decoration: none; 80 | cursor: pointer; 81 | opacity: 0.4; 82 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); 83 | filter: alpha(opacity = 40); 84 | } 85 | } 86 | 87 | /*Additional properties for button version 88 | iOS requires the button element instead of an anchor tag. 89 | If you want the anchor version, it requires `href="#"`.*/ 90 | 91 | button.toast-close-button { 92 | padding: 0; 93 | cursor: pointer; 94 | background: transparent; 95 | border: 0; 96 | -webkit-appearance: none; 97 | } 98 | 99 | .toast-top-full-width { 100 | top: $toastPositionFullWidthTop; 101 | right: 0; 102 | width: 100%; 103 | } 104 | 105 | .toast-bottom-full-width { 106 | bottom: $toastPositionFullWidthBottom; 107 | right: 0; 108 | width: 100%; 109 | } 110 | 111 | .toast-top-left { 112 | top: $toastPossitionTop; 113 | left: $toastPossitionLeft; 114 | } 115 | 116 | .toast-top-center { 117 | top: $toastPossitionTop; 118 | } 119 | 120 | .toast-top-right { 121 | top: $toastPossitionTop; 122 | right: $toastPossitionRight; 123 | } 124 | 125 | .toast-bottom-right { 126 | right: $toastPossitionRight; 127 | bottom: $toastPossitionBottom; 128 | } 129 | 130 | .toast-bottom-center { 131 | bottom: $toastPossitionBottom; 132 | } 133 | 134 | .toast-bottom-left { 135 | bottom: $toastPossitionBottom; 136 | left: $toastPossitionLeft; 137 | } 138 | 139 | .toast-center { 140 | top: 45%; 141 | } 142 | 143 | #toast-container { 144 | position: fixed; 145 | z-index: 999999; 146 | /*overrides*/ 147 | &.toast-center, &.toast-top-center, &.toast-bottom-center { 148 | width: 100%; 149 | pointer-events: none; 150 | } 151 | &.toast-center > div, &.toast-top-center > div, &.toast-bottom-center > div { 152 | margin-left: auto; 153 | margin-right: auto; 154 | pointer-events: auto; 155 | } 156 | &.toast-center > button, &.toast-top-center > button, &.toast-bottom-center > button { 157 | pointer-events: auto; 158 | } 159 | * { 160 | -moz-box-sizing: border-box; 161 | -webkit-box-sizing: border-box; 162 | box-sizing: border-box; 163 | } 164 | > { 165 | div { 166 | margin: 0 0 6px; 167 | padding: 15px 15px 15px 50px; 168 | width: 300px; 169 | -moz-border-radius: 3px 3px 3px 3px; 170 | -webkit-border-radius: 3px 3px 3px 3px; 171 | border-radius: 3px 3px 3px 3px; 172 | background-position: 15px center; 173 | background-repeat: no-repeat; 174 | -moz-box-shadow: 0 0 12px $toastContainerShadowColor; 175 | -webkit-box-shadow: 0 0 12px $toastContainerShadowColor; 176 | box-shadow: 0 0 12px $toastContainerShadowColor; 177 | color: $toastContainerColor; 178 | opacity: 0.8; 179 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80); 180 | filter: alpha(opacity = 80); 181 | } 182 | :hover { 183 | -moz-box-shadow: 0 0 12px $toastContainerShadowColorHover; 184 | -webkit-box-shadow: 0 0 12px $toastContainerShadowColorHover; 185 | box-shadow: 0 0 12px $toastContainerShadowColorHover; 186 | opacity: 1; 187 | -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 188 | filter: alpha(opacity = 100); 189 | cursor: pointer; 190 | } 191 | .toast-info { 192 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; 193 | } 194 | .toast-wait { 195 | background-image: url("data:image/gif;base64,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") !important; 196 | } 197 | .toast-error { 198 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; 199 | } 200 | .toast-success { 201 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; 202 | } 203 | .toast-warning { 204 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; 205 | } 206 | } 207 | &.toast-top-full-width > div, &.toast-bottom-full-width > div { 208 | width: 96%; 209 | margin-left: auto; 210 | margin-right: auto; 211 | } 212 | } 213 | 214 | .toast { 215 | background-color: $toast; 216 | } 217 | 218 | .toast-success { 219 | background-color: $toastSuccess; 220 | } 221 | 222 | .toast-error { 223 | background-color: $toastError; 224 | } 225 | 226 | .toast-info, .toast-wait { 227 | background-color: $toastInfo; 228 | } 229 | 230 | .toast-warning { 231 | background-color: $toastWarning; 232 | } 233 | 234 | /*Responsive Design*/ 235 | @media all and (max-width: 240px) { 236 | #toast-container { 237 | > div { 238 | padding: 8px 8px 8px 50px; 239 | width: 11em; 240 | } 241 | .toast-close-button { 242 | right: -0.2em; 243 | top: -0.2em; 244 | } 245 | } 246 | } 247 | 248 | @media all and (min-width: 241px) and (max-width: 480px) { 249 | #toast-container { 250 | > div { 251 | padding: 8px 8px 8px 50px; 252 | width: 18em; 253 | } 254 | .toast-close-button { 255 | right: -0.2em; 256 | top: -0.2em; 257 | } 258 | } 259 | } 260 | 261 | @media all and (min-width: 481px) and (max-width: 768px) { 262 | #toast-container > div { 263 | padding: 15px 15px 15px 50px; 264 | width: 25em; 265 | } 266 | } 267 | 268 | /* 269 | * AngularJS-Toaster 270 | * Version 0.3 271 | */ 272 | 273 | :not(.no-enter)#toast-container > div.ng-enter, :not(.no-leave)#toast-container > div.ng-leave { 274 | -webkit-transition: 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75) all; 275 | -moz-transition: 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75) all; 276 | -ms-transition: 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75) all; 277 | -o-transition: 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75) all; 278 | transition: 1000ms cubic-bezier(0.25, 0.25, 0.75, 0.75) all; 279 | } 280 | 281 | :not(.no-enter)#toast-container > div.ng-enter.ng-enter-active { 282 | opacity: 0.8; 283 | } 284 | 285 | :not(.no-leave)#toast-container > div.ng-leave { 286 | opacity: 0.8; 287 | &.ng-leave-active { 288 | opacity: 0; 289 | } 290 | } 291 | 292 | :not(.no-enter)#toast-container > div.ng-enter { 293 | opacity: 0; 294 | } 295 | --------------------------------------------------------------------------------