3

I wish to prevent the user from uploading a file the server will reject from a page with minimal JavaScript on it, ideally without adding any heavy dependencies like jQuery purely to solve this one problem.

Since I'm not targeting legacy users, I trued using the browser's form validation system to check if the user has selected a valid file, however it only seems to care if the user selects a file regardless of type.

> i = document.querySelector('input[type=file]')
<input type=​"file" accept=​"image/​*" name=​"attachment" required>​
> i.accept
"image/*"
> i.files[0].type
"application/x-zip-compressed"
> i.checkValidity()
true

Is there a simple way of doing this? The only thing that I have found that comes close is jQuery Validate, but it's a bit of a heavyweight solution.

Lexi
  • 1,670
  • 1
  • 19
  • 35

4 Answers4

6

You could just perform a RegExp test — the following converts the wildcard in MIME type strings to match RegExp syntax, and tests that against the input file's type:

( new RegExp( i.accept.replace( '*', '.\*' ) ) ).test( i.files[ 0 ].type )

Demo here.

EDIT:

I eventually found a way to make this functionality seamless with native browser validation behaviour (ie prevent submission for invalid inputs, notify user using native validation warnings), but I'm not exactly sure how the code works or whether it's good practice (I've asked about the stranger parts here). However, this appears to behave as expected, at least in Chrome 31:

void function enhanceFileInputTypeValidityCheck(){
    var inputPrototype      = document.createElement( 'input' ).constructor.prototype;
    var nativeCheckValidity = inputPrototype.checkValidity;

    function validateFileInputType( input ){
        var MIMEtype = new RegExp( input.accept.replace( '*', '.\*' ) );

        return Array.prototype.every.call( input.files, function passesAcceptedFormat( file ){
            return MIMEtype.test( file.type );
        } );
    }
    
    function validateInputs(){
        Array.prototype.forEach.call( document.querySelectorAll( 'input, select' ), function callValidation( input ){
            input.checkValidity();
        } );
    }

    inputPrototype.checkValidity = function enhancedCheckValidity(){        
        if( this.type === 'file' &&  this.accept && this.files && this.files.length ){
            if( !validateFileInputType( this ) ){
                this.setCustomValidity( 'Please only submit files of type ' + this.accept );
                
                return false;
            }
        }

        return nativeCheckValidity.apply( this );
    }
    
    Array.prototype.forEach.call( [ 'change', 'input' ], function bindValidation( event ){
        document.documentElement.addEventListener( event, validateInputs );
    } );
}();

Demo here (attempt to submit with an invalid file type).

Community
  • 1
  • 1
Barney
  • 16,181
  • 5
  • 62
  • 76
  • Thanks, I've adapted that for my project and thrown an answer at your question. Also, your second JSFiddle link doesn't work properly. – Lexi Jan 09 '14 at 16:00
  • 1
    This doesn't work when `accept` contains multiple values, separated by a comma – dude Oct 22 '19 at 16:21
  • Well spotted @dude! Updated the answer to cater for that criterion. – Barney Oct 24 '19 at 14:21
4

There is no need for any complex regexes like other solutions provide.

/**
 * Check if a mime type matches the set given in accept
 *
 * @param type the mime type to test, ex image/png
 * @param accept the mime types to accept, ex audio/*,video/*,image/png
 * @returns true if the mime is accepted, false otherwise
 */
function verifyAccept(type: string, accept: string): boolean {
  const allowed = accept.split(',').map(x => x.trim());
  return allowed.includes(type) || allowed.includes(type.split('/')[0] + '/*');
}
s.meijer
  • 3,403
  • 3
  • 26
  • 23
3

The accepted answer only works when accept is a single value. Also, it doesn't support the multiple attribute. For multiple accept values, comma separated, and multiple files, use the following:

window.validateFileFormat = function() {
  const valid = [...i.files].every(file => {
    if (!i.accept) {
      return true;
    }
    return i.accept.replace(/\s/g, '').split(',').filter(accept => {
      return new RegExp(accept.replace('*', '.*')).test(file.type);
    }).length > 0;
  });
  alert('Valid: ' + valid);
}

Fiddle: http://jsfiddle.net/ynj8dsu6/

dude
  • 5,678
  • 11
  • 54
  • 81
2

This one gathers the techniques from the other posts and make it to a short and easy function, working with multiple expressions:

verifyAccept = function( file-type, accept ) {
    var type-regex = new RegExp( accept.replace( /\*/g, '.\*' ).replace( /\,/g, '|' ) );
    return type-regex.test( file-type );
}

instead of looping through the splitted accept string it uses regular expression features, just replace , through | which means the single expressions are or'ed.

Enno
  • 183
  • 2
  • 9