2 * jQuery File Upload User Interface Plugin
3 * https://github.com/blueimp/jQuery-File-Upload
5 * Copyright 2010, Sebastian Tschan
8 * Licensed under the MIT license:
9 * https://opensource.org/licenses/MIT
12 /* global define, require */
16 if (typeof define === 'function' && define.amd) {
17 // Register as an anonymous AMD module:
21 './jquery.fileupload-image',
22 './jquery.fileupload-audio',
23 './jquery.fileupload-video',
24 './jquery.fileupload-validate'
26 } else if (typeof exports === 'object') {
30 require('blueimp-tmpl'),
31 require('./jquery.fileupload-image'),
32 require('./jquery.fileupload-audio'),
33 require('./jquery.fileupload-video'),
34 require('./jquery.fileupload-validate')
38 factory(window.jQuery, window.tmpl);
40 })(function ($, tmpl) {
43 $.blueimp.fileupload.prototype._specialOptions.push(
49 // The UI version extends the file upload widget
50 // and adds complete user interface interaction:
51 $.widget('blueimp.fileupload', $.blueimp.fileupload, {
53 // By default, files added to the widget are uploaded as soon
54 // as the user clicks on the start buttons. To enable automatic
55 // uploads, set the following option to true:
57 // The class to show/hide UI elements:
58 showElementClass: 'in',
59 // The ID of the upload template:
60 uploadTemplateId: 'template-upload',
61 // The ID of the download template:
62 downloadTemplateId: 'template-download',
63 // The container for the list of files. If undefined, it is set to
64 // an element with class "files" inside of the widget element:
65 filesContainer: undefined,
66 // By default, files are appended to the files container.
67 // Set the following option to true, to prepend files instead:
69 // The expected data type of the upload response, sets the dataType
70 // option of the $.ajax upload requests:
73 // Error and info messages:
75 unknownError: 'Unknown error'
78 // Function returning the current number of files,
79 // used by the maxNumberOfFiles validation:
80 getNumberOfFiles: function () {
81 return this.filesContainer.children().not('.processing').length;
84 // Callback to retrieve the list of files from the server response:
85 getFilesFromResponse: function (data) {
86 if (data.result && $.isArray(data.result.files)) {
87 return data.result.files;
92 // The add callback is invoked as soon as files are added to the fileupload
93 // widget (via file input selection, drag & drop or add API call).
94 // See the basic file upload widget for more information:
95 add: function (e, data) {
96 if (e.isDefaultPrevented()) {
100 that = $this.data('blueimp-fileupload') || $this.data('fileupload'),
101 options = that.options;
103 ._renderUpload(data.files)
105 .addClass('processing');
106 options.filesContainer[options.prependFiles ? 'prepend' : 'append'](
109 that._forceReflow(data.context);
110 that._transition(data.context);
112 .process(function () {
113 return $this.fileupload('process', data);
115 .always(function () {
117 .each(function (index) {
120 .text(that._formatFileSize(data.files[index].size));
122 .removeClass('processing');
123 that._renderPreviews(data);
126 data.context.find('.edit,.start').prop('disabled', false);
128 that._trigger('added', e, data) !== false &&
129 (options.autoUpload || data.autoUpload) &&
130 data.autoUpload !== false
136 if (data.files.error) {
137 data.context.each(function (index) {
138 var error = data.files[index].error;
140 $(this).find('.error').text(error);
146 // Callback for the start of each file upload request:
147 send: function (e, data) {
148 if (e.isDefaultPrevented()) {
152 $(this).data('blueimp-fileupload') || $(this).data('fileupload');
156 data.dataType.substr(0, 6) === 'iframe'
158 // Iframe Transport does not support progress events.
159 // In lack of an indeterminate progress bar, we set
160 // the progress to 100%, showing the full animated bar:
163 .addClass(!$.support.transition && 'progress-animated')
164 .attr('aria-valuenow', 100)
167 .css('width', '100%');
169 return that._trigger('sent', e, data);
171 // Callback for successful uploads:
172 done: function (e, data) {
173 if (e.isDefaultPrevented()) {
177 $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
178 getFilesFromResponse =
179 data.getFilesFromResponse || that.options.getFilesFromResponse,
180 files = getFilesFromResponse(data),
184 data.context.each(function (index) {
185 var file = files[index] || { error: 'Empty file upload result' };
186 deferred = that._addFinishedDeferreds();
187 that._transition($(this)).done(function () {
189 template = that._renderDownload([file]).replaceAll(node);
190 that._forceReflow(template);
191 that._transition(template).done(function () {
192 data.context = $(this);
193 that._trigger('completed', e, data);
194 that._trigger('finished', e, data);
201 ._renderDownload(files)
202 [that.options.prependFiles ? 'prependTo' : 'appendTo'](
203 that.options.filesContainer
205 that._forceReflow(template);
206 deferred = that._addFinishedDeferreds();
207 that._transition(template).done(function () {
208 data.context = $(this);
209 that._trigger('completed', e, data);
210 that._trigger('finished', e, data);
215 // Callback for failed (abort or error) uploads:
216 fail: function (e, data) {
217 if (e.isDefaultPrevented()) {
221 $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
225 data.context.each(function (index) {
226 if (data.errorThrown !== 'abort') {
227 var file = data.files[index];
229 file.error || data.errorThrown || data.i18n('unknownError');
230 deferred = that._addFinishedDeferreds();
231 that._transition($(this)).done(function () {
233 template = that._renderDownload([file]).replaceAll(node);
234 that._forceReflow(template);
235 that._transition(template).done(function () {
236 data.context = $(this);
237 that._trigger('failed', e, data);
238 that._trigger('finished', e, data);
243 deferred = that._addFinishedDeferreds();
244 that._transition($(this)).done(function () {
246 that._trigger('failed', e, data);
247 that._trigger('finished', e, data);
252 } else if (data.errorThrown !== 'abort') {
254 ._renderUpload(data.files)
255 [that.options.prependFiles ? 'prependTo' : 'appendTo'](
256 that.options.filesContainer
259 that._forceReflow(data.context);
260 deferred = that._addFinishedDeferreds();
261 that._transition(data.context).done(function () {
262 data.context = $(this);
263 that._trigger('failed', e, data);
264 that._trigger('finished', e, data);
268 that._trigger('failed', e, data);
269 that._trigger('finished', e, data);
270 that._addFinishedDeferreds().resolve();
273 // Callback for upload progress events:
274 progress: function (e, data) {
275 if (e.isDefaultPrevented()) {
278 var progress = Math.floor((data.loaded / data.total) * 100);
280 data.context.each(function () {
283 .attr('aria-valuenow', progress)
286 .css('width', progress + '%');
290 // Callback for global upload progress events:
291 progressall: function (e, data) {
292 if (e.isDefaultPrevented()) {
296 progress = Math.floor((data.loaded / data.total) * 100),
297 globalProgressNode = $this.find('.fileupload-progress'),
298 extendedProgressNode = globalProgressNode.find('.progress-extended');
299 if (extendedProgressNode.length) {
300 extendedProgressNode.html(
302 $this.data('blueimp-fileupload') || $this.data('fileupload')
303 )._renderExtendedProgress(data)
308 .attr('aria-valuenow', progress)
311 .css('width', progress + '%');
313 // Callback for uploads start, equivalent to the global ajaxStart event:
314 start: function (e) {
315 if (e.isDefaultPrevented()) {
319 $(this).data('blueimp-fileupload') || $(this).data('fileupload');
320 that._resetFinishedDeferreds();
322 ._transition($(this).find('.fileupload-progress'))
324 that._trigger('started', e);
327 // Callback for uploads stop, equivalent to the global ajaxStop event:
329 if (e.isDefaultPrevented()) {
333 $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
334 deferred = that._addFinishedDeferreds();
335 $.when.apply($, that._getFinishedDeferreds()).done(function () {
336 that._trigger('stopped', e);
339 ._transition($(this).find('.fileupload-progress'))
343 .attr('aria-valuenow', '0')
347 $(this).find('.progress-extended').html(' ');
351 processstart: function (e) {
352 if (e.isDefaultPrevented()) {
355 $(this).addClass('fileupload-processing');
357 processstop: function (e) {
358 if (e.isDefaultPrevented()) {
361 $(this).removeClass('fileupload-processing');
363 // Callback for file deletion:
364 destroy: function (e, data) {
365 if (e.isDefaultPrevented()) {
369 $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
370 removeNode = function () {
371 that._transition(data.context).done(function () {
373 that._trigger('destroyed', e, data);
377 data.dataType = data.dataType || that.options.dataType;
381 that._trigger('destroyfailed', e, data);
389 _resetFinishedDeferreds: function () {
390 this._finishedUploads = [];
393 _addFinishedDeferreds: function (deferred) {
394 // eslint-disable-next-line new-cap
395 var promise = deferred || $.Deferred();
396 this._finishedUploads.push(promise);
400 _getFinishedDeferreds: function () {
401 return this._finishedUploads;
404 // Link handler, that allows to download files
405 // by drag & drop of the links to the desktop:
406 _enableDragToDesktop: function () {
408 url = link.prop('href'),
409 name = link.prop('download'),
410 type = 'application/octet-stream';
411 link.on('dragstart', function (e) {
413 e.originalEvent.dataTransfer.setData(
415 [type, name, url].join(':')
423 _formatFileSize: function (bytes) {
424 if (typeof bytes !== 'number') {
427 if (bytes >= 1000000000) {
428 return (bytes / 1000000000).toFixed(2) + ' GB';
430 if (bytes >= 1000000) {
431 return (bytes / 1000000).toFixed(2) + ' MB';
433 return (bytes / 1000).toFixed(2) + ' KB';
436 _formatBitrate: function (bits) {
437 if (typeof bits !== 'number') {
440 if (bits >= 1000000000) {
441 return (bits / 1000000000).toFixed(2) + ' Gbit/s';
443 if (bits >= 1000000) {
444 return (bits / 1000000).toFixed(2) + ' Mbit/s';
447 return (bits / 1000).toFixed(2) + ' kbit/s';
449 return bits.toFixed(2) + ' bit/s';
452 _formatTime: function (seconds) {
453 var date = new Date(seconds * 1000),
454 days = Math.floor(seconds / 86400);
455 days = days ? days + 'd ' : '';
458 ('0' + date.getUTCHours()).slice(-2) +
460 ('0' + date.getUTCMinutes()).slice(-2) +
462 ('0' + date.getUTCSeconds()).slice(-2)
466 _formatPercentage: function (floatValue) {
467 return (floatValue * 100).toFixed(2) + ' %';
470 _renderExtendedProgress: function (data) {
472 this._formatBitrate(data.bitrate) +
474 this._formatTime(((data.total - data.loaded) * 8) / data.bitrate) +
476 this._formatPercentage(data.loaded / data.total) +
478 this._formatFileSize(data.loaded) +
480 this._formatFileSize(data.total)
484 _renderTemplate: function (func, files) {
490 formatFileSize: this._formatFileSize,
491 options: this.options
493 if (result instanceof $) {
496 return $(this.options.templatesContainer).html(result).children();
499 _renderPreviews: function (data) {
500 data.context.find('.preview').each(function (index, elm) {
501 $(elm).empty().append(data.files[index].preview);
505 _renderUpload: function (files) {
506 return this._renderTemplate(this.options.uploadTemplate, files);
509 _renderDownload: function (files) {
510 return this._renderTemplate(this.options.downloadTemplate, files)
512 .each(this._enableDragToDesktop)
516 _editHandler: function (e) {
518 if (!this.options.edit) return;
520 button = $(e.currentTarget),
521 template = button.closest('.template-upload'),
522 data = template.data('data'),
523 index = button.data().index;
524 this.options.edit(data.files[index]).then(function (file) {
526 data.files[index] = file;
527 data.context.addClass('processing');
528 template.find('.edit,.start').prop('disabled', true);
530 .fileupload('process', data)
531 .always(function () {
534 .text(that._formatFileSize(data.files[index].size));
535 data.context.removeClass('processing');
536 that._renderPreviews(data);
539 template.find('.edit,.start').prop('disabled', false);
542 template.find('.edit').prop('disabled', false);
543 var error = data.files[index].error;
545 template.find('.error').text(error);
551 _startHandler: function (e) {
553 var button = $(e.currentTarget),
554 template = button.closest('.template-upload'),
555 data = template.data('data');
556 button.prop('disabled', true);
557 if (data && data.submit) {
562 _cancelHandler: function (e) {
564 var template = $(e.currentTarget).closest(
565 '.template-upload,.template-download'
567 data = template.data('data') || {};
568 data.context = data.context || template;
572 data.errorThrown = 'abort';
573 this._trigger('fail', e, data);
577 _deleteHandler: function (e) {
579 var button = $(e.currentTarget);
585 context: button.closest('.template-download'),
593 _forceReflow: function (node) {
594 return $.support.transition && node.length && node[0].offsetWidth;
597 _transition: function (node) {
598 // eslint-disable-next-line new-cap
599 var dfd = $.Deferred();
601 $.support.transition &&
602 node.hasClass('fade') &&
605 var transitionEndHandler = function (e) {
606 // Make sure we don't respond to other transition events
607 // in the container element, e.g. from button elements:
608 if (e.target === node[0]) {
609 node.off($.support.transition.end, transitionEndHandler);
610 dfd.resolveWith(node);
614 .on($.support.transition.end, transitionEndHandler)
615 .toggleClass(this.options.showElementClass);
617 node.toggleClass(this.options.showElementClass);
618 dfd.resolveWith(node);
623 _initButtonBarEventHandlers: function () {
624 var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'),
625 filesList = this.options.filesContainer;
626 this._on(fileUploadButtonBar.find('.start'), {
627 click: function (e) {
629 filesList.find('.start').trigger('click');
632 this._on(fileUploadButtonBar.find('.cancel'), {
633 click: function (e) {
635 filesList.find('.cancel').trigger('click');
638 this._on(fileUploadButtonBar.find('.delete'), {
639 click: function (e) {
642 .find('.toggle:checked')
643 .closest('.template-download')
646 fileUploadButtonBar.find('.toggle').prop('checked', false);
649 this._on(fileUploadButtonBar.find('.toggle'), {
650 change: function (e) {
653 .prop('checked', $(e.currentTarget).is(':checked'));
658 _destroyButtonBarEventHandlers: function () {
661 .find('.fileupload-buttonbar')
662 .find('.start, .cancel, .delete'),
665 this._off(this.element.find('.fileupload-buttonbar .toggle'), 'change.');
668 _initEventHandlers: function () {
670 this._on(this.options.filesContainer, {
671 'click .edit': this._editHandler,
672 'click .start': this._startHandler,
673 'click .cancel': this._cancelHandler,
674 'click .delete': this._deleteHandler
676 this._initButtonBarEventHandlers();
679 _destroyEventHandlers: function () {
680 this._destroyButtonBarEventHandlers();
681 this._off(this.options.filesContainer, 'click');
685 _enableFileInputButton: function () {
687 .find('.fileinput-button input')
688 .prop('disabled', false)
690 .removeClass('disabled');
693 _disableFileInputButton: function () {
695 .find('.fileinput-button input')
696 .prop('disabled', true)
698 .addClass('disabled');
701 _initTemplates: function () {
702 var options = this.options;
703 options.templatesContainer = this.document[0].createElement(
704 options.filesContainer.prop('nodeName')
707 if (options.uploadTemplateId) {
708 options.uploadTemplate = tmpl(options.uploadTemplateId);
710 if (options.downloadTemplateId) {
711 options.downloadTemplate = tmpl(options.downloadTemplateId);
716 _initFilesContainer: function () {
717 var options = this.options;
718 if (options.filesContainer === undefined) {
719 options.filesContainer = this.element.find('.files');
720 } else if (!(options.filesContainer instanceof $)) {
721 options.filesContainer = $(options.filesContainer);
725 _initSpecialOptions: function () {
727 this._initFilesContainer();
728 this._initTemplates();
731 _create: function () {
733 this._resetFinishedDeferreds();
734 if (!$.support.fileInput) {
735 this._disableFileInputButton();
739 enable: function () {
740 var wasDisabled = false;
741 if (this.options.disabled) {
746 this.element.find('input, button').prop('disabled', false);
747 this._enableFileInputButton();
751 disable: function () {
752 if (!this.options.disabled) {
753 this.element.find('input, button').prop('disabled', true);
754 this._disableFileInputButton();