1/* 2 * jQuery File Upload User Interface Plugin 6.9.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 .attr('aria-valuenow', 100) 132 .find('.bar').css( 133 'width', 134 '100%' 135 ); 136 } 137 return that._trigger('sent', e, data); 138 }, 139 // Callback for successful uploads: 140 done: function (e, data) { 141 var that = $(this).data('fileupload'), 142 template; 143 if (data.context) { 144 data.context.each(function (index) { 145 var file = ($.isArray(data.result) && 146 data.result[index]) || {error: 'emptyResult'}; 147 if (file.error) { 148 that._adjustMaxNumberOfFiles(1); 149 } 150 that._transition($(this)).done( 151 function () { 152 var node = $(this); 153 template = that._renderDownload([file]) 154 .css('height', node.height()) 155 .replaceAll(node); 156 that._forceReflow(template); 157 that._transition(template).done( 158 function () { 159 data.context = $(this); 160 that._trigger('completed', e, data); 161 } 162 ); 163 } 164 ); 165 }); 166 } else { 167 template = that._renderDownload(data.result) 168 .appendTo(that.options.filesContainer); 169 that._forceReflow(template); 170 that._transition(template).done( 171 function () { 172 data.context = $(this); 173 that._trigger('completed', e, data); 174 } 175 ); 176 } 177 }, 178 // Callback for failed (abort or error) uploads: 179 fail: function (e, data) { 180 var that = $(this).data('fileupload'), 181 template; 182 that._adjustMaxNumberOfFiles(data.files.length); 183 if (data.context) { 184 data.context.each(function (index) { 185 if (data.errorThrown !== 'abort') { 186 var file = data.files[index]; 187 file.error = file.error || data.errorThrown || 188 true; 189 that._transition($(this)).done( 190 function () { 191 var node = $(this); 192 template = that._renderDownload([file]) 193 .replaceAll(node); 194 that._forceReflow(template); 195 that._transition(template).done( 196 function () { 197 data.context = $(this); 198 that._trigger('failed', e, data); 199 } 200 ); 201 } 202 ); 203 } else { 204 that._transition($(this)).done( 205 function () { 206 $(this).remove(); 207 that._trigger('failed', e, data); 208 } 209 ); 210 } 211 }); 212 } else if (data.errorThrown !== 'abort') { 213 that._adjustMaxNumberOfFiles(-data.files.length); 214 data.context = that._renderUpload(data.files) 215 .appendTo(that.options.filesContainer) 216 .data('data', data); 217 that._forceReflow(data.context); 218 that._transition(data.context).done( 219 function () { 220 data.context = $(this); 221 that._trigger('failed', e, data); 222 } 223 ); 224 } else { 225 that._trigger('failed', e, data); 226 } 227 }, 228 // Callback for upload progress events: 229 progress: function (e, data) { 230 if (data.context) { 231 var progress = parseInt(data.loaded / data.total * 100, 10); 232 data.context.find('.progress') 233 .attr('aria-valuenow', progress) 234 .find('.bar').css( 235 'width', 236 progress + '%' 237 ); 238 } 239 }, 240 // Callback for global upload progress events: 241 progressall: function (e, data) { 242 var $this = $(this), 243 progress = parseInt(data.loaded / data.total * 100, 10), 244 globalProgressNode = $this.find('.fileupload-progress'), 245 extendedProgressNode = globalProgressNode 246 .find('.progress-extended'); 247 if (extendedProgressNode.length) { 248 extendedProgressNode.html( 249 $this.data('fileupload')._renderExtendedProgress(data) 250 ); 251 } 252 globalProgressNode 253 .find('.progress') 254 .attr('aria-valuenow', progress) 255 .find('.bar').css( 256 'width', 257 progress + '%' 258 ); 259 }, 260 // Callback for uploads start, equivalent to the global ajaxStart event: 261 start: function (e) { 262 var that = $(this).data('fileupload'); 263 that._transition($(this).find('.fileupload-progress')).done( 264 function () { 265 that._trigger('started', e); 266 } 267 ); 268 }, 269 // Callback for uploads stop, equivalent to the global ajaxStop event: 270 stop: function (e) { 271 var that = $(this).data('fileupload'); 272 that._transition($(this).find('.fileupload-progress')).done( 273 function () { 274 $(this).find('.progress') 275 .attr('aria-valuenow', '0') 276 .find('.bar').css('width', '0%'); 277 $(this).find('.progress-extended').html(' '); 278 that._trigger('stopped', e); 279 } 280 ); 281 },
282 // Callback for file deletion: 283 destroy: function (e, data) { 284 var that = $(this).data('fileupload'); 285 if (data.url) { 286 $.ajax(data); 287 that._adjustMaxNumberOfFiles(1); 288 } 289 that._transition(data.context).done( 290 function () { 291 $(this).remove(); 292 that._trigger('destroyed', e, data); 293 } 294 ); 295 } 296 }, 297 298 // Link handler, that allows to download files 299 // by drag & drop of the links to the desktop: 300 _enableDragToDesktop: function () { 301 var link = $(this), 302 url = link.prop('href'), 303 name = link.prop('download'), 304 type = 'application/octet-stream'; 305 link.bind('dragstart', function (e) { 306 try { 307 e.originalEvent.dataTransfer.setData( 308 'DownloadURL', 309 [type, name, url].join(':') 310 ); 311 } catch (err) {} 312 }); 313 }, 314 315 _adjustMaxNumberOfFiles: function (operand) { 316 if (typeof this.options.maxNumberOfFiles === 'number') { 317 this.options.maxNumberOfFiles += operand; 318 if (this.options.maxNumberOfFiles < 1) { 319 this._disableFileInputButton(); 320 } else { 321 this._enableFileInputButton(); 322 } 323 } 324 }, 325 326 _formatFileSize: function (bytes) { 327 if (typeof bytes !== 'number') { 328 return ''; 329 } 330 if (bytes >= 1000000000) { 331 return (bytes / 1000000000).toFixed(2) + ' GB'; 332 } 333 if (bytes >= 1000000) { 334 return (bytes / 1000000).toFixed(2) + ' MB'; 335 } 336 return (bytes / 1000).toFixed(2) + ' KB'; 337 }, 338 339 _formatBitrate: function (bits) { 340 if (typeof bits !== 'number') { 341 return ''; 342 } 343 if (bits >= 1000000000) { 344 return (bits / 1000000000).toFixed(2) + ' Gbit/s'; 345 } 346 if (bits >= 1000000) { 347 return (bits / 1000000).toFixed(2) + ' Mbit/s'; 348 } 349 if (bits >= 1000) { 350 return (bits / 1000).toFixed(2) + ' kbit/s'; 351 } 352 return bits + ' bit/s'; 353 }, 354 355 _formatTime: function (seconds) { 356 var date = new Date(seconds * 1000), 357 days = parseInt(seconds / 86400, 10); 358 days = days ? days + 'd ' : ''; 359 return days + 360 ('0' + date.getUTCHours()).slice(-2) + ':' + 361 ('0' + date.getUTCMinutes()).slice(-2) + ':' + 362 ('0' + date.getUTCSeconds()).slice(-2); 363 }, 364 365 _formatPercentage: function (floatValue) { 366 return (floatValue * 100).toFixed(2) + ' %'; 367 }, 368 369 _renderExtendedProgress: function (data) { 370 return this._formatBitrate(data.bitrate) + ' | ' + 371 this._formatTime( 372 (data.total - data.loaded) * 8 / data.bitrate 373 ) + ' | ' + 374 this._formatPercentage( 375 data.loaded / data.total 376 ) + ' | ' + 377 this._formatFileSize(data.loaded) + ' / ' + 378 this._formatFileSize(data.total); 379 }, 380 381 _hasError: function (file) { 382 if (file.error) { 383 return file.error; 384 } 385 // The number of added files is subtracted from 386 // maxNumberOfFiles before validation, so we check if 387 // maxNumberOfFiles is below 0 (instead of below 1): 388 if (this.options.maxNumberOfFiles < 0) { 389 return 'maxNumberOfFiles'; 390 } 391 // Files are accepted if either the file type or the file name 392 // matches against the acceptFileTypes regular expression, as 393 // only browsers with support for the File API report the type: 394 if (!(this.options.acceptFileTypes.test(file.type) || 395 this.options.acceptFileTypes.test(file.name))) { 396 return 'acceptFileTypes'; 397 } 398 if (this.options.maxFileSize && 399 file.size > this.options.maxFileSize) { 400 return 'maxFileSize'; 401 } 402 if (typeof file.size === 'number' && 403 file.size < this.options.minFileSize) { 404 return 'minFileSize'; 405 } 406 return null; 407 }, 408 409 _validate: function (files) { 410 var that = this, 411 valid = !!files.length; 412 $.each(files, function (index, file) { 413 file.error = that._hasError(file); 414 if (file.error) { 415 valid = false; 416 } 417 }); 418 return valid; 419 }, 420 421 _renderTemplate: function (func, files) { 422 if (!func) { 423 return $(); 424 } 425 var result = func({ 426 files: files, 427 formatFileSize: this._formatFileSize, 428 options: this.options 429 }); 430 if (result instanceof $) { 431 return result; 432 } 433 return $(this.options.templatesContainer).html(result).children();
434 }, 435 436 _renderPreview: function (file, node) { 437 var that = this, 438 options = this.options, 439 dfd = $.Deferred(); 440 return ((loadImage && loadImage( 441 file, 442 function (img) { 443 node.append(img); 444 that._forceReflow(node); 445 that._transition(node).done(function () { 446 dfd.resolveWith(node); 447 }); 448 if (!$.contains(document.body, node[0])) { 449 // If the element is not part of the DOM, 450 // transition events are not triggered, 451 // so we have to resolve manually: 452 dfd.resolveWith(node); 453 } 454 }, 455 { 456 maxWidth: options.previewMaxWidth, 457 maxHeight: options.previewMaxHeight, 458 canvas: options.previewAsCanvas 459 } 460 )) || dfd.resolveWith(node)) && dfd; 461 }, 462 463 _renderPreviews: function (files, nodes) { 464 var that = this, 465 options = this.options; 466 nodes.find('.preview span').each(function (index, element) { 467 var file = files[index]; 468 if (options.previewSourceFileTypes.test(file.type) && 469 ($.type(options.previewSourceMaxFileSize) !== 'number' || 470 file.size < options.previewSourceMaxFileSize)) { 471 that._processingQueue = that._processingQueue.pipe(function () { 472 var dfd = $.Deferred(); 473 that._renderPreview(file, $(element)).done( 474 function () { 475 dfd.resolveWith(that); 476 } 477 ); 478 return dfd.promise(); 479 }); 480 } 481 }); 482 return this._processingQueue; 483 }, 484 485 _renderUpload: function (files) { 486 return this._renderTemplate( 487 this.options.uploadTemplate, 488 files 489 ); 490 }, 491 492 _renderDownload: function (files) { 493 return this._renderTemplate( 494 this.options.downloadTemplate, 495 files 496 ).find('a[download]').each(this._enableDragToDesktop).end(); 497 }, 498 499 _startHandler: function (e) { 500 e.preventDefault(); 501 var button = $(this), 502 template = button.closest('.template-upload'), 503 data = template.data('data'); 504 if (data && data.submit && !data.jqXHR && data.submit()) { 505 button.prop('disabled', true); 506 } 507 }, 508 509 _cancelHandler: function (e) { 510 e.preventDefault(); 511 var template = $(this).closest('.template-upload'), 512 data = template.data('data') || {}; 513 if (!data.jqXHR) { 514 data.errorThrown = 'abort'; 515 e.data.fileupload._trigger('fail', e, data); 516 } else { 517 data.jqXHR.abort(); 518 } 519 }, 520 521 _deleteHandler: function (e) { 522 e.preventDefault(); 523 var button = $(this); 524 e.data.fileupload._trigger('destroy', e, { 525 context: button.closest('.template-download'), 526 url: button.attr('data-url'), 527 type: button.attr('data-type') || 'DELETE', 528 dataType: e.data.fileupload.options.dataType 529 }); 530 }, 531 532 _forceReflow: function (node) { 533 return $.support.transition && node.length && 534 node[0].offsetWidth; 535 }, 536 537 _transition: function (node) { 538 var dfd = $.Deferred(); 539 if ($.support.transition && node.hasClass('fade')) { 540 node.bind( 541 $.support.transition.end, 542 function (e) { 543 // Make sure we don't respond to other transitions events 544 // in the container element, e.g. from button elements: 545 if (e.target === node[0]) { 546 node.unbind($.support.transition.end); 547 dfd.resolveWith(node); 548 } 549 } 550 ).toggleClass('in'); 551 } else { 552 node.toggleClass('in'); 553 dfd.resolveWith(node); 554 } 555 return dfd; 556 }, 557 558 _initButtonBarEventHandlers: function () { 559 var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), 560 filesList = this.options.filesContainer, 561 ns = this.options.namespace; 562 fileUploadButtonBar.find('.start') 563 .bind('click.' + ns, function (e) { 564 e.preventDefault(); 565 filesList.find('.start button').click(); 566 }); 567 fileUploadButtonBar.find('.cancel') 568 .bind('click.' + ns, function (e) { 569 e.preventDefault(); 570 filesList.find('.cancel button').click(); 571 }); 572 fileUploadButtonBar.find('.delete') 573 .bind('click.' + ns, function (e) { 574 e.preventDefault();
575 filesList.find('.delete input:checked') 576 .siblings('button').click(); 577 fileUploadButtonBar.find('.toggle') 578 .prop('checked', false); 579 }); 580 fileUploadButtonBar.find('.toggle') 581 .bind('change.' + ns, function (e) { 582 filesList.find('.delete input').prop( 583 'checked', 584 $(this).is(':checked') 585 ); 586 }); 587 }, 588 589 _destroyButtonBarEventHandlers: function () { 590 this.element.find('.fileupload-buttonbar button') 591 .unbind('click.' + this.options.namespace); 592 this.element.find('.fileupload-buttonbar .toggle') 593 .unbind('change.' + this.options.namespace); 594 }, 595 596 _initEventHandlers: function () { 597 parentWidget.prototype._initEventHandlers.call(this); 598 var eventData = {fileupload: this}; 599 this.options.filesContainer 600 .delegate( 601 '.start button', 602 'click.' + this.options.namespace, 603 eventData, 604 this._startHandler 605 ) 606 .delegate( 607 '.cancel button', 608 'click.' + this.options.namespace, 609 eventData, 610 this._cancelHandler 611 ) 612 .delegate( 613 '.delete button', 614 'click.' + this.options.namespace, 615 eventData, 616 this._deleteHandler 617 ); 618 this._initButtonBarEventHandlers(); 619 }, 620 621 _destroyEventHandlers: function () { 622 var options = this.options; 623 this._destroyButtonBarEventHandlers(); 624 options.filesContainer 625 .undelegate('.start button', 'click.' + options.namespace) 626 .undelegate('.cancel button', 'click.' + options.namespace) 627 .undelegate('.delete button', 'click.' + options.namespace); 628 parentWidget.prototype._destroyEventHandlers.call(this); 629 }, 630 631 _enableFileInputButton: function () { 632 this.element.find('.fileinput-button input') 633 .prop('disabled', false) 634 .parent().removeClass('disabled'); 635 }, 636 637 _disableFileInputButton: function () { 638 this.element.find('.fileinput-button input') 639 .prop('disabled', true) 640 .parent().addClass('disabled'); 641 }, 642 643 _initTemplates: function () { 644 var options = this.options; 645 options.templatesContainer = document.createElement( 646 options.filesContainer.prop('nodeName') 647 ); 648 if (tmpl) { 649 if (options.uploadTemplateId) { 650 options.uploadTemplate = tmpl(options.uploadTemplateId); 651 } 652 if (options.downloadTemplateId) { 653 options.downloadTemplate = tmpl(options.downloadTemplateId); 654 } 655 } 656 }, 657 658 _initFilesContainer: function () { 659 var options = this.options; 660 if (options.filesContainer === undefined) { 661 options.filesContainer = this.element.find('.files'); 662 } else if (!(options.filesContainer instanceof $)) { 663 options.filesContainer = $(options.filesContainer); 664 } 665 }, 666 667 _initSpecialOptions: function () { 668 parentWidget.prototype._initSpecialOptions.call(this); 669 this._initFilesContainer(); 670 this._initTemplates(); 671 }, 672 673 _create: function () { 674 parentWidget.prototype._create.call(this); 675 this._refreshOptionsList.push( 676 'filesContainer', 677 'uploadTemplateId', 678 'downloadTemplateId' 679 ); 680 if (!$.blueimpFP) { 681 this._processingQueue = $.Deferred().resolveWith(this).promise(); 682 this.process = function () { 683 return this._processingQueue; 684 }; 685 } 686 }, 687 688 enable: function () { 689 parentWidget.prototype.enable.call(this); 690 this.element.find('input, button').prop('disabled', false); 691 this._enableFileInputButton(); 692 }, 693 694 disable: function () { 695 this.element.find('input, button').prop('disabled', true); 696 this._disableFileInputButton(); 697 parentWidget.prototype.disable.call(this); 698 } 699 700 });
701 702}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.