Changeset 31373 for trunk/src/wp-includes/js/media/views/selection.js
- Timestamp:
- 02/09/2015 12:42:28 AM (9 years ago)
- Location:
- trunk/src/wp-includes/js/media
- Files:
-
- 2 added
- 1 copied
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/js/media/views/selection.js
r31369 r31373 1 /* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */ 2 ( function( $, _ ) { 3 var l10n, 4 media = wp.media, 5 isTouchDevice = ( 'ontouchend' in document ); 1 /*globals _, Backbone, wp */ 6 2 7 // Link any localized strings. 8 l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n; 3 /** 4 * wp.media.view.Selection 5 * 6 * @class 7 * @augments wp.media.View 8 * @augments wp.Backbone.View 9 * @augments Backbone.View 10 */ 11 var View = require( './view.js' ), 12 AttachmentsSelection = require( './attachments/selection.js' ), 13 l10n = wp.media.view.l10n, 14 Selection; 9 15 10 // Link any settings. 11 media.view.settings = l10n.settings || {}; 12 delete l10n.settings; 16 Selection = View.extend({ 17 tagName: 'div', 18 className: 'media-selection', 19 template: wp.template('media-selection'), 13 20 14 // Copy the `post` setting over to the model settings. 15 media.model.settings.post = media.view.settings.post; 21 events: { 22 'click .edit-selection': 'edit', 23 'click .clear-selection': 'clear' 24 }, 16 25 17 // Check if the browser supports CSS 3.0 transitions 18 $.support.transition = (function(){ 19 var style = document.documentElement.style, 20 transitions = { 21 WebkitTransition: 'webkitTransitionEnd', 22 MozTransition: 'transitionend', 23 OTransition: 'oTransitionEnd otransitionend', 24 transition: 'transitionend' 25 }, transition; 26 27 transition = _.find( _.keys( transitions ), function( transition ) { 28 return ! _.isUndefined( style[ transition ] ); 26 initialize: function() { 27 _.defaults( this.options, { 28 editable: false, 29 clearable: true 29 30 }); 30 31 31 return transition && { 32 end: transitions[ transition ] 33 }; 34 }()); 32 /** 33 * @member {wp.media.view.Attachments.Selection} 34 */ 35 this.attachments = new AttachmentsSelection({ 36 controller: this.controller, 37 collection: this.collection, 38 selection: this.collection, 39 model: new Backbone.Model() 40 }); 35 41 36 /** 37 * A shared event bus used to provide events into 38 * the media workflows that 3rd-party devs can use to hook 39 * in. 40 */ 41 media.events = _.extend( {}, Backbone.Events ); 42 this.views.set( '.selection-view', this.attachments ); 43 this.collection.on( 'add remove reset', this.refresh, this ); 44 this.controller.on( 'content:activate', this.refresh, this ); 45 }, 42 46 43 /** 44 * Makes it easier to bind events using transitions. 45 * 46 * @param {string} selector 47 * @param {Number} sensitivity 48 * @returns {Promise} 49 */ 50 media.transition = function( selector, sensitivity ) { 51 var deferred = $.Deferred(); 47 ready: function() { 48 this.refresh(); 49 }, 52 50 53 sensitivity = sensitivity || 2000; 54 55 if ( $.support.transition ) { 56 if ( ! (selector instanceof $) ) { 57 selector = $( selector ); 58 } 59 60 // Resolve the deferred when the first element finishes animating. 61 selector.first().one( $.support.transition.end, deferred.resolve ); 62 63 // Just in case the event doesn't trigger, fire a callback. 64 _.delay( deferred.resolve, sensitivity ); 65 66 // Otherwise, execute on the spot. 67 } else { 68 deferred.resolve(); 51 refresh: function() { 52 // If the selection hasn't been rendered, bail. 53 if ( ! this.$el.children().length ) { 54 return; 69 55 } 70 56 71 return deferred.promise();72 };57 var collection = this.collection, 58 editing = 'edit-selection' === this.controller.content.mode(); 73 59 74 /** 75 * wp.media.controller.Region 76 * 77 * A region is a persistent application layout area. 78 * 79 * A region assumes one mode at any time, and can be switched to another. 80 * 81 * When mode changes, events are triggered on the region's parent view. 82 * The parent view will listen to specific events and fill the region with an 83 * appropriate view depending on mode. For example, a frame listens for the 84 * 'browse' mode t be activated on the 'content' view and then fills the region 85 * with an AttachmentsBrowser view. 86 * 87 * @class 88 * 89 * @param {object} options Options hash for the region. 90 * @param {string} options.id Unique identifier for the region. 91 * @param {Backbone.View} options.view A parent view the region exists within. 92 * @param {string} options.selector jQuery selector for the region within the parent view. 93 */ 94 media.controller.Region = function( options ) { 95 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) ); 96 }; 60 // If nothing is selected, display nothing. 61 this.$el.toggleClass( 'empty', ! collection.length ); 62 this.$el.toggleClass( 'one', 1 === collection.length ); 63 this.$el.toggleClass( 'editing', editing ); 97 64 98 // Use Backbone's self-propagating `extend` inheritance method.99 media.controller.Region.extend = Backbone.Model.extend;65 this.$('.count').text( l10n.selected.replace('%d', collection.length) ); 66 }, 100 67 101 _.extend( media.controller.Region.prototype, { 102 /** 103 * Activate a mode. 104 * 105 * @since 3.5.0 106 * 107 * @param {string} mode 108 * 109 * @fires this.view#{this.id}:activate:{this._mode} 110 * @fires this.view#{this.id}:activate 111 * @fires this.view#{this.id}:deactivate:{this._mode} 112 * @fires this.view#{this.id}:deactivate 113 * 114 * @returns {wp.media.controller.Region} Returns itself to allow chaining. 115 */ 116 mode: function( mode ) { 117 if ( ! mode ) { 118 return this._mode; 119 } 120 // Bail if we're trying to change to the current mode. 121 if ( mode === this._mode ) { 122 return this; 123 } 68 edit: function( event ) { 69 event.preventDefault(); 70 if ( this.options.editable ) { 71 this.options.editable.call( this, this.collection ); 72 } 73 }, 124 74 125 /** 126 * Region mode deactivation event. 127 * 128 * @event this.view#{this.id}:deactivate:{this._mode} 129 * @event this.view#{this.id}:deactivate 130 */ 131 this.trigger('deactivate'); 75 clear: function( event ) { 76 event.preventDefault(); 77 this.collection.reset(); 132 78 133 this._mode = mode; 134 this.render( mode ); 79 // Keep focus inside media modal 80 // after clear link is selected 81 this.controller.modal.focusManager.focus(); 82 } 83 }); 135 84 136 /** 137 * Region mode activation event. 138 * 139 * @event this.view#{this.id}:activate:{this._mode} 140 * @event this.view#{this.id}:activate 141 */ 142 this.trigger('activate'); 143 return this; 144 }, 145 /** 146 * Render a mode. 147 * 148 * @since 3.5.0 149 * 150 * @param {string} mode 151 * 152 * @fires this.view#{this.id}:create:{this._mode} 153 * @fires this.view#{this.id}:create 154 * @fires this.view#{this.id}:render:{this._mode} 155 * @fires this.view#{this.id}:render 156 * 157 * @returns {wp.media.controller.Region} Returns itself to allow chaining 158 */ 159 render: function( mode ) { 160 // If the mode isn't active, activate it. 161 if ( mode && mode !== this._mode ) { 162 return this.mode( mode ); 163 } 164 165 var set = { view: null }, 166 view; 167 168 /** 169 * Create region view event. 170 * 171 * Region view creation takes place in an event callback on the frame. 172 * 173 * @event this.view#{this.id}:create:{this._mode} 174 * @event this.view#{this.id}:create 175 */ 176 this.trigger( 'create', set ); 177 view = set.view; 178 179 /** 180 * Render region view event. 181 * 182 * Region view creation takes place in an event callback on the frame. 183 * 184 * @event this.view#{this.id}:create:{this._mode} 185 * @event this.view#{this.id}:create 186 */ 187 this.trigger( 'render', view ); 188 if ( view ) { 189 this.set( view ); 190 } 191 return this; 192 }, 193 194 /** 195 * Get the region's view. 196 * 197 * @since 3.5.0 198 * 199 * @returns {wp.media.View} 200 */ 201 get: function() { 202 return this.view.views.first( this.selector ); 203 }, 204 205 /** 206 * Set the region's view as a subview of the frame. 207 * 208 * @since 3.5.0 209 * 210 * @param {Array|Object} views 211 * @param {Object} [options={}] 212 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining 213 */ 214 set: function( views, options ) { 215 if ( options ) { 216 options.add = false; 217 } 218 return this.view.views.set( this.selector, views, options ); 219 }, 220 221 /** 222 * Trigger regional view events on the frame. 223 * 224 * @since 3.5.0 225 * 226 * @param {string} event 227 * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining. 228 */ 229 trigger: function( event ) { 230 var base, args; 231 232 if ( ! this._mode ) { 233 return; 234 } 235 236 args = _.toArray( arguments ); 237 base = this.id + ':' + event; 238 239 // Trigger `{this.id}:{event}:{this._mode}` event on the frame. 240 args[0] = base + ':' + this._mode; 241 this.view.trigger.apply( this.view, args ); 242 243 // Trigger `{this.id}:{event}` event on the frame. 244 args[0] = base; 245 this.view.trigger.apply( this.view, args ); 246 return this; 247 } 248 }); 249 250 /** 251 * wp.media.controller.StateMachine 252 * 253 * A state machine keeps track of state. It is in one state at a time, 254 * and can change from one state to another. 255 * 256 * States are stored as models in a Backbone collection. 257 * 258 * @since 3.5.0 259 * 260 * @class 261 * @augments Backbone.Model 262 * @mixin 263 * @mixes Backbone.Events 264 * 265 * @param {Array} states 266 */ 267 media.controller.StateMachine = function( states ) { 268 // @todo This is dead code. The states collection gets created in media.view.Frame._createStates. 269 this.states = new Backbone.Collection( states ); 270 }; 271 272 // Use Backbone's self-propagating `extend` inheritance method. 273 media.controller.StateMachine.extend = Backbone.Model.extend; 274 275 _.extend( media.controller.StateMachine.prototype, Backbone.Events, { 276 /** 277 * Fetch a state. 278 * 279 * If no `id` is provided, returns the active state. 280 * 281 * Implicitly creates states. 282 * 283 * Ensure that the `states` collection exists so the `StateMachine` 284 * can be used as a mixin. 285 * 286 * @since 3.5.0 287 * 288 * @param {string} id 289 * @returns {wp.media.controller.State} Returns a State model 290 * from the StateMachine collection 291 */ 292 state: function( id ) { 293 this.states = this.states || new Backbone.Collection(); 294 295 // Default to the active state. 296 id = id || this._state; 297 298 if ( id && ! this.states.get( id ) ) { 299 this.states.add({ id: id }); 300 } 301 return this.states.get( id ); 302 }, 303 304 /** 305 * Sets the active state. 306 * 307 * Bail if we're trying to select the current state, if we haven't 308 * created the `states` collection, or are trying to select a state 309 * that does not exist. 310 * 311 * @since 3.5.0 312 * 313 * @param {string} id 314 * 315 * @fires wp.media.controller.State#deactivate 316 * @fires wp.media.controller.State#activate 317 * 318 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining 319 */ 320 setState: function( id ) { 321 var previous = this.state(); 322 323 if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) { 324 return this; 325 } 326 327 if ( previous ) { 328 previous.trigger('deactivate'); 329 this._lastState = previous.id; 330 } 331 332 this._state = id; 333 this.state().trigger('activate'); 334 335 return this; 336 }, 337 338 /** 339 * Returns the previous active state. 340 * 341 * Call the `state()` method with no parameters to retrieve the current 342 * active state. 343 * 344 * @since 3.5.0 345 * 346 * @returns {wp.media.controller.State} Returns a State model 347 * from the StateMachine collection 348 */ 349 lastState: function() { 350 if ( this._lastState ) { 351 return this.state( this._lastState ); 352 } 353 } 354 }); 355 356 // Map all event binding and triggering on a StateMachine to its `states` collection. 357 _.each([ 'on', 'off', 'trigger' ], function( method ) { 358 /** 359 * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining. 360 */ 361 media.controller.StateMachine.prototype[ method ] = function() { 362 // Ensure that the `states` collection exists so the `StateMachine` 363 // can be used as a mixin. 364 this.states = this.states || new Backbone.Collection(); 365 // Forward the method to the `states` collection. 366 this.states[ method ].apply( this.states, arguments ); 367 return this; 368 }; 369 }); 370 371 /** 372 * wp.media.controller.State 373 * 374 * A state is a step in a workflow that when set will trigger the controllers 375 * for the regions to be updated as specified in the frame. 376 * 377 * A state has an event-driven lifecycle: 378 * 379 * 'ready' triggers when a state is added to a state machine's collection. 380 * 'activate' triggers when a state is activated by a state machine. 381 * 'deactivate' triggers when a state is deactivated by a state machine. 382 * 'reset' is not triggered automatically. It should be invoked by the 383 * proper controller to reset the state to its default. 384 * 385 * @class 386 * @augments Backbone.Model 387 */ 388 media.controller.State = Backbone.Model.extend({ 389 /** 390 * Constructor. 391 * 392 * @since 3.5.0 393 */ 394 constructor: function() { 395 this.on( 'activate', this._preActivate, this ); 396 this.on( 'activate', this.activate, this ); 397 this.on( 'activate', this._postActivate, this ); 398 this.on( 'deactivate', this._deactivate, this ); 399 this.on( 'deactivate', this.deactivate, this ); 400 this.on( 'reset', this.reset, this ); 401 this.on( 'ready', this._ready, this ); 402 this.on( 'ready', this.ready, this ); 403 /** 404 * Call parent constructor with passed arguments 405 */ 406 Backbone.Model.apply( this, arguments ); 407 this.on( 'change:menu', this._updateMenu, this ); 408 }, 409 /** 410 * Ready event callback. 411 * 412 * @abstract 413 * @since 3.5.0 414 */ 415 ready: function() {}, 416 417 /** 418 * Activate event callback. 419 * 420 * @abstract 421 * @since 3.5.0 422 */ 423 activate: function() {}, 424 425 /** 426 * Deactivate event callback. 427 * 428 * @abstract 429 * @since 3.5.0 430 */ 431 deactivate: function() {}, 432 433 /** 434 * Reset event callback. 435 * 436 * @abstract 437 * @since 3.5.0 438 */ 439 reset: function() {}, 440 441 /** 442 * @access private 443 * @since 3.5.0 444 */ 445 _ready: function() { 446 this._updateMenu(); 447 }, 448 449 /** 450 * @access private 451 * @since 3.5.0 452 */ 453 _preActivate: function() { 454 this.active = true; 455 }, 456 457 /** 458 * @access private 459 * @since 3.5.0 460 */ 461 _postActivate: function() { 462 this.on( 'change:menu', this._menu, this ); 463 this.on( 'change:titleMode', this._title, this ); 464 this.on( 'change:content', this._content, this ); 465 this.on( 'change:toolbar', this._toolbar, this ); 466 467 this.frame.on( 'title:render:default', this._renderTitle, this ); 468 469 this._title(); 470 this._menu(); 471 this._toolbar(); 472 this._content(); 473 this._router(); 474 }, 475 476 /** 477 * @access private 478 * @since 3.5.0 479 */ 480 _deactivate: function() { 481 this.active = false; 482 483 this.frame.off( 'title:render:default', this._renderTitle, this ); 484 485 this.off( 'change:menu', this._menu, this ); 486 this.off( 'change:titleMode', this._title, this ); 487 this.off( 'change:content', this._content, this ); 488 this.off( 'change:toolbar', this._toolbar, this ); 489 }, 490 491 /** 492 * @access private 493 * @since 3.5.0 494 */ 495 _title: function() { 496 this.frame.title.render( this.get('titleMode') || 'default' ); 497 }, 498 499 /** 500 * @access private 501 * @since 3.5.0 502 */ 503 _renderTitle: function( view ) { 504 view.$el.text( this.get('title') || '' ); 505 }, 506 507 /** 508 * @access private 509 * @since 3.5.0 510 */ 511 _router: function() { 512 var router = this.frame.router, 513 mode = this.get('router'), 514 view; 515 516 this.frame.$el.toggleClass( 'hide-router', ! mode ); 517 if ( ! mode ) { 518 return; 519 } 520 521 this.frame.router.render( mode ); 522 523 view = router.get(); 524 if ( view && view.select ) { 525 view.select( this.frame.content.mode() ); 526 } 527 }, 528 529 /** 530 * @access private 531 * @since 3.5.0 532 */ 533 _menu: function() { 534 var menu = this.frame.menu, 535 mode = this.get('menu'), 536 view; 537 538 this.frame.$el.toggleClass( 'hide-menu', ! mode ); 539 if ( ! mode ) { 540 return; 541 } 542 543 menu.mode( mode ); 544 545 view = menu.get(); 546 if ( view && view.select ) { 547 view.select( this.id ); 548 } 549 }, 550 551 /** 552 * @access private 553 * @since 3.5.0 554 */ 555 _updateMenu: function() { 556 var previous = this.previous('menu'), 557 menu = this.get('menu'); 558 559 if ( previous ) { 560 this.frame.off( 'menu:render:' + previous, this._renderMenu, this ); 561 } 562 563 if ( menu ) { 564 this.frame.on( 'menu:render:' + menu, this._renderMenu, this ); 565 } 566 }, 567 568 /** 569 * Create a view in the media menu for the state. 570 * 571 * @access private 572 * @since 3.5.0 573 * 574 * @param {media.view.Menu} view The menu view. 575 */ 576 _renderMenu: function( view ) { 577 var menuItem = this.get('menuItem'), 578 title = this.get('title'), 579 priority = this.get('priority'); 580 581 if ( ! menuItem && title ) { 582 menuItem = { text: title }; 583 584 if ( priority ) { 585 menuItem.priority = priority; 586 } 587 } 588 589 if ( ! menuItem ) { 590 return; 591 } 592 593 view.set( this.id, menuItem ); 594 } 595 }); 596 597 _.each(['toolbar','content'], function( region ) { 598 /** 599 * @access private 600 */ 601 media.controller.State.prototype[ '_' + region ] = function() { 602 var mode = this.get( region ); 603 if ( mode ) { 604 this.frame[ region ].render( mode ); 605 } 606 }; 607 }); 608 609 /** 610 * wp.media.selectionSync 611 * 612 * Sync an attachments selection in a state with another state. 613 * 614 * Allows for selecting multiple images in the Insert Media workflow, and then 615 * switching to the Insert Gallery workflow while preserving the attachments selection. 616 * 617 * @mixin 618 */ 619 media.selectionSync = { 620 /** 621 * @since 3.5.0 622 */ 623 syncSelection: function() { 624 var selection = this.get('selection'), 625 manager = this.frame._selection; 626 627 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 628 return; 629 } 630 631 // If the selection supports multiple items, validate the stored 632 // attachments based on the new selection's conditions. Record 633 // the attachments that are not included; we'll maintain a 634 // reference to those. Other attachments are considered in flux. 635 if ( selection.multiple ) { 636 selection.reset( [], { silent: true }); 637 selection.validateAll( manager.attachments ); 638 manager.difference = _.difference( manager.attachments.models, selection.models ); 639 } 640 641 // Sync the selection's single item with the master. 642 selection.single( manager.single ); 643 }, 644 645 /** 646 * Record the currently active attachments, which is a combination 647 * of the selection's attachments and the set of selected 648 * attachments that this specific selection considered invalid. 649 * Reset the difference and record the single attachment. 650 * 651 * @since 3.5.0 652 */ 653 recordSelection: function() { 654 var selection = this.get('selection'), 655 manager = this.frame._selection; 656 657 if ( ! this.get('syncSelection') || ! manager || ! selection ) { 658 return; 659 } 660 661 if ( selection.multiple ) { 662 manager.attachments.reset( selection.toArray().concat( manager.difference ) ); 663 manager.difference = []; 664 } else { 665 manager.attachments.add( selection.toArray() ); 666 } 667 668 manager.single = selection._single; 669 } 670 }; 671 672 /** 673 * wp.media.controller.Library 674 * 675 * A state for choosing an attachment or group of attachments from the media library. 676 * 677 * @class 678 * @augments wp.media.controller.State 679 * @augments Backbone.Model 680 * @mixes media.selectionSync 681 * 682 * @param {object} [attributes] The attributes hash passed to the state. 683 * @param {string} [attributes.id=library] Unique identifier. 684 * @param {string} [attributes.title=Media library] Title for the state. Displays in the media menu and the frame's title region. 685 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 686 * If one is not supplied, a collection of all attachments will be created. 687 * @param {wp.media.model.Selection|object} [attributes.selection] A collection to contain attachment selections within the state. 688 * If the 'selection' attribute is a plain JS object, 689 * a Selection will be created using its values as the selection instance's `props` model. 690 * Otherwise, it will copy the library's `props` model. 691 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 692 * @param {string} [attributes.content=upload] Initial mode for the content region. 693 * Overridden by persistent user setting if 'contentUserSetting' is true. 694 * @param {string} [attributes.menu=default] Initial mode for the menu region. 695 * @param {string} [attributes.router=browse] Initial mode for the router region. 696 * @param {string} [attributes.toolbar=select] Initial mode for the toolbar region. 697 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 698 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 699 * Accepts 'all', 'uploaded', or 'unattached'. 700 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 701 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 702 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 703 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 704 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 705 */ 706 media.controller.Library = media.controller.State.extend({ 707 defaults: { 708 id: 'library', 709 title: l10n.mediaLibraryTitle, 710 multiple: false, 711 content: 'upload', 712 menu: 'default', 713 router: 'browse', 714 toolbar: 'select', 715 searchable: true, 716 filterable: false, 717 sortable: true, 718 autoSelect: true, 719 describe: false, 720 contentUserSetting: true, 721 syncSelection: true 722 }, 723 724 /** 725 * If a library isn't provided, query all media items. 726 * If a selection instance isn't provided, create one. 727 * 728 * @since 3.5.0 729 */ 730 initialize: function() { 731 var selection = this.get('selection'), 732 props; 733 734 if ( ! this.get('library') ) { 735 this.set( 'library', media.query() ); 736 } 737 738 if ( ! (selection instanceof media.model.Selection) ) { 739 props = selection; 740 741 if ( ! props ) { 742 props = this.get('library').props.toJSON(); 743 props = _.omit( props, 'orderby', 'query' ); 744 } 745 746 this.set( 'selection', new media.model.Selection( null, { 747 multiple: this.get('multiple'), 748 props: props 749 }) ); 750 } 751 752 this.resetDisplays(); 753 }, 754 755 /** 756 * @since 3.5.0 757 */ 758 activate: function() { 759 this.syncSelection(); 760 761 wp.Uploader.queue.on( 'add', this.uploading, this ); 762 763 this.get('selection').on( 'add remove reset', this.refreshContent, this ); 764 765 if ( this.get( 'router' ) && this.get('contentUserSetting') ) { 766 this.frame.on( 'content:activate', this.saveContentMode, this ); 767 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) ); 768 } 769 }, 770 771 /** 772 * @since 3.5.0 773 */ 774 deactivate: function() { 775 this.recordSelection(); 776 777 this.frame.off( 'content:activate', this.saveContentMode, this ); 778 779 // Unbind all event handlers that use this state as the context 780 // from the selection. 781 this.get('selection').off( null, null, this ); 782 783 wp.Uploader.queue.off( null, null, this ); 784 }, 785 786 /** 787 * Reset the library to its initial state. 788 * 789 * @since 3.5.0 790 */ 791 reset: function() { 792 this.get('selection').reset(); 793 this.resetDisplays(); 794 this.refreshContent(); 795 }, 796 797 /** 798 * Reset the attachment display settings defaults to the site options. 799 * 800 * If site options don't define them, fall back to a persistent user setting. 801 * 802 * @since 3.5.0 803 */ 804 resetDisplays: function() { 805 var defaultProps = media.view.settings.defaultProps; 806 this._displays = []; 807 this._defaultDisplaySettings = { 808 align: defaultProps.align || getUserSetting( 'align', 'none' ), 809 size: defaultProps.size || getUserSetting( 'imgsize', 'medium' ), 810 link: defaultProps.link || getUserSetting( 'urlbutton', 'file' ) 811 }; 812 }, 813 814 /** 815 * Create a model to represent display settings (alignment, etc.) for an attachment. 816 * 817 * @since 3.5.0 818 * 819 * @param {wp.media.model.Attachment} attachment 820 * @returns {Backbone.Model} 821 */ 822 display: function( attachment ) { 823 var displays = this._displays; 824 825 if ( ! displays[ attachment.cid ] ) { 826 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) ); 827 } 828 return displays[ attachment.cid ]; 829 }, 830 831 /** 832 * Given an attachment, create attachment display settings properties. 833 * 834 * @since 3.6.0 835 * 836 * @param {wp.media.model.Attachment} attachment 837 * @returns {Object} 838 */ 839 defaultDisplaySettings: function( attachment ) { 840 var settings = this._defaultDisplaySettings; 841 if ( settings.canEmbed = this.canEmbed( attachment ) ) { 842 settings.link = 'embed'; 843 } 844 return settings; 845 }, 846 847 /** 848 * Whether an attachment can be embedded (audio or video). 849 * 850 * @since 3.6.0 851 * 852 * @param {wp.media.model.Attachment} attachment 853 * @returns {Boolean} 854 */ 855 canEmbed: function( attachment ) { 856 // If uploading, we know the filename but not the mime type. 857 if ( ! attachment.get('uploading') ) { 858 var type = attachment.get('type'); 859 if ( type !== 'audio' && type !== 'video' ) { 860 return false; 861 } 862 } 863 864 return _.contains( media.view.settings.embedExts, attachment.get('filename').split('.').pop() ); 865 }, 866 867 868 /** 869 * If the state is active, no items are selected, and the current 870 * content mode is not an option in the state's router (provided 871 * the state has a router), reset the content mode to the default. 872 * 873 * @since 3.5.0 874 */ 875 refreshContent: function() { 876 var selection = this.get('selection'), 877 frame = this.frame, 878 router = frame.router.get(), 879 mode = frame.content.mode(); 880 881 if ( this.active && ! selection.length && router && ! router.get( mode ) ) { 882 this.frame.content.render( this.get('content') ); 883 } 884 }, 885 886 /** 887 * Callback handler when an attachment is uploaded. 888 * 889 * Switch to the Media Library if uploaded from the 'Upload Files' tab. 890 * 891 * Adds any uploading attachments to the selection. 892 * 893 * If the state only supports one attachment to be selected and multiple 894 * attachments are uploaded, the last attachment in the upload queue will 895 * be selected. 896 * 897 * @since 3.5.0 898 * 899 * @param {wp.media.model.Attachment} attachment 900 */ 901 uploading: function( attachment ) { 902 var content = this.frame.content; 903 904 if ( 'upload' === content.mode() ) { 905 this.frame.content.mode('browse'); 906 } 907 908 if ( this.get( 'autoSelect' ) ) { 909 this.get('selection').add( attachment ); 910 this.frame.trigger( 'library:selection:add' ); 911 } 912 }, 913 914 /** 915 * Persist the mode of the content region as a user setting. 916 * 917 * @since 3.5.0 918 */ 919 saveContentMode: function() { 920 if ( 'browse' !== this.get('router') ) { 921 return; 922 } 923 924 var mode = this.frame.content.mode(), 925 view = this.frame.router.get(); 926 927 if ( view && view.get( mode ) ) { 928 setUserSetting( 'libraryContent', mode ); 929 } 930 } 931 }); 932 933 // Make selectionSync available on any Media Library state. 934 _.extend( media.controller.Library.prototype, media.selectionSync ); 935 936 /** 937 * wp.media.controller.ImageDetails 938 * 939 * A state for editing the attachment display settings of an image that's been 940 * inserted into the editor. 941 * 942 * @class 943 * @augments wp.media.controller.State 944 * @augments Backbone.Model 945 * 946 * @param {object} [attributes] The attributes hash passed to the state. 947 * @param {string} [attributes.id=image-details] Unique identifier. 948 * @param {string} [attributes.title=Image Details] Title for the state. Displays in the frame's title region. 949 * @param {wp.media.model.Attachment} attributes.image The image's model. 950 * @param {string|false} [attributes.content=image-details] Initial mode for the content region. 951 * @param {string|false} [attributes.menu=false] Initial mode for the menu region. 952 * @param {string|false} [attributes.router=false] Initial mode for the router region. 953 * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region. 954 * @param {boolean} [attributes.editing=false] Unused. 955 * @param {int} [attributes.priority=60] Unused. 956 * 957 * @todo This state inherits some defaults from media.controller.Library.prototype.defaults, 958 * however this may not do anything. 959 */ 960 media.controller.ImageDetails = media.controller.State.extend({ 961 defaults: _.defaults({ 962 id: 'image-details', 963 title: l10n.imageDetailsTitle, 964 content: 'image-details', 965 menu: false, 966 router: false, 967 toolbar: 'image-details', 968 editing: false, 969 priority: 60 970 }, media.controller.Library.prototype.defaults ), 971 972 /** 973 * @since 3.9.0 974 * 975 * @param options Attributes 976 */ 977 initialize: function( options ) { 978 this.image = options.image; 979 media.controller.State.prototype.initialize.apply( this, arguments ); 980 }, 981 982 /** 983 * @since 3.9.0 984 */ 985 activate: function() { 986 this.frame.modal.$el.addClass('image-details'); 987 } 988 }); 989 990 /** 991 * wp.media.controller.GalleryEdit 992 * 993 * A state for editing a gallery's images and settings. 994 * 995 * @class 996 * @augments wp.media.controller.Library 997 * @augments wp.media.controller.State 998 * @augments Backbone.Model 999 * 1000 * @param {object} [attributes] The attributes hash passed to the state. 1001 * @param {string} [attributes.id=gallery-edit] Unique identifier. 1002 * @param {string} [attributes.title=Edit Gallery] Title for the state. Displays in the frame's title region. 1003 * @param {wp.media.model.Attachments} [attributes.library] The collection of attachments in the gallery. 1004 * If one is not supplied, an empty media.model.Selection collection is created. 1005 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 1006 * @param {boolean} [attributes.searchable=false] Whether the library is searchable. 1007 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1008 * @param {string|false} [attributes.content=browse] Initial mode for the content region. 1009 * @param {string|false} [attributes.toolbar=image-details] Initial mode for the toolbar region. 1010 * @param {boolean} [attributes.describe=true] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 1011 * @param {boolean} [attributes.displaySettings=true] Whether to show the attachment display settings interface. 1012 * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable. 1013 * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments. 1014 * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance. 1015 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 1016 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 1017 * Defaults to false for this state, because the library passed in *is* the selection. 1018 * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`. 1019 * If none supplied, defaults to wp.media.view.Attachment.EditLibrary. 1020 */ 1021 media.controller.GalleryEdit = media.controller.Library.extend({ 1022 defaults: { 1023 id: 'gallery-edit', 1024 title: l10n.editGalleryTitle, 1025 multiple: false, 1026 searchable: false, 1027 sortable: true, 1028 display: false, 1029 content: 'browse', 1030 toolbar: 'gallery-edit', 1031 describe: true, 1032 displaySettings: true, 1033 dragInfo: true, 1034 idealColumnWidth: 170, 1035 editing: false, 1036 priority: 60, 1037 syncSelection: false 1038 }, 1039 1040 /** 1041 * @since 3.5.0 1042 */ 1043 initialize: function() { 1044 // If we haven't been provided a `library`, create a `Selection`. 1045 if ( ! this.get('library') ) 1046 this.set( 'library', new media.model.Selection() ); 1047 1048 // The single `Attachment` view to be used in the `Attachments` view. 1049 if ( ! this.get('AttachmentView') ) 1050 this.set( 'AttachmentView', media.view.Attachment.EditLibrary ); 1051 media.controller.Library.prototype.initialize.apply( this, arguments ); 1052 }, 1053 1054 /** 1055 * @since 3.5.0 1056 */ 1057 activate: function() { 1058 var library = this.get('library'); 1059 1060 // Limit the library to images only. 1061 library.props.set( 'type', 'image' ); 1062 1063 // Watch for uploaded attachments. 1064 this.get('library').observe( wp.Uploader.queue ); 1065 1066 this.frame.on( 'content:render:browse', this.gallerySettings, this ); 1067 1068 media.controller.Library.prototype.activate.apply( this, arguments ); 1069 }, 1070 1071 /** 1072 * @since 3.5.0 1073 */ 1074 deactivate: function() { 1075 // Stop watching for uploaded attachments. 1076 this.get('library').unobserve( wp.Uploader.queue ); 1077 1078 this.frame.off( 'content:render:browse', this.gallerySettings, this ); 1079 1080 media.controller.Library.prototype.deactivate.apply( this, arguments ); 1081 }, 1082 1083 /** 1084 * @since 3.5.0 1085 * 1086 * @param browser 1087 */ 1088 gallerySettings: function( browser ) { 1089 if ( ! this.get('displaySettings') ) { 1090 return; 1091 } 1092 1093 var library = this.get('library'); 1094 1095 if ( ! library || ! browser ) { 1096 return; 1097 } 1098 1099 library.gallery = library.gallery || new Backbone.Model(); 1100 1101 browser.sidebar.set({ 1102 gallery: new media.view.Settings.Gallery({ 1103 controller: this, 1104 model: library.gallery, 1105 priority: 40 1106 }) 1107 }); 1108 1109 browser.toolbar.set( 'reverse', { 1110 text: l10n.reverseOrder, 1111 priority: 80, 1112 1113 click: function() { 1114 library.reset( library.toArray().reverse() ); 1115 } 1116 }); 1117 } 1118 }); 1119 1120 /** 1121 * A state for selecting more images to add to a gallery. 1122 * 1123 * @class 1124 * @augments wp.media.controller.Library 1125 * @augments wp.media.controller.State 1126 * @augments Backbone.Model 1127 * 1128 * @param {object} [attributes] The attributes hash passed to the state. 1129 * @param {string} [attributes.id=gallery-library] Unique identifier. 1130 * @param {string} [attributes.title=Add to Gallery] Title for the state. Displays in the frame's title region. 1131 * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. 1132 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 1133 * If one is not supplied, a collection of all images will be created. 1134 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 1135 * Accepts 'all', 'uploaded', or 'unattached'. 1136 * @param {string} [attributes.menu=gallery] Initial mode for the menu region. 1137 * @param {string} [attributes.content=upload] Initial mode for the content region. 1138 * Overridden by persistent user setting if 'contentUserSetting' is true. 1139 * @param {string} [attributes.router=browse] Initial mode for the router region. 1140 * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region. 1141 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 1142 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1143 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 1144 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 1145 * @param {int} [attributes.priority=100] The priority for the state link in the media menu. 1146 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 1147 * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. 1148 */ 1149 media.controller.GalleryAdd = media.controller.Library.extend({ 1150 defaults: _.defaults({ 1151 id: 'gallery-library', 1152 title: l10n.addToGalleryTitle, 1153 multiple: 'add', 1154 filterable: 'uploaded', 1155 menu: 'gallery', 1156 toolbar: 'gallery-add', 1157 priority: 100, 1158 syncSelection: false 1159 }, media.controller.Library.prototype.defaults ), 1160 1161 /** 1162 * @since 3.5.0 1163 */ 1164 initialize: function() { 1165 // If a library wasn't supplied, create a library of images. 1166 if ( ! this.get('library') ) 1167 this.set( 'library', media.query({ type: 'image' }) ); 1168 1169 media.controller.Library.prototype.initialize.apply( this, arguments ); 1170 }, 1171 1172 /** 1173 * @since 3.5.0 1174 */ 1175 activate: function() { 1176 var library = this.get('library'), 1177 edit = this.frame.state('gallery-edit').get('library'); 1178 1179 if ( this.editLibrary && this.editLibrary !== edit ) 1180 library.unobserve( this.editLibrary ); 1181 1182 // Accepts attachments that exist in the original library and 1183 // that do not exist in gallery's library. 1184 library.validator = function( attachment ) { 1185 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments ); 1186 }; 1187 1188 // Reset the library to ensure that all attachments are re-added 1189 // to the collection. Do so silently, as calling `observe` will 1190 // trigger the `reset` event. 1191 library.reset( library.mirroring.models, { silent: true }); 1192 library.observe( edit ); 1193 this.editLibrary = edit; 1194 1195 media.controller.Library.prototype.activate.apply( this, arguments ); 1196 } 1197 }); 1198 1199 /** 1200 * wp.media.controller.CollectionEdit 1201 * 1202 * A state for editing a collection, which is used by audio and video playlists, 1203 * and can be used for other collections. 1204 * 1205 * @class 1206 * @augments wp.media.controller.Library 1207 * @augments wp.media.controller.State 1208 * @augments Backbone.Model 1209 * 1210 * @param {object} [attributes] The attributes hash passed to the state. 1211 * @param {string} attributes.title Title for the state. Displays in the media menu and the frame's title region. 1212 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to edit. 1213 * If one is not supplied, an empty media.model.Selection collection is created. 1214 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 1215 * @param {string} [attributes.content=browse] Initial mode for the content region. 1216 * @param {string} attributes.menu Initial mode for the menu region. @todo this needs a better explanation. 1217 * @param {boolean} [attributes.searchable=false] Whether the library is searchable. 1218 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1219 * @param {boolean} [attributes.describe=true] Whether to offer UI to describe the attachments - e.g. captioning images in a gallery. 1220 * @param {boolean} [attributes.dragInfo=true] Whether to show instructional text about the attachments being sortable. 1221 * @param {boolean} [attributes.dragInfoText] Instructional text about the attachments being sortable. 1222 * @param {int} [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments. 1223 * @param {boolean} [attributes.editing=false] Whether the gallery is being created, or editing an existing instance. 1224 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 1225 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 1226 * Defaults to false for this state, because the library passed in *is* the selection. 1227 * @param {view} [attributes.SettingsView] The view to edit the collection instance settings (e.g. Playlist settings with "Show tracklist" checkbox). 1228 * @param {view} [attributes.AttachmentView] The single `Attachment` view to be used in the `Attachments`. 1229 * If none supplied, defaults to wp.media.view.Attachment.EditLibrary. 1230 * @param {string} attributes.type The collection's media type. (e.g. 'video'). 1231 * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). 1232 */ 1233 media.controller.CollectionEdit = media.controller.Library.extend({ 1234 defaults: { 1235 multiple: false, 1236 sortable: true, 1237 searchable: false, 1238 content: 'browse', 1239 describe: true, 1240 dragInfo: true, 1241 idealColumnWidth: 170, 1242 editing: false, 1243 priority: 60, 1244 SettingsView: false, 1245 syncSelection: false 1246 }, 1247 1248 /** 1249 * @since 3.9.0 1250 */ 1251 initialize: function() { 1252 var collectionType = this.get('collectionType'); 1253 1254 if ( 'video' === this.get( 'type' ) ) { 1255 collectionType = 'video-' + collectionType; 1256 } 1257 1258 this.set( 'id', collectionType + '-edit' ); 1259 this.set( 'toolbar', collectionType + '-edit' ); 1260 1261 // If we haven't been provided a `library`, create a `Selection`. 1262 if ( ! this.get('library') ) { 1263 this.set( 'library', new media.model.Selection() ); 1264 } 1265 // The single `Attachment` view to be used in the `Attachments` view. 1266 if ( ! this.get('AttachmentView') ) { 1267 this.set( 'AttachmentView', media.view.Attachment.EditLibrary ); 1268 } 1269 media.controller.Library.prototype.initialize.apply( this, arguments ); 1270 }, 1271 1272 /** 1273 * @since 3.9.0 1274 */ 1275 activate: function() { 1276 var library = this.get('library'); 1277 1278 // Limit the library to images only. 1279 library.props.set( 'type', this.get( 'type' ) ); 1280 1281 // Watch for uploaded attachments. 1282 this.get('library').observe( wp.Uploader.queue ); 1283 1284 this.frame.on( 'content:render:browse', this.renderSettings, this ); 1285 1286 media.controller.Library.prototype.activate.apply( this, arguments ); 1287 }, 1288 1289 /** 1290 * @since 3.9.0 1291 */ 1292 deactivate: function() { 1293 // Stop watching for uploaded attachments. 1294 this.get('library').unobserve( wp.Uploader.queue ); 1295 1296 this.frame.off( 'content:render:browse', this.renderSettings, this ); 1297 1298 media.controller.Library.prototype.deactivate.apply( this, arguments ); 1299 }, 1300 1301 /** 1302 * Render the collection embed settings view in the browser sidebar. 1303 * 1304 * @todo This is against the pattern elsewhere in media. Typically the frame 1305 * is responsible for adding region mode callbacks. Explain. 1306 * 1307 * @since 3.9.0 1308 * 1309 * @param {wp.media.view.attachmentsBrowser} The attachments browser view. 1310 */ 1311 renderSettings: function( attachmentsBrowserView ) { 1312 var library = this.get('library'), 1313 collectionType = this.get('collectionType'), 1314 dragInfoText = this.get('dragInfoText'), 1315 SettingsView = this.get('SettingsView'), 1316 obj = {}; 1317 1318 if ( ! library || ! attachmentsBrowserView ) { 1319 return; 1320 } 1321 1322 library[ collectionType ] = library[ collectionType ] || new Backbone.Model(); 1323 1324 obj[ collectionType ] = new SettingsView({ 1325 controller: this, 1326 model: library[ collectionType ], 1327 priority: 40 1328 }); 1329 1330 attachmentsBrowserView.sidebar.set( obj ); 1331 1332 if ( dragInfoText ) { 1333 attachmentsBrowserView.toolbar.set( 'dragInfo', new media.View({ 1334 el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0], 1335 priority: -40 1336 }) ); 1337 } 1338 1339 // Add the 'Reverse order' button to the toolbar. 1340 attachmentsBrowserView.toolbar.set( 'reverse', { 1341 text: l10n.reverseOrder, 1342 priority: 80, 1343 1344 click: function() { 1345 library.reset( library.toArray().reverse() ); 1346 } 1347 }); 1348 } 1349 }); 1350 1351 /** 1352 * wp.media.controller.CollectionAdd 1353 * 1354 * A state for adding attachments to a collection (e.g. video playlist). 1355 * 1356 * @class 1357 * @augments wp.media.controller.Library 1358 * @augments wp.media.controller.State 1359 * @augments Backbone.Model 1360 * 1361 * @param {object} [attributes] The attributes hash passed to the state. 1362 * @param {string} [attributes.id=library] Unique identifier. 1363 * @param {string} attributes.title Title for the state. Displays in the frame's title region. 1364 * @param {boolean} [attributes.multiple=add] Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean. 1365 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 1366 * If one is not supplied, a collection of attachments of the specified type will be created. 1367 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 1368 * Accepts 'all', 'uploaded', or 'unattached'. 1369 * @param {string} [attributes.menu=gallery] Initial mode for the menu region. 1370 * @param {string} [attributes.content=upload] Initial mode for the content region. 1371 * Overridden by persistent user setting if 'contentUserSetting' is true. 1372 * @param {string} [attributes.router=browse] Initial mode for the router region. 1373 * @param {string} [attributes.toolbar=gallery-add] Initial mode for the toolbar region. 1374 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 1375 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1376 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 1377 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 1378 * @param {int} [attributes.priority=100] The priority for the state link in the media menu. 1379 * @param {boolean} [attributes.syncSelection=false] Whether the Attachments selection should be persisted from the last state. 1380 * Defaults to false because for this state, because the library of the Edit Gallery state is the selection. 1381 * @param {string} attributes.type The collection's media type. (e.g. 'video'). 1382 * @param {string} attributes.collectionType The collection type. (e.g. 'playlist'). 1383 */ 1384 media.controller.CollectionAdd = media.controller.Library.extend({ 1385 defaults: _.defaults( { 1386 // Selection defaults. @see media.model.Selection 1387 multiple: 'add', 1388 // Attachments browser defaults. @see media.view.AttachmentsBrowser 1389 filterable: 'uploaded', 1390 1391 priority: 100, 1392 syncSelection: false 1393 }, media.controller.Library.prototype.defaults ), 1394 1395 /** 1396 * @since 3.9.0 1397 */ 1398 initialize: function() { 1399 var collectionType = this.get('collectionType'); 1400 1401 if ( 'video' === this.get( 'type' ) ) { 1402 collectionType = 'video-' + collectionType; 1403 } 1404 1405 this.set( 'id', collectionType + '-library' ); 1406 this.set( 'toolbar', collectionType + '-add' ); 1407 this.set( 'menu', collectionType ); 1408 1409 // If we haven't been provided a `library`, create a `Selection`. 1410 if ( ! this.get('library') ) { 1411 this.set( 'library', media.query({ type: this.get('type') }) ); 1412 } 1413 media.controller.Library.prototype.initialize.apply( this, arguments ); 1414 }, 1415 1416 /** 1417 * @since 3.9.0 1418 */ 1419 activate: function() { 1420 var library = this.get('library'), 1421 editLibrary = this.get('editLibrary'), 1422 edit = this.frame.state( this.get('collectionType') + '-edit' ).get('library'); 1423 1424 if ( editLibrary && editLibrary !== edit ) { 1425 library.unobserve( editLibrary ); 1426 } 1427 1428 // Accepts attachments that exist in the original library and 1429 // that do not exist in gallery's library. 1430 library.validator = function( attachment ) { 1431 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments ); 1432 }; 1433 1434 // Reset the library to ensure that all attachments are re-added 1435 // to the collection. Do so silently, as calling `observe` will 1436 // trigger the `reset` event. 1437 library.reset( library.mirroring.models, { silent: true }); 1438 library.observe( edit ); 1439 this.set('editLibrary', edit); 1440 1441 media.controller.Library.prototype.activate.apply( this, arguments ); 1442 } 1443 }); 1444 1445 /** 1446 * wp.media.controller.FeaturedImage 1447 * 1448 * A state for selecting a featured image for a post. 1449 * 1450 * @class 1451 * @augments wp.media.controller.Library 1452 * @augments wp.media.controller.State 1453 * @augments Backbone.Model 1454 * 1455 * @param {object} [attributes] The attributes hash passed to the state. 1456 * @param {string} [attributes.id=featured-image] Unique identifier. 1457 * @param {string} [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region. 1458 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 1459 * If one is not supplied, a collection of all images will be created. 1460 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 1461 * @param {string} [attributes.content=upload] Initial mode for the content region. 1462 * Overridden by persistent user setting if 'contentUserSetting' is true. 1463 * @param {string} [attributes.menu=default] Initial mode for the menu region. 1464 * @param {string} [attributes.router=browse] Initial mode for the router region. 1465 * @param {string} [attributes.toolbar=featured-image] Initial mode for the toolbar region. 1466 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 1467 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 1468 * @param {boolean|string} [attributes.filterable=false] Whether the library is filterable, and if so what filters should be shown. 1469 * Accepts 'all', 'uploaded', or 'unattached'. 1470 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1471 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 1472 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 1473 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 1474 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 1475 */ 1476 media.controller.FeaturedImage = media.controller.Library.extend({ 1477 defaults: _.defaults({ 1478 id: 'featured-image', 1479 title: l10n.setFeaturedImageTitle, 1480 multiple: false, 1481 filterable: 'uploaded', 1482 toolbar: 'featured-image', 1483 priority: 60, 1484 syncSelection: true 1485 }, media.controller.Library.prototype.defaults ), 1486 1487 /** 1488 * @since 3.5.0 1489 */ 1490 initialize: function() { 1491 var library, comparator; 1492 1493 // If we haven't been provided a `library`, create a `Selection`. 1494 if ( ! this.get('library') ) { 1495 this.set( 'library', media.query({ type: 'image' }) ); 1496 } 1497 1498 media.controller.Library.prototype.initialize.apply( this, arguments ); 1499 1500 library = this.get('library'); 1501 comparator = library.comparator; 1502 1503 // Overload the library's comparator to push items that are not in 1504 // the mirrored query to the front of the aggregate collection. 1505 library.comparator = function( a, b ) { 1506 var aInQuery = !! this.mirroring.get( a.cid ), 1507 bInQuery = !! this.mirroring.get( b.cid ); 1508 1509 if ( ! aInQuery && bInQuery ) { 1510 return -1; 1511 } else if ( aInQuery && ! bInQuery ) { 1512 return 1; 1513 } else { 1514 return comparator.apply( this, arguments ); 1515 } 1516 }; 1517 1518 // Add all items in the selection to the library, so any featured 1519 // images that are not initially loaded still appear. 1520 library.observe( this.get('selection') ); 1521 }, 1522 1523 /** 1524 * @since 3.5.0 1525 */ 1526 activate: function() { 1527 this.updateSelection(); 1528 this.frame.on( 'open', this.updateSelection, this ); 1529 1530 media.controller.Library.prototype.activate.apply( this, arguments ); 1531 }, 1532 1533 /** 1534 * @since 3.5.0 1535 */ 1536 deactivate: function() { 1537 this.frame.off( 'open', this.updateSelection, this ); 1538 1539 media.controller.Library.prototype.deactivate.apply( this, arguments ); 1540 }, 1541 1542 /** 1543 * @since 3.5.0 1544 */ 1545 updateSelection: function() { 1546 var selection = this.get('selection'), 1547 id = media.view.settings.post.featuredImageId, 1548 attachment; 1549 1550 if ( '' !== id && -1 !== id ) { 1551 attachment = media.model.Attachment.get( id ); 1552 attachment.fetch(); 1553 } 1554 1555 selection.reset( attachment ? [ attachment ] : [] ); 1556 } 1557 }); 1558 1559 /** 1560 * wp.media.controller.ReplaceImage 1561 * 1562 * A state for replacing an image. 1563 * 1564 * @class 1565 * @augments wp.media.controller.Library 1566 * @augments wp.media.controller.State 1567 * @augments Backbone.Model 1568 * 1569 * @param {object} [attributes] The attributes hash passed to the state. 1570 * @param {string} [attributes.id=replace-image] Unique identifier. 1571 * @param {string} [attributes.title=Replace Image] Title for the state. Displays in the media menu and the frame's title region. 1572 * @param {wp.media.model.Attachments} [attributes.library] The attachments collection to browse. 1573 * If one is not supplied, a collection of all images will be created. 1574 * @param {boolean} [attributes.multiple=false] Whether multi-select is enabled. 1575 * @param {string} [attributes.content=upload] Initial mode for the content region. 1576 * Overridden by persistent user setting if 'contentUserSetting' is true. 1577 * @param {string} [attributes.menu=default] Initial mode for the menu region. 1578 * @param {string} [attributes.router=browse] Initial mode for the router region. 1579 * @param {string} [attributes.toolbar=replace] Initial mode for the toolbar region. 1580 * @param {int} [attributes.priority=60] The priority for the state link in the media menu. 1581 * @param {boolean} [attributes.searchable=true] Whether the library is searchable. 1582 * @param {boolean|string} [attributes.filterable=uploaded] Whether the library is filterable, and if so what filters should be shown. 1583 * Accepts 'all', 'uploaded', or 'unattached'. 1584 * @param {boolean} [attributes.sortable=true] Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection. 1585 * @param {boolean} [attributes.autoSelect=true] Whether an uploaded attachment should be automatically added to the selection. 1586 * @param {boolean} [attributes.describe=false] Whether to offer UI to describe attachments - e.g. captioning images in a gallery. 1587 * @param {boolean} [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user. 1588 * @param {boolean} [attributes.syncSelection=true] Whether the Attachments selection should be persisted from the last state. 1589 */ 1590 media.controller.ReplaceImage = media.controller.Library.extend({ 1591 defaults: _.defaults({ 1592 id: 'replace-image', 1593 title: l10n.replaceImageTitle, 1594 multiple: false, 1595 filterable: 'uploaded', 1596 toolbar: 'replace', 1597 menu: false, 1598 priority: 60, 1599 syncSelection: true 1600 }, media.controller.Library.prototype.defaults ), 1601 1602 /** 1603 * @since 3.9.0 1604 * 1605 * @param options 1606 */ 1607 initialize: function( options ) { 1608 var library, comparator; 1609 1610 this.image = options.image; 1611 // If we haven't been provided a `library`, create a `Selection`. 1612 if ( ! this.get('library') ) { 1613 this.set( 'library', media.query({ type: 'image' }) ); 1614 } 1615 1616 media.controller.Library.prototype.initialize.apply( this, arguments ); 1617 1618 library = this.get('library'); 1619 comparator = library.comparator; 1620 1621 // Overload the library's comparator to push items that are not in 1622 // the mirrored query to the front of the aggregate collection. 1623 library.comparator = function( a, b ) { 1624 var aInQuery = !! this.mirroring.get( a.cid ), 1625 bInQuery = !! this.mirroring.get( b.cid ); 1626 1627 if ( ! aInQuery && bInQuery ) { 1628 return -1; 1629 } else if ( aInQuery && ! bInQuery ) { 1630 return 1; 1631 } else { 1632 return comparator.apply( this, arguments ); 1633 } 1634 }; 1635 1636 // Add all items in the selection to the library, so any featured 1637 // images that are not initially loaded still appear. 1638 library.observe( this.get('selection') ); 1639 }, 1640 1641 /** 1642 * @since 3.9.0 1643 */ 1644 activate: function() { 1645 this.updateSelection(); 1646 media.controller.Library.prototype.activate.apply( this, arguments ); 1647 }, 1648 1649 /** 1650 * @since 3.9.0 1651 */ 1652 updateSelection: function() { 1653 var selection = this.get('selection'), 1654 attachment = this.image.attachment; 1655 1656 selection.reset( attachment ? [ attachment ] : [] ); 1657 } 1658 }); 1659 1660 /** 1661 * wp.media.controller.EditImage 1662 * 1663 * A state for editing (cropping, etc.) an image. 1664 * 1665 * @class 1666 * @augments wp.media.controller.State 1667 * @augments Backbone.Model 1668 * 1669 * @param {object} attributes The attributes hash passed to the state. 1670 * @param {wp.media.model.Attachment} attributes.model The attachment. 1671 * @param {string} [attributes.id=edit-image] Unique identifier. 1672 * @param {string} [attributes.title=Edit Image] Title for the state. Displays in the media menu and the frame's title region. 1673 * @param {string} [attributes.content=edit-image] Initial mode for the content region. 1674 * @param {string} [attributes.toolbar=edit-image] Initial mode for the toolbar region. 1675 * @param {string} [attributes.menu=false] Initial mode for the menu region. 1676 * @param {string} [attributes.url] Unused. @todo Consider removal. 1677 */ 1678 media.controller.EditImage = media.controller.State.extend({ 1679 defaults: { 1680 id: 'edit-image', 1681 title: l10n.editImage, 1682 menu: false, 1683 toolbar: 'edit-image', 1684 content: 'edit-image', 1685 url: '' 1686 }, 1687 1688 /** 1689 * @since 3.9.0 1690 */ 1691 activate: function() { 1692 this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar ); 1693 }, 1694 1695 /** 1696 * @since 3.9.0 1697 */ 1698 deactivate: function() { 1699 this.stopListening( this.frame ); 1700 }, 1701 1702 /** 1703 * @since 3.9.0 1704 */ 1705 toolbar: function() { 1706 var frame = this.frame, 1707 lastState = frame.lastState(), 1708 previous = lastState && lastState.id; 1709 1710 frame.toolbar.set( new media.view.Toolbar({ 1711 controller: frame, 1712 items: { 1713 back: { 1714 style: 'primary', 1715 text: l10n.back, 1716 priority: 20, 1717 click: function() { 1718 if ( previous ) { 1719 frame.setState( previous ); 1720 } else { 1721 frame.close(); 1722 } 1723 } 1724 } 1725 } 1726 }) ); 1727 } 1728 }); 1729 1730 /** 1731 * wp.media.controller.MediaLibrary 1732 * 1733 * @class 1734 * @augments wp.media.controller.Library 1735 * @augments wp.media.controller.State 1736 * @augments Backbone.Model 1737 */ 1738 media.controller.MediaLibrary = media.controller.Library.extend({ 1739 defaults: _.defaults({ 1740 // Attachments browser defaults. @see media.view.AttachmentsBrowser 1741 filterable: 'uploaded', 1742 1743 displaySettings: false, 1744 priority: 80, 1745 syncSelection: false 1746 }, media.controller.Library.prototype.defaults ), 1747 1748 /** 1749 * @since 3.9.0 1750 * 1751 * @param options 1752 */ 1753 initialize: function( options ) { 1754 this.media = options.media; 1755 this.type = options.type; 1756 this.set( 'library', media.query({ type: this.type }) ); 1757 1758 media.controller.Library.prototype.initialize.apply( this, arguments ); 1759 }, 1760 1761 /** 1762 * @since 3.9.0 1763 */ 1764 activate: function() { 1765 // @todo this should use this.frame. 1766 if ( media.frame.lastMime ) { 1767 this.set( 'library', media.query({ type: media.frame.lastMime }) ); 1768 delete media.frame.lastMime; 1769 } 1770 media.controller.Library.prototype.activate.apply( this, arguments ); 1771 } 1772 }); 1773 1774 /** 1775 * wp.media.controller.Embed 1776 * 1777 * A state for embedding media from a URL. 1778 * 1779 * @class 1780 * @augments wp.media.controller.State 1781 * @augments Backbone.Model 1782 * 1783 * @param {object} attributes The attributes hash passed to the state. 1784 * @param {string} [attributes.id=embed] Unique identifier. 1785 * @param {string} [attributes.title=Insert From URL] Title for the state. Displays in the media menu and the frame's title region. 1786 * @param {string} [attributes.content=embed] Initial mode for the content region. 1787 * @param {string} [attributes.menu=default] Initial mode for the menu region. 1788 * @param {string} [attributes.toolbar=main-embed] Initial mode for the toolbar region. 1789 * @param {string} [attributes.menu=false] Initial mode for the menu region. 1790 * @param {int} [attributes.priority=120] The priority for the state link in the media menu. 1791 * @param {string} [attributes.type=link] The type of embed. Currently only link is supported. 1792 * @param {string} [attributes.url] The embed URL. 1793 * @param {object} [attributes.metadata={}] Properties of the embed, which will override attributes.url if set. 1794 */ 1795 media.controller.Embed = media.controller.State.extend({ 1796 defaults: { 1797 id: 'embed', 1798 title: l10n.insertFromUrlTitle, 1799 content: 'embed', 1800 menu: 'default', 1801 toolbar: 'main-embed', 1802 priority: 120, 1803 type: 'link', 1804 url: '', 1805 metadata: {} 1806 }, 1807 1808 // The amount of time used when debouncing the scan. 1809 sensitivity: 200, 1810 1811 initialize: function(options) { 1812 this.metadata = options.metadata; 1813 this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity ); 1814 this.props = new Backbone.Model( this.metadata || { url: '' }); 1815 this.props.on( 'change:url', this.debouncedScan, this ); 1816 this.props.on( 'change:url', this.refresh, this ); 1817 this.on( 'scan', this.scanImage, this ); 1818 }, 1819 1820 /** 1821 * Trigger a scan of the embedded URL's content for metadata required to embed. 1822 * 1823 * @fires wp.media.controller.Embed#scan 1824 */ 1825 scan: function() { 1826 var scanners, 1827 embed = this, 1828 attributes = { 1829 type: 'link', 1830 scanners: [] 1831 }; 1832 1833 // Scan is triggered with the list of `attributes` to set on the 1834 // state, useful for the 'type' attribute and 'scanners' attribute, 1835 // an array of promise objects for asynchronous scan operations. 1836 if ( this.props.get('url') ) { 1837 this.trigger( 'scan', attributes ); 1838 } 1839 1840 if ( attributes.scanners.length ) { 1841 scanners = attributes.scanners = $.when.apply( $, attributes.scanners ); 1842 scanners.always( function() { 1843 if ( embed.get('scanners') === scanners ) { 1844 embed.set( 'loading', false ); 1845 } 1846 }); 1847 } else { 1848 attributes.scanners = null; 1849 } 1850 1851 attributes.loading = !! attributes.scanners; 1852 this.set( attributes ); 1853 }, 1854 /** 1855 * Try scanning the embed as an image to discover its dimensions. 1856 * 1857 * @param {Object} attributes 1858 */ 1859 scanImage: function( attributes ) { 1860 var frame = this.frame, 1861 state = this, 1862 url = this.props.get('url'), 1863 image = new Image(), 1864 deferred = $.Deferred(); 1865 1866 attributes.scanners.push( deferred.promise() ); 1867 1868 // Try to load the image and find its width/height. 1869 image.onload = function() { 1870 deferred.resolve(); 1871 1872 if ( state !== frame.state() || url !== state.props.get('url') ) { 1873 return; 1874 } 1875 1876 state.set({ 1877 type: 'image' 1878 }); 1879 1880 state.props.set({ 1881 width: image.width, 1882 height: image.height 1883 }); 1884 }; 1885 1886 image.onerror = deferred.reject; 1887 image.src = url; 1888 }, 1889 1890 refresh: function() { 1891 this.frame.toolbar.get().refresh(); 1892 }, 1893 1894 reset: function() { 1895 this.props.clear().set({ url: '' }); 1896 1897 if ( this.active ) { 1898 this.refresh(); 1899 } 1900 } 1901 }); 1902 1903 /** 1904 * wp.media.controller.Cropper 1905 * 1906 * A state for cropping an image. 1907 * 1908 * @class 1909 * @augments wp.media.controller.State 1910 * @augments Backbone.Model 1911 */ 1912 media.controller.Cropper = media.controller.State.extend({ 1913 defaults: { 1914 id: 'cropper', 1915 title: l10n.cropImage, 1916 // Region mode defaults. 1917 toolbar: 'crop', 1918 content: 'crop', 1919 router: false, 1920 1921 canSkipCrop: false 1922 }, 1923 1924 activate: function() { 1925 this.frame.on( 'content:create:crop', this.createCropContent, this ); 1926 this.frame.on( 'close', this.removeCropper, this ); 1927 this.set('selection', new Backbone.Collection(this.frame._selection.single)); 1928 }, 1929 1930 deactivate: function() { 1931 this.frame.toolbar.mode('browse'); 1932 }, 1933 1934 createCropContent: function() { 1935 this.cropperView = new wp.media.view.Cropper({controller: this, 1936 attachment: this.get('selection').first() }); 1937 this.cropperView.on('image-loaded', this.createCropToolbar, this); 1938 this.frame.content.set(this.cropperView); 1939 1940 }, 1941 removeCropper: function() { 1942 this.imgSelect.cancelSelection(); 1943 this.imgSelect.setOptions({remove: true}); 1944 this.imgSelect.update(); 1945 this.cropperView.remove(); 1946 }, 1947 createCropToolbar: function() { 1948 var canSkipCrop, toolbarOptions; 1949 1950 canSkipCrop = this.get('canSkipCrop') || false; 1951 1952 toolbarOptions = { 1953 controller: this.frame, 1954 items: { 1955 insert: { 1956 style: 'primary', 1957 text: l10n.cropImage, 1958 priority: 80, 1959 requires: { library: false, selection: false }, 1960 1961 click: function() { 1962 var self = this, 1963 selection = this.controller.state().get('selection').first(); 1964 1965 selection.set({cropDetails: this.controller.state().imgSelect.getSelection()}); 1966 1967 this.$el.text(l10n.cropping); 1968 this.$el.attr('disabled', true); 1969 this.controller.state().doCrop( selection ).done( function( croppedImage ) { 1970 self.controller.trigger('cropped', croppedImage ); 1971 self.controller.close(); 1972 }).fail( function() { 1973 self.controller.trigger('content:error:crop'); 1974 }); 1975 } 1976 } 1977 } 1978 }; 1979 1980 if ( canSkipCrop ) { 1981 _.extend( toolbarOptions.items, { 1982 skip: { 1983 style: 'secondary', 1984 text: l10n.skipCropping, 1985 priority: 70, 1986 requires: { library: false, selection: false }, 1987 click: function() { 1988 var selection = this.controller.state().get('selection').first(); 1989 this.controller.state().cropperView.remove(); 1990 this.controller.trigger('skippedcrop', selection); 1991 this.controller.close(); 1992 } 1993 } 1994 }); 1995 } 1996 1997 this.frame.toolbar.set( new wp.media.view.Toolbar(toolbarOptions) ); 1998 }, 1999 2000 doCrop: function( attachment ) { 2001 return wp.ajax.post( 'custom-header-crop', { 2002 nonce: attachment.get('nonces').edit, 2003 id: attachment.get('id'), 2004 cropDetails: attachment.get('cropDetails') 2005 } ); 2006 } 2007 }); 2008 2009 /** 2010 * wp.media.View 2011 * 2012 * The base view class for media. 2013 * 2014 * Undelegating events, removing events from the model, and 2015 * removing events from the controller mirror the code for 2016 * `Backbone.View.dispose` in Backbone 0.9.8 development. 2017 * 2018 * This behavior has since been removed, and should not be used 2019 * outside of the media manager. 2020 * 2021 * @class 2022 * @augments wp.Backbone.View 2023 * @augments Backbone.View 2024 */ 2025 media.View = wp.Backbone.View.extend({ 2026 constructor: function( options ) { 2027 if ( options && options.controller ) { 2028 this.controller = options.controller; 2029 } 2030 wp.Backbone.View.apply( this, arguments ); 2031 }, 2032 /** 2033 * @todo The internal comment mentions this might have been a stop-gap 2034 * before Backbone 0.9.8 came out. Figure out if Backbone core takes 2035 * care of this in Backbone.View now. 2036 * 2037 * @returns {wp.media.View} Returns itself to allow chaining 2038 */ 2039 dispose: function() { 2040 // Undelegating events, removing events from the model, and 2041 // removing events from the controller mirror the code for 2042 // `Backbone.View.dispose` in Backbone 0.9.8 development. 2043 this.undelegateEvents(); 2044 2045 if ( this.model && this.model.off ) { 2046 this.model.off( null, null, this ); 2047 } 2048 2049 if ( this.collection && this.collection.off ) { 2050 this.collection.off( null, null, this ); 2051 } 2052 2053 // Unbind controller events. 2054 if ( this.controller && this.controller.off ) { 2055 this.controller.off( null, null, this ); 2056 } 2057 2058 return this; 2059 }, 2060 /** 2061 * @returns {wp.media.View} Returns itself to allow chaining 2062 */ 2063 remove: function() { 2064 this.dispose(); 2065 /** 2066 * call 'remove' directly on the parent class 2067 */ 2068 return wp.Backbone.View.prototype.remove.apply( this, arguments ); 2069 } 2070 }); 2071 2072 /** 2073 * wp.media.view.Frame 2074 * 2075 * A frame is a composite view consisting of one or more regions and one or more 2076 * states. 2077 * 2078 * @see wp.media.controller.State 2079 * @see wp.media.controller.Region 2080 * 2081 * @class 2082 * @augments wp.media.View 2083 * @augments wp.Backbone.View 2084 * @augments Backbone.View 2085 * @mixes wp.media.controller.StateMachine 2086 */ 2087 media.view.Frame = media.View.extend({ 2088 initialize: function() { 2089 _.defaults( this.options, { 2090 mode: [ 'select' ] 2091 }); 2092 this._createRegions(); 2093 this._createStates(); 2094 this._createModes(); 2095 }, 2096 2097 _createRegions: function() { 2098 // Clone the regions array. 2099 this.regions = this.regions ? this.regions.slice() : []; 2100 2101 // Initialize regions. 2102 _.each( this.regions, function( region ) { 2103 this[ region ] = new media.controller.Region({ 2104 view: this, 2105 id: region, 2106 selector: '.media-frame-' + region 2107 }); 2108 }, this ); 2109 }, 2110 /** 2111 * Create the frame's states. 2112 * 2113 * @see wp.media.controller.State 2114 * @see wp.media.controller.StateMachine 2115 * 2116 * @fires wp.media.controller.State#ready 2117 */ 2118 _createStates: function() { 2119 // Create the default `states` collection. 2120 this.states = new Backbone.Collection( null, { 2121 model: media.controller.State 2122 }); 2123 2124 // Ensure states have a reference to the frame. 2125 this.states.on( 'add', function( model ) { 2126 model.frame = this; 2127 model.trigger('ready'); 2128 }, this ); 2129 2130 if ( this.options.states ) { 2131 this.states.add( this.options.states ); 2132 } 2133 }, 2134 2135 /** 2136 * A frame can be in a mode or multiple modes at one time. 2137 * 2138 * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode. 2139 */ 2140 _createModes: function() { 2141 // Store active "modes" that the frame is in. Unrelated to region modes. 2142 this.activeModes = new Backbone.Collection(); 2143 this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) ); 2144 2145 _.each( this.options.mode, function( mode ) { 2146 this.activateMode( mode ); 2147 }, this ); 2148 }, 2149 /** 2150 * Reset all states on the frame to their defaults. 2151 * 2152 * @returns {wp.media.view.Frame} Returns itself to allow chaining 2153 */ 2154 reset: function() { 2155 this.states.invoke( 'trigger', 'reset' ); 2156 return this; 2157 }, 2158 /** 2159 * Map activeMode collection events to the frame. 2160 */ 2161 triggerModeEvents: function( model, collection, options ) { 2162 var collectionEvent, 2163 modeEventMap = { 2164 add: 'activate', 2165 remove: 'deactivate' 2166 }, 2167 eventToTrigger; 2168 // Probably a better way to do this. 2169 _.each( options, function( value, key ) { 2170 if ( value ) { 2171 collectionEvent = key; 2172 } 2173 } ); 2174 2175 if ( ! _.has( modeEventMap, collectionEvent ) ) { 2176 return; 2177 } 2178 2179 eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent]; 2180 this.trigger( eventToTrigger ); 2181 }, 2182 /** 2183 * Activate a mode on the frame. 2184 * 2185 * @param string mode Mode ID. 2186 * @returns {this} Returns itself to allow chaining. 2187 */ 2188 activateMode: function( mode ) { 2189 // Bail if the mode is already active. 2190 if ( this.isModeActive( mode ) ) { 2191 return; 2192 } 2193 this.activeModes.add( [ { id: mode } ] ); 2194 // Add a CSS class to the frame so elements can be styled for the mode. 2195 this.$el.addClass( 'mode-' + mode ); 2196 2197 return this; 2198 }, 2199 /** 2200 * Deactivate a mode on the frame. 2201 * 2202 * @param string mode Mode ID. 2203 * @returns {this} Returns itself to allow chaining. 2204 */ 2205 deactivateMode: function( mode ) { 2206 // Bail if the mode isn't active. 2207 if ( ! this.isModeActive( mode ) ) { 2208 return this; 2209 } 2210 this.activeModes.remove( this.activeModes.where( { id: mode } ) ); 2211 this.$el.removeClass( 'mode-' + mode ); 2212 /** 2213 * Frame mode deactivation event. 2214 * 2215 * @event this#{mode}:deactivate 2216 */ 2217 this.trigger( mode + ':deactivate' ); 2218 2219 return this; 2220 }, 2221 /** 2222 * Check if a mode is enabled on the frame. 2223 * 2224 * @param string mode Mode ID. 2225 * @return bool 2226 */ 2227 isModeActive: function( mode ) { 2228 return Boolean( this.activeModes.where( { id: mode } ).length ); 2229 } 2230 }); 2231 2232 // Make the `Frame` a `StateMachine`. 2233 _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype ); 2234 2235 /** 2236 * wp.media.view.MediaFrame 2237 * 2238 * The frame used to create the media modal. 2239 * 2240 * @class 2241 * @augments wp.media.view.Frame 2242 * @augments wp.media.View 2243 * @augments wp.Backbone.View 2244 * @augments Backbone.View 2245 * @mixes wp.media.controller.StateMachine 2246 */ 2247 media.view.MediaFrame = media.view.Frame.extend({ 2248 className: 'media-frame', 2249 template: media.template('media-frame'), 2250 regions: ['menu','title','content','toolbar','router'], 2251 2252 events: { 2253 'click div.media-frame-title h1': 'toggleMenu' 2254 }, 2255 2256 /** 2257 * @global wp.Uploader 2258 */ 2259 initialize: function() { 2260 media.view.Frame.prototype.initialize.apply( this, arguments ); 2261 2262 _.defaults( this.options, { 2263 title: '', 2264 modal: true, 2265 uploader: true 2266 }); 2267 2268 // Ensure core UI is enabled. 2269 this.$el.addClass('wp-core-ui'); 2270 2271 // Initialize modal container view. 2272 if ( this.options.modal ) { 2273 this.modal = new media.view.Modal({ 2274 controller: this, 2275 title: this.options.title 2276 }); 2277 2278 this.modal.content( this ); 2279 } 2280 2281 // Force the uploader off if the upload limit has been exceeded or 2282 // if the browser isn't supported. 2283 if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) { 2284 this.options.uploader = false; 2285 } 2286 2287 // Initialize window-wide uploader. 2288 if ( this.options.uploader ) { 2289 this.uploader = new media.view.UploaderWindow({ 2290 controller: this, 2291 uploader: { 2292 dropzone: this.modal ? this.modal.$el : this.$el, 2293 container: this.$el 2294 } 2295 }); 2296 this.views.set( '.media-frame-uploader', this.uploader ); 2297 } 2298 2299 this.on( 'attach', _.bind( this.views.ready, this.views ), this ); 2300 2301 // Bind default title creation. 2302 this.on( 'title:create:default', this.createTitle, this ); 2303 this.title.mode('default'); 2304 2305 this.on( 'title:render', function( view ) { 2306 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' ); 2307 }); 2308 2309 // Bind default menu. 2310 this.on( 'menu:create:default', this.createMenu, this ); 2311 }, 2312 /** 2313 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 2314 */ 2315 render: function() { 2316 // Activate the default state if no active state exists. 2317 if ( ! this.state() && this.options.state ) { 2318 this.setState( this.options.state ); 2319 } 2320 /** 2321 * call 'render' directly on the parent class 2322 */ 2323 return media.view.Frame.prototype.render.apply( this, arguments ); 2324 }, 2325 /** 2326 * @param {Object} title 2327 * @this wp.media.controller.Region 2328 */ 2329 createTitle: function( title ) { 2330 title.view = new media.View({ 2331 controller: this, 2332 tagName: 'h1' 2333 }); 2334 }, 2335 /** 2336 * @param {Object} menu 2337 * @this wp.media.controller.Region 2338 */ 2339 createMenu: function( menu ) { 2340 menu.view = new media.view.Menu({ 2341 controller: this 2342 }); 2343 }, 2344 2345 toggleMenu: function() { 2346 this.$el.find( '.media-menu' ).toggleClass( 'visible' ); 2347 }, 2348 2349 /** 2350 * @param {Object} toolbar 2351 * @this wp.media.controller.Region 2352 */ 2353 createToolbar: function( toolbar ) { 2354 toolbar.view = new media.view.Toolbar({ 2355 controller: this 2356 }); 2357 }, 2358 /** 2359 * @param {Object} router 2360 * @this wp.media.controller.Region 2361 */ 2362 createRouter: function( router ) { 2363 router.view = new media.view.Router({ 2364 controller: this 2365 }); 2366 }, 2367 /** 2368 * @param {Object} options 2369 */ 2370 createIframeStates: function( options ) { 2371 var settings = media.view.settings, 2372 tabs = settings.tabs, 2373 tabUrl = settings.tabUrl, 2374 $postId; 2375 2376 if ( ! tabs || ! tabUrl ) { 2377 return; 2378 } 2379 2380 // Add the post ID to the tab URL if it exists. 2381 $postId = $('#post_ID'); 2382 if ( $postId.length ) { 2383 tabUrl += '&post_id=' + $postId.val(); 2384 } 2385 2386 // Generate the tab states. 2387 _.each( tabs, function( title, id ) { 2388 this.state( 'iframe:' + id ).set( _.defaults({ 2389 tab: id, 2390 src: tabUrl + '&tab=' + id, 2391 title: title, 2392 content: 'iframe', 2393 menu: 'default' 2394 }, options ) ); 2395 }, this ); 2396 2397 this.on( 'content:create:iframe', this.iframeContent, this ); 2398 this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this ); 2399 this.on( 'menu:render:default', this.iframeMenu, this ); 2400 this.on( 'open', this.hijackThickbox, this ); 2401 this.on( 'close', this.restoreThickbox, this ); 2402 }, 2403 2404 /** 2405 * @param {Object} content 2406 * @this wp.media.controller.Region 2407 */ 2408 iframeContent: function( content ) { 2409 this.$el.addClass('hide-toolbar'); 2410 content.view = new media.view.Iframe({ 2411 controller: this 2412 }); 2413 }, 2414 2415 iframeContentCleanup: function() { 2416 this.$el.removeClass('hide-toolbar'); 2417 }, 2418 2419 iframeMenu: function( view ) { 2420 var views = {}; 2421 2422 if ( ! view ) { 2423 return; 2424 } 2425 2426 _.each( media.view.settings.tabs, function( title, id ) { 2427 views[ 'iframe:' + id ] = { 2428 text: this.state( 'iframe:' + id ).get('title'), 2429 priority: 200 2430 }; 2431 }, this ); 2432 2433 view.set( views ); 2434 }, 2435 2436 hijackThickbox: function() { 2437 var frame = this; 2438 2439 if ( ! window.tb_remove || this._tb_remove ) { 2440 return; 2441 } 2442 2443 this._tb_remove = window.tb_remove; 2444 window.tb_remove = function() { 2445 frame.close(); 2446 frame.reset(); 2447 frame.setState( frame.options.state ); 2448 frame._tb_remove.call( window ); 2449 }; 2450 }, 2451 2452 restoreThickbox: function() { 2453 if ( ! this._tb_remove ) { 2454 return; 2455 } 2456 2457 window.tb_remove = this._tb_remove; 2458 delete this._tb_remove; 2459 } 2460 }); 2461 2462 // Map some of the modal's methods to the frame. 2463 _.each(['open','close','attach','detach','escape'], function( method ) { 2464 /** 2465 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 2466 */ 2467 media.view.MediaFrame.prototype[ method ] = function() { 2468 if ( this.modal ) { 2469 this.modal[ method ].apply( this.modal, arguments ); 2470 } 2471 return this; 2472 }; 2473 }); 2474 2475 /** 2476 * wp.media.view.MediaFrame.Select 2477 * 2478 * A frame for selecting an item or items from the media library. 2479 * 2480 * @class 2481 * @augments wp.media.view.MediaFrame 2482 * @augments wp.media.view.Frame 2483 * @augments wp.media.View 2484 * @augments wp.Backbone.View 2485 * @augments Backbone.View 2486 * @mixes wp.media.controller.StateMachine 2487 */ 2488 media.view.MediaFrame.Select = media.view.MediaFrame.extend({ 2489 initialize: function() { 2490 // Call 'initialize' directly on the parent class. 2491 media.view.MediaFrame.prototype.initialize.apply( this, arguments ); 2492 2493 _.defaults( this.options, { 2494 selection: [], 2495 library: {}, 2496 multiple: false, 2497 state: 'library' 2498 }); 2499 2500 this.createSelection(); 2501 this.createStates(); 2502 this.bindHandlers(); 2503 }, 2504 2505 /** 2506 * Attach a selection collection to the frame. 2507 * 2508 * A selection is a collection of attachments used for a specific purpose 2509 * by a media frame. e.g. Selecting an attachment (or many) to insert into 2510 * post content. 2511 * 2512 * @see media.model.Selection 2513 */ 2514 createSelection: function() { 2515 var selection = this.options.selection; 2516 2517 if ( ! (selection instanceof media.model.Selection) ) { 2518 this.options.selection = new media.model.Selection( selection, { 2519 multiple: this.options.multiple 2520 }); 2521 } 2522 2523 this._selection = { 2524 attachments: new media.model.Attachments(), 2525 difference: [] 2526 }; 2527 }, 2528 2529 /** 2530 * Create the default states on the frame. 2531 */ 2532 createStates: function() { 2533 var options = this.options; 2534 2535 if ( this.options.states ) { 2536 return; 2537 } 2538 2539 // Add the default states. 2540 this.states.add([ 2541 // Main states. 2542 new media.controller.Library({ 2543 library: media.query( options.library ), 2544 multiple: options.multiple, 2545 title: options.title, 2546 priority: 20 2547 }) 2548 ]); 2549 }, 2550 2551 /** 2552 * Bind region mode event callbacks. 2553 * 2554 * @see media.controller.Region.render 2555 */ 2556 bindHandlers: function() { 2557 this.on( 'router:create:browse', this.createRouter, this ); 2558 this.on( 'router:render:browse', this.browseRouter, this ); 2559 this.on( 'content:create:browse', this.browseContent, this ); 2560 this.on( 'content:render:upload', this.uploadContent, this ); 2561 this.on( 'toolbar:create:select', this.createSelectToolbar, this ); 2562 }, 2563 2564 /** 2565 * Render callback for the router region in the `browse` mode. 2566 * 2567 * @param {wp.media.view.Router} routerView 2568 */ 2569 browseRouter: function( routerView ) { 2570 routerView.set({ 2571 upload: { 2572 text: l10n.uploadFilesTitle, 2573 priority: 20 2574 }, 2575 browse: { 2576 text: l10n.mediaLibraryTitle, 2577 priority: 40 2578 } 2579 }); 2580 }, 2581 2582 /** 2583 * Render callback for the content region in the `browse` mode. 2584 * 2585 * @param {wp.media.controller.Region} contentRegion 2586 */ 2587 browseContent: function( contentRegion ) { 2588 var state = this.state(); 2589 2590 this.$el.removeClass('hide-toolbar'); 2591 2592 // Browse our library of attachments. 2593 contentRegion.view = new media.view.AttachmentsBrowser({ 2594 controller: this, 2595 collection: state.get('library'), 2596 selection: state.get('selection'), 2597 model: state, 2598 sortable: state.get('sortable'), 2599 search: state.get('searchable'), 2600 filters: state.get('filterable'), 2601 date: state.get('date'), 2602 display: state.has('display') ? state.get('display') : state.get('displaySettings'), 2603 dragInfo: state.get('dragInfo'), 2604 2605 idealColumnWidth: state.get('idealColumnWidth'), 2606 suggestedWidth: state.get('suggestedWidth'), 2607 suggestedHeight: state.get('suggestedHeight'), 2608 2609 AttachmentView: state.get('AttachmentView') 2610 }); 2611 }, 2612 2613 /** 2614 * Render callback for the content region in the `upload` mode. 2615 */ 2616 uploadContent: function() { 2617 this.$el.removeClass( 'hide-toolbar' ); 2618 this.content.set( new media.view.UploaderInline({ 2619 controller: this 2620 }) ); 2621 }, 2622 2623 /** 2624 * Toolbars 2625 * 2626 * @param {Object} toolbar 2627 * @param {Object} [options={}] 2628 * @this wp.media.controller.Region 2629 */ 2630 createSelectToolbar: function( toolbar, options ) { 2631 options = options || this.options.button || {}; 2632 options.controller = this; 2633 2634 toolbar.view = new media.view.Toolbar.Select( options ); 2635 } 2636 }); 2637 2638 /** 2639 * wp.media.view.MediaFrame.Post 2640 * 2641 * The frame for manipulating media on the Edit Post page. 2642 * 2643 * @class 2644 * @augments wp.media.view.MediaFrame.Select 2645 * @augments wp.media.view.MediaFrame 2646 * @augments wp.media.view.Frame 2647 * @augments wp.media.View 2648 * @augments wp.Backbone.View 2649 * @augments Backbone.View 2650 * @mixes wp.media.controller.StateMachine 2651 */ 2652 media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({ 2653 initialize: function() { 2654 this.counts = { 2655 audio: { 2656 count: media.view.settings.attachmentCounts.audio, 2657 state: 'playlist' 2658 }, 2659 video: { 2660 count: media.view.settings.attachmentCounts.video, 2661 state: 'video-playlist' 2662 } 2663 }; 2664 2665 _.defaults( this.options, { 2666 multiple: true, 2667 editing: false, 2668 state: 'insert', 2669 metadata: {} 2670 }); 2671 2672 // Call 'initialize' directly on the parent class. 2673 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments ); 2674 this.createIframeStates(); 2675 2676 }, 2677 2678 /** 2679 * Create the default states. 2680 */ 2681 createStates: function() { 2682 var options = this.options; 2683 2684 this.states.add([ 2685 // Main states. 2686 new media.controller.Library({ 2687 id: 'insert', 2688 title: l10n.insertMediaTitle, 2689 priority: 20, 2690 toolbar: 'main-insert', 2691 filterable: 'all', 2692 library: media.query( options.library ), 2693 multiple: options.multiple ? 'reset' : false, 2694 editable: true, 2695 2696 // If the user isn't allowed to edit fields, 2697 // can they still edit it locally? 2698 allowLocalEdits: true, 2699 2700 // Show the attachment display settings. 2701 displaySettings: true, 2702 // Update user settings when users adjust the 2703 // attachment display settings. 2704 displayUserSettings: true 2705 }), 2706 2707 new media.controller.Library({ 2708 id: 'gallery', 2709 title: l10n.createGalleryTitle, 2710 priority: 40, 2711 toolbar: 'main-gallery', 2712 filterable: 'uploaded', 2713 multiple: 'add', 2714 editable: false, 2715 2716 library: media.query( _.defaults({ 2717 type: 'image' 2718 }, options.library ) ) 2719 }), 2720 2721 // Embed states. 2722 new media.controller.Embed( { metadata: options.metadata } ), 2723 2724 new media.controller.EditImage( { model: options.editImage } ), 2725 2726 // Gallery states. 2727 new media.controller.GalleryEdit({ 2728 library: options.selection, 2729 editing: options.editing, 2730 menu: 'gallery' 2731 }), 2732 2733 new media.controller.GalleryAdd(), 2734 2735 new media.controller.Library({ 2736 id: 'playlist', 2737 title: l10n.createPlaylistTitle, 2738 priority: 60, 2739 toolbar: 'main-playlist', 2740 filterable: 'uploaded', 2741 multiple: 'add', 2742 editable: false, 2743 2744 library: media.query( _.defaults({ 2745 type: 'audio' 2746 }, options.library ) ) 2747 }), 2748 2749 // Playlist states. 2750 new media.controller.CollectionEdit({ 2751 type: 'audio', 2752 collectionType: 'playlist', 2753 title: l10n.editPlaylistTitle, 2754 SettingsView: media.view.Settings.Playlist, 2755 library: options.selection, 2756 editing: options.editing, 2757 menu: 'playlist', 2758 dragInfoText: l10n.playlistDragInfo, 2759 dragInfo: false 2760 }), 2761 2762 new media.controller.CollectionAdd({ 2763 type: 'audio', 2764 collectionType: 'playlist', 2765 title: l10n.addToPlaylistTitle 2766 }), 2767 2768 new media.controller.Library({ 2769 id: 'video-playlist', 2770 title: l10n.createVideoPlaylistTitle, 2771 priority: 60, 2772 toolbar: 'main-video-playlist', 2773 filterable: 'uploaded', 2774 multiple: 'add', 2775 editable: false, 2776 2777 library: media.query( _.defaults({ 2778 type: 'video' 2779 }, options.library ) ) 2780 }), 2781 2782 new media.controller.CollectionEdit({ 2783 type: 'video', 2784 collectionType: 'playlist', 2785 title: l10n.editVideoPlaylistTitle, 2786 SettingsView: media.view.Settings.Playlist, 2787 library: options.selection, 2788 editing: options.editing, 2789 menu: 'video-playlist', 2790 dragInfoText: l10n.videoPlaylistDragInfo, 2791 dragInfo: false 2792 }), 2793 2794 new media.controller.CollectionAdd({ 2795 type: 'video', 2796 collectionType: 'playlist', 2797 title: l10n.addToVideoPlaylistTitle 2798 }) 2799 ]); 2800 2801 if ( media.view.settings.post.featuredImageId ) { 2802 this.states.add( new media.controller.FeaturedImage() ); 2803 } 2804 }, 2805 2806 bindHandlers: function() { 2807 var handlers, checkCounts; 2808 2809 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments ); 2810 2811 this.on( 'activate', this.activate, this ); 2812 2813 // Only bother checking media type counts if one of the counts is zero 2814 checkCounts = _.find( this.counts, function( type ) { 2815 return type.count === 0; 2816 } ); 2817 2818 if ( typeof checkCounts !== 'undefined' ) { 2819 this.listenTo( media.model.Attachments.all, 'change:type', this.mediaTypeCounts ); 2820 } 2821 2822 this.on( 'menu:create:gallery', this.createMenu, this ); 2823 this.on( 'menu:create:playlist', this.createMenu, this ); 2824 this.on( 'menu:create:video-playlist', this.createMenu, this ); 2825 this.on( 'toolbar:create:main-insert', this.createToolbar, this ); 2826 this.on( 'toolbar:create:main-gallery', this.createToolbar, this ); 2827 this.on( 'toolbar:create:main-playlist', this.createToolbar, this ); 2828 this.on( 'toolbar:create:main-video-playlist', this.createToolbar, this ); 2829 this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this ); 2830 this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this ); 2831 2832 handlers = { 2833 menu: { 2834 'default': 'mainMenu', 2835 'gallery': 'galleryMenu', 2836 'playlist': 'playlistMenu', 2837 'video-playlist': 'videoPlaylistMenu' 2838 }, 2839 2840 content: { 2841 'embed': 'embedContent', 2842 'edit-image': 'editImageContent', 2843 'edit-selection': 'editSelectionContent' 2844 }, 2845 2846 toolbar: { 2847 'main-insert': 'mainInsertToolbar', 2848 'main-gallery': 'mainGalleryToolbar', 2849 'gallery-edit': 'galleryEditToolbar', 2850 'gallery-add': 'galleryAddToolbar', 2851 'main-playlist': 'mainPlaylistToolbar', 2852 'playlist-edit': 'playlistEditToolbar', 2853 'playlist-add': 'playlistAddToolbar', 2854 'main-video-playlist': 'mainVideoPlaylistToolbar', 2855 'video-playlist-edit': 'videoPlaylistEditToolbar', 2856 'video-playlist-add': 'videoPlaylistAddToolbar' 2857 } 2858 }; 2859 2860 _.each( handlers, function( regionHandlers, region ) { 2861 _.each( regionHandlers, function( callback, handler ) { 2862 this.on( region + ':render:' + handler, this[ callback ], this ); 2863 }, this ); 2864 }, this ); 2865 }, 2866 2867 activate: function() { 2868 // Hide menu items for states tied to particular media types if there are no items 2869 _.each( this.counts, function( type ) { 2870 if ( type.count < 1 ) { 2871 this.menuItemVisibility( type.state, 'hide' ); 2872 } 2873 }, this ); 2874 }, 2875 2876 mediaTypeCounts: function( model, attr ) { 2877 if ( typeof this.counts[ attr ] !== 'undefined' && this.counts[ attr ].count < 1 ) { 2878 this.counts[ attr ].count++; 2879 this.menuItemVisibility( this.counts[ attr ].state, 'show' ); 2880 } 2881 }, 2882 2883 // Menus 2884 /** 2885 * @param {wp.Backbone.View} view 2886 */ 2887 mainMenu: function( view ) { 2888 view.set({ 2889 'library-separator': new media.View({ 2890 className: 'separator', 2891 priority: 100 2892 }) 2893 }); 2894 }, 2895 2896 menuItemVisibility: function( state, visibility ) { 2897 var menu = this.menu.get(); 2898 if ( visibility === 'hide' ) { 2899 menu.hide( state ); 2900 } else if ( visibility === 'show' ) { 2901 menu.show( state ); 2902 } 2903 }, 2904 /** 2905 * @param {wp.Backbone.View} view 2906 */ 2907 galleryMenu: function( view ) { 2908 var lastState = this.lastState(), 2909 previous = lastState && lastState.id, 2910 frame = this; 2911 2912 view.set({ 2913 cancel: { 2914 text: l10n.cancelGalleryTitle, 2915 priority: 20, 2916 click: function() { 2917 if ( previous ) { 2918 frame.setState( previous ); 2919 } else { 2920 frame.close(); 2921 } 2922 2923 // Keep focus inside media modal 2924 // after canceling a gallery 2925 this.controller.modal.focusManager.focus(); 2926 } 2927 }, 2928 separateCancel: new media.View({ 2929 className: 'separator', 2930 priority: 40 2931 }) 2932 }); 2933 }, 2934 2935 playlistMenu: function( view ) { 2936 var lastState = this.lastState(), 2937 previous = lastState && lastState.id, 2938 frame = this; 2939 2940 view.set({ 2941 cancel: { 2942 text: l10n.cancelPlaylistTitle, 2943 priority: 20, 2944 click: function() { 2945 if ( previous ) { 2946 frame.setState( previous ); 2947 } else { 2948 frame.close(); 2949 } 2950 } 2951 }, 2952 separateCancel: new media.View({ 2953 className: 'separator', 2954 priority: 40 2955 }) 2956 }); 2957 }, 2958 2959 videoPlaylistMenu: function( view ) { 2960 var lastState = this.lastState(), 2961 previous = lastState && lastState.id, 2962 frame = this; 2963 2964 view.set({ 2965 cancel: { 2966 text: l10n.cancelVideoPlaylistTitle, 2967 priority: 20, 2968 click: function() { 2969 if ( previous ) { 2970 frame.setState( previous ); 2971 } else { 2972 frame.close(); 2973 } 2974 } 2975 }, 2976 separateCancel: new media.View({ 2977 className: 'separator', 2978 priority: 40 2979 }) 2980 }); 2981 }, 2982 2983 // Content 2984 embedContent: function() { 2985 var view = new media.view.Embed({ 2986 controller: this, 2987 model: this.state() 2988 }).render(); 2989 2990 this.content.set( view ); 2991 2992 if ( ! isTouchDevice ) { 2993 view.url.focus(); 2994 } 2995 }, 2996 2997 editSelectionContent: function() { 2998 var state = this.state(), 2999 selection = state.get('selection'), 3000 view; 3001 3002 view = new media.view.AttachmentsBrowser({ 3003 controller: this, 3004 collection: selection, 3005 selection: selection, 3006 model: state, 3007 sortable: true, 3008 search: false, 3009 date: false, 3010 dragInfo: true, 3011 3012 AttachmentView: media.view.Attachment.EditSelection 3013 }).render(); 3014 3015 view.toolbar.set( 'backToLibrary', { 3016 text: l10n.returnToLibrary, 3017 priority: -100, 3018 3019 click: function() { 3020 this.controller.content.mode('browse'); 3021 } 3022 }); 3023 3024 // Browse our library of attachments. 3025 this.content.set( view ); 3026 3027 // Trigger the controller to set focus 3028 this.trigger( 'edit:selection', this ); 3029 }, 3030 3031 editImageContent: function() { 3032 var image = this.state().get('image'), 3033 view = new media.view.EditImage( { model: image, controller: this } ).render(); 3034 3035 this.content.set( view ); 3036 3037 // after creating the wrapper view, load the actual editor via an ajax call 3038 view.loadEditor(); 3039 3040 }, 3041 3042 // Toolbars 3043 3044 /** 3045 * @param {wp.Backbone.View} view 3046 */ 3047 selectionStatusToolbar: function( view ) { 3048 var editable = this.state().get('editable'); 3049 3050 view.set( 'selection', new media.view.Selection({ 3051 controller: this, 3052 collection: this.state().get('selection'), 3053 priority: -40, 3054 3055 // If the selection is editable, pass the callback to 3056 // switch the content mode. 3057 editable: editable && function() { 3058 this.controller.content.mode('edit-selection'); 3059 } 3060 }).render() ); 3061 }, 3062 3063 /** 3064 * @param {wp.Backbone.View} view 3065 */ 3066 mainInsertToolbar: function( view ) { 3067 var controller = this; 3068 3069 this.selectionStatusToolbar( view ); 3070 3071 view.set( 'insert', { 3072 style: 'primary', 3073 priority: 80, 3074 text: l10n.insertIntoPost, 3075 requires: { selection: true }, 3076 3077 /** 3078 * @fires wp.media.controller.State#insert 3079 */ 3080 click: function() { 3081 var state = controller.state(), 3082 selection = state.get('selection'); 3083 3084 controller.close(); 3085 state.trigger( 'insert', selection ).reset(); 3086 } 3087 }); 3088 }, 3089 3090 /** 3091 * @param {wp.Backbone.View} view 3092 */ 3093 mainGalleryToolbar: function( view ) { 3094 var controller = this; 3095 3096 this.selectionStatusToolbar( view ); 3097 3098 view.set( 'gallery', { 3099 style: 'primary', 3100 text: l10n.createNewGallery, 3101 priority: 60, 3102 requires: { selection: true }, 3103 3104 click: function() { 3105 var selection = controller.state().get('selection'), 3106 edit = controller.state('gallery-edit'), 3107 models = selection.where({ type: 'image' }); 3108 3109 edit.set( 'library', new media.model.Selection( models, { 3110 props: selection.props.toJSON(), 3111 multiple: true 3112 }) ); 3113 3114 this.controller.setState('gallery-edit'); 3115 3116 // Keep focus inside media modal 3117 // after jumping to gallery view 3118 this.controller.modal.focusManager.focus(); 3119 } 3120 }); 3121 }, 3122 3123 mainPlaylistToolbar: function( view ) { 3124 var controller = this; 3125 3126 this.selectionStatusToolbar( view ); 3127 3128 view.set( 'playlist', { 3129 style: 'primary', 3130 text: l10n.createNewPlaylist, 3131 priority: 100, 3132 requires: { selection: true }, 3133 3134 click: function() { 3135 var selection = controller.state().get('selection'), 3136 edit = controller.state('playlist-edit'), 3137 models = selection.where({ type: 'audio' }); 3138 3139 edit.set( 'library', new media.model.Selection( models, { 3140 props: selection.props.toJSON(), 3141 multiple: true 3142 }) ); 3143 3144 this.controller.setState('playlist-edit'); 3145 3146 // Keep focus inside media modal 3147 // after jumping to playlist view 3148 this.controller.modal.focusManager.focus(); 3149 } 3150 }); 3151 }, 3152 3153 mainVideoPlaylistToolbar: function( view ) { 3154 var controller = this; 3155 3156 this.selectionStatusToolbar( view ); 3157 3158 view.set( 'video-playlist', { 3159 style: 'primary', 3160 text: l10n.createNewVideoPlaylist, 3161 priority: 100, 3162 requires: { selection: true }, 3163 3164 click: function() { 3165 var selection = controller.state().get('selection'), 3166 edit = controller.state('video-playlist-edit'), 3167 models = selection.where({ type: 'video' }); 3168 3169 edit.set( 'library', new media.model.Selection( models, { 3170 props: selection.props.toJSON(), 3171 multiple: true 3172 }) ); 3173 3174 this.controller.setState('video-playlist-edit'); 3175 3176 // Keep focus inside media modal 3177 // after jumping to video playlist view 3178 this.controller.modal.focusManager.focus(); 3179 } 3180 }); 3181 }, 3182 3183 featuredImageToolbar: function( toolbar ) { 3184 this.createSelectToolbar( toolbar, { 3185 text: l10n.setFeaturedImage, 3186 state: this.options.state 3187 }); 3188 }, 3189 3190 mainEmbedToolbar: function( toolbar ) { 3191 toolbar.view = new media.view.Toolbar.Embed({ 3192 controller: this 3193 }); 3194 }, 3195 3196 galleryEditToolbar: function() { 3197 var editing = this.state().get('editing'); 3198 this.toolbar.set( new media.view.Toolbar({ 3199 controller: this, 3200 items: { 3201 insert: { 3202 style: 'primary', 3203 text: editing ? l10n.updateGallery : l10n.insertGallery, 3204 priority: 80, 3205 requires: { library: true }, 3206 3207 /** 3208 * @fires wp.media.controller.State#update 3209 */ 3210 click: function() { 3211 var controller = this.controller, 3212 state = controller.state(); 3213 3214 controller.close(); 3215 state.trigger( 'update', state.get('library') ); 3216 3217 // Restore and reset the default state. 3218 controller.setState( controller.options.state ); 3219 controller.reset(); 3220 } 3221 } 3222 } 3223 }) ); 3224 }, 3225 3226 galleryAddToolbar: function() { 3227 this.toolbar.set( new media.view.Toolbar({ 3228 controller: this, 3229 items: { 3230 insert: { 3231 style: 'primary', 3232 text: l10n.addToGallery, 3233 priority: 80, 3234 requires: { selection: true }, 3235 3236 /** 3237 * @fires wp.media.controller.State#reset 3238 */ 3239 click: function() { 3240 var controller = this.controller, 3241 state = controller.state(), 3242 edit = controller.state('gallery-edit'); 3243 3244 edit.get('library').add( state.get('selection').models ); 3245 state.trigger('reset'); 3246 controller.setState('gallery-edit'); 3247 } 3248 } 3249 } 3250 }) ); 3251 }, 3252 3253 playlistEditToolbar: function() { 3254 var editing = this.state().get('editing'); 3255 this.toolbar.set( new media.view.Toolbar({ 3256 controller: this, 3257 items: { 3258 insert: { 3259 style: 'primary', 3260 text: editing ? l10n.updatePlaylist : l10n.insertPlaylist, 3261 priority: 80, 3262 requires: { library: true }, 3263 3264 /** 3265 * @fires wp.media.controller.State#update 3266 */ 3267 click: function() { 3268 var controller = this.controller, 3269 state = controller.state(); 3270 3271 controller.close(); 3272 state.trigger( 'update', state.get('library') ); 3273 3274 // Restore and reset the default state. 3275 controller.setState( controller.options.state ); 3276 controller.reset(); 3277 } 3278 } 3279 } 3280 }) ); 3281 }, 3282 3283 playlistAddToolbar: function() { 3284 this.toolbar.set( new media.view.Toolbar({ 3285 controller: this, 3286 items: { 3287 insert: { 3288 style: 'primary', 3289 text: l10n.addToPlaylist, 3290 priority: 80, 3291 requires: { selection: true }, 3292 3293 /** 3294 * @fires wp.media.controller.State#reset 3295 */ 3296 click: function() { 3297 var controller = this.controller, 3298 state = controller.state(), 3299 edit = controller.state('playlist-edit'); 3300 3301 edit.get('library').add( state.get('selection').models ); 3302 state.trigger('reset'); 3303 controller.setState('playlist-edit'); 3304 } 3305 } 3306 } 3307 }) ); 3308 }, 3309 3310 videoPlaylistEditToolbar: function() { 3311 var editing = this.state().get('editing'); 3312 this.toolbar.set( new media.view.Toolbar({ 3313 controller: this, 3314 items: { 3315 insert: { 3316 style: 'primary', 3317 text: editing ? l10n.updateVideoPlaylist : l10n.insertVideoPlaylist, 3318 priority: 140, 3319 requires: { library: true }, 3320 3321 click: function() { 3322 var controller = this.controller, 3323 state = controller.state(), 3324 library = state.get('library'); 3325 3326 library.type = 'video'; 3327 3328 controller.close(); 3329 state.trigger( 'update', library ); 3330 3331 // Restore and reset the default state. 3332 controller.setState( controller.options.state ); 3333 controller.reset(); 3334 } 3335 } 3336 } 3337 }) ); 3338 }, 3339 3340 videoPlaylistAddToolbar: function() { 3341 this.toolbar.set( new media.view.Toolbar({ 3342 controller: this, 3343 items: { 3344 insert: { 3345 style: 'primary', 3346 text: l10n.addToVideoPlaylist, 3347 priority: 140, 3348 requires: { selection: true }, 3349 3350 click: function() { 3351 var controller = this.controller, 3352 state = controller.state(), 3353 edit = controller.state('video-playlist-edit'); 3354 3355 edit.get('library').add( state.get('selection').models ); 3356 state.trigger('reset'); 3357 controller.setState('video-playlist-edit'); 3358 } 3359 } 3360 } 3361 }) ); 3362 } 3363 }); 3364 3365 /** 3366 * wp.media.view.MediaFrame.ImageDetails 3367 * 3368 * A media frame for manipulating an image that's already been inserted 3369 * into a post. 3370 * 3371 * @class 3372 * @augments wp.media.view.MediaFrame.Select 3373 * @augments wp.media.view.MediaFrame 3374 * @augments wp.media.view.Frame 3375 * @augments wp.media.View 3376 * @augments wp.Backbone.View 3377 * @augments Backbone.View 3378 * @mixes wp.media.controller.StateMachine 3379 */ 3380 media.view.MediaFrame.ImageDetails = media.view.MediaFrame.Select.extend({ 3381 defaults: { 3382 id: 'image', 3383 url: '', 3384 menu: 'image-details', 3385 content: 'image-details', 3386 toolbar: 'image-details', 3387 type: 'link', 3388 title: l10n.imageDetailsTitle, 3389 priority: 120 3390 }, 3391 3392 initialize: function( options ) { 3393 this.image = new media.model.PostImage( options.metadata ); 3394 this.options.selection = new media.model.Selection( this.image.attachment, { multiple: false } ); 3395 media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments ); 3396 }, 3397 3398 bindHandlers: function() { 3399 media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments ); 3400 this.on( 'menu:create:image-details', this.createMenu, this ); 3401 this.on( 'content:create:image-details', this.imageDetailsContent, this ); 3402 this.on( 'content:render:edit-image', this.editImageContent, this ); 3403 this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this ); 3404 // override the select toolbar 3405 this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this ); 3406 }, 3407 3408 createStates: function() { 3409 this.states.add([ 3410 new media.controller.ImageDetails({ 3411 image: this.image, 3412 editable: false 3413 }), 3414 new media.controller.ReplaceImage({ 3415 id: 'replace-image', 3416 library: media.query( { type: 'image' } ), 3417 image: this.image, 3418 multiple: false, 3419 title: l10n.imageReplaceTitle, 3420 toolbar: 'replace', 3421 priority: 80, 3422 displaySettings: true 3423 }), 3424 new media.controller.EditImage( { 3425 image: this.image, 3426 selection: this.options.selection 3427 } ) 3428 ]); 3429 }, 3430 3431 imageDetailsContent: function( options ) { 3432 options.view = new media.view.ImageDetails({ 3433 controller: this, 3434 model: this.state().image, 3435 attachment: this.state().image.attachment 3436 }); 3437 }, 3438 3439 editImageContent: function() { 3440 var state = this.state(), 3441 model = state.get('image'), 3442 view; 3443 3444 if ( ! model ) { 3445 return; 3446 } 3447 3448 view = new media.view.EditImage( { model: model, controller: this } ).render(); 3449 3450 this.content.set( view ); 3451 3452 // after bringing in the frame, load the actual editor via an ajax call 3453 view.loadEditor(); 3454 3455 }, 3456 3457 renderImageDetailsToolbar: function() { 3458 this.toolbar.set( new media.view.Toolbar({ 3459 controller: this, 3460 items: { 3461 select: { 3462 style: 'primary', 3463 text: l10n.update, 3464 priority: 80, 3465 3466 click: function() { 3467 var controller = this.controller, 3468 state = controller.state(); 3469 3470 controller.close(); 3471 3472 // not sure if we want to use wp.media.string.image which will create a shortcode or 3473 // perhaps wp.html.string to at least to build the <img /> 3474 state.trigger( 'update', controller.image.toJSON() ); 3475 3476 // Restore and reset the default state. 3477 controller.setState( controller.options.state ); 3478 controller.reset(); 3479 } 3480 } 3481 } 3482 }) ); 3483 }, 3484 3485 renderReplaceImageToolbar: function() { 3486 var frame = this, 3487 lastState = frame.lastState(), 3488 previous = lastState && lastState.id; 3489 3490 this.toolbar.set( new media.view.Toolbar({ 3491 controller: this, 3492 items: { 3493 back: { 3494 text: l10n.back, 3495 priority: 20, 3496 click: function() { 3497 if ( previous ) { 3498 frame.setState( previous ); 3499 } else { 3500 frame.close(); 3501 } 3502 } 3503 }, 3504 3505 replace: { 3506 style: 'primary', 3507 text: l10n.replace, 3508 priority: 80, 3509 3510 click: function() { 3511 var controller = this.controller, 3512 state = controller.state(), 3513 selection = state.get( 'selection' ), 3514 attachment = selection.single(); 3515 3516 controller.close(); 3517 3518 controller.image.changeAttachment( attachment, state.display( attachment ) ); 3519 3520 // not sure if we want to use wp.media.string.image which will create a shortcode or 3521 // perhaps wp.html.string to at least to build the <img /> 3522 state.trigger( 'replace', controller.image.toJSON() ); 3523 3524 // Restore and reset the default state. 3525 controller.setState( controller.options.state ); 3526 controller.reset(); 3527 } 3528 } 3529 } 3530 }) ); 3531 } 3532 3533 }); 3534 3535 /** 3536 * wp.media.view.Modal 3537 * 3538 * A modal view, which the media modal uses as its default container. 3539 * 3540 * @class 3541 * @augments wp.media.View 3542 * @augments wp.Backbone.View 3543 * @augments Backbone.View 3544 */ 3545 media.view.Modal = media.View.extend({ 3546 tagName: 'div', 3547 template: media.template('media-modal'), 3548 3549 attributes: { 3550 tabindex: 0 3551 }, 3552 3553 events: { 3554 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 3555 'keydown': 'keydown' 3556 }, 3557 3558 initialize: function() { 3559 _.defaults( this.options, { 3560 container: document.body, 3561 title: '', 3562 propagate: true, 3563 freeze: true 3564 }); 3565 3566 this.focusManager = new media.view.FocusManager({ 3567 el: this.el 3568 }); 3569 }, 3570 /** 3571 * @returns {Object} 3572 */ 3573 prepare: function() { 3574 return { 3575 title: this.options.title 3576 }; 3577 }, 3578 3579 /** 3580 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3581 */ 3582 attach: function() { 3583 if ( this.views.attached ) { 3584 return this; 3585 } 3586 3587 if ( ! this.views.rendered ) { 3588 this.render(); 3589 } 3590 3591 this.$el.appendTo( this.options.container ); 3592 3593 // Manually mark the view as attached and trigger ready. 3594 this.views.attached = true; 3595 this.views.ready(); 3596 3597 return this.propagate('attach'); 3598 }, 3599 3600 /** 3601 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3602 */ 3603 detach: function() { 3604 if ( this.$el.is(':visible') ) { 3605 this.close(); 3606 } 3607 3608 this.$el.detach(); 3609 this.views.attached = false; 3610 return this.propagate('detach'); 3611 }, 3612 3613 /** 3614 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3615 */ 3616 open: function() { 3617 var $el = this.$el, 3618 options = this.options, 3619 mceEditor; 3620 3621 if ( $el.is(':visible') ) { 3622 return this; 3623 } 3624 3625 if ( ! this.views.attached ) { 3626 this.attach(); 3627 } 3628 3629 // If the `freeze` option is set, record the window's scroll position. 3630 if ( options.freeze ) { 3631 this._freeze = { 3632 scrollTop: $( window ).scrollTop() 3633 }; 3634 } 3635 3636 // Disable page scrolling. 3637 $( 'body' ).addClass( 'modal-open' ); 3638 3639 $el.show(); 3640 3641 // Try to close the onscreen keyboard 3642 if ( 'ontouchend' in document ) { 3643 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor ) && ! mceEditor.isHidden() && mceEditor.iframeElement ) { 3644 mceEditor.iframeElement.focus(); 3645 mceEditor.iframeElement.blur(); 3646 3647 setTimeout( function() { 3648 mceEditor.iframeElement.blur(); 3649 }, 100 ); 3650 } 3651 } 3652 3653 this.$el.focus(); 3654 3655 return this.propagate('open'); 3656 }, 3657 3658 /** 3659 * @param {Object} options 3660 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3661 */ 3662 close: function( options ) { 3663 var freeze = this._freeze; 3664 3665 if ( ! this.views.attached || ! this.$el.is(':visible') ) { 3666 return this; 3667 } 3668 3669 // Enable page scrolling. 3670 $( 'body' ).removeClass( 'modal-open' ); 3671 3672 // Hide modal and remove restricted media modal tab focus once it's closed 3673 this.$el.hide().undelegate( 'keydown' ); 3674 3675 // Put focus back in useful location once modal is closed 3676 $('#wpbody-content').focus(); 3677 3678 this.propagate('close'); 3679 3680 // If the `freeze` option is set, restore the container's scroll position. 3681 if ( freeze ) { 3682 $( window ).scrollTop( freeze.scrollTop ); 3683 } 3684 3685 if ( options && options.escape ) { 3686 this.propagate('escape'); 3687 } 3688 3689 return this; 3690 }, 3691 /** 3692 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3693 */ 3694 escape: function() { 3695 return this.close({ escape: true }); 3696 }, 3697 /** 3698 * @param {Object} event 3699 */ 3700 escapeHandler: function( event ) { 3701 event.preventDefault(); 3702 this.escape(); 3703 }, 3704 3705 /** 3706 * @param {Array|Object} content Views to register to '.media-modal-content' 3707 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3708 */ 3709 content: function( content ) { 3710 this.views.set( '.media-modal-content', content ); 3711 return this; 3712 }, 3713 3714 /** 3715 * Triggers a modal event and if the `propagate` option is set, 3716 * forwards events to the modal's controller. 3717 * 3718 * @param {string} id 3719 * @returns {wp.media.view.Modal} Returns itself to allow chaining 3720 */ 3721 propagate: function( id ) { 3722 this.trigger( id ); 3723 3724 if ( this.options.propagate ) { 3725 this.controller.trigger( id ); 3726 } 3727 3728 return this; 3729 }, 3730 /** 3731 * @param {Object} event 3732 */ 3733 keydown: function( event ) { 3734 // Close the modal when escape is pressed. 3735 if ( 27 === event.which && this.$el.is(':visible') ) { 3736 this.escape(); 3737 event.stopImmediatePropagation(); 3738 } 3739 } 3740 }); 3741 3742 /** 3743 * wp.media.view.FocusManager 3744 * 3745 * @class 3746 * @augments wp.media.View 3747 * @augments wp.Backbone.View 3748 * @augments Backbone.View 3749 */ 3750 media.view.FocusManager = media.View.extend({ 3751 3752 events: { 3753 'keydown': 'constrainTabbing' 3754 }, 3755 3756 focus: function() { // Reset focus on first left menu item 3757 this.$('.media-menu-item').first().focus(); 3758 }, 3759 /** 3760 * @param {Object} event 3761 */ 3762 constrainTabbing: function( event ) { 3763 var tabbables; 3764 3765 // Look for the tab key. 3766 if ( 9 !== event.keyCode ) { 3767 return; 3768 } 3769 3770 // Skip the file input added by Plupload. 3771 tabbables = this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' ); 3772 3773 // Keep tab focus within media modal while it's open 3774 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) { 3775 tabbables.first().focus(); 3776 return false; 3777 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) { 3778 tabbables.last().focus(); 3779 return false; 3780 } 3781 } 3782 3783 }); 3784 3785 /** 3786 * wp.media.view.UploaderWindow 3787 * 3788 * An uploader window that allows for dragging and dropping media. 3789 * 3790 * @class 3791 * @augments wp.media.View 3792 * @augments wp.Backbone.View 3793 * @augments Backbone.View 3794 * 3795 * @param {object} [options] Options hash passed to the view. 3796 * @param {object} [options.uploader] Uploader properties. 3797 * @param {jQuery} [options.uploader.browser] 3798 * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone. 3799 * @param {object} [options.uploader.params] 3800 */ 3801 media.view.UploaderWindow = media.View.extend({ 3802 tagName: 'div', 3803 className: 'uploader-window', 3804 template: media.template('uploader-window'), 3805 3806 initialize: function() { 3807 var uploader; 3808 3809 this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body'); 3810 3811 uploader = this.options.uploader = _.defaults( this.options.uploader || {}, { 3812 dropzone: this.$el, 3813 browser: this.$browser, 3814 params: {} 3815 }); 3816 3817 // Ensure the dropzone is a jQuery collection. 3818 if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) { 3819 uploader.dropzone = $( uploader.dropzone ); 3820 } 3821 3822 this.controller.on( 'activate', this.refresh, this ); 3823 3824 this.controller.on( 'detach', function() { 3825 this.$browser.remove(); 3826 }, this ); 3827 }, 3828 3829 refresh: function() { 3830 if ( this.uploader ) { 3831 this.uploader.refresh(); 3832 } 3833 }, 3834 3835 ready: function() { 3836 var postId = media.view.settings.post.id, 3837 dropzone; 3838 3839 // If the uploader already exists, bail. 3840 if ( this.uploader ) { 3841 return; 3842 } 3843 3844 if ( postId ) { 3845 this.options.uploader.params.post_id = postId; 3846 } 3847 this.uploader = new wp.Uploader( this.options.uploader ); 3848 3849 dropzone = this.uploader.dropzone; 3850 dropzone.on( 'dropzone:enter', _.bind( this.show, this ) ); 3851 dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) ); 3852 3853 $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) ); 3854 }, 3855 3856 _ready: function() { 3857 this.controller.trigger( 'uploader:ready' ); 3858 }, 3859 3860 show: function() { 3861 var $el = this.$el.show(); 3862 3863 // Ensure that the animation is triggered by waiting until 3864 // the transparent element is painted into the DOM. 3865 _.defer( function() { 3866 $el.css({ opacity: 1 }); 3867 }); 3868 }, 3869 3870 hide: function() { 3871 var $el = this.$el.css({ opacity: 0 }); 3872 3873 media.transition( $el ).done( function() { 3874 // Transition end events are subject to race conditions. 3875 // Make sure that the value is set as intended. 3876 if ( '0' === $el.css('opacity') ) { 3877 $el.hide(); 3878 } 3879 }); 3880 3881 // https://core.trac.wordpress.org/ticket/27341 3882 _.delay( function() { 3883 if ( '0' === $el.css('opacity') && $el.is(':visible') ) { 3884 $el.hide(); 3885 } 3886 }, 500 ); 3887 } 3888 }); 3889 3890 /** 3891 * Creates a dropzone on WP editor instances (elements with .wp-editor-wrap 3892 * or #wp-fullscreen-body) and relays drag'n'dropped files to a media workflow. 3893 * 3894 * wp.media.view.EditorUploader 3895 * 3896 * @class 3897 * @augments wp.media.View 3898 * @augments wp.Backbone.View 3899 * @augments Backbone.View 3900 */ 3901 media.view.EditorUploader = media.View.extend({ 3902 tagName: 'div', 3903 className: 'uploader-editor', 3904 template: media.template( 'uploader-editor' ), 3905 3906 localDrag: false, 3907 overContainer: false, 3908 overDropzone: false, 3909 draggingFile: null, 3910 3911 /** 3912 * Bind drag'n'drop events to callbacks. 3913 */ 3914 initialize: function() { 3915 var self = this; 3916 3917 this.initialized = false; 3918 3919 // Bail if not enabled or UA does not support drag'n'drop or File API. 3920 if ( ! window.tinyMCEPreInit || ! window.tinyMCEPreInit.dragDropUpload || ! this.browserSupport() ) { 3921 return this; 3922 } 3923 3924 this.$document = $(document); 3925 this.dropzones = []; 3926 this.files = []; 3927 3928 this.$document.on( 'drop', '.uploader-editor', _.bind( this.drop, this ) ); 3929 this.$document.on( 'dragover', '.uploader-editor', _.bind( this.dropzoneDragover, this ) ); 3930 this.$document.on( 'dragleave', '.uploader-editor', _.bind( this.dropzoneDragleave, this ) ); 3931 this.$document.on( 'click', '.uploader-editor', _.bind( this.click, this ) ); 3932 3933 this.$document.on( 'dragover', _.bind( this.containerDragover, this ) ); 3934 this.$document.on( 'dragleave', _.bind( this.containerDragleave, this ) ); 3935 3936 this.$document.on( 'dragstart dragend drop', function( event ) { 3937 self.localDrag = event.type === 'dragstart'; 3938 }); 3939 3940 this.initialized = true; 3941 return this; 3942 }, 3943 3944 /** 3945 * Check browser support for drag'n'drop. 3946 * 3947 * @return Boolean 3948 */ 3949 browserSupport: function() { 3950 var supports = false, div = document.createElement('div'); 3951 3952 supports = ( 'draggable' in div ) || ( 'ondragstart' in div && 'ondrop' in div ); 3953 supports = supports && !! ( window.File && window.FileList && window.FileReader ); 3954 return supports; 3955 }, 3956 3957 isDraggingFile: function( event ) { 3958 if ( this.draggingFile !== null ) { 3959 return this.draggingFile; 3960 } 3961 3962 if ( _.isUndefined( event.originalEvent ) || _.isUndefined( event.originalEvent.dataTransfer ) ) { 3963 return false; 3964 } 3965 3966 this.draggingFile = _.indexOf( event.originalEvent.dataTransfer.types, 'Files' ) > -1 && 3967 _.indexOf( event.originalEvent.dataTransfer.types, 'text/plain' ) === -1; 3968 3969 return this.draggingFile; 3970 }, 3971 3972 refresh: function( e ) { 3973 var dropzone_id; 3974 for ( dropzone_id in this.dropzones ) { 3975 // Hide the dropzones only if dragging has left the screen. 3976 this.dropzones[ dropzone_id ].toggle( this.overContainer || this.overDropzone ); 3977 } 3978 3979 if ( ! _.isUndefined( e ) ) { 3980 $( e.target ).closest( '.uploader-editor' ).toggleClass( 'droppable', this.overDropzone ); 3981 } 3982 3983 if ( ! this.overContainer && ! this.overDropzone ) { 3984 this.draggingFile = null; 3985 } 3986 3987 return this; 3988 }, 3989 3990 render: function() { 3991 if ( ! this.initialized ) { 3992 return this; 3993 } 3994 3995 media.View.prototype.render.apply( this, arguments ); 3996 $( '.wp-editor-wrap, #wp-fullscreen-body' ).each( _.bind( this.attach, this ) ); 3997 return this; 3998 }, 3999 4000 attach: function( index, editor ) { 4001 // Attach a dropzone to an editor. 4002 var dropzone = this.$el.clone(); 4003 this.dropzones.push( dropzone ); 4004 $( editor ).append( dropzone ); 4005 return this; 4006 }, 4007 4008 /** 4009 * When a file is dropped on the editor uploader, open up an editor media workflow 4010 * and upload the file immediately. 4011 * 4012 * @param {jQuery.Event} event The 'drop' event. 4013 */ 4014 drop: function( event ) { 4015 var $wrap = null, uploadView; 4016 4017 this.containerDragleave( event ); 4018 this.dropzoneDragleave( event ); 4019 4020 this.files = event.originalEvent.dataTransfer.files; 4021 if ( this.files.length < 1 ) { 4022 return; 4023 } 4024 4025 // Set the active editor to the drop target. 4026 $wrap = $( event.target ).parents( '.wp-editor-wrap' ); 4027 if ( $wrap.length > 0 && $wrap[0].id ) { 4028 window.wpActiveEditor = $wrap[0].id.slice( 3, -5 ); 4029 } 4030 4031 if ( ! this.workflow ) { 4032 this.workflow = wp.media.editor.open( 'content', { 4033 frame: 'post', 4034 state: 'insert', 4035 title: wp.media.view.l10n.addMedia, 4036 multiple: true 4037 }); 4038 uploadView = this.workflow.uploader; 4039 if ( uploadView.uploader && uploadView.uploader.ready ) { 4040 this.addFiles.apply( this ); 4041 } else { 4042 this.workflow.on( 'uploader:ready', this.addFiles, this ); 4043 } 4044 } else { 4045 this.workflow.state().reset(); 4046 this.addFiles.apply( this ); 4047 this.workflow.open(); 4048 } 4049 4050 return false; 4051 }, 4052 4053 /** 4054 * Add the files to the uploader. 4055 */ 4056 addFiles: function() { 4057 if ( this.files.length ) { 4058 this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) ); 4059 this.files = []; 4060 } 4061 return this; 4062 }, 4063 4064 containerDragover: function( event ) { 4065 if ( this.localDrag || ! this.isDraggingFile( event ) ) { 4066 return; 4067 } 4068 4069 this.overContainer = true; 4070 this.refresh(); 4071 }, 4072 4073 containerDragleave: function() { 4074 this.overContainer = false; 4075 4076 // Throttle dragleave because it's called when bouncing from some elements to others. 4077 _.delay( _.bind( this.refresh, this ), 50 ); 4078 }, 4079 4080 dropzoneDragover: function( event ) { 4081 if ( this.localDrag || ! this.isDraggingFile( event ) ) { 4082 return; 4083 } 4084 4085 this.overDropzone = true; 4086 this.refresh( event ); 4087 return false; 4088 }, 4089 4090 dropzoneDragleave: function( e ) { 4091 this.overDropzone = false; 4092 _.delay( _.bind( this.refresh, this, e ), 50 ); 4093 }, 4094 4095 click: function( e ) { 4096 // In the rare case where the dropzone gets stuck, hide it on click. 4097 this.containerDragleave( e ); 4098 this.dropzoneDragleave( e ); 4099 this.localDrag = false; 4100 } 4101 }); 4102 4103 /** 4104 * wp.media.view.UploaderInline 4105 * 4106 * The inline uploader that shows up in the 'Upload Files' tab. 4107 * 4108 * @class 4109 * @augments wp.media.View 4110 * @augments wp.Backbone.View 4111 * @augments Backbone.View 4112 */ 4113 media.view.UploaderInline = media.View.extend({ 4114 tagName: 'div', 4115 className: 'uploader-inline', 4116 template: media.template('uploader-inline'), 4117 4118 events: { 4119 'click .close': 'hide' 4120 }, 4121 4122 initialize: function() { 4123 _.defaults( this.options, { 4124 message: '', 4125 status: true, 4126 canClose: false 4127 }); 4128 4129 if ( ! this.options.$browser && this.controller.uploader ) { 4130 this.options.$browser = this.controller.uploader.$browser; 4131 } 4132 4133 if ( _.isUndefined( this.options.postId ) ) { 4134 this.options.postId = media.view.settings.post.id; 4135 } 4136 4137 if ( this.options.status ) { 4138 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({ 4139 controller: this.controller 4140 }) ); 4141 } 4142 }, 4143 4144 prepare: function() { 4145 var suggestedWidth = this.controller.state().get('suggestedWidth'), 4146 suggestedHeight = this.controller.state().get('suggestedHeight'), 4147 data = {}; 4148 4149 data.message = this.options.message; 4150 data.canClose = this.options.canClose; 4151 4152 if ( suggestedWidth && suggestedHeight ) { 4153 data.suggestedWidth = suggestedWidth; 4154 data.suggestedHeight = suggestedHeight; 4155 } 4156 4157 return data; 4158 }, 4159 /** 4160 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 4161 */ 4162 dispose: function() { 4163 if ( this.disposing ) { 4164 /** 4165 * call 'dispose' directly on the parent class 4166 */ 4167 return media.View.prototype.dispose.apply( this, arguments ); 4168 } 4169 4170 // Run remove on `dispose`, so we can be sure to refresh the 4171 // uploader with a view-less DOM. Track whether we're disposing 4172 // so we don't trigger an infinite loop. 4173 this.disposing = true; 4174 return this.remove(); 4175 }, 4176 /** 4177 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining 4178 */ 4179 remove: function() { 4180 /** 4181 * call 'remove' directly on the parent class 4182 */ 4183 var result = media.View.prototype.remove.apply( this, arguments ); 4184 4185 _.defer( _.bind( this.refresh, this ) ); 4186 return result; 4187 }, 4188 4189 refresh: function() { 4190 var uploader = this.controller.uploader; 4191 4192 if ( uploader ) { 4193 uploader.refresh(); 4194 } 4195 }, 4196 /** 4197 * @returns {wp.media.view.UploaderInline} 4198 */ 4199 ready: function() { 4200 var $browser = this.options.$browser, 4201 $placeholder; 4202 4203 if ( this.controller.uploader ) { 4204 $placeholder = this.$('.browser'); 4205 4206 // Check if we've already replaced the placeholder. 4207 if ( $placeholder[0] === $browser[0] ) { 4208 return; 4209 } 4210 4211 $browser.detach().text( $placeholder.text() ); 4212 $browser[0].className = $placeholder[0].className; 4213 $placeholder.replaceWith( $browser.show() ); 4214 } 4215 4216 this.refresh(); 4217 return this; 4218 }, 4219 show: function() { 4220 this.$el.removeClass( 'hidden' ); 4221 }, 4222 hide: function() { 4223 this.$el.addClass( 'hidden' ); 4224 } 4225 4226 }); 4227 4228 /** 4229 * wp.media.view.UploaderStatus 4230 * 4231 * An uploader status for on-going uploads. 4232 * 4233 * @class 4234 * @augments wp.media.View 4235 * @augments wp.Backbone.View 4236 * @augments Backbone.View 4237 */ 4238 media.view.UploaderStatus = media.View.extend({ 4239 className: 'media-uploader-status', 4240 template: media.template('uploader-status'), 4241 4242 events: { 4243 'click .upload-dismiss-errors': 'dismiss' 4244 }, 4245 4246 initialize: function() { 4247 this.queue = wp.Uploader.queue; 4248 this.queue.on( 'add remove reset', this.visibility, this ); 4249 this.queue.on( 'add remove reset change:percent', this.progress, this ); 4250 this.queue.on( 'add remove reset change:uploading', this.info, this ); 4251 4252 this.errors = wp.Uploader.errors; 4253 this.errors.reset(); 4254 this.errors.on( 'add remove reset', this.visibility, this ); 4255 this.errors.on( 'add', this.error, this ); 4256 }, 4257 /** 4258 * @global wp.Uploader 4259 * @returns {wp.media.view.UploaderStatus} 4260 */ 4261 dispose: function() { 4262 wp.Uploader.queue.off( null, null, this ); 4263 /** 4264 * call 'dispose' directly on the parent class 4265 */ 4266 media.View.prototype.dispose.apply( this, arguments ); 4267 return this; 4268 }, 4269 4270 visibility: function() { 4271 this.$el.toggleClass( 'uploading', !! this.queue.length ); 4272 this.$el.toggleClass( 'errors', !! this.errors.length ); 4273 this.$el.toggle( !! this.queue.length || !! this.errors.length ); 4274 }, 4275 4276 ready: function() { 4277 _.each({ 4278 '$bar': '.media-progress-bar div', 4279 '$index': '.upload-index', 4280 '$total': '.upload-total', 4281 '$filename': '.upload-filename' 4282 }, function( selector, key ) { 4283 this[ key ] = this.$( selector ); 4284 }, this ); 4285 4286 this.visibility(); 4287 this.progress(); 4288 this.info(); 4289 }, 4290 4291 progress: function() { 4292 var queue = this.queue, 4293 $bar = this.$bar; 4294 4295 if ( ! $bar || ! queue.length ) { 4296 return; 4297 } 4298 4299 $bar.width( ( queue.reduce( function( memo, attachment ) { 4300 if ( ! attachment.get('uploading') ) { 4301 return memo + 100; 4302 } 4303 4304 var percent = attachment.get('percent'); 4305 return memo + ( _.isNumber( percent ) ? percent : 100 ); 4306 }, 0 ) / queue.length ) + '%' ); 4307 }, 4308 4309 info: function() { 4310 var queue = this.queue, 4311 index = 0, active; 4312 4313 if ( ! queue.length ) { 4314 return; 4315 } 4316 4317 active = this.queue.find( function( attachment, i ) { 4318 index = i; 4319 return attachment.get('uploading'); 4320 }); 4321 4322 this.$index.text( index + 1 ); 4323 this.$total.text( queue.length ); 4324 this.$filename.html( active ? this.filename( active.get('filename') ) : '' ); 4325 }, 4326 /** 4327 * @param {string} filename 4328 * @returns {string} 4329 */ 4330 filename: function( filename ) { 4331 return media.truncate( _.escape( filename ), 24 ); 4332 }, 4333 /** 4334 * @param {Backbone.Model} error 4335 */ 4336 error: function( error ) { 4337 this.views.add( '.upload-errors', new media.view.UploaderStatusError({ 4338 filename: this.filename( error.get('file').name ), 4339 message: error.get('message') 4340 }), { at: 0 }); 4341 }, 4342 4343 /** 4344 * @global wp.Uploader 4345 * 4346 * @param {Object} event 4347 */ 4348 dismiss: function( event ) { 4349 var errors = this.views.get('.upload-errors'); 4350 4351 event.preventDefault(); 4352 4353 if ( errors ) { 4354 _.invoke( errors, 'remove' ); 4355 } 4356 wp.Uploader.errors.reset(); 4357 } 4358 }); 4359 4360 /** 4361 * wp.media.view.UploaderStatusError 4362 * 4363 * @class 4364 * @augments wp.media.View 4365 * @augments wp.Backbone.View 4366 * @augments Backbone.View 4367 */ 4368 media.view.UploaderStatusError = media.View.extend({ 4369 className: 'upload-error', 4370 template: media.template('uploader-status-error') 4371 }); 4372 4373 /** 4374 * wp.media.view.Toolbar 4375 * 4376 * A toolbar which consists of a primary and a secondary section. Each sections 4377 * can be filled with views. 4378 * 4379 * @class 4380 * @augments wp.media.View 4381 * @augments wp.Backbone.View 4382 * @augments Backbone.View 4383 */ 4384 media.view.Toolbar = media.View.extend({ 4385 tagName: 'div', 4386 className: 'media-toolbar', 4387 4388 initialize: function() { 4389 var state = this.controller.state(), 4390 selection = this.selection = state.get('selection'), 4391 library = this.library = state.get('library'); 4392 4393 this._views = {}; 4394 4395 // The toolbar is composed of two `PriorityList` views. 4396 this.primary = new media.view.PriorityList(); 4397 this.secondary = new media.view.PriorityList(); 4398 this.primary.$el.addClass('media-toolbar-primary search-form'); 4399 this.secondary.$el.addClass('media-toolbar-secondary'); 4400 4401 this.views.set([ this.secondary, this.primary ]); 4402 4403 if ( this.options.items ) { 4404 this.set( this.options.items, { silent: true }); 4405 } 4406 4407 if ( ! this.options.silent ) { 4408 this.render(); 4409 } 4410 4411 if ( selection ) { 4412 selection.on( 'add remove reset', this.refresh, this ); 4413 } 4414 4415 if ( library ) { 4416 library.on( 'add remove reset', this.refresh, this ); 4417 } 4418 }, 4419 /** 4420 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining 4421 */ 4422 dispose: function() { 4423 if ( this.selection ) { 4424 this.selection.off( null, null, this ); 4425 } 4426 4427 if ( this.library ) { 4428 this.library.off( null, null, this ); 4429 } 4430 /** 4431 * call 'dispose' directly on the parent class 4432 */ 4433 return media.View.prototype.dispose.apply( this, arguments ); 4434 }, 4435 4436 ready: function() { 4437 this.refresh(); 4438 }, 4439 4440 /** 4441 * @param {string} id 4442 * @param {Backbone.View|Object} view 4443 * @param {Object} [options={}] 4444 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 4445 */ 4446 set: function( id, view, options ) { 4447 var list; 4448 options = options || {}; 4449 4450 // Accept an object with an `id` : `view` mapping. 4451 if ( _.isObject( id ) ) { 4452 _.each( id, function( view, id ) { 4453 this.set( id, view, { silent: true }); 4454 }, this ); 4455 4456 } else { 4457 if ( ! ( view instanceof Backbone.View ) ) { 4458 view.classes = [ 'media-button-' + id ].concat( view.classes || [] ); 4459 view = new media.view.Button( view ).render(); 4460 } 4461 4462 view.controller = view.controller || this.controller; 4463 4464 this._views[ id ] = view; 4465 4466 list = view.options.priority < 0 ? 'secondary' : 'primary'; 4467 this[ list ].set( id, view, options ); 4468 } 4469 4470 if ( ! options.silent ) { 4471 this.refresh(); 4472 } 4473 4474 return this; 4475 }, 4476 /** 4477 * @param {string} id 4478 * @returns {wp.media.view.Button} 4479 */ 4480 get: function( id ) { 4481 return this._views[ id ]; 4482 }, 4483 /** 4484 * @param {string} id 4485 * @param {Object} options 4486 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining 4487 */ 4488 unset: function( id, options ) { 4489 delete this._views[ id ]; 4490 this.primary.unset( id, options ); 4491 this.secondary.unset( id, options ); 4492 4493 if ( ! options || ! options.silent ) { 4494 this.refresh(); 4495 } 4496 return this; 4497 }, 4498 4499 refresh: function() { 4500 var state = this.controller.state(), 4501 library = state.get('library'), 4502 selection = state.get('selection'); 4503 4504 _.each( this._views, function( button ) { 4505 if ( ! button.model || ! button.options || ! button.options.requires ) { 4506 return; 4507 } 4508 4509 var requires = button.options.requires, 4510 disabled = false; 4511 4512 // Prevent insertion of attachments if any of them are still uploading 4513 disabled = _.some( selection.models, function( attachment ) { 4514 return attachment.get('uploading') === true; 4515 }); 4516 4517 if ( requires.selection && selection && ! selection.length ) { 4518 disabled = true; 4519 } else if ( requires.library && library && ! library.length ) { 4520 disabled = true; 4521 } 4522 button.model.set( 'disabled', disabled ); 4523 }); 4524 } 4525 }); 4526 4527 /** 4528 * wp.media.view.Toolbar.Select 4529 * 4530 * @class 4531 * @augments wp.media.view.Toolbar 4532 * @augments wp.media.View 4533 * @augments wp.Backbone.View 4534 * @augments Backbone.View 4535 */ 4536 media.view.Toolbar.Select = media.view.Toolbar.extend({ 4537 initialize: function() { 4538 var options = this.options; 4539 4540 _.bindAll( this, 'clickSelect' ); 4541 4542 _.defaults( options, { 4543 event: 'select', 4544 state: false, 4545 reset: true, 4546 close: true, 4547 text: l10n.select, 4548 4549 // Does the button rely on the selection? 4550 requires: { 4551 selection: true 4552 } 4553 }); 4554 4555 options.items = _.defaults( options.items || {}, { 4556 select: { 4557 style: 'primary', 4558 text: options.text, 4559 priority: 80, 4560 click: this.clickSelect, 4561 requires: options.requires 4562 } 4563 }); 4564 // Call 'initialize' directly on the parent class. 4565 media.view.Toolbar.prototype.initialize.apply( this, arguments ); 4566 }, 4567 4568 clickSelect: function() { 4569 var options = this.options, 4570 controller = this.controller; 4571 4572 if ( options.close ) { 4573 controller.close(); 4574 } 4575 4576 if ( options.event ) { 4577 controller.state().trigger( options.event ); 4578 } 4579 4580 if ( options.state ) { 4581 controller.setState( options.state ); 4582 } 4583 4584 if ( options.reset ) { 4585 controller.reset(); 4586 } 4587 } 4588 }); 4589 4590 /** 4591 * wp.media.view.Toolbar.Embed 4592 * 4593 * @class 4594 * @augments wp.media.view.Toolbar.Select 4595 * @augments wp.media.view.Toolbar 4596 * @augments wp.media.View 4597 * @augments wp.Backbone.View 4598 * @augments Backbone.View 4599 */ 4600 media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({ 4601 initialize: function() { 4602 _.defaults( this.options, { 4603 text: l10n.insertIntoPost, 4604 requires: false 4605 }); 4606 // Call 'initialize' directly on the parent class. 4607 media.view.Toolbar.Select.prototype.initialize.apply( this, arguments ); 4608 }, 4609 4610 refresh: function() { 4611 var url = this.controller.state().props.get('url'); 4612 this.get('select').model.set( 'disabled', ! url || url === 'http://' ); 4613 /** 4614 * call 'refresh' directly on the parent class 4615 */ 4616 media.view.Toolbar.Select.prototype.refresh.apply( this, arguments ); 4617 } 4618 }); 4619 4620 /** 4621 * wp.media.view.Button 4622 * 4623 * @class 4624 * @augments wp.media.View 4625 * @augments wp.Backbone.View 4626 * @augments Backbone.View 4627 */ 4628 media.view.Button = media.View.extend({ 4629 tagName: 'a', 4630 className: 'media-button', 4631 attributes: { href: '#' }, 4632 4633 events: { 4634 'click': 'click' 4635 }, 4636 4637 defaults: { 4638 text: '', 4639 style: '', 4640 size: 'large', 4641 disabled: false 4642 }, 4643 4644 initialize: function() { 4645 /** 4646 * Create a model with the provided `defaults`. 4647 * 4648 * @member {Backbone.Model} 4649 */ 4650 this.model = new Backbone.Model( this.defaults ); 4651 4652 // If any of the `options` have a key from `defaults`, apply its 4653 // value to the `model` and remove it from the `options object. 4654 _.each( this.defaults, function( def, key ) { 4655 var value = this.options[ key ]; 4656 if ( _.isUndefined( value ) ) { 4657 return; 4658 } 4659 4660 this.model.set( key, value ); 4661 delete this.options[ key ]; 4662 }, this ); 4663 4664 this.listenTo( this.model, 'change', this.render ); 4665 }, 4666 /** 4667 * @returns {wp.media.view.Button} Returns itself to allow chaining 4668 */ 4669 render: function() { 4670 var classes = [ 'button', this.className ], 4671 model = this.model.toJSON(); 4672 4673 if ( model.style ) { 4674 classes.push( 'button-' + model.style ); 4675 } 4676 4677 if ( model.size ) { 4678 classes.push( 'button-' + model.size ); 4679 } 4680 4681 classes = _.uniq( classes.concat( this.options.classes ) ); 4682 this.el.className = classes.join(' '); 4683 4684 this.$el.attr( 'disabled', model.disabled ); 4685 this.$el.text( this.model.get('text') ); 4686 4687 return this; 4688 }, 4689 /** 4690 * @param {Object} event 4691 */ 4692 click: function( event ) { 4693 if ( '#' === this.attributes.href ) { 4694 event.preventDefault(); 4695 } 4696 4697 if ( this.options.click && ! this.model.get('disabled') ) { 4698 this.options.click.apply( this, arguments ); 4699 } 4700 } 4701 }); 4702 4703 /** 4704 * wp.media.view.ButtonGroup 4705 * 4706 * @class 4707 * @augments wp.media.View 4708 * @augments wp.Backbone.View 4709 * @augments Backbone.View 4710 */ 4711 media.view.ButtonGroup = media.View.extend({ 4712 tagName: 'div', 4713 className: 'button-group button-large media-button-group', 4714 4715 initialize: function() { 4716 /** 4717 * @member {wp.media.view.Button[]} 4718 */ 4719 this.buttons = _.map( this.options.buttons || [], function( button ) { 4720 if ( button instanceof Backbone.View ) { 4721 return button; 4722 } else { 4723 return new media.view.Button( button ).render(); 4724 } 4725 }); 4726 4727 delete this.options.buttons; 4728 4729 if ( this.options.classes ) { 4730 this.$el.addClass( this.options.classes ); 4731 } 4732 }, 4733 4734 /** 4735 * @returns {wp.media.view.ButtonGroup} 4736 */ 4737 render: function() { 4738 this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() ); 4739 return this; 4740 } 4741 }); 4742 4743 /** 4744 * wp.media.view.PriorityList 4745 * 4746 * @class 4747 * @augments wp.media.View 4748 * @augments wp.Backbone.View 4749 * @augments Backbone.View 4750 */ 4751 media.view.PriorityList = media.View.extend({ 4752 tagName: 'div', 4753 4754 initialize: function() { 4755 this._views = {}; 4756 4757 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 4758 delete this.options.views; 4759 4760 if ( ! this.options.silent ) { 4761 this.render(); 4762 } 4763 }, 4764 /** 4765 * @param {string} id 4766 * @param {wp.media.View|Object} view 4767 * @param {Object} options 4768 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining 4769 */ 4770 set: function( id, view, options ) { 4771 var priority, views, index; 4772 4773 options = options || {}; 4774 4775 // Accept an object with an `id` : `view` mapping. 4776 if ( _.isObject( id ) ) { 4777 _.each( id, function( view, id ) { 4778 this.set( id, view ); 4779 }, this ); 4780 return this; 4781 } 4782 4783 if ( ! (view instanceof Backbone.View) ) { 4784 view = this.toView( view, id, options ); 4785 } 4786 view.controller = view.controller || this.controller; 4787 4788 this.unset( id ); 4789 4790 priority = view.options.priority || 10; 4791 views = this.views.get() || []; 4792 4793 _.find( views, function( existing, i ) { 4794 if ( existing.options.priority > priority ) { 4795 index = i; 4796 return true; 4797 } 4798 }); 4799 4800 this._views[ id ] = view; 4801 this.views.add( view, { 4802 at: _.isNumber( index ) ? index : views.length || 0 4803 }); 4804 4805 return this; 4806 }, 4807 /** 4808 * @param {string} id 4809 * @returns {wp.media.View} 4810 */ 4811 get: function( id ) { 4812 return this._views[ id ]; 4813 }, 4814 /** 4815 * @param {string} id 4816 * @returns {wp.media.view.PriorityList} 4817 */ 4818 unset: function( id ) { 4819 var view = this.get( id ); 4820 4821 if ( view ) { 4822 view.remove(); 4823 } 4824 4825 delete this._views[ id ]; 4826 return this; 4827 }, 4828 /** 4829 * @param {Object} options 4830 * @returns {wp.media.View} 4831 */ 4832 toView: function( options ) { 4833 return new media.View( options ); 4834 } 4835 }); 4836 4837 /** 4838 * wp.media.view.MenuItem 4839 * 4840 * @class 4841 * @augments wp.media.View 4842 * @augments wp.Backbone.View 4843 * @augments Backbone.View 4844 */ 4845 media.view.MenuItem = media.View.extend({ 4846 tagName: 'a', 4847 className: 'media-menu-item', 4848 4849 attributes: { 4850 href: '#' 4851 }, 4852 4853 events: { 4854 'click': '_click' 4855 }, 4856 /** 4857 * @param {Object} event 4858 */ 4859 _click: function( event ) { 4860 var clickOverride = this.options.click; 4861 4862 if ( event ) { 4863 event.preventDefault(); 4864 } 4865 4866 if ( clickOverride ) { 4867 clickOverride.call( this ); 4868 } else { 4869 this.click(); 4870 } 4871 4872 // When selecting a tab along the left side, 4873 // focus should be transferred into the main panel 4874 if ( ! isTouchDevice ) { 4875 $('.media-frame-content input').first().focus(); 4876 } 4877 }, 4878 4879 click: function() { 4880 var state = this.options.state; 4881 4882 if ( state ) { 4883 this.controller.setState( state ); 4884 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below 4885 } 4886 }, 4887 /** 4888 * @returns {wp.media.view.MenuItem} returns itself to allow chaining 4889 */ 4890 render: function() { 4891 var options = this.options; 4892 4893 if ( options.text ) { 4894 this.$el.text( options.text ); 4895 } else if ( options.html ) { 4896 this.$el.html( options.html ); 4897 } 4898 4899 return this; 4900 } 4901 }); 4902 4903 /** 4904 * wp.media.view.Menu 4905 * 4906 * @class 4907 * @augments wp.media.view.PriorityList 4908 * @augments wp.media.View 4909 * @augments wp.Backbone.View 4910 * @augments Backbone.View 4911 */ 4912 media.view.Menu = media.view.PriorityList.extend({ 4913 tagName: 'div', 4914 className: 'media-menu', 4915 property: 'state', 4916 ItemView: media.view.MenuItem, 4917 region: 'menu', 4918 4919 /* TODO: alternatively hide on any click anywhere 4920 events: { 4921 'click': 'click' 4922 }, 4923 4924 click: function() { 4925 this.$el.removeClass( 'visible' ); 4926 }, 4927 */ 4928 4929 /** 4930 * @param {Object} options 4931 * @param {string} id 4932 * @returns {wp.media.View} 4933 */ 4934 toView: function( options, id ) { 4935 options = options || {}; 4936 options[ this.property ] = options[ this.property ] || id; 4937 return new this.ItemView( options ).render(); 4938 }, 4939 4940 ready: function() { 4941 /** 4942 * call 'ready' directly on the parent class 4943 */ 4944 media.view.PriorityList.prototype.ready.apply( this, arguments ); 4945 this.visibility(); 4946 }, 4947 4948 set: function() { 4949 /** 4950 * call 'set' directly on the parent class 4951 */ 4952 media.view.PriorityList.prototype.set.apply( this, arguments ); 4953 this.visibility(); 4954 }, 4955 4956 unset: function() { 4957 /** 4958 * call 'unset' directly on the parent class 4959 */ 4960 media.view.PriorityList.prototype.unset.apply( this, arguments ); 4961 this.visibility(); 4962 }, 4963 4964 visibility: function() { 4965 var region = this.region, 4966 view = this.controller[ region ].get(), 4967 views = this.views.get(), 4968 hide = ! views || views.length < 2; 4969 4970 if ( this === view ) { 4971 this.controller.$el.toggleClass( 'hide-' + region, hide ); 4972 } 4973 }, 4974 /** 4975 * @param {string} id 4976 */ 4977 select: function( id ) { 4978 var view = this.get( id ); 4979 4980 if ( ! view ) { 4981 return; 4982 } 4983 4984 this.deselect(); 4985 view.$el.addClass('active'); 4986 }, 4987 4988 deselect: function() { 4989 this.$el.children().removeClass('active'); 4990 }, 4991 4992 hide: function( id ) { 4993 var view = this.get( id ); 4994 4995 if ( ! view ) { 4996 return; 4997 } 4998 4999 view.$el.addClass('hidden'); 5000 }, 5001 5002 show: function( id ) { 5003 var view = this.get( id ); 5004 5005 if ( ! view ) { 5006 return; 5007 } 5008 5009 view.$el.removeClass('hidden'); 5010 } 5011 }); 5012 5013 /** 5014 * wp.media.view.RouterItem 5015 * 5016 * @class 5017 * @augments wp.media.view.MenuItem 5018 * @augments wp.media.View 5019 * @augments wp.Backbone.View 5020 * @augments Backbone.View 5021 */ 5022 media.view.RouterItem = media.view.MenuItem.extend({ 5023 /** 5024 * On click handler to activate the content region's corresponding mode. 5025 */ 5026 click: function() { 5027 var contentMode = this.options.contentMode; 5028 if ( contentMode ) { 5029 this.controller.content.mode( contentMode ); 5030 } 5031 } 5032 }); 5033 5034 /** 5035 * wp.media.view.Router 5036 * 5037 * @class 5038 * @augments wp.media.view.Menu 5039 * @augments wp.media.view.PriorityList 5040 * @augments wp.media.View 5041 * @augments wp.Backbone.View 5042 * @augments Backbone.View 5043 */ 5044 media.view.Router = media.view.Menu.extend({ 5045 tagName: 'div', 5046 className: 'media-router', 5047 property: 'contentMode', 5048 ItemView: media.view.RouterItem, 5049 region: 'router', 5050 5051 initialize: function() { 5052 this.controller.on( 'content:render', this.update, this ); 5053 // Call 'initialize' directly on the parent class. 5054 media.view.Menu.prototype.initialize.apply( this, arguments ); 5055 }, 5056 5057 update: function() { 5058 var mode = this.controller.content.mode(); 5059 if ( mode ) { 5060 this.select( mode ); 5061 } 5062 } 5063 }); 5064 5065 /** 5066 * wp.media.view.Sidebar 5067 * 5068 * @class 5069 * @augments wp.media.view.PriorityList 5070 * @augments wp.media.View 5071 * @augments wp.Backbone.View 5072 * @augments Backbone.View 5073 */ 5074 media.view.Sidebar = media.view.PriorityList.extend({ 5075 className: 'media-sidebar' 5076 }); 5077 5078 /** 5079 * wp.media.view.Attachment 5080 * 5081 * @class 5082 * @augments wp.media.View 5083 * @augments wp.Backbone.View 5084 * @augments Backbone.View 5085 */ 5086 media.view.Attachment = media.View.extend({ 5087 tagName: 'li', 5088 className: 'attachment', 5089 template: media.template('attachment'), 5090 5091 attributes: function() { 5092 return { 5093 'tabIndex': 0, 5094 'role': 'checkbox', 5095 'aria-label': this.model.get( 'title' ), 5096 'aria-checked': false, 5097 'data-id': this.model.get( 'id' ) 5098 }; 5099 }, 5100 5101 events: { 5102 'click .js--select-attachment': 'toggleSelectionHandler', 5103 'change [data-setting]': 'updateSetting', 5104 'change [data-setting] input': 'updateSetting', 5105 'change [data-setting] select': 'updateSetting', 5106 'change [data-setting] textarea': 'updateSetting', 5107 'click .close': 'removeFromLibrary', 5108 'click .check': 'checkClickHandler', 5109 'click a': 'preventDefault', 5110 'keydown .close': 'removeFromLibrary', 5111 'keydown': 'toggleSelectionHandler' 5112 }, 5113 5114 buttons: {}, 5115 5116 initialize: function() { 5117 var selection = this.options.selection, 5118 options = _.defaults( this.options, { 5119 rerenderOnModelChange: true 5120 } ); 5121 5122 if ( options.rerenderOnModelChange ) { 5123 this.listenTo( this.model, 'change', this.render ); 5124 } else { 5125 this.listenTo( this.model, 'change:percent', this.progress ); 5126 } 5127 this.listenTo( this.model, 'change:title', this._syncTitle ); 5128 this.listenTo( this.model, 'change:caption', this._syncCaption ); 5129 this.listenTo( this.model, 'change:artist', this._syncArtist ); 5130 this.listenTo( this.model, 'change:album', this._syncAlbum ); 5131 5132 // Update the selection. 5133 this.listenTo( this.model, 'add', this.select ); 5134 this.listenTo( this.model, 'remove', this.deselect ); 5135 if ( selection ) { 5136 selection.on( 'reset', this.updateSelect, this ); 5137 // Update the model's details view. 5138 this.listenTo( this.model, 'selection:single selection:unsingle', this.details ); 5139 this.details( this.model, this.controller.state().get('selection') ); 5140 } 5141 5142 this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave ); 5143 }, 5144 /** 5145 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 5146 */ 5147 dispose: function() { 5148 var selection = this.options.selection; 5149 5150 // Make sure all settings are saved before removing the view. 5151 this.updateAll(); 5152 5153 if ( selection ) { 5154 selection.off( null, null, this ); 5155 } 5156 /** 5157 * call 'dispose' directly on the parent class 5158 */ 5159 media.View.prototype.dispose.apply( this, arguments ); 5160 return this; 5161 }, 5162 /** 5163 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 5164 */ 5165 render: function() { 5166 var options = _.defaults( this.model.toJSON(), { 5167 orientation: 'landscape', 5168 uploading: false, 5169 type: '', 5170 subtype: '', 5171 icon: '', 5172 filename: '', 5173 caption: '', 5174 title: '', 5175 dateFormatted: '', 5176 width: '', 5177 height: '', 5178 compat: false, 5179 alt: '', 5180 description: '' 5181 }, this.options ); 5182 5183 options.buttons = this.buttons; 5184 options.describe = this.controller.state().get('describe'); 5185 5186 if ( 'image' === options.type ) { 5187 options.size = this.imageSize(); 5188 } 5189 5190 options.can = {}; 5191 if ( options.nonces ) { 5192 options.can.remove = !! options.nonces['delete']; 5193 options.can.save = !! options.nonces.update; 5194 } 5195 5196 if ( this.controller.state().get('allowLocalEdits') ) { 5197 options.allowLocalEdits = true; 5198 } 5199 5200 if ( options.uploading && ! options.percent ) { 5201 options.percent = 0; 5202 } 5203 5204 this.views.detach(); 5205 this.$el.html( this.template( options ) ); 5206 5207 this.$el.toggleClass( 'uploading', options.uploading ); 5208 5209 if ( options.uploading ) { 5210 this.$bar = this.$('.media-progress-bar div'); 5211 } else { 5212 delete this.$bar; 5213 } 5214 5215 // Check if the model is selected. 5216 this.updateSelect(); 5217 5218 // Update the save status. 5219 this.updateSave(); 5220 5221 this.views.render(); 5222 5223 return this; 5224 }, 5225 5226 progress: function() { 5227 if ( this.$bar && this.$bar.length ) { 5228 this.$bar.width( this.model.get('percent') + '%' ); 5229 } 5230 }, 5231 5232 /** 5233 * @param {Object} event 5234 */ 5235 toggleSelectionHandler: function( event ) { 5236 var method; 5237 5238 // Don't do anything inside inputs. 5239 if ( 'INPUT' === event.target.nodeName ) { 5240 return; 5241 } 5242 5243 // Catch arrow events 5244 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 5245 this.controller.trigger( 'attachment:keydown:arrow', event ); 5246 return; 5247 } 5248 5249 // Catch enter and space events 5250 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 5251 return; 5252 } 5253 5254 event.preventDefault(); 5255 5256 // In the grid view, bubble up an edit:attachment event to the controller. 5257 if ( this.controller.isModeActive( 'grid' ) ) { 5258 if ( this.controller.isModeActive( 'edit' ) ) { 5259 // Pass the current target to restore focus when closing 5260 this.controller.trigger( 'edit:attachment', this.model, event.currentTarget ); 5261 return; 5262 } 5263 5264 if ( this.controller.isModeActive( 'select' ) ) { 5265 method = 'toggle'; 5266 } 5267 } 5268 5269 if ( event.shiftKey ) { 5270 method = 'between'; 5271 } else if ( event.ctrlKey || event.metaKey ) { 5272 method = 'toggle'; 5273 } 5274 5275 this.toggleSelection({ 5276 method: method 5277 }); 5278 5279 this.controller.trigger( 'selection:toggle' ); 5280 }, 5281 /** 5282 * @param {Object} options 5283 */ 5284 toggleSelection: function( options ) { 5285 var collection = this.collection, 5286 selection = this.options.selection, 5287 model = this.model, 5288 method = options && options.method, 5289 single, models, singleIndex, modelIndex; 5290 5291 if ( ! selection ) { 5292 return; 5293 } 5294 5295 single = selection.single(); 5296 method = _.isUndefined( method ) ? selection.multiple : method; 5297 5298 // If the `method` is set to `between`, select all models that 5299 // exist between the current and the selected model. 5300 if ( 'between' === method && single && selection.multiple ) { 5301 // If the models are the same, short-circuit. 5302 if ( single === model ) { 5303 return; 5304 } 5305 5306 singleIndex = collection.indexOf( single ); 5307 modelIndex = collection.indexOf( this.model ); 5308 5309 if ( singleIndex < modelIndex ) { 5310 models = collection.models.slice( singleIndex, modelIndex + 1 ); 5311 } else { 5312 models = collection.models.slice( modelIndex, singleIndex + 1 ); 5313 } 5314 5315 selection.add( models ); 5316 selection.single( model ); 5317 return; 5318 5319 // If the `method` is set to `toggle`, just flip the selection 5320 // status, regardless of whether the model is the single model. 5321 } else if ( 'toggle' === method ) { 5322 selection[ this.selected() ? 'remove' : 'add' ]( model ); 5323 selection.single( model ); 5324 return; 5325 } else if ( 'add' === method ) { 5326 selection.add( model ); 5327 selection.single( model ); 5328 return; 5329 } 5330 5331 // Fixes bug that loses focus when selecting a featured image 5332 if ( ! method ) { 5333 method = 'add'; 5334 } 5335 5336 if ( method !== 'add' ) { 5337 method = 'reset'; 5338 } 5339 5340 if ( this.selected() ) { 5341 // If the model is the single model, remove it. 5342 // If it is not the same as the single model, 5343 // it now becomes the single model. 5344 selection[ single === model ? 'remove' : 'single' ]( model ); 5345 } else { 5346 // If the model is not selected, run the `method` on the 5347 // selection. By default, we `reset` the selection, but the 5348 // `method` can be set to `add` the model to the selection. 5349 selection[ method ]( model ); 5350 selection.single( model ); 5351 } 5352 }, 5353 5354 updateSelect: function() { 5355 this[ this.selected() ? 'select' : 'deselect' ](); 5356 }, 5357 /** 5358 * @returns {unresolved|Boolean} 5359 */ 5360 selected: function() { 5361 var selection = this.options.selection; 5362 if ( selection ) { 5363 return !! selection.get( this.model.cid ); 5364 } 5365 }, 5366 /** 5367 * @param {Backbone.Model} model 5368 * @param {Backbone.Collection} collection 5369 */ 5370 select: function( model, collection ) { 5371 var selection = this.options.selection, 5372 controller = this.controller; 5373 5374 // Check if a selection exists and if it's the collection provided. 5375 // If they're not the same collection, bail; we're in another 5376 // selection's event loop. 5377 if ( ! selection || ( collection && collection !== selection ) ) { 5378 return; 5379 } 5380 5381 // Bail if the model is already selected. 5382 if ( this.$el.hasClass( 'selected' ) ) { 5383 return; 5384 } 5385 5386 // Add 'selected' class to model, set aria-checked to true. 5387 this.$el.addClass( 'selected' ).attr( 'aria-checked', true ); 5388 // Make the checkbox tabable, except in media grid (bulk select mode). 5389 if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) { 5390 this.$( '.check' ).attr( 'tabindex', '0' ); 5391 } 5392 }, 5393 /** 5394 * @param {Backbone.Model} model 5395 * @param {Backbone.Collection} collection 5396 */ 5397 deselect: function( model, collection ) { 5398 var selection = this.options.selection; 5399 5400 // Check if a selection exists and if it's the collection provided. 5401 // If they're not the same collection, bail; we're in another 5402 // selection's event loop. 5403 if ( ! selection || ( collection && collection !== selection ) ) { 5404 return; 5405 } 5406 this.$el.removeClass( 'selected' ).attr( 'aria-checked', false ) 5407 .find( '.check' ).attr( 'tabindex', '-1' ); 5408 }, 5409 /** 5410 * @param {Backbone.Model} model 5411 * @param {Backbone.Collection} collection 5412 */ 5413 details: function( model, collection ) { 5414 var selection = this.options.selection, 5415 details; 5416 5417 if ( selection !== collection ) { 5418 return; 5419 } 5420 5421 details = selection.single(); 5422 this.$el.toggleClass( 'details', details === this.model ); 5423 }, 5424 /** 5425 * @param {Object} event 5426 */ 5427 preventDefault: function( event ) { 5428 event.preventDefault(); 5429 }, 5430 /** 5431 * @param {string} size 5432 * @returns {Object} 5433 */ 5434 imageSize: function( size ) { 5435 var sizes = this.model.get('sizes'), matched = false; 5436 5437 size = size || 'medium'; 5438 5439 // Use the provided image size if possible. 5440 if ( sizes ) { 5441 if ( sizes[ size ] ) { 5442 matched = sizes[ size ]; 5443 } else if ( sizes.large ) { 5444 matched = sizes.large; 5445 } else if ( sizes.thumbnail ) { 5446 matched = sizes.thumbnail; 5447 } else if ( sizes.full ) { 5448 matched = sizes.full; 5449 } 5450 5451 if ( matched ) { 5452 return _.clone( matched ); 5453 } 5454 } 5455 5456 return { 5457 url: this.model.get('url'), 5458 width: this.model.get('width'), 5459 height: this.model.get('height'), 5460 orientation: this.model.get('orientation') 5461 }; 5462 }, 5463 /** 5464 * @param {Object} event 5465 */ 5466 updateSetting: function( event ) { 5467 var $setting = $( event.target ).closest('[data-setting]'), 5468 setting, value; 5469 5470 if ( ! $setting.length ) { 5471 return; 5472 } 5473 5474 setting = $setting.data('setting'); 5475 value = event.target.value; 5476 5477 if ( this.model.get( setting ) !== value ) { 5478 this.save( setting, value ); 5479 } 5480 }, 5481 5482 /** 5483 * Pass all the arguments to the model's save method. 5484 * 5485 * Records the aggregate status of all save requests and updates the 5486 * view's classes accordingly. 5487 */ 5488 save: function() { 5489 var view = this, 5490 save = this._save = this._save || { status: 'ready' }, 5491 request = this.model.save.apply( this.model, arguments ), 5492 requests = save.requests ? $.when( request, save.requests ) : request; 5493 5494 // If we're waiting to remove 'Saved.', stop. 5495 if ( save.savedTimer ) { 5496 clearTimeout( save.savedTimer ); 5497 } 5498 5499 this.updateSave('waiting'); 5500 save.requests = requests; 5501 requests.always( function() { 5502 // If we've performed another request since this one, bail. 5503 if ( save.requests !== requests ) { 5504 return; 5505 } 5506 5507 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' ); 5508 save.savedTimer = setTimeout( function() { 5509 view.updateSave('ready'); 5510 delete save.savedTimer; 5511 }, 2000 ); 5512 }); 5513 }, 5514 /** 5515 * @param {string} status 5516 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 5517 */ 5518 updateSave: function( status ) { 5519 var save = this._save = this._save || { status: 'ready' }; 5520 5521 if ( status && status !== save.status ) { 5522 this.$el.removeClass( 'save-' + save.status ); 5523 save.status = status; 5524 } 5525 5526 this.$el.addClass( 'save-' + save.status ); 5527 return this; 5528 }, 5529 5530 updateAll: function() { 5531 var $settings = this.$('[data-setting]'), 5532 model = this.model, 5533 changed; 5534 5535 changed = _.chain( $settings ).map( function( el ) { 5536 var $input = $('input, textarea, select, [value]', el ), 5537 setting, value; 5538 5539 if ( ! $input.length ) { 5540 return; 5541 } 5542 5543 setting = $(el).data('setting'); 5544 value = $input.val(); 5545 5546 // Record the value if it changed. 5547 if ( model.get( setting ) !== value ) { 5548 return [ setting, value ]; 5549 } 5550 }).compact().object().value(); 5551 5552 if ( ! _.isEmpty( changed ) ) { 5553 model.save( changed ); 5554 } 5555 }, 5556 /** 5557 * @param {Object} event 5558 */ 5559 removeFromLibrary: function( event ) { 5560 // Catch enter and space events 5561 if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) { 5562 return; 5563 } 5564 5565 // Stop propagation so the model isn't selected. 5566 event.stopPropagation(); 5567 5568 this.collection.remove( this.model ); 5569 }, 5570 5571 /** 5572 * Add the model if it isn't in the selection, if it is in the selection, 5573 * remove it. 5574 * 5575 * @param {[type]} event [description] 5576 * @return {[type]} [description] 5577 */ 5578 checkClickHandler: function ( event ) { 5579 var selection = this.options.selection; 5580 if ( ! selection ) { 5581 return; 5582 } 5583 event.stopPropagation(); 5584 if ( selection.where( { id: this.model.get( 'id' ) } ).length ) { 5585 selection.remove( this.model ); 5586 // Move focus back to the attachment tile (from the check). 5587 this.$el.focus(); 5588 } else { 5589 selection.add( this.model ); 5590 } 5591 } 5592 }); 5593 5594 // Ensure settings remain in sync between attachment views. 5595 _.each({ 5596 caption: '_syncCaption', 5597 title: '_syncTitle', 5598 artist: '_syncArtist', 5599 album: '_syncAlbum' 5600 }, function( method, setting ) { 5601 /** 5602 * @param {Backbone.Model} model 5603 * @param {string} value 5604 * @returns {wp.media.view.Attachment} Returns itself to allow chaining 5605 */ 5606 media.view.Attachment.prototype[ method ] = function( model, value ) { 5607 var $setting = this.$('[data-setting="' + setting + '"]'); 5608 5609 if ( ! $setting.length ) { 5610 return this; 5611 } 5612 5613 // If the updated value is in sync with the value in the DOM, there 5614 // is no need to re-render. If we're currently editing the value, 5615 // it will automatically be in sync, suppressing the re-render for 5616 // the view we're editing, while updating any others. 5617 if ( value === $setting.find('input, textarea, select, [value]').val() ) { 5618 return this; 5619 } 5620 5621 return this.render(); 5622 }; 5623 }); 5624 5625 /** 5626 * wp.media.view.Attachment.Library 5627 * 5628 * @class 5629 * @augments wp.media.view.Attachment 5630 * @augments wp.media.View 5631 * @augments wp.Backbone.View 5632 * @augments Backbone.View 5633 */ 5634 media.view.Attachment.Library = media.view.Attachment.extend({ 5635 buttons: { 5636 check: true 5637 } 5638 }); 5639 5640 /** 5641 * wp.media.view.Attachment.EditLibrary 5642 * 5643 * @class 5644 * @augments wp.media.view.Attachment 5645 * @augments wp.media.View 5646 * @augments wp.Backbone.View 5647 * @augments Backbone.View 5648 */ 5649 media.view.Attachment.EditLibrary = media.view.Attachment.extend({ 5650 buttons: { 5651 close: true 5652 } 5653 }); 5654 5655 /** 5656 * wp.media.view.Attachments 5657 * 5658 * @class 5659 * @augments wp.media.View 5660 * @augments wp.Backbone.View 5661 * @augments Backbone.View 5662 */ 5663 media.view.Attachments = media.View.extend({ 5664 tagName: 'ul', 5665 className: 'attachments', 5666 5667 attributes: { 5668 tabIndex: -1 5669 }, 5670 5671 initialize: function() { 5672 this.el.id = _.uniqueId('__attachments-view-'); 5673 5674 _.defaults( this.options, { 5675 refreshSensitivity: isTouchDevice ? 300 : 200, 5676 refreshThreshold: 3, 5677 AttachmentView: media.view.Attachment, 5678 sortable: false, 5679 resize: true, 5680 idealColumnWidth: $( window ).width() < 640 ? 135 : 150 5681 }); 5682 5683 this._viewsByCid = {}; 5684 this.$window = $( window ); 5685 this.resizeEvent = 'resize.media-modal-columns'; 5686 5687 this.collection.on( 'add', function( attachment ) { 5688 this.views.add( this.createAttachmentView( attachment ), { 5689 at: this.collection.indexOf( attachment ) 5690 }); 5691 }, this ); 5692 5693 this.collection.on( 'remove', function( attachment ) { 5694 var view = this._viewsByCid[ attachment.cid ]; 5695 delete this._viewsByCid[ attachment.cid ]; 5696 5697 if ( view ) { 5698 view.remove(); 5699 } 5700 }, this ); 5701 5702 this.collection.on( 'reset', this.render, this ); 5703 5704 this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus ); 5705 5706 // Throttle the scroll handler and bind this. 5707 this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); 5708 5709 this.options.scrollElement = this.options.scrollElement || this.el; 5710 $( this.options.scrollElement ).on( 'scroll', this.scroll ); 5711 5712 this.initSortable(); 5713 5714 _.bindAll( this, 'setColumns' ); 5715 5716 if ( this.options.resize ) { 5717 this.on( 'ready', this.bindEvents ); 5718 this.controller.on( 'open', this.setColumns ); 5719 5720 // Call this.setColumns() after this view has been rendered in the DOM so 5721 // attachments get proper width applied. 5722 _.defer( this.setColumns, this ); 5723 } 5724 }, 5725 5726 bindEvents: function() { 5727 this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) ); 5728 }, 5729 5730 attachmentFocus: function() { 5731 this.$( 'li:first' ).focus(); 5732 }, 5733 5734 restoreFocus: function() { 5735 this.$( 'li.selected:first' ).focus(); 5736 }, 5737 5738 arrowEvent: function( event ) { 5739 var attachments = this.$el.children( 'li' ), 5740 perRow = this.columns, 5741 index = attachments.filter( ':focus' ).index(), 5742 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow ); 5743 5744 if ( index === -1 ) { 5745 return; 5746 } 5747 5748 // Left arrow 5749 if ( 37 === event.keyCode ) { 5750 if ( 0 === index ) { 5751 return; 5752 } 5753 attachments.eq( index - 1 ).focus(); 5754 } 5755 5756 // Up arrow 5757 if ( 38 === event.keyCode ) { 5758 if ( 1 === row ) { 5759 return; 5760 } 5761 attachments.eq( index - perRow ).focus(); 5762 } 5763 5764 // Right arrow 5765 if ( 39 === event.keyCode ) { 5766 if ( attachments.length === index ) { 5767 return; 5768 } 5769 attachments.eq( index + 1 ).focus(); 5770 } 5771 5772 // Down arrow 5773 if ( 40 === event.keyCode ) { 5774 if ( Math.ceil( attachments.length / perRow ) === row ) { 5775 return; 5776 } 5777 attachments.eq( index + perRow ).focus(); 5778 } 5779 }, 5780 5781 dispose: function() { 5782 this.collection.props.off( null, null, this ); 5783 if ( this.options.resize ) { 5784 this.$window.off( this.resizeEvent ); 5785 } 5786 5787 /** 5788 * call 'dispose' directly on the parent class 5789 */ 5790 media.View.prototype.dispose.apply( this, arguments ); 5791 }, 5792 5793 setColumns: function() { 5794 var prev = this.columns, 5795 width = this.$el.width(); 5796 5797 if ( width ) { 5798 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1; 5799 5800 if ( ! prev || prev !== this.columns ) { 5801 this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns ); 5802 } 5803 } 5804 }, 5805 5806 initSortable: function() { 5807 var collection = this.collection; 5808 5809 if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 5810 return; 5811 } 5812 5813 this.$el.sortable( _.extend({ 5814 // If the `collection` has a `comparator`, disable sorting. 5815 disabled: !! collection.comparator, 5816 5817 // Change the position of the attachment as soon as the 5818 // mouse pointer overlaps a thumbnail. 5819 tolerance: 'pointer', 5820 5821 // Record the initial `index` of the dragged model. 5822 start: function( event, ui ) { 5823 ui.item.data('sortableIndexStart', ui.item.index()); 5824 }, 5825 5826 // Update the model's index in the collection. 5827 // Do so silently, as the view is already accurate. 5828 update: function( event, ui ) { 5829 var model = collection.at( ui.item.data('sortableIndexStart') ), 5830 comparator = collection.comparator; 5831 5832 // Temporarily disable the comparator to prevent `add` 5833 // from re-sorting. 5834 delete collection.comparator; 5835 5836 // Silently shift the model to its new index. 5837 collection.remove( model, { 5838 silent: true 5839 }); 5840 collection.add( model, { 5841 silent: true, 5842 at: ui.item.index() 5843 }); 5844 5845 // Restore the comparator. 5846 collection.comparator = comparator; 5847 5848 // Fire the `reset` event to ensure other collections sync. 5849 collection.trigger( 'reset', collection ); 5850 5851 // If the collection is sorted by menu order, 5852 // update the menu order. 5853 collection.saveMenuOrder(); 5854 } 5855 }, this.options.sortable ) ); 5856 5857 // If the `orderby` property is changed on the `collection`, 5858 // check to see if we have a `comparator`. If so, disable sorting. 5859 collection.props.on( 'change:orderby', function() { 5860 this.$el.sortable( 'option', 'disabled', !! collection.comparator ); 5861 }, this ); 5862 5863 this.collection.props.on( 'change:orderby', this.refreshSortable, this ); 5864 this.refreshSortable(); 5865 }, 5866 5867 refreshSortable: function() { 5868 if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) { 5869 return; 5870 } 5871 5872 // If the `collection` has a `comparator`, disable sorting. 5873 var collection = this.collection, 5874 orderby = collection.props.get('orderby'), 5875 enabled = 'menuOrder' === orderby || ! collection.comparator; 5876 5877 this.$el.sortable( 'option', 'disabled', ! enabled ); 5878 }, 5879 5880 /** 5881 * @param {wp.media.model.Attachment} attachment 5882 * @returns {wp.media.View} 5883 */ 5884 createAttachmentView: function( attachment ) { 5885 var view = new this.options.AttachmentView({ 5886 controller: this.controller, 5887 model: attachment, 5888 collection: this.collection, 5889 selection: this.options.selection 5890 }); 5891 5892 return this._viewsByCid[ attachment.cid ] = view; 5893 }, 5894 5895 prepare: function() { 5896 // Create all of the Attachment views, and replace 5897 // the list in a single DOM operation. 5898 if ( this.collection.length ) { 5899 this.views.set( this.collection.map( this.createAttachmentView, this ) ); 5900 5901 // If there are no elements, clear the views and load some. 5902 } else { 5903 this.views.unset(); 5904 this.collection.more().done( this.scroll ); 5905 } 5906 }, 5907 5908 ready: function() { 5909 // Trigger the scroll event to check if we're within the 5910 // threshold to query for additional attachments. 5911 this.scroll(); 5912 }, 5913 5914 scroll: function() { 5915 var view = this, 5916 el = this.options.scrollElement, 5917 scrollTop = el.scrollTop, 5918 toolbar; 5919 5920 // The scroll event occurs on the document, but the element 5921 // that should be checked is the document body. 5922 if ( el == document ) { 5923 el = document.body; 5924 scrollTop = $(document).scrollTop(); 5925 } 5926 5927 if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) { 5928 return; 5929 } 5930 5931 toolbar = this.views.parent.toolbar; 5932 5933 // Show the spinner only if we are close to the bottom. 5934 if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) { 5935 toolbar.get('spinner').show(); 5936 } 5937 5938 if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) { 5939 this.collection.more().done(function() { 5940 view.scroll(); 5941 toolbar.get('spinner').hide(); 5942 }); 5943 } 5944 } 5945 }); 5946 5947 /** 5948 * wp.media.view.Search 5949 * 5950 * @class 5951 * @augments wp.media.View 5952 * @augments wp.Backbone.View 5953 * @augments Backbone.View 5954 */ 5955 media.view.Search = media.View.extend({ 5956 tagName: 'input', 5957 className: 'search', 5958 id: 'media-search-input', 5959 5960 attributes: { 5961 type: 'search', 5962 placeholder: l10n.search 5963 }, 5964 5965 events: { 5966 'input': 'search', 5967 'keyup': 'search', 5968 'change': 'search', 5969 'search': 'search' 5970 }, 5971 5972 /** 5973 * @returns {wp.media.view.Search} Returns itself to allow chaining 5974 */ 5975 render: function() { 5976 this.el.value = this.model.escape('search'); 5977 return this; 5978 }, 5979 5980 search: function( event ) { 5981 if ( event.target.value ) { 5982 this.model.set( 'search', event.target.value ); 5983 } else { 5984 this.model.unset('search'); 5985 } 5986 } 5987 }); 5988 5989 /** 5990 * wp.media.view.AttachmentFilters 5991 * 5992 * @class 5993 * @augments wp.media.View 5994 * @augments wp.Backbone.View 5995 * @augments Backbone.View 5996 */ 5997 media.view.AttachmentFilters = media.View.extend({ 5998 tagName: 'select', 5999 className: 'attachment-filters', 6000 id: 'media-attachment-filters', 6001 6002 events: { 6003 change: 'change' 6004 }, 6005 6006 keys: [], 6007 6008 initialize: function() { 6009 this.createFilters(); 6010 _.extend( this.filters, this.options.filters ); 6011 6012 // Build `<option>` elements. 6013 this.$el.html( _.chain( this.filters ).map( function( filter, value ) { 6014 return { 6015 el: $( '<option></option>' ).val( value ).html( filter.text )[0], 6016 priority: filter.priority || 50 6017 }; 6018 }, this ).sortBy('priority').pluck('el').value() ); 6019 6020 this.listenTo( this.model, 'change', this.select ); 6021 this.select(); 6022 }, 6023 6024 /** 6025 * @abstract 6026 */ 6027 createFilters: function() { 6028 this.filters = {}; 6029 }, 6030 6031 /** 6032 * When the selected filter changes, update the Attachment Query properties to match. 6033 */ 6034 change: function() { 6035 var filter = this.filters[ this.el.value ]; 6036 if ( filter ) { 6037 this.model.set( filter.props ); 6038 } 6039 }, 6040 6041 select: function() { 6042 var model = this.model, 6043 value = 'all', 6044 props = model.toJSON(); 6045 6046 _.find( this.filters, function( filter, id ) { 6047 var equal = _.all( filter.props, function( prop, key ) { 6048 return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] ); 6049 }); 6050 6051 if ( equal ) { 6052 return value = id; 6053 } 6054 }); 6055 6056 this.$el.val( value ); 6057 } 6058 }); 6059 6060 /** 6061 * A filter dropdown for month/dates. 6062 * 6063 * @class 6064 * @augments wp.media.view.AttachmentFilters 6065 * @augments wp.media.View 6066 * @augments wp.Backbone.View 6067 * @augments Backbone.View 6068 */ 6069 media.view.DateFilter = media.view.AttachmentFilters.extend({ 6070 id: 'media-attachment-date-filters', 6071 6072 createFilters: function() { 6073 var filters = {}; 6074 _.each( media.view.settings.months || {}, function( value, index ) { 6075 filters[ index ] = { 6076 text: value.text, 6077 props: { 6078 year: value.year, 6079 monthnum: value.month 6080 } 6081 }; 6082 }); 6083 filters.all = { 6084 text: l10n.allDates, 6085 props: { 6086 monthnum: false, 6087 year: false 6088 }, 6089 priority: 10 6090 }; 6091 this.filters = filters; 6092 } 6093 }); 6094 6095 /** 6096 * wp.media.view.AttachmentFilters.Uploaded 6097 * 6098 * @class 6099 * @augments wp.media.view.AttachmentFilters 6100 * @augments wp.media.View 6101 * @augments wp.Backbone.View 6102 * @augments Backbone.View 6103 */ 6104 media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({ 6105 createFilters: function() { 6106 var type = this.model.get('type'), 6107 types = media.view.settings.mimeTypes, 6108 text; 6109 6110 if ( types && type ) { 6111 text = types[ type ]; 6112 } 6113 6114 this.filters = { 6115 all: { 6116 text: text || l10n.allMediaItems, 6117 props: { 6118 uploadedTo: null, 6119 orderby: 'date', 6120 order: 'DESC' 6121 }, 6122 priority: 10 6123 }, 6124 6125 uploaded: { 6126 text: l10n.uploadedToThisPost, 6127 props: { 6128 uploadedTo: media.view.settings.post.id, 6129 orderby: 'menuOrder', 6130 order: 'ASC' 6131 }, 6132 priority: 20 6133 }, 6134 6135 unattached: { 6136 text: l10n.unattached, 6137 props: { 6138 uploadedTo: 0, 6139 orderby: 'menuOrder', 6140 order: 'ASC' 6141 }, 6142 priority: 50 6143 } 6144 }; 6145 } 6146 }); 6147 6148 /** 6149 * wp.media.view.AttachmentFilters.All 6150 * 6151 * @class 6152 * @augments wp.media.view.AttachmentFilters 6153 * @augments wp.media.View 6154 * @augments wp.Backbone.View 6155 * @augments Backbone.View 6156 */ 6157 media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({ 6158 createFilters: function() { 6159 var filters = {}; 6160 6161 _.each( media.view.settings.mimeTypes || {}, function( text, key ) { 6162 filters[ key ] = { 6163 text: text, 6164 props: { 6165 status: null, 6166 type: key, 6167 uploadedTo: null, 6168 orderby: 'date', 6169 order: 'DESC' 6170 } 6171 }; 6172 }); 6173 6174 filters.all = { 6175 text: l10n.allMediaItems, 6176 props: { 6177 status: null, 6178 type: null, 6179 uploadedTo: null, 6180 orderby: 'date', 6181 order: 'DESC' 6182 }, 6183 priority: 10 6184 }; 6185 6186 if ( media.view.settings.post.id ) { 6187 filters.uploaded = { 6188 text: l10n.uploadedToThisPost, 6189 props: { 6190 status: null, 6191 type: null, 6192 uploadedTo: media.view.settings.post.id, 6193 orderby: 'menuOrder', 6194 order: 'ASC' 6195 }, 6196 priority: 20 6197 }; 6198 } 6199 6200 filters.unattached = { 6201 text: l10n.unattached, 6202 props: { 6203 status: null, 6204 uploadedTo: 0, 6205 type: null, 6206 orderby: 'menuOrder', 6207 order: 'ASC' 6208 }, 6209 priority: 50 6210 }; 6211 6212 if ( media.view.settings.mediaTrash && 6213 this.controller.isModeActive( 'grid' ) ) { 6214 6215 filters.trash = { 6216 text: l10n.trash, 6217 props: { 6218 uploadedTo: null, 6219 status: 'trash', 6220 type: null, 6221 orderby: 'date', 6222 order: 'DESC' 6223 }, 6224 priority: 50 6225 }; 6226 } 6227 6228 this.filters = filters; 6229 } 6230 }); 6231 6232 /** 6233 * wp.media.view.AttachmentsBrowser 6234 * 6235 * @class 6236 * @augments wp.media.View 6237 * @augments wp.Backbone.View 6238 * @augments Backbone.View 6239 * 6240 * @param {object} options 6241 * @param {object} [options.filters=false] Which filters to show in the browser's toolbar. 6242 * Accepts 'uploaded' and 'all'. 6243 * @param {object} [options.search=true] Whether to show the search interface in the 6244 * browser's toolbar. 6245 * @param {object} [options.date=true] Whether to show the date filter in the 6246 * browser's toolbar. 6247 * @param {object} [options.display=false] Whether to show the attachments display settings 6248 * view in the sidebar. 6249 * @param {bool|string} [options.sidebar=true] Whether to create a sidebar for the browser. 6250 * Accepts true, false, and 'errors'. 6251 */ 6252 media.view.AttachmentsBrowser = media.View.extend({ 6253 tagName: 'div', 6254 className: 'attachments-browser', 6255 6256 initialize: function() { 6257 _.defaults( this.options, { 6258 filters: false, 6259 search: true, 6260 date: true, 6261 display: false, 6262 sidebar: true, 6263 AttachmentView: media.view.Attachment.Library 6264 }); 6265 6266 this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) ); 6267 this.controller.on( 'edit:selection', this.editSelection ); 6268 this.createToolbar(); 6269 if ( this.options.sidebar ) { 6270 this.createSidebar(); 6271 } 6272 this.createUploader(); 6273 this.createAttachments(); 6274 this.updateContent(); 6275 6276 if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) { 6277 this.$el.addClass( 'hide-sidebar' ); 6278 6279 if ( 'errors' === this.options.sidebar ) { 6280 this.$el.addClass( 'sidebar-for-errors' ); 6281 } 6282 } 6283 6284 this.collection.on( 'add remove reset', this.updateContent, this ); 6285 }, 6286 6287 editSelection: function( modal ) { 6288 modal.$( '.media-button-backToLibrary' ).focus(); 6289 }, 6290 6291 /** 6292 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining 6293 */ 6294 dispose: function() { 6295 this.options.selection.off( null, null, this ); 6296 media.View.prototype.dispose.apply( this, arguments ); 6297 return this; 6298 }, 6299 6300 createToolbar: function() { 6301 var LibraryViewSwitcher, Filters, toolbarOptions; 6302 6303 toolbarOptions = { 6304 controller: this.controller 6305 }; 6306 6307 if ( this.controller.isModeActive( 'grid' ) ) { 6308 toolbarOptions.className = 'media-toolbar wp-filter'; 6309 } 6310 6311 /** 6312 * @member {wp.media.view.Toolbar} 6313 */ 6314 this.toolbar = new media.view.Toolbar( toolbarOptions ); 6315 6316 this.views.add( this.toolbar ); 6317 6318 this.toolbar.set( 'spinner', new media.view.Spinner({ 6319 priority: -60 6320 }) ); 6321 6322 if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) { 6323 // "Filters" will return a <select>, need to render 6324 // screen reader text before 6325 this.toolbar.set( 'filtersLabel', new media.view.Label({ 6326 value: l10n.filterByType, 6327 attributes: { 6328 'for': 'media-attachment-filters' 6329 }, 6330 priority: -80 6331 }).render() ); 6332 6333 if ( 'uploaded' === this.options.filters ) { 6334 this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({ 6335 controller: this.controller, 6336 model: this.collection.props, 6337 priority: -80 6338 }).render() ); 6339 } else { 6340 Filters = new media.view.AttachmentFilters.All({ 6341 controller: this.controller, 6342 model: this.collection.props, 6343 priority: -80 6344 }); 6345 6346 this.toolbar.set( 'filters', Filters.render() ); 6347 } 6348 } 6349 6350 // Feels odd to bring the global media library switcher into the Attachment 6351 // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar ); 6352 // which the controller can tap into and add this view? 6353 if ( this.controller.isModeActive( 'grid' ) ) { 6354 LibraryViewSwitcher = media.View.extend({ 6355 className: 'view-switch media-grid-view-switch', 6356 template: media.template( 'media-library-view-switcher') 6357 }); 6358 6359 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({ 6360 controller: this.controller, 6361 priority: -90 6362 }).render() ); 6363 6364 // DateFilter is a <select>, screen reader text needs to be rendered before 6365 this.toolbar.set( 'dateFilterLabel', new media.view.Label({ 6366 value: l10n.filterByDate, 6367 attributes: { 6368 'for': 'media-attachment-date-filters' 6369 }, 6370 priority: -75 6371 }).render() ); 6372 this.toolbar.set( 'dateFilter', new media.view.DateFilter({ 6373 controller: this.controller, 6374 model: this.collection.props, 6375 priority: -75 6376 }).render() ); 6377 6378 // BulkSelection is a <div> with subviews, including screen reader text 6379 this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({ 6380 text: l10n.bulkSelect, 6381 controller: this.controller, 6382 priority: -70 6383 }).render() ); 6384 6385 this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({ 6386 filters: Filters, 6387 style: 'primary', 6388 disabled: true, 6389 text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected, 6390 controller: this.controller, 6391 priority: -60, 6392 click: function() { 6393 var changed = [], removed = [], self = this, 6394 selection = this.controller.state().get( 'selection' ), 6395 library = this.controller.state().get( 'library' ); 6396 6397 if ( ! selection.length ) { 6398 return; 6399 } 6400 6401 if ( ! media.view.settings.mediaTrash && ! confirm( l10n.warnBulkDelete ) ) { 6402 return; 6403 } 6404 6405 if ( media.view.settings.mediaTrash && 6406 'trash' !== selection.at( 0 ).get( 'status' ) && 6407 ! confirm( l10n.warnBulkTrash ) ) { 6408 6409 return; 6410 } 6411 6412 selection.each( function( model ) { 6413 if ( ! model.get( 'nonces' )['delete'] ) { 6414 removed.push( model ); 6415 return; 6416 } 6417 6418 if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) { 6419 model.set( 'status', 'inherit' ); 6420 changed.push( model.save() ); 6421 removed.push( model ); 6422 } else if ( media.view.settings.mediaTrash ) { 6423 model.set( 'status', 'trash' ); 6424 changed.push( model.save() ); 6425 removed.push( model ); 6426 } else { 6427 model.destroy({wait: true}); 6428 } 6429 } ); 6430 6431 if ( changed.length ) { 6432 selection.remove( removed ); 6433 6434 $.when.apply( null, changed ).then( function() { 6435 library._requery( true ); 6436 self.controller.trigger( 'selection:action:done' ); 6437 } ); 6438 } else { 6439 this.controller.trigger( 'selection:action:done' ); 6440 } 6441 } 6442 }).render() ); 6443 6444 if ( media.view.settings.mediaTrash ) { 6445 this.toolbar.set( 'deleteSelectedPermanentlyButton', new media.view.DeleteSelectedPermanentlyButton({ 6446 filters: Filters, 6447 style: 'primary', 6448 disabled: true, 6449 text: l10n.deleteSelected, 6450 controller: this.controller, 6451 priority: -55, 6452 click: function() { 6453 var removed = [], selection = this.controller.state().get( 'selection' ); 6454 6455 if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) { 6456 return; 6457 } 6458 6459 selection.each( function( model ) { 6460 if ( ! model.get( 'nonces' )['delete'] ) { 6461 removed.push( model ); 6462 return; 6463 } 6464 6465 model.destroy(); 6466 } ); 6467 6468 selection.remove( removed ); 6469 this.controller.trigger( 'selection:action:done' ); 6470 } 6471 }).render() ); 6472 } 6473 6474 } else if ( this.options.date ) { 6475 // DateFilter is a <select>, screen reader text needs to be rendered before 6476 this.toolbar.set( 'dateFilterLabel', new media.view.Label({ 6477 value: l10n.filterByDate, 6478 attributes: { 6479 'for': 'media-attachment-date-filters' 6480 }, 6481 priority: -75 6482 }).render() ); 6483 this.toolbar.set( 'dateFilter', new media.view.DateFilter({ 6484 controller: this.controller, 6485 model: this.collection.props, 6486 priority: -75 6487 }).render() ); 6488 } 6489 6490 if ( this.options.search ) { 6491 // Search is an input, screen reader text needs to be rendered before 6492 this.toolbar.set( 'searchLabel', new media.view.Label({ 6493 value: l10n.searchMediaLabel, 6494 attributes: { 6495 'for': 'media-search-input' 6496 }, 6497 priority: 60 6498 }).render() ); 6499 this.toolbar.set( 'search', new media.view.Search({ 6500 controller: this.controller, 6501 model: this.collection.props, 6502 priority: 60 6503 }).render() ); 6504 } 6505 6506 if ( this.options.dragInfo ) { 6507 this.toolbar.set( 'dragInfo', new media.View({ 6508 el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0], 6509 priority: -40 6510 }) ); 6511 } 6512 6513 if ( this.options.suggestedWidth && this.options.suggestedHeight ) { 6514 this.toolbar.set( 'suggestedDimensions', new media.View({ 6515 el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' × ' + this.options.suggestedHeight + '</div>' )[0], 6516 priority: -40 6517 }) ); 6518 } 6519 }, 6520 6521 updateContent: function() { 6522 var view = this, 6523 noItemsView; 6524 6525 if ( this.controller.isModeActive( 'grid' ) ) { 6526 noItemsView = view.attachmentsNoResults; 6527 } else { 6528 noItemsView = view.uploader; 6529 } 6530 6531 if ( ! this.collection.length ) { 6532 this.toolbar.get( 'spinner' ).show(); 6533 this.dfd = this.collection.more().done( function() { 6534 if ( ! view.collection.length ) { 6535 noItemsView.$el.removeClass( 'hidden' ); 6536 } else { 6537 noItemsView.$el.addClass( 'hidden' ); 6538 } 6539 view.toolbar.get( 'spinner' ).hide(); 6540 } ); 6541 } else { 6542 noItemsView.$el.addClass( 'hidden' ); 6543 view.toolbar.get( 'spinner' ).hide(); 6544 } 6545 }, 6546 6547 createUploader: function() { 6548 this.uploader = new media.view.UploaderInline({ 6549 controller: this.controller, 6550 status: false, 6551 message: this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound, 6552 canClose: this.controller.isModeActive( 'grid' ) 6553 }); 6554 6555 this.uploader.hide(); 6556 this.views.add( this.uploader ); 6557 }, 6558 6559 toggleUploader: function() { 6560 if ( this.uploader.$el.hasClass( 'hidden' ) ) { 6561 this.uploader.show(); 6562 } else { 6563 this.uploader.hide(); 6564 } 6565 }, 6566 6567 createAttachments: function() { 6568 this.attachments = new media.view.Attachments({ 6569 controller: this.controller, 6570 collection: this.collection, 6571 selection: this.options.selection, 6572 model: this.model, 6573 sortable: this.options.sortable, 6574 scrollElement: this.options.scrollElement, 6575 idealColumnWidth: this.options.idealColumnWidth, 6576 6577 // The single `Attachment` view to be used in the `Attachments` view. 6578 AttachmentView: this.options.AttachmentView 6579 }); 6580 6581 // Add keydown listener to the instance of the Attachments view 6582 this.attachments.listenTo( this.controller, 'attachment:keydown:arrow', this.attachments.arrowEvent ); 6583 this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus ); 6584 6585 this.views.add( this.attachments ); 6586 6587 6588 if ( this.controller.isModeActive( 'grid' ) ) { 6589 this.attachmentsNoResults = new media.View({ 6590 controller: this.controller, 6591 tagName: 'p' 6592 }); 6593 6594 this.attachmentsNoResults.$el.addClass( 'hidden no-media' ); 6595 this.attachmentsNoResults.$el.html( l10n.noMedia ); 6596 6597 this.views.add( this.attachmentsNoResults ); 6598 } 6599 }, 6600 6601 createSidebar: function() { 6602 var options = this.options, 6603 selection = options.selection, 6604 sidebar = this.sidebar = new media.view.Sidebar({ 6605 controller: this.controller 6606 }); 6607 6608 this.views.add( sidebar ); 6609 6610 if ( this.controller.uploader ) { 6611 sidebar.set( 'uploads', new media.view.UploaderStatus({ 6612 controller: this.controller, 6613 priority: 40 6614 }) ); 6615 } 6616 6617 selection.on( 'selection:single', this.createSingle, this ); 6618 selection.on( 'selection:unsingle', this.disposeSingle, this ); 6619 6620 if ( selection.single() ) { 6621 this.createSingle(); 6622 } 6623 }, 6624 6625 createSingle: function() { 6626 var sidebar = this.sidebar, 6627 single = this.options.selection.single(); 6628 6629 sidebar.set( 'details', new media.view.Attachment.Details({ 6630 controller: this.controller, 6631 model: single, 6632 priority: 80 6633 }) ); 6634 6635 sidebar.set( 'compat', new media.view.AttachmentCompat({ 6636 controller: this.controller, 6637 model: single, 6638 priority: 120 6639 }) ); 6640 6641 if ( this.options.display ) { 6642 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({ 6643 controller: this.controller, 6644 model: this.model.display( single ), 6645 attachment: single, 6646 priority: 160, 6647 userSettings: this.model.get('displayUserSettings') 6648 }) ); 6649 } 6650 6651 // Show the sidebar on mobile 6652 if ( this.model.id === 'insert' ) { 6653 sidebar.$el.addClass( 'visible' ); 6654 } 6655 }, 6656 6657 disposeSingle: function() { 6658 var sidebar = this.sidebar; 6659 sidebar.unset('details'); 6660 sidebar.unset('compat'); 6661 sidebar.unset('display'); 6662 // Hide the sidebar on mobile 6663 sidebar.$el.removeClass( 'visible' ); 6664 } 6665 }); 6666 6667 /** 6668 * wp.media.view.Selection 6669 * 6670 * @class 6671 * @augments wp.media.View 6672 * @augments wp.Backbone.View 6673 * @augments Backbone.View 6674 */ 6675 media.view.Selection = media.View.extend({ 6676 tagName: 'div', 6677 className: 'media-selection', 6678 template: media.template('media-selection'), 6679 6680 events: { 6681 'click .edit-selection': 'edit', 6682 'click .clear-selection': 'clear' 6683 }, 6684 6685 initialize: function() { 6686 _.defaults( this.options, { 6687 editable: false, 6688 clearable: true 6689 }); 6690 6691 /** 6692 * @member {wp.media.view.Attachments.Selection} 6693 */ 6694 this.attachments = new media.view.Attachments.Selection({ 6695 controller: this.controller, 6696 collection: this.collection, 6697 selection: this.collection, 6698 model: new Backbone.Model() 6699 }); 6700 6701 this.views.set( '.selection-view', this.attachments ); 6702 this.collection.on( 'add remove reset', this.refresh, this ); 6703 this.controller.on( 'content:activate', this.refresh, this ); 6704 }, 6705 6706 ready: function() { 6707 this.refresh(); 6708 }, 6709 6710 refresh: function() { 6711 // If the selection hasn't been rendered, bail. 6712 if ( ! this.$el.children().length ) { 6713 return; 6714 } 6715 6716 var collection = this.collection, 6717 editing = 'edit-selection' === this.controller.content.mode(); 6718 6719 // If nothing is selected, display nothing. 6720 this.$el.toggleClass( 'empty', ! collection.length ); 6721 this.$el.toggleClass( 'one', 1 === collection.length ); 6722 this.$el.toggleClass( 'editing', editing ); 6723 6724 this.$('.count').text( l10n.selected.replace('%d', collection.length) ); 6725 }, 6726 6727 edit: function( event ) { 6728 event.preventDefault(); 6729 if ( this.options.editable ) { 6730 this.options.editable.call( this, this.collection ); 6731 } 6732 }, 6733 6734 clear: function( event ) { 6735 event.preventDefault(); 6736 this.collection.reset(); 6737 6738 // Keep focus inside media modal 6739 // after clear link is selected 6740 this.controller.modal.focusManager.focus(); 6741 } 6742 }); 6743 6744 6745 /** 6746 * wp.media.view.Attachment.Selection 6747 * 6748 * @class 6749 * @augments wp.media.view.Attachment 6750 * @augments wp.media.View 6751 * @augments wp.Backbone.View 6752 * @augments Backbone.View 6753 */ 6754 media.view.Attachment.Selection = media.view.Attachment.extend({ 6755 className: 'attachment selection', 6756 6757 // On click, just select the model, instead of removing the model from 6758 // the selection. 6759 toggleSelection: function() { 6760 this.options.selection.single( this.model ); 6761 } 6762 }); 6763 6764 /** 6765 * wp.media.view.Attachments.Selection 6766 * 6767 * @class 6768 * @augments wp.media.view.Attachments 6769 * @augments wp.media.View 6770 * @augments wp.Backbone.View 6771 * @augments Backbone.View 6772 */ 6773 media.view.Attachments.Selection = media.view.Attachments.extend({ 6774 events: {}, 6775 initialize: function() { 6776 _.defaults( this.options, { 6777 sortable: false, 6778 resize: false, 6779 6780 // The single `Attachment` view to be used in the `Attachments` view. 6781 AttachmentView: media.view.Attachment.Selection 6782 }); 6783 // Call 'initialize' directly on the parent class. 6784 return media.view.Attachments.prototype.initialize.apply( this, arguments ); 6785 } 6786 }); 6787 6788 /** 6789 * wp.media.view.Attachments.EditSelection 6790 * 6791 * @class 6792 * @augments wp.media.view.Attachment.Selection 6793 * @augments wp.media.view.Attachment 6794 * @augments wp.media.View 6795 * @augments wp.Backbone.View 6796 * @augments Backbone.View 6797 */ 6798 media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({ 6799 buttons: { 6800 close: true 6801 } 6802 }); 6803 6804 6805 /** 6806 * wp.media.view.Settings 6807 * 6808 * @class 6809 * @augments wp.media.View 6810 * @augments wp.Backbone.View 6811 * @augments Backbone.View 6812 */ 6813 media.view.Settings = media.View.extend({ 6814 events: { 6815 'click button': 'updateHandler', 6816 'change input': 'updateHandler', 6817 'change select': 'updateHandler', 6818 'change textarea': 'updateHandler' 6819 }, 6820 6821 initialize: function() { 6822 this.model = this.model || new Backbone.Model(); 6823 this.listenTo( this.model, 'change', this.updateChanges ); 6824 }, 6825 6826 prepare: function() { 6827 return _.defaults({ 6828 model: this.model.toJSON() 6829 }, this.options ); 6830 }, 6831 /** 6832 * @returns {wp.media.view.Settings} Returns itself to allow chaining 6833 */ 6834 render: function() { 6835 media.View.prototype.render.apply( this, arguments ); 6836 // Select the correct values. 6837 _( this.model.attributes ).chain().keys().each( this.update, this ); 6838 return this; 6839 }, 6840 /** 6841 * @param {string} key 6842 */ 6843 update: function( key ) { 6844 var value = this.model.get( key ), 6845 $setting = this.$('[data-setting="' + key + '"]'), 6846 $buttons, $value; 6847 6848 // Bail if we didn't find a matching setting. 6849 if ( ! $setting.length ) { 6850 return; 6851 } 6852 6853 // Attempt to determine how the setting is rendered and update 6854 // the selected value. 6855 6856 // Handle dropdowns. 6857 if ( $setting.is('select') ) { 6858 $value = $setting.find('[value="' + value + '"]'); 6859 6860 if ( $value.length ) { 6861 $setting.find('option').prop( 'selected', false ); 6862 $value.prop( 'selected', true ); 6863 } else { 6864 // If we can't find the desired value, record what *is* selected. 6865 this.model.set( key, $setting.find(':selected').val() ); 6866 } 6867 6868 // Handle button groups. 6869 } else if ( $setting.hasClass('button-group') ) { 6870 $buttons = $setting.find('button').removeClass('active'); 6871 $buttons.filter( '[value="' + value + '"]' ).addClass('active'); 6872 6873 // Handle text inputs and textareas. 6874 } else if ( $setting.is('input[type="text"], textarea') ) { 6875 if ( ! $setting.is(':focus') ) { 6876 $setting.val( value ); 6877 } 6878 // Handle checkboxes. 6879 } else if ( $setting.is('input[type="checkbox"]') ) { 6880 $setting.prop( 'checked', !! value && 'false' !== value ); 6881 } 6882 }, 6883 /** 6884 * @param {Object} event 6885 */ 6886 updateHandler: function( event ) { 6887 var $setting = $( event.target ).closest('[data-setting]'), 6888 value = event.target.value, 6889 userSetting; 6890 6891 event.preventDefault(); 6892 6893 if ( ! $setting.length ) { 6894 return; 6895 } 6896 6897 // Use the correct value for checkboxes. 6898 if ( $setting.is('input[type="checkbox"]') ) { 6899 value = $setting[0].checked; 6900 } 6901 6902 // Update the corresponding setting. 6903 this.model.set( $setting.data('setting'), value ); 6904 6905 // If the setting has a corresponding user setting, 6906 // update that as well. 6907 if ( userSetting = $setting.data('userSetting') ) { 6908 setUserSetting( userSetting, value ); 6909 } 6910 }, 6911 6912 updateChanges: function( model ) { 6913 if ( model.hasChanged() ) { 6914 _( model.changed ).chain().keys().each( this.update, this ); 6915 } 6916 } 6917 }); 6918 6919 /** 6920 * wp.media.view.Settings.AttachmentDisplay 6921 * 6922 * @class 6923 * @augments wp.media.view.Settings 6924 * @augments wp.media.View 6925 * @augments wp.Backbone.View 6926 * @augments Backbone.View 6927 */ 6928 media.view.Settings.AttachmentDisplay = media.view.Settings.extend({ 6929 className: 'attachment-display-settings', 6930 template: media.template('attachment-display-settings'), 6931 6932 initialize: function() { 6933 var attachment = this.options.attachment; 6934 6935 _.defaults( this.options, { 6936 userSettings: false 6937 }); 6938 // Call 'initialize' directly on the parent class. 6939 media.view.Settings.prototype.initialize.apply( this, arguments ); 6940 this.listenTo( this.model, 'change:link', this.updateLinkTo ); 6941 6942 if ( attachment ) { 6943 attachment.on( 'change:uploading', this.render, this ); 6944 } 6945 }, 6946 6947 dispose: function() { 6948 var attachment = this.options.attachment; 6949 if ( attachment ) { 6950 attachment.off( null, null, this ); 6951 } 6952 /** 6953 * call 'dispose' directly on the parent class 6954 */ 6955 media.view.Settings.prototype.dispose.apply( this, arguments ); 6956 }, 6957 /** 6958 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining 6959 */ 6960 render: function() { 6961 var attachment = this.options.attachment; 6962 if ( attachment ) { 6963 _.extend( this.options, { 6964 sizes: attachment.get('sizes'), 6965 type: attachment.get('type') 6966 }); 6967 } 6968 /** 6969 * call 'render' directly on the parent class 6970 */ 6971 media.view.Settings.prototype.render.call( this ); 6972 this.updateLinkTo(); 6973 return this; 6974 }, 6975 6976 updateLinkTo: function() { 6977 var linkTo = this.model.get('link'), 6978 $input = this.$('.link-to-custom'), 6979 attachment = this.options.attachment; 6980 6981 if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) { 6982 $input.addClass( 'hidden' ); 6983 return; 6984 } 6985 6986 if ( attachment ) { 6987 if ( 'post' === linkTo ) { 6988 $input.val( attachment.get('link') ); 6989 } else if ( 'file' === linkTo ) { 6990 $input.val( attachment.get('url') ); 6991 } else if ( ! this.model.get('linkUrl') ) { 6992 $input.val('http://'); 6993 } 6994 6995 $input.prop( 'readonly', 'custom' !== linkTo ); 6996 } 6997 6998 $input.removeClass( 'hidden' ); 6999 7000 // If the input is visible, focus and select its contents. 7001 if ( ! isTouchDevice && $input.is(':visible') ) { 7002 $input.focus()[0].select(); 7003 } 7004 } 7005 }); 7006 7007 /** 7008 * wp.media.view.Settings.Gallery 7009 * 7010 * @class 7011 * @augments wp.media.view.Settings 7012 * @augments wp.media.View 7013 * @augments wp.Backbone.View 7014 * @augments Backbone.View 7015 */ 7016 media.view.Settings.Gallery = media.view.Settings.extend({ 7017 className: 'collection-settings gallery-settings', 7018 template: media.template('gallery-settings') 7019 }); 7020 7021 /** 7022 * wp.media.view.Settings.Playlist 7023 * 7024 * @class 7025 * @augments wp.media.view.Settings 7026 * @augments wp.media.View 7027 * @augments wp.Backbone.View 7028 * @augments Backbone.View 7029 */ 7030 media.view.Settings.Playlist = media.view.Settings.extend({ 7031 className: 'collection-settings playlist-settings', 7032 template: media.template('playlist-settings') 7033 }); 7034 7035 /** 7036 * wp.media.view.Attachment.Details 7037 * 7038 * @class 7039 * @augments wp.media.view.Attachment 7040 * @augments wp.media.View 7041 * @augments wp.Backbone.View 7042 * @augments Backbone.View 7043 */ 7044 media.view.Attachment.Details = media.view.Attachment.extend({ 7045 tagName: 'div', 7046 className: 'attachment-details', 7047 template: media.template('attachment-details'), 7048 7049 attributes: function() { 7050 return { 7051 'tabIndex': 0, 7052 'data-id': this.model.get( 'id' ) 7053 }; 7054 }, 7055 7056 events: { 7057 'change [data-setting]': 'updateSetting', 7058 'change [data-setting] input': 'updateSetting', 7059 'change [data-setting] select': 'updateSetting', 7060 'change [data-setting] textarea': 'updateSetting', 7061 'click .delete-attachment': 'deleteAttachment', 7062 'click .trash-attachment': 'trashAttachment', 7063 'click .untrash-attachment': 'untrashAttachment', 7064 'click .edit-attachment': 'editAttachment', 7065 'click .refresh-attachment': 'refreshAttachment', 7066 'keydown': 'toggleSelectionHandler' 7067 }, 7068 7069 initialize: function() { 7070 this.options = _.defaults( this.options, { 7071 rerenderOnModelChange: false 7072 }); 7073 7074 this.on( 'ready', this.initialFocus ); 7075 // Call 'initialize' directly on the parent class. 7076 media.view.Attachment.prototype.initialize.apply( this, arguments ); 7077 }, 7078 7079 initialFocus: function() { 7080 if ( ! isTouchDevice ) { 7081 this.$( ':input' ).eq( 0 ).focus(); 7082 } 7083 }, 7084 /** 7085 * @param {Object} event 7086 */ 7087 deleteAttachment: function( event ) { 7088 event.preventDefault(); 7089 7090 if ( confirm( l10n.warnDelete ) ) { 7091 this.model.destroy(); 7092 // Keep focus inside media modal 7093 // after image is deleted 7094 this.controller.modal.focusManager.focus(); 7095 } 7096 }, 7097 /** 7098 * @param {Object} event 7099 */ 7100 trashAttachment: function( event ) { 7101 var library = this.controller.library; 7102 event.preventDefault(); 7103 7104 if ( media.view.settings.mediaTrash && 7105 'edit-metadata' === this.controller.content.mode() ) { 7106 7107 this.model.set( 'status', 'trash' ); 7108 this.model.save().done( function() { 7109 library._requery( true ); 7110 } ); 7111 } else { 7112 this.model.destroy(); 7113 } 7114 }, 7115 /** 7116 * @param {Object} event 7117 */ 7118 untrashAttachment: function( event ) { 7119 var library = this.controller.library; 7120 event.preventDefault(); 7121 7122 this.model.set( 'status', 'inherit' ); 7123 this.model.save().done( function() { 7124 library._requery( true ); 7125 } ); 7126 }, 7127 /** 7128 * @param {Object} event 7129 */ 7130 editAttachment: function( event ) { 7131 var editState = this.controller.states.get( 'edit-image' ); 7132 if ( window.imageEdit && editState ) { 7133 event.preventDefault(); 7134 7135 editState.set( 'image', this.model ); 7136 this.controller.setState( 'edit-image' ); 7137 } else { 7138 this.$el.addClass('needs-refresh'); 7139 } 7140 }, 7141 /** 7142 * @param {Object} event 7143 */ 7144 refreshAttachment: function( event ) { 7145 this.$el.removeClass('needs-refresh'); 7146 event.preventDefault(); 7147 this.model.fetch(); 7148 }, 7149 /** 7150 * When reverse tabbing(shift+tab) out of the right details panel, deliver 7151 * the focus to the item in the list that was being edited. 7152 * 7153 * @param {Object} event 7154 */ 7155 toggleSelectionHandler: function( event ) { 7156 if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) { 7157 this.controller.trigger( 'attachment:details:shift-tab', event ); 7158 return false; 7159 } 7160 7161 if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) { 7162 this.controller.trigger( 'attachment:keydown:arrow', event ); 7163 return; 7164 } 7165 } 7166 }); 7167 7168 /** 7169 * wp.media.view.AttachmentCompat 7170 * 7171 * A view to display fields added via the `attachment_fields_to_edit` filter. 7172 * 7173 * @class 7174 * @augments wp.media.View 7175 * @augments wp.Backbone.View 7176 * @augments Backbone.View 7177 */ 7178 media.view.AttachmentCompat = media.View.extend({ 7179 tagName: 'form', 7180 className: 'compat-item', 7181 7182 events: { 7183 'submit': 'preventDefault', 7184 'change input': 'save', 7185 'change select': 'save', 7186 'change textarea': 'save' 7187 }, 7188 7189 initialize: function() { 7190 this.listenTo( this.model, 'change:compat', this.render ); 7191 }, 7192 /** 7193 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 7194 */ 7195 dispose: function() { 7196 if ( this.$(':focus').length ) { 7197 this.save(); 7198 } 7199 /** 7200 * call 'dispose' directly on the parent class 7201 */ 7202 return media.View.prototype.dispose.apply( this, arguments ); 7203 }, 7204 /** 7205 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining 7206 */ 7207 render: function() { 7208 var compat = this.model.get('compat'); 7209 if ( ! compat || ! compat.item ) { 7210 return; 7211 } 7212 7213 this.views.detach(); 7214 this.$el.html( compat.item ); 7215 this.views.render(); 7216 return this; 7217 }, 7218 /** 7219 * @param {Object} event 7220 */ 7221 preventDefault: function( event ) { 7222 event.preventDefault(); 7223 }, 7224 /** 7225 * @param {Object} event 7226 */ 7227 save: function( event ) { 7228 var data = {}; 7229 7230 if ( event ) { 7231 event.preventDefault(); 7232 } 7233 7234 _.each( this.$el.serializeArray(), function( pair ) { 7235 data[ pair.name ] = pair.value; 7236 }); 7237 7238 this.controller.trigger( 'attachment:compat:waiting', ['waiting'] ); 7239 this.model.saveCompat( data ).always( _.bind( this.postSave, this ) ); 7240 }, 7241 7242 postSave: function() { 7243 this.controller.trigger( 'attachment:compat:ready', ['ready'] ); 7244 } 7245 }); 7246 7247 /** 7248 * wp.media.view.Iframe 7249 * 7250 * @class 7251 * @augments wp.media.View 7252 * @augments wp.Backbone.View 7253 * @augments Backbone.View 7254 */ 7255 media.view.Iframe = media.View.extend({ 7256 className: 'media-iframe', 7257 /** 7258 * @returns {wp.media.view.Iframe} Returns itself to allow chaining 7259 */ 7260 render: function() { 7261 this.views.detach(); 7262 this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' ); 7263 this.views.render(); 7264 return this; 7265 } 7266 }); 7267 7268 /** 7269 * wp.media.view.Embed 7270 * 7271 * @class 7272 * @augments wp.media.View 7273 * @augments wp.Backbone.View 7274 * @augments Backbone.View 7275 */ 7276 media.view.Embed = media.View.extend({ 7277 className: 'media-embed', 7278 7279 initialize: function() { 7280 /** 7281 * @member {wp.media.view.EmbedUrl} 7282 */ 7283 this.url = new media.view.EmbedUrl({ 7284 controller: this.controller, 7285 model: this.model.props 7286 }).render(); 7287 7288 this.views.set([ this.url ]); 7289 this.refresh(); 7290 this.listenTo( this.model, 'change:type', this.refresh ); 7291 this.listenTo( this.model, 'change:loading', this.loading ); 7292 }, 7293 7294 /** 7295 * @param {Object} view 7296 */ 7297 settings: function( view ) { 7298 if ( this._settings ) { 7299 this._settings.remove(); 7300 } 7301 this._settings = view; 7302 this.views.add( view ); 7303 }, 7304 7305 refresh: function() { 7306 var type = this.model.get('type'), 7307 constructor; 7308 7309 if ( 'image' === type ) { 7310 constructor = media.view.EmbedImage; 7311 } else if ( 'link' === type ) { 7312 constructor = media.view.EmbedLink; 7313 } else { 7314 return; 7315 } 7316 7317 this.settings( new constructor({ 7318 controller: this.controller, 7319 model: this.model.props, 7320 priority: 40 7321 }) ); 7322 }, 7323 7324 loading: function() { 7325 this.$el.toggleClass( 'embed-loading', this.model.get('loading') ); 7326 } 7327 }); 7328 7329 /** 7330 * @class 7331 * @augments wp.media.View 7332 * @augments wp.Backbone.View 7333 * @augments Backbone.View 7334 */ 7335 media.view.Label = media.View.extend({ 7336 tagName: 'label', 7337 className: 'screen-reader-text', 7338 7339 initialize: function() { 7340 this.value = this.options.value; 7341 }, 7342 7343 render: function() { 7344 this.$el.html( this.value ); 7345 7346 return this; 7347 } 7348 }); 7349 7350 /** 7351 * wp.media.view.EmbedUrl 7352 * 7353 * @class 7354 * @augments wp.media.View 7355 * @augments wp.Backbone.View 7356 * @augments Backbone.View 7357 */ 7358 media.view.EmbedUrl = media.View.extend({ 7359 tagName: 'label', 7360 className: 'embed-url', 7361 7362 events: { 7363 'input': 'url', 7364 'keyup': 'url', 7365 'change': 'url' 7366 }, 7367 7368 initialize: function() { 7369 var self = this; 7370 7371 this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') ); 7372 this.input = this.$input[0]; 7373 7374 this.spinner = $('<span class="spinner" />')[0]; 7375 this.$el.append([ this.input, this.spinner ]); 7376 7377 this.listenTo( this.model, 'change:url', this.render ); 7378 7379 if ( this.model.get( 'url' ) ) { 7380 _.delay( function () { 7381 self.model.trigger( 'change:url' ); 7382 }, 500 ); 7383 } 7384 }, 7385 /** 7386 * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining 7387 */ 7388 render: function() { 7389 var $input = this.$input; 7390 7391 if ( $input.is(':focus') ) { 7392 return; 7393 } 7394 7395 this.input.value = this.model.get('url') || 'http://'; 7396 /** 7397 * Call `render` directly on parent class with passed arguments 7398 */ 7399 media.View.prototype.render.apply( this, arguments ); 7400 return this; 7401 }, 7402 7403 ready: function() { 7404 if ( ! isTouchDevice ) { 7405 this.focus(); 7406 } 7407 }, 7408 7409 url: function( event ) { 7410 this.model.set( 'url', event.target.value ); 7411 }, 7412 7413 /** 7414 * If the input is visible, focus and select its contents. 7415 */ 7416 focus: function() { 7417 var $input = this.$input; 7418 if ( $input.is(':visible') ) { 7419 $input.focus()[0].select(); 7420 } 7421 } 7422 }); 7423 7424 /** 7425 * wp.media.view.EmbedLink 7426 * 7427 * @class 7428 * @augments wp.media.view.Settings 7429 * @augments wp.media.View 7430 * @augments wp.Backbone.View 7431 * @augments Backbone.View 7432 */ 7433 media.view.EmbedLink = media.view.Settings.extend({ 7434 className: 'embed-link-settings', 7435 template: media.template('embed-link-settings'), 7436 7437 initialize: function() { 7438 this.spinner = $('<span class="spinner" />'); 7439 this.$el.append( this.spinner[0] ); 7440 this.listenTo( this.model, 'change:url', this.updateoEmbed ); 7441 }, 7442 7443 updateoEmbed: function() { 7444 var url = this.model.get( 'url' ); 7445 7446 this.$('.setting.title').show(); 7447 // clear out previous results 7448 this.$('.embed-container').hide().find('.embed-preview').html(''); 7449 7450 // only proceed with embed if the field contains more than 6 characters 7451 if ( url && url.length < 6 ) { 7452 return; 7453 } 7454 7455 this.spinner.show(); 7456 7457 setTimeout( _.bind( this.fetch, this ), 500 ); 7458 }, 7459 7460 fetch: function() { 7461 // check if they haven't typed in 500 ms 7462 if ( $('#embed-url-field').val() !== this.model.get('url') ) { 7463 return; 7464 } 7465 7466 wp.ajax.send( 'parse-embed', { 7467 data : { 7468 post_ID: media.view.settings.post.id, 7469 shortcode: '[embed]' + this.model.get('url') + '[/embed]' 7470 } 7471 } ).done( _.bind( this.renderoEmbed, this ) ); 7472 }, 7473 7474 renderoEmbed: function( response ) { 7475 var html = ( response && response.body ) || ''; 7476 7477 this.spinner.hide(); 7478 7479 this.$('.setting.title').hide(); 7480 this.$('.embed-container').show().find('.embed-preview').html( html ); 7481 } 7482 }); 7483 7484 /** 7485 * wp.media.view.EmbedImage 7486 * 7487 * @class 7488 * @augments wp.media.view.Settings.AttachmentDisplay 7489 * @augments wp.media.view.Settings 7490 * @augments wp.media.View 7491 * @augments wp.Backbone.View 7492 * @augments Backbone.View 7493 */ 7494 media.view.EmbedImage = media.view.Settings.AttachmentDisplay.extend({ 7495 className: 'embed-media-settings', 7496 template: media.template('embed-image-settings'), 7497 7498 initialize: function() { 7499 /** 7500 * Call `initialize` directly on parent class with passed arguments 7501 */ 7502 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments ); 7503 this.listenTo( this.model, 'change:url', this.updateImage ); 7504 }, 7505 7506 updateImage: function() { 7507 this.$('img').attr( 'src', this.model.get('url') ); 7508 } 7509 }); 7510 7511 /** 7512 * wp.media.view.ImageDetails 7513 * 7514 * @class 7515 * @augments wp.media.view.Settings.AttachmentDisplay 7516 * @augments wp.media.view.Settings 7517 * @augments wp.media.View 7518 * @augments wp.Backbone.View 7519 * @augments Backbone.View 7520 */ 7521 media.view.ImageDetails = media.view.Settings.AttachmentDisplay.extend({ 7522 className: 'image-details', 7523 template: media.template('image-details'), 7524 events: _.defaults( media.view.Settings.AttachmentDisplay.prototype.events, { 7525 'click .edit-attachment': 'editAttachment', 7526 'click .replace-attachment': 'replaceAttachment', 7527 'click .advanced-toggle': 'onToggleAdvanced', 7528 'change [data-setting="customWidth"]': 'onCustomSize', 7529 'change [data-setting="customHeight"]': 'onCustomSize', 7530 'keyup [data-setting="customWidth"]': 'onCustomSize', 7531 'keyup [data-setting="customHeight"]': 'onCustomSize' 7532 } ), 7533 initialize: function() { 7534 // used in AttachmentDisplay.prototype.updateLinkTo 7535 this.options.attachment = this.model.attachment; 7536 this.listenTo( this.model, 'change:url', this.updateUrl ); 7537 this.listenTo( this.model, 'change:link', this.toggleLinkSettings ); 7538 this.listenTo( this.model, 'change:size', this.toggleCustomSize ); 7539 7540 media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments ); 7541 }, 7542 7543 prepare: function() { 7544 var attachment = false; 7545 7546 if ( this.model.attachment ) { 7547 attachment = this.model.attachment.toJSON(); 7548 } 7549 return _.defaults({ 7550 model: this.model.toJSON(), 7551 attachment: attachment 7552 }, this.options ); 7553 }, 7554 7555 render: function() { 7556 var self = this, 7557 args = arguments; 7558 7559 if ( this.model.attachment && 'pending' === this.model.dfd.state() ) { 7560 this.model.dfd.done( function() { 7561 media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args ); 7562 self.postRender(); 7563 } ).fail( function() { 7564 self.model.attachment = false; 7565 media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args ); 7566 self.postRender(); 7567 } ); 7568 } else { 7569 media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments ); 7570 this.postRender(); 7571 } 7572 7573 return this; 7574 }, 7575 7576 postRender: function() { 7577 setTimeout( _.bind( this.resetFocus, this ), 10 ); 7578 this.toggleLinkSettings(); 7579 if ( getUserSetting( 'advImgDetails' ) === 'show' ) { 7580 this.toggleAdvanced( true ); 7581 } 7582 this.trigger( 'post-render' ); 7583 }, 7584 7585 resetFocus: function() { 7586 this.$( '.link-to-custom' ).blur(); 7587 this.$( '.embed-media-settings' ).scrollTop( 0 ); 7588 }, 7589 7590 updateUrl: function() { 7591 this.$( '.image img' ).attr( 'src', this.model.get( 'url' ) ); 7592 this.$( '.url' ).val( this.model.get( 'url' ) ); 7593 }, 7594 7595 toggleLinkSettings: function() { 7596 if ( this.model.get( 'link' ) === 'none' ) { 7597 this.$( '.link-settings' ).addClass('hidden'); 7598 } else { 7599 this.$( '.link-settings' ).removeClass('hidden'); 7600 } 7601 }, 7602 7603 toggleCustomSize: function() { 7604 if ( this.model.get( 'size' ) !== 'custom' ) { 7605 this.$( '.custom-size' ).addClass('hidden'); 7606 } else { 7607 this.$( '.custom-size' ).removeClass('hidden'); 7608 } 7609 }, 7610 7611 onCustomSize: function( event ) { 7612 var dimension = $( event.target ).data('setting'), 7613 num = $( event.target ).val(), 7614 value; 7615 7616 // Ignore bogus input 7617 if ( ! /^\d+/.test( num ) || parseInt( num, 10 ) < 1 ) { 7618 event.preventDefault(); 7619 return; 7620 } 7621 7622 if ( dimension === 'customWidth' ) { 7623 value = Math.round( 1 / this.model.get( 'aspectRatio' ) * num ); 7624 this.model.set( 'customHeight', value, { silent: true } ); 7625 this.$( '[data-setting="customHeight"]' ).val( value ); 7626 } else { 7627 value = Math.round( this.model.get( 'aspectRatio' ) * num ); 7628 this.model.set( 'customWidth', value, { silent: true } ); 7629 this.$( '[data-setting="customWidth"]' ).val( value ); 7630 } 7631 }, 7632 7633 onToggleAdvanced: function( event ) { 7634 event.preventDefault(); 7635 this.toggleAdvanced(); 7636 }, 7637 7638 toggleAdvanced: function( show ) { 7639 var $advanced = this.$el.find( '.advanced-section' ), 7640 mode; 7641 7642 if ( $advanced.hasClass('advanced-visible') || show === false ) { 7643 $advanced.removeClass('advanced-visible'); 7644 $advanced.find('.advanced-settings').addClass('hidden'); 7645 mode = 'hide'; 7646 } else { 7647 $advanced.addClass('advanced-visible'); 7648 $advanced.find('.advanced-settings').removeClass('hidden'); 7649 mode = 'show'; 7650 } 7651 7652 setUserSetting( 'advImgDetails', mode ); 7653 }, 7654 7655 editAttachment: function( event ) { 7656 var editState = this.controller.states.get( 'edit-image' ); 7657 7658 if ( window.imageEdit && editState ) { 7659 event.preventDefault(); 7660 editState.set( 'image', this.model.attachment ); 7661 this.controller.setState( 'edit-image' ); 7662 } 7663 }, 7664 7665 replaceAttachment: function( event ) { 7666 event.preventDefault(); 7667 this.controller.setState( 'replace-image' ); 7668 } 7669 }); 7670 7671 /** 7672 * wp.media.view.Cropper 7673 * 7674 * Uses the imgAreaSelect plugin to allow a user to crop an image. 7675 * 7676 * Takes imgAreaSelect options from 7677 * wp.customize.HeaderControl.calculateImageSelectOptions via 7678 * wp.customize.HeaderControl.openMM. 7679 * 7680 * @class 7681 * @augments wp.media.View 7682 * @augments wp.Backbone.View 7683 * @augments Backbone.View 7684 */ 7685 media.view.Cropper = media.View.extend({ 7686 className: 'crop-content', 7687 template: media.template('crop-content'), 7688 initialize: function() { 7689 _.bindAll(this, 'onImageLoad'); 7690 }, 7691 ready: function() { 7692 this.controller.frame.on('content:error:crop', this.onError, this); 7693 this.$image = this.$el.find('.crop-image'); 7694 this.$image.on('load', this.onImageLoad); 7695 $(window).on('resize.cropper', _.debounce(this.onImageLoad, 250)); 7696 }, 7697 remove: function() { 7698 $(window).off('resize.cropper'); 7699 this.$el.remove(); 7700 this.$el.off(); 7701 wp.media.View.prototype.remove.apply(this, arguments); 7702 }, 7703 prepare: function() { 7704 return { 7705 title: l10n.cropYourImage, 7706 url: this.options.attachment.get('url') 7707 }; 7708 }, 7709 onImageLoad: function() { 7710 var imgOptions = this.controller.get('imgSelectOptions'); 7711 if (typeof imgOptions === 'function') { 7712 imgOptions = imgOptions(this.options.attachment, this.controller); 7713 } 7714 7715 imgOptions = _.extend(imgOptions, {parent: this.$el}); 7716 this.trigger('image-loaded'); 7717 this.controller.imgSelect = this.$image.imgAreaSelect(imgOptions); 7718 }, 7719 onError: function() { 7720 var filename = this.options.attachment.get('filename'); 7721 7722 this.views.add( '.upload-errors', new media.view.UploaderStatusError({ 7723 filename: media.view.UploaderStatus.prototype.filename(filename), 7724 message: _wpMediaViewsL10n.cropError 7725 }), { at: 0 }); 7726 } 7727 }); 7728 7729 media.view.EditImage = media.View.extend({ 7730 7731 className: 'image-editor', 7732 template: media.template('image-editor'), 7733 7734 initialize: function( options ) { 7735 this.editor = window.imageEdit; 7736 this.controller = options.controller; 7737 media.View.prototype.initialize.apply( this, arguments ); 7738 }, 7739 7740 prepare: function() { 7741 return this.model.toJSON(); 7742 }, 7743 7744 render: function() { 7745 media.View.prototype.render.apply( this, arguments ); 7746 return this; 7747 }, 7748 7749 loadEditor: function() { 7750 var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this ); 7751 dfd.done( _.bind( this.focus, this ) ); 7752 }, 7753 7754 focus: function() { 7755 this.$( '.imgedit-submit .button' ).eq( 0 ).focus(); 7756 }, 7757 7758 back: function() { 7759 var lastState = this.controller.lastState(); 7760 this.controller.setState( lastState ); 7761 }, 7762 7763 refresh: function() { 7764 this.model.fetch(); 7765 }, 7766 7767 save: function() { 7768 var self = this, 7769 lastState = this.controller.lastState(); 7770 7771 this.model.fetch().done( function() { 7772 self.controller.setState( lastState ); 7773 }); 7774 } 7775 7776 }); 7777 7778 /** 7779 * wp.media.view.Spinner 7780 * 7781 * @class 7782 * @augments wp.media.View 7783 * @augments wp.Backbone.View 7784 * @augments Backbone.View 7785 */ 7786 media.view.Spinner = media.View.extend({ 7787 tagName: 'span', 7788 className: 'spinner', 7789 spinnerTimeout: false, 7790 delay: 400, 7791 7792 show: function() { 7793 if ( ! this.spinnerTimeout ) { 7794 this.spinnerTimeout = _.delay(function( $el ) { 7795 $el.show(); 7796 }, this.delay, this.$el ); 7797 } 7798 7799 return this; 7800 }, 7801 7802 hide: function() { 7803 this.$el.hide(); 7804 this.spinnerTimeout = clearTimeout( this.spinnerTimeout ); 7805 7806 return this; 7807 } 7808 }); 7809 }(jQuery, _)); 85 module.exports = Selection;
Note: See TracChangeset
for help on using the changeset viewer.