/* * Class: fileUploader * Use: Upload multiple files using jquery * Author: Matt Brailsford * Version: 1.0 */ (function ($, window, document, undefined) { $.event.props.push('dataTransfer'); var defaultOptions = { autoUpload: false, limit: false, allowedExtension: 'jpg|jpeg|gif|png|pdf', dropTarget: null, //Callbacks onValidationError: null, onAdd: function () { }, onRemove: function () { }, onUpload: function () { }, onUploadAll: function () { }, onProgress: function () { }, onDone: function () { }, onDoneAll: function () { } }; var isHtml5 = (window.FormData) ? true : false; function FileUploader(el, options) { var self = this; var $el = $(el); // Setup instance self.input = el; self.form = $el.parents("form"); self.uploaderId = ++$.fileUploader.count; self.opts = $.extend({}, defaultOptions, options); // Initialize self._init(); // Return configures component return self; } FileUploader.prototype = { // Private methods _init: function () { var self = this; // Init vars self.wrapperId = 'fu-fileUploader-' + self.uploaderId; self.wrapper = '
'; self.wrapperSelector = '#' + self.wrapperId; self.formContainerSelector = self.wrapperSelector + " .fu-formContainer"; self.itemContainerSelector = self.wrapperSelector + " .fu-itemContainer"; self.itemCount = 0; self.progressTimeout = null; self.inProgressJqXhr = null; self.inProgressItemId = 0; //prepend wrapper markup self.form.before(self.wrapper); //clear all form data $(":file", self.form).each(function () { $(this).val(''); }); // Hide the form (we'll just use it as a template) self.form.hide(); // Create the actual form users will interact with self._createNewForm(); // Hookup drag and drop support $(self.opts.dropTarget !== null ? self.opts.dropTarget : self.wrapperSelector).bind('dragenter dragover', false).bind('drop', function (e) { e.stopPropagation(); e.preventDefault(); self._addFileHtml5(e.dataTransfer); }); }, _getFileName: function (filePath) { var fileName = filePath; if (fileName.indexOf('/') > -1) { fileName = fileName.substring(fileName.lastIndexOf('/') + 1); } else if (fileName.indexOf('\\') > -1) { fileName = fileName.substring(fileName.lastIndexOf('\\') + 1); } return fileName; }, _validateFileName: function (fileName) { var self = this; var extensions = new RegExp(self.opts.allowedExtension + '$', 'i'); if (extensions.test(fileName)) { return fileName; } else { return -1; } }, _getFileSize: function (file) { var fileSize = 0; if (file.size > 1024 * 1024) { fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; } else { fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; } return fileSize; }, _createNewForm: function () { var self = this; var id = ++self.itemCount; var itemId = 'fu-item-' + self.uploaderId + '-' + id; var iframeId = 'fu-frame-' + self.uploaderId + '-' + id; var formId = 'fu-form-' + self.uploaderId + '-' + id; // Create item container var item = $('
'); // Create iFrame for iFrame based uploads $('').attr({ id: iframeId, src: 'about:blank', style: 'display:none' }).prependTo(item); // Clone the form var newForm = self.form.clone().attr({ id: formId, target: iframeId }).prependTo(item).show(); // Remove any id's / css classes from file inputs $(":file", newForm).each(function (idx, itm) { $(itm).removeAttr("id").removeAttr("class"); }); // Hide everything in the form, other than file inputs newForm.children().each(function () { var isFile = $(this).is(':file'); if (!isFile && $(this).find(':file').length === 0) { $(this).hide(); } }); // Append everything to the form container item.prependTo(self.formContainerSelector); // Attatch event listener to file input $(":file", newForm).change(function () { if (isHtml5) { self._addFileHtml5(this); } else { self._addFile($(this)); } }); }, _addFileHtml5: function (input) { var self = this; $.each(input.files, function (index, file) { self._addFile(file); }); self._afterAddFile(); }, _addFile: function (input) { var self = this; var id = self.itemCount; var $item = $('#fu-item-' + self.uploaderId + '-' + id); // Validate file var filename = (isHtml5) ? input.name : self._getFileName($(input).val()); if (self._validateFileName(filename) == -1) { if ($.isFunction(self.opts.onValidationError)) { self.opts.onValidationError({ input: input, type: 'format' }); } else { alert('Invalid file!'); } $item.find("form :file").val(''); return false; } // Check to see if we have reached the limit if (self.opts.limit !== false && self.opts.limit >= $(self.itemContainerSelector + " .fu-item").length) { if ($.isFunction(self.opts.onValidationError)) { self.opts.onValidationError({ input: input, type: 'limit' }); } else { alert('Maximum limit reached!'); } $item.find("form :file").val(''); return false; } // Create context object var data = { uploaderId: self.uploaderId, itemId: id, name: filename, size: (isHtml5) ? self._getFileSize(input) : "Unkown", status: 'queued', progress: 0, input: input, context: null // Somewhere for users to store cutom data }; //Store data in item $item.data('data', data); // Move item to queue, and hide it $item.appendTo(self.itemContainerSelector).hide(); //Create a new form self._createNewForm(); //Callback on file queued self.opts.onAdd(data); if (!isHtml5) { self._afterAddFile(); } }, _afterAddFile: function () { var self = this; if (self.opts.autoUpload) { self._processNextItemInQueue(true); } }, _processNextItemInQueue: function (autoProceed) { var self = this; var totalItems = $(self.itemContainerSelector + " .fu-item"); if (totalItems.length > 0) { var $pendingUpload = $(totalItems.get(0)); var data = $pendingUpload.data('data'); //before upload var response = self.opts.onUpload(data); if (response === false) { return false; //TODO: Raise onDone event? } self.inProgressItemId = data.itemId; data.status = "inprogress"; data.progress = 0; if (isHtml5) { //Upload Using Html5 api self._uploadHtml5($pendingUpload, autoProceed); } else { //upload using iframe self._uploadIFrame($pendingUpload, autoProceed); } } }, _uploadHtml5: function ($item, autoProceed) { var self = this; var $form = $item.find("form"); var data = $item.data('data'); var file = data.input; if (file) { var fd = new FormData(); fd.append($(":file", $form).first().attr('name'), file); //get other form input and append to formData $form.find(':input').each(function () { if (!$(this).is(':file')) { fd.append($(this).attr('name'), $(this).val()); } }); //Upload using jQuery AJAX self.inProgressJqXhr = $.ajax({ url: $form.attr('action'), data: fd, cache: false, contentType: false, processData: false, type: 'POST', xhr: function () { var req = $.ajaxSettings.xhr(); if (req) { req.upload.addEventListener('progress', function (ev) { data.progress = Math.round(ev.loaded * 100 / ev.total); self.opts.onProgress(data); }, false); } return req; } }).success(function (d) { data.status = 'success'; self.opts.onDone(data); }).error(function (jqXhr, textStatus, errorThrown) { data.status = 'error'; data.message = errorThrown; self.opts.onDone(data); }).complete(function (jqXhr, textStatus) { self._afterUpload($item, autoProceed); }); } }, _uploadIFrame: function ($item, autoProceed) { var self = this; var $form = $item.find("form"); var $iframe = $item.find("iframe"); var data = $item.data('data'); self._startDummyProgress(data); $form.submit(); $iframe.load(function () { self._stopDummyProgress(data, true); // Read content returned var rawResponse; if (this.contentDocument) { rawResponse = this.contentDocument.body.innerHTML; } else { rawResponse = this.contentWindow.document.body.innerHTML; } var response = $.parseJSON(rawResponse); if (response.success) { data.status = 'success'; } else { data.status = 'error'; data.message = 'An error occured whilst uploading.'; } self.opts.onDone(data); self._afterUpload($item, autoProceed); }); }, _startDummyProgress: function (data, count) { var self = this; if (count === undefined) { count = 0; } if ($.fileUploader.percentageInterval[count]) { data.progress = $.fileUploader.percentageInterval[count] + Math.floor(Math.random() * 5 + 1); self.opts.onProgress(data); } if ($.fileUploader.timeInterval[count]) { self.progressTimeout = setTimeout(function () { self._startDummyProgress(data, ++count); }, $.fileUploader.timeInterval[count] * 1000); } }, _stopDummyProgress: function (data, success) { var self = this; clearTimeout(self.progressTimeout); if (success) { // Force a 100% onProgress event data.progress = 100; self.opts.onProgress(data); } }, _afterUpload: function ($item, autoProceed) { var self = this; var data = $item.data('data'); if (data.itemId == self.inProgressItemId) { self.inProgressItemId = 0; self.inProgressJqXhr = null; } $item.remove(); if ($(self.itemContainerSelector + " .fu-item").length > 0) { if (autoProceed) { self._processNextItemInQueue(autoProceed); } } else { self.opts.onDoneAll(); } }, // Public methods uploadAll: function () { var self = this; var result = self.opts.onUploadAll(self); if (result === false) { return false; } self._processNextItemInQueue(true); }, cancelAll: function () { var self = this; // Remove current form $(self.formContainerSelector).empty(); var queuedItems = $(self.itemContainerSelector + " .fu-item"); for (var i = 0; i < queuedItems.length; i++) { var data = $(queuedItems.get(i)).data('data'); self.cancelItem(data.itemId); } self.itemCount = 0; // Recreate the form self._createNewForm(); }, cancelItem: function (itemId) { var self = this; var $item = $("#fu-item-" + self.uploaderId + "-" + itemId); var data = $item.data('data'); // If the item to cancel is in progress, abort the upload if (self.inProgressItemId == itemId && self.inProgressJqXhr) { //NB: The abort event handler will cleanup the aborted item from the queue self.inProgressJqXhr.abort(); } else { // Update the status data.status = 'canceled'; self.opts.onDone(data); self._afterUpload($item, false); } } }; $.fileUploader = { version: '1.0', count: 0, timeInterval: [1, 2, 4, 2, 1, 5, 5, 5, 5], percentageInterval: [10, 20, 30, 40, 60, 80, 85, 90, 95] }; $.fn.fileUploader = function (o) { var args = arguments; if (typeof o === 'string') { var api = this.fileUploaderApi(); if (api[o]) { return api[o].apply(api, Array.prototype.slice.call(args, 1)); } else { $.error('Method ' + o + ' does not exist on jQuery.fileUploader'); } } else if (typeof o === 'object' || !o) { return this.each(function () { var fileUploader = new FileUploader(this, o); $(this).data("fileuploader_api", fileUploader); }); } }; $.fn.fileUploaderApi = function () { //ensure there's only 1 if (this.length != 1) { throw "Requesting the API can only match one element"; } //ensure thsi is a collapse panel if (this.data("fileuploader_api") === null) { throw "The matching element had not been bound to a fileUploader"; } return this.data("fileuploader_api"); }; })(jQuery, window, document);