REBELinBLUE/deployer

View on GitHub
resources/assets/src/handlers/uploader.js

Summary

Maintainability
C
1 day
Test Coverage
// TODO: Convert to ES6
let iframeCount = 0;

function Uploader(options) {
  if (!(this instanceof Uploader)) {
    return new Uploader(options);
  }

  if (isString(options)) {
    options = { trigger: options };
  }

  const settings = {
    trigger: null,
    name: null,
    action: null,
    data: null,
    accept: null,
    change: null,
    error: null,
    multiple: true,
    success: null
  };

  if (options) {
    $.extend(settings, options);
  }

  const $trigger = $(settings.trigger);

  settings.action = settings.action || $trigger.data('action') || '/upload';
  settings.name = settings.name || $trigger.attr('name') || $trigger.data('name') || 'file';
  settings.data = settings.data || parse($trigger.data('data'));
  settings.accept = settings.accept || $trigger.data('accept');
  settings.success = settings.success || $trigger.data('success');

  this.settings = settings;

  this.setup();
  this.bind();
}

// initialize
// create input, form, iframe
Uploader.prototype.setup = function() {
  this.form = $(`<form method="post" enctype="multipart/form-data" target="" action="${this.settings.action}" />`);

  this.iframe = newIframe();
  this.form.attr('target', this.iframe.attr('name'));

  const data = this.settings.data;
  this.form.append(createInputs(data));

  let uploader = 'iframe';
  if (window.FormData) {
    uploader = 'formdata';
  }

  this.form.append(createInputs({
    _uploader_: uploader
  }));

  const input = document.createElement('input');
  input.type = 'file';
  input.name = this.settings.name;

  if (this.settings.accept) {
    input.accept = this.settings.accept;
  }

  if (this.settings.multiple) {
    input.multiple = true;
    input.setAttribute('multiple', 'multiple');
  }

  this.input = $(input);

  var $trigger = $(this.settings.trigger);
  this.input.attr('hidefocus', true).css({
    position: 'absolute',
    top: 0,
    right: 0,
    opacity: 0,
    outline: 0,
    cursor: 'pointer',
    height: $trigger.outerHeight(),
    fontSize: Math.max(64, $trigger.outerHeight() * 5),
  });

  this.form.append(this.input);

  this.form.css({
    position: 'absolute',
    top: $trigger.offset().top,
    left: $trigger.offset().left,
    overflow: 'hidden',
    width: $trigger.outerWidth(),
    height: $trigger.outerHeight(),
    zIndex: findzIndex($trigger) + 10,
  }).appendTo('body');

  return this;
};

// bind events
Uploader.prototype.bind = function() {
  const self = this;
  const $trigger = $(self.settings.trigger);

  $trigger.mouseenter(function() {
    self.form.css({
      top: $trigger.offset().top,
      left: $trigger.offset().left,
      width: $trigger.outerWidth(),
      height: $trigger.outerHeight(),
    });
  });

  self.bindInput();
};

Uploader.prototype.bindInput = function() {
  const self = this;

  self.input.change(function(e) {
    // ie9 don't support FileList Object
    // http://stackoverflow.com/questions/12830058/ie8-input-type-file-get-files
    self._files = this.files || [{ name: e.target.value }];

    const file = self.input.val();
    if (self.settings.change) {
      self.settings.change.call(self, self._files);
    } else if (file) {
      return self.submit();
    }
  });
};

// handle submit event
// prepare for submiting form
Uploader.prototype.submit = function() {
  const self = this;

  if (window.FormData && self._files) {
    // build a FormData
    const form = new FormData(self.form.get(0));

    // use FormData to upload
    form.append(self.settings.name, self._files);

    let optionXhr;
    if (self.settings.progress) {
      // fix the progress target file
      const files = self._files;

      optionXhr = function() {
        const xhr = $.ajaxSettings.xhr();

        if (xhr.upload) {
          xhr.upload.addEventListener('progress', function(event) {
            let percent = 0;
            const position = event.loaded || event.position; /*event.position is deprecated*/
            const total = event.total;

            if (event.lengthComputable) {
              percent = Math.ceil(position / total * 100);
            }

            self.settings.progress(event, position, total, percent, files);
          }, false);
        }

        return xhr;
      };
    }

    $.ajax({
      url: self.settings.action,
      type: 'post',
      processData: false,
      contentType: false,
      data: form,
      xhr: optionXhr,
      context: this,
      success: self.settings.success,
      error: self.settings.error,
    });

    return this;
  } else {
    // iframe upload
    self.iframe = newIframe();
    self.form.attr('target', self.iframe.attr('name'));

    $('body').append(self.iframe);

    self.iframe.one('load', function() {
      // https://github.com/blueimp/jQuery-File-Upload/blob/9.5.6/js/jquery.iframe-transport.js#L102
      // Fix for IE endless progress bar activity bug
      // (happens on form submits to iframe targets):
      $('<iframe src="javascript:false;"></iframe>')
        .appendTo(self.form)
        .remove();

      let response;
      try {
        response = $(this).contents().find('body').html();
      } catch (e) {
        response = 'cross-domain';
      }

      $(this).remove();

      if (!response) {
        if (self.settings.error) {
          self.settings.error(self.input.val());
        }
      } else {
        if (self.settings.success) {
          self.settings.success(response);
        }
      }
    });

    self.form.submit();
  }

  return this;
};

