jquery.fileupload-ui.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. /*
  2. * jQuery File Upload User Interface Plugin 9.4.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. /* 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. $.when(
  92. that._transition(data.context),
  93. data.process(function () {
  94. return $this.fileupload('process', data);
  95. })
  96. ).always(function () {
  97. data.context.each(function (index) {
  98. $(this).find('.size').text(
  99. that._formatFileSize(data.files[index].size)
  100. );
  101. }).removeClass('processing');
  102. that._renderPreviews(data);
  103. }).done(function () {
  104. data.context.find('.start').prop('disabled', false);
  105. if ((that._trigger('added', e, data) !== false) &&
  106. (options.autoUpload || data.autoUpload) &&
  107. data.autoUpload !== false) {
  108. data.submit();
  109. }
  110. }).fail(function () {
  111. if (data.files.error) {
  112. data.context.each(function (index) {
  113. var error = data.files[index].error;
  114. if (error) {
  115. $(this).find('.error').text(error);
  116. }
  117. });
  118. }
  119. });
  120. },
  121. // Callback for the start of each file upload request:
  122. send: function (e, data) {
  123. if (e.isDefaultPrevented()) {
  124. return false;
  125. }
  126. var that = $(this).data('blueimp-fileupload') ||
  127. $(this).data('fileupload');
  128. if (data.context && data.dataType &&
  129. data.dataType.substr(0, 6) === 'iframe') {
  130. // Iframe Transport does not support progress events.
  131. // In lack of an indeterminate progress bar, we set
  132. // the progress to 100%, showing the full animated bar:
  133. data.context
  134. .find('.progress').addClass(
  135. !$.support.transition && 'progress-animated'
  136. )
  137. .attr('aria-valuenow', 100)
  138. .children().first().css(
  139. 'width',
  140. '100%'
  141. );
  142. }
  143. return that._trigger('sent', e, data);
  144. },
  145. // Callback for successful uploads:
  146. done: function (e, data) {
  147. if (e.isDefaultPrevented()) {
  148. return false;
  149. }
  150. var that = $(this).data('blueimp-fileupload') ||
  151. $(this).data('fileupload'),
  152. getFilesFromResponse = data.getFilesFromResponse ||
  153. that.options.getFilesFromResponse,
  154. files = getFilesFromResponse(data),
  155. template,
  156. deferred;
  157. if (data.context) {
  158. data.context.each(function (index) {
  159. var file = files[index] ||
  160. {error: 'Empty file upload result'};
  161. deferred = that._addFinishedDeferreds();
  162. that._transition($(this)).done(
  163. function () {
  164. var node = $(this);
  165. template = that._renderDownload([file])
  166. .replaceAll(node);
  167. that._forceReflow(template);
  168. that._transition(template).done(
  169. function () {
  170. data.context = $(this);
  171. that._trigger('completed', e, data);
  172. that._trigger('finished', e, data);
  173. deferred.resolve();
  174. }
  175. );
  176. }
  177. );
  178. });
  179. } else {
  180. template = that._renderDownload(files)[
  181. that.options.prependFiles ? 'prependTo' : 'appendTo'
  182. ](that.options.filesContainer);
  183. that._forceReflow(template);
  184. deferred = that._addFinishedDeferreds();
  185. that._transition(template).done(
  186. function () {
  187. data.context = $(this);
  188. that._trigger('completed', e, data);
  189. that._trigger('finished', e, data);
  190. deferred.resolve();
  191. }
  192. );
  193. }
  194. },
  195. // Callback for failed (abort or error) uploads:
  196. fail: function (e, data) {
  197. if (e.isDefaultPrevented()) {
  198. return false;
  199. }
  200. var that = $(this).data('blueimp-fileupload') ||
  201. $(this).data('fileupload'),
  202. template,
  203. deferred;
  204. if (data.context) {
  205. data.context.each(function (index) {
  206. if (data.errorThrown !== 'abort') {
  207. var file = data.files[index];
  208. file.error = file.error || data.errorThrown ||
  209. true;
  210. deferred = that._addFinishedDeferreds();
  211. that._transition($(this)).done(
  212. function () {
  213. var node = $(this);
  214. template = that._renderDownload([file])
  215. .replaceAll(node);
  216. that._forceReflow(template);
  217. that._transition(template).done(
  218. function () {
  219. data.context = $(this);
  220. that._trigger('failed', e, data);
  221. that._trigger('finished', e, data);
  222. deferred.resolve();
  223. }
  224. );
  225. }
  226. );
  227. } else {
  228. deferred = that._addFinishedDeferreds();
  229. that._transition($(this)).done(
  230. function () {
  231. $(this).remove();
  232. that._trigger('failed', e, data);
  233. that._trigger('finished', e, data);
  234. deferred.resolve();
  235. }
  236. );
  237. }
  238. });
  239. } else if (data.errorThrown !== 'abort') {
  240. data.context = that._renderUpload(data.files)[
  241. that.options.prependFiles ? 'prependTo' : 'appendTo'
  242. ](that.options.filesContainer)
  243. .data('data', data);
  244. that._forceReflow(data.context);
  245. deferred = that._addFinishedDeferreds();
  246. that._transition(data.context).done(
  247. function () {
  248. data.context = $(this);
  249. that._trigger('failed', e, data);
  250. that._trigger('finished', e, data);
  251. deferred.resolve();
  252. }
  253. );
  254. } else {
  255. that._trigger('failed', e, data);
  256. that._trigger('finished', e, data);
  257. that._addFinishedDeferreds().resolve();
  258. }
  259. },
  260. // Callback for upload progress events:
  261. progress: function (e, data) {
  262. if (e.isDefaultPrevented()) {
  263. return false;
  264. }
  265. var progress = Math.floor(data.loaded / data.total * 100);
  266. if (data.context) {
  267. data.context.each(function () {
  268. $(this).find('.progress')
  269. .attr('aria-valuenow', progress)
  270. .children().first().css(
  271. 'width',
  272. progress + '%'
  273. );
  274. });
  275. }
  276. },
  277. // Callback for global upload progress events:
  278. progressall: function (e, data) {
  279. if (e.isDefaultPrevented()) {
  280. return false;
  281. }
  282. var $this = $(this),
  283. progress = Math.floor(data.loaded / data.total * 100),
  284. globalProgressNode = $this.find('.fileupload-progress'),
  285. extendedProgressNode = globalProgressNode
  286. .find('.progress-extended');
  287. if (extendedProgressNode.length) {
  288. extendedProgressNode.html(
  289. ($this.data('blueimp-fileupload') || $this.data('fileupload'))
  290. ._renderExtendedProgress(data)
  291. );
  292. }
  293. globalProgressNode
  294. .find('.progress')
  295. .attr('aria-valuenow', progress)
  296. .children().first().css(
  297. 'width',
  298. progress + '%'
  299. );
  300. },
  301. // Callback for uploads start, equivalent to the global ajaxStart event:
  302. start: function (e) {
  303. if (e.isDefaultPrevented()) {
  304. return false;
  305. }
  306. var that = $(this).data('blueimp-fileupload') ||
  307. $(this).data('fileupload');
  308. that._resetFinishedDeferreds();
  309. that._transition($(this).find('.fileupload-progress')).done(
  310. function () {
  311. that._trigger('started', e);
  312. }
  313. );
  314. },
  315. // Callback for uploads stop, equivalent to the global ajaxStop event:
  316. stop: function (e) {
  317. if (e.isDefaultPrevented()) {
  318. return false;
  319. }
  320. var that = $(this).data('blueimp-fileupload') ||
  321. $(this).data('fileupload'),
  322. deferred = that._addFinishedDeferreds();
  323. $.when.apply($, that._getFinishedDeferreds())
  324. .done(function () {
  325. that._trigger('stopped', e);
  326. });
  327. that._transition($(this).find('.fileupload-progress')).done(
  328. function () {
  329. $(this).find('.progress')
  330. .attr('aria-valuenow', '0')
  331. .children().first().css('width', '0%');
  332. $(this).find('.progress-extended').html(' ');
  333. deferred.resolve();
  334. }
  335. );
  336. },
  337. processstart: function (e) {
  338. if (e.isDefaultPrevented()) {
  339. return false;
  340. }
  341. $(this).addClass('fileupload-processing');
  342. },
  343. processstop: function (e) {
  344. if (e.isDefaultPrevented()) {
  345. return false;
  346. }
  347. $(this).removeClass('fileupload-processing');
  348. },
  349. // Callback for file deletion:
  350. destroy: function (e, data) {
  351. if (e.isDefaultPrevented()) {
  352. return false;
  353. }
  354. var that = $(this).data('blueimp-fileupload') ||
  355. $(this).data('fileupload'),
  356. removeNode = function () {
  357. that._transition(data.context).done(
  358. function () {
  359. $(this).remove();
  360. that._trigger('destroyed', e, data);
  361. }
  362. );
  363. };
  364. if (data.url) {
  365. data.dataType = data.dataType || that.options.dataType;
  366. $.ajax(data).done(removeNode);
  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. }));