text: /*
  * MIT Licensed
  * https://www.twentythree.com
  * https://github.com/23/resumable.js
  * Steffen Fagerström Christensen, steffen@twentythree.com
  */
  
  (function(){
  "use strict";
  
    var Resumable = function(opts){
      if ( !(this instanceof Resumable) ) {
        return new Resumable(opts);
      }
      this.version = 1.0;
      // SUPPORTED BY BROWSER?
      // Check if these features are support by the browser:
      // - File object type
      // - Blob object type
      // - FileList object type
      // - slicing files
      this.support = (
                     (typeof(File)!=='undefined')
                     &&
                     (typeof(Blob)!=='undefined')
                     &&
                     (typeof(FileList)!=='undefined')
                     &&
                     (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false)
                     );
      if(!this.support) return(false);
  
  
      // PROPERTIES
      var $ = this;
      $.files = [];
      $.defaults = {
        chunkSize:1*1024*1024,
        forceChunkSize:false,
        simultaneousUploads:3,
        fileParameterName:'file',
        chunkNumberParameterName: 'resumableChunkNumber',
        chunkSizeParameterName: 'resumableChunkSize',
        currentChunkSizeParameterName: 'resumableCurrentChunkSize',
        totalSizeParameterName: 'resumableTotalSize',
        typeParameterName: 'resumableType',
        identifierParameterName: 'resumableIdentifier',
        fileNameParameterName: 'resumableFilename',
        relativePathParameterName: 'resumableRelativePath',
        totalChunksParameterName: 'resumableTotalChunks',
        dragOverClass: 'dragover',
        throttleProgressCallbacks: 0.5,
        query:{},
        headers:{},
        preprocess:null,
        preprocessFile:null,
        method:'multipart',
        uploadMethod: 'POST',
        testMethod: 'GET',
        prioritizeFirstAndLastChunk:false,
        target:'/',
        testTarget: null,
        parameterNamespace:'',
        testChunks:true,
        generateUniqueIdentifier:null,
        getTarget:null,
        maxChunkRetries:100,
        chunkRetryInterval:undefined,
        permanentErrors:[400, 404, 409, 415, 500, 501],
        maxFiles:undefined,
        withCredentials:false,
        xhrTimeout:0,
        clearInput:true,
        chunkFormat:'blob',
        setChunkTypeFromFile:false,
        maxFilesErrorCallback:function (files, errorCount) {
          var maxFiles = $.getOpt('maxFiles');
          alert('Please upload no more than ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.');
        },
        minFileSize:1,
        minFileSizeErrorCallback:function(file, errorCount) {
          alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.');
        },
        maxFileSize:undefined,
        maxFileSizeErrorCallback:function(file, errorCount) {
          alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.');
        },
        fileType: [],
        fileTypeErrorCallback: function(file, errorCount) {
          alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.');
        }
      };
      $.opts = opts||{};
      $.getOpt = function(o) {
        var $opt = this;
        // Get multiple option if passed an array
        if(o instanceof Array) {
          var options = {};
          $h.each(o, function(option){
            options[option] = $opt.getOpt(option);
          });
          return options;
        }
        // Otherwise, just return a simple option
        if ($opt instanceof ResumableChunk) {
          if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
          else { $opt = $opt.fileObj; }
        }
        if ($opt instanceof ResumableFile) {
          if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
          else { $opt = $opt.resumableObj; }
        }
        if ($opt instanceof Resumable) {
          if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; }
          else { return $opt.defaults[o]; }
        }
      };
      $.indexOf = function(array, obj) {
      	if (array.indexOf) { return array.indexOf(obj); }     
      	for (var i = 0; i < array.length; i++) {
              if (array[i] === obj) { return i; }
          }
          return -1;
      }
  
      // EVENTS
      // catchAll(event, ...)
      // fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file),
      // fileError(file, message), complete(), progress(), error(message, file), pause()
      $.events = [];
      $.on = function(event,callback){
        $.events.push(event.toLowerCase(), callback);
      };
      $.fire = function(){
        // `arguments` is an object, not array, in FF, so:
        var args = [];
        for (var i=0; i<arguments.length; i++) args.push(arguments[i]);
        // Find event listeners, and support pseudo-event `catchAll`
        var event = args[0].toLowerCase();
        for (var i=0; i<=$.events.length; i+=2) {
          if($.events[i]==event) $.events[i+1].apply($,args.slice(1));
          if($.events[i]=='catchall') $.events[i+1].apply(null,args);
        }
        if(event=='fileerror') $.fire('error', args[2], args[1]);
        if(event=='fileprogress') $.fire('progress');
      };
  
  
      // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
      var $h = {
        stopEvent: function(e){
          e.stopPropagation();
          e.preventDefault();
        },
        each: function(o,callback){
          if(typeof(o.length)!=='undefined') {
            for (var i=0; i<o.length; i++) {
              // Array or FileList
              if(callback(o[i])===false) return;
            }
          } else {
            for (i in o) {
              // Object
              if(callback(i,o[i])===false) return;
            }
          }
        },
        generateUniqueIdentifier:function(file, event){
          var custom = $.getOpt('generateUniqueIdentifier');
          if(typeof custom === 'function') {
            return custom(file, event);
          }
          var relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name; // Some confusion in different versions of Firefox
          var size = file.size;
          return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''));
        },
        contains:function(array,test) {
          var result = false;
  
          $h.each(array, function(value) {
            if (value == test) {
              result = true;
              return false;
            }
            return true;
          });
  
          return result;
        },
        formatSize:function(size){
          if(size<1024) {
            return size + ' bytes';
          } else if(size<1024*1024) {
            return (size/1024.0).toFixed(0) + ' KB';
          } else if(size<1024*1024*1024) {
            return (size/1024.0/1024.0).toFixed(1) + ' MB';
          } else {
            return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB';
          }
        },
        getTarget:function(request, params){
          var target = $.getOpt('target');
  
          if (request === 'test' && $.getOpt('testTarget')) {
            target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget');
          }
  
          if (typeof target === 'function') {
            return target(params);
          }
  
          var separator = target.indexOf('?') < 0 ? '?' : '&';
          var joinedParams = params.join('&');
  
          return target + separator + joinedParams;
        }
      };
  
      var onDrop = function(e){
        e.currentTarget.classList.remove($.getOpt('dragOverClass'));
        $h.stopEvent(e);
  
        //handle dropped things as items if we can (this lets us deal with folders nicer in some cases)
        if (e.dataTransfer && e.dataTransfer.items) {
          loadFiles(e.dataTransfer.items, event);
        }
        //else handle them as files
        else if (e.dataTransfer && e.dataTransfer.files) {
          loadFiles(e.dataTransfer.files, event);
        }
      };
      var onDragLeave = function(e){
        e.currentTarget.classList.remove($.getOpt('dragOverClass'));
      };
      var onDragOverEnter = function(e) {
        e.preventDefault();
        var dt = e.dataTransfer;
        if ($.indexOf(dt.types, "Files") >= 0) { // only for file drop
          e.stopPropagation();
          dt.dropEffect = "copy";
          dt.effectAllowed = "copy";
          e.currentTarget.classList.add($.getOpt('dragOverClass'));
        } else { // not work on IE/Edge....
          dt.dropEffect = "none";
          dt.effectAllowed = "none";
        }
      };
  
      /**
       * processes a single upload item (file or directory)
       * @param {Object} item item to upload, may be file or directory entry
       * @param {string} path current file path
       * @param {File[]} items list of files to append new items to
       * @param {Function} cb callback invoked when item is processed
       */
      function processItem(item, path, items, cb) {
        var entry;
        if(item.isFile){
          // file provided
          return item.file(function(file){
            file.relativePath = path + file.name;
            items.push(file);
            cb();
          });
        }else if(item.isDirectory){
          // item is already a directory entry, just assign
          entry = item;
        }else if(item instanceof File) {
          items.push(item);
        }
        if('function' === typeof item.webkitGetAsEntry){
          // get entry from file object
          entry = item.webkitGetAsEntry();
        }
        if(entry && entry.isDirectory){
          // directory provided, process it
          return processDirectory(entry, path + entry.name + '/', items, cb);
        }
        if('function' === typeof item.getAsFile){
          // item represents a File object, convert it
          item = item.getAsFile();
          if(item instanceof File) {
            item.relativePath = path + item.name;
            items.push(item);
          }
        }
        cb(); // indicate processing is done
      }
  
  
      /**
       * cps-style list iteration.
       * invokes all functions in list and waits for their callback to be
       * triggered.
       * @param  {Function[]}   items list of functions expecting callback parameter
       * @param  {Function} cb    callback to trigger after the last callback has been invoked
       */
      function processCallbacks(items, cb){
        if(!items || items.length === 0){
          // empty or no list, invoke callback
          return cb();
        }
        // invoke current function, pass the next part as continuation
        items[0](function(){
          processCallbacks(items.slice(1), cb);
        });
      }
  
      /**
       * recursively traverse directory and collect files to upload
       * @param  {Object}   directory directory to process
       * @param  {string}   path      current path
       * @param  {File[]}   items     target list of items
       * @param  {Function} cb        callback invoked after traversing directory
       */
      function processDirectory (directory, path, items, cb) {
        var dirReader = directory.createReader();
        var allEntries = [];
  
        function readEntries () {
          dirReader.readEntries(function(entries){
            if (entries.length) {
              allEntries = allEntries.concat(entries);
              return readEntries();
            }
  
            // process all conversion callbacks, finally invoke own one
            processCallbacks(
              allEntries.map(function(entry){
                // bind all properties except for callback
                return processItem.bind(null, entry, path, items);
              }),
              cb
            );
          });
        }
  
        readEntries();
      }
  
      /**
       * process items to extract files to be uploaded
       * @param  {File[]} items items to process
       * @param  {Event} event event that led to upload
       */
      function loadFiles(items, event) {
        if(!items.length){
          return; // nothing to do
        }
        $.fire('beforeAdd');
        var files = [];
        processCallbacks(
            Array.prototype.map.call(items, function(item){
              // bind all properties except for callback
              return processItem.bind(null, item, "", files);
            }),
            function(){
              if(files.length){
                // at least one file found
                appendFilesFromFileList(files, event);
              }
            }
        );
      };
  
      var appendFilesFromFileList = function(fileList, event){
        // check for uploading too many files
        var errorCount = 0;
        var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']);
        if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) {
          // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
          if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) {
            $.removeFile($.files[0]);
          } else {
            o.maxFilesErrorCallback(fileList, errorCount++);
            return false;
          }
        }
        var files = [], filesSkipped = [], remaining = fileList.length;
        var decreaseReamining = function(){
          if(!--remaining){
            // all files processed, trigger event
            if(!files.length && !filesSkipped.length){
              // no succeeded files, just skip
              return;
            }
            window.setTimeout(function(){
              $.fire('filesAdded', files, filesSkipped);
            },0);
          }
        };
        $h.each(fileList, function(file){
          var fileName = file.name;
          var fileType = file.type; // e.g video/mp4
          if(o.fileType.length > 0){
            var fileTypeFound = false;
            for(var index in o.fileType){
              // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all
              o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase();
  
              // Allowing for both [extension, .extension, mime/type, mime/*]
              var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index];
  
              if ((fileName.substr(-1 * extension.length).toLowerCase() === extension) ||
                //If MIME type, check for wildcard or if extension matches the files tiletype
                (extension.indexOf('/') !== -1 && (
                  (extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) ||
                  fileType === extension
                ))
              ){
                fileTypeFound = true;
                break;
              }
            }
            if (!fileTypeFound) {
              o.fileTypeErrorCallback(file, errorCount++);
              return true;
            }
          }
  
          if (typeof(o.minFileSize)!=='undefined' && file.size<o.minFileSize) {
            o.minFileSizeErrorCallback(file, errorCount++);
            return true;
          }
          if (typeof(o.maxFileSize)!=='undefined' && file.size>o.maxFileSize) {
            o.maxFileSizeErrorCallback(file, errorCount++);
            return true;
          }
  
          function addFile(uniqueIdentifier){
            if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){
              file.uniqueIdentifier = uniqueIdentifier;
              var f = new ResumableFile($, file, uniqueIdentifier);
              $.files.push(f);
              files.push(f);
              f.container = (typeof event != 'undefined' ? event.srcElement : null);
              window.setTimeout(function(){
                $.fire('fileAdded', f, event)
              },0);
            })()} else {
              filesSkipped.push(file);
            };
            decreaseReamining();
          }
          // directories have size == 0
          var uniqueIdentifier = $h.generateUniqueIdentifier(file, event);
          if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){
            // Promise or Promise-like object provided as unique identifier
            uniqueIdentifier
            .then(
              function(uniqueIdentifier){
                // unique identifier generation succeeded
                addFile(uniqueIdentifier);
              },
             function(){
                // unique identifier generation failed
                // skip further processing, only decrease file count
                decreaseReamining();
              }
            );
          }else{
            // non-Promise provided as unique identifier, process synchronously
            addFile(uniqueIdentifier);
          }
        });
      };
  
      // INTERNAL OBJECT TYPES
      function ResumableFile(resumableObj, file, uniqueIdentifier){
        var $ = this;
        $.opts = {};
        $.getOpt = resumableObj.getOpt;
        $._prevProgress = 0;
        $.resumableObj = resumableObj;
        $.file = file;
        $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox
        $.size = file.size;
        $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName;
        $.uniqueIdentifier = uniqueIdentifier;
        $._pause = false;
        $.container = '';
        $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
        var _error = uniqueIdentifier !== undefined;
  
        // Callback when something happens within the chunk
        var chunkEvent = function(event, message){
          // event can be 'progress', 'success', 'error' or 'retry'
          switch(event){
          case 'progress':
            $.resumableObj.fire('fileProgress', $, message);
            break;
          case 'error':
            $.abort();
            _error = true;
            $.chunks = [];
            $.resumableObj.fire('fileError', $, message);
            break;
          case 'success':
            if(_error) return;
            $.resumableObj.fire('fileProgress', $, message); // it's at least progress
            if($.isComplete()) {
              $.resumableObj.fire('fileSuccess', $, message);
            }
            break;
          case 'retry':
            $.resumableObj.fire('fileRetry', $);
            break;
          }
        };
  
        // Main code to set up a file object with chunks,
        // packaged to be able to handle retries if needed.
        $.chunks = [];
        $.abort = function(){
          // Stop current uploads
          var abortCount = 0;
          $h.each($.chunks, function(c){
            if(c.status()=='uploading') {
              c.abort();
              abortCount++;
            }
          });
          if(abortCount>0) $.resumableObj.fire('fileProgress', $);
        };
        $.cancel = function(){
          // Reset this file to be void
          var _chunks = $.chunks;
          $.chunks = [];
          // Stop current uploads
          $h.each(_chunks, function(c){
            if(c.status()=='uploading')  {
              c.abort();
              $.resumableObj.uploadNextChunk();
            }
          });
          $.resumableObj.removeFile($);
          $.resumableObj.fire('fileProgress', $);
        };
        $.retry = function(){
          $.bootstrap();
          var firedRetry = false;
          $.resumableObj.on('chunkingComplete', function(){
            if(!firedRetry) $.resumableObj.upload();
            firedRetry = true;
          });
        };
        $.bootstrap = function(){
          $.abort();
          _error = false;
          // Rebuild stack of chunks from file
          $.chunks = [];
          $._prevProgress = 0;
          var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor;
          var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1);
          for (var offset=0; offset<maxOffset; offset++) {(function(offset){
              window.setTimeout(function(){
                  $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
                  $.resumableObj.fire('chunkingProgress',$,offset/maxOffset);
              },0);
          })(offset)}
          window.setTimeout(function(){
              $.resumableObj.fire('chunkingComplete',$);
          },0);
        };
        $.progress = function(){
          if(_error) return(1);
          // Sum up progress across everything
          var ret = 0;
          var error = false;
          $h.each($.chunks, function(c){
            if(c.status()=='error') error = true;
            ret += c.progress(true); // get chunk progress relative to entire file
          });
          ret = (error ? 1 : (ret>0.99999 ? 1 : ret));
          ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused
          $._prevProgress = ret;
          return(ret);
        };
        $.isUploading = function(){
          var uploading = false;
          $h.each($.chunks, function(chunk){
            if(chunk.status()=='uploading') {
              uploading = true;
              return(false);
            }
          });
          return(uploading);
        };
        $.isComplete = function(){
          var outstanding = false;
          if ($.preprocessState === 1) {
            return(false);
          }
          $h.each($.chunks, function(chunk){
            var status = chunk.status();
            if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) {
              outstanding = true;
              return(false);
            }
          });
          return(!outstanding);
        };
        $.pause = function(pause){
            if(typeof(pause)==='undefined'){
                $._pause = ($._pause ? false : true);
            }else{
                $._pause = pause;
            }
        };
        $.isPaused = function() {
          return $._pause;
        };
        $.preprocessFinished = function(){
          $.preprocessState = 2;
          $.upload();
        };
        $.upload = function () {
          var found = false;
          if ($.isPaused() === false) {
            var preprocess = $.getOpt('preprocessFile');
            if(typeof preprocess === 'function') {
              switch($.preprocessState) {
              case 0: $.preprocessState = 1; preprocess($); return(true);
              case 1: return(true);
              case 2: break;
              }
            }
            $h.each($.chunks, function (chunk) {
              if (chunk.status() == 'pending' && chunk.preprocessState !== 1) {
                chunk.send();
                found = true;
                return(false);
              }
            });
          }
          return(found);
        }
        $.markChunksCompleted = function (chunkNumber) {
          if (!$.chunks || $.chunks.length <= chunkNumber) {
              return;
          }
          for (var num = 0; num < chunkNumber; num++) {
              $.chunks[num].markComplete = true;
          }
        };
  
        // Bootstrap and return
        $.resumableObj.fire('chunkingStart', $);
        $.bootstrap();
        return(this);
      }
  
  
      function ResumableChunk(resumableObj, fileObj, offset, callback){
        var $ = this;
        $.opts = {};
        $.getOpt = resumableObj.getOpt;
        $.resumableObj = resumableObj;
        $.fileObj = fileObj;
        $.fileObjSize = fileObj.size;
        $.fileObjType = fileObj.file.type;
        $.offset = offset;
        $.callback = callback;
        $.lastProgressCallback = (new Date);
        $.tested = false;
        $.retries = 0;
        $.pendingRetry = false;
        $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished
        $.markComplete = false;
  
        // Computed properties
        var chunkSize = $.getOpt('chunkSize');
        $.loaded = 0;
        $.startByte = $.offset*chunkSize;
        $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize);
        if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
          // The last chunk will be bigger than the chunk size, but less than 2*chunkSize
          $.endByte = $.fileObjSize;
        }
        $.xhr = null;
  
        // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
        $.test = function(){
          // Set up request and listen for event
          $.xhr = new XMLHttpRequest();
  
          var testHandler = function(e){
            $.tested = true;
            var status = $.status();
            if(status=='success') {
              $.callback(status, $.message());
              $.resumableObj.uploadNextChunk();
            } else {
              $.send();
            }
          };
          $.xhr.addEventListener('load', testHandler, false);
          $.xhr.addEventListener('error', testHandler, false);
          $.xhr.addEventListener('timeout', testHandler, false);
  
          // Add data from the query options
          var params = [];
          var parameterNamespace = $.getOpt('parameterNamespace');
          var customQuery = $.getOpt('query');
          if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
          $h.each(customQuery, function(k,v){
            params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('='));
          });
          // Add extra data to identify chunk
          params = params.concat(
            [
              // define key/value pairs for additional parameters
              ['chunkNumberParameterName', $.offset + 1],
              ['chunkSizeParameterName', $.getOpt('chunkSize')],
              ['currentChunkSizeParameterName', $.endByte - $.startByte],
              ['totalSizeParameterName', $.fileObjSize],
              ['typeParameterName', $.fileObjType],
              ['identifierParameterName', $.fileObj.uniqueIdentifier],
              ['fileNameParameterName', $.fileObj.fileName],
              ['relativePathParameterName', $.fileObj.relativePath],
              ['totalChunksParameterName', $.fileObj.chunks.length]
            ].filter(function(pair){
              // include items that resolve to truthy values
              // i.e. exclude false, null, undefined and empty strings
              return $.getOpt(pair[0]);
            })
            .map(function(pair){
              // map each key/value pair to its final form
              return [
                parameterNamespace + $.getOpt(pair[0]),
                encodeURIComponent(pair[1])
              ].join('=');
            })
          );
          // Append the relevant chunk and send it
          $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params));
          $.xhr.timeout = $.getOpt('xhrTimeout');
          $.xhr.withCredentials = $.getOpt('withCredentials');
          // Add data from header options
          var customHeaders = $.getOpt('headers');
          if(typeof customHeaders === 'function') {
            customHeaders = customHeaders($.fileObj, $);
          }
          $h.each(customHeaders, function(k,v) {
            $.xhr.setRequestHeader(k, v);
          });
          $.xhr.send(null);
        };
  
        $.preprocessFinished = function(){
          $.preprocessState = 2;
          $.send();
        };
  
        // send() uploads the actual data in a POST call
        $.send = function(){
          var preprocess = $.getOpt('preprocess');
          if(typeof preprocess === 'function') {
            switch($.preprocessState) {
            case 0: $.preprocessState = 1; preprocess($); return;
            case 1: return;
            case 2: break;
            }
          }
          if($.getOpt('testChunks') && !$.tested) {
            $.test();
            return;
          }
  
          // Set up request and listen for event
          $.xhr = new XMLHttpRequest();
  
          // Progress
          $.xhr.upload.addEventListener('progress', function(e){
            if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) {
              $.callback('progress');
              $.lastProgressCallback = (new Date);
            }
            $.loaded=e.loaded||0;
          }, false);
          $.loaded = 0;
          $.pendingRetry = false;
          $.callback('progress');
  
          // Done (either done, failed or retry)
          var doneHandler = function(e){
            var status = $.status();
            if(status=='success'||status=='error') {
              $.callback(status, $.message());
              $.resumableObj.uploadNextChunk();
            } else {
              $.callback('retry', $.message());
              $.abort();
              $.retries++;
              var retryInterval = $.getOpt('chunkRetryInterval');
              if(retryInterval !== undefined) {
                $.pendingRetry = true;
                setTimeout($.send, retryInterval);
              } else {
                $.send();
              }
            }
          };
          $.xhr.addEventListener('load', doneHandler, false);
          $.xhr.addEventListener('error', doneHandler, false);
          $.xhr.addEventListener('timeout', doneHandler, false);
  
          // Set up the basic query data from Resumable
          var query = [
            ['chunkNumberParameterName', $.offset + 1],
            ['chunkSizeParameterName', $.getOpt('chunkSize')],
            ['currentChunkSizeParameterName', $.endByte - $.startByte],
            ['totalSizeParameterName', $.fileObjSize],
            ['typeParameterName', $.fileObjType],
            ['identifierParameterName', $.fileObj.uniqueIdentifier],
            ['fileNameParameterName', $.fileObj.fileName],
            ['relativePathParameterName', $.fileObj.relativePath],
            ['totalChunksParameterName', $.fileObj.chunks.length],
          ].filter(function(pair){
            // include items that resolve to truthy values
            // i.e. exclude false, null, undefined and empty strings
            return $.getOpt(pair[0]);
          })
          .reduce(function(query, pair){
            // assign query key/value
            query[$.getOpt(pair[0])] = pair[1];
            return query;
          }, {});
          // Mix in custom data
          var customQuery = $.getOpt('query');
          if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $);
          $h.each(customQuery, function(k,v){
            query[k] = v;
          });
  
          var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice')));
          var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : "");
          var data = null;
          var params = [];
  
          var parameterNamespace = $.getOpt('parameterNamespace');
                  if ($.getOpt('method') === 'octet') {
                      // Add data from the query options
                      data = bytes;
                      $h.each(query, function (k, v) {
                          params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
                      });
                  } else {
                      // Add data from the query options
                      data = new FormData();
                      $h.each(query, function (k, v) {
                          data.append(parameterNamespace + k, v);
                          params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='));
                      });
                      if ($.getOpt('chunkFormat') == 'blob') {
                          data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName);
                      }
                      else if ($.getOpt('chunkFormat') == 'base64') {
                          var fr = new FileReader();
                          fr.onload = function (e) {
                              data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result);
                              $.xhr.send(data);
                          }
                          fr.readAsDataURL(bytes);
                      }
                  }
  
          var target = $h.getTarget('upload', params);
          var method = $.getOpt('uploadMethod');
  
          $.xhr.open(method, target);
          if ($.getOpt('method') === 'octet') {
            $.xhr.setRequestHeader('Content-Type', 'application/octet-stream');
          }
          $.xhr.timeout = $.getOpt('xhrTimeout');
          $.xhr.withCredentials = $.getOpt('withCredentials');
          // Add data from header options
          var customHeaders = $.getOpt('headers');
          if(typeof customHeaders === 'function') {
            customHeaders = customHeaders($.fileObj, $);
          }
  
          $h.each(customHeaders, function(k,v) {
            $.xhr.setRequestHeader(k, v);
          });
  
          if ($.getOpt('chunkFormat') == 'blob') {
              $.xhr.send(data);
          }
        };
        $.abort = function(){
          // Abort and reset
          if($.xhr) $.xhr.abort();
          $.xhr = null;
        };
        $.status = function(){
          // Returns: 'pending', 'uploading', 'success', 'error'
          if($.pendingRetry) {
            // if pending retry then that's effectively the same as actively uploading,
            // there might just be a slight delay before the retry starts
            return('uploading');
          } else if($.markComplete) {
            return 'success';
          } else if(!$.xhr) {
            return('pending');
          } else if($.xhr.readyState<4) {
            // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
            return('uploading');
          } else {
            if($.xhr.status == 200 || $.xhr.status == 201) {
              // HTTP 200, 201 (created)
              return('success');
            } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) {
              // HTTP 400, 404, 409, 415, 500, 501 (permanent error)
              return('error');
            } else {
              // this should never happen, but we'll reset and queue a retry
              // a likely case for this would be 503 service unavailable
              $.abort();
              return('pending');
            }
          }
        };
        $.message = function(){
          return($.xhr ? $.xhr.responseText : '');
        };
        $.progress = function(relative){
          if(typeof(relative)==='undefined') relative = false;
          var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1);
          if($.pendingRetry) return(0);
          if((!$.xhr || !$.xhr.status) && !$.markComplete) factor*=.95;
          var s = $.status();
          switch(s){
          case 'success':
          case 'error':
            return(1*factor);
          case 'pending':
            return(0*factor);
          default:
            return($.loaded/($.endByte-$.startByte)*factor);
          }
        };
        return(this);
      }
  
      // QUEUE
      $.uploadNextChunk = function(){
        var found = false;
  
        // In some cases (such as videos) it's really handy to upload the first
        // and last chunk of a file quickly; this let's the server check the file's
        // metadata and determine if there's even a point in continuing.
        if ($.getOpt('prioritizeFirstAndLastChunk')) {
          $h.each($.files, function(file){
            if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) {
              file.chunks[0].send();
              found = true;
              return(false);
            }
            if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) {
              file.chunks[file.chunks.length-1].send();
              found = true;
              return(false);
            }
          });
          if(found) return(true);
        }
  
        // Now, simply look for the next, best thing to upload
        $h.each($.files, function(file){
          found = file.upload();
          if(found) return(false);
        });
        if(found) return(true);
  
        // The are no more outstanding chunks to upload, check is everything is done
        var outstanding = false;
        $h.each($.files, function(file){
          if(!file.isComplete()) {
            outstanding = true;
            return(false);
          }
        });
        if(!outstanding) {
          // All chunks have been uploaded, complete
          $.fire('complete');
        }
        return(false);
      };
  
  
      // PUBLIC METHODS FOR RESUMABLE.JS
      $.assignBrowse = function(domNodes, isDirectory){
        if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
        $h.each(domNodes, function(domNode) {
          var input;
          if(domNode.tagName==='INPUT' && domNode.type==='file'){
            input = domNode;
          } else {
            input = document.createElement('input');
            input.setAttribute('type', 'file');
            input.style.display = 'none';
            domNode.addEventListener('click', function(){
              input.style.opacity = 0;
              input.style.display='block';
              input.focus();
              input.click();
              input.style.display='none';
            }, false);
            domNode.appendChild(input);
          }
          var maxFiles = $.getOpt('maxFiles');
          if (typeof(maxFiles)==='undefined'||maxFiles!=1){
            input.setAttribute('multiple', 'multiple');
          } else {
            input.removeAttribute('multiple');
          }
          if(isDirectory){
            input.setAttribute('webkitdirectory', 'webkitdirectory');
          } else {
            input.removeAttribute('webkitdirectory');
          }
          var fileTypes = $.getOpt('fileType');
          if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) {
            input.setAttribute('accept', fileTypes.map(function (e) {
              e = e.replace(/\s/g, '').toLowerCase();
              if(e.match(/^[^.][^/]+$/)){
                e = '.' + e;
              }
              return e;
            }).join(','));
          }
          else {
            input.removeAttribute('accept');
          }
          // When new files are added, simply append them to the overall list
          input.addEventListener('change', function(e){
            appendFilesFromFileList(e.target.files,e);
            var clearInput = $.getOpt('clearInput');
            if (clearInput) {
              e.target.value = '';
            }
          }, false);
        });
      };
      $.assignDrop = function(domNodes){
        if(typeof(domNodes.length)=='undefined') domNodes = [domNodes];
  
        $h.each(domNodes, function(domNode) {
          domNode.addEventListener('dragover', onDragOverEnter, false);
          domNode.addEventListener('dragenter', onDragOverEnter, false);
          domNode.addEventListener('dragleave', onDragLeave, false);
          domNode.addEventListener('drop', onDrop, false);
        });
      };
      $.unAssignDrop = function(domNodes) {
        if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes];
  
        $h.each(domNodes, function(domNode) {
          domNode.removeEventListener('dragover', onDragOverEnter);
          domNode.removeEventListener('dragenter', onDragOverEnter);
          domNode.removeEventListener('dragleave', onDragLeave);
          domNode.removeEventListener('drop', onDrop);
        });
      };
      $.isUploading = function(){
        var uploading = false;
        $h.each($.files, function(file){
          if (file.isUploading()) {
            uploading = true;
            return(false);
          }
        });
        return(uploading);
      };
      $.upload = function(){
        // Make sure we don't start too many uploads at once
        if($.isUploading()) return;
        // Kick off the queue
        $.fire('uploadStart');
        for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) {
          $.uploadNextChunk();
        }
      };
      $.pause = function(){
        // Resume all chunks currently being uploaded
        $h.each($.files, function(file){
          file.abort();
        });
        $.fire('pause');
      };
      $.cancel = function(){
        $.fire('beforeCancel');
        for(var i = $.files.length - 1; i >= 0; i--) {
          $.files[i].cancel();
        }
        $.fire('cancel');
      };
      $.progress = function(){
        var totalDone = 0;
        var totalSize = 0;
        // Resume all chunks currently being uploaded
        $h.each($.files, function(file){
          totalDone += file.progress()*file.size;
          totalSize += file.size;
        });
        return(totalSize>0 ? totalDone/totalSize : 0);
      };
      $.addFile = function(file, event){
        appendFilesFromFileList([file], event);
      };
      $.addFiles = function(files, event){
        appendFilesFromFileList(files, event);
      };
      $.removeFile = function(file){
        for(var i = $.files.length - 1; i >= 0; i--) {
          if($.files[i] === file) {
            $.files.splice(i, 1);
          }
        }
      };
      $.getFromUniqueIdentifier = function(uniqueIdentifier){
        var ret = false;
        $h.each($.files, function(f){
          if(f.uniqueIdentifier==uniqueIdentifier) ret = f;
        });
        return(ret);
      };
      $.getSize = function(){
        var totalSize = 0;
        $h.each($.files, function(file){
          totalSize += file.size;
        });
        return(totalSize);
      };
      $.handleDropEvent = function (e) {
        onDrop(e);
      };
      $.handleChangeEvent = function (e) {
        appendFilesFromFileList(e.target.files, e);
        e.target.value = '';
      };
      $.updateQuery = function(query){
          $.opts.query = query;
      };
  
      return(this);
    };
  
  
    // Node.js-style export for Node and Component
    if (typeof module != 'undefined') {
      module.exports = Resumable;
    } else if (typeof define === "function" && define.amd) {
      // AMD/requirejs: Define the module
      define(function(){
        return Resumable;
      });
    } else {
      // Browser: Expose to window
      window.Resumable = Resumable;
    }
  
  })();
  