/* * jQuery File Upload User Interface Plugin 6.7 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: * http://www.opensource.org/licenses/MIT */ /*jslint nomen: true, unparam: true, regexp: true */ /*global define, window, document, URL, webkitURL, FileReader */ (function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: define([ 'jquery', 'tmpl', 'load-image', './jquery.fileupload-fp' ], factory); } else { // Browser globals: factory( window.jQuery, window.tmpl, window.loadImage ); } }(function ($, tmpl, loadImage) { 'use strict'; // The UI version extends the FP (file processing) version or the basic // file upload widget and adds complete user interface interaction: var parentWidget = ($.blueimpFP || $.blueimp).fileupload; $.widget('blueimpUI.fileupload', parentWidget, { options: { // By default, files added to the widget are uploaded as soon // as the user clicks on the start buttons. To enable automatic // uploads, set the following option to true: autoUpload: false, // The following option limits the number of files that are // allowed to be uploaded using this widget: maxNumberOfFiles: undefined, // The maximum allowed file size: maxFileSize: undefined, // The minimum allowed file size: minFileSize: undefined, // The regular expression for allowed file types, matches // against either file type or file name: acceptFileTypes: /.+$/i, // The regular expression to define for which files a preview // image is shown, matched against the file type: previewSourceFileTypes: /^image\/(gif|jpeg|png)$/, // The maximum file size of images that are to be displayed as preview: previewSourceMaxFileSize: 5000000, // 5MB // The maximum width of the preview images: previewMaxWidth: 80, // The maximum height of the preview images: previewMaxHeight: 80, // By default, preview images are displayed as canvas elements // if supported by the browser. Set the following option to false // to always display preview images as img elements: previewAsCanvas: true, // The ID of the upload template: uploadTemplateId: 'template-upload', // The ID of the download template: downloadTemplateId: 'template-download', // The container for the list of files. If undefined, it is set to // an element with class "files" inside of the widget element: filesContainer: undefined, // By default, files are appended to the files container. // Set the following option to true, to prepend files instead: prependFiles: false, // The expected data type of the upload response, sets the dataType // option of the $.ajax upload requests: dataType: 'json', // The add callback is invoked as soon as files are added to the fileupload // widget (via file input selection, drag & drop or add API call). // See the basic file upload widget for more information: add: function (e, data) { var that = $(this).data('fileupload'), options = that.options, files = data.files; $(this).fileupload('process', data).done(function () { that._adjustMaxNumberOfFiles(-files.length); data.isAdjusted = true; data.files.valid = data.isValidated = that._validate(files); data.context = that._renderUpload(files).data('data', data); options.filesContainer[ options.prependFiles ? 'prepend' : 'append' ](data.context); that._renderPreviews(files, data.context); that._forceReflow(data.context); that._transition(data.context).done( function () { if ((that._trigger('added', e, data) !== false) && (options.autoUpload || data.autoUpload) && data.autoUpload !== false && data.isValidated) { data.submit(); } } ); }); }, // Callback for the start of each file upload request: send: function (e, data) { var that = $(this).data('fileupload'); if (!data.isValidated) { if (!data.isAdjusted) { that._adjustMaxNumberOfFiles(-data.files.length); } if (!that._validate(data.files)) { return false; } } if (data.context && data.dataType && data.dataType.substr(0, 6) === 'iframe') { // Iframe Transport does not support progress events. // In lack of an indeterminate progress bar, we set // the progress to 100%, showing the full animated bar: data.context .find('.progress').addClass( !$.support.transition && 'progress-animated' ) .find('.bar').css( 'width', parseInt(100, 10) + '%' ); } return that._trigger('sent', e, data); }, // Callback for successful uploads: done: function (e, data) { var that = $(this).data('fileupload'), template, preview; if (data.context) { data.context.each(function (index) { var file = ($.isArray(data.result) && data.result[index]) || {error: 'emptyResult'}; if (file.error) { that._adjustMaxNumberOfFiles(1); } that._transition($(this)).done( function () { var node = $(this); template = that._renderDownload([file]) .css('height', node.height()) .replaceAll(node); that._forceReflow(template); that._transition(template).done( function () { data.context = $(this); that._trigger('completed', e, data); } ); } ); }); } else { template = that._renderDownload(data.result) .appendTo(that.options.filesContainer); that._forceReflow(template); that._transition(template).done( function () { data.context = $(this); that._trigger('completed', e, data); } ); } }, // Callback for failed (abort or error) uploads: fail: function (e, data) { var that = $(this).data('fileupload'), template; that._adjustMaxNumberOfFiles(data.files.length); if (data.context) { data.context.each(function (index) { if (data.errorThrown !== 'abort') { var file = data.files[index]; file.error = file.error || data.errorThrown || true; that._transition($(this)).done( function () { var node = $(this); template = that._renderDownload([file]) .replaceAll(node); that._forceReflow(template); that._transition(template).done( function () { data.context = $(this); that._trigger('failed', e, data); } ); } ); } else { that._transition($(this)).done( function () { $(this).remove(); that._trigger('failed', e, data); } ); } }); } else if (data.errorThrown !== 'abort') { that._adjustMaxNumberOfFiles(-data.files.length); data.context = that._renderUpload(data.files) .appendTo(that.options.filesContainer) .data('data', data); that._forceReflow(data.context); that._transition(data.context).done( function () { data.context = $(this); that._trigger('failed', e, data); } ); } else { that._trigger('failed', e, data); } }, // Callback for upload progress events: progress: function (e, data) { if (data.context) { data.context.find('.progress .bar').css( 'width', parseInt(data.loaded / data.total * 100, 10) + '%' ); } }, // Callback for global upload progress events: progressall: function (e, data) { $(this).find('.fileupload-buttonbar .progress .bar').css( 'width', parseInt(data.loaded / data.total * 100, 10) + '%' ); }, // Callback for uploads start, equivalent to the global ajaxStart event: start: function (e) { var that = $(this).data('fileupload'); that._transition($(this).find('.fileupload-buttonbar .progress')).done( function () { that._trigger('started', e); } ); }, // Callback for uploads stop, equivalent to the global ajaxStop event: stop: function (e) { var that = $(this).data('fileupload'); that._transition($(this).find('.fileupload-buttonbar .progress')).done( function () { $(this).find('.bar').css('width', '0%'); that._trigger('stopped', e); } ); }, // Callback for file deletion: destroy: function (e, data) { var that = $(this).data('fileupload'); if (data.url) { $.ajax(data); } that._adjustMaxNumberOfFiles(1); that._transition(data.context).done( function () { $(this).remove(); that._trigger('destroyed', e, data); } ); } }, // Link handler, that allows to download files // by drag & drop of the links to the desktop: _enableDragToDesktop: function () { var link = $(this), url = link.prop('href'), name = link.prop('download'), type = 'application/octet-stream'; link.bind('dragstart', function (e) { try { e.originalEvent.dataTransfer.setData( 'DownloadURL', [type, name, url].join(':') ); } catch (err) {} }); }, _adjustMaxNumberOfFiles: function (operand) { if (typeof this.options.maxNumberOfFiles === 'number') { this.options.maxNumberOfFiles += operand; if (this.options.maxNumberOfFiles < 1) { this._disableFileInputButton(); } else { this._enableFileInputButton(); } } }, _formatFileSize: function (bytes) { if (typeof bytes !== 'number') { return ''; } if (bytes >= 1000000000) { return (bytes / 1000000000).toFixed(2) + ' GB'; } if (bytes >= 1000000) { return (bytes / 1000000).toFixed(2) + ' MB'; } return (bytes / 1000).toFixed(2) + ' KB'; }, _hasError: function (file) { if (file.error) { return file.error; } // The number of added files is subtracted from // maxNumberOfFiles before validation, so we check if // maxNumberOfFiles is below 0 (instead of below 1): if (this.options.maxNumberOfFiles < 0) { return 'maxNumberOfFiles'; } // Files are accepted if either the file type or the file name // matches against the acceptFileTypes regular expression, as // only browsers with support for the File API report the type: if (!(this.options.acceptFileTypes.test(file.type) || this.options.acceptFileTypes.test(file.name))) { return 'acceptFileTypes'; } if (this.options.maxFileSize && file.size > this.options.maxFileSize) { return 'maxFileSize'; } if (typeof file.size === 'number' && file.size < this.options.minFileSize) { return 'minFileSize'; } return null; }, _validate: function (files) { var that = this, valid = !!files.length; $.each(files, function (index, file) { file.error = that._hasError(file); if (file.error) { valid = false; } }); return valid; }, _renderTemplate: function (func, files) { if (!func) { return $(); } var result = func({ files: files, formatFileSize: this._formatFileSize, options: this.options }); if (result instanceof $) { return result; } return $(this.options.templatesContainer).html(result).children(); }, _renderPreview: function (file, node) { var that = this, options = this.options, dfd = $.Deferred(); return ((loadImage && loadImage( file, function (img) { node.append(img); that._forceReflow(node); that._transition(node).done(function () { dfd.resolveWith(node); }); if (!$.contains(document.body, node[0])) { // If the element is not part of the DOM, // transition events are not triggered, // so we have to resolve manually: dfd.resolveWith(node); } }, { maxWidth: options.previewMaxWidth, maxHeight: options.previewMaxHeight, canvas: options.previewAsCanvas } )) || dfd.resolveWith(node)) && dfd; }, _renderPreviews: function (files, nodes) { var that = this, options = this.options; nodes.find('.preview span').each(function (index, element) { var file = files[index]; if (options.previewSourceFileTypes.test(file.type) && ($.type(options.previewSourceMaxFileSize) !== 'number' || file.size < options.previewSourceMaxFileSize)) { that._processingQueue = that._processingQueue.pipe(function () { var dfd = $.Deferred(); that._renderPreview(file, $(element)).done( function () { dfd.resolveWith(that); } ); return dfd.promise(); }); } }); return this._processingQueue; }, _renderUpload: function (files) { return this._renderTemplate( this.options.uploadTemplate, files ); }, _renderDownload: function (files) { return this._renderTemplate( this.options.downloadTemplate, files ).find('a[download]').each(this._enableDragToDesktop).end(); }, _startHandler: function (e) { e.preventDefault(); var button = $(this), template = button.closest('.template-upload'), data = template.data('data'); if (data && data.submit && !data.jqXHR && data.submit()) { button.prop('disabled', true); } }, _cancelHandler: function (e) { e.preventDefault(); var template = $(this).closest('.template-upload'), data = template.data('data') || {}; if (!data.jqXHR) { data.errorThrown = 'abort'; e.data.fileupload._trigger('fail', e, data); } else { data.jqXHR.abort(); } }, _deleteHandler: function (e) { e.preventDefault(); var button = $(this); e.data.fileupload._trigger('destroy', e, { context: button.closest('.template-download'), url: button.attr('data-url'), type: button.attr('data-type') || 'DELETE', dataType: e.data.fileupload.options.dataType }); }, _forceReflow: function (node) { this._reflow = $.support.transition && node.length && node[0].offsetWidth; }, _transition: function (node) { var that = this, dfd = $.Deferred(); if ($.support.transition && node.hasClass('fade')) { node.bind( $.support.transition.end, function (e) { // Make sure we don't respond to other transitions events // in the container element, e.g. from button elements: if (e.target === node[0]) { node.unbind($.support.transition.end); dfd.resolveWith(node); } } ).toggleClass('in'); } else { node.toggleClass('in'); dfd.resolveWith(node); } return dfd; }, _initButtonBarEventHandlers: function () { var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), filesList = this.options.filesContainer, ns = this.options.namespace; fileUploadButtonBar.find('.start') .bind('click.' + ns, function (e) { e.preventDefault(); filesList.find('.start button').click(); }); fileUploadButtonBar.find('.cancel') .bind('click.' + ns, function (e) { e.preventDefault(); filesList.find('.cancel button').click(); }); fileUploadButtonBar.find('.delete') .bind('click.' + ns, function (e) { e.preventDefault(); filesList.find('.delete input:checked') .siblings('button').click(); fileUploadButtonBar.find('.toggle') .prop('checked', false); }); fileUploadButtonBar.find('.toggle') .bind('change.' + ns, function (e) { filesList.find('.delete input').prop( 'checked', $(this).is(':checked') ); }); }, _destroyButtonBarEventHandlers: function () { this.element.find('.fileupload-buttonbar button') .unbind('click.' + this.options.namespace); this.element.find('.fileupload-buttonbar .toggle') .unbind('change.' + this.options.namespace); }, _initEventHandlers: function () { parentWidget.prototype._initEventHandlers.call(this); var eventData = {fileupload: this}; this.options.filesContainer .delegate( '.start button', 'click.' + this.options.namespace, eventData, this._startHandler ) .delegate( '.cancel button', 'click.' + this.options.namespace, eventData, this._cancelHandler ) .delegate( '.delete button', 'click.' + this.options.namespace, eventData, this._deleteHandler ); this._initButtonBarEventHandlers(); }, _destroyEventHandlers: function () { var options = this.options; this._destroyButtonBarEventHandlers(); options.filesContainer .undelegate('.start button', 'click.' + options.namespace) .undelegate('.cancel button', 'click.' + options.namespace) .undelegate('.delete button', 'click.' + options.namespace); parentWidget.prototype._destroyEventHandlers.call(this); }, _enableFileInputButton: function () { this.element.find('.fileinput-button input') .prop('disabled', false) .parent().removeClass('disabled'); }, _disableFileInputButton: function () { this.element.find('.fileinput-button input') .prop('disabled', true) .parent().addClass('disabled'); }, _initTemplates: function () { var options = this.options; options.templatesContainer = document.createElement( options.filesContainer.prop('nodeName') ); if (tmpl) { if (options.uploadTemplateId) { options.uploadTemplate = tmpl(options.uploadTemplateId); } if (options.downloadTemplateId) { options.downloadTemplate = tmpl(options.downloadTemplateId); } } }, _initFilesContainer: function () { var options = this.options; if (options.filesContainer === undefined) { options.filesContainer = this.element.find('.files'); } else if (!(options.filesContainer instanceof $)) { options.filesContainer = $(options.filesContainer); } }, _initSpecialOptions: function () { parentWidget.prototype._initSpecialOptions.call(this); this._initFilesContainer(); this._initTemplates(); }, _create: function () { parentWidget.prototype._create.call(this); this._refreshOptionsList.push( 'filesContainer', 'uploadTemplateId', 'downloadTemplateId' ); if (!$.blueimpFP) { this._processingQueue = $.Deferred().resolveWith(this).promise(); this.process = function () { return this._processingQueue; }; } }, enable: function () { parentWidget.prototype.enable.call(this); this.element.find('input, button').prop('disabled', false); this._enableFileInputButton(); }, disable: function () { this.element.find('input, button').prop('disabled', true); this._disableFileInputButton(); parentWidget.prototype.disable.call(this); } }); }));