├── .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 | [](https://travis-ci.org/rnicholus/file-input)
7 | [](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 |
36 |
37 |
55 |
56 |
57 |
58 |
59 |
60 |
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 | });
--------------------------------------------------------------------------------