├── .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 [](https://codeclimate.com/github/tameraydin/ngToast/dist/ngToast.js) [](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 | '
' +
143 | '' +
145 | '' +
146 | '' +
147 | '
' +
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 |
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 | * | Request expectations | Backend definitions |
925 | *
926 | * Syntax |
927 | * .expect(...).respond(...) |
928 | * .when(...).respond(...) |
929 | *
930 | *
931 | * Typical usage |
932 | * strict unit tests |
933 | * loose (black-box) unit testing |
934 | *
935 | *
936 | * Fulfills multiple requests |
937 | * NO |
938 | * YES |
939 | *
940 | *
941 | * Order of requests matters |
942 | * YES |
943 | * NO |
944 | *
945 | *
946 | * Request required |
947 | * YES |
948 | * NO |
949 | *
950 | *
951 | * Response required |
952 | * optional (see below) |
953 | * YES |
954 | *
955 | *
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 | Directive |
90 | How |
91 | Source |
92 | Rendered |
93 |
94 |
95 | ng-bind-html |
96 | Automatically uses $sanitize |
97 | <div ng-bind-html="snippet"> </div> |
98 | |
99 |
100 |
101 | ng-bind-html |
102 | Bypass $sanitize by explicitly trusting the dangerous value |
103 |
104 | <div ng-bind-html="deliberatelyTrustDangerousSnippet()">
105 | </div>
106 | |
107 | |
108 |
109 |
110 | ng-bind |
111 | Automatically escapes |
112 | <div ng-bind="snippet"> </div> |
113 | |
114 |
115 |
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 = /^,
177 | BEGING_END_TAGE_REGEXP = /^<\//,
178 | COMMENT_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('');
507 | out(tag);
508 | 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 | Filter |
566 | Source |
567 | Rendered |
568 |
569 |
570 | linky filter |
571 |
572 | <div ng-bind-html="snippet | linky"> </div>
573 | |
574 |
575 |
576 | |
577 |
578 |
579 | linky target |
580 |
581 | <div ng-bind-html="snippetWithTarget | linky:'_blank'"> </div>
582 | |
583 |
584 |
585 | |
586 |
587 |
588 | no filter |
589 | <div ng-bind="snippet"> </div> |
590 | |
591 |
592 |
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 |
--------------------------------------------------------------------------------