media.js (15835B)
1 ( function ( $, wp, _, rwmb, i18n ) { 2 'use strict'; 3 4 var views = rwmb.views = rwmb.views || {}, 5 models = rwmb.models = rwmb.models || {}, 6 media = wp.media, 7 MediaFrame = media.view.MediaFrame, 8 MediaCollection, Controller, MediaField, MediaList, MediaItem, MediaButton, MediaStatus, EditMedia, 9 MediaDetails, MediaLibrary, MediaSelect; 10 11 MediaCollection = Backbone.Collection.extend( { 12 model: wp.media.model.Attachment, 13 14 initialize: function ( models, options ) { 15 this.controller = options.controller || new models.Controller; 16 this.on( 'add remove reset', function () { 17 var max = this.controller.get( 'maxFiles' ); 18 this.controller.set( 'length', this.length ); 19 this.controller.set( 'full', max > 0 && this.length >= max ); 20 } ); 21 }, 22 23 add: function ( models, options ) { 24 var max = this.controller.get( 'maxFiles' ), 25 left = max - this.length; 26 27 if ( ! models || ( max > 0 && left <= 0 ) ) { 28 return this; 29 } 30 if ( ! models.hasOwnProperty( 'length' ) ) { 31 models = [models]; 32 } else if ( models instanceof media.model.Attachments ) { 33 models = models.models; 34 } 35 36 models = _.difference( models, this.models ); 37 if ( left > 0 ) { 38 models = _.first( models, left ); 39 } 40 41 Backbone.Collection.prototype.add.call( this, models, options ); 42 }, 43 44 remove: function ( models, options ) { 45 // Don't remove models if event is not fired from MB plugin. 46 if( ! $( event.target ).closest( '.rwmb-field, [data-class="rwmb-field"]' ).length ) { 47 return; 48 } 49 models = Backbone.Collection.prototype.remove.call( this, models, options ); 50 if ( this.controller.get( 'forceDelete' ) === true ) { 51 models = ! _.isArray( models ) ? [models] : models; 52 _.each( models, function ( model ) { 53 model.destroy(); 54 } ); 55 } 56 }, 57 58 destroyAll: function () { 59 _.each( _.clone( this.models ), function ( model ) { 60 model.destroy(); 61 } ); 62 } 63 } ); 64 65 /*** 66 * Controller Model 67 * Manages data of media field and media models. Most of the media views will use this to manage the media 68 */ 69 Controller = models.Controller = Backbone.Model.extend( { 70 //Default options 71 defaults: { 72 maxFiles: 0, 73 ids: [], 74 mimeType: '', 75 forceDelete: false, 76 maxStatus: true, 77 length: 0 78 }, 79 80 //Initialize Controller model 81 initialize: function () { 82 // All numbers, no 0 ids 83 this.set( 'ids', _.without( _.map( this.get( 'ids' ), Number ), 0, - 1 ) ); 84 85 // Create items collection 86 this.set( 'items', new MediaCollection( [], {controller: this} ) ); 87 88 // Listen for destroy event on controller, delete all models when triggered 89 this.on( 'destroy', function () { 90 if ( this.get( 'forceDelete' ) ) { 91 this.get( 'items' ).destroyAll(); 92 } 93 } ); 94 } 95 } ); 96 97 /*** 98 * MediaField 99 * Sets up media field view and subviews 100 */ 101 MediaField = views.MediaField = Backbone.View.extend( { 102 className: 'rwmb-media-view', 103 initialize: function ( options ) { 104 var that = this, 105 fieldName = options.input.name; 106 this.$input = $( options.input ); 107 108 if ( 1 != this.$input.attr( 'data-single-image' ) ) { 109 fieldName += '[]'; 110 } 111 112 this.controller = new Controller( _.extend( 113 { 114 fieldName: fieldName, 115 ids: this.$input.val().split( ',' ) 116 }, 117 this.$input.data( 'options' ) 118 ) ); 119 120 // Create views 121 this.createList(); 122 this.createAddButton(); 123 this.createStatus(); 124 125 this.render(); 126 this.loadInitialAttachments(); 127 128 // Listen for destroy event on input 129 this.$input.on( 'remove', function () { 130 that.controller.destroy(); 131 } ); 132 133 var collection = this.controller.get( 'items' ); 134 this.$input.on( 'media:reset', function() { 135 collection.reset(); 136 } ); 137 138 collection.on( 'add remove reset', _.debounce( function () { 139 var ids = collection.pluck( 'id' ).join( ',' ); 140 that.$input.val( ids ).trigger( 'change', [that.$( '.rwmb-media-input' )] ); 141 }, 500 ) ); 142 }, 143 144 loadInitialAttachments: function () { 145 if ( ! this.$input.val() ) { 146 return; 147 } 148 var models = this.$input.data( 'attachments' ).map( function( attachment ) { 149 return wp.media.model.Attachment.create( attachment ); 150 } ); 151 this.controller.get( 'items' ).add( models ); 152 }, 153 154 // Creates media list 155 createList: function () { 156 this.list = new MediaList( {controller: this.controller} ); 157 }, 158 159 // Creates button that adds media 160 createAddButton: function () { 161 this.addButton = new MediaButton( {controller: this.controller} ); 162 }, 163 164 // Creates status 165 createStatus: function () { 166 this.status = new MediaStatus( {controller: this.controller} ); 167 }, 168 169 // Render field and adds sub fields 170 render: function () { 171 // Empty then add parts 172 this.$el.empty().append( 173 this.list.el, 174 this.status.el, 175 this.addButton.el 176 ); 177 } 178 } ); 179 180 /*** 181 * Media List 182 * lists media 183 */ 184 MediaList = views.MediaList = Backbone.View.extend( { 185 tagName: 'ul', 186 className: 'rwmb-media-list', 187 188 initialize: function ( options ) { 189 this.controller = options.controller; 190 this.collection = this.controller.get( 'items' ); 191 this.itemView = options.itemView || MediaItem; 192 this.getItemView = _.memoize( function ( item ) { 193 var itemView = new this.itemView( { 194 model: item, 195 controller: this.controller 196 } ); 197 198 this.listenToItemView( itemView ); 199 200 return itemView; 201 }, 202 function ( item ) { 203 return item.cid; 204 } 205 ); 206 207 this.listenTo( this.collection, 'add', this.addItemView ); 208 this.listenTo( this.collection, 'remove', this.removeItemView ); 209 this.listenTo( this.collection, 'reset', this.resetItemViews ); 210 211 // Sort items using helper 'clone' to prevent trigger click on the image, which means reselect. 212 this.$el.sortable( { 213 helper : 'clone', 214 start: function ( event, ui ) { 215 ui.placeholder.height( ui.helper.outerHeight() ); 216 ui.placeholder.width( ui.helper.outerWidth() ); 217 }, 218 update: function( event, ui ) { 219 ui.item.find( rwmb.inputSelectors ).first().trigger( 'mb_change' ); 220 } 221 } ); 222 }, 223 224 listenToItemView: function ( itemView ) { 225 this.listenTo( itemView, 'click:remove', this.removeItem ); 226 this.listenTo( itemView, 'click:switch', this.switchItem ); 227 this.listenTo( itemView, 'click:edit', this.editItem ); 228 }, 229 230 addItemView: function ( item ) { 231 var index = this.collection.indexOf( item ), 232 itemEl = this.getItemView( item ).el, 233 $children = this.$el.children(); 234 235 if ( 0 >= index ) { 236 this.$el.prepend( itemEl ); 237 } else if ( $children.length <= index ) { 238 this.$el.append( itemEl ) 239 } else { 240 $children.eq( index - 1 ).after( itemEl ); 241 } 242 }, 243 244 // Remove item view 245 removeItemView: function ( item ) { 246 this.getItemView( item ).$el.detach(); 247 }, 248 249 removeItem: function ( item ) { 250 this.collection.remove( item ); 251 }, 252 253 resetItemViews: function( items ){ 254 var that = this; 255 _.each( that.models, that.removeItemView ); 256 items.each( that.addItemView ); 257 }, 258 259 switchItem: function ( item ) { 260 if ( this._switchFrame ) { 261 this._switchFrame.dispose(); 262 } 263 this._switchFrame = new MediaSelect( { 264 multiple: false, 265 editing: true, 266 library: { 267 type: this.controller.get( 'mimeType' ) 268 }, 269 edit: this.collection 270 } ); 271 272 // Refresh content when frame opens 273 this._switchFrame.on( 'open', function() { 274 var frameContent = this._switchFrame.content.get(); 275 if ( frameContent && frameContent.collection ) { 276 frameContent.collection.mirroring._hasMore = true; 277 frameContent.collection.more(); 278 } 279 }, this ); 280 281 this._switchFrame.on( 'select', function () { 282 var selection = this._switchFrame.state().get( 'selection' ), 283 collection = this.collection, 284 index = collection.indexOf( item ); 285 286 if ( ! _.isEmpty( selection ) ) { 287 collection.remove( item ); 288 collection.add( selection, {at: index} ); 289 } 290 }, this ); 291 292 this._switchFrame.open(); 293 return false; 294 }, 295 296 editItem: function ( item ) { 297 if ( this._editFrame ) { 298 this._editFrame.dispose(); 299 } 300 301 // Trigger the media frame to open the correct item. 302 this._editFrame = new EditMedia( { 303 frame: 'edit-attachments', 304 controller: { 305 gridRouter: new wp.media.view.MediaFrame.Manage.Router() 306 }, 307 library: this.collection, 308 model: item 309 } ); 310 311 this._editFrame.open(); 312 } 313 } ); 314 315 /*** 316 * MediaStatus view. 317 * Show number of selected/uploaded files and number of files remain if "maxStatus" parameter is true. 318 */ 319 MediaStatus = views.MediaStatus = Backbone.View.extend( { 320 tagName: 'div', 321 className: 'rwmb-media-status', 322 template: wp.template( 'rwmb-media-status' ), 323 324 initialize: function ( options ) { 325 this.controller = options.controller; 326 327 // Auto hide if maxStatus is false 328 if ( ! this.controller.get( 'maxStatus' ) ) { 329 this.$el.hide(); 330 return; 331 } 332 333 // Re-render if changes happen in controller 334 this.listenTo( this.controller.get( 'items' ), 'update', this.render ); 335 this.listenTo( this.controller.get( 'items' ), 'reset', this.render ); 336 337 // Render 338 this.render(); 339 }, 340 341 render: function () { 342 this.$el.html( this.template( this.controller.toJSON() ) ); 343 } 344 } ); 345 346 /*** 347 * Media Button 348 * Selects and adds media to controller 349 */ 350 MediaButton = views.MediaButton = Backbone.View.extend( { 351 tagName: 'div', 352 className: 'rwmb-media-add', 353 template: wp.template( 'rwmb-media-button' ), 354 events: { 355 'click .button': function () { 356 if ( this._frame ) { 357 this._frame.dispose(); 358 } 359 var maxFiles = this.controller.get( 'maxFiles' ); 360 this._frame = new MediaSelect( { 361 multiple: maxFiles > 1 || maxFiles <= 0 ? 'add' : false, 362 editing: true, 363 library: { 364 type: this.controller.get( 'mimeType' ) 365 }, 366 edit: this.collection 367 } ); 368 369 // Refresh content when frame opens 370 this._frame.on( 'open', function() { 371 var frameContent = this._frame.content.get(); 372 if ( frameContent && frameContent.collection ) { 373 frameContent.collection.mirroring._hasMore = true; 374 frameContent.collection.more(); 375 } 376 }, this ); 377 378 this._frame.on( 'select', function () { 379 var selection = this._frame.state().get( 'selection' ); 380 if ( this.controller.get( 'addTo' ) === 'beginning' ) { 381 this.collection.add( selection.models, {at: 0} ); 382 } else { 383 this.collection.add( selection.models ); 384 } 385 }, this ); 386 387 this._frame.open(); 388 } 389 }, 390 render: function () { 391 this.$el.html( this.template( {text: i18n.add} ) ); 392 return this; 393 }, 394 395 initialize: function ( options ) { 396 this.controller = options.controller; 397 this.collection = this.controller.get( 'items' ); 398 399 // Auto hide if you reach the max number of media 400 this.listenTo( this.controller, 'change:full', function () { 401 this.$el.toggle( ! this.controller.get( 'full' ) ); 402 } ); 403 404 this.render(); 405 } 406 } ); 407 408 /*** 409 * MediaItem 410 * View for individual media items 411 */ 412 MediaItem = views.MediaItem = Backbone.View.extend( { 413 tagName: 'li', 414 className: 'rwmb-file', 415 template: wp.template( 'rwmb-media-item' ), 416 initialize: function ( options ) { 417 this.controller = options.controller; 418 this.collection = this.controller.get( 'items' ); 419 this.render(); 420 this.listenTo( this.model, 'change', this.render ); 421 422 this.$el.data( 'id', this.model.cid ); 423 }, 424 425 events: { 426 'click .rwmb-image-overlay': function ( e ) { 427 e.preventDefault(); 428 this.trigger( 'click:switch', this.model ); 429 }, 430 'click .rwmb-remove-media': function ( e ) { 431 e.preventDefault(); 432 this.trigger( 'click:remove', this.model ); 433 }, 434 'click .rwmb-edit-media': function ( e ) { 435 e.preventDefault(); 436 this.trigger( 'click:edit', this.model ); 437 } 438 }, 439 440 render: function () { 441 var data = this.model.toJSON(); 442 data.controller = this.controller.toJSON(); 443 this.$el.html( this.template( data ) ); 444 return this; 445 } 446 } ); 447 448 /** 449 * Extend media frames to make things work right 450 */ 451 452 /** 453 * MediaDetails 454 * Custom version of TwoColumn view to prevent all video and audio from being unset 455 */ 456 MediaDetails = views.MediaDetails = media.view.Attachment.Details.TwoColumn.extend( { 457 render: function () { 458 var that = this; 459 media.view.Attachment.Details.prototype.render.apply( this, arguments ); 460 this.players = this.players || []; 461 462 media.mixin.unsetPlayers.call( this ); 463 464 this.$( 'audio, video' ).each( function ( i, elem ) { 465 var el = media.view.MediaDetails.prepareSrc( elem ); 466 that.players.push( new window.MediaElementPlayer( el, media.mixin.mejsSettings ) ); 467 } ); 468 } 469 } ); 470 471 /** 472 * MediaLibrary 473 * Custom version of Library to exclude already selected media in a media frame 474 */ 475 MediaLibrary = media.controller.Library.extend( { 476 defaults: _.defaults( { 477 multiple: 'add', 478 filterable: 'all', 479 priority: 100, 480 syncSelection: false 481 }, media.controller.Library.prototype.defaults ), 482 483 activate: function () { 484 var library = this.get( 'library' ), 485 edit = this.frame.options.edit; 486 487 if ( this.editLibrary && this.editLibrary !== edit ) { 488 library.unobserve( this.editLibrary ); 489 } 490 491 // Accepts attachments that exist in the original library and 492 // that do not exist in gallery's library. 493 library.validator = function ( attachment ) { 494 return ! ! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments ); 495 }; 496 497 // Reset the library to ensure that all attachments are re-added 498 // to the collection. Do so silently, as calling `observe` will 499 // trigger the `reset` event. 500 library.reset( library.mirroring.models, {silent: true} ); 501 library.observe( edit ); 502 this.editLibrary = edit; 503 504 media.controller.Library.prototype.activate.apply( this, arguments ); 505 } 506 } ); 507 508 /** 509 * MediaSelect 510 * Custom version of Select media frame that uses MediaLibrary 511 */ 512 MediaSelect = views.MediaSelect = MediaFrame.Select.extend( { 513 /** 514 * Create the default states on the frame. 515 */ 516 createStates: function () { 517 var options = this.options; 518 519 // Add reference so we know MediaFrame belongs to MB plugin. 520 this.$el.attr( 'data-class', 'rwmb-field' ); 521 522 if ( this.options.states ) { 523 return; 524 } 525 526 // Add the default states. 527 this.states.add( [ 528 // Main states. 529 new MediaLibrary( { 530 library: media.query( options.library ), 531 multiple: options.multiple, 532 priority: 20 533 } ) 534 ] ); 535 } 536 } ); 537 538 /*** 539 * EditMedia 540 * Custom version of EditAttachments frame to prevent all video and audio from being unset 541 */ 542 EditMedia = views.EditMedia = MediaFrame.EditAttachments.extend( { 543 /** 544 * Content region rendering callback for the `edit-metadata` mode. 545 * 546 * @param {Object} contentRegion Basic object with a `view` property, which 547 * should be set with the proper region view. 548 */ 549 editMetadataMode: function ( contentRegion ) { 550 contentRegion.view = new MediaDetails( { 551 controller: this, 552 model: this.model 553 } ); 554 555 /** 556 * Attach a subview to display fields added via the 557 * `attachment_fields_to_edit` filter. 558 */ 559 contentRegion.view.views.set( '.attachment-compat', new media.view.AttachmentCompat( { 560 controller: this, 561 model: this.model 562 } ) ); 563 }, 564 resetRoute: function() {} 565 } ); 566 567 function initMediaField() { 568 var $this = $( this ), 569 view = $this.data( 'view' ); 570 571 if ( view ) { 572 return; 573 } 574 575 view = new MediaField( { input: this } ); 576 577 $this.siblings( '.rwmb-media-view' ).remove(); 578 $this.after( view.el ); 579 $this.data( 'view', view ); 580 } 581 582 function init( e ) { 583 $( e.target ).find( '.rwmb-file_advanced' ).each( initMediaField ); 584 } 585 586 rwmb.$document 587 .on( 'mb_ready', init ) 588 .on( 'clone', '.rwmb-file_advanced', initMediaField ); 589 } )( jQuery, wp, _, rwmb, i18nRwmbMedia );