text: ﻿/*
  * MIT Licensed
  * http://www.23developer.com/opensource
  * http://github.com/23/resumable.js
  * Steffen Tiedemann Christensen, steffen@23company.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, 401, 403, 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('&');
  
                  if (joinedParams) target = target + separator + joinedParams;
  
                  return target;
              }
          };
  
          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, e);
              }
              //else handle them as files
              else if (e.dataTransfer && e.dataTransfer.files) {
                  loadFiles(e.dataTransfer.files, e);
              }
          };
          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
                      var entry = item;
                      if ('function' === typeof item.webkitGetAsEntry) {
                          entry = item.webkitGetAsEntry();
                      }
                      return processItem.bind(null, entry, "", 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) {
                          $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent));
                          $.resumableObj.fire('chunkingProgress', $, offset / maxOffset);
                      })(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') {
          // left here for backwards compatibility
          module.exports = Resumable;
          module.exports.Resumable = 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;
      }
  
  })();