├── .bowerrc ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── demo.html ├── file-input.html ├── file-input.js ├── grunt_tasks ├── jshint.js └── karma.js ├── gruntfile.js ├── index.html ├── package.json └── test └── unit └── file-input-spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "../" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | .c9 3 | coverage 4 | node_modules 5 | .idea -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": false, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "indent": 4, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": false, 14 | "nonew": true, 15 | "plusplus": false, 16 | "quotmark": "double", 17 | "undef": true, 18 | "unused": false, 19 | "strict": false, 20 | "trailing": false, 21 | "maxparams": 3, 22 | "maxdepth": 3, 23 | "asi": false, 24 | "boss": false, 25 | "eqnull": true, 26 | "evil": false, 27 | "expr": true, 28 | "funcscope": false, 29 | "globalstrict": false, 30 | "iterator": false, 31 | "lastsemic": false, 32 | "laxbreak": false, 33 | "laxcomma": false, 34 | "loopfunc": false, 35 | "multistr": false, 36 | "notypeof": false, 37 | "proto": false, 38 | "scripturl": false, 39 | "smarttabs": false, 40 | "shadow": false, 41 | "sub": true, 42 | "supernew": false, 43 | "predef": [ 44 | "afterEach", 45 | "ArrayBuffer", 46 | "atob", 47 | "beforeEach", 48 | "Blob", 49 | "console", 50 | "describe", 51 | "document", 52 | "expect", 53 | "jasmine", 54 | "navigator", 55 | "FrameGrab", 56 | "it", 57 | "RSVP", 58 | "spyOn", 59 | "Uint8Array", 60 | "window", 61 | "XMLHttpRequest" 62 | ] 63 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | addons: 2 | firefox: '40.0' 3 | language: node_js 4 | node_js: 5 | - '0.10' 6 | before_install: 7 | - npm install -g grunt-cli 8 | - export DISPLAY=:99.0 9 | - sh -e /etc/init.d/xvfb start 10 | script: 11 | - grunt travis 12 | env: 13 | global: 14 | secure: E+UHUxpP/b6A8Xp6o7Ef8HzJbgVtVgA9x/3hTp9FLBB83NfR6p1JJmoSvCgo50iZ2jBk/ZpjKV+LfAoF6cigF5slsDXGVSKB3YDnlSQVZHe6YK9qlNPs+YvRco8TK2ZKT/AfV6GD50hqSPG6iOjbGLSIobqNTMYbjAM87CyFdvI= 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 GarStasio 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 all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | file-input 2 | ========== 3 | 4 | A better ``. 5 | 6 | [![Build Status](https://travis-ci.org/rnicholus/file-input.svg?branch=master)](https://travis-ci.org/rnicholus/file-input) 7 | [![Coverage Status](https://coveralls.io/repos/garstasio/file-input/badge.png?branch=master)](https://coveralls.io/r/garstasio/file-input?branch=master) 8 | 9 | ## Installation 10 | 11 | `bower install file-input` 12 | 13 | ...or if you have a bower.json file with an entry for file-input: 14 | 15 | `bower update` 16 | 17 | See the [component page](http://file-input.raynicholus.com) for complete documentation and demos. 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-input", 3 | "version": "2.0.0", 4 | "authors": [ 5 | "Ray Nicholus" 6 | ], 7 | "description": "web component for styling, normalizing, and generally fixing ", 8 | "main": "element/*", 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "keywords": ["web-components", "input", "file", "upload"] 18 | } 19 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | file-input demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 |

Easily style a file chooser and set restrictions:

26 | 27 |

See the code for this demo in the demo.html file 28 | of the file-input GitHub repository.

29 | 30 | 31 | Select a file 32 | 33 | 34 |
35 |
36 |

We've restricted files to those with a "jpeg" or jpg" extension. Files also must be between 500 kB and 3 MB in size.

37 |
38 | 39 |

valid selected files:

40 |
41 | No files selected. 42 |
43 | 44 |

invalid selected files:

45 |
46 | No files selected. 47 |
48 | 49 | 50 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /file-input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 61 | 62 | 279 | -------------------------------------------------------------------------------- /file-input.js: -------------------------------------------------------------------------------- 1 | // jshint maxparams:4 2 | /*global HTMLElement, CustomEvent*/ 3 | var fileInput = (function() { 4 | var insertIntoDocument = (function () { 5 | "use strict"; 6 | var importDoc; 7 | 8 | importDoc = (document._currentScript || document.currentScript).ownerDocument; 9 | 10 | return function (obj, idTemplate) { 11 | var template = importDoc.getElementById(idTemplate), 12 | clone = document.importNode(template.content, true); 13 | 14 | obj.appendChild(clone); 15 | }; 16 | }()), 17 | declaredProps = (function () { 18 | "use strict"; 19 | var exports = {}; 20 | 21 | function parse(val, type) { 22 | switch (type) { 23 | case Number: 24 | return parseFloat(val || 0, 10); 25 | case Boolean: 26 | return val !== null; 27 | case Object: 28 | case Array: 29 | return JSON.parse(val); 30 | case Date: 31 | return new Date(val); 32 | default: 33 | return val || ""; 34 | } 35 | } 36 | function toHyphens(str) { 37 | return str.replace(/([A-Z])/g, "-$1").toLowerCase(); 38 | } 39 | function toCamelCase(str) { 40 | return str.split("-") 41 | .map(function (x, i) { 42 | return i === 0 ? x : x[0].toUpperCase() + x.slice(1); 43 | }).join(""); 44 | } 45 | exports.serialize = function (val) { 46 | if (typeof val === "string") { 47 | return val; 48 | } 49 | if (typeof val === "number" || val instanceof Date) { 50 | return val.toString(); 51 | } 52 | return JSON.stringify(val); 53 | }; 54 | 55 | exports.syncProperty = function (obj, props, attr, val) { 56 | var name = toCamelCase(attr), type; 57 | if (props[name]) { 58 | type = props[name].type || props[name]; 59 | obj[name] = parse(val, type); 60 | } 61 | }; 62 | 63 | exports.init = function (obj, props) { 64 | Object.defineProperty(obj, "props", { 65 | enumerable : false, 66 | configurable : true, 67 | value : {} 68 | }); 69 | 70 | Object.keys(props).forEach(function (name) { 71 | var attrName = toHyphens(name), desc, value; 72 | 73 | desc = props[name].type ? props[name] : { type : props[name] }; 74 | value = typeof desc.value === "function" ? desc.value() : desc.value; 75 | obj.props[name] = obj[name] || value; 76 | 77 | if (obj.getAttribute(attrName) === null) { 78 | if (desc.reflectToAttribute) { 79 | obj.setAttribute(attrName, exports.serialize(obj.props[name])); 80 | } 81 | } else { 82 | obj.props[name] = parse(obj.getAttribute(attrName), desc.type); 83 | } 84 | Object.defineProperty(obj, name, { 85 | get : function () { 86 | return obj.props[name] || parse(obj.getAttribute(attrName), desc.type); 87 | }, 88 | set : function (val) { 89 | var old = obj.props[name]; 90 | obj.props[name] = val; 91 | if (desc.reflectToAttribute) { 92 | if (desc.type === Boolean) { 93 | if (val) { 94 | obj.setAttribute(attrName, ""); 95 | } else { 96 | obj.removeAttribute(attrName); 97 | } 98 | } else { 99 | obj.setAttribute(attrName, exports.serialize(val)); 100 | } 101 | } 102 | if (typeof obj[desc.observer] === "function") { 103 | obj[desc.observer](val, old); 104 | } 105 | } 106 | }); 107 | }); 108 | }; 109 | 110 | return exports; 111 | }()), 112 | 113 | arrayOf = function(pseudoArray) { 114 | return Array.prototype.slice.call(pseudoArray); 115 | }, 116 | 117 | getLowerCaseExtension = function(filename) { 118 | var extIdx = filename.lastIndexOf(".") + 1; 119 | 120 | if (extIdx > 0) { 121 | return filename.substr(extIdx, filename.length - extIdx).toLowerCase(); 122 | } 123 | }, 124 | 125 | getResultOfCountLimitValidation = function(limit, files) { 126 | if (limit > 0 && limit < files.length) { 127 | return { 128 | invalid: files.slice(limit, files.length), 129 | valid: files.slice(0, limit) 130 | }; 131 | } 132 | 133 | return {invalid: [], valid: files}; 134 | }, 135 | 136 | getResultOfExtensionsValidation = function(extensionsStr, files) { 137 | if (extensionsStr) { 138 | var negate = extensionsStr.charAt(0) === "!", 139 | extensions = JSON.parse(extensionsStr.toLowerCase().substr(negate ? 1 : 0)), 140 | result = {invalid: [], valid: []}; 141 | 142 | files.forEach(function(file) { 143 | var extension = getLowerCaseExtension(file.name); 144 | 145 | if (extensions.indexOf(extension) >= 0) { 146 | result[negate ? "invalid" : "valid"].push(file); 147 | } 148 | else { 149 | result[negate? "valid" : "invalid"].push(file); 150 | } 151 | }); 152 | 153 | return result; 154 | } 155 | 156 | return {invalid: [], valid: files}; 157 | }, 158 | 159 | getResultOfSizeValidation = function(minSize, maxSize, files) { 160 | if (!minSize && !maxSize) { 161 | return {tooBig: [], tooSmall: [], valid: files}; 162 | } 163 | 164 | var valid = [], 165 | tooBig = [], 166 | tooSmall = []; 167 | 168 | files.forEach(function(file) { 169 | if (minSize && file.size < minSize) { 170 | tooSmall.push(file); 171 | } 172 | else if (maxSize && file.size > maxSize) { 173 | tooBig.push(file); 174 | } 175 | else { 176 | valid.push(file); 177 | } 178 | }); 179 | 180 | return {tooBig: tooBig, tooSmall: tooSmall, valid: valid}; 181 | }, 182 | 183 | isIos = function() { 184 | return navigator.userAgent.indexOf("iPad") !== -1 || 185 | navigator.userAgent.indexOf("iPod") !== -1 || 186 | navigator.userAgent.indexOf("iPhone") !== -1; 187 | }, 188 | 189 | // This is the only way (I am aware of) to reset an `` 190 | // without removing it from the DOM. Removing it disconnects it 191 | // from the CE. 192 | resetInput = function(customEl) { 193 | // create a form with a hidden reset button 194 | var tempForm = document.createElement("form"), 195 | fileInput = customEl.querySelector(".fileInput"), 196 | tempResetButton = document.createElement("button"); 197 | 198 | tempResetButton.setAttribute("type", "reset"); 199 | tempResetButton.style.display = "none"; 200 | tempForm.appendChild(tempResetButton); 201 | 202 | // temporarily move the `` into the form & add form to DOM 203 | fileInput.parentNode.insertBefore(tempForm, fileInput); 204 | tempForm.appendChild(fileInput); 205 | 206 | // reset the `` 207 | tempResetButton.click(); 208 | 209 | // move the `` back to its original spot & remove form 210 | tempForm.parentNode.appendChild(fileInput); 211 | tempForm.parentNode.removeChild(tempForm); 212 | 213 | customEl.files = []; 214 | customEl.invalid = {count: 0}; 215 | customEl.valid = []; 216 | 217 | updateValidity(customEl); 218 | }, 219 | 220 | setupValidationTarget = function(customEl) { 221 | validationTarget = document.createElement("input"); 222 | validationTarget.setAttribute("tabindex", "-1"); 223 | validationTarget.setAttribute("type", "text"); 224 | 225 | // Strange margin/padding needed to ensure some browsers 226 | // don't hide the validation message immediately after it 227 | // appears (Chrome at this time) 228 | validationTarget.style.padding = "1px"; 229 | validationTarget.style.margin = "-1px"; 230 | 231 | validationTarget.style.border = 0; 232 | validationTarget.style.height = 0; 233 | validationTarget.style.opacity = 0; 234 | validationTarget.style.width = 0; 235 | 236 | validationTarget.className = "fileInputDelegate"; 237 | 238 | validationTarget.customElementRef = customEl; 239 | 240 | customEl.parentNode.insertBefore(validationTarget, customEl); 241 | 242 | updateValidity(customEl); 243 | }, 244 | 245 | updateValidity = function(customEl) { 246 | if (validationTarget) { 247 | if (customEl.files.length) { 248 | validationTarget.setCustomValidity(""); 249 | } 250 | else { 251 | validationTarget.setCustomValidity(customEl.invalidText); 252 | } 253 | } 254 | }, 255 | 256 | validationTarget, 257 | 258 | properties = { 259 | accept : { 260 | type : String, 261 | observer : "setAccept" 262 | }, 263 | camera : Boolean, 264 | directory : { 265 | type: Boolean, 266 | value: false, 267 | observer: "setDirectory" 268 | }, 269 | extensions : { 270 | type : String //JSON array 271 | }, 272 | maxFiles : { 273 | type : Number, 274 | value : 0, 275 | observer : "setMaxFiles" 276 | }, 277 | maxSize : { 278 | type : Number, 279 | value : 0 280 | }, 281 | minSize : { 282 | type : Number, 283 | value : 0 284 | }, 285 | required: { 286 | type : Boolean, 287 | value: false 288 | } 289 | }; 290 | 291 | var fileInputPrototype = Object.create(HTMLElement.prototype); 292 | fileInputPrototype.changeHandler = function(event) { 293 | event.stopPropagation(); 294 | 295 | var customEl = this, 296 | fileInput = customEl.querySelector(".fileInput"), 297 | files = arrayOf(fileInput.files), 298 | invalid = {count: 0}, 299 | valid = []; 300 | 301 | // Some browsers may fire a change event when the file chooser 302 | // dialog is closed via cancel button. In this case, the 303 | //files array will be empty and the event should be ignored. 304 | if (files.length) { 305 | var sizeValidationResult = getResultOfSizeValidation(customEl.minSize, customEl.maxSize, files); 306 | var extensionValidationResult = getResultOfExtensionsValidation(customEl.extensions, sizeValidationResult.valid); 307 | var countLimitValidationResult = getResultOfCountLimitValidation(customEl.maxFiles, extensionValidationResult.valid); 308 | 309 | if (sizeValidationResult.tooBig.length) { 310 | invalid.tooBig = sizeValidationResult.tooBig; 311 | invalid.count += sizeValidationResult.tooBig.length; 312 | } 313 | if (sizeValidationResult.tooSmall.length) { 314 | invalid.tooSmall = sizeValidationResult.tooSmall; 315 | invalid.count += sizeValidationResult.tooSmall.length; 316 | } 317 | if (extensionValidationResult.invalid.length) { 318 | invalid.badExtension = extensionValidationResult.invalid; 319 | invalid.count += extensionValidationResult.invalid.length; 320 | } 321 | if (countLimitValidationResult.invalid.length) { 322 | invalid.tooMany = countLimitValidationResult.invalid; 323 | invalid.count += countLimitValidationResult.invalid.length; 324 | } 325 | 326 | valid = countLimitValidationResult.valid; 327 | 328 | customEl.invalid = invalid; 329 | customEl.files = valid; 330 | 331 | updateValidity(customEl); 332 | customEl.dispatchEvent(new CustomEvent("change", { detail : {invalid: invalid, valid: valid} })); 333 | } 334 | }; 335 | 336 | 337 | fileInputPrototype.invalidText = "No valid files selected."; 338 | 339 | fileInputPrototype.setAccept = function (val) { 340 | var fileInput = this.querySelector(".fileInput"); 341 | fileInput.setAttribute("accept", val); 342 | }; 343 | 344 | fileInputPrototype.setDirectory = function(val) { 345 | var fileInput = this.querySelector(".fileInput"); 346 | if (val && fileInput.webkitdirectory !== undefined) { 347 | fileInput.setAttribute("webkitdirectory", ""); 348 | } 349 | else { 350 | fileInput.removeAttribute("webkitdirectory"); 351 | } 352 | }; 353 | 354 | fileInputPrototype.setMaxFiles = function (val) { 355 | var fileInput = this.querySelector(".fileInput"); 356 | if (val !== 1) { 357 | fileInput.setAttribute("multiple", ""); 358 | } 359 | else { 360 | fileInput.removeAttribute("multiple"); 361 | } 362 | }; 363 | 364 | fileInputPrototype.attributeChangedCallback = function(attr, oldVal, newVal) { 365 | declaredProps.syncProperty(this, properties, attr, newVal); 366 | }; 367 | 368 | fileInputPrototype.createdCallback = function() { 369 | var fileInput, customEl = this; 370 | 371 | insertIntoDocument(this, "file-input"); 372 | declaredProps.init(this, properties); 373 | 374 | this.setAccept(this.accept); 375 | 376 | fileInput = customEl.querySelector(".fileInput"); 377 | fileInput.addEventListener("change", this.changeHandler.bind(this)); 378 | 379 | customEl.files = []; 380 | customEl.invalid = {count: 0}; 381 | 382 | if (customEl.camera && isIos()) { 383 | customEl.maxFiles = 1; 384 | 385 | var iosCameraAccept = "image/*;capture=camera"; 386 | if (customEl.accept && customEl.accept.length.trim().length > 0) { 387 | customEl.accept += "," + iosCameraAccept; 388 | } 389 | else { 390 | customEl.accept = iosCameraAccept; 391 | } 392 | } 393 | 394 | this.setMaxFiles(customEl.maxFiles); 395 | this.setDirectory(customEl.directory); 396 | 397 | if (customEl.required) { 398 | setupValidationTarget(customEl); 399 | } 400 | }; 401 | 402 | fileInputPrototype.reset = function() { 403 | var customEl = this; 404 | 405 | resetInput(customEl); 406 | }; 407 | 408 | return fileInputPrototype; 409 | }()); 410 | -------------------------------------------------------------------------------- /grunt_tasks/jshint.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* globals module */ 3 | module.exports = { 4 | files: [ 5 | "file-input.js", 6 | "gruntfile.js", 7 | "grunt_tasks/*.js", 8 | "test/unit/*.js" 9 | ], 10 | options: { 11 | jshintrc: true 12 | } 13 | }; -------------------------------------------------------------------------------- /grunt_tasks/karma.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | /* globals module */ 3 | module.exports = { 4 | options: { 5 | autoWatch : false, 6 | 7 | basePath : ".", 8 | 9 | browserNoActivityTimeout: 300000, 10 | 11 | files : [ 12 | "node_modules/webcomponents.js/webcomponents-lite.js", 13 | "file-input.js", 14 | "file-input.html", 15 | "test/unit/*-spec.js" 16 | ], 17 | 18 | frameworks: ["jasmine"], 19 | 20 | plugins : [ 21 | "karma-coverage", 22 | "karma-coveralls", 23 | "karma-firefox-launcher", 24 | "karma-jasmine", 25 | "karma-spec-reporter" 26 | ], 27 | 28 | preprocessors: { 29 | "file-input.js": "coverage" 30 | }, 31 | 32 | reporters : [ 33 | "spec", 34 | "coverage", 35 | "coveralls" 36 | ], 37 | 38 | coverageReporter: { 39 | type: "lcov", // lcov or lcovonly are required for generating lcov.info files 40 | dir: "coverage/" 41 | }, 42 | 43 | singleRun: true 44 | 45 | }, 46 | dev: { 47 | browsers: ["Firefox"] 48 | }, 49 | travis: { 50 | browsers: ["Firefox"] 51 | } 52 | }; -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | function config(name) { 3 | return require("./grunt_tasks/" + name + ".js"); 4 | } 5 | 6 | module.exports = function(grunt) { 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON("package.json"), 9 | jshint: config("jshint"), 10 | karma: config("karma") 11 | }); 12 | 13 | grunt.loadNpmTasks("grunt-contrib-jshint"); 14 | grunt.loadNpmTasks("grunt-karma"); 15 | 16 | grunt.registerTask("default", ["jshint", "karma:dev"]); 17 | grunt.registerTask("travis", ["jshint", "karma:travis"]); 18 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Ray Nicholus" 4 | }, 5 | "bugs": "https://github.com/garstasio/file-input/issues", 6 | "description": "web component for styling, normalizing, and generally fixing ", 7 | "devDependencies": { 8 | "grunt": "0.4.x", 9 | "grunt-contrib-jshint": "0.10.x", 10 | "grunt-karma": "~0.8.2", 11 | "karma": "~0.12.0", 12 | "karma-coverage": "0.2.x", 13 | "karma-coveralls": "0.1.x", 14 | "karma-firefox-launcher": "~0.1.2", 15 | "karma-jasmine": "~0.2.0", 16 | "karma-spec-reporter": "0.0.13", 17 | "webcomponents.js": "0.7.2" 18 | }, 19 | "license": "MIT", 20 | "name": "file-input", 21 | "repository": { 22 | "type" : "git", 23 | "url" : "https://github.com/garstasio/file-input.git" 24 | }, 25 | "version": "2.0.0" 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/file-input-spec.js: -------------------------------------------------------------------------------- 1 | /* globals CustomEvent */ 2 | describe("file-input custom element tests", function() { 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 4 | 5 | var loadFileInput = function() { 6 | var fileInputEl = document.createElement("file-input"); 7 | document.body.appendChild(fileInputEl); 8 | return fileInputEl; 9 | }, 10 | removeFileInput = function() { 11 | var fileInputEl = document.querySelector("file-input"); 12 | fileInputEl && fileInputEl.parentNode.removeChild(fileInputEl); 13 | }; 14 | 15 | afterEach(function() { 16 | removeFileInput(); 17 | }); 18 | 19 | describe("initialization tests", function() { 20 | it("initializes objects & arrays in the 'created' callback", function(done) { 21 | var fileInputEl = loadFileInput(); 22 | 23 | window.addEventListener("WebComponentsReady", function(e) { 24 | expect(fileInputEl.files).toEqual([]); 25 | expect(fileInputEl.invalid).toEqual({count: 0}); 26 | done(); 27 | }); 28 | }); 29 | 30 | it("doesn't set the multiple attr if maxFiles === 1", function() { 31 | var fileInputEl = loadFileInput(); 32 | fileInputEl.maxFiles = 1; 33 | expect(fileInputEl.hasAttribute("multiple")).toBeFalsy(); 34 | }); 35 | 36 | it("does set the multiple attr if maxFiles === 0", function() { 37 | var fileInputEl = loadFileInput(); 38 | fileInputEl.maxFiles = 0; 39 | expect(fileInputEl.querySelector(".fileInput").hasAttribute("multiple")).toBeTruthy(); 40 | }); 41 | 42 | it("does set the multiple attr if maxFiles > 1", function() { 43 | var fileInputEl = loadFileInput(); 44 | fileInputEl.maxFiles = 2; 45 | expect(fileInputEl.querySelector(".fileInput").hasAttribute("multiple")).toBeTruthy(); 46 | }); 47 | 48 | it("enables directory selection only if requested & supported by UA", function() { 49 | var fileInputEl = loadFileInput(); 50 | 51 | // fake file-input into thinking directory selection is supported; 52 | fileInputEl.querySelector(".fileInput").webkitdirectory = null; 53 | 54 | expect(fileInputEl.querySelector(".fileInput").hasAttribute("webkitdirectory")).toBeFalsy(); 55 | 56 | fileInputEl.directory = false; 57 | expect(fileInputEl.querySelector(".fileInput").hasAttribute("webkitdirectory")).toBeFalsy(); 58 | 59 | fileInputEl.directory = true; 60 | expect(fileInputEl.querySelector(".fileInput").hasAttribute("webkitdirectory")).toBeTruthy(); 61 | }); 62 | }); 63 | 64 | describe("reset tests", function() { 65 | it("resets the file arrays on reset", function() { 66 | var fileInputEl = loadFileInput(); 67 | 68 | fileInputEl.files = [1,2,3]; 69 | fileInputEl.invalid = {count: 1, tooBig: [4]}; 70 | 71 | fileInputEl.reset(); 72 | 73 | expect(fileInputEl.files).toEqual([]); 74 | expect(fileInputEl.invalid).toEqual({count: 0}); 75 | }); 76 | }); 77 | 78 | describe("validation tests", function() { 79 | it("doesn't reject any files if no validation rules are present, coverts psuedo-array of files to 'real' Array, & passes this info to event handler as well", function() { 80 | var fileInputEl = loadFileInput(), 81 | expectedValid = [ 82 | {name: "pic.jpg", size: 1000}, 83 | {name: "plain.txt", size: 2000} 84 | ]; 85 | 86 | spyOn(fileInputEl, "querySelector").and.returnValue({ 87 | files: { 88 | "0": expectedValid[0], 89 | "1": expectedValid[1], 90 | length: 2 91 | } 92 | }); 93 | 94 | spyOn(fileInputEl, "dispatchEvent"); 95 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 96 | expect(fileInputEl.dispatchEvent).toHaveBeenCalledWith(new CustomEvent("change", { detail : {count: 0}, valid: expectedValid})); 97 | expect(fileInputEl.files).toEqual(expectedValid); 98 | expect(fileInputEl.invalid).toEqual({count: 0}); 99 | }); 100 | 101 | it("ignores native change event if no files were selected", function() { 102 | var fileInputEl = loadFileInput(); 103 | 104 | fileInputEl.files = [1, 2]; 105 | 106 | spyOn(fileInputEl, "querySelector").and.returnValue({ 107 | files: { 108 | length: 0 109 | } 110 | }); 111 | 112 | spyOn(fileInputEl, "dispatchEvent"); 113 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 114 | 115 | expect(fileInputEl.dispatchEvent).not.toHaveBeenCalled(); 116 | expect(fileInputEl.files).toEqual([1, 2]); 117 | }); 118 | 119 | it("rejects files that are too big or too small", function() { 120 | var fileInputEl = loadFileInput(), 121 | expectedValid = [ 122 | {name: "plain.txt", size: 2000} 123 | ], 124 | expectedInvalid = { 125 | count: 2, 126 | 127 | tooBig: [ 128 | {name: "foo.bar", size: 3000} 129 | ], 130 | 131 | tooSmall: [ 132 | {name: "pic.jpg", size: 1000} 133 | ] 134 | }; 135 | 136 | spyOn(fileInputEl, "querySelector").and.returnValue({ 137 | files: [ 138 | {name: "pic.jpg", size: 1000}, 139 | {name: "plain.txt", size: 2000}, 140 | {name: "foo.bar", size: 3000} 141 | ] 142 | }); 143 | 144 | fileInputEl.maxSize = 2500; 145 | fileInputEl.minSize = 1500; 146 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 147 | 148 | expect(fileInputEl.files).toEqual(expectedValid); 149 | expect(fileInputEl.invalid).toEqual(expectedInvalid); 150 | }); 151 | 152 | it("rejects files with an invalid extension", function() { 153 | var fileInputEl = loadFileInput(), 154 | expectedValid = [ 155 | {name: "pic.jpg", size: 1000} 156 | ], 157 | expectedInvalid = { 158 | count: 2, 159 | 160 | badExtension: [ 161 | {name: "plain.txt", size: 2000}, 162 | {name: "foo.bar", size: 3000} 163 | ] 164 | }; 165 | 166 | spyOn(fileInputEl, "querySelector").and.returnValue({ 167 | files: [ 168 | {name: "pic.jpg", size: 1000}, 169 | {name: "plain.txt", size: 2000}, 170 | {name: "foo.bar", size: 3000} 171 | ] 172 | }); 173 | 174 | /* jshint quotmark:false */ 175 | fileInputEl.extensions = '["jpg"]'; 176 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 177 | 178 | expect(fileInputEl.files).toEqual(expectedValid); 179 | expect(fileInputEl.invalid).toEqual(expectedInvalid); 180 | }); 181 | 182 | it("rejects files with an invalid extension (negated)", function() { 183 | var fileInputEl = loadFileInput(), 184 | expectedValid = [ 185 | {name: "plain.txt", size: 2000}, 186 | {name: "foo.bar", size: 3000} 187 | ], 188 | expectedInvalid = { 189 | count: 1, 190 | 191 | badExtension: [ 192 | {name: "pic.jpg", size: 1000} 193 | ] 194 | }; 195 | 196 | spyOn(fileInputEl, "querySelector").and.returnValue({ 197 | files: [ 198 | {name: "pic.jpg", size: 1000}, 199 | {name: "plain.txt", size: 2000}, 200 | {name: "foo.bar", size: 3000} 201 | ] 202 | }); 203 | 204 | /* jshint quotmark:false */ 205 | fileInputEl.extensions = '!["jpg"]'; 206 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 207 | 208 | expect(fileInputEl.files).toEqual(expectedValid); 209 | expect(fileInputEl.invalid).toEqual(expectedInvalid); 210 | }); 211 | 212 | it("rejects files passed the maxFiles limit", function() { 213 | var fileInputEl = loadFileInput(), 214 | expectedValid = [ 215 | {name: "pic.jpg", size: 1000} 216 | ], 217 | expectedInvalid = { 218 | count: 2, 219 | 220 | tooMany: [ 221 | {name: "plain.txt", size: 2000}, 222 | {name: "foo.bar", size: 3000} 223 | ] 224 | }; 225 | 226 | spyOn(fileInputEl, "querySelector").and.returnValue({ 227 | files: [ 228 | {name: "pic.jpg", size: 1000}, 229 | {name: "plain.txt", size: 2000}, 230 | {name: "foo.bar", size: 3000} 231 | ], 232 | removeAttribute: function() {} 233 | }); 234 | 235 | /* jshint quotmark:false */ 236 | fileInputEl.maxFiles = 1; 237 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 238 | 239 | expect(fileInputEl.files).toEqual(expectedValid); 240 | expect(fileInputEl.invalid).toEqual(expectedInvalid); 241 | }); 242 | 243 | it("respects all validation rules at once in the proper order", function() { 244 | var fileInputEl = loadFileInput(), 245 | expectedValid = [ 246 | {name: "pic.jpg", size: 1000}, 247 | {name: "pic2.jpg", size: 1000}, 248 | {name: "pic3.jpg", size: 1000}, 249 | ], 250 | expectedInvalid = { 251 | count: 5, 252 | 253 | badExtension: [ 254 | {name: "plain.txt", size: 2000}, 255 | {name: "foo.bar", size: 3000} 256 | ], 257 | 258 | tooBig: [ 259 | {name: "pi5.jpg", size: 9999}, 260 | ], 261 | 262 | tooMany: [ 263 | {name: "pic4.jpg", size: 1000}, 264 | {name: "pic6.jpg", size: 1000}, 265 | ] 266 | }; 267 | 268 | spyOn(fileInputEl, "querySelector").and.returnValue({ 269 | files: [ 270 | {name: "pic.jpg", size: 1000}, 271 | {name: "pic2.jpg", size: 1000}, 272 | {name: "pic3.jpg", size: 1000}, 273 | {name: "pic4.jpg", size: 1000}, 274 | {name: "pi5.jpg", size: 9999}, 275 | {name: "pic6.jpg", size: 1000}, 276 | {name: "plain.txt", size: 2000}, 277 | {name: "foo.bar", size: 3000} 278 | ], 279 | setAttribute: function() {} 280 | }); 281 | 282 | /* jshint quotmark:false */ 283 | fileInputEl.extensions = '["jpg"]'; 284 | fileInputEl.maxFiles = 3; 285 | fileInputEl.maxSize = 8000; 286 | fileInputEl.changeHandler.call(fileInputEl, {stopPropagation: function(){}}); 287 | 288 | expect(fileInputEl.files).toEqual(expectedValid); 289 | expect(fileInputEl.invalid).toEqual(expectedInvalid); 290 | }); 291 | 292 | it("marks the element as invalid on load if `required` attribute exists", function(done) { 293 | var fileInputElParent = document.createElement("div"), 294 | delegateInputEl; 295 | 296 | spyOn(fileInputElParent, "insertBefore").and.callFake(function(delegateInput) { 297 | delegateInputEl = delegateInput; 298 | 299 | expect(delegateInput.tagName.toLowerCase()).toEqual("input"); 300 | expect(delegateInput.validity.valid).toBe(true); 301 | expect(delegateInputEl.customElementRef).toEqual(fileInputElParent.children[0]); 302 | window.setTimeout(function() { 303 | expect(delegateInput.validity.valid).toBe(false); 304 | done(); 305 | }, 100); 306 | }); 307 | 308 | fileInputElParent.insertAdjacentHTML("afterbegin", ""); 309 | document.body.appendChild(fileInputElParent); 310 | }); 311 | 312 | it("marks the element as valid on load if `required` attribute exists once it is truly valid", function(done) { 313 | var fileInputElParent = document.createElement("div"), 314 | delegateInputEl; 315 | 316 | spyOn(fileInputElParent, "insertBefore").and.callFake(function(delegateInput) { 317 | delegateInputEl = delegateInput; 318 | 319 | fileInputElParent.children[0].files = [ 320 | {name: "pic.jpg", size: 1000} 321 | ]; 322 | window.setTimeout(function() { 323 | expect(delegateInput.validity.valid).toBe(true); 324 | done(); 325 | }, 100); 326 | }); 327 | 328 | fileInputElParent.insertAdjacentHTML("afterbegin", ""); 329 | document.body.appendChild(fileInputElParent); 330 | }); 331 | }); 332 | }); --------------------------------------------------------------------------------