├── .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 | [![Build Status](https://travis-ci.org/deiwin/ngImageInputWithPreview.png)](https://travis-ci.org/deiwin/ngImageInputWithPreview) 5 | [![Coverage Status](https://coveralls.io/repos/deiwin/ngImageInputWithPreview/badge.png?branch=master)](https://coveralls.io/r/deiwin/ngImageInputWithPreview?branch=master) 6 | [![devDependency Status](https://david-dm.org/deiwin/ngImageInputWithPreview/dev-status.svg)](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 |
48 | 53 | 54 | Not a JPEG or a PNG! 55 | 56 | 57 | Invalid dimensions! Expecting a landscape image smaller than 1800x400. 58 | 59 | 60 |
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 |
13 |

14 | myForm.myImage.$valid = {{myForm.myImage.$valid}} 15 |
16 | myForm.myImage.$error = {{myForm.myImage.$error}} 17 |
18 | myForm.$valid = {{myForm.$valid}} 19 |
20 | myForm.$error.image = {{!!myForm.$error.image}} 21 |
22 | myForm.$error.dimensions (height < 400) = {{!!myForm.$error.dimensions}} 23 |
24 | myForm.$error.size (size < 2097152) = {{!!myForm.$error.size}} 25 |
26 |

27 |

28 | 35 |
36 | 37 | Not a JPEG or a PNG! 38 | 39 |
40 | 41 | Image height should be smaller than 400! 42 | 43 |
44 | 45 | Image filesize should not be larger than 2MB! 46 | 47 |
48 |

49 | 50 |
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 | --------------------------------------------------------------------------------