jquery.fileupload-ui.js 27 KB

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