jquery.fileupload-ui.js 27 KB

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