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