Uploader.prototype.refreshInput = function() {
  // replace the input element, or the same file can not to be uploaded
  const newInput = this.input.clone();

  this.input.before(newInput);
  this.input.off('change');
  this.input.remove();
  this.input = newInput;

  this.bindInput();
};

// handle change event
// when value in file input changed
Uploader.prototype.change = function(callback) {
  if (!callback) {
    return this;
  }

  this.settings.change = callback;

  return this;
};

// handle when upload success
Uploader.prototype.success = function(callback) {
  const self = this;

  this.settings.success = function(response) {
    self.refreshInput();

    if (callback) {
      callback(response);
    }
  };

  return this;
};

// handle when upload success
Uploader.prototype.error = function(callback) {
  const self = this;

  this.settings.error = function(response) {
    if (callback) {
      self.refreshInput();

      callback(response);
    }
  };
  return this;
};

// enable
Uploader.prototype.enable = function() {
  this.input.prop('disabled', false);
  this.input.css('cursor', 'pointer');
};

// disable
Uploader.prototype.disable = function() {
  this.input.prop('disabled', true);
  this.input.css('cursor', 'not-allowed');
};

// Helpers
// -------------

function isString(val) {
  return Object.prototype.toString.call(val) === '[object String]';
}

function createInputs(data) {
  if (!data) {
    return [];
  }

  const inputs = [];
  for (var name in data) {
    const input = document.createElement('input');

    input.type = 'hidden';
    input.name = name;
    input.value = data[name];

    inputs.push(input);
  }

  return inputs;
}

function parse(str) {
  if (!str) {
    return {};
  }

  const ret = {};

  const pairs = str.split('&');
  const unescape = s => decodeURIComponent(s.replace(/\+/g, ' '));

  for (let i = 0; i < pairs.length; i++) {
    const pair = pairs[i].split('=');
    const key = unescape(pair[0]);
    const val = unescape(pair[1]);

    ret[key] = val;
  }

  return ret;
}

function findzIndex($node) {
  const parents = $node.parentsUntil('body');
  let zIndex = 0;

  for (let i = 0; i < parents.length; i++) {
    const item = parents.eq(i);

    if (item.css('position') !== 'static') {
      zIndex = parseInt(item.css('zIndex'), 10) || zIndex;
    }
  }

  return zIndex;
}

function newIframe() {
  const iframe = $(`<iframe name="iframe-uploader-${iframeCount}" />`).hide();

  iframeCount += 1;

  return iframe;
}

/*
function MultipleUploader(options) {
  if (!(this instanceof MultipleUploader)) {
    return new MultipleUploader(options);
  }

  if (isString(options)) {
    options = {trigger: options};
  }
  var $trigger = $(options.trigger);

  var uploaders = [];
  $trigger.each(function(i, item) {
    options.trigger = item;
    uploaders.push(new Uploader(options));
  });
  this._uploaders = uploaders;
}
MultipleUploader.prototype.submit = function() {
  $.each(this._uploaders, function(i, item) {
    item.submit();
  });
  return this;
};
MultipleUploader.prototype.change = function(callback) {
  $.each(this._uploaders, function(i, item) {
    item.change(callback);
  });
  return this;
};
MultipleUploader.prototype.success = function(callback) {
  $.each(this._uploaders, function(i, item) {
    item.success(callback);
  });
  return this;
};
MultipleUploader.prototype.error = function(callback) {
  $.each(this._uploaders, function(i, item) {
    item.error(callback);
  });
  return this;
};
MultipleUploader.prototype.enable = function (){
  $.each(this._uploaders, function (i, item){
    item.enable();
  });
  return this;
};
MultipleUploader.prototype.disable = function (){
  $.each(this._uploaders, function (i, item){
    item.disable();
  });
  return this;
};
MultipleUploader.Uploader = Uploader;
*/

module.exports = Uploader;