wp-plupload.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /* global pluploadL10n, plupload, _wpPluploadSettings */
  2. window.wp = window.wp || {};
  3. ( function( exports, $ ) {
  4. var Uploader;
  5. if ( typeof _wpPluploadSettings === 'undefined' ) {
  6. return;
  7. }
  8. /**
  9. * A WordPress uploader.
  10. *
  11. * The Plupload library provides cross-browser uploader UI integration.
  12. * This object bridges the Plupload API to integrate uploads into the
  13. * WordPress back end and the WordPress media experience.
  14. *
  15. * @param {object} options The options passed to the new plupload instance.
  16. * @param {object} options.container The id of uploader container.
  17. * @param {object} options.browser The id of button to trigger the file select.
  18. * @param {object} options.dropzone The id of file drop target.
  19. * @param {object} options.plupload An object of parameters to pass to the plupload instance.
  20. * @param {object} options.params An object of parameters to pass to $_POST when uploading the file.
  21. * Extends this.plupload.multipart_params under the hood.
  22. */
  23. Uploader = function( options ) {
  24. var self = this,
  25. isIE = navigator.userAgent.indexOf('Trident/') != -1 || navigator.userAgent.indexOf('MSIE ') != -1,
  26. elements = {
  27. container: 'container',
  28. browser: 'browse_button',
  29. dropzone: 'drop_element'
  30. },
  31. key, error;
  32. this.supports = {
  33. upload: Uploader.browser.supported
  34. };
  35. this.supported = this.supports.upload;
  36. if ( ! this.supported ) {
  37. return;
  38. }
  39. // Arguments to send to pluplad.Uploader().
  40. // Use deep extend to ensure that multipart_params and other objects are cloned.
  41. this.plupload = $.extend( true, { multipart_params: {} }, Uploader.defaults );
  42. this.container = document.body; // Set default container.
  43. // Extend the instance with options.
  44. //
  45. // Use deep extend to allow options.plupload to override individual
  46. // default plupload keys.
  47. $.extend( true, this, options );
  48. // Proxy all methods so this always refers to the current instance.
  49. for ( key in this ) {
  50. if ( $.isFunction( this[ key ] ) ) {
  51. this[ key ] = $.proxy( this[ key ], this );
  52. }
  53. }
  54. // Ensure all elements are jQuery elements and have id attributes,
  55. // then set the proper plupload arguments to the ids.
  56. for ( key in elements ) {
  57. if ( ! this[ key ] ) {
  58. continue;
  59. }
  60. this[ key ] = $( this[ key ] ).first();
  61. if ( ! this[ key ].length ) {
  62. delete this[ key ];
  63. continue;
  64. }
  65. if ( ! this[ key ].prop('id') ) {
  66. this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ );
  67. }
  68. this.plupload[ elements[ key ] ] = this[ key ].prop('id');
  69. }
  70. // If the uploader has neither a browse button nor a dropzone, bail.
  71. if ( ! ( this.browser && this.browser.length ) && ! ( this.dropzone && this.dropzone.length ) ) {
  72. return;
  73. }
  74. // Make sure flash sends cookies (seems in IE it does without switching to urlstream mode)
  75. if ( ! isIE && 'flash' === plupload.predictRuntime( this.plupload ) &&
  76. ( ! this.plupload.required_features || ! this.plupload.required_features.hasOwnProperty( 'send_binary_string' ) ) ) {
  77. this.plupload.required_features = this.plupload.required_features || {};
  78. this.plupload.required_features.send_binary_string = true;
  79. }
  80. // Initialize the plupload instance.
  81. this.uploader = new plupload.Uploader( this.plupload );
  82. delete this.plupload;
  83. // Set default params and remove this.params alias.
  84. this.param( this.params || {} );
  85. delete this.params;
  86. /**
  87. * Custom error callback.
  88. *
  89. * Add a new error to the errors collection, so other modules can track
  90. * and display errors. @see wp.Uploader.errors.
  91. *
  92. * @param {string} message
  93. * @param {object} data
  94. * @param {plupload.File} file File that was uploaded.
  95. */
  96. error = function( message, data, file ) {
  97. if ( file.attachment ) {
  98. file.attachment.destroy();
  99. }
  100. Uploader.errors.unshift({
  101. message: message || pluploadL10n.default_error,
  102. data: data,
  103. file: file
  104. });
  105. self.error( message, data, file );
  106. };
  107. /**
  108. * After the Uploader has been initialized, initialize some behaviors for the dropzone.
  109. *
  110. * @param {plupload.Uploader} uploader Uploader instance.
  111. */
  112. this.uploader.bind( 'init', function( uploader ) {
  113. var timer, active, dragdrop,
  114. dropzone = self.dropzone;
  115. dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile;
  116. // Generate drag/drop helper classes.
  117. if ( ! dropzone ) {
  118. return;
  119. }
  120. dropzone.toggleClass( 'supports-drag-drop', !! dragdrop );
  121. if ( ! dragdrop ) {
  122. return dropzone.unbind('.wp-uploader');
  123. }
  124. // 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'.
  125. dropzone.bind( 'dragover.wp-uploader', function() {
  126. if ( timer ) {
  127. clearTimeout( timer );
  128. }
  129. if ( active ) {
  130. return;
  131. }
  132. dropzone.trigger('dropzone:enter').addClass('drag-over');
  133. active = true;
  134. });
  135. dropzone.bind('dragleave.wp-uploader, drop.wp-uploader', function() {
  136. // Using an instant timer prevents the drag-over class from
  137. // being quickly removed and re-added when elements inside the
  138. // dropzone are repositioned.
  139. //
  140. // @see https://core.trac.wordpress.org/ticket/21705
  141. timer = setTimeout( function() {
  142. active = false;
  143. dropzone.trigger('dropzone:leave').removeClass('drag-over');
  144. }, 0 );
  145. });
  146. self.ready = true;
  147. $(self).trigger( 'uploader:ready' );
  148. });
  149. this.uploader.bind( 'postinit', function( up ) {
  150. up.refresh();
  151. self.init();
  152. });
  153. this.uploader.init();
  154. if ( this.browser ) {
  155. this.browser.on( 'mouseenter', this.refresh );
  156. } else {
  157. this.uploader.disableBrowse( true );
  158. // If HTML5 mode, hide the auto-created file container.
  159. $('#' + this.uploader.id + '_html5_container').hide();
  160. }
  161. /**
  162. * After files were filtered and added to the queue, create a model for each.
  163. *
  164. * @param {plupload.Uploader} uploader Uploader instance.
  165. * @param {Array} files Array of file objects that were added to queue by the user.
  166. */
  167. this.uploader.bind( 'FilesAdded', function( up, files ) {
  168. _.each( files, function( file ) {
  169. var attributes, image;
  170. // Ignore failed uploads.
  171. if ( plupload.FAILED === file.status ) {
  172. return;
  173. }
  174. // Generate attributes for a new `Attachment` model.
  175. attributes = _.extend({
  176. file: file,
  177. uploading: true,
  178. date: new Date(),
  179. filename: file.name,
  180. menuOrder: 0,
  181. uploadedTo: wp.media.model.settings.post.id
  182. }, _.pick( file, 'loaded', 'size', 'percent' ) );
  183. // Handle early mime type scanning for images.
  184. image = /(?:jpe?g|png|gif)$/i.exec( file.name );
  185. // For images set the model's type and subtype attributes.
  186. if ( image ) {
  187. attributes.type = 'image';
  188. // `jpeg`, `png` and `gif` are valid subtypes.
  189. // `jpg` is not, so map it to `jpeg`.
  190. attributes.subtype = ( 'jpg' === image[0] ) ? 'jpeg' : image[0];
  191. }
  192. // Create a model for the attachment, and add it to the Upload queue collection
  193. // so listeners to the upload queue can track and display upload progress.
  194. file.attachment = wp.media.model.Attachment.create( attributes );
  195. Uploader.queue.add( file.attachment );
  196. self.added( file.attachment );
  197. });
  198. up.refresh();
  199. up.start();
  200. });
  201. this.uploader.bind( 'UploadProgress', function( up, file ) {
  202. file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
  203. self.progress( file.attachment );
  204. });
  205. /**
  206. * After a file is successfully uploaded, update its model.
  207. *
  208. * @param {plupload.Uploader} uploader Uploader instance.
  209. * @param {plupload.File} file File that was uploaded.
  210. * @param {Object} response Object with response properties.
  211. * @return {mixed}
  212. */
  213. this.uploader.bind( 'FileUploaded', function( up, file, response ) {
  214. var complete;
  215. try {
  216. response = JSON.parse( response.response );
  217. } catch ( e ) {
  218. return error( pluploadL10n.default_error, e, file );
  219. }
  220. if ( ! _.isObject( response ) || _.isUndefined( response.success ) )
  221. return error( pluploadL10n.default_error, null, file );
  222. else if ( ! response.success )
  223. return error( response.data && response.data.message, response.data, file );
  224. _.each(['file','loaded','size','percent'], function( key ) {
  225. file.attachment.unset( key );
  226. });
  227. file.attachment.set( _.extend( response.data, { uploading: false }) );
  228. wp.media.model.Attachment.get( response.data.id, file.attachment );
  229. complete = Uploader.queue.all( function( attachment ) {
  230. return ! attachment.get('uploading');
  231. });
  232. if ( complete )
  233. Uploader.queue.reset();
  234. self.success( file.attachment );
  235. });
  236. /**
  237. * When plupload surfaces an error, send it to the error handler.
  238. *
  239. * @param {plupload.Uploader} uploader Uploader instance.
  240. * @param {Object} error Contains code, message and sometimes file and other details.
  241. */
  242. this.uploader.bind( 'Error', function( up, pluploadError ) {
  243. var message = pluploadL10n.default_error,
  244. key;
  245. // Check for plupload errors.
  246. for ( key in Uploader.errorMap ) {
  247. if ( pluploadError.code === plupload[ key ] ) {
  248. message = Uploader.errorMap[ key ];
  249. if ( _.isFunction( message ) ) {
  250. message = message( pluploadError.file, pluploadError );
  251. }
  252. break;
  253. }
  254. }
  255. error( message, pluploadError, pluploadError.file );
  256. up.refresh();
  257. });
  258. };
  259. // Adds the 'defaults' and 'browser' properties.
  260. $.extend( Uploader, _wpPluploadSettings );
  261. Uploader.uuid = 0;
  262. // Map Plupload error codes to user friendly error messages.
  263. Uploader.errorMap = {
  264. 'FAILED': pluploadL10n.upload_failed,
  265. 'FILE_EXTENSION_ERROR': pluploadL10n.invalid_filetype,
  266. 'IMAGE_FORMAT_ERROR': pluploadL10n.not_an_image,
  267. 'IMAGE_MEMORY_ERROR': pluploadL10n.image_memory_exceeded,
  268. 'IMAGE_DIMENSIONS_ERROR': pluploadL10n.image_dimensions_exceeded,
  269. 'GENERIC_ERROR': pluploadL10n.upload_failed,
  270. 'IO_ERROR': pluploadL10n.io_error,
  271. 'HTTP_ERROR': pluploadL10n.http_error,
  272. 'SECURITY_ERROR': pluploadL10n.security_error,
  273. 'FILE_SIZE_ERROR': function( file ) {
  274. return pluploadL10n.file_exceeds_size_limit.replace('%s', file.name);
  275. }
  276. };
  277. $.extend( Uploader.prototype, /** @lends wp.Uploader.prototype */{
  278. /**
  279. * Acts as a shortcut to extending the uploader's multipart_params object.
  280. *
  281. * param( key )
  282. * Returns the value of the key.
  283. *
  284. * param( key, value )
  285. * Sets the value of a key.
  286. *
  287. * param( map )
  288. * Sets values for a map of data.
  289. */
  290. param: function( key, value ) {
  291. if ( arguments.length === 1 && typeof key === 'string' ) {
  292. return this.uploader.settings.multipart_params[ key ];
  293. }
  294. if ( arguments.length > 1 ) {
  295. this.uploader.settings.multipart_params[ key ] = value;
  296. } else {
  297. $.extend( this.uploader.settings.multipart_params, key );
  298. }
  299. },
  300. /**
  301. * Make a few internal event callbacks available on the wp.Uploader object
  302. * to change the Uploader internals if absolutely necessary.
  303. */
  304. init: function() {},
  305. error: function() {},
  306. success: function() {},
  307. added: function() {},
  308. progress: function() {},
  309. complete: function() {},
  310. refresh: function() {
  311. var node, attached, container, id;
  312. if ( this.browser ) {
  313. node = this.browser[0];
  314. // Check if the browser node is in the DOM.
  315. while ( node ) {
  316. if ( node === document.body ) {
  317. attached = true;
  318. break;
  319. }
  320. node = node.parentNode;
  321. }
  322. // If the browser node is not attached to the DOM, use a
  323. // temporary container to house it, as the browser button
  324. // shims require the button to exist in the DOM at all times.
  325. if ( ! attached ) {
  326. id = 'wp-uploader-browser-' + this.uploader.id;
  327. container = $( '#' + id );
  328. if ( ! container.length ) {
  329. container = $('<div class="wp-uploader-browser" />').css({
  330. position: 'fixed',
  331. top: '-1000px',
  332. left: '-1000px',
  333. height: 0,
  334. width: 0
  335. }).attr( 'id', 'wp-uploader-browser-' + this.uploader.id ).appendTo('body');
  336. }
  337. container.append( this.browser );
  338. }
  339. }
  340. this.uploader.refresh();
  341. }
  342. });
  343. // Create a collection of attachments in the upload queue,
  344. // so that other modules can track and display upload progress.
  345. Uploader.queue = new wp.media.model.Attachments( [], { query: false });
  346. // Create a collection to collect errors incurred while attempting upload.
  347. Uploader.errors = new Backbone.Collection();
  348. exports.Uploader = Uploader;
  349. })( wp, jQuery );