├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── src ├── Html5FileSelector.js └── index.js ├── test └── Html5FileSelctorSpec.js └── tools └── build.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true, 5 | 'node': true 6 | }, 7 | /* 'parser': 'Babel-ESLint', */ 8 | 'parserOptions': { 9 | 'sourceType': 'module' 10 | }, 11 | 'ecmaFeatures': { 12 | 'arrowFunctions': true, 13 | 'blockBindings': true, 14 | 'classes': true, 15 | 'defaultParams': true, 16 | 'destructuring': true, 17 | 'forOf': true, 18 | 'generators': false, 19 | 'modules': true, 20 | 'objectLiteralComputedProperties': true, 21 | 'objectLiteralDuplicateProperties': false, 22 | 'objectLiteralShorthandMethods': true, 23 | 'objectLiteralShorthandProperties': true, 24 | 'spread': true, 25 | 'superInFunctions': true, 26 | 'templateStrings': true, 27 | 'jsx': true 28 | }, 29 | 'rules': { 30 | // Enforces getter/setter pairs in objects 31 | 'accessor-pairs': 0, 32 | // treat var statements as if they were block scoped 33 | 'block-scoped-var': 0, 34 | // specify the maximum cyclomatic complexity allowed in a program 35 | 'complexity': [0, 11], 36 | // require return statements to either always or never specify values 37 | 'consistent-return': 2, 38 | // specify curly brace conventions for all control statements 39 | 'curly': [2, 'multi-line'], 40 | // require default case in switch statements 41 | 'default-case': 2, 42 | // encourages use of dot notation whenever possible 43 | 'dot-notation': [2, { 'allowKeywords': true}], 44 | // enforces consistent newlines before or after dots 45 | 'dot-location': 0, 46 | // require the use of === and !== 47 | 'eqeqeq': 2, 48 | // make sure for-in loops have an if statement 49 | 'guard-for-in': 2, 50 | // disallow the use of alert, confirm, and prompt 51 | 'no-alert': 2, 52 | // disallow use of arguments.caller or arguments.callee 53 | 'no-caller': 2, 54 | // disallow division operators explicitly at beginning of regular expression 55 | 'no-div-regex': 0, 56 | // disallow else after a return in an if 57 | 'no-else-return': 2, 58 | // disallow comparisons to null without a type-checking operator 59 | 'no-eq-null': 0, 60 | // disallow use of eval() 61 | 'no-eval': 2, 62 | // disallow adding to native types 63 | 'no-extend-native': 2, 64 | // disallow unnecessary function binding 65 | 'no-extra-bind': 2, 66 | // disallow fallthrough of case statements 67 | 'no-fallthrough': 2, 68 | // disallow the use of leading or trailing decimal points in numeric literals 69 | 'no-floating-decimal': 2, 70 | // disallow the type conversions with shorter notations 71 | 'no-implicit-coercion': 0, 72 | // disallow use of eval()-like methods 73 | 'no-implied-eval': 2, 74 | // disallow this keywords outside of classes or class-like objects 75 | 'no-invalid-this': 0, 76 | // disallow usage of __iterator__ property 77 | 'no-iterator': 2, 78 | // disallow use of labeled statements 79 | 'no-labels': 2, 80 | // disallow unnecessary nested blocks 81 | 'no-lone-blocks': 2, 82 | // disallow creation of functions within loops 83 | 'no-loop-func': 2, 84 | // disallow use of multiple spaces 85 | 'no-multi-spaces': 2, 86 | // disallow use of multiline strings 87 | 'no-multi-str': 2, 88 | // disallow reassignments of native objects 89 | 'no-native-reassign': 2, 90 | // disallow use of new operator when not part of the assignment or comparison 91 | 'no-new': 2, 92 | // disallow use of new operator for Function object 93 | 'no-new-func': 2, 94 | // disallows creating new instances of String,Number, and Boolean 95 | 'no-new-wrappers': 2, 96 | // disallow use of (old style) octal literals 97 | 'no-octal': 2, 98 | // disallow use of octal escape sequences in string literals, such as 99 | // var foo = 'Copyright \251'; 100 | 'no-octal-escape': 2, 101 | // disallow reassignment of function parameters 102 | 'no-param-reassign': 2, 103 | // disallow use of process.env 104 | 'no-process-env': 0, 105 | // disallow usage of __proto__ property 106 | 'no-proto': 2, 107 | // disallow declaring the same variable more then once 108 | 'no-redeclare': 2, 109 | // disallow use of assignment in return statement 110 | 'no-return-assign': 2, 111 | // disallow use of javascript: urls. 112 | 'no-script-url': 2, 113 | // disallow comparisons where both sides are exactly the same 114 | 'no-self-compare': 2, 115 | // disallow use of comma operator 116 | 'no-sequences': 2, 117 | // restrict what can be thrown as an exception 118 | 'no-throw-literal': 2, 119 | // disallow usage of expressions in statement position 120 | 'no-unused-expressions': 2, 121 | // disallow unnecessary .call() and .apply() 122 | 'no-useless-call': 0, 123 | // disallow use of void operator 124 | 'no-void': 0, 125 | // disallow usage of configurable warning terms in comments: e.g. todo 126 | 'no-warning-comments': [0, { 'terms': ['todo', 'fixme', 'xxx'], 'location': 'start' }], 127 | // disallow use of the with statement 128 | 'no-with': 2, 129 | // require use of the second argument for parseInt() 130 | 'radix': 2, 131 | // requires to declare all vars on top of their containing scope 132 | 'vars-on-top': 2, 133 | // require immediate function invocation to be wrapped in parentheses 134 | 'wrap-iife': [2, 'any'], 135 | // require or disallow Yoda conditions 136 | 'yoda': 2, 137 | // disallow trailing commas in object literals 138 | 'comma-dangle': [0, 'always-multiline'], 139 | // disallow assignment in conditional expressions 140 | 'no-cond-assign': [2, 'always'], 141 | // disallow use of console 142 | 'no-console': 1, 143 | // disallow use of constant expressions in conditions 144 | 'no-constant-condition': 1, 145 | // disallow control characters in regular expressions 146 | 'no-control-regex': 2, 147 | // disallow use of debugger 148 | 'no-debugger': 1, 149 | // disallow duplicate arguments in functions 150 | 'no-dupe-args': 2, 151 | // disallow duplicate keys when creating object literals 152 | 'no-dupe-keys': 2, 153 | // disallow a duplicate case label. 154 | 'no-duplicate-case': 2, 155 | // disallow the use of empty character classes in regular expressions 156 | 'no-empty-character-class': 2, 157 | // disallow empty statements 158 | 'no-empty': 2, 159 | // disallow assigning to the exception in a catch block 160 | 'no-ex-assign': 2, 161 | // disallow double-negation boolean casts in a boolean context 162 | 'no-extra-boolean-cast': 0, 163 | // disallow unnecessary parentheses 164 | 'no-extra-parens': [2, 'functions'], 165 | // disallow unnecessary semicolons 166 | 'no-extra-semi': 2, 167 | // disallow overwriting functions written as function declarations 168 | 'no-func-assign': 2, 169 | // disallow function or variable declarations in nested blocks 170 | 'no-inner-declarations': 2, 171 | // disallow invalid regular expression strings in the RegExp constructor 172 | 'no-invalid-regexp': 2, 173 | // disallow irregular whitespace outside of strings and comments 174 | 'no-irregular-whitespace': 2, 175 | // disallow negation of the left operand of an in expression 176 | 'no-negated-in-lhs': 2, 177 | // disallow the use of object properties of the global object (Math and JSON) as functions 178 | 'no-obj-calls': 2, 179 | // disallow multiple spaces in a regular expression literal 180 | 'no-regex-spaces': 2, 181 | // disallow sparse arrays 182 | 'no-sparse-arrays': 2, 183 | // disallow unreachable statements after a return, throw, continue, or break statement 184 | 'no-unreachable': 2, 185 | // disallow comparisons with the value NaN 186 | 'use-isnan': 2, 187 | // ensure JSDoc comments are valid 188 | 'valid-jsdoc': 0, 189 | // ensure that the results of typeof are compared against a valid string 190 | 'valid-typeof': 2, 191 | // Avoid code that looks like two expressions but is actually one 192 | 'no-unexpected-multiline': 0, 193 | // require parens in arrow function arguments 194 | 'arrow-parens': 0, 195 | // require space before/after arrow function's arrow 196 | 'arrow-spacing': 0, 197 | // verify super() callings in constructors 198 | 'constructor-super': 0, 199 | // enforce the spacing around the * in generator functions 200 | 'generator-star-spacing': 0, 201 | // disallow modifying variables of class declarations 202 | 'no-class-assign': 0, 203 | // disallow modifying variables that are declared using const 204 | /* 'no-const-assign': 2, */ 205 | // disallow to use this/super before super() calling in constructors. 206 | 'no-this-before-super': 0, 207 | // require let or const instead of var 208 | 'no-var': 2, 209 | // require method and property shorthand syntax for object literals 210 | 'object-shorthand': 0, 211 | // suggest using of const declaration for variables that are never modified after declared 212 | 'prefer-const': 2, 213 | // suggest using the spread operator instead of .apply() 214 | 'prefer-spread': 0, 215 | // suggest using Reflect methods where applicable 216 | 'prefer-reflect': 0, 217 | // disallow generator functions that do not have yield 218 | 'require-yield': 0, 219 | // specify the maximum depth that blocks can be nested 220 | 'max-depth': [0, 4], 221 | // specify the maximum length of a line in your program 222 | 'max-len': [0, 80, 4], 223 | // limits the number of parameters that can be used in the function declaration. 224 | 'max-params': [0, 3], 225 | // specify the maximum number of statement allowed in a function 226 | 'max-statements': [0, 10], 227 | // disallow use of bitwise operators 228 | 'no-bitwise': 0, 229 | // disallow use of unary operators, ++ and -- 230 | 'no-plusplus': 0, 231 | // enforce return after a callback 232 | 'callback-return': 0, 233 | // enforces error handling in callbacks (node environment) 234 | 'handle-callback-err': 0, 235 | // disallow mixing regular variable and require declarations 236 | 'no-mixed-requires': [0, false], 237 | // disallow use of new operator with the require function 238 | 'no-new-require': 0, 239 | // disallow string concatenation with __dirname and __filename 240 | 'no-path-concat': 0, 241 | // disallow process.exit() 242 | 'no-process-exit': 0, 243 | // restrict usage of specified node modules 244 | 'no-restricted-modules': 0, 245 | // disallow use of synchronous methods (off by default) 246 | 'no-sync': 0, 247 | // babel inserts `'use strict';` for us 248 | 'strict': [2, 'never'], 249 | // enforce spacing inside array brackets 250 | 'array-bracket-spacing': 0, 251 | // enforce one true brace style 252 | 'brace-style': [2, '1tbs', {'allowSingleLine': true }], 253 | // require camel case names 254 | 'camelcase': [2, {'properties': 'never'}], 255 | // enforce spacing before and after comma 256 | 'comma-spacing': [2, {'before': false, 'after': true}], 257 | // enforce one true comma style 258 | 'comma-style': [2, 'last'], 259 | // require or disallow padding inside computed properties 260 | 'computed-property-spacing': 0, 261 | // enforces consistent naming when capturing the current execution context 262 | 'consistent-this': 0, 263 | // enforce newline at the end of file, with no multiple empty lines 264 | 'eol-last': 2, 265 | // require function expressions to have a name 266 | 'func-names': 0, 267 | // enforces use of function declarations or expressions 268 | 'func-style': 0, 269 | // this option enforces minimum and maximum identifier lengths (variable names, property names etc.) 270 | 'id-length': 0, 271 | // this option sets a specific tab width for your code 272 | 'indent': [2, 2], 273 | // specify whether double or single quotes should be used in JSX attributes 274 | /* 'jsx-quotes': 2, */ 275 | // enforces spacing between keys and values in object literal properties 276 | 'key-spacing': [2, {'beforeColon': false, 'afterColon': true}], 277 | // enforces empty lines around comments 278 | 'lines-around-comment': 1, 279 | // disallow mixed 'LF' and 'CRLF' as linebreaks 280 | 'linebreak-style': 0, 281 | // specify the maximum depth callbacks can be nested 282 | 'max-nested-callbacks': 0, 283 | // require a capital letter for constructors 284 | 'new-cap': [2, {'newIsCap': true}], 285 | // disallow the omission of parentheses when invoking a constructor with no arguments 286 | 'new-parens': 0, 287 | // allow/disallow an empty newline after var statement 288 | 'newline-after-var': 0, 289 | // disallow use of the Array constructor 290 | 'no-array-constructor': 0, 291 | // disallow use of the continue statement 292 | 'no-continue': 0, 293 | // disallow comments inline after code 294 | 'no-inline-comments': 0, 295 | // disallow if as the only statement in an else block 296 | 'no-lonely-if': 0, 297 | // disallow mixed spaces and tabs for indentation 298 | 'no-mixed-spaces-and-tabs': 2, 299 | // disallow multiple empty lines 300 | 'no-multiple-empty-lines': [2, {'max': 2}], 301 | // disallow nested ternary expressions 302 | 'no-nested-ternary': 2, 303 | // disallow use of the Object constructor 304 | 'no-new-object': 2, 305 | // disallow space between function identifier and application 306 | 'no-spaced-func': 2, 307 | // disallow the use of ternary operators 308 | 'no-ternary': 0, 309 | // disallow trailing whitespace at the end of lines 310 | 'no-trailing-spaces': 2, 311 | // disallow dangling underscores in identifiers 312 | 'no-underscore-dangle': 0, 313 | // disallow the use of Boolean literals in conditional expressions 314 | 'no-unneeded-ternary': 0, 315 | // require or disallow padding inside curly braces 316 | 'object-curly-spacing': 0, 317 | // allow just one var statement per function 318 | 'one-var': [2, 'never'], 319 | // require assignment operator shorthand where possible or prohibit it entirely 320 | 'operator-assignment': 0, 321 | // enforce operators to be placed before or after line breaks 322 | 'operator-linebreak': 0, 323 | // enforce padding within blocks 324 | 'padded-blocks': [2, 'never'], 325 | // require quotes around object literal property names 326 | 'quote-props': 0, 327 | // specify whether double or single quotes should be used 328 | 'quotes': [2, 'single', 'avoid-escape'], 329 | // require identifiers to match the provided regular expression 330 | 'id-match': 0, 331 | // enforce spacing before and after semicolons 332 | 'semi-spacing': [2, {'before': false, 'after': true}], 333 | // require or disallow use of semicolons instead of ASI 334 | 'semi': [2, 'always'], 335 | // sort variables within the same declaration block 336 | 'sort-vars': 0, 337 | // require a space before certain keywords 338 | /* 'space-before-keywords': [2, 'always'], */ 339 | // require or disallow space before blocks 340 | 'space-before-blocks': 2, 341 | // require or disallow space before function opening parenthesis 342 | 'space-before-function-paren': [2, 'never'], 343 | // require or disallow spaces inside parentheses 344 | 'space-in-parens': 0, 345 | // require spaces around operators 346 | 'space-infix-ops': 2, 347 | // Require or disallow spaces before/after unary operators 348 | 'space-unary-ops': 0, 349 | // require or disallow a space immediately following the // or /* in a comment 350 | 'spaced-comment': [2, 'always', { 351 | 'exceptions': ['-', '+'], 352 | 'markers': ['=', '!'] // space here to support sprockets directives 353 | }], 354 | // require regex literals to be wrapped in parentheses 355 | 'wrap-regex': 0, 356 | // enforce or disallow variable initializations at definition 357 | 'init-declarations': 0, 358 | // disallow the catch clause parameter name being the same as a variable in the outer scope 359 | 'no-catch-shadow': 0, 360 | // disallow deletion of variables 361 | 'no-delete-var': 2, 362 | // disallow labels that share a name with a variable 363 | 'no-label-var': 0, 364 | // disallow shadowing of names such as arguments 365 | 'no-shadow-restricted-names': 2, 366 | // disallow declaration of variables already declared in the outer scope 367 | 'no-shadow': 0, 368 | // disallow use of undefined when initializing variables 369 | 'no-undef-init': 0, 370 | // disallow use of undeclared variables unless mentioned in a /*global */ block 371 | 'no-undef': 2, 372 | // disallow use of undefined variable 373 | 'no-undefined': 0, 374 | // disallow declaration of variables that are not used in the code 375 | 'no-unused-vars': [2, {'vars': 'local', 'args': 'after-used'}], 376 | // disallow use of variables before they are defined 377 | 'no-use-before-define': 2 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.ejs text eol=lf 12 | *.js text eol=lf 13 | *.md text eol=lf 14 | *.txt text eol=lf 15 | *.json text eol=lf 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | coverage 5 | dist 6 | docs/dist 7 | node_modules 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6' 5 | - '5' 6 | - '4' 7 | script: 8 | - npm run lint 9 | - npm run test:cover 10 | after_success: 11 | - npm run coveralls 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## HTML5 File Selector Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ### [v2.1.0] - 2018-05-18 6 | 7 | - Remove babel-runtime dependency that was not necessary 8 | 9 | ### [v2.0.1] - 2018-05-16 10 | 11 | - Fix entry is null if text is dragged onto the dropzone ( #3 ) 12 | 13 | ### [v2.0.0] - 2018-05-16 14 | 15 | - remove mime-types and mime-db dependency to reduce bundle size 16 | - ensure `webkitRelativePath`, `lastModified`, and `lastModifiedDate` are copied/included from the File objects 17 | - force memory copy of `entry.fullPath` to better guarantee it is available in the returned wrapped File objects 18 | 19 | ### [v1.0.2] - 2018-04-17 20 | 21 | - check if file has name, required for react-dropzone pull-request #594 22 | 23 | ### [v1.0.1] - 2017-02-20 24 | 25 | - Initial version with drag/drop and file input selection support - allowing selection of both folders and files using chrome, firefox, and MS Edge browsers. 26 | - Browsers that do not include folder selection support will return a File object in the format { type: '', name: 'folder name' }, and will need to be programmatically handled 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Project jumpstarted using kriasoft/babel-starter-kit, which is 4 | Copyright (c) 2015-2016 Konstantin Tarkus, Kriasoft LLC. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML5 File Selector 2 | 3 | ### Documentation 4 | 5 | ... coming soon 6 | 7 | ### License 8 | 9 | This source code is licensed under the MIT license found in 10 | the [LICENSE.txt](https://github.com/quarklemotion/html5-file-selector/blob/master/LICENSE.txt) file. 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "html5-file-selector", 4 | "version": "2.1.0", 5 | "description": "A wrapper library for more easily handling html5 drag/drop and file input file and folder selections", 6 | "homepage": "https://www.github.com/quarklemotion/html5-file-selector", 7 | "repository": "quarklemotion/html5-file-selector", 8 | "author": "Michael Fields - @quarklemotion", 9 | "contributors": [ 10 | "Michael Fields - @quarklemotion" 11 | ], 12 | "license": "MIT", 13 | "keywords": [], 14 | "main": "index.js", 15 | "jsnext:main": "index.es.js", 16 | "babel": { 17 | "presets": [ 18 | "latest", 19 | "stage-0" 20 | ] 21 | }, 22 | "eslintConfig": { 23 | "parser": "babel-eslint", 24 | "extends": "airbnb-base" 25 | }, 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "babel-cli": "6.16.x", 29 | "babel-core": "6.17.x", 30 | "babel-eslint": "7.0.x", 31 | "babel-polyfill": "6.23.x", 32 | "babel-preset-latest": "6.16.x", 33 | "babel-preset-stage-0": "6.16.x", 34 | "babel-register": "6.16.x", 35 | "chai": "4.0.0-canary.1", 36 | "coveralls": "2.11.x", 37 | "del": "2.2.x", 38 | "eslint": "3.8.x", 39 | "eslint-plugin-import": "2.2.x", 40 | "istanbul": "1.1.0-alpha.1", 41 | "mocha": "3.1.x", 42 | "rollup": "0.36.x", 43 | "rollup-plugin-babel": "2.6.x", 44 | "sinon": "2.0.0-pre.3" 45 | }, 46 | "scripts": { 47 | "lint": "eslint src", 48 | "test": "mocha --compilers js:babel-register", 49 | "test:watch": "mocha --compilers js:babel-register --reporter min --watch", 50 | "test:cover": "babel-node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha", 51 | "coveralls": "cat ./coverage/lcov.info | coveralls", 52 | "build": "node tools/build", 53 | "prepublish": "npm run build" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Html5FileSelector.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_FILES_TO_IGNORE = [ 2 | '.DS_Store', // OSX indexing file 3 | 'Thumbs.db' // Windows indexing file 4 | ]; 5 | 6 | // map of common (mostly media types) mime types to use when the browser does not supply the mime type 7 | const EXTENSION_TO_MIME_TYPE_MAP = { 8 | avi: 'video/avi', 9 | gif: 'image/gif', 10 | ico: 'image/x-icon', 11 | jpeg: 'image/jpeg', 12 | jpg: 'image/jpeg', 13 | mkv: 'video/x-matroska', 14 | mov: 'video/quicktime', 15 | mp4: 'video/mp4', 16 | pdf: 'application/pdf', 17 | png: 'image/png', 18 | zip: 'application/zip' 19 | }; 20 | 21 | function shouldIgnoreFile(file) { 22 | return DEFAULT_FILES_TO_IGNORE.indexOf(file.name) >= 0; 23 | } 24 | 25 | function copyString(aString) { 26 | return ` ${aString}`.slice(1); 27 | } 28 | 29 | function traverseDirectory(entry) { 30 | const reader = entry.createReader(); 31 | // Resolved when the entire directory is traversed 32 | return new Promise((resolveDirectory) => { 33 | const iterationAttempts = []; 34 | const errorHandler = () => {}; 35 | function readEntries() { 36 | // According to the FileSystem API spec, readEntries() must be called until 37 | // it calls the callback with an empty array. 38 | reader.readEntries((batchEntries) => { 39 | if (!batchEntries.length) { 40 | // Done iterating this particular directory 41 | resolveDirectory(Promise.all(iterationAttempts)); 42 | } else { 43 | // Add a list of promises for each directory entry. If the entry is itself 44 | // a directory, then that promise won't resolve until it is fully traversed. 45 | iterationAttempts.push(Promise.all(batchEntries.map((batchEntry) => { 46 | if (batchEntry.isDirectory) { 47 | return traverseDirectory(batchEntry); 48 | } 49 | return Promise.resolve(batchEntry); 50 | }))); 51 | // Try calling readEntries() again for the same dir, according to spec 52 | readEntries(); 53 | } 54 | }, errorHandler); 55 | } 56 | // initial call to recursive entry reader function 57 | readEntries(); 58 | }); 59 | } 60 | 61 | // package the file in an object that includes the fullPath from the file entry 62 | // that would otherwise be lost 63 | function packageFile(file, entry) { 64 | let fileTypeOverride = ''; 65 | // handle some browsers sometimes missing mime types for dropped files 66 | const hasExtension = file.name && file.name.lastIndexOf('.') !== -1; 67 | if (hasExtension && !file.type) { 68 | const fileExtension = (file.name || '').split('.').pop(); 69 | fileTypeOverride = EXTENSION_TO_MIME_TYPE_MAP[fileExtension]; 70 | } 71 | return { 72 | fileObject: file, // provide access to the raw File object (required for uploading) 73 | fullPath: entry ? copyString(entry.fullPath) : file.name, 74 | lastModified: file.lastModified, 75 | lastModifiedDate: file.lastModifiedDate, 76 | name: file.name, 77 | size: file.size, 78 | type: file.type ? file.type : fileTypeOverride, 79 | webkitRelativePath: file.webkitRelativePath 80 | }; 81 | } 82 | 83 | function getFile(entry) { 84 | return new Promise((resolve) => { 85 | entry.file((file) => { 86 | resolve(packageFile(file, entry)); 87 | }); 88 | }); 89 | } 90 | 91 | function handleFilePromises(promises, fileList) { 92 | return Promise.all(promises).then((files) => { 93 | files.forEach((file) => { 94 | if (!shouldIgnoreFile(file)) { 95 | fileList.push(file); 96 | } 97 | }); 98 | return fileList; 99 | }); 100 | } 101 | 102 | export function getDataTransferFiles(dataTransfer) { 103 | const dataTransferFiles = []; 104 | const folderPromises = []; 105 | const filePromises = []; 106 | 107 | [].slice.call(dataTransfer.items).forEach((listItem) => { 108 | if (typeof listItem.webkitGetAsEntry === 'function') { 109 | const entry = listItem.webkitGetAsEntry(); 110 | 111 | if (entry) { 112 | if (entry.isDirectory) { 113 | folderPromises.push(traverseDirectory(entry)); 114 | } else { 115 | filePromises.push(getFile(entry)); 116 | } 117 | } 118 | } else { 119 | dataTransferFiles.push(listItem); 120 | } 121 | }); 122 | if (folderPromises.length) { 123 | const flatten = (array) => array.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); 124 | return Promise.all(folderPromises).then((fileEntries) => { 125 | const flattenedEntries = flatten(fileEntries); 126 | // collect async promises to convert each fileEntry into a File object 127 | flattenedEntries.forEach((fileEntry) => { 128 | filePromises.push(getFile(fileEntry)); 129 | }); 130 | return handleFilePromises(filePromises, dataTransferFiles); 131 | }); 132 | } else if (filePromises.length) { 133 | return handleFilePromises(filePromises, dataTransferFiles); 134 | } 135 | return Promise.resolve(dataTransferFiles); 136 | } 137 | 138 | /** 139 | * This function should be called from both the onDrop event from your drag/drop 140 | * dropzone as well as from the HTML5 file selector input field onChange event 141 | * handler. Pass the event object from the triggered event into this function. 142 | * Supports mix of files and folders dropped via drag/drop. 143 | * 144 | * Returns: an array of File objects, that includes all files within folders 145 | * and subfolders of the dropped/selected items. 146 | */ 147 | export function getDroppedOrSelectedFiles(event) { 148 | const dataTransfer = event.dataTransfer; 149 | if (dataTransfer && dataTransfer.items) { 150 | return getDataTransferFiles(dataTransfer).then((fileList) => { 151 | return Promise.resolve(fileList); 152 | }); 153 | } 154 | const files = []; 155 | const dragDropFileList = dataTransfer && dataTransfer.files; 156 | const inputFieldFileList = event.target && event.target.files; 157 | const fileList = dragDropFileList || inputFieldFileList || []; 158 | // convert the FileList to a simple array of File objects 159 | for (let i = 0; i < fileList.length; i++) { 160 | files.push(packageFile(fileList[i])); 161 | } 162 | return Promise.resolve(files); 163 | } 164 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Html5 File Selector 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE.txt file in the root directory of this source tree. 6 | */ 7 | 8 | export { getDataTransferFiles, getDroppedOrSelectedFiles } from './Html5FileSelector.js'; 9 | -------------------------------------------------------------------------------- /test/Html5FileSelctorSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Html5 File Selector 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE.txt file in the root directory of this source tree. 6 | */ 7 | 8 | import { expect } from 'chai'; 9 | import * as Html5FileSelector from '../src/Html5FileSelector'; 10 | require('babel-polyfill'); 11 | 12 | const MOCK_MEDIA_FILES = [ 13 | { type: 'image/jpg', name: 'monkey_see.jpg' }, 14 | { type: 'video/mp4', name: 'monkey_pod.mp4' }, 15 | { type: 'image/jpg', name: 'cat1.jpg' }, 16 | { type: 'video/mp4', name: 'cat_pile.mp4' }, 17 | { type: 'image/jpg', name: 'dog.jpg' }, 18 | { type: 'video/mp4', name: 'dog_barking.mp4' }, 19 | { type: 'image/jpg', name: 'giraffe.jpg' }, 20 | { type: 'video/mp4', name: 'giraffe_eats.mp4' }, 21 | { type: 'image/jpg', name: 'rabbit.jpg' }, 22 | { type: 'video/mp4', name: 'white_rabbit_song.mp4' } 23 | ]; 24 | const MOCK_INDEX_FILES = [ 25 | { type: '', name: '.DS_Store' }, 26 | { type: '', name: 'Thumbs.db' } 27 | ]; 28 | 29 | function fileComparator(a, b) { 30 | var textA = a.name; 31 | var textB = b.name; 32 | return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; 33 | } 34 | 35 | function createDataTransferItemFile(file) { 36 | return { 37 | isDirectory: false, 38 | isFile: true, 39 | file: (callback) => { return callback(file); } 40 | }; 41 | } 42 | 43 | function createDataTransferItemFolder(containedFiles) { 44 | return { 45 | isDirectory: true, 46 | isFile: false, 47 | createReader: () => { 48 | return { 49 | sentEntries: false, // not part of the actual API, just used for real API behavior mimicking 50 | readEntries: function(callback) { 51 | if (!this.sentEntries) { 52 | this.sentEntries = true; 53 | callback(containedFiles); 54 | } else { 55 | callback([]); 56 | } 57 | } 58 | }; 59 | } 60 | }; 61 | } 62 | 63 | function topLevelEntry(entry) { 64 | return { 65 | webkitGetAsEntry: () => { 66 | return entry; 67 | } 68 | } 69 | } 70 | 71 | describe('Html5FileSelector', () => { 72 | 73 | describe('.getDroppedOrSelectedFiles', () => { 74 | it('handles files manually selected through a HTML file picker', (done) => { 75 | const mockSelectedFiles = MOCK_MEDIA_FILES.slice(0,2); 76 | const filesSelectedEvent = { 77 | target: { 78 | files: mockSelectedFiles 79 | } 80 | }; 81 | 82 | Html5FileSelector.getDroppedOrSelectedFiles(filesSelectedEvent).then((selectedFiles) => { 83 | expect(selectedFiles.map(file => file.name).join(',')).to.equal( 84 | mockSelectedFiles.map(file => file.name).join(',') 85 | ); 86 | done(); 87 | }); 88 | }); 89 | 90 | it('handles individual files dropped to a drop zone', (done) => { 91 | const mockDataTransferItems = [ 92 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[0])), 93 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[1])), 94 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[2])), 95 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[3])), 96 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[4])) 97 | ]; 98 | const mockFilesDroppedEvent = { 99 | dataTransfer: { 100 | items: mockDataTransferItems 101 | } 102 | }; 103 | Html5FileSelector.getDroppedOrSelectedFiles(mockFilesDroppedEvent).then((selectedFiles) => { 104 | const sortedFiles = selectedFiles.sort(fileComparator); 105 | expect(sortedFiles.map(file => file.name).join(',')).to.equal( 106 | MOCK_MEDIA_FILES.slice(0,5).sort(fileComparator).map(file => file.name).join(',') 107 | ); 108 | done(); 109 | }).catch(error => { 110 | done(error); 111 | }); 112 | }); 113 | 114 | it('returns flattened files dropped from a mix of files and multi-nested folders dropped to a drop zone', (done) => { 115 | const mockDataTransferItems = [ 116 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[0])), 117 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[1])), 118 | topLevelEntry(createDataTransferItemFolder([ 119 | createDataTransferItemFolder([ 120 | createDataTransferItemFile(MOCK_MEDIA_FILES[2]), 121 | createDataTransferItemFile(MOCK_MEDIA_FILES[3]), 122 | createDataTransferItemFile(MOCK_MEDIA_FILES[4]) 123 | ]), 124 | createDataTransferItemFile(MOCK_MEDIA_FILES[5]), 125 | createDataTransferItemFile(MOCK_MEDIA_FILES[6]) 126 | ])), 127 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[7])), 128 | topLevelEntry(createDataTransferItemFolder([ 129 | createDataTransferItemFile(MOCK_MEDIA_FILES[8]), 130 | createDataTransferItemFile(MOCK_MEDIA_FILES[9]) 131 | ])) 132 | ]; 133 | const mockFilesDroppedEvent = { 134 | dataTransfer: { 135 | items: mockDataTransferItems 136 | } 137 | }; 138 | Html5FileSelector.getDroppedOrSelectedFiles(mockFilesDroppedEvent).then((selectedFiles) => { 139 | const sortedFiles = selectedFiles.sort(fileComparator); 140 | expect(sortedFiles.map(file => file.name).join(',')).to.equal( 141 | MOCK_MEDIA_FILES.sort(fileComparator).map(file => file.name).join(',') 142 | ); 143 | done(); 144 | }).catch(error => { 145 | done(error); 146 | }); 147 | }); 148 | 149 | it('ignores OS-specific indexing files', (done) => { 150 | const mockDataTransferItems = [ 151 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[0])), 152 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[1])), 153 | topLevelEntry(createDataTransferItemFolder([ 154 | createDataTransferItemFolder([ 155 | createDataTransferItemFile(MOCK_INDEX_FILES[0]), 156 | createDataTransferItemFile(MOCK_INDEX_FILES[1]), 157 | createDataTransferItemFile(MOCK_MEDIA_FILES[2]) 158 | ]), 159 | createDataTransferItemFile(MOCK_INDEX_FILES[0]), 160 | createDataTransferItemFile(MOCK_MEDIA_FILES[3]) 161 | ])), 162 | topLevelEntry(createDataTransferItemFile(MOCK_MEDIA_FILES[4])), 163 | topLevelEntry(createDataTransferItemFolder([ 164 | createDataTransferItemFile(MOCK_INDEX_FILES[1]), 165 | createDataTransferItemFile(MOCK_MEDIA_FILES[5]) 166 | ])) 167 | ]; 168 | const mockFilesDroppedEvent = { 169 | dataTransfer: { 170 | items: mockDataTransferItems 171 | } 172 | }; 173 | Html5FileSelector.getDroppedOrSelectedFiles(mockFilesDroppedEvent).then((selectedFiles) => { 174 | const sortedFiles = selectedFiles.sort(fileComparator); 175 | expect(sortedFiles.map(file => file.name).join(',')).to.equal( 176 | MOCK_MEDIA_FILES.slice(0,6).sort(fileComparator).map(file => file.name).join(',') 177 | ); 178 | done(); 179 | }).catch(error => { 180 | done(error); 181 | }); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Babel Starter Kit (https://www.kriasoft.com/babel-starter-kit) 3 | * 4 | * Copyright © 2015-2016 Kriasoft, LLC. All rights reserved. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE.txt file in the root directory of this source tree. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | const fs = require('fs'); 13 | const del = require('del'); 14 | const rollup = require('rollup'); 15 | const babel = require('rollup-plugin-babel'); 16 | const pkg = require('../package.json'); 17 | 18 | let promise = Promise.resolve(); 19 | 20 | // Clean up the output directory 21 | promise = promise.then(() => del(['dist/*'])); 22 | 23 | // Compile source code into a distributable format with Babel 24 | ['es', 'cjs', 'umd'].forEach((format) => { 25 | promise = promise.then(() => rollup.rollup({ 26 | entry: 'src/index.js', 27 | external: Object.keys(pkg.dependencies), 28 | plugins: [babel(Object.assign(pkg.babel, { 29 | babelrc: false, 30 | exclude: 'node_modules/**', 31 | runtimeHelpers: true, 32 | presets: pkg.babel.presets.map(x => (x === 'latest' ? ['latest', { es2015: { modules: false } }] : x)), 33 | }))], 34 | }).then(bundle => bundle.write({ 35 | dest: `dist/${format === 'cjs' ? 'index' : `index.${format}`}.js`, 36 | format, 37 | sourceMap: true, 38 | moduleName: format === 'umd' ? pkg.name : undefined, 39 | }))); 40 | }); 41 | 42 | // Copy package.json and LICENSE.txt 43 | promise = promise.then(() => { 44 | delete pkg.private; 45 | delete pkg.devDependencies; 46 | delete pkg.scripts; 47 | delete pkg.eslintConfig; 48 | delete pkg.babel; 49 | fs.writeFileSync('dist/package.json', JSON.stringify(pkg, null, ' '), 'utf-8'); 50 | fs.writeFileSync('dist/LICENSE.txt', fs.readFileSync('LICENSE.txt', 'utf-8'), 'utf-8'); 51 | }); 52 | 53 | promise.catch(err => console.error(err.stack)); // eslint-disable-line no-console 54 | --------------------------------------------------------------------------------