videopress-plupload.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /* global pluploadL10n, plupload, _wpPluploadSettings, JSON */
  2. window.wp = window.wp || {};
  3. ( function( exports, $ ) {
  4. var Uploader, vp;
  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. // Make sure that the VideoPress object is available
  87. if ( typeof exports.VideoPress !== 'undefined' ) {
  88. vp = exports.VideoPress;
  89. } else {
  90. window.console && window.console.error( 'The VideoPress object was not loaded. Errors may occur.' );
  91. }
  92. /**
  93. * Custom error callback.
  94. *
  95. * Add a new error to the errors collection, so other modules can track
  96. * and display errors. @see wp.Uploader.errors.
  97. *
  98. * @param {string} message
  99. * @param {object} data
  100. * @param {plupload.File} file File that was uploaded.
  101. */
  102. error = function( message, data, file ) {
  103. if ( file.attachment ) {
  104. file.attachment.destroy();
  105. }
  106. Uploader.errors.unshift({
  107. message: message || pluploadL10n.default_error,
  108. data: data,
  109. file: file
  110. });
  111. self.error( message, data, file );
  112. };
  113. /**
  114. * After the Uploader has been initialized, initialize some behaviors for the dropzone.
  115. *
  116. * @param {plupload.Uploader} uploader Uploader instance.
  117. */
  118. this.uploader.bind( 'init', function( uploader ) {
  119. var timer, active, dragdrop,
  120. dropzone = self.dropzone;
  121. dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile;
  122. // Generate drag/drop helper classes.
  123. if ( ! dropzone ) {
  124. return;
  125. }
  126. dropzone.toggleClass( 'supports-drag-drop', !! dragdrop );
  127. if ( ! dragdrop ) {
  128. return dropzone.unbind('.wp-uploader');
  129. }
  130. // 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'.
  131. dropzone.bind( 'dragover.wp-uploader', function() {
  132. if ( timer ) {
  133. clearTimeout( timer );
  134. }
  135. if ( active ) {
  136. return;
  137. }
  138. dropzone.trigger('dropzone:enter').addClass('drag-over');
  139. active = true;
  140. });
  141. dropzone.bind('dragleave.wp-uploader, drop.wp-uploader', function() {
  142. // Using an instant timer prevents the drag-over class from
  143. // being quickly removed and re-added when elements inside the
  144. // dropzone are repositioned.
  145. //
  146. // @see https://core.trac.wordpress.org/ticket/21705
  147. timer = setTimeout( function() {
  148. active = false;
  149. dropzone.trigger('dropzone:leave').removeClass('drag-over');
  150. }, 0 );
  151. });
  152. self.ready = true;
  153. $(self).trigger( 'uploader:ready' );
  154. });
  155. this.uploader.bind( 'postinit', function( up ) {
  156. up.refresh();
  157. self.init();
  158. });
  159. this.uploader.init();
  160. if ( this.browser ) {
  161. this.browser.on( 'mouseenter', this.refresh );
  162. } else {
  163. this.uploader.disableBrowse( true );
  164. // If HTML5 mode, hide the auto-created file container.
  165. $('#' + this.uploader.id + '_html5_container').hide();
  166. }
  167. /**
  168. * After files were filtered and added to the queue, create a model for each.
  169. *
  170. * @event FilesAdded
  171. * @param {plupload.Uploader} uploader Uploader instance.
  172. * @param {Array} files Array of file objects that were added to queue by the user.
  173. */
  174. this.uploader.bind( 'FilesAdded', function( up, files ) {
  175. _.each( files, function( file ) {
  176. var attributes, image;
  177. // Ignore failed uploads.
  178. if ( plupload.FAILED === file.status ) {
  179. return;
  180. }
  181. // Generate attributes for a new `Attachment` model.
  182. attributes = _.extend({
  183. file: file,
  184. uploading: true,
  185. date: new Date(),
  186. filename: file.name,
  187. menuOrder: 0,
  188. uploadedTo: wp.media.model.settings.post.id
  189. }, _.pick( file, 'loaded', 'size', 'percent' ) );
  190. // Handle early mime type scanning for images.
  191. image = /(?:jpe?g|png|gif)$/i.exec( file.name );
  192. // For images set the model's type and subtype attributes.
  193. if ( image ) {
  194. attributes.type = 'image';
  195. // `jpeg`, `png` and `gif` are valid subtypes.
  196. // `jpg` is not, so map it to `jpeg`.
  197. attributes.subtype = ( 'jpg' === image[0] ) ? 'jpeg' : image[0];
  198. }
  199. // Create a model for the attachment, and add it to the Upload queue collection
  200. // so listeners to the upload queue can track and display upload progress.
  201. file.attachment = wp.media.model.Attachment.create( attributes );
  202. Uploader.queue.add( file.attachment );
  203. self.added( file.attachment );
  204. });
  205. up.refresh();
  206. up.start();
  207. });
  208. this.uploader.bind( 'UploadProgress', function( up, file ) {
  209. file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
  210. self.progress( file.attachment );
  211. });
  212. /**
  213. * After a file is successfully uploaded, update its model.
  214. *
  215. * @param {plupload.Uploader} uploader Uploader instance.
  216. * @param {plupload.File} file File that was uploaded.
  217. * @param {Object} response Object with response properties.
  218. * @return {mixed}
  219. */
  220. this.uploader.bind( 'FileUploaded', function( up, file, response ) {
  221. var complete;
  222. try {
  223. response = JSON.parse( response.response );
  224. } catch ( e ) {
  225. return error( pluploadL10n.default_error, e, file );
  226. }
  227. if ( typeof response.media !== 'undefined' ) {
  228. response = vp.handleRestApiResponse( response, file );
  229. } else {
  230. response = vp.handleStandardResponse( response, file );
  231. }
  232. _.each(['file','loaded','size','percent'], function( key ) {
  233. file.attachment.unset( key );
  234. });
  235. file.attachment.set( _.extend( response.data, { uploading: false }) );
  236. wp.media.model.Attachment.get( response.data.id, file.attachment );
  237. complete = Uploader.queue.all( function( attachment ) {
  238. return ! attachment.get('uploading');
  239. });
  240. if ( complete ) {
  241. vp && vp.resetToOriginalOptions( up );
  242. Uploader.queue.reset();
  243. }
  244. self.success( file.attachment );
  245. });
  246. /**
  247. * When plupload surfaces an error, send it to the error handler.
  248. *
  249. * @param {plupload.Uploader} uploader Uploader instance.
  250. * @param {Object} error Contains code, message and sometimes file and other details.
  251. */
  252. this.uploader.bind( 'Error', function( up, pluploadError ) {
  253. var message = pluploadL10n.default_error,
  254. key;
  255. // Check for plupload errors.
  256. for ( key in Uploader.errorMap ) {
  257. if ( pluploadError.code === plupload[ key ] ) {
  258. message = Uploader.errorMap[ key ];
  259. if ( _.isFunction( message ) ) {
  260. message = message( pluploadError.file, pluploadError );
  261. }
  262. break;
  263. }
  264. }
  265. error( message, pluploadError, pluploadError.file );
  266. vp && vp.resetToOriginalOptions( up );
  267. up.refresh();
  268. });
  269. /**
  270. * Add in a way for the uploader to reset itself when uploads are complete.
  271. */
  272. this.uploader.bind( 'UploadComplete', function( up ) {
  273. vp && vp.resetToOriginalOptions( up );
  274. });
  275. /**
  276. * Before we upload, check to see if this file is a videopress upload, if so, set new options and save the old ones.
  277. */
  278. this.uploader.bind( 'BeforeUpload', function( up, file ) {
  279. if ( typeof file.videopress !== 'undefined' ) {
  280. vp.originalOptions.url = up.getOption( 'url' );
  281. vp.originalOptions.multipart_params = up.getOption( 'multipart_params' );
  282. vp.originalOptions.file_data_name = up.getOption( 'file_data_name' );
  283. up.setOption( 'file_data_name', 'media[]' );
  284. up.setOption( 'url', file.videopress.upload_action_url );
  285. up.setOption( 'headers', {
  286. Authorization: 'X_UPLOAD_TOKEN token="' + file.videopress.upload_token + '" blog_id="' + file.videopress.upload_blog_id + '"'
  287. });
  288. }
  289. });
  290. };
  291. // Adds the 'defaults' and 'browser' properties.
  292. $.extend( Uploader, _wpPluploadSettings );
  293. Uploader.uuid = 0;
  294. // Map Plupload error codes to user friendly error messages.
  295. Uploader.errorMap = {
  296. 'FAILED': pluploadL10n.upload_failed,
  297. 'FILE_EXTENSION_ERROR': pluploadL10n.invalid_filetype,
  298. 'IMAGE_FORMAT_ERROR': pluploadL10n.not_an_image,
  299. 'IMAGE_MEMORY_ERROR': pluploadL10n.image_memory_exceeded,
  300. 'IMAGE_DIMENSIONS_ERROR': pluploadL10n.image_dimensions_exceeded,
  301. 'GENERIC_ERROR': pluploadL10n.upload_failed,
  302. 'IO_ERROR': pluploadL10n.io_error,
  303. 'HTTP_ERROR': pluploadL10n.http_error,
  304. 'SECURITY_ERROR': pluploadL10n.security_error,
  305. 'FILE_SIZE_ERROR': function( file ) {
  306. return pluploadL10n.file_exceeds_size_limit.replace('%s', file.name);
  307. }
  308. };
  309. $.extend( Uploader.prototype, {
  310. /**
  311. * Acts as a shortcut to extending the uploader's multipart_params object.
  312. *
  313. * param( key )
  314. * Returns the value of the key.
  315. *
  316. * param( key, value )
  317. * Sets the value of a key.
  318. *
  319. * param( map )
  320. * Sets values for a map of data.
  321. */
  322. param: function( key, value ) {
  323. if ( arguments.length === 1 && typeof key === 'string' ) {
  324. return this.uploader.settings.multipart_params[ key ];
  325. }
  326. if ( arguments.length > 1 ) {
  327. this.uploader.settings.multipart_params[ key ] = value;
  328. } else {
  329. $.extend( this.uploader.settings.multipart_params, key );
  330. }
  331. },
  332. /**
  333. * Make a few internal event callbacks available on the wp.Uploader object
  334. * to change the Uploader internals if absolutely necessary.
  335. */
  336. init: function() {},
  337. error: function() {},
  338. success: function() {},
  339. added: function() {},
  340. progress: function() {},
  341. complete: function() {},
  342. refresh: function() {
  343. var node, attached, container, id;
  344. if ( this.browser ) {
  345. node = this.browser[0];
  346. // Check if the browser node is in the DOM.
  347. while ( node ) {
  348. if ( node === document.body ) {
  349. attached = true;
  350. break;
  351. }
  352. node = node.parentNode;
  353. }
  354. // If the browser node is not attached to the DOM, use a
  355. // temporary container to house it, as the browser button
  356. // shims require the button to exist in the DOM at all times.
  357. if ( ! attached ) {
  358. id = 'wp-uploader-browser-' + this.uploader.id;
  359. container = $( '#' + id );
  360. if ( ! container.length ) {
  361. container = $('<div class="wp-uploader-browser" />').css({
  362. position: 'fixed',
  363. top: '-1000px',
  364. left: '-1000px',
  365. height: 0,
  366. width: 0
  367. }).attr( 'id', 'wp-uploader-browser-' + this.uploader.id ).appendTo('body');
  368. }
  369. container.append( this.browser );
  370. }
  371. }
  372. this.uploader.refresh();
  373. }
  374. });
  375. // Create a collection of attachments in the upload queue,
  376. // so that other modules can track and display upload progress.
  377. Uploader.queue = new wp.media.model.Attachments( [], { query: false });
  378. // Create a collection to collect errors incurred while attempting upload.
  379. Uploader.errors = new Backbone.Collection();
  380. exports.Uploader = Uploader;
  381. })( wp, jQuery );