hjg
2024-07-09 30304784e82d4bba24121328da8eb8490aec4f4f
提交 | 用户 | 时间
58d006 1 /*
A 2  * jQuery File Upload User Interface Plugin 9.5.1
3  * https://github.com/blueimp/jQuery-File-Upload
4  *
5  * Copyright 2010, Sebastian Tschan
6  * https://blueimp.net
7  *
8  * Licensed under the MIT license:
9  * http://www.opensource.org/licenses/MIT
10  */
11
12 /* jshint nomen:false */
13 /* global define, window */
14
15 (function (factory) {
16     'use strict';
17     if (typeof define === 'function' && define.amd) {
18         // Register as an anonymous AMD module:
19         define([
20             'jquery',
21             'tmpl',
22             './jquery.fileupload-image',
23             './jquery.fileupload-audio',
24             './jquery.fileupload-video',
25             './jquery.fileupload-validate'
26         ], factory);
27     } else {
28         // Browser globals:
29         factory(
30             window.jQuery,
31             window.tmpl
32         );
33     }
34 }(function ($, tmpl) {
35     'use strict';
36
37     $.blueimp.fileupload.prototype._specialOptions.push(
38         'filesContainer',
39         'uploadTemplateId',
40         'downloadTemplateId'
41     );
42
43     // The UI version extends the file upload widget
44     // and adds complete user interface interaction:
45     $.widget('blueimp.fileupload', $.blueimp.fileupload, {
46
47         options: {
48             // By default, files added to the widget are uploaded as soon
49             // as the user clicks on the start buttons. To enable automatic
50             // uploads, set the following option to true:
51             autoUpload: false,
52             // The ID of the upload template:
53             uploadTemplateId: 'template-upload',
54             // The ID of the download template:
55             downloadTemplateId: 'template-download',
56             // The container for the list of files. If undefined, it is set to
57             // an element with class "files" inside of the widget element:
58             filesContainer: undefined,
59             // By default, files are appended to the files container.
60             // Set the following option to true, to prepend files instead:
61             prependFiles: false,
62             // The expected data type of the upload response, sets the dataType
63             // option of the $.ajax upload requests:
64             dataType: 'json',
65
66             // Function returning the current number of files,
67             // used by the maxNumberOfFiles validation:
68             getNumberOfFiles: function () {
69                 return this.filesContainer.children()
70                     .not('.processing').length;
71             },
72
73             // Callback to retrieve the list of files from the server response:
74             getFilesFromResponse: function (data) {
75                 if (data.result && $.isArray(data.result.files)) {
76                     return data.result.files;
77                 }
78                 return [];
79             },
80
81             // The add callback is invoked as soon as files are added to the fileupload
82             // widget (via file input selection, drag & drop or add API call).
83             // See the basic file upload widget for more information:
84             add: function (e, data) {
85                 if (e.isDefaultPrevented()) {
86                     return false;
87                 }
88                 var $this = $(this),
89                     that = $this.data('blueimp-fileupload') ||
90                         $this.data('fileupload'),
91                     options = that.options;
92                 data.context = that._renderUpload(data.files)
93                     .data('data', data)
94                     .addClass('processing');
95                 options.filesContainer[
96                     options.prependFiles ? 'prepend' : 'append'
97                 ](data.context);
98                 that._forceReflow(data.context);
99                 $.when(
100                     that._transition(data.context),
101                     data.process(function () {
102                         return $this.fileupload('process', data);
103                     })
104                 ).always(function () {
105                     data.context.each(function (index) {
106                         $(this).find('.size').text(
107                             that._formatFileSize(data.files[index].size)
108                         );
109                     }).removeClass('processing');
110                     that._renderPreviews(data);
111                 }).done(function () {
112                     data.context.find('.start').prop('disabled', false);
113                     if ((that._trigger('added', e, data) !== false) &&
114                             (options.autoUpload || data.autoUpload) &&
115                             data.autoUpload !== false) {
116                         data.submit();
117                     }
118                 }).fail(function () {
119                     if (data.files.error) {
120                         data.context.each(function (index) {
121                             var error = data.files[index].error;
122                             if (error) {
123                                 $(this).find('.error').text(error);
124                             }
125                         });
126                     }
127                 });
128             },
129             // Callback for the start of each file upload request:
130             send: function (e, data) {
131                 if (e.isDefaultPrevented()) {
132                     return false;
133                 }
134                 var that = $(this).data('blueimp-fileupload') ||
135                         $(this).data('fileupload');
136                 if (data.context && data.dataType &&
137                         data.dataType.substr(0, 6) === 'iframe') {
138                     // Iframe Transport does not support progress events.
139                     // In lack of an indeterminate progress bar, we set
140                     // the progress to 100%, showing the full animated bar:
141                     data.context
142                         .find('.progress').addClass(
143                             !$.support.transition && 'progress-animated'
144                         )
145                         .attr('aria-valuenow', 100)
146                         .children().first().css(
147                             'width',
148                             '100%'
149                         );
150                 }
151                 return that._trigger('sent', e, data);
152             },
153             // Callback for successful uploads:
154             done: function (e, data) {
155                 if (e.isDefaultPrevented()) {
156                     return false;
157                 }
158                 var that = $(this).data('blueimp-fileupload') ||
159                         $(this).data('fileupload'),
160                     getFilesFromResponse = data.getFilesFromResponse ||
161                         that.options.getFilesFromResponse,
162                     files = getFilesFromResponse(data),
163                     template,
164                     deferred;
165                 if (data.context) {
166                     data.context.each(function (index) {
167                         var file = files[index] ||
168                                 {error: 'Empty file upload result'};
169                         deferred = that._addFinishedDeferreds();
170                         that._transition($(this)).done(
171                             function () {
172                                 var node = $(this);
173                                 template = that._renderDownload([file])
174                                     .replaceAll(node);
175                                 that._forceReflow(template);
176                                 that._transition(template).done(
177                                     function () {
178                                         data.context = $(this);
179                                         that._trigger('completed', e, data);
180                                         that._trigger('finished', e, data);
181                                         deferred.resolve();
182                                     }
183                                 );
184                             }
185                         );
186                     });
187                 } else {
188                     template = that._renderDownload(files)[
189                         that.options.prependFiles ? 'prependTo' : 'appendTo'
190                     ](that.options.filesContainer);
191                     that._forceReflow(template);
192                     deferred = that._addFinishedDeferreds();
193                     that._transition(template).done(
194                         function () {
195                             data.context = $(this);
196                             that._trigger('completed', e, data);
197                             that._trigger('finished', e, data);
198                             deferred.resolve();
199                         }
200                     );
201                 }
202             },
203             // Callback for failed (abort or error) uploads:
204             fail: function (e, data) {
205                 if (e.isDefaultPrevented()) {
206                     return false;
207                 }
208                 var that = $(this).data('blueimp-fileupload') ||
209                         $(this).data('fileupload'),
210                     template,
211                     deferred;
212                 if (data.context) {
213                     data.context.each(function (index) {
214                         if (data.errorThrown !== 'abort') {
215                             var file = data.files[index];
216                             file.error = file.error || data.errorThrown ||
217                                 true;
218                             deferred = that._addFinishedDeferreds();
219                             that._transition($(this)).done(
220                                 function () {
221                                     var node = $(this);
222                                     template = that._renderDownload([file])
223                                         .replaceAll(node);
224                                     that._forceReflow(template);
225                                     that._transition(template).done(
226                                         function () {
227                                             data.context = $(this);
228                                             that._trigger('failed', e, data);
229                                             that._trigger('finished', e, data);
230                                             deferred.resolve();
231                                         }
232                                     );
233                                 }
234                             );
235                         } else {
236                             deferred = that._addFinishedDeferreds();
237                             that._transition($(this)).done(
238                                 function () {
239                                     $(this).remove();
240                                     that._trigger('failed', e, data);
241                                     that._trigger('finished', e, data);
242                                     deferred.resolve();
243                                 }
244                             );
245                         }
246                     });
247                 } else if (data.errorThrown !== 'abort') {
248                     data.context = that._renderUpload(data.files)[
249                         that.options.prependFiles ? 'prependTo' : 'appendTo'
250                     ](that.options.filesContainer)
251                         .data('data', data);
252                     that._forceReflow(data.context);
253                     deferred = that._addFinishedDeferreds();
254                     that._transition(data.context).done(
255                         function () {
256                             data.context = $(this);
257                             that._trigger('failed', e, data);
258                             that._trigger('finished', e, data);
259                             deferred.resolve();
260                         }
261                     );
262                 } else {
263                     that._trigger('failed', e, data);
264                     that._trigger('finished', e, data);
265                     that._addFinishedDeferreds().resolve();
266                 }
267             },
268             // Callback for upload progress events:
269             progress: function (e, data) {
270                 if (e.isDefaultPrevented()) {
271                     return false;
272                 }
273                 var progress = Math.floor(data.loaded / data.total * 100);
274                 if (data.context) {
275                     data.context.each(function () {
276                         $(this).find('.progress')
277                             .attr('aria-valuenow', progress)
278                             .children().first().css(
279                                 'width',
280                                 progress + '%'
281                             );
282                     });
283                 }
284             },
285             // Callback for global upload progress events:
286             progressall: function (e, data) {
287                 if (e.isDefaultPrevented()) {
288                     return false;
289                 }
290                 var $this = $(this),
291                     progress = Math.floor(data.loaded / data.total * 100),
292                     globalProgressNode = $this.find('.fileupload-progress'),
293                     extendedProgressNode = globalProgressNode
294                         .find('.progress-extended');
295                 if (extendedProgressNode.length) {
296                     extendedProgressNode.html(
297                         ($this.data('blueimp-fileupload') || $this.data('fileupload'))
298                             ._renderExtendedProgress(data)
299                     );
300                 }
301                 globalProgressNode
302                     .find('.progress')
303                     .attr('aria-valuenow', progress)
304                     .children().first().css(
305                         'width',
306                         progress + '%'
307                     );
308             },
309             // Callback for uploads start, equivalent to the global ajaxStart event:
310             start: function (e) {
311                 if (e.isDefaultPrevented()) {
312                     return false;
313                 }
314                 var that = $(this).data('blueimp-fileupload') ||
315                         $(this).data('fileupload');
316                 that._resetFinishedDeferreds();
317                 that._transition($(this).find('.fileupload-progress')).done(
318                     function () {
319                         that._trigger('started', e);
320                     }
321                 );
322             },
323             // Callback for uploads stop, equivalent to the global ajaxStop event:
324             stop: function (e) {
325                 if (e.isDefaultPrevented()) {
326                     return false;
327                 }
328                 var that = $(this).data('blueimp-fileupload') ||
329                         $(this).data('fileupload'),
330                     deferred = that._addFinishedDeferreds();
331                 $.when.apply($, that._getFinishedDeferreds())
332                     .done(function () {
333                         that._trigger('stopped', e);
334                     });
335                 that._transition($(this).find('.fileupload-progress')).done(
336                     function () {
337                         $(this).find('.progress')
338                             .attr('aria-valuenow', '0')
339                             .children().first().css('width', '0%');
340                         $(this).find('.progress-extended').html(' ');
341                         deferred.resolve();
342                     }
343                 );
344             },
345             processstart: function (e) {
346                 if (e.isDefaultPrevented()) {
347                     return false;
348                 }
349                 $(this).addClass('fileupload-processing');
350             },
351             processstop: function (e) {
352                 if (e.isDefaultPrevented()) {
353                     return false;
354                 }
355                 $(this).removeClass('fileupload-processing');
356             },
357             // Callback for file deletion:
358             destroy: function (e, data) {
359                 if (e.isDefaultPrevented()) {
360                     return false;
361                 }
362                 var that = $(this).data('blueimp-fileupload') ||
363                         $(this).data('fileupload'),
364                     removeNode = function () {
365                         that._transition(data.context).done(
366                             function () {
367                                 $(this).remove();
368                                 that._trigger('destroyed', e, data);
369                             }
370                         );
371                     };
372                 if (data.url) {
373                     data.dataType = data.dataType || that.options.dataType;
374                     $.ajax(data).done(removeNode).fail(function () {
375                         that._trigger('destroyfailed', e, data);
376                     });
377                 } else {
378                     removeNode();
379                 }
380             }
381         },
382
383         _resetFinishedDeferreds: function () {
384             this._finishedUploads = [];
385         },
386
387         _addFinishedDeferreds: function (deferred) {
388             if (!deferred) {
389                 deferred = $.Deferred();
390             }
391             this._finishedUploads.push(deferred);
392             return deferred;
393         },
394
395         _getFinishedDeferreds: function () {
396             return this._finishedUploads;
397         },
398
399         // Link handler, that allows to download files
400         // by drag & drop of the links to the desktop:
401         _enableDragToDesktop: function () {
402             var link = $(this),
403                 url = link.prop('href'),
404                 name = link.prop('download'),
405                 type = 'application/octet-stream';
406             link.bind('dragstart', function (e) {
407                 try {
408                     e.originalEvent.dataTransfer.setData(
409                         'DownloadURL',
410                         [type, name, url].join(':')
411                     );
412                 } catch (ignore) {}
413             });
414         },
415
416         _formatFileSize: function (bytes) {
417             if (typeof bytes !== 'number') {
418                 return '';
419             }
420             if (bytes >= 1000000000) {
421                 return (bytes / 1000000000).toFixed(2) + ' GB';
422             }
423             if (bytes >= 1000000) {
424                 return (bytes / 1000000).toFixed(2) + ' MB';
425             }
426             return (bytes / 1000).toFixed(2) + ' KB';
427         },
428
429         _formatBitrate: function (bits) {
430             if (typeof bits !== 'number') {
431                 return '';
432             }
433             if (bits >= 1000000000) {
434                 return (bits / 1000000000).toFixed(2) + ' Gbit/s';
435             }
436             if (bits >= 1000000) {
437                 return (bits / 1000000).toFixed(2) + ' Mbit/s';
438             }
439             if (bits >= 1000) {
440                 return (bits / 1000).toFixed(2) + ' kbit/s';
441             }
442             return bits.toFixed(2) + ' bit/s';
443         },
444
445         _formatTime: function (seconds) {
446             var date = new Date(seconds * 1000),
447                 days = Math.floor(seconds / 86400);
448             days = days ? days + 'd ' : '';
449             return days +
450                 ('0' + date.getUTCHours()).slice(-2) + ':' +
451                 ('0' + date.getUTCMinutes()).slice(-2) + ':' +
452                 ('0' + date.getUTCSeconds()).slice(-2);
453         },
454
455         _formatPercentage: function (floatValue) {
456             return (floatValue * 100).toFixed(2) + ' %';
457         },
458
459         _renderExtendedProgress: function (data) {
460             return this._formatBitrate(data.bitrate) + ' | ' +
461                 this._formatTime(
462                     (data.total - data.loaded) * 8 / data.bitrate
463                 ) + ' | ' +
464                 this._formatPercentage(
465                     data.loaded / data.total
466                 ) + ' | ' +
467                 this._formatFileSize(data.loaded) + ' / ' +
468                 this._formatFileSize(data.total);
469         },
470
471         _renderTemplate: function (func, files) {
472             if (!func) {
473                 return $();
474             }
475             var result = func({
476                 files: files,
477                 formatFileSize: this._formatFileSize,
478                 options: this.options
479             });
480             if (result instanceof $) {
481                 return result;
482             }
483             return $(this.options.templatesContainer).html(result).children();
484         },
485
486         _renderPreviews: function (data) {
487             data.context.find('.preview').each(function (index, elm) {
488                 $(elm).append(data.files[index].preview);
489             });
490         },
491
492         _renderUpload: function (files) {
493             return this._renderTemplate(
494                 this.options.uploadTemplate,
495                 files
496             );
497         },
498
499         _renderDownload: function (files) {
500             return this._renderTemplate(
501                 this.options.downloadTemplate,
502                 files
503             ).find('a[download]').each(this._enableDragToDesktop).end();
504         },
505
506         _startHandler: function (e) {
507             e.preventDefault();
508             var button = $(e.currentTarget),
509                 template = button.closest('.template-upload'),
510                 data = template.data('data');
511             button.prop('disabled', true);
512             if (data && data.submit) {
513                 data.submit();
514             }
515         },
516
517         _cancelHandler: function (e) {
518             e.preventDefault();
519             var template = $(e.currentTarget)
520                     .closest('.template-upload,.template-download'),
521                 data = template.data('data') || {};
522             data.context = data.context || template;
523             if (data.abort) {
524                 data.abort();
525             } else {
526                 data.errorThrown = 'abort';
527                 this._trigger('fail', e, data);
528             }
529         },
530
531         _deleteHandler: function (e) {
532             e.preventDefault();
533             var button = $(e.currentTarget);
534             this._trigger('destroy', e, $.extend({
535                 context: button.closest('.template-download'),
536                 type: 'DELETE'
537             }, button.data()));
538         },
539
540         _forceReflow: function (node) {
541             return $.support.transition && node.length &&
542                 node[0].offsetWidth;
543         },
544
545         _transition: function (node) {
546             var dfd = $.Deferred();
547             if ($.support.transition && node.hasClass('fade') && node.is(':visible')) {
548                 node.bind(
549                     $.support.transition.end,
550                     function (e) {
551                         // Make sure we don't respond to other transitions events
552                         // in the container element, e.g. from button elements:
553                         if (e.target === node[0]) {
554                             node.unbind($.support.transition.end);
555                             dfd.resolveWith(node);
556                         }
557                     }
558                 ).toggleClass('in');
559             } else {
560                 node.toggleClass('in');
561                 dfd.resolveWith(node);
562             }
563             return dfd;
564         },
565
566         _initButtonBarEventHandlers: function () {
567             var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'),
568                 filesList = this.options.filesContainer;
569             this._on(fileUploadButtonBar.find('.start'), {
570                 click: function (e) {
571                     e.preventDefault();
572                     filesList.find('.start').click();
573                 }
574             });
575             this._on(fileUploadButtonBar.find('.cancel'), {
576                 click: function (e) {
577                     e.preventDefault();
578                     filesList.find('.cancel').click();
579                 }
580             });
581             this._on(fileUploadButtonBar.find('.delete'), {
582                 click: function (e) {
583                     e.preventDefault();
584                     filesList.find('.toggle:checked')
585                         .closest('.template-download')
586                         .find('.delete').click();
587                     fileUploadButtonBar.find('.toggle')
588                         .prop('checked', false);
589                 }
590             });
591             this._on(fileUploadButtonBar.find('.toggle'), {
592                 change: function (e) {
593                     filesList.find('.toggle').prop(
594                         'checked',
595                         $(e.currentTarget).is(':checked')
596                     );
597                 }
598             });
599         },
600
601         _destroyButtonBarEventHandlers: function () {
602             this._off(
603                 this.element.find('.fileupload-buttonbar')
604                     .find('.start, .cancel, .delete'),
605                 'click'
606             );
607             this._off(
608                 this.element.find('.fileupload-buttonbar .toggle'),
609                 'change.'
610             );
611         },
612
613         _initEventHandlers: function () {
614             this._super();
615             this._on(this.options.filesContainer, {
616                 'click .start': this._startHandler,
617                 'click .cancel': this._cancelHandler,
618                 'click .delete': this._deleteHandler
619             });
620             this._initButtonBarEventHandlers();
621         },
622
623         _destroyEventHandlers: function () {
624             this._destroyButtonBarEventHandlers();
625             this._off(this.options.filesContainer, 'click');
626             this._super();
627         },
628
629         _enableFileInputButton: function () {
630             this.element.find('.fileinput-button input')
631                 .prop('disabled', false)
632                 .parent().removeClass('disabled');
633         },
634
635         _disableFileInputButton: function () {
636             this.element.find('.fileinput-button input')
637                 .prop('disabled', true)
638                 .parent().addClass('disabled');
639         },
640
641         _initTemplates: function () {
642             var options = this.options;
643             options.templatesContainer = this.document[0].createElement(
644                 options.filesContainer.prop('nodeName')
645             );
646             if (tmpl) {
647                 if (options.uploadTemplateId) {
648                     options.uploadTemplate = tmpl(options.uploadTemplateId);
649                 }
650                 if (options.downloadTemplateId) {
651                     options.downloadTemplate = tmpl(options.downloadTemplateId);
652                 }
653             }
654         },
655
656         _initFilesContainer: function () {
657             var options = this.options;
658             if (options.filesContainer === undefined) {
659                 options.filesContainer = this.element.find('.files');
660             } else if (!(options.filesContainer instanceof $)) {
661                 options.filesContainer = $(options.filesContainer);
662             }
663         },
664
665         _initSpecialOptions: function () {
666             this._super();
667             this._initFilesContainer();
668             this._initTemplates();
669         },
670
671         _create: function () {
672             this._super();
673             this._resetFinishedDeferreds();
674             if (!$.support.fileInput) {
675                 this._disableFileInputButton();
676             }
677         },
678
679         enable: function () {
680             var wasDisabled = false;
681             if (this.options.disabled) {
682                 wasDisabled = true;
683             }
684             this._super();
685             if (wasDisabled) {
686                 this.element.find('input, button').prop('disabled', false);
687                 this._enableFileInputButton();
688             }
689         },
690
691         disable: function () {
692             if (!this.options.disabled) {
693                 this.element.find('input, button').prop('disabled', true);
694                 this._disableFileInputButton();
695             }
696             this._super();
697         }
698
699     });
700
701 }));