├── .gitignore
├── .jshintrc
├── .travis.yml
├── Gruntfile.js
├── LICENSE
├── README.md
├── demo
├── app.js
├── index.html
└── style.less
├── dist
├── ng-image-input-with-preview.css
├── ng-image-input-with-preview.js
├── ng-image-input-with-preview.min.css
└── ng-image-input-with-preview.min.js
├── package-lock.json
├── package.json
├── src
├── js
│ ├── fileReader.service.js
│ └── imageWithPreview.directive.js
└── less
│ └── style.less
├── tasks
├── files.js
├── helpers.js
├── options
│ ├── bump.js
│ ├── concat.js
│ ├── connect.js
│ ├── coveralls.js
│ ├── jshint.js
│ ├── karma.js
│ ├── less.js
│ ├── ngAnnotate.js
│ ├── npm-contributors.js
│ ├── parallel.js
│ ├── shell.js
│ ├── uglify.js
│ └── watch.js
└── templates
│ ├── banner-min.tpl
│ ├── banner.tpl
│ ├── wrap-bottom.tpl
│ └── wrap-top.tpl
└── test
└── unit
├── SpecHelper.js
├── imageWithPreview.directiveSpec.js
└── setupSpec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | node_modules/
3 | npm-debug.log
4 | test/e2e/env/all-partials.js
5 | grunt
6 | .tmp
7 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "maxerr" : false,
3 | "bitwise" : true,
4 | "camelcase" : true,
5 | "curly" : true,
6 | "eqeqeq" : true,
7 | "forin" : true,
8 | "immed" : true,
9 | "indent" : 2,
10 | "latedef" : true,
11 | "newcap" : true,
12 | "noarg" : true,
13 | "noempty" : true,
14 | "nonew" : true,
15 | "plusplus" : false,
16 | "quotmark" : "single",
17 | "undef" : true,
18 | "unused" : true,
19 | "strict" : false,
20 | "trailing" : true,
21 | "maxparams" : 3,
22 | "maxdepth" : 3,
23 | "maxstatements" : false,
24 | "maxcomplexity" : false,
25 | "maxlen" : 120,
26 | "asi" : false,
27 | "boss" : false,
28 | "debug" : false,
29 | "eqnull" : false,
30 | "es5" : false,
31 | "esnext" : false,
32 | "evil" : false,
33 | "expr" : false,
34 | "funcscope" : false,
35 | "globalstrict" : false,
36 | "iterator" : false,
37 | "lastsemic" : false,
38 | "laxbreak" : false,
39 | "laxcomma" : false,
40 | "loopfunc" : false,
41 | "multistr" : false,
42 | "proto" : false,
43 | "scripturl" : false,
44 | "smarttabs" : false,
45 | "shadow" : false,
46 | "sub" : false,
47 | "supernew" : false,
48 | "validthis" : false,
49 | "browser" : true,
50 | "couch" : false,
51 | "devel" : false,
52 | "dojo" : false,
53 | "jquery" : false,
54 | "mootools" : false,
55 | "node" : false,
56 | "nonstandard" : false,
57 | "prototypejs" : false,
58 | "rhino" : false,
59 | "worker" : false,
60 | "wsh" : false,
61 | "yui" : false,
62 | "nomen" : false,
63 | "onevar" : false,
64 | "passfail" : false,
65 | "white" : false,
66 | "predef" : [
67 | "angular",
68 | "jasmine",
69 | "beforeEach",
70 | "afterEach",
71 | "describe",
72 | "xdescribe",
73 | "fdescribe",
74 | "it",
75 | "xit",
76 | "fit",
77 | "expect",
78 | "spyOn",
79 | "inject",
80 | "module",
81 | "exports",
82 | "require",
83 | "process",
84 | "element",
85 | "by"
86 | ]
87 | }
88 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 |
5 | env:
6 | global:
7 | - KARMA_BROWSERS=Firefox,PhantomJS
8 |
9 | install:
10 | - npm install
11 |
12 | script:
13 | - grunt test --reporter=spec
14 |
15 | before_script:
16 | - export DISPLAY=:99.0
17 | - sh -e /etc/init.d/xvfb start
18 |
19 | after_success:
20 | - grunt coveralls
21 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build instructions for grunt.
3 | *
4 | * @param {Object} grunt
5 | * @return {void}
6 | */
7 | module.exports = function(grunt) {
8 | 'use strict';
9 |
10 | var Helpers = require('./tasks/helpers');
11 | var config = Helpers.config;
12 | var _ = grunt.util._;
13 |
14 | /* Task configuration is in ./tasks/options - load here */
15 | config = _.extend(config, Helpers.loadConfig('./tasks/options/'));
16 |
17 | /* Load grunt tasks from NPM packages */
18 | require('load-grunt-tasks')(grunt);
19 | grunt.loadTasks('tasks');
20 |
21 |
22 | grunt.registerTask(
23 | 'tdd',
24 | 'Watch source and test files and execute tests on change',
25 | function() {
26 | var tasks = [
27 | 'karma:watch:start',
28 | 'watch:andtestunit',
29 | ];
30 | grunt.task.run(tasks);
31 | }
32 | );
33 |
34 | grunt.registerTask('demo', 'Start the demo app', [
35 | 'connect:demo',
36 | 'parallel:watchdemo'
37 | ]);
38 |
39 | grunt.registerTask('coverage', 'Serve coverage report', ['connect:coverage']);
40 |
41 | grunt.registerTask(
42 | 'test',
43 | 'Execute all the tests',
44 | function() {
45 | var tasks = [
46 | 'jshint',
47 | 'shell:deleteCoverages',
48 | 'karma:all',
49 | ];
50 | process.env.defaultBrowsers = 'Firefox,Chrome';
51 | grunt.task.run(tasks);
52 | }
53 | );
54 |
55 | grunt.registerTask(
56 | 'build',
57 | 'Build dist files',
58 | [
59 | 'less:dist',
60 | 'less:distmin',
61 | 'concat:bannerToDistStyle',
62 | 'concat:bannerToDistStyleMin',
63 | 'concat:dist',
64 | 'ngAnnotate:dist',
65 | 'uglify'
66 | ]
67 | );
68 |
69 | grunt.registerTask('release', 'Test, bump, build and release.', function(type) {
70 | grunt.task.run([
71 | 'test',
72 | 'npm-contributors',
73 | 'bump-only:' + (type || 'patch'),
74 | 'build',
75 | 'bump-commit'
76 | ]);
77 | });
78 |
79 | grunt.registerTask('default', 'Test', ['test']);
80 |
81 | grunt.initConfig(config);
82 | };
83 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2015 Deiwin Sarjas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ngImageInputWithPreview
2 | ======================
3 |
4 | [](https://travis-ci.org/deiwin/ngImageInputWithPreview)
5 | [](https://coveralls.io/r/deiwin/ngImageInputWithPreview?branch=master)
6 | [](https://david-dm.org/deiwin/ngImageInputWithPreview#info=devDependencies)
7 |
8 | A FileReader based Angular directive to easily preview and upload image files.
9 |
10 | A live demo: http://deiwin.github.io/ngImageInputWithPreview/
11 |
12 | Installation
13 | ----------
14 |
15 | npm install --save ng-image-input-with-preview
16 |
17 | or
18 |
19 | bower install --save ng-image-input-with-preview
20 |
21 | ### Issues with installation:
22 |
23 | If bower fails to load the module, try adding the following to your `bower.json`:
24 | ```javascript
25 | "overrides": {
26 | "ng-image-input-with-preview": {
27 | "main": "./dist/ng-image-input-with-preview.js"
28 | }
29 | }
30 | ```
31 |
32 | Getting started
33 | -------------
34 |
35 | Include the dependency in your module definition:
36 |
37 | ```javascript
38 | angular.module('app', [
39 | // ... other dependencies
40 | 'ngImageInputWithPreview'
41 | ])
42 | ```
43 |
44 | Then you can use the directive:
45 |
46 | ```html
47 |
61 |
62 |
63 | ```
64 |
65 | Options
66 | -------
67 |
68 | ```html
69 |
72 | ```
73 |
74 | ### Parameters
75 |
76 | | Name | Type | Description | Defaults to
77 | | -----|------|-------------|------------
78 | | ngModel (*required*) | `string` | Assignable angular expression to data-bind to. The data URL encoded image data will be available on the `src` property. If the angular expression is a string, it will be assumed to be an URL path to an image. The path will then be converted to an object with the path available on the `src` property and with the `isPath` property set to `true`. | - |
79 | | accept | `string` | Works similarly to the [HTML specification](https://html.spec.whatwg.org/multipage/forms.html#attr-input-accept) to restrict the input to certain mime types. Sets `image` validation error key if the user selected file does not match. *NB: File extensions are currently not supported.* | image/jpeg,image/png |
80 | | dimensions | `expression` | If this expression is evaluated to be false (or falsy) the `$error.dimensions` property will be set and the image will be considered invalid. The image's dimensions are available as `height` and `width`. | - |
81 | | size | `experssion` | Specifically in bytes. If this expressions is evaluated to be false (or falsy) the `$error.size` property will be set and the image will be considered invalud. The image's size is available as `size`. | - |
82 |
83 | Demo !
84 | ------
85 |
86 | Clone this repo, run `npm install` and then start the demo server with
87 | `grunt demo` and go to [http://localhost:8000/demo/](http://localhost:8000/demo/).
88 |
--------------------------------------------------------------------------------
/demo/app.js:
--------------------------------------------------------------------------------
1 | angular.bootstrap(document, ['ngImageInputWithPreview']);
2 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ngImageInputWithPreview
7 |
8 |
9 |
10 |
11 |
12 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/demo/style.less:
--------------------------------------------------------------------------------
1 | body {
2 | background: #f2f2f2;
3 | }
4 |
5 | form {
6 | padding-top: 50px;
7 | margin-left: auto;
8 | margin-right: auto;
9 | width: 400px;
10 | height: 600px;
11 | }
12 |
13 | img {
14 | max-width: 100%;
15 | max-height: 100%;
16 | }
17 |
18 | .error {
19 | color: red;
20 | }
21 |
--------------------------------------------------------------------------------
/dist/ng-image-input-with-preview.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * ng-image-input-with-preview v1.0.0
3 | * https://github.com/deiwin/ngImageInputWithPreview
4 | *
5 | * A FileReader based directive to easily preview and upload image files.
6 | *
7 | * Copyright 2018, Deiwin Sarjas
8 | * Released under the MIT license
9 | */
10 |
--------------------------------------------------------------------------------
/dist/ng-image-input-with-preview.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * ng-image-input-with-preview v1.0.0
3 | * https://github.com/deiwin/ngImageInputWithPreview
4 | *
5 | * A FileReader based directive to easily preview and upload image files.
6 | *
7 | * Copyright 2018, Deiwin Sarjas
8 | * Released under the MIT license
9 | */
10 | (function(angular, undefined) {
11 | 'use strict';
12 |
13 | // src/js/fileReader.service.js
14 | (function() {
15 | 'use strict';
16 | var module = angular.module('fileReaderService', []);
17 |
18 | // Copied from the following link with onProgress excluded because it's not needed
19 | // http://odetocode.com/blogs/scott/archive/2013/07/03/building-a-filereader-service-for-angularjs-the-service.aspx
20 | module.factory('fileReader', ['$q',
21 | function($q) {
22 | var onLoad = function(reader, deferred, scope) {
23 | return function() {
24 | scope.$apply(function() {
25 | deferred.resolve(reader.result);
26 | });
27 | };
28 | };
29 |
30 | var onError = function(reader, deferred, scope) {
31 | return function() {
32 | scope.$apply(function() {
33 | deferred.reject(reader.result);
34 | });
35 | };
36 | };
37 |
38 | var getReader = function(deferred, scope) {
39 | var reader = new FileReader();
40 | reader.onload = onLoad(reader, deferred, scope);
41 | reader.onerror = onError(reader, deferred, scope);
42 | return reader;
43 | };
44 |
45 | var readAsDataURL = function(file, scope) {
46 | var deferred = $q.defer();
47 |
48 | var reader = getReader(deferred, scope);
49 | reader.readAsDataURL(file);
50 |
51 | return deferred.promise;
52 | };
53 |
54 | return {
55 | readAsDataUrl: readAsDataURL
56 | };
57 | }
58 | ]);
59 | })();
60 |
61 | // src/js/imageWithPreview.directive.js
62 | /*jshint -W072 */
63 | // ^ ignore jshint warning about link method having too many parameters
64 | (function() {
65 | 'use strict';
66 | var module = angular.module('ngImageInputWithPreview', [
67 | 'fileReaderService',
68 | ]);
69 |
70 | module.directive('imageWithPreview', ['fileReader', '$q',
71 | function(fileReader, $q) {
72 | var DEFAULT_MIMETYPES = 'image/png,image/jpeg';
73 | var NOT_AN_IMAGE = 'this-is-not-an-image';
74 |
75 | var isAnAllowedImage = function(allowedTypes, file) {
76 | if (!allowedTypes) {
77 | allowedTypes = DEFAULT_MIMETYPES;
78 | }
79 | var allowedTypeArray = allowedTypes.split(',');
80 | return allowedTypeArray.some(function(allowedType) {
81 | if (allowedType === file.type) {
82 | return true;
83 | }
84 | var allowedTypeSplit = allowedType.split('/');
85 | var fileTypeSplit = file.type.split('/');
86 | return allowedTypeSplit.length === 2 && fileTypeSplit.length === 2 && allowedTypeSplit[1] === '*' &&
87 | allowedTypeSplit[0] === fileTypeSplit[0];
88 | });
89 | };
90 | var createResolvedPromise = function() {
91 | var d = $q.defer();
92 | d.resolve();
93 | return d.promise;
94 | };
95 |
96 | return {
97 | restrict: 'A',
98 | require: 'ngModel',
99 | scope: {
100 | image: '=ngModel',
101 | allowedTypes: '@accept',
102 | dimensionRestrictions: '&dimensions',
103 | sizeRestrictions: '&size'
104 | },
105 | link: function($scope, element, attrs, ngModel) {
106 | element.bind('change', function(event) {
107 | var file = (event.srcElement || event.target).files[0];
108 | // the following link recommends making a copy of the object, but as the value will only be changed
109 | // from the view, we don't have to worry about making a copy
110 | // https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$setViewValue
111 | ngModel.$setViewValue(file, 'change');
112 | });
113 | ngModel.$parsers.push(function(file) {
114 | if (!file) {
115 | return file;
116 | }
117 | if (!isAnAllowedImage($scope.allowedTypes, file)) {
118 | return NOT_AN_IMAGE;
119 | }
120 | return {
121 | fileReaderPromise: fileReader.readAsDataUrl(file, $scope),
122 | };
123 | });
124 | $scope.$watch('image', function(value) {
125 | if (value && typeof value === 'string') {
126 | $scope.image = {
127 | src: value,
128 | isPath: true,
129 | };
130 | }
131 | });
132 | ngModel.$validators.image = function(modelValue, viewValue) {
133 | var value = modelValue || viewValue;
134 | return value !== NOT_AN_IMAGE;
135 | };
136 | ngModel.$asyncValidators.parsing = function(modelValue, viewValue) {
137 | var value = modelValue || viewValue;
138 | if (!value || !value.fileReaderPromise) {
139 | return createResolvedPromise();
140 | }
141 | // This should help keep the model value clean. At least I hope it does
142 | value.fileReaderPromise.finally(function() {
143 | delete value.fileReaderPromise;
144 | });
145 | return value.fileReaderPromise.then(function(dataUrl) {
146 | value.src = dataUrl;
147 | }, function() {
148 | return $q.reject('Failed to parse');
149 | });
150 | };
151 | ngModel.$asyncValidators.dimensions = function(modelValue, viewValue) {
152 | if (!attrs.dimensions) {
153 | return createResolvedPromise();
154 | }
155 | var value = modelValue || viewValue;
156 | if (!value || !value.fileReaderPromise) {
157 | return createResolvedPromise();
158 | }
159 | var deferred = $q.defer();
160 | value.fileReaderPromise.then(function(dataUrl) {
161 | // creating an image lets us find out its dimensions after it's loaded
162 | var image = document.createElement('img');
163 | image.addEventListener('load', function() {
164 | var valid = $scope.dimensionRestrictions({
165 | width: image.width,
166 | height: image.height,
167 | });
168 | $scope.$apply(function() {
169 | if (valid) {
170 | deferred.resolve();
171 | } else {
172 | deferred.reject('Invalid dimensions');
173 | }
174 | });
175 | });
176 | image.addEventListener('error', function() {
177 | $scope.$apply(function() {
178 | deferred.reject('Failed to detect dimensions. Not an image!');
179 | });
180 | });
181 | image.src = dataUrl;
182 | }, function() {
183 | deferred.reject('Failed to detect dimensions');
184 | });
185 | return deferred.promise;
186 | };
187 | ngModel.$asyncValidators.size = function(modelValue, viewValue) {
188 | if (!attrs.size) {
189 | return createResolvedPromise();
190 | }
191 |
192 | var value = modelValue || viewValue;
193 |
194 | if (!value || !value.fileReaderPromise) {
195 | return createResolvedPromise();
196 | }
197 |
198 | var deferred = $q.defer();
199 |
200 | value.fileReaderPromise.then(function(dataUrl) {
201 |
202 | var image = document.createElement('img');
203 |
204 | image.addEventListener('load', function() {
205 | var valid = $scope.sizeRestrictions({
206 | size: viewValue.size
207 | });
208 |
209 | $scope.$apply(function() {
210 | if (valid) {
211 | deferred.resolve();
212 | } else {
213 | deferred.reject('Invalid size');
214 | }
215 | });
216 |
217 | });
218 |
219 | image.src = dataUrl;
220 | }, function() {
221 | deferred.reject('Failed to detect size');
222 | });
223 | return deferred.promise;
224 | };
225 | }
226 | };
227 | }
228 | ]);
229 | })();
230 | })(window.angular);
231 |
--------------------------------------------------------------------------------
/dist/ng-image-input-with-preview.min.css:
--------------------------------------------------------------------------------
1 | /*! ng-image-input-with-preview v1.0.0 */
2 |
--------------------------------------------------------------------------------
/dist/ng-image-input-with-preview.min.js:
--------------------------------------------------------------------------------
1 | /*! ng-image-input-with-preview v1.0.0 */
2 |
3 | !function(e,i){"use strict";e.module("fileReaderService",[]).factory("fileReader",["$q",function(f){return{readAsDataUrl:function(e,i){var n,r,t,a,o,s,c,d,l,u=f.defer();return(n=u,r=i,(l=new FileReader).onload=(t=l,a=n,o=r,function(){o.$apply(function(){a.resolve(t.result)})}),l.onerror=(s=l,c=n,d=r,function(){d.$apply(function(){c.reject(s.result)})}),l).readAsDataURL(e),u.promise}}}]),e.module("ngImageInputWithPreview",["fileReaderService"]).directive("imageWithPreview",["fileReader","$q",function(o,s){var c="this-is-not-an-image",d=function(){var e=s.defer();return e.resolve(),e.promise};return{restrict:"A",require:"ngModel",scope:{image:"=ngModel",allowedTypes:"@accept",dimensionRestrictions:"&dimensions",sizeRestrictions:"&size"},link:function(t,e,a,n){e.bind("change",function(e){var i=(e.srcElement||e.target).files[0];n.$setViewValue(i,"change")}),n.$parsers.push(function(e){return e?(i=t.allowedTypes,r=e,i||(i="image/png,image/jpeg"),i.split(",").some(function(e){if(e===r.type)return!0;var i=e.split("/"),n=r.type.split("/");return 2===i.length&&2===n.length&&"*"===i[1]&&i[0]===n[0]})?{fileReaderPromise:o.readAsDataUrl(e,t)}:c):e;var i,r}),t.$watch("image",function(e){e&&"string"==typeof e&&(t.image={src:e,isPath:!0})}),n.$validators.image=function(e,i){return(e||i)!==c},n.$asyncValidators.parsing=function(e,i){var n=e||i;return n&&n.fileReaderPromise?(n.fileReaderPromise.finally(function(){delete n.fileReaderPromise}),n.fileReaderPromise.then(function(e){n.src=e},function(){return s.reject("Failed to parse")})):d()},n.$asyncValidators.dimensions=function(e,i){if(!a.dimensions)return d();var n=e||i;if(!n||!n.fileReaderPromise)return d();var r=s.defer();return n.fileReaderPromise.then(function(e){var i=document.createElement("img");i.addEventListener("load",function(){var e=t.dimensionRestrictions({width:i.width,height:i.height});t.$apply(function(){e?r.resolve():r.reject("Invalid dimensions")})}),i.addEventListener("error",function(){t.$apply(function(){r.reject("Failed to detect dimensions. Not an image!")})}),i.src=e},function(){r.reject("Failed to detect dimensions")}),r.promise},n.$asyncValidators.size=function(e,n){if(!a.size)return d();var i=e||n;if(!i||!i.fileReaderPromise)return d();var r=s.defer();return i.fileReaderPromise.then(function(e){var i=document.createElement("img");i.addEventListener("load",function(){var e=t.sizeRestrictions({size:n.size});t.$apply(function(){e?r.resolve():r.reject("Invalid size")})}),i.src=e},function(){r.reject("Failed to detect size")}),r.promise}}}}])}(window.angular);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-image-input-with-preview",
3 | "version": "1.0.0",
4 | "description": "A FileReader based directive to easily preview and upload image files.",
5 | "homepage": "https://github.com/deiwin/ngImageInputWithPreview",
6 | "author": "Deiwin Sarjas ",
7 | "main": "Gruntfile.js",
8 | "keywords": [
9 | "directive",
10 | "angular",
11 | "file",
12 | "image",
13 | "input"
14 | ],
15 | "scripts": {
16 | "test": "grunt test",
17 | "build": "grunt && grunt build"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/deiwin/ngImageInputWithPreview.git"
22 | },
23 | "license": "MIT",
24 | "devDependencies": {
25 | "angular": "~1.7.2",
26 | "angular-mocks": "~1.7.2",
27 | "coffee-script": "~1.12.7",
28 | "connect-less": "^0.3.1",
29 | "glob": "~7.1.2",
30 | "grunt": "~1.0.3",
31 | "grunt-bump": "0.8.0",
32 | "grunt-cli": "~1.2.0",
33 | "grunt-contrib-concat": "~1.0.1",
34 | "grunt-contrib-connect": "^1.0.2",
35 | "grunt-contrib-jshint": "~1.1.0",
36 | "grunt-contrib-less": "~2.0.0",
37 | "grunt-contrib-uglify": "~3.3.0",
38 | "grunt-contrib-watch": "~1.1.0",
39 | "grunt-coveralls": "^2.0.0",
40 | "grunt-karma": "~2.0.0",
41 | "grunt-ng-annotate": "^3.0.0",
42 | "grunt-npm": "https://github.com/Xiphe/grunt-npm/tarball/98187270151e53d45214d778de394a557e81958c",
43 | "grunt-parallel": "^0.5.1",
44 | "grunt-shell": "~2.1.0",
45 | "jasmine-core": "^3.1.0",
46 | "jasmine-spec-reporter": "^4.2.1",
47 | "karma": "~2.0.4",
48 | "karma-chrome-launcher": "~2.2.0",
49 | "karma-coffee-preprocessor": "~1.0.1",
50 | "karma-coverage": "^1.1.2",
51 | "karma-firefox-launcher": "~1.1.0",
52 | "karma-html2js-preprocessor": "~1.1.0",
53 | "karma-jasmine": "~1.1.2",
54 | "karma-phantomjs-launcher": "~1.0.4",
55 | "karma-requirejs": "~1.1.0",
56 | "karma-script-launcher": "~1.0.0",
57 | "karma-spec-reporter": "0.0.32",
58 | "load-grunt-tasks": "~4.0.0",
59 | "mkdirp": "^0.5.0",
60 | "requirejs": "^2.3.5"
61 | },
62 | "contributors": [
63 | "Unknown ",
64 | "Deiwin Sarjas ",
65 | "HormCZ ",
66 | "Jakub Knejzlík "
67 | ],
68 | "maintainers": [
69 | "Deiwin Sarjas ",
70 | "Adam Jarvis ",
71 | "Deiwin Sarjas "
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/src/js/fileReader.service.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 | var module = angular.module('fileReaderService', []);
4 |
5 | // Copied from the following link with onProgress excluded because it's not needed
6 | // http://odetocode.com/blogs/scott/archive/2013/07/03/building-a-filereader-service-for-angularjs-the-service.aspx
7 | module.factory('fileReader', ['$q',
8 | function($q) {
9 | var onLoad = function(reader, deferred, scope) {
10 | return function() {
11 | scope.$apply(function() {
12 | deferred.resolve(reader.result);
13 | });
14 | };
15 | };
16 |
17 | var onError = function(reader, deferred, scope) {
18 | return function() {
19 | scope.$apply(function() {
20 | deferred.reject(reader.result);
21 | });
22 | };
23 | };
24 |
25 | var getReader = function(deferred, scope) {
26 | var reader = new FileReader();
27 | reader.onload = onLoad(reader, deferred, scope);
28 | reader.onerror = onError(reader, deferred, scope);
29 | return reader;
30 | };
31 |
32 | var readAsDataURL = function(file, scope) {
33 | var deferred = $q.defer();
34 |
35 | var reader = getReader(deferred, scope);
36 | reader.readAsDataURL(file);
37 |
38 | return deferred.promise;
39 | };
40 |
41 | return {
42 | readAsDataUrl: readAsDataURL
43 | };
44 | }
45 | ]);
46 | })();
47 |
--------------------------------------------------------------------------------
/src/js/imageWithPreview.directive.js:
--------------------------------------------------------------------------------
1 | /*jshint -W072 */
2 | // ^ ignore jshint warning about link method having too many parameters
3 | (function() {
4 | 'use strict';
5 | var module = angular.module('ngImageInputWithPreview', [
6 | 'fileReaderService',
7 | ]);
8 |
9 | module.directive('imageWithPreview', ['fileReader', '$q',
10 | function(fileReader, $q) {
11 | var DEFAULT_MIMETYPES = 'image/png,image/jpeg';
12 | var NOT_AN_IMAGE = 'this-is-not-an-image';
13 |
14 | var isAnAllowedImage = function(allowedTypes, file) {
15 | if (!allowedTypes) {
16 | allowedTypes = DEFAULT_MIMETYPES;
17 | }
18 | var allowedTypeArray = allowedTypes.split(',');
19 | return allowedTypeArray.some(function(allowedType) {
20 | if (allowedType === file.type) {
21 | return true;
22 | }
23 | var allowedTypeSplit = allowedType.split('/');
24 | var fileTypeSplit = file.type.split('/');
25 | return allowedTypeSplit.length === 2 && fileTypeSplit.length === 2 && allowedTypeSplit[1] === '*' &&
26 | allowedTypeSplit[0] === fileTypeSplit[0];
27 | });
28 | };
29 | var createResolvedPromise = function() {
30 | var d = $q.defer();
31 | d.resolve();
32 | return d.promise;
33 | };
34 |
35 | return {
36 | restrict: 'A',
37 | require: 'ngModel',
38 | scope: {
39 | image: '=ngModel',
40 | allowedTypes: '@accept',
41 | dimensionRestrictions: '&dimensions',
42 | sizeRestrictions: '&size'
43 | },
44 | link: function($scope, element, attrs, ngModel) {
45 | element.bind('change', function(event) {
46 | var file = (event.srcElement || event.target).files[0];
47 | // the following link recommends making a copy of the object, but as the value will only be changed
48 | // from the view, we don't have to worry about making a copy
49 | // https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$setViewValue
50 | ngModel.$setViewValue(file, 'change');
51 | });
52 | ngModel.$parsers.push(function(file) {
53 | if (!file) {
54 | return file;
55 | }
56 | if (!isAnAllowedImage($scope.allowedTypes, file)) {
57 | return NOT_AN_IMAGE;
58 | }
59 | return {
60 | fileReaderPromise: fileReader.readAsDataUrl(file, $scope),
61 | };
62 | });
63 | $scope.$watch('image', function(value) {
64 | if (value && typeof value === 'string') {
65 | $scope.image = {
66 | src: value,
67 | isPath: true,
68 | };
69 | }
70 | });
71 | ngModel.$validators.image = function(modelValue, viewValue) {
72 | var value = modelValue || viewValue;
73 | return value !== NOT_AN_IMAGE;
74 | };
75 | ngModel.$asyncValidators.parsing = function(modelValue, viewValue) {
76 | var value = modelValue || viewValue;
77 | if (!value || !value.fileReaderPromise) {
78 | return createResolvedPromise();
79 | }
80 | // This should help keep the model value clean. At least I hope it does
81 | value.fileReaderPromise.finally(function() {
82 | delete value.fileReaderPromise;
83 | });
84 | return value.fileReaderPromise.then(function(dataUrl) {
85 | value.src = dataUrl;
86 | }, function() {
87 | return $q.reject('Failed to parse');
88 | });
89 | };
90 | ngModel.$asyncValidators.dimensions = function(modelValue, viewValue) {
91 | if (!attrs.dimensions) {
92 | return createResolvedPromise();
93 | }
94 | var value = modelValue || viewValue;
95 | if (!value || !value.fileReaderPromise) {
96 | return createResolvedPromise();
97 | }
98 | var deferred = $q.defer();
99 | value.fileReaderPromise.then(function(dataUrl) {
100 | // creating an image lets us find out its dimensions after it's loaded
101 | var image = document.createElement('img');
102 | image.addEventListener('load', function() {
103 | var valid = $scope.dimensionRestrictions({
104 | width: image.width,
105 | height: image.height,
106 | });
107 | $scope.$apply(function() {
108 | if (valid) {
109 | deferred.resolve();
110 | } else {
111 | deferred.reject('Invalid dimensions');
112 | }
113 | });
114 | });
115 | image.addEventListener('error', function() {
116 | $scope.$apply(function() {
117 | deferred.reject('Failed to detect dimensions. Not an image!');
118 | });
119 | });
120 | image.src = dataUrl;
121 | }, function() {
122 | deferred.reject('Failed to detect dimensions');
123 | });
124 | return deferred.promise;
125 | };
126 | ngModel.$asyncValidators.size = function(modelValue, viewValue) {
127 | if (!attrs.size) {
128 | return createResolvedPromise();
129 | }
130 |
131 | var value = modelValue || viewValue;
132 |
133 | if (!value || !value.fileReaderPromise) {
134 | return createResolvedPromise();
135 | }
136 |
137 | var deferred = $q.defer();
138 |
139 | value.fileReaderPromise.then(function(dataUrl) {
140 |
141 | var image = document.createElement('img');
142 |
143 | image.addEventListener('load', function() {
144 | var valid = $scope.sizeRestrictions({
145 | size: viewValue.size
146 | });
147 |
148 | $scope.$apply(function() {
149 | if (valid) {
150 | deferred.resolve();
151 | } else {
152 | deferred.reject('Invalid size');
153 | }
154 | });
155 |
156 | });
157 |
158 | image.src = dataUrl;
159 | }, function() {
160 | deferred.reject('Failed to detect size');
161 | });
162 | return deferred.promise;
163 | };
164 | }
165 | };
166 | }
167 | ]);
168 | })();
169 |
--------------------------------------------------------------------------------
/src/less/style.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deiwin/ngImageInputWithPreview/6247398bf9599cb4a6fed8931872aadee00b1b9f/src/less/style.less
--------------------------------------------------------------------------------
/tasks/files.js:
--------------------------------------------------------------------------------
1 | var _ = require('grunt').util._;
2 | var files = {
3 | grunt: 'Gruntfile.js',
4 |
5 | source: [
6 | 'src/js/helper.module.js',
7 | 'src/js/!(helper.module)*.js'
8 | ],
9 | sourceStyle: [
10 | 'src/less/style.less'
11 | ],
12 |
13 | distStyle: 'dist/<%= pkg.name %>.css',
14 | distStyleMin: 'dist/<%= pkg.name %>.min.css',
15 | dist: 'dist/<%= pkg.name %>.js',
16 | distMin: 'dist/<%= pkg.name %>.min.js',
17 | dists: 'dist/*',
18 |
19 | unitTests: ['test/unit/SpecHelper.+(js|coffee)', 'test/unit/**/*Spec.+(js|coffee)'],
20 |
21 | environments: {},
22 |
23 | demo: 'demo/*',
24 |
25 | 'package': ['package.json']
26 | };
27 |
28 | var baseEnvironment = [].concat(
29 | 'node_modules/angular/angular.js',
30 | files.source
31 | );
32 |
33 | var demoEnvironment = _.clone(baseEnvironment);
34 | var karmaEnvironment = _.clone(baseEnvironment);
35 |
36 | karmaEnvironment.push('node_modules/angular-mocks/angular-mocks.js');
37 |
38 |
39 | files.environments.demo = demoEnvironment;
40 | files.environments.karma = karmaEnvironment;
41 |
42 | if (typeof module === 'object') {
43 | module.exports = files;
44 | }
45 |
--------------------------------------------------------------------------------
/tasks/helpers.js:
--------------------------------------------------------------------------------
1 | /* global process */
2 | var grunt = require('grunt');
3 | var lf = grunt.util.linefeed;
4 | var fs = require('fs');
5 | var path = require('path');
6 | var Helpers = {};
7 | var base = process.cwd();
8 | var glob = require('glob');
9 |
10 | /* Overwrite browser env variables if grunt options are set */
11 | var browsers = grunt.option('browser') || grunt.option('browsers');
12 | if (browsers) {
13 | process.env.KARMA_BROWSERS = browsers;
14 | process.env.PROTRACTOR_BROWSERS = browsers;
15 | }
16 |
17 | var reporters = grunt.option('reporter') || grunt.option('reporters');
18 | if (reporters) {
19 | process.env.KARMA_REPORTERS = reporters;
20 | process.env.PROTRACTOR_REPORTERS = reporters;
21 | }
22 |
23 | Helpers.config = {
24 | pkg: grunt.file.readJSON('./package.json'),
25 | env: process.env
26 | };
27 |
28 | Helpers.loadConfig = function(path) {
29 | var glob = require('glob');
30 | var object = {};
31 | var key = null;
32 |
33 | glob.sync('*', { cwd: path }).forEach(function(option) {
34 | key = option.replace(/\.js$/, '');
35 | object[key] = require('../' + path + option);
36 | });
37 |
38 | return object;
39 | };
40 |
41 | Helpers.cleanupModules = function(src, filepath) {
42 | /* Normalize line-feeds */
43 | src = grunt.util.normalizelf(src);
44 |
45 | /* Remove jshint comments */
46 | src = src.replace(/[\s]*\/\* (jshint|global).*\n/g, '');
47 |
48 | /* Trim */
49 | src = src.replace(/^\s+|\s+$/g, '');
50 |
51 | /* Indent */
52 | src = src.split(lf).map(function(line) {
53 | return ' ' + line;
54 | }).join(lf);
55 |
56 | return ' // ' + filepath + lf + src;
57 | };
58 |
59 | Helpers.getTemplate = function(name) {
60 | return fs.readFileSync('./tasks/templates/' + name + '.tpl', 'utf8');
61 | };
62 |
63 | function getScripts(env) {
64 | var scripts = '';
65 | var tag = '\n';
66 | require('./files').environments[env].forEach(function(fileGlobs) {
67 | glob.sync(fileGlobs).forEach(function(file) {
68 | scripts += tag.replace(':src', '/' + file);
69 | });
70 | });
71 |
72 | return scripts;
73 | }
74 |
75 | function getStyles() {
76 | var styles = '';
77 | var tag = ' \n';
78 | require('./files').sourceStyle.forEach(function(file) {
79 | styles += tag.replace(':href', '/' + file.replace('.less', '.css'));
80 | });
81 |
82 | return styles;
83 | }
84 |
85 | Helpers.getIndex = function(dir, env, callback) {
86 | fs.readFile(path.join(base, dir, 'index.html'), function(err, index) {
87 | callback(index.toString()
88 | .replace('', getScripts(env))
89 | .replace('', getStyles())
90 | );
91 | });
92 | };
93 |
94 | module.exports = Helpers;
95 |
--------------------------------------------------------------------------------
/tasks/options/bump.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 |
3 | module.exports = {
4 | options: {
5 | files: files.package,
6 | updateConfigs: ['pkg'],
7 | commitFiles: files.package.concat([files.dists]),
8 | pushTo: 'origin'
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/tasks/options/concat.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 | var Helpers = require('../helpers');
3 |
4 | module.exports = {
5 | options: {
6 | separator: '\n\n',
7 | stripBanners: true,
8 | banner: Helpers.getTemplate('banner') + Helpers.getTemplate('wrap-top'),
9 | footer: Helpers.getTemplate('wrap-bottom'),
10 | process: Helpers.cleanupModules
11 | },
12 | dist: {
13 | src: files.source,
14 | dest: files.dist
15 | },
16 | bannerToDistStyle: {
17 | src: [files.distStyle],
18 | dest: files.distStyle,
19 | options: {
20 | banner: Helpers.getTemplate('banner'),
21 | process: false,
22 | footer: ''
23 | }
24 | },
25 | bannerToDistStyleMin: {
26 | src: [files.distStyleMin],
27 | dest: files.distStyleMin,
28 | options: {
29 | banner: Helpers.getTemplate('banner-min'),
30 | process: false,
31 | footer: ''
32 | }
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/tasks/options/connect.js:
--------------------------------------------------------------------------------
1 | var optPort = require('grunt').option('port');
2 | var helpers = require('../helpers');
3 | var connectLess = require('connect-less');
4 | var mkdirp = require('mkdirp');
5 | var path = require('path');
6 | var base = process.cwd();
7 |
8 | var baseDirs = ['.', '.tmp'];
9 |
10 | mkdirp(path.join(base, '.tmp/demo'));
11 | mkdirp(path.join(base, '.tmp/src/less'));
12 |
13 | function middleware(dir, env) {
14 | return function(connect, options, middlewares) {
15 | /* Prepare index.html */
16 | middlewares.unshift(function addJs(req, res, next) {
17 | if (req.method === 'GET' && req.url === '/') {
18 | helpers.getIndex(dir, env, function(index) {
19 | res.end(index);
20 | });
21 | return;
22 | }
23 | next();
24 | });
25 |
26 | /* Handle style requests */
27 | middlewares.unshift(connectLess({
28 | dst: path.join(base, '.tmp')
29 | }));
30 |
31 | return middlewares;
32 | };
33 | }
34 |
35 | module.exports = {
36 | options: {
37 | hostname: '*'
38 | },
39 | demo: {
40 | options: {
41 | port: optPort || process.env.DEMO_PORT || 8000,
42 | middleware: middleware('demo', 'demo'),
43 | base: baseDirs.concat('demo'),
44 | livereload: true
45 | }
46 | },
47 | coverage: {
48 | options: {
49 | port: optPort || process.env.COVERAGE_PORT || 7000,
50 | base: path.join(base, '.tmp/coverage/lcov-report'),
51 | keepalive: true,
52 | open: true
53 | }
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/tasks/options/coveralls.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | coveralls: {
3 | src: '.tmp/coverage/lcov.info'
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/tasks/options/jshint.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 |
3 | module.exports = {
4 | files: files.source.concat([files.grunt, files.unitTests]),
5 | options: {
6 | ignores: ['**/*.coffee'],
7 | jshintrc: true
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/tasks/options/karma.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 | var DEFAULT_BROWSERS = 'Chrome,Firefox,PhantomJS';
3 | var browsers = process.env.KARMA_BROWSERS;
4 | var reporters = process.env.KARMA_REPORTERS;
5 |
6 | module.exports = {
7 | options: {
8 | browsers: (browsers || 'Chrome').split(','),
9 | preprocessors: {
10 | 'src/**/*.+(js|coffee)': ['coverage'],
11 | '**/*.coffee': ['coffee']
12 | },
13 | frameworks: [
14 | 'jasmine'
15 | ],
16 | coverageReporter: {
17 | reporters: [{
18 | type: 'lcov',
19 | dir: '.tmp/coverage',
20 | subdir: '.'
21 | }, {
22 | dir: '.tmp/coverage',
23 | type: 'text-summary'
24 | }]
25 | },
26 | reporters: (reporters || 'progress').split(',').concat('coverage'),
27 | singleRun: true,
28 | },
29 | all: {
30 | options: {
31 | browsers: (browsers || DEFAULT_BROWSERS).split(',')
32 | },
33 | files: {
34 | src: files.environments.karma.concat([files.unitTests])
35 | }
36 | },
37 | watch: {
38 | options: {
39 | background: true,
40 | singleRun: false,
41 | autoWatch: false
42 | }
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/tasks/options/less.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 |
3 | var f = {};
4 | var fm = {};
5 | f[files.distStyle] = files.sourceStyle;
6 | fm[files.distStyleMin] = files.sourceStyle;
7 |
8 | module.exports = {
9 | dist: {
10 | files: f
11 | },
12 | distmin: {
13 | options: {
14 | cleancss: true
15 | },
16 | files: fm
17 | }
18 | };
--------------------------------------------------------------------------------
/tasks/options/ngAnnotate.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 |
3 | var fls = {};
4 | fls[files.dist] = [files.dist];
5 |
6 | module.exports = {
7 | options: {
8 | singleQuotes: true
9 | },
10 | dist: {
11 | files: fls
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/tasks/options/npm-contributors.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | contributors: {
3 | options: {
4 | commit: false,
5 | filter: function(contributors) {
6 | return contributors.filter(function(contributor) {
7 | return contributor.commitCount < 3;
8 | });
9 | }
10 | }
11 | },
12 | maintainers: {
13 | options: {
14 | filter: function(contributors) {
15 | return contributors.filter(function(contributor) {
16 | return contributor.commitCount >= 3;
17 | });
18 | },
19 | as: 'maintainers'
20 | }
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/tasks/options/parallel.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | watchdemo: {
3 | options: {
4 | grunt: true,
5 | stream: true
6 | },
7 | tasks: ['watch:demo']
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/tasks/options/shell.js:
--------------------------------------------------------------------------------
1 | var optPort = require('grunt').option('port');
2 | var path = require('path');
3 | var base = process.cwd();
4 |
5 | module.exports = {
6 | deleteCoverages: {
7 | command: [
8 | 'rm -rf',
9 | path.join(base, '.tmp/coverage')
10 | ].join(' ')
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/tasks/options/uglify.js:
--------------------------------------------------------------------------------
1 | var files = require('../files');
2 | var Helpers = require('../helpers');
3 |
4 | var fls = {};
5 | fls[files.distMin] = [files.dist];
6 |
7 | module.exports = {
8 | options: {
9 | banner: Helpers.getTemplate('banner-min')
10 | },
11 | dist: {
12 | files: fls
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/tasks/options/watch.js:
--------------------------------------------------------------------------------
1 | var grunt = require('grunt');
2 | var files = require('../files');
3 | var testfiles = files.source.concat(files.sourceStyle).concat([files.grunt]);
4 | var unitTestfiles = grunt.util._.clone(testfiles).concat([files.unitTests]);
5 | var demoFiles = files.source.concat(files.sourceStyle, files.demo);
6 |
7 |
8 | module.exports = {
9 | andtestunit: {
10 | files: unitTestfiles,
11 | tasks: ['shell:deleteCoverages', 'karma:watch:run']
12 | },
13 | demo: {
14 | files: demoFiles,
15 | tasks: [],
16 | options: {
17 | livereload: true
18 | }
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/tasks/templates/banner-min.tpl:
--------------------------------------------------------------------------------
1 | /*! <%= pkg.name %> v<%= pkg.version %> */
2 |
--------------------------------------------------------------------------------
/tasks/templates/banner.tpl:
--------------------------------------------------------------------------------
1 | /*!
2 | * <%= pkg.name %> v<%= pkg.version %>
3 | * <%= pkg.homepage %>
4 | *
5 | * <%= pkg.description %>
6 | *
7 | * Copyright <%= grunt.template.today("yyyy") %>, <%= pkg.author %>
8 | * Released under the <%= pkg.license %> license
9 | */
10 |
--------------------------------------------------------------------------------
/tasks/templates/wrap-bottom.tpl:
--------------------------------------------------------------------------------
1 |
2 | })(window.angular);
3 |
--------------------------------------------------------------------------------
/tasks/templates/wrap-top.tpl:
--------------------------------------------------------------------------------
1 | (function(angular, undefined) {
2 | 'use strict';
3 |
4 |
--------------------------------------------------------------------------------
/test/unit/SpecHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Some tasks we need to perform before any test-suite starts.
3 | */
4 | /* jshint undef: false, unused: false */
5 |
6 | function compile(html) {
7 | var element, scope, parentScope;
8 | inject(function($compile, $rootScope, $timeout) {
9 | parentScope = $rootScope.$new();
10 | // if (prepareParentFunction) {
11 | // parentScope.$apply(function() {
12 | // prepareParentFunction(parentScope);
13 | // });
14 | // }
15 | element = angular.element(html);
16 | $compile(element)(parentScope);
17 | $timeout(function() {
18 | scope = element.isolateScope();
19 | parentScope.$digest();
20 | });
21 | $timeout.flush();
22 | });
23 |
24 | return {
25 | element: element,
26 | scope: scope,
27 | parentScope: parentScope
28 | };
29 | }
30 |
31 | /* Make sure, there are no unexpected request */
32 | afterEach(function() {
33 | if (window.$httpBackend) {
34 | $httpBackend.verifyNoOutstandingExpectation();
35 | $httpBackend.verifyNoOutstandingRequest();
36 | }
37 | });
38 |
--------------------------------------------------------------------------------
/test/unit/imageWithPreview.directiveSpec.js:
--------------------------------------------------------------------------------
1 | /* global compile*/
2 | describe('ngImageInputWithPreview', function() {
3 | 'use strict';
4 | beforeEach(function() {
5 | module('ngImageInputWithPreview', function($provide) {
6 | // mock the fileReader service
7 | $provide.provider('fileReader', {
8 | $get: function() {
9 | return {};
10 | }
11 | });
12 | });
13 | });
14 |
15 | var waitWhileThenRun = function(shouldContinue, f) {
16 | // I'm sorry, but I don't know a better way to see if the validators have
17 | // finished with a failure
18 | var interval = setInterval(function() {
19 | if (shouldContinue()) {
20 | return;
21 | }
22 | clearInterval(interval);
23 | f();
24 | }, 50);
25 | };
26 |
27 | var testSelectUnselectWorks = function(type, context) {
28 | var result, file, ngModel, element, $parentScope;
29 | beforeEach(inject(function($q, fileReader) {
30 | var deferred = $q.defer();
31 | element = context.element;
32 | $parentScope = context.$parentScope;
33 | // a single pixel image
34 | result = 'data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
35 | deferred.resolve(result);
36 | fileReader.readAsDataUrl = jasmine.createSpy().and.returnValue(deferred.promise);
37 |
38 | file = new Blob([result], {
39 | type: type,
40 | });
41 | file.lastModifiedDate = '';
42 | file.name = 'filename';
43 |
44 | element.prop('files', [file]);
45 | ngModel = element.data('$ngModelController');
46 | }));
47 |
48 | it('should set the data url the result', function(done) {
49 | ngModel.$viewChangeListeners.push(function() {
50 | expect($parentScope.image.src).toEqual(result);
51 | done();
52 | });
53 | element.triggerHandler('change');
54 | });
55 |
56 | describe('and then unselected', function() {
57 | beforeEach(function() {
58 | element.prop('files', [undefined]);
59 | element.triggerHandler('change');
60 | });
61 |
62 | it('should set the data url to an empty string', function() {
63 | expect($parentScope.image).toBeUndefined();
64 | element.triggerHandler('change');
65 | });
66 | });
67 | };
68 |
69 | var testTextFile = function(context) {
70 | var file, ngModel, element, $parentScope;
71 | beforeEach(function() {
72 | element = context.element;
73 | $parentScope = context.$parentScope;
74 | file = new Blob([''], {
75 | type: 'text/plain',
76 | });
77 | file.lastModifiedDate = '';
78 | file.name = 'filename';
79 | element.prop('files', [file]);
80 | element.triggerHandler('change');
81 | ngModel = element.data('$ngModelController');
82 | });
83 |
84 | it('should not set the data url', function() {
85 | expect($parentScope.image).toBeUndefined();
86 | });
87 |
88 | it('should have an error', function() {
89 | expect(ngModel.$error.image).toBe(true);
90 | });
91 |
92 | describe('and then unselected', function() {
93 | beforeEach(function() {
94 | element.prop('files', [undefined]);
95 | element.triggerHandler('change');
96 | });
97 |
98 | it('should not set the data url', function() {
99 | expect($parentScope.image).toBeUndefined();
100 | });
101 |
102 | it('should not have an error', function() {
103 | expect(ngModel.$error.image).toBeFalsy();
104 | });
105 | });
106 | };
107 |
108 | var testPNGFile = function(context) {
109 | testSelectUnselectWorks('image/png', context);
110 | };
111 |
112 | var testDimensionsCheckFails = function(context) {
113 | var file, ngModel, element, $parentScope;
114 | beforeEach(inject(function($q, fileReader) {
115 | var deferred = $q.defer();
116 | element = context.element;
117 | $parentScope = context.$parentScope;
118 | // a single pixel image
119 | var result = 'data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
120 | deferred.resolve(result);
121 | fileReader.readAsDataUrl = jasmine.createSpy().and.returnValue(deferred.promise);
122 | file = new Blob([result], {
123 | type: 'image/png',
124 | });
125 | file.lastModifiedDate = '';
126 | file.name = 'filename';
127 | element.prop('files', [file]);
128 | element.triggerHandler('change');
129 | ngModel = element.data('$ngModelController');
130 | }));
131 |
132 | it('should not set the data url', function(done) {
133 | waitWhileThenRun(function() {
134 | return ngModel.$pending && Object.keys(ngModel.$pending).length > 0;
135 | }, function() {
136 | expect($parentScope.image).toBeUndefined();
137 | done();
138 | });
139 | });
140 |
141 | it('should have an error', function(done) {
142 | waitWhileThenRun(function() {
143 | return ngModel.$pending && Object.keys(ngModel.$pending).length > 0;
144 | }, function() {
145 | expect(ngModel.$error.dimensions).toBe(true);
146 | done();
147 | });
148 | });
149 |
150 | describe('and then unselected', function() {
151 | beforeEach(function() {
152 | element.prop('files', [undefined]);
153 | element.triggerHandler('change');
154 | });
155 |
156 | it('should not set the data url', function() {
157 | expect($parentScope.image).toBeUndefined();
158 | });
159 |
160 | it('should not have an error', function() {
161 | expect(ngModel.$error.image).toBeFalsy();
162 | });
163 | });
164 | };
165 |
166 | var testSizeCheckFails = function(context) {
167 | var file, ngModel, element, $parentScope;
168 | beforeEach(inject(function($q, fileReader) {
169 | var deferred = $q.defer();
170 | element = context.element;
171 | $parentScope = context.$parentScope;
172 | // a single pixel image
173 | var result = 'data:image/gif;base64,R0lGODlhAQABAPAAAP8REf///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
174 | deferred.resolve(result);
175 | fileReader.readAsDataUrl = jasmine.createSpy().and.returnValue(deferred.promise);
176 | file = new Blob([result], {
177 | type: 'image/png',
178 | });
179 | file.lastModifiedDate = '';
180 | file.name = 'filename';
181 | element.prop('files', [file]);
182 | element.triggerHandler('change');
183 | ngModel = element.data('$ngModelController');
184 | }));
185 |
186 | it('should not set the data url', function(done) {
187 | waitWhileThenRun(function() {
188 | return ngModel.$pending && Object.keys(ngModel.$pending).length > 0;
189 | }, function() {
190 | expect($parentScope.image).toBeUndefined();
191 | done();
192 | });
193 | });
194 |
195 | it('should have an error', function(done) {
196 | waitWhileThenRun(function() {
197 | return ngModel.$pending && Object.keys(ngModel.$pending).length > 0;
198 | }, function() {
199 | expect(ngModel.$error.size).toBe(true);
200 | done();
201 | });
202 | });
203 |
204 | describe('and then unselected', function() {
205 | beforeEach(function() {
206 | element.prop('files', [undefined]);
207 | element.triggerHandler('change');
208 | });
209 |
210 | it('should not set the data url', function() {
211 | expect($parentScope.image).toBeUndefined();
212 | });
213 |
214 | it('should not have an error', function() {
215 | expect(ngModel.$error.image).toBeFalsy();
216 | });
217 | });
218 | };
219 |
220 | describe('previewImage directive with attrs', function() {
221 | var element, $parentScope;
222 | var context = {};
223 | beforeEach(function() {
224 | // using instead of because browser's don't allow setting the
225 | // 'file' property on the input element and therefore make this more
226 | // difficult to test
227 | var compiled = compile('
');
229 | element = compiled.element;
230 | $parentScope = compiled.parentScope;
231 | context.element = element;
232 | context.$parentScope = $parentScope;
233 | });
234 |
235 | describe('with an image path set on model', function() {
236 | beforeEach(function() {
237 | $parentScope.$apply(function() {
238 | $parentScope.image = 'some/image/path.jpg';
239 | });
240 | });
241 |
242 | it('should set the path as source', function() {
243 | expect($parentScope.image.src).toEqual('some/image/path.jpg');
244 | });
245 | });
246 |
247 | describe('image selection canceled', function() {
248 | beforeEach(function() {
249 | element.prop('files', [undefined]);
250 | element.triggerHandler('change');
251 | });
252 |
253 | it('should not set the data url', function() {
254 | expect($parentScope.image).toBeUndefined();
255 | });
256 | });
257 |
258 | describe('with an image with a specified (mocked) mimetype selected', function() {
259 | testSelectUnselectWorks('image/mockedpng', context);
260 | });
261 |
262 | describe('with an image with a specified (mocked and asterisk suffixed) mimetype selected', function() {
263 | testSelectUnselectWorks('mockprefix/whatever', context);
264 | });
265 |
266 | describe('with an image selected', function() {
267 | testPNGFile(context);
268 | });
269 |
270 | describe('with a non-image file selected', function() {
271 | testTextFile(context);
272 | });
273 | });
274 |
275 | describe('previewImage directive', function() {
276 | describe('with dimension restrictions set to true', function() {
277 | var context = {};
278 | beforeEach(function() {
279 | // using instead of because browser's don't allow setting the
280 | // 'file' property on the input element and therefore make this more
281 | // difficult to test
282 | var compiled = compile('
');
283 | context.element = compiled.element;
284 | context.$parentScope = compiled.parentScope;
285 | });
286 |
287 | describe('with an image selected', function() {
288 | testPNGFile(context);
289 | });
290 |
291 | describe('with a non-image file selected', function() {
292 | testTextFile(context);
293 | });
294 | });
295 |
296 | describe('with dimension restrictions set to false', function() {
297 | var context = {};
298 | beforeEach(function() {
299 | // using instead of because browser's don't allow setting the
300 | // 'file' property on the input element and therefore make this more
301 | // difficult to test
302 | var compiled = compile('
');
303 | context.element = compiled.element;
304 | context.$parentScope = compiled.parentScope;
305 | });
306 |
307 | describe('with an image selected', function() {
308 | testDimensionsCheckFails(context);
309 | });
310 |
311 | describe('with a non-image file selected', function() {
312 | testTextFile(context);
313 | });
314 | });
315 |
316 | describe('with dimension restrictions which will evaluate to false', function() {
317 | var context = {};
318 | beforeEach(function() {
319 | // using instead of because browser's don't allow setting the
320 | // 'file' property on the input element and therefore make this more
321 | // difficult to test
322 | var compiled = compile('
');
323 | context.element = compiled.element;
324 | context.$parentScope = compiled.parentScope;
325 | });
326 |
327 | describe('with an image selected', function() {
328 | testDimensionsCheckFails(context);
329 | });
330 |
331 | describe('with a non-image file selected', function() {
332 | testTextFile(context);
333 | });
334 | });
335 |
336 | describe('with dimension restrictions which will evaluate to true', function() {
337 | var context = {};
338 | beforeEach(function() {
339 | // using instead of because browser's don't allow setting the
340 | // 'file' property on the input element and therefore make this more
341 | // difficult to test
342 | var compiled = compile('
');
343 | context.element = compiled.element;
344 | context.$parentScope = compiled.parentScope;
345 | });
346 |
347 | describe('with an image selected', function() {
348 | testPNGFile(context);
349 | });
350 |
351 | describe('with a non-image file selected', function() {
352 | testTextFile(context);
353 | });
354 | });
355 |
356 | describe('with dimension restriction with multiple height/width var replacements', function() {
357 | var context = {};
358 | beforeEach(function() {
359 | // using instead of because browser's don't allow setting the
360 | // 'file' property on the input element and therefore make this more
361 | // difficult to test
362 | var dimensions = 'height < 400 && width < 2000 && width > 0.5 * height';
363 | var compiled = compile('
');
364 | context.element = compiled.element;
365 | context.$parentScope = compiled.parentScope;
366 | });
367 |
368 | describe('with an image selected', function() {
369 | testPNGFile(context);
370 | });
371 |
372 | describe('with a non-image file selected', function() {
373 | testTextFile(context);
374 | });
375 | });
376 |
377 | describe('with size restrictions which will evaluate to false', function() {
378 | var context = {};
379 | beforeEach(function() {
380 | // using instead of because browser's don't allow setting the
381 | // 'file' property on the input element and therefore make this more
382 | // difficult to test
383 | var compiled = compile('
');
384 | context.element = compiled.element;
385 | context.$parentScope = compiled.parentScope;
386 | });
387 |
388 | describe('with an image selected', function() {
389 | testSizeCheckFails(context);
390 | });
391 |
392 | describe('with a non-image file selected', function() {
393 | testTextFile(context);
394 | });
395 | });
396 |
397 | describe('with size restrictions which will evaluate to true', function() {
398 | var context = {};
399 | beforeEach(function() {
400 | // using instead of becau§e browser's don't allow setting the
401 | // 'file' property on the input element and therefore make this more
402 | // difficult to test
403 | var compiled = compile('
');
404 | context.element = compiled.element;
405 | context.$parentScope = compiled.parentScope;
406 | });
407 |
408 | describe('with an image selected', function() {
409 | testPNGFile(context);
410 | });
411 |
412 | describe('with a non-image file selected', function() {
413 | testTextFile(context);
414 | });
415 | });
416 | });
417 |
418 | describe('previewImage directive', function() {
419 | var context = {};
420 | beforeEach(function() {
421 | // using instead of because browser's don't allow setting the
422 | // 'file' property on the input element and therefore make this more
423 | // difficult to test
424 | var compiled = compile('
');
425 | context.element = compiled.element;
426 | context.$parentScope = compiled.parentScope;
427 | });
428 |
429 | describe('with an image selected', function() {
430 | testPNGFile(context);
431 | });
432 |
433 | describe('with a non-image file selected', function() {
434 | testTextFile(context);
435 | });
436 | });
437 | });
438 |
--------------------------------------------------------------------------------
/test/unit/setupSpec.js:
--------------------------------------------------------------------------------
1 | describe('Setup', function() {
2 | 'use strict';
3 |
4 | it('should be able to execute tests.', function() {
5 | expect(true).toBe(true);
6 | });
7 |
8 | it('should have angular defined.', function() {
9 | expect(angular).toBeDefined();
10 | });
11 |
12 | it('should use jasmine 2.0 done callbacks', function(done) {
13 | expect(window.waitsFor).toBeUndefined();
14 | window.setTimeout(done, 10);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------