2 * jQuery File Upload User Interface Plugin 6.11
3 * https://github.com/blueimp/jQuery-File-Upload
5 * Copyright 2010, Sebastian Tschan
8 * Licensed under the MIT license:
9 * http://www.opensource.org/licenses/MIT
12 /*jslint nomen: true, unparam: true, regexp: true */
13 /*global define, window, URL, webkitURL, FileReader */
17 if (typeof define === 'function' && define.amd) {
18 // Register as an anonymous AMD module:
23 './jquery.fileupload-fp'
33 }(function ($, tmpl, loadImage) {
36 // The UI version extends the file upload widget
37 // and adds complete user interface interaction:
38 $.widget('blueimp.fileupload', $.blueimp.fileupload, {
41 // By default, files added to the widget are uploaded as soon
42 // as the user clicks on the start buttons. To enable automatic
43 // uploads, set the following option to true:
45 // The following option limits the number of files that are
46 // allowed to be uploaded using this widget:
47 maxNumberOfFiles: undefined,
48 // The maximum allowed file size:
49 maxFileSize: undefined,
50 // The minimum allowed file size:
51 minFileSize: undefined,
52 // The regular expression for allowed file types, matches
53 // against either file type or file name:
54 acceptFileTypes: /.+$/i,
55 // The regular expression to define for which files a preview
56 // image is shown, matched against the file type:
57 previewSourceFileTypes: /^image\/(gif|jpeg|png)$/,
58 // The maximum file size of images that are to be displayed as preview:
59 previewSourceMaxFileSize: 5000000, // 5MB
60 // The maximum width of the preview images:
62 // The maximum height of the preview images:
64 // By default, preview images are displayed as canvas elements
65 // if supported by the browser. Set the following option to false
66 // to always display preview images as img elements:
67 previewAsCanvas: true,
68 // The ID of the upload template:
69 uploadTemplateId: 'template-upload',
70 // The ID of the download template:
71 downloadTemplateId: 'template-download',
72 // The container for the list of files. If undefined, it is set to
73 // an element with class "files" inside of the widget element:
74 filesContainer: undefined,
75 // By default, files are appended to the files container.
76 // Set the following option to true, to prepend files instead:
78 // The expected data type of the upload response, sets the dataType
79 // option of the $.ajax upload requests:
82 // The add callback is invoked as soon as files are added to the fileupload
83 // widget (via file input selection, drag & drop or add API call).
84 // See the basic file upload widget for more information:
85 add: function (e, data) {
86 var that = $(this).data('fileupload'),
87 options = that.options,
89 $(this).fileupload('process', data).done(function () {
90 that._adjustMaxNumberOfFiles(-files.length);
91 data.maxNumberOfFilesAdjusted = true;
92 data.files.valid = data.isValidated = that._validate(files);
93 data.context = that._renderUpload(files).data('data', data);
94 options.filesContainer[
95 options.prependFiles ? 'prepend' : 'append'
97 that._renderPreviews(files, data.context);
98 that._forceReflow(data.context);
99 that._transition(data.context).done(
101 if ((that._trigger('added', e, data) !== false) &&
102 (options.autoUpload || data.autoUpload) &&
103 data.autoUpload !== false && data.isValidated) {
110 // Callback for the start of each file upload request:
111 send: function (e, data) {
112 var that = $(this).data('fileupload');
113 if (!data.isValidated) {
114 if (!data.maxNumberOfFilesAdjusted) {
115 that._adjustMaxNumberOfFiles(-data.files.length);
116 data.maxNumberOfFilesAdjusted = true;
118 if (!that._validate(data.files)) {
122 if (data.context && data.dataType &&
123 data.dataType.substr(0, 6) === 'iframe') {
124 // Iframe Transport does not support progress events.
125 // In lack of an indeterminate progress bar, we set
126 // the progress to 100%, showing the full animated bar:
128 .find('.progress').addClass(
129 !$.support.transition && 'progress-animated'
131 .attr('aria-valuenow', 100)
137 return that._trigger('sent', e, data);
139 // Callback for successful uploads:
140 done: function (e, data) {
141 var that = $(this).data('fileupload'),
144 data.context.each(function (index) {
145 var file = ($.isArray(data.result) &&
146 data.result[index]) ||
147 {error: 'Empty file upload result'};
149 that._adjustMaxNumberOfFiles(1);
151 that._transition($(this)).done(
154 template = that._renderDownload([file])
156 that._forceReflow(template);
157 that._transition(template).done(
159 data.context = $(this);
160 that._trigger('completed', e, data);
167 if ($.isArray(data.result)) {
168 $.each(data.result, function (index, file) {
169 if (data.maxNumberOfFilesAdjusted && file.error) {
170 that._adjustMaxNumberOfFiles(1);
171 } else if (!data.maxNumberOfFilesAdjusted &&
173 that._adjustMaxNumberOfFiles(-1);
176 data.maxNumberOfFilesAdjusted = true;
178 template = that._renderDownload(data.result)
179 .appendTo(that.options.filesContainer);
180 that._forceReflow(template);
181 that._transition(template).done(
183 data.context = $(this);
184 that._trigger('completed', e, data);
189 // Callback for failed (abort or error) uploads:
190 fail: function (e, data) {
191 var that = $(this).data('fileupload'),
193 if (data.maxNumberOfFilesAdjusted) {
194 that._adjustMaxNumberOfFiles(data.files.length);
197 data.context.each(function (index) {
198 if (data.errorThrown !== 'abort') {
199 var file = data.files[index];
200 file.error = file.error || data.errorThrown ||
202 that._transition($(this)).done(
205 template = that._renderDownload([file])
207 that._forceReflow(template);
208 that._transition(template).done(
210 data.context = $(this);
211 that._trigger('failed', e, data);
217 that._transition($(this)).done(
220 that._trigger('failed', e, data);
225 } else if (data.errorThrown !== 'abort') {
226 data.context = that._renderUpload(data.files)
227 .appendTo(that.options.filesContainer)
229 that._forceReflow(data.context);
230 that._transition(data.context).done(
232 data.context = $(this);
233 that._trigger('failed', e, data);
237 that._trigger('failed', e, data);
240 // Callback for upload progress events:
241 progress: function (e, data) {
243 var progress = parseInt(data.loaded / data.total * 100, 10);
244 data.context.find('.progress')
245 .attr('aria-valuenow', progress)
252 // Callback for global upload progress events:
253 progressall: function (e, data) {
255 progress = parseInt(data.loaded / data.total * 100, 10),
256 globalProgressNode = $this.find('.fileupload-progress'),
257 extendedProgressNode = globalProgressNode
258 .find('.progress-extended');
259 if (extendedProgressNode.length) {
260 extendedProgressNode.html(
261 $this.data('fileupload')._renderExtendedProgress(data)
266 .attr('aria-valuenow', progress)
272 // Callback for uploads start, equivalent to the global ajaxStart event:
273 start: function (e) {
274 var that = $(this).data('fileupload');
275 that._transition($(this).find('.fileupload-progress')).done(
277 that._trigger('started', e);
281 // Callback for uploads stop, equivalent to the global ajaxStop event:
283 var that = $(this).data('fileupload');
284 that._transition($(this).find('.fileupload-progress')).done(
286 $(this).find('.progress')
287 .attr('aria-valuenow', '0')
288 .find('.bar').css('width', '0%');
289 $(this).find('.progress-extended').html(' ');
290 that._trigger('stopped', e);
294 // Callback for file deletion:
295 destroy: function (e, data) {
296 var that = $(this).data('fileupload');
299 that._adjustMaxNumberOfFiles(1);
301 that._transition(data.context).done(
304 that._trigger('destroyed', e, data);
310 // Link handler, that allows to download files
311 // by drag & drop of the links to the desktop:
312 _enableDragToDesktop: function () {
314 url = link.prop('href'),
315 name = link.prop('download'),
316 type = 'application/octet-stream';
317 link.bind('dragstart', function (e) {
319 e.originalEvent.dataTransfer.setData(
321 [type, name, url].join(':')
327 _adjustMaxNumberOfFiles: function (operand) {
328 if (typeof this.options.maxNumberOfFiles === 'number') {
329 this.options.maxNumberOfFiles += operand;
330 if (this.options.maxNumberOfFiles < 1) {
331 this._disableFileInputButton();
333 this._enableFileInputButton();
338 _formatFileSize: function (bytes) {
339 if (typeof bytes !== 'number') {
342 if (bytes >= 1000000000) {
343 return (bytes / 1000000000).toFixed(2) + ' GB';
345 if (bytes >= 1000000) {
346 return (bytes / 1000000).toFixed(2) + ' MB';
348 return (bytes / 1000).toFixed(2) + ' KB';
351 _formatBitrate: function (bits) {
352 if (typeof bits !== 'number') {
355 if (bits >= 1000000000) {
356 return (bits / 1000000000).toFixed(2) + ' Gbit/s';
358 if (bits >= 1000000) {
359 return (bits / 1000000).toFixed(2) + ' Mbit/s';
362 return (bits / 1000).toFixed(2) + ' kbit/s';
364 return bits + ' bit/s';
367 _formatTime: function (seconds) {
368 var date = new Date(seconds * 1000),
369 days = parseInt(seconds / 86400, 10);
370 days = days ? days + 'd ' : '';
372 ('0' + date.getUTCHours()).slice(-2) + ':' +
373 ('0' + date.getUTCMinutes()).slice(-2) + ':' +
374 ('0' + date.getUTCSeconds()).slice(-2);
377 _formatPercentage: function (floatValue) {
378 return (floatValue * 100).toFixed(2) + ' %';
381 _renderExtendedProgress: function (data) {
382 return this._formatBitrate(data.bitrate) + ' | ' +
384 (data.total - data.loaded) * 8 / data.bitrate
386 this._formatPercentage(
387 data.loaded / data.total
389 this._formatFileSize(data.loaded) + ' / ' +
390 this._formatFileSize(data.total);
393 _hasError: function (file) {
397 // The number of added files is subtracted from
398 // maxNumberOfFiles before validation, so we check if
399 // maxNumberOfFiles is below 0 (instead of below 1):
400 if (this.options.maxNumberOfFiles < 0) {
401 return 'Maximum number of files exceeded';
403 // Files are accepted if either the file type or the file name
404 // matches against the acceptFileTypes regular expression, as
405 // only browsers with support for the File API report the type:
406 if (!(this.options.acceptFileTypes.test(file.type) ||
407 this.options.acceptFileTypes.test(file.name))) {
408 return 'Filetype not allowed';
410 if (this.options.maxFileSize &&
411 file.size > this.options.maxFileSize) {
412 return 'File is too big';
414 if (typeof file.size === 'number' &&
415 file.size < this.options.minFileSize) {
416 return 'File is too small';
421 _validate: function (files) {
423 valid = !!files.length;
424 $.each(files, function (index, file) {
425 file.error = that._hasError(file);
433 _renderTemplate: function (func, files) {
439 formatFileSize: this._formatFileSize,
440 options: this.options
442 if (result instanceof $) {
445 return $(this.options.templatesContainer).html(result).children();
448 _renderPreview: function (file, node) {
450 options = this.options,
452 return ((loadImage && loadImage(
456 that._forceReflow(node);
457 that._transition(node).done(function () {
458 dfd.resolveWith(node);
460 if (!$.contains(that.document[0].body, node[0])) {
461 // If the element is not part of the DOM,
462 // transition events are not triggered,
463 // so we have to resolve manually:
464 dfd.resolveWith(node);
468 maxWidth: options.previewMaxWidth,
469 maxHeight: options.previewMaxHeight,
470 canvas: options.previewAsCanvas
472 )) || dfd.resolveWith(node)) && dfd;
475 _renderPreviews: function (files, nodes) {
477 options = this.options;
478 nodes.find('.preview span').each(function (index, element) {
479 var file = files[index];
480 if (options.previewSourceFileTypes.test(file.type) &&
481 ($.type(options.previewSourceMaxFileSize) !== 'number' ||
482 file.size < options.previewSourceMaxFileSize)) {
483 that._processingQueue = that._processingQueue.pipe(function () {
484 var dfd = $.Deferred();
485 that._renderPreview(file, $(element)).done(
487 dfd.resolveWith(that);
490 return dfd.promise();
494 return this._processingQueue;
497 _renderUpload: function (files) {
498 return this._renderTemplate(
499 this.options.uploadTemplate,
504 _renderDownload: function (files) {
505 return this._renderTemplate(
506 this.options.downloadTemplate,
508 ).find('a[download]').each(this._enableDragToDesktop).end();
511 _startHandler: function (e) {
513 var button = $(e.currentTarget),
514 template = button.closest('.template-upload'),
515 data = template.data('data');
516 if (data && data.submit && !data.jqXHR && data.submit()) {
517 button.prop('disabled', true);
521 _cancelHandler: function (e) {
523 var template = $(e.currentTarget).closest('.template-upload'),
524 data = template.data('data') || {};
526 data.errorThrown = 'abort';
527 this._trigger('fail', e, data);
533 _deleteHandler: function (e) {
535 var button = $(e.currentTarget);
536 this._trigger('destroy', e, $.extend({
537 context: button.closest('.template-download'),
539 dataType: this.options.dataType
543 _forceReflow: function (node) {
544 return $.support.transition && node.length &&
548 _transition: function (node) {
549 var dfd = $.Deferred();
550 if ($.support.transition && node.hasClass('fade')) {
552 $.support.transition.end,
554 // Make sure we don't respond to other transitions events
555 // in the container element, e.g. from button elements:
556 if (e.target === node[0]) {
557 node.unbind($.support.transition.end);
558 dfd.resolveWith(node);
563 node.toggleClass('in');
564 dfd.resolveWith(node);
569 _initButtonBarEventHandlers: function () {
570 var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'),
571 filesList = this.options.filesContainer;
572 this._on(fileUploadButtonBar.find('.start'), {
573 click: function (e) {
575 filesList.find('.start button').click();
578 this._on(fileUploadButtonBar.find('.cancel'), {
579 click: function (e) {
581 filesList.find('.cancel button').click();
584 this._on(fileUploadButtonBar.find('.delete'), {
585 click: function (e) {
587 filesList.find('.delete input:checked')
588 .siblings('button').click();
589 fileUploadButtonBar.find('.toggle')
590 .prop('checked', false);
593 this._on(fileUploadButtonBar.find('.toggle'), {
594 change: function (e) {
595 filesList.find('.delete input').prop(
597 $(e.currentTarget).is(':checked')
603 _destroyButtonBarEventHandlers: function () {
605 this.element.find('.fileupload-buttonbar button'),
609 this.element.find('.fileupload-buttonbar .toggle'),
614 _initEventHandlers: function () {
616 this._on(this.options.filesContainer, {
617 'click .start button': this._startHandler,
618 'click .cancel button': this._cancelHandler,
619 'click .delete button': this._deleteHandler
621 this._initButtonBarEventHandlers();
624 _destroyEventHandlers: function () {
625 this._destroyButtonBarEventHandlers();
626 this._off(this.options.filesContainer, 'click');
630 _enableFileInputButton: function () {
631 this.element.find('.fileinput-button input')
632 .prop('disabled', false)
633 .parent().removeClass('disabled');
636 _disableFileInputButton: function () {
637 this.element.find('.fileinput-button input')
638 .prop('disabled', true)
639 .parent().addClass('disabled');
642 _initTemplates: function () {
643 var options = this.options;
644 options.templatesContainer = this.document[0].createElement(
645 options.filesContainer.prop('nodeName')
648 if (options.uploadTemplateId) {
649 options.uploadTemplate = tmpl(options.uploadTemplateId);
651 if (options.downloadTemplateId) {
652 options.downloadTemplate = tmpl(options.downloadTemplateId);
657 _initFilesContainer: function () {
658 var options = this.options;
659 if (options.filesContainer === undefined) {
660 options.filesContainer = this.element.find('.files');
661 } else if (!(options.filesContainer instanceof $)) {
662 options.filesContainer = $(options.filesContainer);
666 _stringToRegExp: function (str) {
667 var parts = str.split('/'),
668 modifiers = parts.pop();
670 return new RegExp(parts.join('/'), modifiers);
673 _initRegExpOptions: function () {
674 var options = this.options;
675 if ($.type(options.acceptFileTypes) === 'string') {
676 options.acceptFileTypes = this._stringToRegExp(
677 options.acceptFileTypes
680 if ($.type(options.previewSourceFileTypes) === 'string') {
681 options.previewSourceFileTypes = this._stringToRegExp(
682 options.previewSourceFileTypes
687 _initSpecialOptions: function () {
689 this._initFilesContainer();
690 this._initTemplates();
691 this._initRegExpOptions();
694 _create: function () {
696 this._refreshOptionsList.push(
701 if (!this._processingQueue) {
702 this._processingQueue = $.Deferred().resolveWith(this).promise();
703 this.process = function () {
704 return this._processingQueue;
709 enable: function () {
710 var wasDisabled = false;
711 if (this.options.disabled) {
716 this.element.find('input, button').prop('disabled', false);
717 this._enableFileInputButton();
721 disable: function () {
722 if (!this.options.disabled) {
723 this.element.find('input, button').prop('disabled', true);
724 this._disableFileInputButton();