jquery.fileupload-ui.js 27 KB

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