├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── README.md ├── bower.json ├── dist ├── ngToast-animations.css ├── ngToast-animations.min.css ├── ngToast.css ├── ngToast.js ├── ngToast.min.css └── ngToast.min.js ├── index.html ├── package.json ├── src ├── scripts │ ├── directives.js │ ├── module.js │ └── provider.js └── styles │ ├── less │ ├── ngToast-animations.less │ ├── ngToast.less │ └── variables.less │ └── sass │ ├── ngToast.scss │ ├── ngtoast-animations.scss │ └── variables.scss └── test ├── .jshintrc ├── karma.conf.js ├── runner.html ├── spec ├── directive.js └── service.js └── vendor ├── angular-mocks.js ├── angular-sanitize.js └── angular.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | demo 4 | .tmp 5 | .sass-cache 6 | .idea 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "0.10" 3 | before_install: npm install -g grunt-cli 4 | install: npm install 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | var jsdiff = require('diff'); 3 | var fs = require('fs'); 4 | var _ = require('lodash'); 5 | var pkg = require('./package.json'); 6 | 7 | var paths = { 8 | dist: 'dist/', 9 | src: 'src/', 10 | sassCache: '.sass-cache/', 11 | sass: 'src/styles/sass/', 12 | less: 'src/styles/less/', 13 | scripts: 'src/scripts/', 14 | styles: 'src/styles/', 15 | test: 'test/', 16 | testCSS: 'test/css-files/', 17 | testLESS: 'test/css-files/less/', 18 | testSASS: 'test/css-files/sass/', 19 | }; 20 | 21 | var moduleName = _.camelCase(pkg.name); 22 | 23 | module.exports = function(grunt) { 24 | grunt.initConfig({ 25 | pkg: grunt.file.readJSON('package.json'), 26 | banner: '/*!\n' + 27 | ' * ngToast v<%= pkg.version %> (<%= pkg.homepage %>)\n' + 28 | ' * Copyright <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + 29 | ' * Licensed under <%= pkg.license %> (http://tameraydin.mit-license.org/)\n' + 30 | ' */\n', 31 | karma: { 32 | unit: { 33 | configFile: paths.test + 'karma.conf.js', 34 | singleRun: true 35 | } 36 | }, 37 | clean: { 38 | dist: { 39 | src: [paths.dist] 40 | }, 41 | sass: { 42 | src: [paths.sassCache] 43 | } 44 | }, 45 | concat: { 46 | options: { 47 | stripBanners: { 48 | 'block': true, 49 | 'line': true 50 | } 51 | }, 52 | dist: { 53 | src: [ 54 | paths.scripts + 'provider.js', 55 | paths.scripts + 'directives.js', 56 | paths.scripts + 'module.js' 57 | ], 58 | dest: paths.dist + moduleName + '.js' 59 | } 60 | }, 61 | sass: { 62 | dist: { 63 | files: [ 64 | { 65 | src: paths.sass + 'ngToast.scss', 66 | dest: paths.dist + 'ngToast.css' 67 | }, 68 | { 69 | src: paths.sass + 'ngToast-animations.scss', 70 | dest: paths.dist + 'ngToast-animations.css' 71 | } 72 | ] 73 | }, 74 | test: { 75 | files: [ 76 | { 77 | src: paths.sass + 'ngToast.scss', 78 | dest: paths.testSASS + 'ngToast.css' 79 | }, 80 | { 81 | src: paths.sass + 'ngToast-animations.scss', 82 | dest: paths.testSASS + 'ngToast-animations.css' 83 | } 84 | ] 85 | } 86 | }, 87 | less: { 88 | test: { 89 | files: [ 90 | { 91 | expand: true, 92 | cwd: paths.less, 93 | src: ['ngToast.less', 'ngToast-animations.less'], 94 | dest: paths.testLESS, 95 | ext: '.css' 96 | } 97 | ] 98 | } 99 | }, 100 | cssbeautifier: { 101 | files: [paths.testCSS + '**/*.css'] 102 | }, 103 | cssmin: { 104 | minify: { 105 | options: { 106 | keepSpecialComments: 0 107 | }, 108 | expand: true, 109 | src: paths.dist + '*.css', 110 | ext: '.min.css' 111 | } 112 | }, 113 | autoprefixer: { 114 | dist: { 115 | options: { 116 | browsers: ['last 2 versions'] 117 | }, 118 | expand: true, 119 | flatten: true, 120 | src: paths.dist + '*.css', 121 | dest: paths.dist 122 | } 123 | }, 124 | uglify: { 125 | build: { 126 | src: paths.dist + moduleName + '.js', 127 | dest: paths.dist + moduleName + '.min.js' 128 | } 129 | }, 130 | usebanner: { 131 | options: { 132 | position: 'top', 133 | banner: '<%= banner %>' 134 | }, 135 | files: { 136 | src: [ 137 | paths.dist + '*' 138 | ] 139 | } 140 | }, 141 | jshint: { 142 | options: { 143 | jshintrc: '.jshintrc' 144 | }, 145 | all: paths.scripts + '*.js' 146 | }, 147 | watch: { 148 | src: { 149 | files: [paths.src + '**/*.*'], 150 | tasks: [ 151 | 'default', 152 | ], 153 | options: { 154 | spawn: false, 155 | }, 156 | }, 157 | }, 158 | }); 159 | 160 | grunt.registerTask('test-generated-css', function() { 161 | this.requires('less:test'); 162 | this.requires('sass:test'); 163 | this.requires('cssbeautifier'); 164 | 165 | var sassBaseCSS = grunt.file.read(paths.testSASS + 'ngToast.css'); 166 | var sassAnimationsCSS = grunt.file.read(paths.testSASS + 'ngToast-animations.css'); 167 | var lessBaseCSS = grunt.file.read(paths.testLESS + 'ngToast.css'); 168 | var lessAnimationsCSS = grunt.file.read(paths.testLESS + 'ngToast-animations.css'); 169 | grunt.file.delete('test/css-files'); 170 | 171 | if (lessBaseCSS === sassBaseCSS && lessAnimationsCSS === sassAnimationsCSS) { 172 | // pass 173 | grunt.log.ok('LESS/SASS generated CSS matches.'); 174 | } else { 175 | // fail 176 | var headerFooter = 'SASS differences\n'.magenta + 'LESS differences\n\n'.blue; 177 | var baseDiff = jsdiff.diffCss(lessBaseCSS, sassBaseCSS); 178 | var animationDiff = jsdiff.diffCss(lessAnimationsCSS, sassAnimationsCSS); 179 | 180 | grunt.log.write(headerFooter); 181 | 182 | baseDiff.forEach(function(line) { 183 | var color = line.added ? 'magenta' : line.removed ? 'blue' : 'gray'; 184 | grunt.log.write(line.value[color]); 185 | }); 186 | 187 | animationDiff.forEach(function(line) { 188 | var color = line.added ? 'magenta' : line.removed ? 'blue' : 'gray'; 189 | grunt.log.write(line.value[color]); 190 | }); 191 | 192 | grunt.log.write(headerFooter); 193 | grunt.fail.warn('Generated LESS/SASS CSS does not match!', 6); 194 | } 195 | }); 196 | 197 | grunt.loadNpmTasks('grunt-contrib-uglify'); 198 | grunt.loadNpmTasks('grunt-contrib-jshint'); 199 | grunt.loadNpmTasks('grunt-contrib-clean'); 200 | grunt.loadNpmTasks('grunt-contrib-concat'); 201 | grunt.loadNpmTasks('grunt-sass'); 202 | grunt.loadNpmTasks('grunt-contrib-less'); 203 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 204 | grunt.loadNpmTasks('grunt-cssbeautifier'); 205 | grunt.loadNpmTasks('grunt-autoprefixer'); 206 | grunt.loadNpmTasks('grunt-karma'); 207 | grunt.loadNpmTasks('grunt-contrib-watch'); 208 | grunt.loadNpmTasks('grunt-banner'); 209 | grunt.registerTask('default', [ 210 | 'sass:test', 211 | 'clean:sass', 212 | 'less:test', 213 | 'cssbeautifier', 214 | 'test-generated-css', 215 | 'jshint', 216 | 'karma', 217 | 'clean:dist', 218 | 'concat', 219 | 'sass:dist', 220 | 'clean:sass', 221 | 'autoprefixer:dist', 222 | 'cssmin', 223 | 'uglify', 224 | 'usebanner' 225 | ]); 226 | }; 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ngToast [![Code Climate](https://img.shields.io/codeclimate/maintainability/tameraydin/ngToast.svg?style=flat-square&label=code%20climate)](https://codeclimate.com/github/tameraydin/ngToast/dist/ngToast.js) [![Build Status](https://img.shields.io/travis/tameraydin/ngToast/master.svg?style=flat-square)](https://travis-ci.org/tameraydin/ngToast) 2 | ======= 3 | 4 | ngToast is a simple Angular provider for toast notifications. 5 | 6 | **[Demo](http://tameraydin.github.io/ngToast)** 7 | 8 | ## Usage 9 | 10 | 1. Install via [Bower](http://bower.io/) or [NPM](http://www.npmjs.org): 11 | ```bash 12 | bower install ngtoast --production 13 | # or 14 | npm install ng-toast --production 15 | ``` 16 | or manually [download](https://github.com/tameraydin/ngToast/archive/master.zip). 17 | 18 | 2. Include ngToast source files and dependencies ([ngSanitize](http://docs.angularjs.org/api/ngSanitize), [Bootstrap CSS](http://getbootstrap.com/)): 19 | ```html 20 | 21 | 22 | 23 | 24 | 25 | ``` 26 | *Note: only the [Alerts](http://getbootstrap.com/components/#alerts) component is used as style base, so you don't have to include complete CSS* 27 | 28 | 3. Include ngToast as a dependency in your application module: 29 | ```javascript 30 | var app = angular.module('myApp', ['ngToast']); 31 | ``` 32 | 33 | 4. Place `toast` element into your HTML: 34 | ```html 35 | 36 | 37 | ... 38 | 39 | ``` 40 | 41 | 5. Inject ngToast provider in your controller: 42 | ```javascript 43 | app.controller('myCtrl', function(ngToast) { 44 | ngToast.create('a toast message...'); 45 | }); 46 | // for more info: http://tameraydin.github.io/ngToast/#api 47 | ``` 48 | 49 | ## Animations 50 | ngToast comes with optional animations. In order to enable animations in ngToast, you need to include [ngAnimate](http://docs.angularjs.org/api/ngAnimate) module into your app: 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | **Built-in** 57 | 1. Include the ngToast animation stylesheet: 58 | 59 | ```html 60 | 61 | ``` 62 | 63 | 2. Set the `animation` option. 64 | ```javascript 65 | app.config(['ngToastProvider', function(ngToastProvider) { 66 | ngToastProvider.configure({ 67 | animation: 'slide' // or 'fade' 68 | }); 69 | }]); 70 | ``` 71 | Built-in ngToast animations include `slide` & `fade`. 72 | 73 | **Custom** 74 | 75 | See the [plunker](http://plnkr.co/edit/wglAvsCuTLLykLNqVGwU) using [animate.css](http://daneden.github.io/animate.css/). 76 | 77 | 1. Using the `additionalClasses` option and [ngAnimate](http://docs.angularjs.org/api/ngAnimate) you can easily add your own animations or wire up 3rd party css animations. 78 | ```javascript 79 | app.config(['ngToastProvider', function(ngToastProvider) { 80 | ngToastProvider.configure({ 81 | additionalClasses: 'my-animation' 82 | }); 83 | }]); 84 | ``` 85 | 86 | 2. Then in your CSS (example using animate.css): 87 | ```css 88 | /* Add any vendor prefixes you need */ 89 | .my-animation.ng-enter { 90 | animation: flipInY 1s; 91 | } 92 | 93 | .my-animation.ng-leave { 94 | animation: flipOutY 1s; 95 | } 96 | ``` 97 | 98 | ## Settings & API 99 | 100 | Please find at the [project website](http://tameraydin.github.io/ngToast/#api). 101 | 102 | ## Development 103 | 104 | * Clone the repo or [download](https://github.com/tameraydin/ngToast/archive/master.zip) 105 | * Install dependencies: ``npm install && bower install`` 106 | * Run ``grunt watch``, play on **/src** 107 | * Build: ``grunt`` 108 | 109 | ## License 110 | 111 | MIT [http://tameraydin.mit-license.org/](http://tameraydin.mit-license.org/) 112 | 113 | ## Maintainers 114 | 115 | - [Tamer Aydin](http://tamerayd.in) 116 | - [Levi Thomason](http://www.levithomason.com) 117 | 118 | ## TODO 119 | - Add more unit & e2e tests 120 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngToast", 3 | "version": "2.0.0", 4 | "description": "Angular provider for toast notifications", 5 | "main": [ 6 | "dist/ngToast.js", 7 | "dist/ngToast.css" 8 | ], 9 | "keywords": [ 10 | "angular", 11 | "toast", 12 | "message", 13 | "notification", 14 | "toastr" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/tameraydin/ngToast.git" 19 | }, 20 | "homepage": "http://tameraydin.github.io/ngToast", 21 | "authors": [ 22 | "Tamer Aydin (http://tamerayd.in)", 23 | "Levi Thomason (http://www.levithomason.com)" 24 | ], 25 | "license": "MIT", 26 | "dependencies": { 27 | "angular": ">=1.2.15 <1.6", 28 | "angular-sanitize": ">=1.2.15 <1.6" 29 | }, 30 | "devDependencies": { 31 | "angular-animate": ">=1.2.17 <1.6", 32 | "bootstrap": "~3.3.2", 33 | "Faker": "~2.1.2" 34 | }, 35 | "ignore": [ 36 | "**/.*", 37 | "node_modules", 38 | "test", 39 | "src", 40 | ".editorconfig", 41 | ".gitignore", 42 | ".gitattributes", 43 | ".jshintrc", 44 | ".travis.yml", 45 | "Gruntfile.js", 46 | "package.json", 47 | "index.html" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /dist/ngToast-animations.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) 3 | * Copyright 2016 Tamer Aydin (http://tamerayd.in) 4 | * Licensed under MIT (http://tameraydin.mit-license.org/) 5 | */ 6 | 7 | .ng-toast--animate-fade .ng-enter, 8 | .ng-toast--animate-fade .ng-leave, 9 | .ng-toast--animate-fade .ng-move { 10 | transition-property: opacity; 11 | transition-duration: 0.3s; 12 | transition-timing-function: ease; } 13 | 14 | .ng-toast--animate-fade .ng-enter { 15 | opacity: 0; } 16 | 17 | .ng-toast--animate-fade .ng-enter.ng-enter-active { 18 | opacity: 1; } 19 | 20 | .ng-toast--animate-fade .ng-leave { 21 | opacity: 1; } 22 | 23 | .ng-toast--animate-fade .ng-leave.ng-leave-active { 24 | opacity: 0; } 25 | 26 | .ng-toast--animate-fade .ng-move { 27 | opacity: 0.5; } 28 | 29 | .ng-toast--animate-fade .ng-move.ng-move-active { 30 | opacity: 1; } 31 | 32 | .ng-toast--animate-slide .ng-enter, 33 | .ng-toast--animate-slide .ng-leave, 34 | .ng-toast--animate-slide .ng-move { 35 | position: relative; 36 | transition-duration: 0.3s; 37 | transition-timing-function: ease; } 38 | 39 | .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message { 40 | position: relative; 41 | transition-property: top, margin-top, opacity; } 42 | .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter { 43 | opacity: 0; 44 | top: -100px; } 45 | .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter.ng-enter-active { 46 | opacity: 1; 47 | top: 0; } 48 | .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave { 49 | opacity: 1; 50 | top: 0; } 51 | .ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave.ng-leave-active { 52 | opacity: 0; 53 | margin-top: -72px; } 54 | 55 | .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message { 56 | position: relative; 57 | transition-property: bottom, margin-bottom, opacity; } 58 | .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter { 59 | opacity: 0; 60 | bottom: -100px; } 61 | .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter.ng-enter-active { 62 | opacity: 1; 63 | bottom: 0; } 64 | .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave { 65 | opacity: 1; 66 | bottom: 0; } 67 | .ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave.ng-leave-active { 68 | opacity: 0; 69 | margin-bottom: -72px; } 70 | 71 | .ng-toast--animate-slide.ng-toast--right { 72 | transition-property: right, margin-right, opacity; } 73 | .ng-toast--animate-slide.ng-toast--right .ng-enter { 74 | opacity: 0; 75 | right: -200%; 76 | margin-right: 20px; } 77 | .ng-toast--animate-slide.ng-toast--right .ng-enter.ng-enter-active { 78 | opacity: 1; 79 | right: 0; 80 | margin-right: 0; } 81 | .ng-toast--animate-slide.ng-toast--right .ng-leave { 82 | opacity: 1; 83 | right: 0; 84 | margin-right: 0; } 85 | .ng-toast--animate-slide.ng-toast--right .ng-leave.ng-leave-active { 86 | opacity: 0; 87 | right: -200%; 88 | margin-right: 20px; } 89 | 90 | .ng-toast--animate-slide.ng-toast--left { 91 | transition-property: left, margin-left, opacity; } 92 | .ng-toast--animate-slide.ng-toast--left .ng-enter { 93 | opacity: 0; 94 | left: -200%; 95 | margin-left: 20px; } 96 | .ng-toast--animate-slide.ng-toast--left .ng-enter.ng-enter-active { 97 | opacity: 1; 98 | left: 0; 99 | margin-left: 0; } 100 | .ng-toast--animate-slide.ng-toast--left .ng-leave { 101 | opacity: 1; 102 | left: 0; 103 | margin-left: 0; } 104 | .ng-toast--animate-slide.ng-toast--left .ng-leave.ng-leave-active { 105 | opacity: 0; 106 | left: -200%; 107 | margin-left: 20px; } 108 | -------------------------------------------------------------------------------- /dist/ngToast-animations.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) 3 | * Copyright 2016 Tamer Aydin (http://tamerayd.in) 4 | * Licensed under MIT (http://tameraydin.mit-license.org/) 5 | */ 6 | 7 | .ng-toast--animate-fade .ng-enter,.ng-toast--animate-fade .ng-leave,.ng-toast--animate-fade .ng-move{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease}.ng-toast--animate-fade .ng-enter{opacity:0}.ng-toast--animate-fade .ng-enter.ng-enter-active,.ng-toast--animate-fade .ng-leave{opacity:1}.ng-toast--animate-fade .ng-leave.ng-leave-active{opacity:0}.ng-toast--animate-fade .ng-move{opacity:.5}.ng-toast--animate-fade .ng-move.ng-move-active{opacity:1}.ng-toast--animate-slide .ng-enter,.ng-toast--animate-slide .ng-leave,.ng-toast--animate-slide .ng-move{position:relative;transition-duration:.3s;transition-timing-function:ease}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message{position:relative;transition-property:top,margin-top,opacity}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter{opacity:0;top:-100px}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave{opacity:1;top:0}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave.ng-leave-active{opacity:0;margin-top:-72px}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message{position:relative;transition-property:bottom,margin-bottom,opacity}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter{opacity:0;bottom:-100px}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave{opacity:1;bottom:0}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave.ng-leave-active{opacity:0;margin-bottom:-72px}.ng-toast--animate-slide.ng-toast--right{transition-property:right,margin-right,opacity}.ng-toast--animate-slide.ng-toast--right .ng-enter{opacity:0;right:-200%;margin-right:20px}.ng-toast--animate-slide.ng-toast--right .ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--right .ng-leave{opacity:1;right:0;margin-right:0}.ng-toast--animate-slide.ng-toast--right .ng-leave.ng-leave-active{opacity:0;right:-200%;margin-right:20px}.ng-toast--animate-slide.ng-toast--left{transition-property:left,margin-left,opacity}.ng-toast--animate-slide.ng-toast--left .ng-enter{opacity:0;left:-200%;margin-left:20px}.ng-toast--animate-slide.ng-toast--left .ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--left .ng-leave{opacity:1;left:0;margin-left:0}.ng-toast--animate-slide.ng-toast--left .ng-leave.ng-leave-active{opacity:0;left:-200%;margin-left:20px} -------------------------------------------------------------------------------- /dist/ngToast.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) 3 | * Copyright 2016 Tamer Aydin (http://tamerayd.in) 4 | * Licensed under MIT (http://tameraydin.mit-license.org/) 5 | */ 6 | 7 | .ng-toast { 8 | position: fixed; 9 | z-index: 1080; 10 | width: 100%; 11 | height: 0; 12 | margin-top: 20px; 13 | text-align: center; } 14 | .ng-toast.ng-toast--top { 15 | top: 0; 16 | bottom: auto; } 17 | .ng-toast.ng-toast--top .ng-toast__list { 18 | top: 0; 19 | bottom: auto; } 20 | .ng-toast.ng-toast--top.ng-toast--center .ng-toast__list { 21 | position: static; } 22 | .ng-toast.ng-toast--bottom { 23 | top: auto; 24 | bottom: 0; } 25 | .ng-toast.ng-toast--bottom .ng-toast__list { 26 | top: auto; 27 | bottom: 0; } 28 | .ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__list { 29 | pointer-events: none; } 30 | .ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__message .alert { 31 | pointer-events: auto; } 32 | .ng-toast.ng-toast--right .ng-toast__list { 33 | left: auto; 34 | right: 0; 35 | margin-right: 20px; } 36 | .ng-toast.ng-toast--right .ng-toast__message { 37 | text-align: right; } 38 | .ng-toast.ng-toast--left .ng-toast__list { 39 | right: auto; 40 | left: 0; 41 | margin-left: 20px; } 42 | .ng-toast.ng-toast--left .ng-toast__message { 43 | text-align: left; } 44 | .ng-toast .ng-toast__list { 45 | display: inline-block; 46 | position: absolute; 47 | right: 0; 48 | left: 0; 49 | margin: 0 auto; 50 | padding: 0; 51 | list-style: none; } 52 | .ng-toast .ng-toast__message { 53 | display: block; 54 | width: 100%; 55 | text-align: center; } 56 | .ng-toast .ng-toast__message .alert { 57 | display: inline-block; } 58 | .ng-toast .ng-toast__message__count { 59 | display: inline-block; 60 | margin: 0 15px 0 5px; } 61 | -------------------------------------------------------------------------------- /dist/ngToast.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) 3 | * Copyright 2016 Tamer Aydin (http://tamerayd.in) 4 | * Licensed under MIT (http://tameraydin.mit-license.org/) 5 | */ 6 | 7 | (function(window, angular, undefined) { 8 | 'use strict'; 9 | 10 | angular.module('ngToast.provider', []) 11 | .provider('ngToast', [ 12 | function() { 13 | var messages = [], 14 | messageStack = []; 15 | 16 | var defaults = { 17 | animation: false, 18 | className: 'success', 19 | additionalClasses: null, 20 | dismissOnTimeout: true, 21 | timeout: 4000, 22 | dismissButton: false, 23 | dismissButtonHtml: '×', 24 | dismissOnClick: true, 25 | onDismiss: null, 26 | compileContent: false, 27 | combineDuplications: false, 28 | horizontalPosition: 'right', // right, center, left 29 | verticalPosition: 'top', // top, bottom, 30 | maxNumber: 0, 31 | newestOnTop: true 32 | }; 33 | 34 | function Message(msg) { 35 | var id = Math.floor(Math.random()*1000); 36 | while (messages.indexOf(id) > -1) { 37 | id = Math.floor(Math.random()*1000); 38 | } 39 | 40 | this.id = id; 41 | this.count = 0; 42 | this.animation = defaults.animation; 43 | this.className = defaults.className; 44 | this.additionalClasses = defaults.additionalClasses; 45 | this.dismissOnTimeout = defaults.dismissOnTimeout; 46 | this.timeout = defaults.timeout; 47 | this.dismissButton = defaults.dismissButton; 48 | this.dismissButtonHtml = defaults.dismissButtonHtml; 49 | this.dismissOnClick = defaults.dismissOnClick; 50 | this.onDismiss = defaults.onDismiss; 51 | this.compileContent = defaults.compileContent; 52 | 53 | angular.extend(this, msg); 54 | } 55 | 56 | this.configure = function(config) { 57 | angular.extend(defaults, config); 58 | }; 59 | 60 | this.$get = [function() { 61 | var _createWithClassName = function(className, msg) { 62 | msg = (typeof msg === 'object') ? msg : {content: msg}; 63 | msg.className = className; 64 | 65 | return this.create(msg); 66 | }; 67 | 68 | return { 69 | settings: defaults, 70 | messages: messages, 71 | dismiss: function(id) { 72 | if (id) { 73 | for (var i = messages.length - 1; i >= 0; i--) { 74 | if (messages[i].id === id) { 75 | messages.splice(i, 1); 76 | messageStack.splice(messageStack.indexOf(id), 1); 77 | return; 78 | } 79 | } 80 | 81 | } else { 82 | while(messages.length > 0) { 83 | messages.pop(); 84 | } 85 | messageStack = []; 86 | } 87 | }, 88 | create: function(msg) { 89 | msg = (typeof msg === 'object') ? msg : {content: msg}; 90 | 91 | if (defaults.combineDuplications) { 92 | for (var i = messageStack.length - 1; i >= 0; i--) { 93 | var _msg = messages[i]; 94 | var _className = msg.className || 'success'; 95 | 96 | if (_msg.content === msg.content && 97 | _msg.className === _className) { 98 | messages[i].count++; 99 | return; 100 | } 101 | } 102 | } 103 | 104 | if (defaults.maxNumber > 0 && 105 | messageStack.length >= defaults.maxNumber) { 106 | this.dismiss(messageStack[0]); 107 | } 108 | 109 | var newMsg = new Message(msg); 110 | messages[defaults.newestOnTop ? 'unshift' : 'push'](newMsg); 111 | messageStack.push(newMsg.id); 112 | 113 | return newMsg.id; 114 | }, 115 | success: function(msg) { 116 | return _createWithClassName.call(this, 'success', msg); 117 | }, 118 | info: function(msg) { 119 | return _createWithClassName.call(this, 'info', msg); 120 | }, 121 | warning: function(msg) { 122 | return _createWithClassName.call(this, 'warning', msg); 123 | }, 124 | danger: function(msg) { 125 | return _createWithClassName.call(this, 'danger', msg); 126 | } 127 | }; 128 | }]; 129 | } 130 | ]); 131 | 132 | })(window, window.angular); 133 | 134 | (function(window, angular) { 135 | 'use strict'; 136 | 137 | angular.module('ngToast.directives', ['ngToast.provider']) 138 | .run(['$templateCache', 139 | function($templateCache) { 140 | $templateCache.put('ngToast/toast.html', 141 | '
' + 142 | '' + 148 | '
'); 149 | $templateCache.put('ngToast/toastMessage.html', 150 | '
  • ' + 153 | '
    ' + 155 | '' + 160 | '' + 161 | '{{count + 1}}' + 162 | '' + 163 | '' + 164 | '
    ' + 165 | '
  • '); 166 | } 167 | ]) 168 | .directive('toast', ['ngToast', '$templateCache', '$log', 169 | function(ngToast, $templateCache, $log) { 170 | return { 171 | replace: true, 172 | restrict: 'EA', 173 | templateUrl: 'ngToast/toast.html', 174 | compile: function(tElem, tAttrs) { 175 | if (tAttrs.template) { 176 | var template = $templateCache.get(tAttrs.template); 177 | if (template) { 178 | tElem.replaceWith(template); 179 | } else { 180 | $log.warn('ngToast: Provided template could not be loaded. ' + 181 | 'Please be sure that it is populated before the element is represented.'); 182 | } 183 | } 184 | 185 | return function(scope) { 186 | scope.hPos = ngToast.settings.horizontalPosition; 187 | scope.vPos = ngToast.settings.verticalPosition; 188 | scope.animation = ngToast.settings.animation; 189 | scope.messages = ngToast.messages; 190 | }; 191 | } 192 | }; 193 | } 194 | ]) 195 | .directive('toastMessage', ['$timeout', '$compile', 'ngToast', 196 | function($timeout, $compile, ngToast) { 197 | return { 198 | replace: true, 199 | transclude: true, 200 | restrict: 'EA', 201 | scope: { 202 | message: '=', 203 | count: '=' 204 | }, 205 | controller: ['$scope', 'ngToast', function($scope, ngToast) { 206 | $scope.dismiss = function() { 207 | ngToast.dismiss($scope.message.id); 208 | }; 209 | }], 210 | templateUrl: 'ngToast/toastMessage.html', 211 | link: function(scope, element, attrs, ctrl, transclude) { 212 | element.attr('data-message-id', scope.message.id); 213 | 214 | var dismissTimeout; 215 | var scopeToBind = scope.message.compileContent; 216 | 217 | scope.cancelTimeout = function() { 218 | $timeout.cancel(dismissTimeout); 219 | }; 220 | 221 | scope.startTimeout = function() { 222 | if (scope.message.dismissOnTimeout) { 223 | dismissTimeout = $timeout(function() { 224 | ngToast.dismiss(scope.message.id); 225 | }, scope.message.timeout); 226 | } 227 | }; 228 | 229 | scope.onMouseEnter = function() { 230 | scope.cancelTimeout(); 231 | }; 232 | 233 | scope.onMouseLeave = function() { 234 | scope.startTimeout(); 235 | }; 236 | 237 | if (scopeToBind) { 238 | var transcludedEl; 239 | 240 | transclude(scope, function(clone) { 241 | transcludedEl = clone; 242 | element.children().append(transcludedEl); 243 | }); 244 | 245 | $timeout(function() { 246 | $compile(transcludedEl.contents()) 247 | (typeof scopeToBind === 'boolean' ? 248 | scope.$parent : scopeToBind, function(compiledClone) { 249 | transcludedEl.replaceWith(compiledClone); 250 | }); 251 | }, 0); 252 | } 253 | 254 | scope.startTimeout(); 255 | 256 | if (scope.message.dismissOnClick) { 257 | element.bind('click', function() { 258 | ngToast.dismiss(scope.message.id); 259 | scope.$apply(); 260 | }); 261 | } 262 | 263 | if (scope.message.onDismiss) { 264 | scope.$on('$destroy', 265 | scope.message.onDismiss.bind(scope.message)); 266 | } 267 | } 268 | }; 269 | } 270 | ]); 271 | 272 | })(window, window.angular); 273 | 274 | (function(window, angular) { 275 | 'use strict'; 276 | 277 | angular 278 | .module('ngToast', [ 279 | 'ngSanitize', 280 | 'ngToast.directives', 281 | 'ngToast.provider' 282 | ]); 283 | 284 | })(window, window.angular); 285 | -------------------------------------------------------------------------------- /dist/ngToast.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) 3 | * Copyright 2016 Tamer Aydin (http://tamerayd.in) 4 | * Licensed under MIT (http://tameraydin.mit-license.org/) 5 | */ 6 | 7 | .ng-toast{position:fixed;z-index:1080;width:100%;height:0;margin-top:20px;text-align:center}.ng-toast.ng-toast--top,.ng-toast.ng-toast--top .ng-toast__list{top:0;bottom:auto}.ng-toast.ng-toast--top.ng-toast--center .ng-toast__list{position:static}.ng-toast.ng-toast--bottom,.ng-toast.ng-toast--bottom .ng-toast__list{top:auto;bottom:0}.ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__list{pointer-events:none}.ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__message .alert{pointer-events:auto}.ng-toast.ng-toast--right .ng-toast__list{left:auto;right:0;margin-right:20px}.ng-toast.ng-toast--right .ng-toast__message{text-align:right}.ng-toast.ng-toast--left .ng-toast__list{right:auto;left:0;margin-left:20px}.ng-toast.ng-toast--left .ng-toast__message{text-align:left}.ng-toast .ng-toast__list{display:inline-block;position:absolute;right:0;left:0;margin:0 auto;padding:0;list-style:none}.ng-toast .ng-toast__message{display:block;width:100%;text-align:center}.ng-toast .ng-toast__message .alert{display:inline-block}.ng-toast .ng-toast__message__count{display:inline-block;margin:0 15px 0 5px} -------------------------------------------------------------------------------- /dist/ngToast.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) 3 | * Copyright 2016 Tamer Aydin (http://tamerayd.in) 4 | * Licensed under MIT (http://tameraydin.mit-license.org/) 5 | */ 6 | 7 | !function(a,b,c){"use strict";b.module("ngToast.provider",[]).provider("ngToast",[function(){function a(a){for(var d=Math.floor(1e3*Math.random());c.indexOf(d)>-1;)d=Math.floor(1e3*Math.random());this.id=d,this.count=0,this.animation=e.animation,this.className=e.className,this.additionalClasses=e.additionalClasses,this.dismissOnTimeout=e.dismissOnTimeout,this.timeout=e.timeout,this.dismissButton=e.dismissButton,this.dismissButtonHtml=e.dismissButtonHtml,this.dismissOnClick=e.dismissOnClick,this.onDismiss=e.onDismiss,this.compileContent=e.compileContent,b.extend(this,a)}var c=[],d=[],e={animation:!1,className:"success",additionalClasses:null,dismissOnTimeout:!0,timeout:4e3,dismissButton:!1,dismissButtonHtml:"×",dismissOnClick:!0,onDismiss:null,compileContent:!1,combineDuplications:!1,horizontalPosition:"right",verticalPosition:"top",maxNumber:0,newestOnTop:!0};this.configure=function(a){b.extend(e,a)},this.$get=[function(){var b=function(a,b){return b="object"==typeof b?b:{content:b},b.className=a,this.create(b)};return{settings:e,messages:c,dismiss:function(a){if(a){for(var b=c.length-1;b>=0;b--)if(c[b].id===a)return c.splice(b,1),void d.splice(d.indexOf(a),1)}else{for(;c.length>0;)c.pop();d=[]}},create:function(b){if(b="object"==typeof b?b:{content:b},e.combineDuplications)for(var f=d.length-1;f>=0;f--){var g=c[f],h=b.className||"success";if(g.content===b.content&&g.className===h)return void c[f].count++}e.maxNumber>0&&d.length>=e.maxNumber&&this.dismiss(d[0]);var i=new a(b);return c[e.newestOnTop?"unshift":"push"](i),d.push(i.id),i.id},success:function(a){return b.call(this,"success",a)},info:function(a){return b.call(this,"info",a)},warning:function(a){return b.call(this,"warning",a)},danger:function(a){return b.call(this,"danger",a)}}}]}])}(window,window.angular),function(a,b){"use strict";b.module("ngToast.directives",["ngToast.provider"]).run(["$templateCache",function(a){a.put("ngToast/toast.html",'
    '),a.put("ngToast/toastMessage.html",'
  • {{count + 1}}
  • ')}]).directive("toast",["ngToast","$templateCache","$log",function(a,b,c){return{replace:!0,restrict:"EA",templateUrl:"ngToast/toast.html",compile:function(d,e){if(e.template){var f=b.get(e.template);f?d.replaceWith(f):c.warn("ngToast: Provided template could not be loaded. Please be sure that it is populated before the element is represented.")}return function(b){b.hPos=a.settings.horizontalPosition,b.vPos=a.settings.verticalPosition,b.animation=a.settings.animation,b.messages=a.messages}}}}]).directive("toastMessage",["$timeout","$compile","ngToast",function(a,b,c){return{replace:!0,transclude:!0,restrict:"EA",scope:{message:"=",count:"="},controller:["$scope","ngToast",function(a,b){a.dismiss=function(){b.dismiss(a.message.id)}}],templateUrl:"ngToast/toastMessage.html",link:function(d,e,f,g,h){e.attr("data-message-id",d.message.id);var i,j=d.message.compileContent;if(d.cancelTimeout=function(){a.cancel(i)},d.startTimeout=function(){d.message.dismissOnTimeout&&(i=a(function(){c.dismiss(d.message.id)},d.message.timeout))},d.onMouseEnter=function(){d.cancelTimeout()},d.onMouseLeave=function(){d.startTimeout()},j){var k;h(d,function(a){k=a,e.children().append(k)}),a(function(){b(k.contents())("boolean"==typeof j?d.$parent:j,function(a){k.replaceWith(a)})},0)}d.startTimeout(),d.message.dismissOnClick&&e.bind("click",function(){c.dismiss(d.message.id),d.$apply()}),d.message.onDismiss&&d.$on("$destroy",d.message.onDismiss.bind(d.message))}}}])}(window,window.angular),function(a,b){"use strict";b.module("ngToast",["ngSanitize","ngToast.directives","ngToast.provider"])}(window,window.angular); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ngToast Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
    23 |
    24 |

    ngToast

    25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |
    32 |
    33 | Create 34 |
    35 |
     36 | ngToastProvider.configure({
     37 |   animation: 'slide',
     38 |   horizontalPosition: 'right',
     39 |   verticalPosition: 'top',
     40 |   maxNumber: 0,
     41 |   combineDuplications: true
     42 | });
     43 | 
    44 | 45 |
    46 | 49 | 50 |
    51 |
    52 | 55 | 56 |
    57 |
    58 | 61 | 62 |
    63 | 64 |
    65 | 66 |
    67 | 71 |
    72 |
    73 | 77 |
    78 |
    79 | 83 |
    84 |
    85 | 89 |
    90 |
    91 | 92 |
    93 |
    94 | 98 |
    99 |
    100 | 104 |
    105 |
    106 | 110 |
    111 |
    112 | 116 |
    117 |
    118 | 119 |
    120 | 124 |
    125 | 126 |
    127 | 128 |
    129 | 130 |
    131 |
    132 | 133 |
    134 |
    135 | Random 136 | 137 |
    138 |
    139 | 143 |
    144 |
    145 | 146 |
    147 | 151 |
    152 |
    {{ ctrl.randomOptions | json }}
    153 |
    154 | 155 |
    156 |
    157 |
    158 | 159 | 160 | 161 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-toast", 3 | "version": "2.0.0", 4 | "description": "Angular provider for toast notifications", 5 | "main": "dist/ngToast.js", 6 | "keywords": [ 7 | "angular", 8 | "toast", 9 | "message", 10 | "notification", 11 | "toastr" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/tameraydin/ngToast.git" 16 | }, 17 | "author": "Tamer Aydin (http://tamerayd.in)", 18 | "contributors": [ 19 | { 20 | "name": "Tamer Aydin", 21 | "url": "http://tamerayd.in" 22 | }, 23 | { 24 | "name": "Levi Thomason", 25 | "url": "http://www.levithomason.com" 26 | } 27 | ], 28 | "license": "MIT", 29 | "homepage": "http://tameraydin.github.io/ngToast", 30 | "bugs": { 31 | "url": "https://github.com/tameraydin/ngToast/issues" 32 | }, 33 | "dependencies": { 34 | "angular": ">=1.2.15 <1.6", 35 | "angular-sanitize": ">=1.2.15 <1.6" 36 | }, 37 | "devDependencies": { 38 | "caniuse-db": "latest", 39 | "colors": "^1.0.3", 40 | "diff": "^1.0.8", 41 | "grunt": "^0.4.5", 42 | "grunt-autoprefixer": "^2.2.0", 43 | "grunt-banner": "^0.6.0", 44 | "grunt-contrib-clean": "~0.5.0", 45 | "grunt-contrib-concat": "~0.3.0", 46 | "grunt-contrib-cssmin": "0.8.0", 47 | "grunt-contrib-jshint": "~0.7.1", 48 | "grunt-contrib-less": "^0.12.0", 49 | "grunt-contrib-uglify": "~0.2.0", 50 | "grunt-contrib-watch": "^0.6.1", 51 | "grunt-cssbeautifier": "^0.1.2", 52 | "grunt-karma": "^0.12.1", 53 | "grunt-sass": "^1.1.0", 54 | "jasmine-core": "^2.4.1", 55 | "karma": "^0.13.22", 56 | "karma-jasmine": "^0.3.7", 57 | "karma-phantomjs-launcher": "^1.0.0", 58 | "lodash": "^3.10.1", 59 | "phantomjs-prebuilt": "^2.1.5" 60 | }, 61 | "scripts": { 62 | "test": "grunt karma" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/scripts/directives.js: -------------------------------------------------------------------------------- 1 | (function(window, angular) { 2 | 'use strict'; 3 | 4 | angular.module('ngToast.directives', ['ngToast.provider']) 5 | .run(['$templateCache', 6 | function($templateCache) { 7 | $templateCache.put('ngToast/toast.html', 8 | '
    ' + 9 | '
      ' + 10 | '' + 12 | '' + 13 | '' + 14 | '
    ' + 15 | '
    '); 16 | $templateCache.put('ngToast/toastMessage.html', 17 | '
  • ' + 20 | '
    ' + 22 | '' + 27 | '' + 28 | '{{count + 1}}' + 29 | '' + 30 | '' + 31 | '
    ' + 32 | '
  • '); 33 | } 34 | ]) 35 | .directive('toast', ['ngToast', '$templateCache', '$log', 36 | function(ngToast, $templateCache, $log) { 37 | return { 38 | replace: true, 39 | restrict: 'EA', 40 | templateUrl: 'ngToast/toast.html', 41 | compile: function(tElem, tAttrs) { 42 | if (tAttrs.template) { 43 | var template = $templateCache.get(tAttrs.template); 44 | if (template) { 45 | tElem.replaceWith(template); 46 | } else { 47 | $log.warn('ngToast: Provided template could not be loaded. ' + 48 | 'Please be sure that it is populated before the element is represented.'); 49 | } 50 | } 51 | 52 | return function(scope) { 53 | scope.hPos = ngToast.settings.horizontalPosition; 54 | scope.vPos = ngToast.settings.verticalPosition; 55 | scope.animation = ngToast.settings.animation; 56 | scope.messages = ngToast.messages; 57 | }; 58 | } 59 | }; 60 | } 61 | ]) 62 | .directive('toastMessage', ['$timeout', '$compile', 'ngToast', 63 | function($timeout, $compile, ngToast) { 64 | return { 65 | replace: true, 66 | transclude: true, 67 | restrict: 'EA', 68 | scope: { 69 | message: '=', 70 | count: '=' 71 | }, 72 | controller: ['$scope', 'ngToast', function($scope, ngToast) { 73 | $scope.dismiss = function() { 74 | ngToast.dismiss($scope.message.id); 75 | }; 76 | }], 77 | templateUrl: 'ngToast/toastMessage.html', 78 | link: function(scope, element, attrs, ctrl, transclude) { 79 | element.attr('data-message-id', scope.message.id); 80 | 81 | var dismissTimeout; 82 | var scopeToBind = scope.message.compileContent; 83 | 84 | scope.cancelTimeout = function() { 85 | $timeout.cancel(dismissTimeout); 86 | }; 87 | 88 | scope.startTimeout = function() { 89 | if (scope.message.dismissOnTimeout) { 90 | dismissTimeout = $timeout(function() { 91 | ngToast.dismiss(scope.message.id); 92 | }, scope.message.timeout); 93 | } 94 | }; 95 | 96 | scope.onMouseEnter = function() { 97 | scope.cancelTimeout(); 98 | }; 99 | 100 | scope.onMouseLeave = function() { 101 | scope.startTimeout(); 102 | }; 103 | 104 | if (scopeToBind) { 105 | var transcludedEl; 106 | 107 | transclude(scope, function(clone) { 108 | transcludedEl = clone; 109 | element.children().append(transcludedEl); 110 | }); 111 | 112 | $timeout(function() { 113 | $compile(transcludedEl.contents()) 114 | (typeof scopeToBind === 'boolean' ? 115 | scope.$parent : scopeToBind, function(compiledClone) { 116 | transcludedEl.replaceWith(compiledClone); 117 | }); 118 | }, 0); 119 | } 120 | 121 | scope.startTimeout(); 122 | 123 | if (scope.message.dismissOnClick) { 124 | element.bind('click', function() { 125 | ngToast.dismiss(scope.message.id); 126 | scope.$apply(); 127 | }); 128 | } 129 | 130 | if (scope.message.onDismiss) { 131 | scope.$on('$destroy', 132 | scope.message.onDismiss.bind(scope.message)); 133 | } 134 | } 135 | }; 136 | } 137 | ]); 138 | 139 | })(window, window.angular); 140 | -------------------------------------------------------------------------------- /src/scripts/module.js: -------------------------------------------------------------------------------- 1 | (function(window, angular) { 2 | 'use strict'; 3 | 4 | angular 5 | .module('ngToast', [ 6 | 'ngSanitize', 7 | 'ngToast.directives', 8 | 'ngToast.provider' 9 | ]); 10 | 11 | })(window, window.angular); 12 | -------------------------------------------------------------------------------- /src/scripts/provider.js: -------------------------------------------------------------------------------- 1 | (function(window, angular, undefined) { 2 | 'use strict'; 3 | 4 | angular.module('ngToast.provider', []) 5 | .provider('ngToast', [ 6 | function() { 7 | var messages = [], 8 | messageStack = []; 9 | 10 | var defaults = { 11 | animation: false, 12 | className: 'success', 13 | additionalClasses: null, 14 | dismissOnTimeout: true, 15 | timeout: 4000, 16 | dismissButton: false, 17 | dismissButtonHtml: '×', 18 | dismissOnClick: true, 19 | onDismiss: null, 20 | compileContent: false, 21 | combineDuplications: false, 22 | horizontalPosition: 'right', // right, center, left 23 | verticalPosition: 'top', // top, bottom, 24 | maxNumber: 0, 25 | newestOnTop: true 26 | }; 27 | 28 | function Message(msg) { 29 | var id = Math.floor(Math.random()*1000); 30 | while (messages.indexOf(id) > -1) { 31 | id = Math.floor(Math.random()*1000); 32 | } 33 | 34 | this.id = id; 35 | this.count = 0; 36 | this.animation = defaults.animation; 37 | this.className = defaults.className; 38 | this.additionalClasses = defaults.additionalClasses; 39 | this.dismissOnTimeout = defaults.dismissOnTimeout; 40 | this.timeout = defaults.timeout; 41 | this.dismissButton = defaults.dismissButton; 42 | this.dismissButtonHtml = defaults.dismissButtonHtml; 43 | this.dismissOnClick = defaults.dismissOnClick; 44 | this.onDismiss = defaults.onDismiss; 45 | this.compileContent = defaults.compileContent; 46 | 47 | angular.extend(this, msg); 48 | } 49 | 50 | this.configure = function(config) { 51 | angular.extend(defaults, config); 52 | }; 53 | 54 | this.$get = [function() { 55 | var _createWithClassName = function(className, msg) { 56 | msg = (typeof msg === 'object') ? msg : {content: msg}; 57 | msg.className = className; 58 | 59 | return this.create(msg); 60 | }; 61 | 62 | return { 63 | settings: defaults, 64 | messages: messages, 65 | dismiss: function(id) { 66 | if (id) { 67 | for (var i = messages.length - 1; i >= 0; i--) { 68 | if (messages[i].id === id) { 69 | messages.splice(i, 1); 70 | messageStack.splice(messageStack.indexOf(id), 1); 71 | return; 72 | } 73 | } 74 | 75 | } else { 76 | while(messages.length > 0) { 77 | messages.pop(); 78 | } 79 | messageStack = []; 80 | } 81 | }, 82 | create: function(msg) { 83 | msg = (typeof msg === 'object') ? msg : {content: msg}; 84 | 85 | if (defaults.combineDuplications) { 86 | for (var i = messageStack.length - 1; i >= 0; i--) { 87 | var _msg = messages[i]; 88 | var _className = msg.className || 'success'; 89 | 90 | if (_msg.content === msg.content && 91 | _msg.className === _className) { 92 | messages[i].count++; 93 | return; 94 | } 95 | } 96 | } 97 | 98 | if (defaults.maxNumber > 0 && 99 | messageStack.length >= defaults.maxNumber) { 100 | this.dismiss(messageStack[0]); 101 | } 102 | 103 | var newMsg = new Message(msg); 104 | messages[defaults.newestOnTop ? 'unshift' : 'push'](newMsg); 105 | messageStack.push(newMsg.id); 106 | 107 | return newMsg.id; 108 | }, 109 | success: function(msg) { 110 | return _createWithClassName.call(this, 'success', msg); 111 | }, 112 | info: function(msg) { 113 | return _createWithClassName.call(this, 'info', msg); 114 | }, 115 | warning: function(msg) { 116 | return _createWithClassName.call(this, 'warning', msg); 117 | }, 118 | danger: function(msg) { 119 | return _createWithClassName.call(this, 'danger', msg); 120 | } 121 | }; 122 | }]; 123 | } 124 | ]); 125 | 126 | })(window, window.angular); 127 | -------------------------------------------------------------------------------- /src/styles/less/ngToast-animations.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | // Fade 4 | .@{ngt-module}--animate-fade { 5 | .ng-enter, 6 | .ng-leave, 7 | .ng-move { 8 | transition-property: opacity; 9 | transition-duration: @ngt-transition-duration; 10 | transition-timing-function: @ngt-transition-timing-function; 11 | } 12 | .ng-enter { 13 | opacity: 0; 14 | } 15 | .ng-enter.ng-enter-active { 16 | opacity: 1; 17 | } 18 | .ng-leave { 19 | opacity: 1; 20 | } 21 | .ng-leave.ng-leave-active { 22 | opacity: 0; 23 | } 24 | .ng-move { 25 | opacity: 0.5; 26 | } 27 | .ng-move.ng-move-active { 28 | opacity: 1; 29 | } 30 | } 31 | 32 | // Slide 33 | .@{ngt-module}--animate-slide { 34 | .ng-enter, 35 | .ng-leave, 36 | .ng-move { 37 | position: relative; 38 | transition-duration: @ngt-transition-duration; 39 | transition-timing-function: @ngt-transition-timing-function; 40 | } 41 | 42 | &.@{ngt-module}--center { 43 | 44 | // in/out from top when centered and top aligned 45 | &.@{ngt-module}--top { 46 | 47 | .@{ngt-module}__message { 48 | position: relative; 49 | transition-property: top, margin-top, opacity; 50 | &.ng-enter { 51 | opacity: 0; 52 | top: -100px; 53 | } 54 | &.ng-enter.ng-enter-active { 55 | opacity: 1; 56 | top: 0; 57 | } 58 | &.ng-leave { 59 | opacity: 1; 60 | top: 0; 61 | } 62 | &.ng-leave.ng-leave-active { 63 | opacity: 0; 64 | margin-top: -(52px + @ngt-spacing); 65 | } 66 | } 67 | } 68 | 69 | // in/out from top when centered and bottom aligned 70 | &.@{ngt-module}--bottom { 71 | 72 | .@{ngt-module}__message { 73 | position: relative; 74 | transition-property: bottom, margin-bottom, opacity; 75 | &.ng-enter { 76 | opacity: 0; 77 | bottom: -100px; 78 | } 79 | &.ng-enter.ng-enter-active { 80 | opacity: 1; 81 | bottom: 0; 82 | } 83 | &.ng-leave { 84 | opacity: 1; 85 | bottom: 0; 86 | } 87 | &.ng-leave.ng-leave-active { 88 | opacity: 0; 89 | margin-bottom: -(52px + @ngt-spacing); 90 | } 91 | } 92 | } 93 | } 94 | 95 | // in/out from right when right aligned 96 | &.@{ngt-module}--right { 97 | transition-property: right, margin-right, opacity; 98 | 99 | .ng-enter { 100 | opacity: 0; 101 | right: -200%; 102 | margin-right: 20px; 103 | } 104 | .ng-enter.ng-enter-active { 105 | opacity: 1; 106 | right: 0; 107 | margin-right: 0; 108 | } 109 | .ng-leave { 110 | opacity: 1; 111 | right: 0; 112 | margin-right: 0; 113 | } 114 | .ng-leave.ng-leave-active { 115 | opacity: 0; 116 | right: -200%; 117 | margin-right: 20px; 118 | } 119 | } 120 | 121 | // in/out from left when left aligned 122 | &.@{ngt-module}--left { 123 | transition-property: left, margin-left, opacity; 124 | 125 | .ng-enter { 126 | opacity: 0; 127 | left: -200%; 128 | margin-left: 20px; 129 | } 130 | .ng-enter.ng-enter-active { 131 | opacity: 1; 132 | left: 0; 133 | margin-left: 0; 134 | } 135 | .ng-leave { 136 | opacity: 1; 137 | left: 0; 138 | margin-left: 0; 139 | } 140 | .ng-leave.ng-leave-active { 141 | opacity: 0; 142 | left: -200%; 143 | margin-left: 20px; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/styles/less/ngToast.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | // Base style 4 | .@{ngt-module} { 5 | position: fixed; 6 | z-index: 1080; 7 | width: 100%; 8 | height: 0; 9 | margin-top: @ngt-spacing; 10 | text-align: center; 11 | 12 | &.@{ngt-module}--top { 13 | top: 0; 14 | bottom: auto; 15 | 16 | .@{ngt-module}__list { 17 | top: 0; 18 | bottom: auto; 19 | } 20 | 21 | &.@{ngt-module}--center { 22 | 23 | .@{ngt-module}__list { 24 | position: static; 25 | } 26 | } 27 | } 28 | 29 | &.@{ngt-module}--bottom { 30 | top: auto; 31 | bottom: 0; 32 | 33 | .@{ngt-module}__list { 34 | top: auto; 35 | bottom: 0; 36 | } 37 | 38 | &.@{ngt-module}--center { 39 | 40 | .@{ngt-module}__list { 41 | pointer-events: none; 42 | } 43 | 44 | .@{ngt-module}__message { 45 | .alert { 46 | pointer-events: auto; 47 | } 48 | } 49 | } 50 | } 51 | 52 | &.@{ngt-module}--right { 53 | 54 | .@{ngt-module}__list { 55 | left: auto; 56 | right: 0; 57 | margin-right: @ngt-spacing; 58 | } 59 | 60 | .@{ngt-module}__message { 61 | text-align: right; 62 | } 63 | } 64 | 65 | &.@{ngt-module}--left { 66 | 67 | .@{ngt-module}__list { 68 | right: auto; 69 | left: 0; 70 | margin-left: @ngt-spacing; 71 | } 72 | 73 | .@{ngt-module}__message { 74 | text-align: left; 75 | } 76 | } 77 | 78 | .@{ngt-module}__list { 79 | display: inline-block; 80 | position: absolute; 81 | right: 0; 82 | left: 0; 83 | margin: 0 auto; 84 | padding: 0; 85 | list-style: none; 86 | } 87 | 88 | .@{ngt-module}__message { 89 | display: block; 90 | width: 100%; 91 | text-align: center; 92 | 93 | .alert { 94 | display: inline-block; 95 | } 96 | } 97 | 98 | .@{ngt-module}__message__count { 99 | display: inline-block; 100 | margin: 0 @ngt-spacing / 4*3 0 @ngt-spacing / 4; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/styles/less/variables.less: -------------------------------------------------------------------------------- 1 | @ngt-module: ~'ng-toast'; 2 | @ngt-spacing: 20px; 3 | @ngt-transition-duration: 0.3s; 4 | @ngt-transition-timing-function: ease; 5 | -------------------------------------------------------------------------------- /src/styles/sass/ngToast.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | // Base style 4 | .#{$ngt-module} { 5 | position: fixed; 6 | z-index: 1080; 7 | width: 100%; 8 | height: 0; 9 | margin-top: $ngt-spacing; 10 | text-align: center; 11 | 12 | &.#{$ngt-module}--top { 13 | top: 0; 14 | bottom: auto; 15 | 16 | .#{$ngt-module}__list { 17 | top: 0; 18 | bottom: auto; 19 | } 20 | 21 | &.#{$ngt-module}--center { 22 | 23 | .#{$ngt-module}__list { 24 | position: static; 25 | } 26 | } 27 | } 28 | 29 | &.#{$ngt-module}--bottom { 30 | top: auto; 31 | bottom: 0; 32 | 33 | .#{$ngt-module}__list { 34 | top: auto; 35 | bottom: 0; 36 | } 37 | 38 | &.#{$ngt-module}--center { 39 | 40 | .#{$ngt-module}__list { 41 | pointer-events: none; 42 | } 43 | 44 | .#{$ngt-module}__message { 45 | .alert { 46 | pointer-events: auto; 47 | } 48 | } 49 | } 50 | } 51 | 52 | &.#{$ngt-module}--right { 53 | 54 | .#{$ngt-module}__list { 55 | left: auto; 56 | right: 0; 57 | margin-right: $ngt-spacing; 58 | } 59 | 60 | .#{$ngt-module}__message { 61 | text-align: right; 62 | } 63 | } 64 | 65 | &.#{$ngt-module}--left { 66 | 67 | .#{$ngt-module}__list { 68 | right: auto; 69 | left: 0; 70 | margin-left: $ngt-spacing; 71 | } 72 | 73 | .#{$ngt-module}__message { 74 | text-align: left; 75 | } 76 | } 77 | 78 | .#{$ngt-module}__list { 79 | display: inline-block; 80 | position: absolute; 81 | right: 0; 82 | left: 0; 83 | margin: 0 auto; 84 | padding: 0; 85 | list-style: none; 86 | } 87 | 88 | .#{$ngt-module}__message { 89 | display: block; 90 | width: 100%; 91 | text-align: center; 92 | 93 | .alert { 94 | display: inline-block; 95 | } 96 | } 97 | 98 | .#{$ngt-module}__message__count { 99 | display: inline-block; 100 | margin: 0 $ngt-spacing / 4*3 0 $ngt-spacing / 4; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/styles/sass/ngtoast-animations.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | // Fade 4 | .#{$ngt-module}--animate-fade { 5 | .ng-enter, 6 | .ng-leave, 7 | .ng-move { 8 | transition-property: opacity; 9 | transition-duration: $ngt-transition-duration; 10 | transition-timing-function: $ngt-transition-timing-function; 11 | } 12 | .ng-enter { 13 | opacity: 0; 14 | } 15 | .ng-enter.ng-enter-active { 16 | opacity: 1; 17 | } 18 | .ng-leave { 19 | opacity: 1; 20 | } 21 | .ng-leave.ng-leave-active { 22 | opacity: 0; 23 | } 24 | .ng-move { 25 | opacity: 0.5; 26 | } 27 | .ng-move.ng-move-active { 28 | opacity: 1; 29 | } 30 | } 31 | 32 | // Slide 33 | .#{$ngt-module}--animate-slide { 34 | .ng-enter, 35 | .ng-leave, 36 | .ng-move { 37 | position: relative; 38 | transition-duration: $ngt-transition-duration; 39 | transition-timing-function: $ngt-transition-timing-function; 40 | } 41 | 42 | &.#{$ngt-module}--center { 43 | 44 | // in/out from top when centered and top aligned 45 | &.#{$ngt-module}--top { 46 | 47 | .#{$ngt-module}__message { 48 | position: relative; 49 | transition-property: top, margin-top, opacity; 50 | &.ng-enter { 51 | opacity: 0; 52 | top: -100px; 53 | } 54 | &.ng-enter.ng-enter-active { 55 | opacity: 1; 56 | top: 0; 57 | } 58 | &.ng-leave { 59 | opacity: 1; 60 | top: 0; 61 | } 62 | &.ng-leave.ng-leave-active { 63 | opacity: 0; 64 | margin-top: -(52px + $ngt-spacing); 65 | } 66 | } 67 | } 68 | 69 | // in/out from bottom when centered and bottom aligned 70 | &.#{$ngt-module}--bottom { 71 | 72 | .#{$ngt-module}__message { 73 | position: relative; 74 | transition-property: bottom, margin-bottom, opacity; 75 | &.ng-enter { 76 | opacity: 0; 77 | bottom: -100px; 78 | } 79 | &.ng-enter.ng-enter-active { 80 | opacity: 1; 81 | bottom: 0; 82 | } 83 | &.ng-leave { 84 | opacity: 1; 85 | bottom: 0; 86 | } 87 | &.ng-leave.ng-leave-active { 88 | opacity: 0; 89 | margin-bottom: -(52px + $ngt-spacing); 90 | } 91 | } 92 | } 93 | } 94 | 95 | // in/out from right when right aligned 96 | &.#{$ngt-module}--right { 97 | transition-property: right, margin-right, opacity; 98 | 99 | .ng-enter { 100 | opacity: 0; 101 | right: -200%; 102 | margin-right: 20px; 103 | } 104 | .ng-enter.ng-enter-active { 105 | opacity: 1; 106 | right: 0; 107 | margin-right: 0; 108 | } 109 | .ng-leave { 110 | opacity: 1; 111 | right: 0; 112 | margin-right: 0; 113 | } 114 | .ng-leave.ng-leave-active { 115 | opacity: 0; 116 | right: -200%; 117 | margin-right: 20px; 118 | } 119 | } 120 | 121 | // in/out from left when left aligned 122 | &.#{$ngt-module}--left { 123 | transition-property: left, margin-left, opacity; 124 | 125 | .ng-enter { 126 | opacity: 0; 127 | left: -200%; 128 | margin-left: 20px; 129 | } 130 | .ng-enter.ng-enter-active { 131 | opacity: 1; 132 | left: 0; 133 | margin-left: 0; 134 | } 135 | .ng-leave { 136 | opacity: 1; 137 | left: 0; 138 | margin-left: 0; 139 | } 140 | .ng-leave.ng-leave-active { 141 | opacity: 0; 142 | left: -200%; 143 | margin-left: 20px; 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/styles/sass/variables.scss: -------------------------------------------------------------------------------- 1 | $ngt-module: 'ng-toast'; 2 | $ngt-spacing: 20px; 3 | $ngt-transition-duration: 0.3s; 4 | $ngt-transition-timing-function: ease; 5 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-06-04 using 4 | // generator-karma 0.8.1 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | // enable / disable watching file and executing tests whenever any file changes 9 | autoWatch: true, 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: '../', 13 | 14 | // testing framework to use (jasmine/mocha/qunit/...) 15 | frameworks: ['jasmine'], 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'test/vendor/angular.js', 20 | 'test/vendor/angular-mocks.js', 21 | 'test/vendor/angular-sanitize.js', 22 | 'src/scripts/*.js', 23 | 'test/spec/*.js' 24 | ], 25 | 26 | // list of files / patterns to exclude 27 | exclude: [], 28 | 29 | // web server port 30 | port: 8080, 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: [ 41 | 'PhantomJS' 42 | ], 43 | 44 | // Which plugins to enable 45 | plugins: [ 46 | 'karma-phantomjs-launcher', 47 | 'karma-jasmine' 48 | ], 49 | 50 | // Continuous Integration mode 51 | // if true, it capture browsers, run tests and exit 52 | singleRun: false, 53 | 54 | colors: true, 55 | 56 | // level of logging 57 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 58 | logLevel: config.LOG_INFO 59 | 60 | // Uncomment the following lines if you are using grunt's server to run the tests 61 | // proxies: { 62 | // '/': 'http://localhost:9000/' 63 | // }, 64 | // URL root prevent conflicts with the site root 65 | // urlRoot: '_karma_' 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/spec/directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ngToast:', function() { 4 | 5 | describe('directive:', function() { 6 | var ngToast, 7 | compiler, 8 | rootScope, 9 | element; 10 | 11 | beforeEach(function () { 12 | module('ngToast'); 13 | }); 14 | 15 | beforeEach(function() { 16 | inject(function (_ngToast_, _$compile_, _$rootScope_) { 17 | ngToast = _ngToast_; 18 | compiler = _$compile_; 19 | rootScope = _$rootScope_; 20 | }); 21 | 22 | ngToast.messages = [ 23 | { 24 | id: 1, 25 | content: 'test1', 26 | compileContent: true // somehow throws error if not defined... 27 | }, 28 | { 29 | id: 2, 30 | content: 'test2', 31 | compileContent: true 32 | } 33 | ]; 34 | 35 | element = compiler('')(rootScope); 36 | rootScope.$digest(); 37 | }); 38 | 39 | //TODO: button should work although dismissOnClick 40 | 41 | it('should initialize properly', function() { 42 | expect(element.html().indexOf('ng-toast__list') > -1).toBeTruthy(); 43 | 44 | var messages = element.children().children(); 45 | expect(messages.length).toBe(2); 46 | expect(angular.element(messages['0']).html()).toContain('test'); 47 | expect(angular.element(messages['1']).attr('data-message-id')).toBe('2'); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ngToast:', function() { 4 | 5 | describe('service:', function() { 6 | var ngToast; 7 | 8 | beforeEach(function () { 9 | module('ngToast.provider'); 10 | }); 11 | 12 | beforeEach(inject(function (_ngToast_) { 13 | ngToast = _ngToast_; 14 | })); 15 | 16 | it('initial values should be set', function () { 17 | expect(ngToast.messages).toEqual([]); 18 | expect(ngToast.settings).not.toEqual({}); 19 | }); 20 | 21 | it('create should work', function () { 22 | ngToast.create('toast1'); 23 | expect(ngToast.messages.length).toBe(1); 24 | expect(ngToast.messages[0].content).toBe('toast1'); 25 | 26 | ngToast.create({content: 'toast2'}); 27 | expect(ngToast.messages.length).toBe(2); 28 | expect(ngToast.messages[0].content).toBe('toast2'); 29 | }); 30 | 31 | it('success should work', function () { 32 | ngToast.success('toast1'); 33 | expect(ngToast.messages.length).toBe(1); 34 | expect(ngToast.messages[0].content).toBe('toast1'); 35 | expect(ngToast.messages[0].className).toBe('success'); 36 | 37 | ngToast.success({ 38 | content: 'toast2' 39 | }); 40 | expect(ngToast.messages.length).toBe(2); 41 | expect(ngToast.messages[0].content).toBe('toast2'); 42 | expect(ngToast.messages[0].className).toBe('success'); 43 | }); 44 | 45 | it('info should work', function () { 46 | ngToast.info('toast1'); 47 | expect(ngToast.messages.length).toBe(1); 48 | expect(ngToast.messages[0].content).toBe('toast1'); 49 | expect(ngToast.messages[0].className).toBe('info'); 50 | }); 51 | 52 | it('warning should work', function () { 53 | ngToast.warning('toast1'); 54 | expect(ngToast.messages.length).toBe(1); 55 | expect(ngToast.messages[0].content).toBe('toast1'); 56 | expect(ngToast.messages[0].className).toBe('warning'); 57 | }); 58 | 59 | it('danger should work', function () { 60 | ngToast.danger('toast1'); 61 | expect(ngToast.messages.length).toBe(1); 62 | expect(ngToast.messages[0].content).toBe('toast1'); 63 | expect(ngToast.messages[0].className).toBe('danger'); 64 | }); 65 | 66 | it('should respect to newestOnTop flag', function () { 67 | ngToast.create('toast1'); 68 | 69 | ngToast.create('toast2'); 70 | expect(ngToast.messages[0].content).toBe('toast2'); 71 | 72 | ngToast.settings.newestOnTop = false; 73 | ngToast.create('toast3'); 74 | expect(ngToast.messages[2].content).toBe('toast3'); 75 | }); 76 | 77 | it('create should dismiss first message when reached to max limit', function () { 78 | ngToast.settings.maxNumber = 2; 79 | ngToast.create('toast1'); 80 | ngToast.create('toast2'); 81 | 82 | ngToast.create('toast3'); 83 | expect(ngToast.messages.length).toBe(2); 84 | expect(ngToast.messages[0].content).toBe('toast3'); 85 | expect(ngToast.messages[1].content).toBe('toast2'); 86 | }); 87 | 88 | it('dismiss should work', function () { 89 | var toast1 = ngToast.create('toast1'); 90 | var toast2 = ngToast.create('toast2'); 91 | 92 | ngToast.dismiss(-1); // non-existent id 93 | expect(ngToast.messages.length).toBe(2); 94 | expect(ngToast.messages[0].content).toBe('toast2'); 95 | 96 | ngToast.dismiss(toast2); 97 | expect(ngToast.messages.length).toBe(1); 98 | expect(ngToast.messages[0].content).toBe('toast1'); 99 | 100 | ngToast.dismiss(toast1); 101 | expect(ngToast.messages.length).toBe(0); 102 | }); 103 | 104 | it('dismiss all should work', function () { 105 | ngToast.create('toast1'); 106 | ngToast.create('toast2'); 107 | ngToast.create('toast3'); 108 | 109 | ngToast.dismiss(); 110 | expect(ngToast.messages.length).toBe(0); 111 | }); 112 | }); 113 | 114 | describe('service configuration:', function() { 115 | beforeEach(module('ngToast.provider', function(ngToastProvider) { 116 | ngToastProvider.configure({ 117 | className: "info", 118 | timeout: 3000, 119 | dismissButton: true, 120 | maxNumber: 3 121 | }); 122 | })); 123 | 124 | it('should respect config values', inject(function(ngToast) { 125 | expect(ngToast.settings.className).toBe("info"); 126 | expect(ngToast.settings.timeout).toBe(3000); 127 | expect(ngToast.settings.dismissButton).toBe(true); 128 | expect(ngToast.settings.maxNumber).toBe(3); 129 | })); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/vendor/angular-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.15 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 8 | 'use strict'; 9 | 10 | /** 11 | * @ngdoc object 12 | * @name angular.mock 13 | * @description 14 | * 15 | * Namespace from 'angular-mocks.js' which contains testing related code. 16 | */ 17 | angular.mock = {}; 18 | 19 | /** 20 | * ! This is a private undocumented service ! 21 | * 22 | * @name $browser 23 | * 24 | * @description 25 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 26 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 27 | * cookies, etc... 28 | * 29 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 30 | * that there are several helper methods available which can be used in tests. 31 | */ 32 | angular.mock.$BrowserProvider = function() { 33 | this.$get = function() { 34 | return new angular.mock.$Browser(); 35 | }; 36 | }; 37 | 38 | angular.mock.$Browser = function() { 39 | var self = this; 40 | 41 | this.isMock = true; 42 | self.$$url = "http://server/"; 43 | self.$$lastUrl = self.$$url; // used by url polling fn 44 | self.pollFns = []; 45 | 46 | // TODO(vojta): remove this temporary api 47 | self.$$completeOutstandingRequest = angular.noop; 48 | self.$$incOutstandingRequestCount = angular.noop; 49 | 50 | 51 | // register url polling fn 52 | 53 | self.onUrlChange = function(listener) { 54 | self.pollFns.push( 55 | function() { 56 | if (self.$$lastUrl != self.$$url) { 57 | self.$$lastUrl = self.$$url; 58 | listener(self.$$url); 59 | } 60 | } 61 | ); 62 | 63 | return listener; 64 | }; 65 | 66 | self.cookieHash = {}; 67 | self.lastCookieHash = {}; 68 | self.deferredFns = []; 69 | self.deferredNextId = 0; 70 | 71 | self.defer = function(fn, delay) { 72 | delay = delay || 0; 73 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 74 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 75 | return self.deferredNextId++; 76 | }; 77 | 78 | 79 | /** 80 | * @name $browser#defer.now 81 | * 82 | * @description 83 | * Current milliseconds mock time. 84 | */ 85 | self.defer.now = 0; 86 | 87 | 88 | self.defer.cancel = function(deferId) { 89 | var fnIndex; 90 | 91 | angular.forEach(self.deferredFns, function(fn, index) { 92 | if (fn.id === deferId) fnIndex = index; 93 | }); 94 | 95 | if (fnIndex !== undefined) { 96 | self.deferredFns.splice(fnIndex, 1); 97 | return true; 98 | } 99 | 100 | return false; 101 | }; 102 | 103 | 104 | /** 105 | * @name $browser#defer.flush 106 | * 107 | * @description 108 | * Flushes all pending requests and executes the defer callbacks. 109 | * 110 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 111 | */ 112 | self.defer.flush = function(delay) { 113 | if (angular.isDefined(delay)) { 114 | self.defer.now += delay; 115 | } else { 116 | if (self.deferredFns.length) { 117 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 118 | } else { 119 | throw new Error('No deferred tasks to be flushed'); 120 | } 121 | } 122 | 123 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 124 | self.deferredFns.shift().fn(); 125 | } 126 | }; 127 | 128 | self.$$baseHref = ''; 129 | self.baseHref = function() { 130 | return this.$$baseHref; 131 | }; 132 | }; 133 | angular.mock.$Browser.prototype = { 134 | 135 | /** 136 | * @name $browser#poll 137 | * 138 | * @description 139 | * run all fns in pollFns 140 | */ 141 | poll: function poll() { 142 | angular.forEach(this.pollFns, function(pollFn){ 143 | pollFn(); 144 | }); 145 | }, 146 | 147 | addPollFn: function(pollFn) { 148 | this.pollFns.push(pollFn); 149 | return pollFn; 150 | }, 151 | 152 | url: function(url, replace) { 153 | if (url) { 154 | this.$$url = url; 155 | return this; 156 | } 157 | 158 | return this.$$url; 159 | }, 160 | 161 | cookies: function(name, value) { 162 | if (name) { 163 | if (angular.isUndefined(value)) { 164 | delete this.cookieHash[name]; 165 | } else { 166 | if (angular.isString(value) && //strings only 167 | value.length <= 4096) { //strict cookie storage limits 168 | this.cookieHash[name] = value; 169 | } 170 | } 171 | } else { 172 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 173 | this.lastCookieHash = angular.copy(this.cookieHash); 174 | this.cookieHash = angular.copy(this.cookieHash); 175 | } 176 | return this.cookieHash; 177 | } 178 | }, 179 | 180 | notifyWhenNoOutstandingRequests: function(fn) { 181 | fn(); 182 | } 183 | }; 184 | 185 | 186 | /** 187 | * @ngdoc provider 188 | * @name $exceptionHandlerProvider 189 | * 190 | * @description 191 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors 192 | * passed into the `$exceptionHandler`. 193 | */ 194 | 195 | /** 196 | * @ngdoc service 197 | * @name $exceptionHandler 198 | * 199 | * @description 200 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 201 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 202 | * information. 203 | * 204 | * 205 | * ```js 206 | * describe('$exceptionHandlerProvider', function() { 207 | * 208 | * it('should capture log messages and exceptions', function() { 209 | * 210 | * module(function($exceptionHandlerProvider) { 211 | * $exceptionHandlerProvider.mode('log'); 212 | * }); 213 | * 214 | * inject(function($log, $exceptionHandler, $timeout) { 215 | * $timeout(function() { $log.log(1); }); 216 | * $timeout(function() { $log.log(2); throw 'banana peel'; }); 217 | * $timeout(function() { $log.log(3); }); 218 | * expect($exceptionHandler.errors).toEqual([]); 219 | * expect($log.assertEmpty()); 220 | * $timeout.flush(); 221 | * expect($exceptionHandler.errors).toEqual(['banana peel']); 222 | * expect($log.log.logs).toEqual([[1], [2], [3]]); 223 | * }); 224 | * }); 225 | * }); 226 | * ``` 227 | */ 228 | 229 | angular.mock.$ExceptionHandlerProvider = function() { 230 | var handler; 231 | 232 | /** 233 | * @ngdoc method 234 | * @name $exceptionHandlerProvider#mode 235 | * 236 | * @description 237 | * Sets the logging mode. 238 | * 239 | * @param {string} mode Mode of operation, defaults to `rethrow`. 240 | * 241 | * - `rethrow`: If any errors are passed into the handler in tests, it typically 242 | * means that there is a bug in the application or test, so this mock will 243 | * make these tests fail. 244 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` 245 | * mode stores an array of errors in `$exceptionHandler.errors`, to allow later 246 | * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and 247 | * {@link ngMock.$log#reset reset()} 248 | */ 249 | this.mode = function(mode) { 250 | switch(mode) { 251 | case 'rethrow': 252 | handler = function(e) { 253 | throw e; 254 | }; 255 | break; 256 | case 'log': 257 | var errors = []; 258 | 259 | handler = function(e) { 260 | if (arguments.length == 1) { 261 | errors.push(e); 262 | } else { 263 | errors.push([].slice.call(arguments, 0)); 264 | } 265 | }; 266 | 267 | handler.errors = errors; 268 | break; 269 | default: 270 | throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 271 | } 272 | }; 273 | 274 | this.$get = function() { 275 | return handler; 276 | }; 277 | 278 | this.mode('rethrow'); 279 | }; 280 | 281 | 282 | /** 283 | * @ngdoc service 284 | * @name $log 285 | * 286 | * @description 287 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 288 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 289 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 290 | * 291 | */ 292 | angular.mock.$LogProvider = function() { 293 | var debug = true; 294 | 295 | function concat(array1, array2, index) { 296 | return array1.concat(Array.prototype.slice.call(array2, index)); 297 | } 298 | 299 | this.debugEnabled = function(flag) { 300 | if (angular.isDefined(flag)) { 301 | debug = flag; 302 | return this; 303 | } else { 304 | return debug; 305 | } 306 | }; 307 | 308 | this.$get = function () { 309 | var $log = { 310 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 311 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 312 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 313 | error: function() { $log.error.logs.push(concat([], arguments, 0)); }, 314 | debug: function() { 315 | if (debug) { 316 | $log.debug.logs.push(concat([], arguments, 0)); 317 | } 318 | } 319 | }; 320 | 321 | /** 322 | * @ngdoc method 323 | * @name $log#reset 324 | * 325 | * @description 326 | * Reset all of the logging arrays to empty. 327 | */ 328 | $log.reset = function () { 329 | /** 330 | * @ngdoc property 331 | * @name $log#log.logs 332 | * 333 | * @description 334 | * Array of messages logged using {@link ngMock.$log#log}. 335 | * 336 | * @example 337 | * ```js 338 | * $log.log('Some Log'); 339 | * var first = $log.log.logs.unshift(); 340 | * ``` 341 | */ 342 | $log.log.logs = []; 343 | /** 344 | * @ngdoc property 345 | * @name $log#info.logs 346 | * 347 | * @description 348 | * Array of messages logged using {@link ngMock.$log#info}. 349 | * 350 | * @example 351 | * ```js 352 | * $log.info('Some Info'); 353 | * var first = $log.info.logs.unshift(); 354 | * ``` 355 | */ 356 | $log.info.logs = []; 357 | /** 358 | * @ngdoc property 359 | * @name $log#warn.logs 360 | * 361 | * @description 362 | * Array of messages logged using {@link ngMock.$log#warn}. 363 | * 364 | * @example 365 | * ```js 366 | * $log.warn('Some Warning'); 367 | * var first = $log.warn.logs.unshift(); 368 | * ``` 369 | */ 370 | $log.warn.logs = []; 371 | /** 372 | * @ngdoc property 373 | * @name $log#error.logs 374 | * 375 | * @description 376 | * Array of messages logged using {@link ngMock.$log#error}. 377 | * 378 | * @example 379 | * ```js 380 | * $log.error('Some Error'); 381 | * var first = $log.error.logs.unshift(); 382 | * ``` 383 | */ 384 | $log.error.logs = []; 385 | /** 386 | * @ngdoc property 387 | * @name $log#debug.logs 388 | * 389 | * @description 390 | * Array of messages logged using {@link ngMock.$log#debug}. 391 | * 392 | * @example 393 | * ```js 394 | * $log.debug('Some Error'); 395 | * var first = $log.debug.logs.unshift(); 396 | * ``` 397 | */ 398 | $log.debug.logs = []; 399 | }; 400 | 401 | /** 402 | * @ngdoc method 403 | * @name $log#assertEmpty 404 | * 405 | * @description 406 | * Assert that the all of the logging methods have no logged messages. If messages present, an 407 | * exception is thrown. 408 | */ 409 | $log.assertEmpty = function() { 410 | var errors = []; 411 | angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { 412 | angular.forEach($log[logLevel].logs, function(log) { 413 | angular.forEach(log, function (logItem) { 414 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + 415 | (logItem.stack || '')); 416 | }); 417 | }); 418 | }); 419 | if (errors.length) { 420 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ 421 | "an expected log message was not checked and removed:"); 422 | errors.push(''); 423 | throw new Error(errors.join('\n---------\n')); 424 | } 425 | }; 426 | 427 | $log.reset(); 428 | return $log; 429 | }; 430 | }; 431 | 432 | 433 | /** 434 | * @ngdoc service 435 | * @name $interval 436 | * 437 | * @description 438 | * Mock implementation of the $interval service. 439 | * 440 | * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to 441 | * move forward by `millis` milliseconds and trigger any functions scheduled to run in that 442 | * time. 443 | * 444 | * @param {function()} fn A function that should be called repeatedly. 445 | * @param {number} delay Number of milliseconds between each function call. 446 | * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat 447 | * indefinitely. 448 | * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise 449 | * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. 450 | * @returns {promise} A promise which will be notified on each iteration. 451 | */ 452 | angular.mock.$IntervalProvider = function() { 453 | this.$get = ['$rootScope', '$q', 454 | function($rootScope, $q) { 455 | var repeatFns = [], 456 | nextRepeatId = 0, 457 | now = 0; 458 | 459 | var $interval = function(fn, delay, count, invokeApply) { 460 | var deferred = $q.defer(), 461 | promise = deferred.promise, 462 | iteration = 0, 463 | skipApply = (angular.isDefined(invokeApply) && !invokeApply); 464 | 465 | count = (angular.isDefined(count)) ? count : 0, 466 | promise.then(null, null, fn); 467 | 468 | promise.$$intervalId = nextRepeatId; 469 | 470 | function tick() { 471 | deferred.notify(iteration++); 472 | 473 | if (count > 0 && iteration >= count) { 474 | var fnIndex; 475 | deferred.resolve(iteration); 476 | 477 | angular.forEach(repeatFns, function(fn, index) { 478 | if (fn.id === promise.$$intervalId) fnIndex = index; 479 | }); 480 | 481 | if (fnIndex !== undefined) { 482 | repeatFns.splice(fnIndex, 1); 483 | } 484 | } 485 | 486 | if (!skipApply) $rootScope.$apply(); 487 | } 488 | 489 | repeatFns.push({ 490 | nextTime:(now + delay), 491 | delay: delay, 492 | fn: tick, 493 | id: nextRepeatId, 494 | deferred: deferred 495 | }); 496 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 497 | 498 | nextRepeatId++; 499 | return promise; 500 | }; 501 | /** 502 | * @ngdoc method 503 | * @name $interval#cancel 504 | * 505 | * @description 506 | * Cancels a task associated with the `promise`. 507 | * 508 | * @param {promise} promise A promise from calling the `$interval` function. 509 | * @returns {boolean} Returns `true` if the task was successfully cancelled. 510 | */ 511 | $interval.cancel = function(promise) { 512 | if(!promise) return false; 513 | var fnIndex; 514 | 515 | angular.forEach(repeatFns, function(fn, index) { 516 | if (fn.id === promise.$$intervalId) fnIndex = index; 517 | }); 518 | 519 | if (fnIndex !== undefined) { 520 | repeatFns[fnIndex].deferred.reject('canceled'); 521 | repeatFns.splice(fnIndex, 1); 522 | return true; 523 | } 524 | 525 | return false; 526 | }; 527 | 528 | /** 529 | * @ngdoc method 530 | * @name $interval#flush 531 | * @description 532 | * 533 | * Runs interval tasks scheduled to be run in the next `millis` milliseconds. 534 | * 535 | * @param {number=} millis maximum timeout amount to flush up until. 536 | * 537 | * @return {number} The amount of time moved forward. 538 | */ 539 | $interval.flush = function(millis) { 540 | now += millis; 541 | while (repeatFns.length && repeatFns[0].nextTime <= now) { 542 | var task = repeatFns[0]; 543 | task.fn(); 544 | task.nextTime += task.delay; 545 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 546 | } 547 | return millis; 548 | }; 549 | 550 | return $interval; 551 | }]; 552 | }; 553 | 554 | 555 | /* jshint -W101 */ 556 | /* The R_ISO8061_STR regex is never going to fit into the 100 char limit! 557 | * This directive should go inside the anonymous function but a bug in JSHint means that it would 558 | * not be enacted early enough to prevent the warning. 559 | */ 560 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 561 | 562 | function jsonStringToDate(string) { 563 | var match; 564 | if (match = string.match(R_ISO8061_STR)) { 565 | var date = new Date(0), 566 | tzHour = 0, 567 | tzMin = 0; 568 | if (match[9]) { 569 | tzHour = int(match[9] + match[10]); 570 | tzMin = int(match[9] + match[11]); 571 | } 572 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 573 | date.setUTCHours(int(match[4]||0) - tzHour, 574 | int(match[5]||0) - tzMin, 575 | int(match[6]||0), 576 | int(match[7]||0)); 577 | return date; 578 | } 579 | return string; 580 | } 581 | 582 | function int(str) { 583 | return parseInt(str, 10); 584 | } 585 | 586 | function padNumber(num, digits, trim) { 587 | var neg = ''; 588 | if (num < 0) { 589 | neg = '-'; 590 | num = -num; 591 | } 592 | num = '' + num; 593 | while(num.length < digits) num = '0' + num; 594 | if (trim) 595 | num = num.substr(num.length - digits); 596 | return neg + num; 597 | } 598 | 599 | 600 | /** 601 | * @ngdoc type 602 | * @name angular.mock.TzDate 603 | * @description 604 | * 605 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 606 | * 607 | * Mock of the Date type which has its timezone specified via constructor arg. 608 | * 609 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 610 | * offset, so that we can test code that depends on local timezone settings without dependency on 611 | * the time zone settings of the machine where the code is running. 612 | * 613 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 614 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 615 | * 616 | * @example 617 | * !!!! WARNING !!!!! 618 | * This is not a complete Date object so only methods that were implemented can be called safely. 619 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 620 | * 621 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 622 | * incomplete we might be missing some non-standard methods. This can result in errors like: 623 | * "Date.prototype.foo called on incompatible Object". 624 | * 625 | * ```js 626 | * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); 627 | * newYearInBratislava.getTimezoneOffset() => -60; 628 | * newYearInBratislava.getFullYear() => 2010; 629 | * newYearInBratislava.getMonth() => 0; 630 | * newYearInBratislava.getDate() => 1; 631 | * newYearInBratislava.getHours() => 0; 632 | * newYearInBratislava.getMinutes() => 0; 633 | * newYearInBratislava.getSeconds() => 0; 634 | * ``` 635 | * 636 | */ 637 | angular.mock.TzDate = function (offset, timestamp) { 638 | var self = new Date(0); 639 | if (angular.isString(timestamp)) { 640 | var tsStr = timestamp; 641 | 642 | self.origDate = jsonStringToDate(timestamp); 643 | 644 | timestamp = self.origDate.getTime(); 645 | if (isNaN(timestamp)) 646 | throw { 647 | name: "Illegal Argument", 648 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 649 | }; 650 | } else { 651 | self.origDate = new Date(timestamp); 652 | } 653 | 654 | var localOffset = new Date(timestamp).getTimezoneOffset(); 655 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 656 | self.date = new Date(timestamp + self.offsetDiff); 657 | 658 | self.getTime = function() { 659 | return self.date.getTime() - self.offsetDiff; 660 | }; 661 | 662 | self.toLocaleDateString = function() { 663 | return self.date.toLocaleDateString(); 664 | }; 665 | 666 | self.getFullYear = function() { 667 | return self.date.getFullYear(); 668 | }; 669 | 670 | self.getMonth = function() { 671 | return self.date.getMonth(); 672 | }; 673 | 674 | self.getDate = function() { 675 | return self.date.getDate(); 676 | }; 677 | 678 | self.getHours = function() { 679 | return self.date.getHours(); 680 | }; 681 | 682 | self.getMinutes = function() { 683 | return self.date.getMinutes(); 684 | }; 685 | 686 | self.getSeconds = function() { 687 | return self.date.getSeconds(); 688 | }; 689 | 690 | self.getMilliseconds = function() { 691 | return self.date.getMilliseconds(); 692 | }; 693 | 694 | self.getTimezoneOffset = function() { 695 | return offset * 60; 696 | }; 697 | 698 | self.getUTCFullYear = function() { 699 | return self.origDate.getUTCFullYear(); 700 | }; 701 | 702 | self.getUTCMonth = function() { 703 | return self.origDate.getUTCMonth(); 704 | }; 705 | 706 | self.getUTCDate = function() { 707 | return self.origDate.getUTCDate(); 708 | }; 709 | 710 | self.getUTCHours = function() { 711 | return self.origDate.getUTCHours(); 712 | }; 713 | 714 | self.getUTCMinutes = function() { 715 | return self.origDate.getUTCMinutes(); 716 | }; 717 | 718 | self.getUTCSeconds = function() { 719 | return self.origDate.getUTCSeconds(); 720 | }; 721 | 722 | self.getUTCMilliseconds = function() { 723 | return self.origDate.getUTCMilliseconds(); 724 | }; 725 | 726 | self.getDay = function() { 727 | return self.date.getDay(); 728 | }; 729 | 730 | // provide this method only on browsers that already have it 731 | if (self.toISOString) { 732 | self.toISOString = function() { 733 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 734 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 735 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 736 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 737 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 738 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 739 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; 740 | }; 741 | } 742 | 743 | //hide all methods not implemented in this mock that the Date prototype exposes 744 | var unimplementedMethods = ['getUTCDay', 745 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 746 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 747 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 748 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 749 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 750 | 751 | angular.forEach(unimplementedMethods, function(methodName) { 752 | self[methodName] = function() { 753 | throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 754 | }; 755 | }); 756 | 757 | return self; 758 | }; 759 | 760 | //make "tzDateInstance instanceof Date" return true 761 | angular.mock.TzDate.prototype = Date.prototype; 762 | /* jshint +W101 */ 763 | 764 | angular.mock.animate = angular.module('ngAnimateMock', ['ng']) 765 | 766 | .config(['$provide', function($provide) { 767 | 768 | var reflowQueue = []; 769 | $provide.value('$$animateReflow', function(fn) { 770 | var index = reflowQueue.length; 771 | reflowQueue.push(fn); 772 | return function cancel() { 773 | reflowQueue.splice(index, 1); 774 | }; 775 | }); 776 | 777 | $provide.decorator('$animate', function($delegate, $$asyncCallback) { 778 | var animate = { 779 | queue : [], 780 | enabled : $delegate.enabled, 781 | triggerCallbacks : function() { 782 | $$asyncCallback.flush(); 783 | }, 784 | triggerReflow : function() { 785 | angular.forEach(reflowQueue, function(fn) { 786 | fn(); 787 | }); 788 | reflowQueue = []; 789 | } 790 | }; 791 | 792 | angular.forEach( 793 | ['enter','leave','move','addClass','removeClass','setClass'], function(method) { 794 | animate[method] = function() { 795 | animate.queue.push({ 796 | event : method, 797 | element : arguments[0], 798 | args : arguments 799 | }); 800 | $delegate[method].apply($delegate, arguments); 801 | }; 802 | }); 803 | 804 | return animate; 805 | }); 806 | 807 | }]); 808 | 809 | 810 | /** 811 | * @ngdoc function 812 | * @name angular.mock.dump 813 | * @description 814 | * 815 | * *NOTE*: this is not an injectable instance, just a globally available function. 816 | * 817 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for 818 | * debugging. 819 | * 820 | * This method is also available on window, where it can be used to display objects on debug 821 | * console. 822 | * 823 | * @param {*} object - any object to turn into string. 824 | * @return {string} a serialized string of the argument 825 | */ 826 | angular.mock.dump = function(object) { 827 | return serialize(object); 828 | 829 | function serialize(object) { 830 | var out; 831 | 832 | if (angular.isElement(object)) { 833 | object = angular.element(object); 834 | out = angular.element('
    '); 835 | angular.forEach(object, function(element) { 836 | out.append(angular.element(element).clone()); 837 | }); 838 | out = out.html(); 839 | } else if (angular.isArray(object)) { 840 | out = []; 841 | angular.forEach(object, function(o) { 842 | out.push(serialize(o)); 843 | }); 844 | out = '[ ' + out.join(', ') + ' ]'; 845 | } else if (angular.isObject(object)) { 846 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 847 | out = serializeScope(object); 848 | } else if (object instanceof Error) { 849 | out = object.stack || ('' + object.name + ': ' + object.message); 850 | } else { 851 | // TODO(i): this prevents methods being logged, 852 | // we should have a better way to serialize objects 853 | out = angular.toJson(object, true); 854 | } 855 | } else { 856 | out = String(object); 857 | } 858 | 859 | return out; 860 | } 861 | 862 | function serializeScope(scope, offset) { 863 | offset = offset || ' '; 864 | var log = [offset + 'Scope(' + scope.$id + '): {']; 865 | for ( var key in scope ) { 866 | if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { 867 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 868 | } 869 | } 870 | var child = scope.$$childHead; 871 | while(child) { 872 | log.push(serializeScope(child, offset + ' ')); 873 | child = child.$$nextSibling; 874 | } 875 | log.push('}'); 876 | return log.join('\n' + offset); 877 | } 878 | }; 879 | 880 | /** 881 | * @ngdoc service 882 | * @name $httpBackend 883 | * @description 884 | * Fake HTTP backend implementation suitable for unit testing applications that use the 885 | * {@link ng.$http $http service}. 886 | * 887 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less 888 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 889 | * 890 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 891 | * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or 892 | * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is 893 | * to verify whether a certain request has been sent or not, or alternatively just let the 894 | * application make requests, respond with pre-trained responses and assert that the end result is 895 | * what we expect it to be. 896 | * 897 | * This mock implementation can be used to respond with static or dynamic responses via the 898 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 899 | * 900 | * When an Angular application needs some data from a server, it calls the $http service, which 901 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 902 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 903 | * the requests and respond with some testing data without sending a request to real server. 904 | * 905 | * There are two ways to specify what test data should be returned as http responses by the mock 906 | * backend when the code under test makes http requests: 907 | * 908 | * - `$httpBackend.expect` - specifies a request expectation 909 | * - `$httpBackend.when` - specifies a backend definition 910 | * 911 | * 912 | * # Request Expectations vs Backend Definitions 913 | * 914 | * Request expectations provide a way to make assertions about requests made by the application and 915 | * to define responses for those requests. The test will fail if the expected requests are not made 916 | * or they are made in the wrong order. 917 | * 918 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 919 | * if a particular request was made or not, it just returns a trained response if a request is made. 920 | * The test will pass whether or not the request gets made during testing. 921 | * 922 | * 923 | * 924 | * 925 | * 926 | * 927 | * 928 | * 929 | * 930 | * 931 | * 932 | * 933 | * 934 | * 935 | * 936 | * 937 | * 938 | * 939 | * 940 | * 941 | * 942 | * 943 | * 944 | * 945 | * 946 | * 947 | * 948 | * 949 | * 950 | * 951 | * 952 | * 953 | * 954 | * 955 | *
    Request expectationsBackend definitions
    Syntax.expect(...).respond(...).when(...).respond(...)
    Typical usagestrict unit testsloose (black-box) unit testing
    Fulfills multiple requestsNOYES
    Order of requests mattersYESNO
    Request requiredYESNO
    Response requiredoptional (see below)YES
    956 | * 957 | * In cases where both backend definitions and request expectations are specified during unit 958 | * testing, the request expectations are evaluated first. 959 | * 960 | * If a request expectation has no response specified, the algorithm will search your backend 961 | * definitions for an appropriate response. 962 | * 963 | * If a request didn't match any expectation or if the expectation doesn't have the response 964 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 965 | * the request. The response from the first matched definition is returned. 966 | * 967 | * 968 | * # Flushing HTTP requests 969 | * 970 | * The $httpBackend used in production always responds to requests asynchronously. If we preserved 971 | * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, 972 | * to follow and to maintain. But neither can the testing mock respond synchronously; that would 973 | * change the execution of the code under test. For this reason, the mock $httpBackend has a 974 | * `flush()` method, which allows the test to explicitly flush pending requests. This preserves 975 | * the async api of the backend, while allowing the test to execute synchronously. 976 | * 977 | * 978 | * # Unit testing with mock $httpBackend 979 | * The following code shows how to setup and use the mock backend when unit testing a controller. 980 | * First we create the controller under test: 981 | * 982 | ```js 983 | // The controller code 984 | function MyController($scope, $http) { 985 | var authToken; 986 | 987 | $http.get('/auth.py').success(function(data, status, headers) { 988 | authToken = headers('A-Token'); 989 | $scope.user = data; 990 | }); 991 | 992 | $scope.saveMessage = function(message) { 993 | var headers = { 'Authorization': authToken }; 994 | $scope.status = 'Saving...'; 995 | 996 | $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { 997 | $scope.status = ''; 998 | }).error(function() { 999 | $scope.status = 'ERROR!'; 1000 | }); 1001 | }; 1002 | } 1003 | ``` 1004 | * 1005 | * Now we setup the mock backend and create the test specs: 1006 | * 1007 | ```js 1008 | // testing controller 1009 | describe('MyController', function() { 1010 | var $httpBackend, $rootScope, createController; 1011 | 1012 | beforeEach(inject(function($injector) { 1013 | // Set up the mock http service responses 1014 | $httpBackend = $injector.get('$httpBackend'); 1015 | // backend definition common for all tests 1016 | $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'}); 1017 | 1018 | // Get hold of a scope (i.e. the root scope) 1019 | $rootScope = $injector.get('$rootScope'); 1020 | // The $controller service is used to create instances of controllers 1021 | var $controller = $injector.get('$controller'); 1022 | 1023 | createController = function() { 1024 | return $controller('MyController', {'$scope' : $rootScope }); 1025 | }; 1026 | })); 1027 | 1028 | 1029 | afterEach(function() { 1030 | $httpBackend.verifyNoOutstandingExpectation(); 1031 | $httpBackend.verifyNoOutstandingRequest(); 1032 | }); 1033 | 1034 | 1035 | it('should fetch authentication token', function() { 1036 | $httpBackend.expectGET('/auth.py'); 1037 | var controller = createController(); 1038 | $httpBackend.flush(); 1039 | }); 1040 | 1041 | 1042 | it('should send msg to server', function() { 1043 | var controller = createController(); 1044 | $httpBackend.flush(); 1045 | 1046 | // now you don’t care about the authentication, but 1047 | // the controller will still send the request and 1048 | // $httpBackend will respond without you having to 1049 | // specify the expectation and response for this request 1050 | 1051 | $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, ''); 1052 | $rootScope.saveMessage('message content'); 1053 | expect($rootScope.status).toBe('Saving...'); 1054 | $httpBackend.flush(); 1055 | expect($rootScope.status).toBe(''); 1056 | }); 1057 | 1058 | 1059 | it('should send auth header', function() { 1060 | var controller = createController(); 1061 | $httpBackend.flush(); 1062 | 1063 | $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { 1064 | // check if the header was send, if it wasn't the expectation won't 1065 | // match the request and the test will fail 1066 | return headers['Authorization'] == 'xxx'; 1067 | }).respond(201, ''); 1068 | 1069 | $rootScope.saveMessage('whatever'); 1070 | $httpBackend.flush(); 1071 | }); 1072 | }); 1073 | ``` 1074 | */ 1075 | angular.mock.$HttpBackendProvider = function() { 1076 | this.$get = ['$rootScope', createHttpBackendMock]; 1077 | }; 1078 | 1079 | /** 1080 | * General factory function for $httpBackend mock. 1081 | * Returns instance for unit testing (when no arguments specified): 1082 | * - passing through is disabled 1083 | * - auto flushing is disabled 1084 | * 1085 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 1086 | * - passing through (delegating request to real backend) is enabled 1087 | * - auto flushing is enabled 1088 | * 1089 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 1090 | * @param {Object=} $browser Auto-flushing enabled if specified 1091 | * @return {Object} Instance of $httpBackend mock 1092 | */ 1093 | function createHttpBackendMock($rootScope, $delegate, $browser) { 1094 | var definitions = [], 1095 | expectations = [], 1096 | responses = [], 1097 | responsesPush = angular.bind(responses, responses.push), 1098 | copy = angular.copy; 1099 | 1100 | function createResponse(status, data, headers) { 1101 | if (angular.isFunction(status)) return status; 1102 | 1103 | return function() { 1104 | return angular.isNumber(status) 1105 | ? [status, data, headers] 1106 | : [200, status, data]; 1107 | }; 1108 | } 1109 | 1110 | // TODO(vojta): change params to: method, url, data, headers, callback 1111 | function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { 1112 | var xhr = new MockXhr(), 1113 | expectation = expectations[0], 1114 | wasExpected = false; 1115 | 1116 | function prettyPrint(data) { 1117 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 1118 | ? data 1119 | : angular.toJson(data); 1120 | } 1121 | 1122 | function wrapResponse(wrapped) { 1123 | if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); 1124 | 1125 | return handleResponse; 1126 | 1127 | function handleResponse() { 1128 | var response = wrapped.response(method, url, data, headers); 1129 | xhr.$$respHeaders = response[2]; 1130 | callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders()); 1131 | } 1132 | 1133 | function handleTimeout() { 1134 | for (var i = 0, ii = responses.length; i < ii; i++) { 1135 | if (responses[i] === handleResponse) { 1136 | responses.splice(i, 1); 1137 | callback(-1, undefined, ''); 1138 | break; 1139 | } 1140 | } 1141 | } 1142 | } 1143 | 1144 | if (expectation && expectation.match(method, url)) { 1145 | if (!expectation.matchData(data)) 1146 | throw new Error('Expected ' + expectation + ' with different data\n' + 1147 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 1148 | 1149 | if (!expectation.matchHeaders(headers)) 1150 | throw new Error('Expected ' + expectation + ' with different headers\n' + 1151 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 1152 | prettyPrint(headers)); 1153 | 1154 | expectations.shift(); 1155 | 1156 | if (expectation.response) { 1157 | responses.push(wrapResponse(expectation)); 1158 | return; 1159 | } 1160 | wasExpected = true; 1161 | } 1162 | 1163 | var i = -1, definition; 1164 | while ((definition = definitions[++i])) { 1165 | if (definition.match(method, url, data, headers || {})) { 1166 | if (definition.response) { 1167 | // if $browser specified, we do auto flush all requests 1168 | ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); 1169 | } else if (definition.passThrough) { 1170 | $delegate(method, url, data, callback, headers, timeout, withCredentials); 1171 | } else throw new Error('No response defined !'); 1172 | return; 1173 | } 1174 | } 1175 | throw wasExpected ? 1176 | new Error('No response defined !') : 1177 | new Error('Unexpected request: ' + method + ' ' + url + '\n' + 1178 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 1179 | } 1180 | 1181 | /** 1182 | * @ngdoc method 1183 | * @name $httpBackend#when 1184 | * @description 1185 | * Creates a new backend definition. 1186 | * 1187 | * @param {string} method HTTP method. 1188 | * @param {string|RegExp} url HTTP url. 1189 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1190 | * data string and returns true if the data is as expected. 1191 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1192 | * object and returns true if the headers match the current definition. 1193 | * @returns {requestHandler} Returns an object with `respond` method that controls how a matched 1194 | * request is handled. 1195 | * 1196 | * - respond – 1197 | * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1198 | * – The respond method takes a set of static data to be returned or a function that can return 1199 | * an array containing response status (number), response data (string) and response headers 1200 | * (Object). 1201 | */ 1202 | $httpBackend.when = function(method, url, data, headers) { 1203 | var definition = new MockHttpExpectation(method, url, data, headers), 1204 | chain = { 1205 | respond: function(status, data, headers) { 1206 | definition.response = createResponse(status, data, headers); 1207 | } 1208 | }; 1209 | 1210 | if ($browser) { 1211 | chain.passThrough = function() { 1212 | definition.passThrough = true; 1213 | }; 1214 | } 1215 | 1216 | definitions.push(definition); 1217 | return chain; 1218 | }; 1219 | 1220 | /** 1221 | * @ngdoc method 1222 | * @name $httpBackend#whenGET 1223 | * @description 1224 | * Creates a new backend definition for GET requests. For more info see `when()`. 1225 | * 1226 | * @param {string|RegExp} url HTTP url. 1227 | * @param {(Object|function(Object))=} headers HTTP headers. 1228 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1229 | * request is handled. 1230 | */ 1231 | 1232 | /** 1233 | * @ngdoc method 1234 | * @name $httpBackend#whenHEAD 1235 | * @description 1236 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1237 | * 1238 | * @param {string|RegExp} url HTTP url. 1239 | * @param {(Object|function(Object))=} headers HTTP headers. 1240 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1241 | * request is handled. 1242 | */ 1243 | 1244 | /** 1245 | * @ngdoc method 1246 | * @name $httpBackend#whenDELETE 1247 | * @description 1248 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1249 | * 1250 | * @param {string|RegExp} url HTTP url. 1251 | * @param {(Object|function(Object))=} headers HTTP headers. 1252 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1253 | * request is handled. 1254 | */ 1255 | 1256 | /** 1257 | * @ngdoc method 1258 | * @name $httpBackend#whenPOST 1259 | * @description 1260 | * Creates a new backend definition for POST requests. For more info see `when()`. 1261 | * 1262 | * @param {string|RegExp} url HTTP url. 1263 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1264 | * data string and returns true if the data is as expected. 1265 | * @param {(Object|function(Object))=} headers HTTP headers. 1266 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1267 | * request is handled. 1268 | */ 1269 | 1270 | /** 1271 | * @ngdoc method 1272 | * @name $httpBackend#whenPUT 1273 | * @description 1274 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1275 | * 1276 | * @param {string|RegExp} url HTTP url. 1277 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1278 | * data string and returns true if the data is as expected. 1279 | * @param {(Object|function(Object))=} headers HTTP headers. 1280 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1281 | * request is handled. 1282 | */ 1283 | 1284 | /** 1285 | * @ngdoc method 1286 | * @name $httpBackend#whenJSONP 1287 | * @description 1288 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1289 | * 1290 | * @param {string|RegExp} url HTTP url. 1291 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1292 | * request is handled. 1293 | */ 1294 | createShortMethods('when'); 1295 | 1296 | 1297 | /** 1298 | * @ngdoc method 1299 | * @name $httpBackend#expect 1300 | * @description 1301 | * Creates a new request expectation. 1302 | * 1303 | * @param {string} method HTTP method. 1304 | * @param {string|RegExp} url HTTP url. 1305 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1306 | * receives data string and returns true if the data is as expected, or Object if request body 1307 | * is in JSON format. 1308 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1309 | * object and returns true if the headers match the current expectation. 1310 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1311 | * request is handled. 1312 | * 1313 | * - respond – 1314 | * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1315 | * – The respond method takes a set of static data to be returned or a function that can return 1316 | * an array containing response status (number), response data (string) and response headers 1317 | * (Object). 1318 | */ 1319 | $httpBackend.expect = function(method, url, data, headers) { 1320 | var expectation = new MockHttpExpectation(method, url, data, headers); 1321 | expectations.push(expectation); 1322 | return { 1323 | respond: function(status, data, headers) { 1324 | expectation.response = createResponse(status, data, headers); 1325 | } 1326 | }; 1327 | }; 1328 | 1329 | 1330 | /** 1331 | * @ngdoc method 1332 | * @name $httpBackend#expectGET 1333 | * @description 1334 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1335 | * 1336 | * @param {string|RegExp} url HTTP url. 1337 | * @param {Object=} headers HTTP headers. 1338 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1339 | * request is handled. See #expect for more info. 1340 | */ 1341 | 1342 | /** 1343 | * @ngdoc method 1344 | * @name $httpBackend#expectHEAD 1345 | * @description 1346 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1347 | * 1348 | * @param {string|RegExp} url HTTP url. 1349 | * @param {Object=} headers HTTP headers. 1350 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1351 | * request is handled. 1352 | */ 1353 | 1354 | /** 1355 | * @ngdoc method 1356 | * @name $httpBackend#expectDELETE 1357 | * @description 1358 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1359 | * 1360 | * @param {string|RegExp} url HTTP url. 1361 | * @param {Object=} headers HTTP headers. 1362 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1363 | * request is handled. 1364 | */ 1365 | 1366 | /** 1367 | * @ngdoc method 1368 | * @name $httpBackend#expectPOST 1369 | * @description 1370 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1371 | * 1372 | * @param {string|RegExp} url HTTP url. 1373 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1374 | * receives data string and returns true if the data is as expected, or Object if request body 1375 | * is in JSON format. 1376 | * @param {Object=} headers HTTP headers. 1377 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1378 | * request is handled. 1379 | */ 1380 | 1381 | /** 1382 | * @ngdoc method 1383 | * @name $httpBackend#expectPUT 1384 | * @description 1385 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1386 | * 1387 | * @param {string|RegExp} url HTTP url. 1388 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1389 | * receives data string and returns true if the data is as expected, or Object if request body 1390 | * is in JSON format. 1391 | * @param {Object=} headers HTTP headers. 1392 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1393 | * request is handled. 1394 | */ 1395 | 1396 | /** 1397 | * @ngdoc method 1398 | * @name $httpBackend#expectPATCH 1399 | * @description 1400 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1401 | * 1402 | * @param {string|RegExp} url HTTP url. 1403 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1404 | * receives data string and returns true if the data is as expected, or Object if request body 1405 | * is in JSON format. 1406 | * @param {Object=} headers HTTP headers. 1407 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1408 | * request is handled. 1409 | */ 1410 | 1411 | /** 1412 | * @ngdoc method 1413 | * @name $httpBackend#expectJSONP 1414 | * @description 1415 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1416 | * 1417 | * @param {string|RegExp} url HTTP url. 1418 | * @returns {requestHandler} Returns an object with `respond` method that control how a matched 1419 | * request is handled. 1420 | */ 1421 | createShortMethods('expect'); 1422 | 1423 | 1424 | /** 1425 | * @ngdoc method 1426 | * @name $httpBackend#flush 1427 | * @description 1428 | * Flushes all pending requests using the trained responses. 1429 | * 1430 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1431 | * all pending requests will be flushed. If there are no pending requests when the flush method 1432 | * is called an exception is thrown (as this typically a sign of programming error). 1433 | */ 1434 | $httpBackend.flush = function(count) { 1435 | $rootScope.$digest(); 1436 | if (!responses.length) throw new Error('No pending request to flush !'); 1437 | 1438 | if (angular.isDefined(count)) { 1439 | while (count--) { 1440 | if (!responses.length) throw new Error('No more pending request to flush !'); 1441 | responses.shift()(); 1442 | } 1443 | } else { 1444 | while (responses.length) { 1445 | responses.shift()(); 1446 | } 1447 | } 1448 | $httpBackend.verifyNoOutstandingExpectation(); 1449 | }; 1450 | 1451 | 1452 | /** 1453 | * @ngdoc method 1454 | * @name $httpBackend#verifyNoOutstandingExpectation 1455 | * @description 1456 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1457 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1458 | * 1459 | * Typically, you would call this method following each test case that asserts requests using an 1460 | * "afterEach" clause. 1461 | * 1462 | * ```js 1463 | * afterEach($httpBackend.verifyNoOutstandingExpectation); 1464 | * ``` 1465 | */ 1466 | $httpBackend.verifyNoOutstandingExpectation = function() { 1467 | $rootScope.$digest(); 1468 | if (expectations.length) { 1469 | throw new Error('Unsatisfied requests: ' + expectations.join(', ')); 1470 | } 1471 | }; 1472 | 1473 | 1474 | /** 1475 | * @ngdoc method 1476 | * @name $httpBackend#verifyNoOutstandingRequest 1477 | * @description 1478 | * Verifies that there are no outstanding requests that need to be flushed. 1479 | * 1480 | * Typically, you would call this method following each test case that asserts requests using an 1481 | * "afterEach" clause. 1482 | * 1483 | * ```js 1484 | * afterEach($httpBackend.verifyNoOutstandingRequest); 1485 | * ``` 1486 | */ 1487 | $httpBackend.verifyNoOutstandingRequest = function() { 1488 | if (responses.length) { 1489 | throw new Error('Unflushed requests: ' + responses.length); 1490 | } 1491 | }; 1492 | 1493 | 1494 | /** 1495 | * @ngdoc method 1496 | * @name $httpBackend#resetExpectations 1497 | * @description 1498 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1499 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1500 | * $httpBackend mock. 1501 | */ 1502 | $httpBackend.resetExpectations = function() { 1503 | expectations.length = 0; 1504 | responses.length = 0; 1505 | }; 1506 | 1507 | return $httpBackend; 1508 | 1509 | 1510 | function createShortMethods(prefix) { 1511 | angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { 1512 | $httpBackend[prefix + method] = function(url, headers) { 1513 | return $httpBackend[prefix](method, url, undefined, headers); 1514 | }; 1515 | }); 1516 | 1517 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1518 | $httpBackend[prefix + method] = function(url, data, headers) { 1519 | return $httpBackend[prefix](method, url, data, headers); 1520 | }; 1521 | }); 1522 | } 1523 | } 1524 | 1525 | function MockHttpExpectation(method, url, data, headers) { 1526 | 1527 | this.data = data; 1528 | this.headers = headers; 1529 | 1530 | this.match = function(m, u, d, h) { 1531 | if (method != m) return false; 1532 | if (!this.matchUrl(u)) return false; 1533 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1534 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1535 | return true; 1536 | }; 1537 | 1538 | this.matchUrl = function(u) { 1539 | if (!url) return true; 1540 | if (angular.isFunction(url.test)) return url.test(u); 1541 | return url == u; 1542 | }; 1543 | 1544 | this.matchHeaders = function(h) { 1545 | if (angular.isUndefined(headers)) return true; 1546 | if (angular.isFunction(headers)) return headers(h); 1547 | return angular.equals(headers, h); 1548 | }; 1549 | 1550 | this.matchData = function(d) { 1551 | if (angular.isUndefined(data)) return true; 1552 | if (data && angular.isFunction(data.test)) return data.test(d); 1553 | if (data && angular.isFunction(data)) return data(d); 1554 | if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d)); 1555 | return data == d; 1556 | }; 1557 | 1558 | this.toString = function() { 1559 | return method + ' ' + url; 1560 | }; 1561 | } 1562 | 1563 | function createMockXhr() { 1564 | return new MockXhr(); 1565 | } 1566 | 1567 | function MockXhr() { 1568 | 1569 | // hack for testing $http, $httpBackend 1570 | MockXhr.$$lastInstance = this; 1571 | 1572 | this.open = function(method, url, async) { 1573 | this.$$method = method; 1574 | this.$$url = url; 1575 | this.$$async = async; 1576 | this.$$reqHeaders = {}; 1577 | this.$$respHeaders = {}; 1578 | }; 1579 | 1580 | this.send = function(data) { 1581 | this.$$data = data; 1582 | }; 1583 | 1584 | this.setRequestHeader = function(key, value) { 1585 | this.$$reqHeaders[key] = value; 1586 | }; 1587 | 1588 | this.getResponseHeader = function(name) { 1589 | // the lookup must be case insensitive, 1590 | // that's why we try two quick lookups first and full scan last 1591 | var header = this.$$respHeaders[name]; 1592 | if (header) return header; 1593 | 1594 | name = angular.lowercase(name); 1595 | header = this.$$respHeaders[name]; 1596 | if (header) return header; 1597 | 1598 | header = undefined; 1599 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1600 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1601 | }); 1602 | return header; 1603 | }; 1604 | 1605 | this.getAllResponseHeaders = function() { 1606 | var lines = []; 1607 | 1608 | angular.forEach(this.$$respHeaders, function(value, key) { 1609 | lines.push(key + ': ' + value); 1610 | }); 1611 | return lines.join('\n'); 1612 | }; 1613 | 1614 | this.abort = angular.noop; 1615 | } 1616 | 1617 | 1618 | /** 1619 | * @ngdoc service 1620 | * @name $timeout 1621 | * @description 1622 | * 1623 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1624 | * that adds a "flush" and "verifyNoPendingTasks" methods. 1625 | */ 1626 | 1627 | angular.mock.$TimeoutDecorator = function($delegate, $browser) { 1628 | 1629 | /** 1630 | * @ngdoc method 1631 | * @name $timeout#flush 1632 | * @description 1633 | * 1634 | * Flushes the queue of pending tasks. 1635 | * 1636 | * @param {number=} delay maximum timeout amount to flush up until 1637 | */ 1638 | $delegate.flush = function(delay) { 1639 | $browser.defer.flush(delay); 1640 | }; 1641 | 1642 | /** 1643 | * @ngdoc method 1644 | * @name $timeout#verifyNoPendingTasks 1645 | * @description 1646 | * 1647 | * Verifies that there are no pending tasks that need to be flushed. 1648 | */ 1649 | $delegate.verifyNoPendingTasks = function() { 1650 | if ($browser.deferredFns.length) { 1651 | throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + 1652 | formatPendingTasksAsString($browser.deferredFns)); 1653 | } 1654 | }; 1655 | 1656 | function formatPendingTasksAsString(tasks) { 1657 | var result = []; 1658 | angular.forEach(tasks, function(task) { 1659 | result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); 1660 | }); 1661 | 1662 | return result.join(', '); 1663 | } 1664 | 1665 | return $delegate; 1666 | }; 1667 | 1668 | angular.mock.$RAFDecorator = function($delegate) { 1669 | var queue = []; 1670 | var rafFn = function(fn) { 1671 | var index = queue.length; 1672 | queue.push(fn); 1673 | return function() { 1674 | queue.splice(index, 1); 1675 | }; 1676 | }; 1677 | 1678 | rafFn.supported = $delegate.supported; 1679 | 1680 | rafFn.flush = function() { 1681 | if(queue.length === 0) { 1682 | throw new Error('No rAF callbacks present'); 1683 | } 1684 | 1685 | var length = queue.length; 1686 | for(var i=0;i'); 1716 | }; 1717 | }; 1718 | 1719 | /** 1720 | * @ngdoc module 1721 | * @name ngMock 1722 | * @description 1723 | * 1724 | * # ngMock 1725 | * 1726 | * The `ngMock` module providers support to inject and mock Angular services into unit tests. 1727 | * In addition, ngMock also extends various core ng services such that they can be 1728 | * inspected and controlled in a synchronous manner within test code. 1729 | * 1730 | * 1731 | *
    1732 | * 1733 | */ 1734 | angular.module('ngMock', ['ng']).provider({ 1735 | $browser: angular.mock.$BrowserProvider, 1736 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1737 | $log: angular.mock.$LogProvider, 1738 | $interval: angular.mock.$IntervalProvider, 1739 | $httpBackend: angular.mock.$HttpBackendProvider, 1740 | $rootElement: angular.mock.$RootElementProvider 1741 | }).config(['$provide', function($provide) { 1742 | $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); 1743 | $provide.decorator('$$rAF', angular.mock.$RAFDecorator); 1744 | $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); 1745 | }]); 1746 | 1747 | /** 1748 | * @ngdoc module 1749 | * @name ngMockE2E 1750 | * @module ngMockE2E 1751 | * @description 1752 | * 1753 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1754 | * Currently there is only one mock present in this module - 1755 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1756 | */ 1757 | angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { 1758 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1759 | }]); 1760 | 1761 | /** 1762 | * @ngdoc service 1763 | * @name $httpBackend 1764 | * @module ngMockE2E 1765 | * @description 1766 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1767 | * applications that use the {@link ng.$http $http service}. 1768 | * 1769 | * *Note*: For fake http backend implementation suitable for unit testing please see 1770 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1771 | * 1772 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1773 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1774 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1775 | * templates from a webserver). 1776 | * 1777 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1778 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1779 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1780 | * templates or static files from the webserver). To configure the backend with this behavior 1781 | * use the `passThrough` request handler of `when` instead of `respond`. 1782 | * 1783 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1784 | * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests 1785 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1786 | * 1787 | * To setup the application to run with this http backend, you have to create a module that depends 1788 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1789 | * 1790 | * ```js 1791 | * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); 1792 | * myAppDev.run(function($httpBackend) { 1793 | * phones = [{name: 'phone1'}, {name: 'phone2'}]; 1794 | * 1795 | * // returns the current list of phones 1796 | * $httpBackend.whenGET('/phones').respond(phones); 1797 | * 1798 | * // adds a new phone to the phones array 1799 | * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { 1800 | * phones.push(angular.fromJson(data)); 1801 | * }); 1802 | * $httpBackend.whenGET(/^\/templates\//).passThrough(); 1803 | * //... 1804 | * }); 1805 | * ``` 1806 | * 1807 | * Afterwards, bootstrap your app with this new module. 1808 | */ 1809 | 1810 | /** 1811 | * @ngdoc method 1812 | * @name $httpBackend#when 1813 | * @module ngMockE2E 1814 | * @description 1815 | * Creates a new backend definition. 1816 | * 1817 | * @param {string} method HTTP method. 1818 | * @param {string|RegExp} url HTTP url. 1819 | * @param {(string|RegExp)=} data HTTP request body. 1820 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1821 | * object and returns true if the headers match the current definition. 1822 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1823 | * control how a matched request is handled. 1824 | * 1825 | * - respond – 1826 | * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` 1827 | * – The respond method takes a set of static data to be returned or a function that can return 1828 | * an array containing response status (number), response data (string) and response headers 1829 | * (Object). 1830 | * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` 1831 | * handler will be passed through to the real backend (an XHR request will be made to the 1832 | * server.) 1833 | */ 1834 | 1835 | /** 1836 | * @ngdoc method 1837 | * @name $httpBackend#whenGET 1838 | * @module ngMockE2E 1839 | * @description 1840 | * Creates a new backend definition for GET requests. For more info see `when()`. 1841 | * 1842 | * @param {string|RegExp} url HTTP url. 1843 | * @param {(Object|function(Object))=} headers HTTP headers. 1844 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1845 | * control how a matched request is handled. 1846 | */ 1847 | 1848 | /** 1849 | * @ngdoc method 1850 | * @name $httpBackend#whenHEAD 1851 | * @module ngMockE2E 1852 | * @description 1853 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1854 | * 1855 | * @param {string|RegExp} url HTTP url. 1856 | * @param {(Object|function(Object))=} headers HTTP headers. 1857 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1858 | * control how a matched request is handled. 1859 | */ 1860 | 1861 | /** 1862 | * @ngdoc method 1863 | * @name $httpBackend#whenDELETE 1864 | * @module ngMockE2E 1865 | * @description 1866 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1867 | * 1868 | * @param {string|RegExp} url HTTP url. 1869 | * @param {(Object|function(Object))=} headers HTTP headers. 1870 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1871 | * control how a matched request is handled. 1872 | */ 1873 | 1874 | /** 1875 | * @ngdoc method 1876 | * @name $httpBackend#whenPOST 1877 | * @module ngMockE2E 1878 | * @description 1879 | * Creates a new backend definition for POST requests. For more info see `when()`. 1880 | * 1881 | * @param {string|RegExp} url HTTP url. 1882 | * @param {(string|RegExp)=} data HTTP request body. 1883 | * @param {(Object|function(Object))=} headers HTTP headers. 1884 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1885 | * control how a matched request is handled. 1886 | */ 1887 | 1888 | /** 1889 | * @ngdoc method 1890 | * @name $httpBackend#whenPUT 1891 | * @module ngMockE2E 1892 | * @description 1893 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1894 | * 1895 | * @param {string|RegExp} url HTTP url. 1896 | * @param {(string|RegExp)=} data HTTP request body. 1897 | * @param {(Object|function(Object))=} headers HTTP headers. 1898 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1899 | * control how a matched request is handled. 1900 | */ 1901 | 1902 | /** 1903 | * @ngdoc method 1904 | * @name $httpBackend#whenPATCH 1905 | * @module ngMockE2E 1906 | * @description 1907 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1908 | * 1909 | * @param {string|RegExp} url HTTP url. 1910 | * @param {(string|RegExp)=} data HTTP request body. 1911 | * @param {(Object|function(Object))=} headers HTTP headers. 1912 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1913 | * control how a matched request is handled. 1914 | */ 1915 | 1916 | /** 1917 | * @ngdoc method 1918 | * @name $httpBackend#whenJSONP 1919 | * @module ngMockE2E 1920 | * @description 1921 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1922 | * 1923 | * @param {string|RegExp} url HTTP url. 1924 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1925 | * control how a matched request is handled. 1926 | */ 1927 | angular.mock.e2e = {}; 1928 | angular.mock.e2e.$httpBackendDecorator = 1929 | ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; 1930 | 1931 | 1932 | angular.mock.clearDataCache = function() { 1933 | var key, 1934 | cache = angular.element.cache; 1935 | 1936 | for(key in cache) { 1937 | if (Object.prototype.hasOwnProperty.call(cache,key)) { 1938 | var handle = cache[key].handle; 1939 | 1940 | handle && angular.element(handle.elem).off(); 1941 | delete cache[key]; 1942 | } 1943 | } 1944 | }; 1945 | 1946 | 1947 | if(window.jasmine || window.mocha) { 1948 | 1949 | var currentSpec = null, 1950 | isSpecRunning = function() { 1951 | return !!currentSpec; 1952 | }; 1953 | 1954 | 1955 | beforeEach(function() { 1956 | currentSpec = this; 1957 | }); 1958 | 1959 | afterEach(function() { 1960 | var injector = currentSpec.$injector; 1961 | 1962 | currentSpec.$injector = null; 1963 | currentSpec.$modules = null; 1964 | currentSpec = null; 1965 | 1966 | if (injector) { 1967 | injector.get('$rootElement').off(); 1968 | injector.get('$browser').pollFns.length = 0; 1969 | } 1970 | 1971 | angular.mock.clearDataCache(); 1972 | 1973 | // clean up jquery's fragment cache 1974 | angular.forEach(angular.element.fragments, function(val, key) { 1975 | delete angular.element.fragments[key]; 1976 | }); 1977 | 1978 | MockXhr.$$lastInstance = null; 1979 | 1980 | angular.forEach(angular.callbacks, function(val, key) { 1981 | delete angular.callbacks[key]; 1982 | }); 1983 | angular.callbacks.counter = 0; 1984 | }); 1985 | 1986 | /** 1987 | * @ngdoc function 1988 | * @name angular.mock.module 1989 | * @description 1990 | * 1991 | * *NOTE*: This function is also published on window for easy access.
    1992 | * 1993 | * This function registers a module configuration code. It collects the configuration information 1994 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 1995 | * 1996 | * See {@link angular.mock.inject inject} for usage example 1997 | * 1998 | * @param {...(string|Function|Object)} fns any number of modules which are represented as string 1999 | * aliases or as anonymous module initialization functions. The modules are used to 2000 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an 2001 | * object literal is passed they will be register as values in the module, the key being 2002 | * the module name and the value being what is returned. 2003 | */ 2004 | window.module = angular.mock.module = function() { 2005 | var moduleFns = Array.prototype.slice.call(arguments, 0); 2006 | return isSpecRunning() ? workFn() : workFn; 2007 | ///////////////////// 2008 | function workFn() { 2009 | if (currentSpec.$injector) { 2010 | throw new Error('Injector already created, can not register a module!'); 2011 | } else { 2012 | var modules = currentSpec.$modules || (currentSpec.$modules = []); 2013 | angular.forEach(moduleFns, function(module) { 2014 | if (angular.isObject(module) && !angular.isArray(module)) { 2015 | modules.push(function($provide) { 2016 | angular.forEach(module, function(value, key) { 2017 | $provide.value(key, value); 2018 | }); 2019 | }); 2020 | } else { 2021 | modules.push(module); 2022 | } 2023 | }); 2024 | } 2025 | } 2026 | }; 2027 | 2028 | /** 2029 | * @ngdoc function 2030 | * @name angular.mock.inject 2031 | * @description 2032 | * 2033 | * *NOTE*: This function is also published on window for easy access.
    2034 | * 2035 | * The inject function wraps a function into an injectable function. The inject() creates new 2036 | * instance of {@link auto.$injector $injector} per test, which is then used for 2037 | * resolving references. 2038 | * 2039 | * 2040 | * ## Resolving References (Underscore Wrapping) 2041 | * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this 2042 | * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable 2043 | * that is declared in the scope of the `describe()` block. Since we would, most likely, want 2044 | * the variable to have the same name of the reference we have a problem, since the parameter 2045 | * to the `inject()` function would hide the outer variable. 2046 | * 2047 | * To help with this, the injected parameters can, optionally, be enclosed with underscores. 2048 | * These are ignored by the injector when the reference name is resolved. 2049 | * 2050 | * For example, the parameter `_myService_` would be resolved as the reference `myService`. 2051 | * Since it is available in the function body as _myService_, we can then assign it to a variable 2052 | * defined in an outer scope. 2053 | * 2054 | * ``` 2055 | * // Defined out reference variable outside 2056 | * var myService; 2057 | * 2058 | * // Wrap the parameter in underscores 2059 | * beforeEach( inject( function(_myService_){ 2060 | * myService = _myService_; 2061 | * })); 2062 | * 2063 | * // Use myService in a series of tests. 2064 | * it('makes use of myService', function() { 2065 | * myService.doStuff(); 2066 | * }); 2067 | * 2068 | * ``` 2069 | * 2070 | * See also {@link angular.mock.module angular.mock.module} 2071 | * 2072 | * ## Example 2073 | * Example of what a typical jasmine tests looks like with the inject method. 2074 | * ```js 2075 | * 2076 | * angular.module('myApplicationModule', []) 2077 | * .value('mode', 'app') 2078 | * .value('version', 'v1.0.1'); 2079 | * 2080 | * 2081 | * describe('MyApp', function() { 2082 | * 2083 | * // You need to load modules that you want to test, 2084 | * // it loads only the "ng" module by default. 2085 | * beforeEach(module('myApplicationModule')); 2086 | * 2087 | * 2088 | * // inject() is used to inject arguments of all given functions 2089 | * it('should provide a version', inject(function(mode, version) { 2090 | * expect(version).toEqual('v1.0.1'); 2091 | * expect(mode).toEqual('app'); 2092 | * })); 2093 | * 2094 | * 2095 | * // The inject and module method can also be used inside of the it or beforeEach 2096 | * it('should override a version and test the new version is injected', function() { 2097 | * // module() takes functions or strings (module aliases) 2098 | * module(function($provide) { 2099 | * $provide.value('version', 'overridden'); // override version here 2100 | * }); 2101 | * 2102 | * inject(function(version) { 2103 | * expect(version).toEqual('overridden'); 2104 | * }); 2105 | * }); 2106 | * }); 2107 | * 2108 | * ``` 2109 | * 2110 | * @param {...Function} fns any number of functions which will be injected using the injector. 2111 | */ 2112 | 2113 | 2114 | 2115 | var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { 2116 | this.message = e.message; 2117 | this.name = e.name; 2118 | if (e.line) this.line = e.line; 2119 | if (e.sourceId) this.sourceId = e.sourceId; 2120 | if (e.stack && errorForStack) 2121 | this.stack = e.stack + '\n' + errorForStack.stack; 2122 | if (e.stackArray) this.stackArray = e.stackArray; 2123 | }; 2124 | ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; 2125 | 2126 | window.inject = angular.mock.inject = function() { 2127 | var blockFns = Array.prototype.slice.call(arguments, 0); 2128 | var errorForStack = new Error('Declaration Location'); 2129 | return isSpecRunning() ? workFn.call(currentSpec) : workFn; 2130 | ///////////////////// 2131 | function workFn() { 2132 | var modules = currentSpec.$modules || []; 2133 | 2134 | modules.unshift('ngMock'); 2135 | modules.unshift('ng'); 2136 | var injector = currentSpec.$injector; 2137 | if (!injector) { 2138 | injector = currentSpec.$injector = angular.injector(modules); 2139 | } 2140 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 2141 | try { 2142 | /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ 2143 | injector.invoke(blockFns[i] || angular.noop, this); 2144 | /* jshint +W040 */ 2145 | } catch (e) { 2146 | if (e.stack && errorForStack) { 2147 | throw new ErrorAddingDeclarationLocationStack(e, errorForStack); 2148 | } 2149 | throw e; 2150 | } finally { 2151 | errorForStack = null; 2152 | } 2153 | } 2154 | } 2155 | }; 2156 | } 2157 | 2158 | 2159 | })(window, window.angular); 2160 | -------------------------------------------------------------------------------- /test/vendor/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.3.15 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 9 | * Any commits to this file should be reviewed with security in mind. * 10 | * Changes to this file can potentially create security vulnerabilities. * 11 | * An approval from 2 Core members with history of modifying * 12 | * this file is required. * 13 | * * 14 | * Does the change somehow allow for arbitrary javascript to be executed? * 15 | * Or allows for someone to change the prototype of built-in objects? * 16 | * Or gives undesired access to variables likes document or window? * 17 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 18 | 19 | var $sanitizeMinErr = angular.$$minErr('$sanitize'); 20 | 21 | /** 22 | * @ngdoc module 23 | * @name ngSanitize 24 | * @description 25 | * 26 | * # ngSanitize 27 | * 28 | * The `ngSanitize` module provides functionality to sanitize HTML. 29 | * 30 | * 31 | *
    32 | * 33 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 34 | */ 35 | 36 | /* 37 | * HTML Parser By Misko Hevery (misko@hevery.com) 38 | * based on: HTML Parser By John Resig (ejohn.org) 39 | * Original code by Erik Arvidsson, Mozilla Public License 40 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 41 | * 42 | * // Use like so: 43 | * htmlParser(htmlString, { 44 | * start: function(tag, attrs, unary) {}, 45 | * end: function(tag) {}, 46 | * chars: function(text) {}, 47 | * comment: function(text) {} 48 | * }); 49 | * 50 | */ 51 | 52 | 53 | /** 54 | * @ngdoc service 55 | * @name $sanitize 56 | * @kind function 57 | * 58 | * @description 59 | * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are 60 | * then serialized back to properly escaped html string. This means that no unsafe input can make 61 | * it into the returned string, however, since our parser is more strict than a typical browser 62 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 63 | * browser, won't make it through the sanitizer. The input may also contain SVG markup. 64 | * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and 65 | * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. 66 | * 67 | * @param {string} html HTML input. 68 | * @returns {string} Sanitized HTML. 69 | * 70 | * @example 71 | 72 | 73 | 85 |
    86 | Snippet: 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
    DirectiveHowSourceRendered
    ng-bind-htmlAutomatically uses $sanitize
    <div ng-bind-html="snippet">
    </div>
    ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value 104 |
    <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
    105 | </div>
    106 |
    ng-bindAutomatically escapes
    <div ng-bind="snippet">
    </div>
    116 |
    117 |
    118 | 119 | it('should sanitize the html snippet by default', function() { 120 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 121 | toBe('

    an html\nclick here\nsnippet

    '); 122 | }); 123 | 124 | it('should inline raw snippet if bound to a trusted value', function() { 125 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). 126 | toBe("

    an html\n" + 127 | "click here\n" + 128 | "snippet

    "); 129 | }); 130 | 131 | it('should escape snippet without any filter', function() { 132 | expect(element(by.css('#bind-default div')).getInnerHtml()). 133 | toBe("<p style=\"color:blue\">an html\n" + 134 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 135 | "snippet</p>"); 136 | }); 137 | 138 | it('should update', function() { 139 | element(by.model('snippet')).clear(); 140 | element(by.model('snippet')).sendKeys('new text'); 141 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 142 | toBe('new text'); 143 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( 144 | 'new text'); 145 | expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( 146 | "new <b onclick=\"alert(1)\">text</b>"); 147 | }); 148 |
    149 |
    150 | */ 151 | function $SanitizeProvider() { 152 | this.$get = ['$$sanitizeUri', function($$sanitizeUri) { 153 | return function(html) { 154 | var buf = []; 155 | htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { 156 | return !/^unsafe/.test($$sanitizeUri(uri, isImage)); 157 | })); 158 | return buf.join(''); 159 | }; 160 | }]; 161 | } 162 | 163 | function sanitizeText(chars) { 164 | var buf = []; 165 | var writer = htmlSanitizeWriter(buf, angular.noop); 166 | writer.chars(chars); 167 | return buf.join(''); 168 | } 169 | 170 | 171 | // Regular Expressions for parsing tags and attributes 172 | var START_TAG_REGEXP = 173 | /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, 174 | END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, 175 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 176 | BEGIN_TAG_REGEXP = /^/g, 179 | DOCTYPE_REGEXP = /]*?)>/i, 180 | CDATA_REGEXP = //g, 181 | SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 182 | // Match everything outside of normal chars and " (quote character) 183 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; 184 | 185 | 186 | // Good source of info about elements and attributes 187 | // http://dev.w3.org/html5/spec/Overview.html#semantics 188 | // http://simon.html5.org/html-elements 189 | 190 | // Safe Void Elements - HTML5 191 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 192 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 193 | 194 | // Elements that you can, intentionally, leave open (and which close themselves) 195 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 196 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 197 | optionalEndTagInlineElements = makeMap("rp,rt"), 198 | optionalEndTagElements = angular.extend({}, 199 | optionalEndTagInlineElements, 200 | optionalEndTagBlockElements); 201 | 202 | // Safe Block Elements - HTML5 203 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + 204 | "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + 205 | "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 206 | 207 | // Inline Elements - HTML5 208 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + 209 | "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + 210 | "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); 211 | 212 | // SVG Elements 213 | // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements 214 | var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + 215 | "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + 216 | "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + 217 | "stop,svg,switch,text,title,tspan,use"); 218 | 219 | // Special Elements (can contain anything) 220 | var specialElements = makeMap("script,style"); 221 | 222 | var validElements = angular.extend({}, 223 | voidElements, 224 | blockElements, 225 | inlineElements, 226 | optionalEndTagElements, 227 | svgElements); 228 | 229 | //Attributes that have href and hence need to be sanitized 230 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); 231 | 232 | var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + 233 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + 234 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + 235 | 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + 236 | 'valign,value,vspace,width'); 237 | 238 | // SVG attributes (without "id" and "name" attributes) 239 | // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes 240 | var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + 241 | 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + 242 | 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + 243 | 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + 244 | 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + 245 | 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + 246 | 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + 247 | 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + 248 | 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + 249 | 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + 250 | 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + 251 | 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + 252 | 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + 253 | 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + 254 | 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + 255 | 'zoomAndPan'); 256 | 257 | var validAttrs = angular.extend({}, 258 | uriAttrs, 259 | svgAttrs, 260 | htmlAttrs); 261 | 262 | function makeMap(str) { 263 | var obj = {}, items = str.split(','), i; 264 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 265 | return obj; 266 | } 267 | 268 | 269 | /** 270 | * @example 271 | * htmlParser(htmlString, { 272 | * start: function(tag, attrs, unary) {}, 273 | * end: function(tag) {}, 274 | * chars: function(text) {}, 275 | * comment: function(text) {} 276 | * }); 277 | * 278 | * @param {string} html string 279 | * @param {object} handler 280 | */ 281 | function htmlParser(html, handler) { 282 | if (typeof html !== 'string') { 283 | if (html === null || typeof html === 'undefined') { 284 | html = ''; 285 | } else { 286 | html = '' + html; 287 | } 288 | } 289 | var index, chars, match, stack = [], last = html, text; 290 | stack.last = function() { return stack[stack.length - 1]; }; 291 | 292 | while (html) { 293 | text = ''; 294 | chars = true; 295 | 296 | // Make sure we're not in a script or style element 297 | if (!stack.last() || !specialElements[stack.last()]) { 298 | 299 | // Comment 300 | if (html.indexOf("", index) === index) { 305 | if (handler.comment) handler.comment(html.substring(4, index)); 306 | html = html.substring(index + 3); 307 | chars = false; 308 | } 309 | // DOCTYPE 310 | } else if (DOCTYPE_REGEXP.test(html)) { 311 | match = html.match(DOCTYPE_REGEXP); 312 | 313 | if (match) { 314 | html = html.replace(match[0], ''); 315 | chars = false; 316 | } 317 | // end tag 318 | } else if (BEGING_END_TAGE_REGEXP.test(html)) { 319 | match = html.match(END_TAG_REGEXP); 320 | 321 | if (match) { 322 | html = html.substring(match[0].length); 323 | match[0].replace(END_TAG_REGEXP, parseEndTag); 324 | chars = false; 325 | } 326 | 327 | // start tag 328 | } else if (BEGIN_TAG_REGEXP.test(html)) { 329 | match = html.match(START_TAG_REGEXP); 330 | 331 | if (match) { 332 | // We only have a valid start-tag if there is a '>'. 333 | if (match[4]) { 334 | html = html.substring(match[0].length); 335 | match[0].replace(START_TAG_REGEXP, parseStartTag); 336 | } 337 | chars = false; 338 | } else { 339 | // no ending tag found --- this piece should be encoded as an entity. 340 | text += '<'; 341 | html = html.substring(1); 342 | } 343 | } 344 | 345 | if (chars) { 346 | index = html.indexOf("<"); 347 | 348 | text += index < 0 ? html : html.substring(0, index); 349 | html = index < 0 ? "" : html.substring(index); 350 | 351 | if (handler.chars) handler.chars(decodeEntities(text)); 352 | } 353 | 354 | } else { 355 | // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. 356 | html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), 357 | function(all, text) { 358 | text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); 359 | 360 | if (handler.chars) handler.chars(decodeEntities(text)); 361 | 362 | return ""; 363 | }); 364 | 365 | parseEndTag("", stack.last()); 366 | } 367 | 368 | if (html == last) { 369 | throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + 370 | "of html: {0}", html); 371 | } 372 | last = html; 373 | } 374 | 375 | // Clean up any remaining tags 376 | parseEndTag(); 377 | 378 | function parseStartTag(tag, tagName, rest, unary) { 379 | tagName = angular.lowercase(tagName); 380 | if (blockElements[tagName]) { 381 | while (stack.last() && inlineElements[stack.last()]) { 382 | parseEndTag("", stack.last()); 383 | } 384 | } 385 | 386 | if (optionalEndTagElements[tagName] && stack.last() == tagName) { 387 | parseEndTag("", tagName); 388 | } 389 | 390 | unary = voidElements[tagName] || !!unary; 391 | 392 | if (!unary) 393 | stack.push(tagName); 394 | 395 | var attrs = {}; 396 | 397 | rest.replace(ATTR_REGEXP, 398 | function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { 399 | var value = doubleQuotedValue 400 | || singleQuotedValue 401 | || unquotedValue 402 | || ''; 403 | 404 | attrs[name] = decodeEntities(value); 405 | }); 406 | if (handler.start) handler.start(tagName, attrs, unary); 407 | } 408 | 409 | function parseEndTag(tag, tagName) { 410 | var pos = 0, i; 411 | tagName = angular.lowercase(tagName); 412 | if (tagName) 413 | // Find the closest opened tag of the same type 414 | for (pos = stack.length - 1; pos >= 0; pos--) 415 | if (stack[pos] == tagName) 416 | break; 417 | 418 | if (pos >= 0) { 419 | // Close all the open elements, up the stack 420 | for (i = stack.length - 1; i >= pos; i--) 421 | if (handler.end) handler.end(stack[i]); 422 | 423 | // Remove the open elements from the stack 424 | stack.length = pos; 425 | } 426 | } 427 | } 428 | 429 | var hiddenPre=document.createElement("pre"); 430 | /** 431 | * decodes all entities into regular string 432 | * @param value 433 | * @returns {string} A string with decoded entities. 434 | */ 435 | function decodeEntities(value) { 436 | if (!value) { return ''; } 437 | 438 | hiddenPre.innerHTML = value.replace(//g, '>'); 464 | } 465 | 466 | /** 467 | * create an HTML/XML writer which writes to buffer 468 | * @param {Array} buf use buf.jain('') to get out sanitized html string 469 | * @returns {object} in the form of { 470 | * start: function(tag, attrs, unary) {}, 471 | * end: function(tag) {}, 472 | * chars: function(text) {}, 473 | * comment: function(text) {} 474 | * } 475 | */ 476 | function htmlSanitizeWriter(buf, uriValidator) { 477 | var ignore = false; 478 | var out = angular.bind(buf, buf.push); 479 | return { 480 | start: function(tag, attrs, unary) { 481 | tag = angular.lowercase(tag); 482 | if (!ignore && specialElements[tag]) { 483 | ignore = tag; 484 | } 485 | if (!ignore && validElements[tag] === true) { 486 | out('<'); 487 | out(tag); 488 | angular.forEach(attrs, function(value, key) { 489 | var lkey=angular.lowercase(key); 490 | var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); 491 | if (validAttrs[lkey] === true && 492 | (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { 493 | out(' '); 494 | out(key); 495 | out('="'); 496 | out(encodeEntities(value)); 497 | out('"'); 498 | } 499 | }); 500 | out(unary ? '/>' : '>'); 501 | } 502 | }, 503 | end: function(tag) { 504 | tag = angular.lowercase(tag); 505 | if (!ignore && validElements[tag] === true) { 506 | out(''); 509 | } 510 | if (tag == ignore) { 511 | ignore = false; 512 | } 513 | }, 514 | chars: function(chars) { 515 | if (!ignore) { 516 | out(encodeEntities(chars)); 517 | } 518 | } 519 | }; 520 | } 521 | 522 | 523 | // define ngSanitize module and register $sanitize service 524 | angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); 525 | 526 | /* global sanitizeText: false */ 527 | 528 | /** 529 | * @ngdoc filter 530 | * @name linky 531 | * @kind function 532 | * 533 | * @description 534 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 535 | * plain email address links. 536 | * 537 | * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 538 | * 539 | * @param {string} text Input text. 540 | * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. 541 | * @returns {string} Html-linkified text. 542 | * 543 | * @usage 544 | 545 | * 546 | * @example 547 | 548 | 549 | 561 |
    562 | Snippet: 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 574 | 577 | 578 | 579 | 580 | 583 | 586 | 587 | 588 | 589 | 590 | 591 | 592 |
    FilterSourceRendered
    linky filter 572 |
    <div ng-bind-html="snippet | linky">
    </div>
    573 |
    575 |
    576 |
    linky target 581 |
    <div ng-bind-html="snippetWithTarget | linky:'_blank'">
    </div>
    582 |
    584 |
    585 |
    no filter
    <div ng-bind="snippet">
    </div>
    593 | 594 | 595 | it('should linkify the snippet with urls', function() { 596 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 597 | toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + 598 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 599 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); 600 | }); 601 | 602 | it('should not linkify snippet without the linky filter', function() { 603 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). 604 | toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + 605 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 606 | expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); 607 | }); 608 | 609 | it('should update', function() { 610 | element(by.model('snippet')).clear(); 611 | element(by.model('snippet')).sendKeys('new http://link.'); 612 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 613 | toBe('new http://link.'); 614 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); 615 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) 616 | .toBe('new http://link.'); 617 | }); 618 | 619 | it('should work with the target property', function() { 620 | expect(element(by.id('linky-target')). 621 | element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). 622 | toBe('http://angularjs.org/'); 623 | expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); 624 | }); 625 | 626 | 627 | */ 628 | angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { 629 | var LINKY_URL_REGEXP = 630 | /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, 631 | MAILTO_REGEXP = /^mailto:/; 632 | 633 | return function(text, target) { 634 | if (!text) return text; 635 | var match; 636 | var raw = text; 637 | var html = []; 638 | var url; 639 | var i; 640 | while ((match = raw.match(LINKY_URL_REGEXP))) { 641 | // We can not end in these as they are sometimes found at the end of the sentence 642 | url = match[0]; 643 | // if we did not match ftp/http/www/mailto then assume mailto 644 | if (!match[2] && !match[4]) { 645 | url = (match[3] ? 'http://' : 'mailto:') + url; 646 | } 647 | i = match.index; 648 | addText(raw.substr(0, i)); 649 | addLink(url, match[0].replace(MAILTO_REGEXP, '')); 650 | raw = raw.substring(i + match[0].length); 651 | } 652 | addText(raw); 653 | return $sanitize(html.join('')); 654 | 655 | function addText(text) { 656 | if (!text) { 657 | return; 658 | } 659 | html.push(sanitizeText(text)); 660 | } 661 | 662 | function addLink(url, text) { 663 | html.push(''); 672 | addText(text); 673 | html.push(''); 674 | } 675 | }; 676 | }]); 677 | 678 | 679 | })(window, window.angular); 680 | --------------------------------------------------------------------------------