[ Index ]

WordPress Cross Reference

title

Body

[close]

/wp-includes/js/ -> media-views.js (source)

   1  /* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */
   2  (function($){
   3      var media       = wp.media,
   4          Attachment  = media.model.Attachment,
   5          Attachments = media.model.Attachments,
   6          l10n;
   7  
   8      // Link any localized strings.
   9      l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
  10  
  11      // Link any settings.
  12      media.view.settings = l10n.settings || {};
  13      delete l10n.settings;
  14  
  15      // Copy the `post` setting over to the model settings.
  16      media.model.settings.post = media.view.settings.post;
  17  
  18      // Check if the browser supports CSS 3.0 transitions
  19      $.support.transition = (function(){
  20          var style = document.documentElement.style,
  21              transitions = {
  22                  WebkitTransition: 'webkitTransitionEnd',
  23                  MozTransition:    'transitionend',
  24                  OTransition:      'oTransitionEnd otransitionend',
  25                  transition:       'transitionend'
  26              }, transition;
  27  
  28          transition = _.find( _.keys( transitions ), function( transition ) {
  29              return ! _.isUndefined( style[ transition ] );
  30          });
  31  
  32          return transition && {
  33              end: transitions[ transition ]
  34          };
  35      }());
  36  
  37      // Makes it easier to bind events using transitions.
  38      media.transition = function( selector, sensitivity ) {
  39          var deferred = $.Deferred();
  40  
  41          sensitivity = sensitivity || 2000;
  42  
  43          if ( $.support.transition ) {
  44              if ( ! (selector instanceof $) )
  45                  selector = $( selector );
  46  
  47              // Resolve the deferred when the first element finishes animating.
  48              selector.first().one( $.support.transition.end, deferred.resolve );
  49  
  50              // Just in case the event doesn't trigger, fire a callback.
  51              _.delay( deferred.resolve, sensitivity );
  52  
  53          // Otherwise, execute on the spot.
  54          } else {
  55              deferred.resolve();
  56          }
  57  
  58          return deferred.promise();
  59      };
  60  
  61      /**
  62       * ========================================================================
  63       * CONTROLLERS
  64       * ========================================================================
  65       */
  66  
  67      /**
  68       * wp.media.controller.Region
  69       */
  70      media.controller.Region = function( options ) {
  71          _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
  72      };
  73  
  74      // Use Backbone's self-propagating `extend` inheritance method.
  75      media.controller.Region.extend = Backbone.Model.extend;
  76  
  77      _.extend( media.controller.Region.prototype, {
  78          mode: function( mode ) {
  79              if ( ! mode )
  80                  return this._mode;
  81  
  82              // Bail if we're trying to change to the current mode.
  83              if ( mode === this._mode )
  84                  return this;
  85  
  86              this.trigger('deactivate');
  87              this._mode = mode;
  88              this.render( mode );
  89              this.trigger('activate');
  90              return this;
  91          },
  92  
  93          render: function( mode ) {
  94              // If no mode is provided, just re-render the current mode.
  95              // If the provided mode isn't active, perform a full switch.
  96              if ( mode && mode !== this._mode )
  97                  return this.mode( mode );
  98  
  99              var set = { view: null },
 100                  view;
 101  
 102              this.trigger( 'create', set );
 103              view = set.view;
 104              this.trigger( 'render', view );
 105              if ( view )
 106                  this.set( view );
 107              return this;
 108          },
 109  
 110          get: function() {
 111              return this.view.views.first( this.selector );
 112          },
 113  
 114          set: function( views, options ) {
 115              if ( options )
 116                  options.add = false;
 117              return this.view.views.set( this.selector, views, options );
 118          },
 119  
 120          trigger: function( event ) {
 121              var base, args;
 122  
 123              if ( ! this._mode )
 124                  return;
 125  
 126              args = _.toArray( arguments );
 127              base = this.id + ':' + event;
 128  
 129              // Trigger `region:action:mode` event.
 130              args[0] = base + ':' + this._mode;
 131              this.view.trigger.apply( this.view, args );
 132  
 133              // Trigger `region:action` event.
 134              args[0] = base;
 135              this.view.trigger.apply( this.view, args );
 136              return this;
 137          }
 138      });
 139  
 140      /**
 141       * wp.media.controller.StateMachine
 142       */
 143      media.controller.StateMachine = function( states ) {
 144          this.states = new Backbone.Collection( states );
 145      };
 146  
 147      // Use Backbone's self-propagating `extend` inheritance method.
 148      media.controller.StateMachine.extend = Backbone.Model.extend;
 149  
 150      // Add events to the `StateMachine`.
 151      _.extend( media.controller.StateMachine.prototype, Backbone.Events, {
 152  
 153          // Fetch a state.
 154          //
 155          // If no `id` is provided, returns the active state.
 156          //
 157          // Implicitly creates states.
 158          state: function( id ) {
 159              // Ensure that the `states` collection exists so the `StateMachine`
 160              // can be used as a mixin.
 161              this.states = this.states || new Backbone.Collection();
 162  
 163              // Default to the active state.
 164              id = id || this._state;
 165  
 166              if ( id && ! this.states.get( id ) )
 167                  this.states.add({ id: id });
 168              return this.states.get( id );
 169          },
 170  
 171          // Sets the active state.
 172          setState: function( id ) {
 173              var previous = this.state();
 174  
 175              // Bail if we're trying to select the current state, if we haven't
 176              // created the `states` collection, or are trying to select a state
 177              // that does not exist.
 178              if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) )
 179                  return this;
 180  
 181              if ( previous ) {
 182                  previous.trigger('deactivate');
 183                  this._lastState = previous.id;
 184              }
 185  
 186              this._state = id;
 187              this.state().trigger('activate');
 188  
 189              return this;
 190          },
 191  
 192          // Returns the previous active state.
 193          //
 194          // Call the `state()` method with no parameters to retrieve the current
 195          // active state.
 196          lastState: function() {
 197              if ( this._lastState )
 198                  return this.state( this._lastState );
 199          }
 200      });
 201  
 202      // Map methods from the `states` collection to the `StateMachine` itself.
 203      _.each([ 'on', 'off', 'trigger' ], function( method ) {
 204          media.controller.StateMachine.prototype[ method ] = function() {
 205              // Ensure that the `states` collection exists so the `StateMachine`
 206              // can be used as a mixin.
 207              this.states = this.states || new Backbone.Collection();
 208              // Forward the method to the `states` collection.
 209              this.states[ method ].apply( this.states, arguments );
 210              return this;
 211          };
 212      });
 213  
 214  
 215      // wp.media.controller.State
 216      // ---------------------------
 217      media.controller.State = Backbone.Model.extend({
 218          constructor: function() {
 219              this.on( 'activate', this._preActivate, this );
 220              this.on( 'activate', this.activate, this );
 221              this.on( 'activate', this._postActivate, this );
 222              this.on( 'deactivate', this._deactivate, this );
 223              this.on( 'deactivate', this.deactivate, this );
 224              this.on( 'reset', this.reset, this );
 225              this.on( 'ready', this._ready, this );
 226              this.on( 'ready', this.ready, this );
 227              Backbone.Model.apply( this, arguments );
 228              this.on( 'change:menu', this._updateMenu, this );
 229          },
 230  
 231          ready: function() {},
 232          activate: function() {},
 233          deactivate: function() {},
 234          reset: function() {},
 235  
 236          _ready: function() {
 237              this._updateMenu();
 238          },
 239  
 240          _preActivate: function() {
 241              this.active = true;
 242          },
 243  
 244          _postActivate: function() {
 245              this.on( 'change:menu', this._menu, this );
 246              this.on( 'change:titleMode', this._title, this );
 247              this.on( 'change:content', this._content, this );
 248              this.on( 'change:toolbar', this._toolbar, this );
 249  
 250              this.frame.on( 'title:render:default', this._renderTitle, this );
 251  
 252              this._title();
 253              this._menu();
 254              this._toolbar();
 255              this._content();
 256              this._router();
 257          },
 258  
 259  
 260          _deactivate: function() {
 261              this.active = false;
 262  
 263              this.frame.off( 'title:render:default', this._renderTitle, this );
 264  
 265              this.off( 'change:menu', this._menu, this );
 266              this.off( 'change:titleMode', this._title, this );
 267              this.off( 'change:content', this._content, this );
 268              this.off( 'change:toolbar', this._toolbar, this );
 269          },
 270  
 271          _title: function() {
 272              this.frame.title.render( this.get('titleMode') || 'default' );
 273          },
 274  
 275          _renderTitle: function( view ) {
 276              view.$el.text( this.get('title') || '' );
 277          },
 278  
 279          _router: function() {
 280              var router = this.frame.router,
 281                  mode = this.get('router'),
 282                  view;
 283  
 284              this.frame.$el.toggleClass( 'hide-router', ! mode );
 285              if ( ! mode )
 286                  return;
 287  
 288              this.frame.router.render( mode );
 289  
 290              view = router.get();
 291              if ( view && view.select )
 292                  view.select( this.frame.content.mode() );
 293          },
 294  
 295          _menu: function() {
 296              var menu = this.frame.menu,
 297                  mode = this.get('menu'),
 298                  view;
 299  
 300              if ( ! mode )
 301                  return;
 302  
 303              menu.mode( mode );
 304  
 305              view = menu.get();
 306              if ( view && view.select )
 307                  view.select( this.id );
 308          },
 309  
 310          _updateMenu: function() {
 311              var previous = this.previous('menu'),
 312                  menu = this.get('menu');
 313  
 314              if ( previous )
 315                  this.frame.off( 'menu:render:' + previous, this._renderMenu, this );
 316  
 317              if ( menu )
 318                  this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
 319          },
 320  
 321          _renderMenu: function( view ) {
 322              var menuItem = this.get('menuItem'),
 323                  title = this.get('title'),
 324                  priority = this.get('priority');
 325  
 326              if ( ! menuItem && title ) {
 327                  menuItem = { text: title };
 328  
 329                  if ( priority )
 330                      menuItem.priority = priority;
 331              }
 332  
 333              if ( ! menuItem )
 334                  return;
 335  
 336              view.set( this.id, menuItem );
 337          }
 338      });
 339  
 340      _.each(['toolbar','content'], function( region ) {
 341          media.controller.State.prototype[ '_' + region ] = function() {
 342              var mode = this.get( region );
 343              if ( mode )
 344                  this.frame[ region ].render( mode );
 345          };
 346      });
 347  
 348      // wp.media.controller.Library
 349      // ---------------------------
 350      media.controller.Library = media.controller.State.extend({
 351          defaults: {
 352              id:         'library',
 353              multiple:   false, // false, 'add', 'reset'
 354              describe:   false,
 355              toolbar:    'select',
 356              sidebar:    'settings',
 357              content:    'upload',
 358              router:     'browse',
 359              menu:       'default',
 360              searchable: true,
 361              filterable: false,
 362              sortable:   true,
 363              title:      l10n.mediaLibraryTitle,
 364  
 365              // Uses a user setting to override the content mode.
 366              contentUserSetting: true,
 367  
 368              // Sync the selection from the last state when 'multiple' matches.
 369              syncSelection: true
 370          },
 371  
 372          initialize: function() {
 373              var selection = this.get('selection'),
 374                  props;
 375  
 376              // If a library isn't provided, query all media items.
 377              if ( ! this.get('library') )
 378                  this.set( 'library', media.query() );
 379  
 380              // If a selection instance isn't provided, create one.
 381              if ( ! (selection instanceof media.model.Selection) ) {
 382                  props = selection;
 383  
 384                  if ( ! props ) {
 385                      props = this.get('library').props.toJSON();
 386                      props = _.omit( props, 'orderby', 'query' );
 387                  }
 388  
 389                  // If the `selection` attribute is set to an object,
 390                  // it will use those values as the selection instance's
 391                  // `props` model. Otherwise, it will copy the library's
 392                  // `props` model.
 393                  this.set( 'selection', new media.model.Selection( null, {
 394                      multiple: this.get('multiple'),
 395                      props: props
 396                  }) );
 397              }
 398  
 399              if ( ! this.get('edge') )
 400                  this.set( 'edge', 120 );
 401  
 402              if ( ! this.get('gutter') )
 403                  this.set( 'gutter', 8 );
 404  
 405              this.resetDisplays();
 406          },
 407  
 408          activate: function() {
 409              this.syncSelection();
 410  
 411              wp.Uploader.queue.on( 'add', this.uploading, this );
 412  
 413              this.get('selection').on( 'add remove reset', this.refreshContent, this );
 414  
 415              if ( this.get('contentUserSetting') ) {
 416                  this.frame.on( 'content:activate', this.saveContentMode, this );
 417                  this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
 418              }
 419          },
 420  
 421          deactivate: function() {
 422              this.recordSelection();
 423  
 424              this.frame.off( 'content:activate', this.saveContentMode, this );
 425  
 426              // Unbind all event handlers that use this state as the context
 427              // from the selection.
 428              this.get('selection').off( null, null, this );
 429  
 430              wp.Uploader.queue.off( null, null, this );
 431          },
 432  
 433          reset: function() {
 434              this.get('selection').reset();
 435              this.resetDisplays();
 436              this.refreshContent();
 437          },
 438  
 439          resetDisplays: function() {
 440              var defaultProps = media.view.settings.defaultProps;
 441              this._displays = [];
 442              this._defaultDisplaySettings = {
 443                  align: defaultProps.align || getUserSetting( 'align', 'none' ),
 444                  size:  defaultProps.size  || getUserSetting( 'imgsize', 'medium' ),
 445                  link:  defaultProps.link  || getUserSetting( 'urlbutton', 'file' )
 446              };
 447          },
 448  
 449          display: function( attachment ) {
 450              var displays = this._displays;
 451  
 452              if ( ! displays[ attachment.cid ] )
 453                  displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) );
 454  
 455              return displays[ attachment.cid ];
 456          },
 457  
 458          defaultDisplaySettings: function( attachment ) {
 459              var settings = this._defaultDisplaySettings;
 460              if ( settings.canEmbed = this.canEmbed( attachment ) )
 461                  settings.link = 'embed';
 462              return settings;
 463          },
 464  
 465          canEmbed: function( attachment ) {
 466              // If uploading, we know the filename but not the mime type.
 467              if ( ! attachment.get('uploading') ) {
 468                  var type = attachment.get('type');
 469                  if ( type !== 'audio' && type !== 'video' )
 470                      return false;
 471              }
 472  
 473              return _.contains( media.view.settings.embedExts, attachment.get('filename').split('.').pop() );
 474          },
 475  
 476          syncSelection: function() {
 477              var selection = this.get('selection'),
 478                  manager = this.frame._selection;
 479  
 480              if ( ! this.get('syncSelection') || ! manager || ! selection )
 481                  return;
 482  
 483              // If the selection supports multiple items, validate the stored
 484              // attachments based on the new selection's conditions. Record
 485              // the attachments that are not included; we'll maintain a
 486              // reference to those. Other attachments are considered in flux.
 487              if ( selection.multiple ) {
 488                  selection.reset( [], { silent: true });
 489                  selection.validateAll( manager.attachments );
 490                  manager.difference = _.difference( manager.attachments.models, selection.models );
 491              }
 492  
 493              // Sync the selection's single item with the master.
 494              selection.single( manager.single );
 495          },
 496  
 497          recordSelection: function() {
 498              var selection = this.get('selection'),
 499                  manager = this.frame._selection;
 500  
 501              if ( ! this.get('syncSelection') || ! manager || ! selection )
 502                  return;
 503  
 504              // Record the currently active attachments, which is a combination
 505              // of the selection's attachments and the set of selected
 506              // attachments that this specific selection considered invalid.
 507              // Reset the difference and record the single attachment.
 508              if ( selection.multiple ) {
 509                  manager.attachments.reset( selection.toArray().concat( manager.difference ) );
 510                  manager.difference = [];
 511              } else {
 512                  manager.attachments.add( selection.toArray() );
 513              }
 514  
 515              manager.single = selection._single;
 516          },
 517  
 518          refreshContent: function() {
 519              var selection = this.get('selection'),
 520                  frame = this.frame,
 521                  router = frame.router.get(),
 522                  mode = frame.content.mode();
 523  
 524              // If the state is active, no items are selected, and the current
 525              // content mode is not an option in the state's router (provided
 526              // the state has a router), reset the content mode to the default.
 527              if ( this.active && ! selection.length && router && ! router.get( mode ) )
 528                  this.frame.content.render( this.get('content') );
 529          },
 530  
 531          uploading: function( attachment ) {
 532              var content = this.frame.content;
 533  
 534              // If the uploader was selected, navigate to the browser.
 535              if ( 'upload' === content.mode() )
 536                  this.frame.content.mode('browse');
 537  
 538              // Automatically select any uploading attachments.
 539              //
 540              // Selections that don't support multiple attachments automatically
 541              // limit themselves to one attachment (in this case, the last
 542              // attachment in the upload queue).
 543              this.get('selection').add( attachment );
 544          },
 545  
 546          saveContentMode: function() {
 547              // Only track the browse router on library states.
 548              if ( 'browse' !== this.get('router') )
 549                  return;
 550  
 551              var mode = this.frame.content.mode(),
 552                  view = this.frame.router.get();
 553  
 554              if ( view && view.get( mode ) )
 555                  setUserSetting( 'libraryContent', mode );
 556          }
 557      });
 558  
 559      // wp.media.controller.GalleryEdit
 560      // -------------------------------
 561      media.controller.GalleryEdit = media.controller.Library.extend({
 562          defaults: {
 563              id:         'gallery-edit',
 564              multiple:   false,
 565              describe:   true,
 566              edge:       199,
 567              editing:    false,
 568              sortable:   true,
 569              searchable: false,
 570              toolbar:    'gallery-edit',
 571              content:    'browse',
 572              title:      l10n.editGalleryTitle,
 573              priority:   60,
 574              dragInfo:   true,
 575  
 576              // Don't sync the selection, as the Edit Gallery library
 577              // *is* the selection.
 578              syncSelection: false
 579          },
 580  
 581          initialize: function() {
 582              // If we haven't been provided a `library`, create a `Selection`.
 583              if ( ! this.get('library') )
 584                  this.set( 'library', new media.model.Selection() );
 585  
 586              // The single `Attachment` view to be used in the `Attachments` view.
 587              if ( ! this.get('AttachmentView') )
 588                  this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
 589              media.controller.Library.prototype.initialize.apply( this, arguments );
 590          },
 591  
 592          activate: function() {
 593              var library = this.get('library');
 594  
 595              // Limit the library to images only.
 596              library.props.set( 'type', 'image' );
 597  
 598              // Watch for uploaded attachments.
 599              this.get('library').observe( wp.Uploader.queue );
 600  
 601              this.frame.on( 'content:render:browse', this.gallerySettings, this );
 602  
 603              media.controller.Library.prototype.activate.apply( this, arguments );
 604          },
 605  
 606          deactivate: function() {
 607              // Stop watching for uploaded attachments.
 608              this.get('library').unobserve( wp.Uploader.queue );
 609  
 610              this.frame.off( 'content:render:browse', this.gallerySettings, this );
 611  
 612              media.controller.Library.prototype.deactivate.apply( this, arguments );
 613          },
 614  
 615          gallerySettings: function( browser ) {
 616              var library = this.get('library');
 617  
 618              if ( ! library || ! browser )
 619                  return;
 620  
 621              library.gallery = library.gallery || new Backbone.Model();
 622  
 623              browser.sidebar.set({
 624                  gallery: new media.view.Settings.Gallery({
 625                      controller: this,
 626                      model:      library.gallery,
 627                      priority:   40
 628                  })
 629              });
 630  
 631              browser.toolbar.set( 'reverse', {
 632                  text:     l10n.reverseOrder,
 633                  priority: 80,
 634  
 635                  click: function() {
 636                      library.reset( library.toArray().reverse() );
 637                  }
 638              });
 639          }
 640      });
 641  
 642      // wp.media.controller.GalleryAdd
 643      // ---------------------------------
 644      media.controller.GalleryAdd = media.controller.Library.extend({
 645          defaults: _.defaults({
 646              id:           'gallery-library',
 647              filterable:   'uploaded',
 648              multiple:     'add',
 649              menu:         'gallery',
 650              toolbar:      'gallery-add',
 651              title:        l10n.addToGalleryTitle,
 652              priority:     100,
 653  
 654              // Don't sync the selection, as the Edit Gallery library
 655              // *is* the selection.
 656              syncSelection: false
 657          }, media.controller.Library.prototype.defaults ),
 658  
 659          initialize: function() {
 660              // If we haven't been provided a `library`, create a `Selection`.
 661              if ( ! this.get('library') )
 662                  this.set( 'library', media.query({ type: 'image' }) );
 663  
 664              media.controller.Library.prototype.initialize.apply( this, arguments );
 665          },
 666  
 667          activate: function() {
 668              var library = this.get('library'),
 669                  edit    = this.frame.state('gallery-edit').get('library');
 670  
 671              if ( this.editLibrary && this.editLibrary !== edit )
 672                  library.unobserve( this.editLibrary );
 673  
 674              // Accepts attachments that exist in the original library and
 675              // that do not exist in gallery's library.
 676              library.validator = function( attachment ) {
 677                  return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
 678              };
 679  
 680              // Reset the library to ensure that all attachments are re-added
 681              // to the collection. Do so silently, as calling `observe` will
 682              // trigger the `reset` event.
 683              library.reset( library.mirroring.models, { silent: true });
 684              library.observe( edit );
 685              this.editLibrary = edit;
 686  
 687              media.controller.Library.prototype.activate.apply( this, arguments );
 688          }
 689      });
 690  
 691      // wp.media.controller.FeaturedImage
 692      // ---------------------------------
 693      media.controller.FeaturedImage = media.controller.Library.extend({
 694          defaults: _.defaults({
 695              id:         'featured-image',
 696              filterable: 'uploaded',
 697              multiple:   false,
 698              toolbar:    'featured-image',
 699              title:      l10n.setFeaturedImageTitle,
 700              priority:   60,
 701  
 702              syncSelection: false
 703          }, media.controller.Library.prototype.defaults ),
 704  
 705          initialize: function() {
 706              var library, comparator;
 707  
 708              // If we haven't been provided a `library`, create a `Selection`.
 709              if ( ! this.get('library') )
 710                  this.set( 'library', media.query({ type: 'image' }) );
 711  
 712              media.controller.Library.prototype.initialize.apply( this, arguments );
 713  
 714              library    = this.get('library');
 715              comparator = library.comparator;
 716  
 717              // Overload the library's comparator to push items that are not in
 718              // the mirrored query to the front of the aggregate collection.
 719              library.comparator = function( a, b ) {
 720                  var aInQuery = !! this.mirroring.get( a.cid ),
 721                      bInQuery = !! this.mirroring.get( b.cid );
 722  
 723                  if ( ! aInQuery && bInQuery )
 724                      return -1;
 725                  else if ( aInQuery && ! bInQuery )
 726                      return 1;
 727                  else
 728                      return comparator.apply( this, arguments );
 729              };
 730  
 731              // Add all items in the selection to the library, so any featured
 732              // images that are not initially loaded still appear.
 733              library.observe( this.get('selection') );
 734          },
 735  
 736          activate: function() {
 737              this.updateSelection();
 738              this.frame.on( 'open', this.updateSelection, this );
 739              media.controller.Library.prototype.activate.apply( this, arguments );
 740          },
 741  
 742          deactivate: function() {
 743              this.frame.off( 'open', this.updateSelection, this );
 744              media.controller.Library.prototype.deactivate.apply( this, arguments );
 745          },
 746  
 747          updateSelection: function() {
 748              var selection = this.get('selection'),
 749                  id = media.view.settings.post.featuredImageId,
 750                  attachment;
 751  
 752              if ( '' !== id && -1 !== id ) {
 753                  attachment = Attachment.get( id );
 754                  attachment.fetch();
 755              }
 756  
 757              selection.reset( attachment ? [ attachment ] : [] );
 758          }
 759      });
 760  
 761  
 762      // wp.media.controller.Embed
 763      // -------------------------
 764      media.controller.Embed = media.controller.State.extend({
 765          defaults: {
 766              id:      'embed',
 767              url:     '',
 768              menu:    'default',
 769              content: 'embed',
 770              toolbar: 'main-embed',
 771              type:    'link',
 772  
 773              title:    l10n.insertFromUrlTitle,
 774              priority: 120
 775          },
 776  
 777          // The amount of time used when debouncing the scan.
 778          sensitivity: 200,
 779  
 780          initialize: function() {
 781              this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
 782              this.props = new Backbone.Model({ url: '' });
 783              this.props.on( 'change:url', this.debouncedScan, this );
 784              this.props.on( 'change:url', this.refresh, this );
 785              this.on( 'scan', this.scanImage, this );
 786          },
 787  
 788          scan: function() {
 789              var scanners,
 790                  embed = this,
 791                  attributes = {
 792                      type: 'link',
 793                      scanners: []
 794                  };
 795  
 796              // Scan is triggered with the list of `attributes` to set on the
 797              // state, useful for the 'type' attribute and 'scanners' attribute,
 798              // an array of promise objects for asynchronous scan operations.
 799              if ( this.props.get('url') )
 800                  this.trigger( 'scan', attributes );
 801  
 802              if ( attributes.scanners.length ) {
 803                  scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
 804                  scanners.always( function() {
 805                      if ( embed.get('scanners') === scanners )
 806                          embed.set( 'loading', false );
 807                  });
 808              } else {
 809                  attributes.scanners = null;
 810              }
 811  
 812              attributes.loading = !! attributes.scanners;
 813              this.set( attributes );
 814          },
 815  
 816          scanImage: function( attributes ) {
 817              var frame = this.frame,
 818                  state = this,
 819                  url = this.props.get('url'),
 820                  image = new Image(),
 821                  deferred = $.Deferred();
 822  
 823              attributes.scanners.push( deferred.promise() );
 824  
 825              // Try to load the image and find its width/height.
 826              image.onload = function() {
 827                  deferred.resolve();
 828  
 829                  if ( state !== frame.state() || url !== state.props.get('url') )
 830                      return;
 831  
 832                  state.set({
 833                      type: 'image'
 834                  });
 835  
 836                  state.props.set({
 837                      width:  image.width,
 838                      height: image.height
 839                  });
 840              };
 841  
 842              image.onerror = deferred.reject;
 843              image.src = url;
 844          },
 845  
 846          refresh: function() {
 847              this.frame.toolbar.get().refresh();
 848          },
 849  
 850          reset: function() {
 851              this.props.clear().set({ url: '' });
 852  
 853              if ( this.active )
 854                  this.refresh();
 855          }
 856      });
 857  
 858      /**
 859       * ========================================================================
 860       * VIEWS
 861       * ========================================================================
 862       */
 863  
 864      // wp.media.View
 865      // -------------
 866      //
 867      // The base view class.
 868      //
 869      // Undelegating events, removing events from the model, and
 870      // removing events from the controller mirror the code for
 871      // `Backbone.View.dispose` in Backbone 0.9.8 development.
 872      //
 873      // This behavior has since been removed, and should not be used
 874      // outside of the media manager.
 875      media.View = wp.Backbone.View.extend({
 876          constructor: function( options ) {
 877              if ( options && options.controller )
 878                  this.controller = options.controller;
 879  
 880              wp.Backbone.View.apply( this, arguments );
 881          },
 882  
 883          dispose: function() {
 884              // Undelegating events, removing events from the model, and
 885              // removing events from the controller mirror the code for
 886              // `Backbone.View.dispose` in Backbone 0.9.8 development.
 887              this.undelegateEvents();
 888  
 889              if ( this.model && this.model.off )
 890                  this.model.off( null, null, this );
 891  
 892              if ( this.collection && this.collection.off )
 893                  this.collection.off( null, null, this );
 894  
 895              // Unbind controller events.
 896              if ( this.controller && this.controller.off )
 897                  this.controller.off( null, null, this );
 898  
 899              return this;
 900          },
 901  
 902          remove: function() {
 903              this.dispose();
 904              return wp.Backbone.View.prototype.remove.apply( this, arguments );
 905          }
 906      });
 907  
 908      /**
 909       * wp.media.view.Frame
 910       */
 911      media.view.Frame = media.View.extend({
 912          initialize: function() {
 913              this._createRegions();
 914              this._createStates();
 915          },
 916  
 917          _createRegions: function() {
 918              // Clone the regions array.
 919              this.regions = this.regions ? this.regions.slice() : [];
 920  
 921              // Initialize regions.
 922              _.each( this.regions, function( region ) {
 923                  this[ region ] = new media.controller.Region({
 924                      view:     this,
 925                      id:       region,
 926                      selector: '.media-frame-' + region
 927                  });
 928              }, this );
 929          },
 930  
 931          _createStates: function() {
 932              // Create the default `states` collection.
 933              this.states = new Backbone.Collection( null, {
 934                  model: media.controller.State
 935              });
 936  
 937              // Ensure states have a reference to the frame.
 938              this.states.on( 'add', function( model ) {
 939                  model.frame = this;
 940                  model.trigger('ready');
 941              }, this );
 942  
 943              if ( this.options.states )
 944                  this.states.add( this.options.states );
 945          },
 946  
 947          reset: function() {
 948              this.states.invoke( 'trigger', 'reset' );
 949              return this;
 950          }
 951      });
 952  
 953      // Make the `Frame` a `StateMachine`.
 954      _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
 955  
 956      /**
 957       * wp.media.view.MediaFrame
 958       */
 959      media.view.MediaFrame = media.view.Frame.extend({
 960          className: 'media-frame',
 961          template:  media.template('media-frame'),
 962          regions:   ['menu','title','content','toolbar','router'],
 963  
 964          initialize: function() {
 965              media.view.Frame.prototype.initialize.apply( this, arguments );
 966  
 967              _.defaults( this.options, {
 968                  title:    '',
 969                  modal:    true,
 970                  uploader: true
 971              });
 972  
 973              // Ensure core UI is enabled.
 974              this.$el.addClass('wp-core-ui');
 975  
 976              // Initialize modal container view.
 977              if ( this.options.modal ) {
 978                  this.modal = new media.view.Modal({
 979                      controller: this,
 980                      title:      this.options.title
 981                  });
 982  
 983                  this.modal.content( this );
 984              }
 985  
 986              // Force the uploader off if the upload limit has been exceeded or
 987              // if the browser isn't supported.
 988              if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported )
 989                  this.options.uploader = false;
 990  
 991              // Initialize window-wide uploader.
 992              if ( this.options.uploader ) {
 993                  this.uploader = new media.view.UploaderWindow({
 994                      controller: this,
 995                      uploader: {
 996                          dropzone:  this.modal ? this.modal.$el : this.$el,
 997                          container: this.$el
 998                      }
 999                  });
1000                  this.views.set( '.media-frame-uploader', this.uploader );
1001              }
1002  
1003              this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1004  
1005              // Bind default title creation.
1006              this.on( 'title:create:default', this.createTitle, this );
1007              this.title.mode('default');
1008  
1009              // Bind default menu.
1010              this.on( 'menu:create:default', this.createMenu, this );
1011          },
1012  
1013          render: function() {
1014              // Activate the default state if no active state exists.
1015              if ( ! this.state() && this.options.state )
1016                  this.setState( this.options.state );
1017  
1018              return media.view.Frame.prototype.render.apply( this, arguments );
1019          },
1020  
1021          createTitle: function( title ) {
1022              title.view = new media.View({
1023                  controller: this,
1024                  tagName: 'h1'
1025              });
1026          },
1027  
1028          createMenu: function( menu ) {
1029              menu.view = new media.view.Menu({
1030                  controller: this
1031              });
1032          },
1033  
1034          createToolbar: function( toolbar ) {
1035              toolbar.view = new media.view.Toolbar({
1036                  controller: this
1037              });
1038          },
1039  
1040          createRouter: function( router ) {
1041              router.view = new media.view.Router({
1042                  controller: this
1043              });
1044          },
1045  
1046          createIframeStates: function( options ) {
1047              var settings = media.view.settings,
1048                  tabs = settings.tabs,
1049                  tabUrl = settings.tabUrl,
1050                  $postId;
1051  
1052              if ( ! tabs || ! tabUrl )
1053                  return;
1054  
1055              // Add the post ID to the tab URL if it exists.
1056              $postId = $('#post_ID');
1057              if ( $postId.length )
1058                  tabUrl += '&post_id=' + $postId.val();
1059  
1060              // Generate the tab states.
1061              _.each( tabs, function( title, id ) {
1062                  this.state( 'iframe:' + id ).set( _.defaults({
1063                      tab:     id,
1064                      src:     tabUrl + '&tab=' + id,
1065                      title:   title,
1066                      content: 'iframe',
1067                      menu:    'default'
1068                  }, options ) );
1069              }, this );
1070  
1071              this.on( 'content:create:iframe', this.iframeContent, this );
1072              this.on( 'menu:render:default', this.iframeMenu, this );
1073              this.on( 'open', this.hijackThickbox, this );
1074              this.on( 'close', this.restoreThickbox, this );
1075          },
1076  
1077          iframeContent: function( content ) {
1078              this.$el.addClass('hide-toolbar');
1079              content.view = new media.view.Iframe({
1080                  controller: this
1081              });
1082          },
1083  
1084          iframeMenu: function( view ) {
1085              var views = {};
1086  
1087              if ( ! view )
1088                  return;
1089  
1090              _.each( media.view.settings.tabs, function( title, id ) {
1091                  views[ 'iframe:' + id ] = {
1092                      text: this.state( 'iframe:' + id ).get('title'),
1093                      priority: 200
1094                  };
1095              }, this );
1096  
1097              view.set( views );
1098          },
1099  
1100          hijackThickbox: function() {
1101              var frame = this;
1102  
1103              if ( ! window.tb_remove || this._tb_remove )
1104                  return;
1105  
1106              this._tb_remove = window.tb_remove;
1107              window.tb_remove = function() {
1108                  frame.close();
1109                  frame.reset();
1110                  frame.setState( frame.options.state );
1111                  frame._tb_remove.call( window );
1112              };
1113          },
1114  
1115          restoreThickbox: function() {
1116              if ( ! this._tb_remove )
1117                  return;
1118  
1119              window.tb_remove = this._tb_remove;
1120              delete this._tb_remove;
1121          }
1122      });
1123  
1124      // Map some of the modal's methods to the frame.
1125      _.each(['open','close','attach','detach','escape'], function( method ) {
1126          media.view.MediaFrame.prototype[ method ] = function() {
1127              if ( this.modal )
1128                  this.modal[ method ].apply( this.modal, arguments );
1129              return this;
1130          };
1131      });
1132  
1133      /**
1134       * wp.media.view.MediaFrame.Select
1135       */
1136      media.view.MediaFrame.Select = media.view.MediaFrame.extend({
1137          initialize: function() {
1138              media.view.MediaFrame.prototype.initialize.apply( this, arguments );
1139  
1140              _.defaults( this.options, {
1141                  selection: [],
1142                  library:   {},
1143                  multiple:  false,
1144                  state:    'library'
1145              });
1146  
1147              this.createSelection();
1148              this.createStates();
1149              this.bindHandlers();
1150          },
1151  
1152          createSelection: function() {
1153              var selection = this.options.selection;
1154  
1155              if ( ! (selection instanceof media.model.Selection) ) {
1156                  this.options.selection = new media.model.Selection( selection, {
1157                      multiple: this.options.multiple
1158                  });
1159              }
1160  
1161              this._selection = {
1162                  attachments: new Attachments(),
1163                  difference: []
1164              };
1165          },
1166  
1167          createStates: function() {
1168              var options = this.options;
1169  
1170              if ( this.options.states )
1171                  return;
1172  
1173              // Add the default states.
1174              this.states.add([
1175                  // Main states.
1176                  new media.controller.Library({
1177                      library:   media.query( options.library ),
1178                      multiple:  options.multiple,
1179                      title:     options.title,
1180                      priority:  20
1181                  })
1182              ]);
1183          },
1184  
1185          bindHandlers: function() {
1186              this.on( 'router:create:browse', this.createRouter, this );
1187              this.on( 'router:render:browse', this.browseRouter, this );
1188              this.on( 'content:create:browse', this.browseContent, this );
1189              this.on( 'content:render:upload', this.uploadContent, this );
1190              this.on( 'toolbar:create:select', this.createSelectToolbar, this );
1191          },
1192  
1193          // Routers
1194          browseRouter: function( view ) {
1195              view.set({
1196                  upload: {
1197                      text:     l10n.uploadFilesTitle,
1198                      priority: 20
1199                  },
1200                  browse: {
1201                      text:     l10n.mediaLibraryTitle,
1202                      priority: 40
1203                  }
1204              });
1205          },
1206  
1207          // Content
1208          browseContent: function( content ) {
1209              var state = this.state();
1210  
1211              this.$el.removeClass('hide-toolbar');
1212  
1213              // Browse our library of attachments.
1214              content.view = new media.view.AttachmentsBrowser({
1215                  controller: this,
1216                  collection: state.get('library'),
1217                  selection:  state.get('selection'),
1218                  model:      state,
1219                  sortable:   state.get('sortable'),
1220                  search:     state.get('searchable'),
1221                  filters:    state.get('filterable'),
1222                  display:    state.get('displaySettings'),
1223                  dragInfo:   state.get('dragInfo'),
1224  
1225                  AttachmentView: state.get('AttachmentView')
1226              });
1227          },
1228  
1229          uploadContent: function() {
1230              this.$el.removeClass('hide-toolbar');
1231              this.content.set( new media.view.UploaderInline({
1232                  controller: this
1233              }) );
1234          },
1235  
1236          // Toolbars
1237          createSelectToolbar: function( toolbar, options ) {
1238              options = options || this.options.button || {};
1239              options.controller = this;
1240  
1241              toolbar.view = new media.view.Toolbar.Select( options );
1242          }
1243      });
1244  
1245      /**
1246       * wp.media.view.MediaFrame.Post
1247       */
1248      media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
1249          initialize: function() {
1250              _.defaults( this.options, {
1251                  multiple:  true,
1252                  editing:   false,
1253                  state:    'insert'
1254              });
1255  
1256              media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
1257              this.createIframeStates();
1258          },
1259  
1260          createStates: function() {
1261              var options = this.options;
1262  
1263              // Add the default states.
1264              this.states.add([
1265                  // Main states.
1266                  new media.controller.Library({
1267                      id:         'insert',
1268                      title:      l10n.insertMediaTitle,
1269                      priority:   20,
1270                      toolbar:    'main-insert',
1271                      filterable: 'all',
1272                      library:    media.query( options.library ),
1273                      multiple:   options.multiple ? 'reset' : false,
1274                      editable:   true,
1275  
1276                      // If the user isn't allowed to edit fields,
1277                      // can they still edit it locally?
1278                      allowLocalEdits: true,
1279  
1280                      // Show the attachment display settings.
1281                      displaySettings: true,
1282                      // Update user settings when users adjust the
1283                      // attachment display settings.
1284                      displayUserSettings: true
1285                  }),
1286  
1287                  new media.controller.Library({
1288                      id:         'gallery',
1289                      title:      l10n.createGalleryTitle,
1290                      priority:   40,
1291                      toolbar:    'main-gallery',
1292                      filterable: 'uploaded',
1293                      multiple:   'add',
1294                      editable:   false,
1295  
1296                      library:  media.query( _.defaults({
1297                          type: 'image'
1298                      }, options.library ) )
1299                  }),
1300  
1301                  // Embed states.
1302                  new media.controller.Embed(),
1303  
1304                  // Gallery states.
1305                  new media.controller.GalleryEdit({
1306                      library: options.selection,
1307                      editing: options.editing,
1308                      menu:    'gallery'
1309                  }),
1310  
1311                  new media.controller.GalleryAdd()
1312              ]);
1313  
1314  
1315              if ( media.view.settings.post.featuredImageId ) {
1316                  this.states.add( new media.controller.FeaturedImage() );
1317              }
1318          },
1319  
1320          bindHandlers: function() {
1321              media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
1322              this.on( 'menu:create:gallery', this.createMenu, this );
1323              this.on( 'toolbar:create:main-insert', this.createToolbar, this );
1324              this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
1325              this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
1326              this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
1327  
1328              var handlers = {
1329                      menu: {
1330                          'default': 'mainMenu',
1331                          'gallery': 'galleryMenu'
1332                      },
1333  
1334                      content: {
1335                          'embed':          'embedContent',
1336                          'edit-selection': 'editSelectionContent'
1337                      },
1338  
1339                      toolbar: {
1340                          'main-insert':      'mainInsertToolbar',
1341                          'main-gallery':     'mainGalleryToolbar',
1342                          'gallery-edit':     'galleryEditToolbar',
1343                          'gallery-add':      'galleryAddToolbar'
1344                      }
1345                  };
1346  
1347              _.each( handlers, function( regionHandlers, region ) {
1348                  _.each( regionHandlers, function( callback, handler ) {
1349                      this.on( region + ':render:' + handler, this[ callback ], this );
1350                  }, this );
1351              }, this );
1352          },
1353  
1354          // Menus
1355          mainMenu: function( view ) {
1356              view.set({
1357                  'library-separator': new media.View({
1358                      className: 'separator',
1359                      priority: 100
1360                  })
1361              });
1362          },
1363  
1364          galleryMenu: function( view ) {
1365              var lastState = this.lastState(),
1366                  previous = lastState && lastState.id,
1367                  frame = this;
1368  
1369              view.set({
1370                  cancel: {
1371                      text:     l10n.cancelGalleryTitle,
1372                      priority: 20,
1373                      click:    function() {
1374                          if ( previous )
1375                              frame.setState( previous );
1376                          else
1377                              frame.close();
1378                      }
1379                  },
1380                  separateCancel: new media.View({
1381                      className: 'separator',
1382                      priority: 40
1383                  })
1384              });
1385          },
1386  
1387          // Content
1388          embedContent: function() {
1389              var view = new media.view.Embed({
1390                  controller: this,
1391                  model:      this.state()
1392              }).render();
1393  
1394              this.content.set( view );
1395              view.url.focus();
1396          },
1397  
1398          editSelectionContent: function() {
1399              var state = this.state(),
1400                  selection = state.get('selection'),
1401                  view;
1402  
1403              view = new media.view.AttachmentsBrowser({
1404                  controller: this,
1405                  collection: selection,
1406                  selection:  selection,
1407                  model:      state,
1408                  sortable:   true,
1409                  search:     false,
1410                  dragInfo:   true,
1411  
1412                  AttachmentView: media.view.Attachment.EditSelection
1413              }).render();
1414  
1415              view.toolbar.set( 'backToLibrary', {
1416                  text:     l10n.returnToLibrary,
1417                  priority: -100,
1418  
1419                  click: function() {
1420                      this.controller.content.mode('browse');
1421                  }
1422              });
1423  
1424              // Browse our library of attachments.
1425              this.content.set( view );
1426          },
1427  
1428          // Toolbars
1429          selectionStatusToolbar: function( view ) {
1430              var editable = this.state().get('editable');
1431  
1432              view.set( 'selection', new media.view.Selection({
1433                  controller: this,
1434                  collection: this.state().get('selection'),
1435                  priority:   -40,
1436  
1437                  // If the selection is editable, pass the callback to
1438                  // switch the content mode.
1439                  editable: editable && function() {
1440                      this.controller.content.mode('edit-selection');
1441                  }
1442              }).render() );
1443          },
1444  
1445          mainInsertToolbar: function( view ) {
1446              var controller = this;
1447  
1448              this.selectionStatusToolbar( view );
1449  
1450              view.set( 'insert', {
1451                  style:    'primary',
1452                  priority: 80,
1453                  text:     l10n.insertIntoPost,
1454                  requires: { selection: true },
1455  
1456                  click: function() {
1457                      var state = controller.state(),
1458                          selection = state.get('selection');
1459  
1460                      controller.close();
1461                      state.trigger( 'insert', selection ).reset();
1462                  }
1463              });
1464          },
1465  
1466          mainGalleryToolbar: function( view ) {
1467              var controller = this;
1468  
1469              this.selectionStatusToolbar( view );
1470  
1471              view.set( 'gallery', {
1472                  style:    'primary',
1473                  text:     l10n.createNewGallery,
1474                  priority: 60,
1475                  requires: { selection: true },
1476  
1477                  click: function() {
1478                      var selection = controller.state().get('selection'),
1479                          edit = controller.state('gallery-edit'),
1480                          models = selection.where({ type: 'image' });
1481  
1482                      edit.set( 'library', new media.model.Selection( models, {
1483                          props:    selection.props.toJSON(),
1484                          multiple: true
1485                      }) );
1486  
1487                      this.controller.setState('gallery-edit');
1488                  }
1489              });
1490          },
1491  
1492          featuredImageToolbar: function( toolbar ) {
1493              this.createSelectToolbar( toolbar, {
1494                  text:  l10n.setFeaturedImage,
1495                  state: this.options.state
1496              });
1497          },
1498  
1499          mainEmbedToolbar: function( toolbar ) {
1500              toolbar.view = new media.view.Toolbar.Embed({
1501                  controller: this
1502              });
1503          },
1504  
1505          galleryEditToolbar: function() {
1506              var editing = this.state().get('editing');
1507              this.toolbar.set( new media.view.Toolbar({
1508                  controller: this,
1509                  items: {
1510                      insert: {
1511                          style:    'primary',
1512                          text:     editing ? l10n.updateGallery : l10n.insertGallery,
1513                          priority: 80,
1514                          requires: { library: true },
1515  
1516                          click: function() {
1517                              var controller = this.controller,
1518                                  state = controller.state();
1519  
1520                              controller.close();
1521                              state.trigger( 'update', state.get('library') );
1522  
1523                              // Restore and reset the default state.
1524                              controller.setState( controller.options.state );
1525                              controller.reset();
1526                          }
1527                      }
1528                  }
1529              }) );
1530          },
1531  
1532          galleryAddToolbar: function() {
1533              this.toolbar.set( new media.view.Toolbar({
1534                  controller: this,
1535                  items: {
1536                      insert: {
1537                          style:    'primary',
1538                          text:     l10n.addToGallery,
1539                          priority: 80,
1540                          requires: { selection: true },
1541  
1542                          click: function() {
1543                              var controller = this.controller,
1544                                  state = controller.state(),
1545                                  edit = controller.state('gallery-edit');
1546  
1547                              edit.get('library').add( state.get('selection').models );
1548                              state.trigger('reset');
1549                              controller.setState('gallery-edit');
1550                          }
1551                      }
1552                  }
1553              }) );
1554          }
1555      });
1556  
1557      /**
1558       * wp.media.view.Modal
1559       */
1560      media.view.Modal = media.View.extend({
1561          tagName:  'div',
1562          template: media.template('media-modal'),
1563  
1564          attributes: {
1565              tabindex: 0
1566          },
1567  
1568          events: {
1569              'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
1570              'keydown': 'keydown'
1571          },
1572  
1573          initialize: function() {
1574              _.defaults( this.options, {
1575                  container: document.body,
1576                  title:     '',
1577                  propagate: true,
1578                  freeze:    true
1579              });
1580          },
1581  
1582          prepare: function() {
1583              return {
1584                  title: this.options.title
1585              };
1586          },
1587  
1588          attach: function() {
1589              if ( this.views.attached )
1590                  return this;
1591  
1592              if ( ! this.views.rendered )
1593                  this.render();
1594  
1595              this.$el.appendTo( this.options.container );
1596  
1597              // Manually mark the view as attached and trigger ready.
1598              this.views.attached = true;
1599              this.views.ready();
1600  
1601              return this.propagate('attach');
1602          },
1603  
1604          detach: function() {
1605              if ( this.$el.is(':visible') )
1606                  this.close();
1607  
1608              this.$el.detach();
1609              this.views.attached = false;
1610              return this.propagate('detach');
1611          },
1612  
1613          open: function() {
1614              var $el = this.$el,
1615                  options = this.options;
1616  
1617              if ( $el.is(':visible') )
1618                  return this;
1619  
1620              if ( ! this.views.attached )
1621                  this.attach();
1622  
1623              // If the `freeze` option is set, record the window's scroll position.
1624              if ( options.freeze ) {
1625                  this._freeze = {
1626                      scrollTop: $( window ).scrollTop()
1627                  };
1628              }
1629  
1630              $el.show().focus();
1631              return this.propagate('open');
1632          },
1633  
1634          close: function( options ) {
1635              var freeze = this._freeze;
1636  
1637              if ( ! this.views.attached || ! this.$el.is(':visible') )
1638                  return this;
1639  
1640              this.$el.hide();
1641              this.propagate('close');
1642  
1643              // If the `freeze` option is set, restore the container's scroll position.
1644              if ( freeze ) {
1645                  $( window ).scrollTop( freeze.scrollTop );
1646              }
1647  
1648              if ( options && options.escape )
1649                  this.propagate('escape');
1650  
1651              return this;
1652          },
1653  
1654          escape: function() {
1655              return this.close({ escape: true });
1656          },
1657  
1658          escapeHandler: function( event ) {
1659              event.preventDefault();
1660              this.escape();
1661          },
1662  
1663          content: function( content ) {
1664              this.views.set( '.media-modal-content', content );
1665              return this;
1666          },
1667  
1668          // Triggers a modal event and if the `propagate` option is set,
1669          // forwards events to the modal's controller.
1670          propagate: function( id ) {
1671              this.trigger( id );
1672  
1673              if ( this.options.propagate )
1674                  this.controller.trigger( id );
1675  
1676              return this;
1677          },
1678  
1679          keydown: function( event ) {
1680              // Close the modal when escape is pressed.
1681              if ( 27 === event.which ) {
1682                  event.preventDefault();
1683                  this.escape();
1684                  return;
1685              }
1686          }
1687      });
1688  
1689      // wp.media.view.FocusManager
1690      // ----------------------------
1691      media.view.FocusManager = media.View.extend({
1692          events: {
1693              keydown: 'recordTab',
1694              focusin: 'updateIndex'
1695          },
1696  
1697          focus: function() {
1698              if ( _.isUndefined( this.index ) )
1699                  return;
1700  
1701              // Update our collection of `$tabbables`.
1702              this.$tabbables = this.$(':tabbable');
1703  
1704              // If tab is saved, focus it.
1705              this.$tabbables.eq( this.index ).focus();
1706          },
1707  
1708          recordTab: function( event ) {
1709              // Look for the tab key.
1710              if ( 9 !== event.keyCode )
1711                  return;
1712  
1713              // First try to update the index.
1714              if ( _.isUndefined( this.index ) )
1715                  this.updateIndex( event );
1716  
1717              // If we still don't have an index, bail.
1718              if ( _.isUndefined( this.index ) )
1719                  return;
1720  
1721              var index = this.index + ( event.shiftKey ? -1 : 1 );
1722  
1723              if ( index >= 0 && index < this.$tabbables.length )
1724                  this.index = index;
1725              else
1726                  delete this.index;
1727          },
1728  
1729          updateIndex: function( event ) {
1730              this.$tabbables = this.$(':tabbable');
1731  
1732              var index = this.$tabbables.index( event.target );
1733  
1734              if ( -1 === index )
1735                  delete this.index;
1736              else
1737                  this.index = index;
1738          }
1739      });
1740  
1741      // wp.media.view.UploaderWindow
1742      // ----------------------------
1743      media.view.UploaderWindow = media.View.extend({
1744          tagName:   'div',
1745          className: 'uploader-window',
1746          template:  media.template('uploader-window'),
1747  
1748          initialize: function() {
1749              var uploader;
1750  
1751              this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
1752  
1753              uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
1754                  dropzone:  this.$el,
1755                  browser:   this.$browser,
1756                  params:    {}
1757              });
1758  
1759              // Ensure the dropzone is a jQuery collection.
1760              if ( uploader.dropzone && ! (uploader.dropzone instanceof $) )
1761                  uploader.dropzone = $( uploader.dropzone );
1762  
1763              this.controller.on( 'activate', this.refresh, this );
1764          },
1765  
1766          refresh: function() {
1767              if ( this.uploader )
1768                  this.uploader.refresh();
1769          },
1770  
1771          ready: function() {
1772              var postId = media.view.settings.post.id,
1773                  dropzone;
1774  
1775              // If the uploader already exists, bail.
1776              if ( this.uploader )
1777                  return;
1778  
1779              if ( postId )
1780                  this.options.uploader.params.post_id = postId;
1781  
1782              this.uploader = new wp.Uploader( this.options.uploader );
1783  
1784              dropzone = this.uploader.dropzone;
1785              dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
1786              dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
1787          },
1788  
1789          show: function() {
1790              var $el = this.$el.show();
1791  
1792              // Ensure that the animation is triggered by waiting until
1793              // the transparent element is painted into the DOM.
1794              _.defer( function() {
1795                  $el.css({ opacity: 1 });
1796              });
1797          },
1798  
1799          hide: function() {
1800              var $el = this.$el.css({ opacity: 0 });
1801  
1802              media.transition( $el ).done( function() {
1803                  // Transition end events are subject to race conditions.
1804                  // Make sure that the value is set as intended.
1805                  if ( '0' === $el.css('opacity') )
1806                      $el.hide();
1807              });
1808          }
1809      });
1810  
1811      media.view.UploaderInline = media.View.extend({
1812          tagName:   'div',
1813          className: 'uploader-inline',
1814          template:  media.template('uploader-inline'),
1815  
1816          initialize: function() {
1817              _.defaults( this.options, {
1818                  message: '',
1819                  status:  true
1820              });
1821  
1822              if ( ! this.options.$browser && this.controller.uploader )
1823                  this.options.$browser = this.controller.uploader.$browser;
1824  
1825              if ( _.isUndefined( this.options.postId ) )
1826                  this.options.postId = media.view.settings.post.id;
1827  
1828              if ( this.options.status ) {
1829                  this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
1830                      controller: this.controller
1831                  }) );
1832              }
1833          },
1834  
1835          dispose: function() {
1836              if ( this.disposing )
1837                  return media.View.prototype.dispose.apply( this, arguments );
1838  
1839              // Run remove on `dispose`, so we can be sure to refresh the
1840              // uploader with a view-less DOM. Track whether we're disposing
1841              // so we don't trigger an infinite loop.
1842              this.disposing = true;
1843              return this.remove();
1844          },
1845  
1846          remove: function() {
1847              var result = media.View.prototype.remove.apply( this, arguments );
1848  
1849              _.defer( _.bind( this.refresh, this ) );
1850              return result;
1851          },
1852  
1853          refresh: function() {
1854              var uploader = this.controller.uploader;
1855  
1856              if ( uploader )
1857                  uploader.refresh();
1858          },
1859  
1860          ready: function() {
1861              var $browser = this.options.$browser,
1862                  $placeholder;
1863  
1864              if ( this.controller.uploader ) {
1865                  $placeholder = this.$('.browser');
1866  
1867                  // Check if we've already replaced the placeholder.
1868                  if ( $placeholder[0] === $browser[0] )
1869                      return;
1870  
1871                  $browser.detach().text( $placeholder.text() );
1872                  $browser[0].className = $placeholder[0].className;
1873                  $placeholder.replaceWith( $browser.show() );
1874              }
1875  
1876              this.refresh();
1877              return this;
1878          }
1879      });
1880  
1881      /**
1882       * wp.media.view.UploaderStatus
1883       */
1884      media.view.UploaderStatus = media.View.extend({
1885          className: 'media-uploader-status',
1886          template:  media.template('uploader-status'),
1887  
1888          events: {
1889              'click .upload-dismiss-errors': 'dismiss'
1890          },
1891  
1892          initialize: function() {
1893              this.queue = wp.Uploader.queue;
1894              this.queue.on( 'add remove reset', this.visibility, this );
1895              this.queue.on( 'add remove reset change:percent', this.progress, this );
1896              this.queue.on( 'add remove reset change:uploading', this.info, this );
1897  
1898              this.errors = wp.Uploader.errors;
1899              this.errors.reset();
1900              this.errors.on( 'add remove reset', this.visibility, this );
1901              this.errors.on( 'add', this.error, this );
1902          },
1903  
1904          dispose: function() {
1905              wp.Uploader.queue.off( null, null, this );
1906              media.View.prototype.dispose.apply( this, arguments );
1907              return this;
1908          },
1909  
1910          visibility: function() {
1911              this.$el.toggleClass( 'uploading', !! this.queue.length );
1912              this.$el.toggleClass( 'errors', !! this.errors.length );
1913              this.$el.toggle( !! this.queue.length || !! this.errors.length );
1914          },
1915  
1916          ready: function() {
1917              _.each({
1918                  '$bar':      '.media-progress-bar div',
1919                  '$index':    '.upload-index',
1920                  '$total':    '.upload-total',
1921                  '$filename': '.upload-filename'
1922              }, function( selector, key ) {
1923                  this[ key ] = this.$( selector );
1924              }, this );
1925  
1926              this.visibility();
1927              this.progress();
1928              this.info();
1929          },
1930  
1931          progress: function() {
1932              var queue = this.queue,
1933                  $bar = this.$bar;
1934  
1935              if ( ! $bar || ! queue.length )
1936                  return;
1937  
1938              $bar.width( ( queue.reduce( function( memo, attachment ) {
1939                  if ( ! attachment.get('uploading') )
1940                      return memo + 100;
1941  
1942                  var percent = attachment.get('percent');
1943                  return memo + ( _.isNumber( percent ) ? percent : 100 );
1944              }, 0 ) / queue.length ) + '%' );
1945          },
1946  
1947          info: function() {
1948              var queue = this.queue,
1949                  index = 0, active;
1950  
1951              if ( ! queue.length )
1952                  return;
1953  
1954              active = this.queue.find( function( attachment, i ) {
1955                  index = i;
1956                  return attachment.get('uploading');
1957              });
1958  
1959              this.$index.text( index + 1 );
1960              this.$total.text( queue.length );
1961              this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
1962          },
1963  
1964          filename: function( filename ) {
1965              return media.truncate( _.escape( filename ), 24 );
1966          },
1967  
1968          error: function( error ) {
1969              this.views.add( '.upload-errors', new media.view.UploaderStatusError({
1970                  filename: this.filename( error.get('file').name ),
1971                  message:  error.get('message')
1972              }), { at: 0 });
1973          },
1974  
1975          dismiss: function( event ) {
1976              var errors = this.views.get('.upload-errors');
1977  
1978              event.preventDefault();
1979  
1980              if ( errors )
1981                  _.invoke( errors, 'remove' );
1982              wp.Uploader.errors.reset();
1983          }
1984      });
1985  
1986      media.view.UploaderStatusError = media.View.extend({
1987          className: 'upload-error',
1988          template:  media.template('uploader-status-error')
1989      });
1990  
1991      /**
1992       * wp.media.view.Toolbar
1993       */
1994      media.view.Toolbar = media.View.extend({
1995          tagName:   'div',
1996          className: 'media-toolbar',
1997  
1998          initialize: function() {
1999              var state = this.controller.state(),
2000                  selection = this.selection = state.get('selection'),
2001                  library = this.library = state.get('library');
2002  
2003              this._views = {};
2004  
2005              // The toolbar is composed of two `PriorityList` views.
2006              this.primary   = new media.view.PriorityList();
2007              this.secondary = new media.view.PriorityList();
2008              this.primary.$el.addClass('media-toolbar-primary');
2009              this.secondary.$el.addClass('media-toolbar-secondary');
2010  
2011              this.views.set([ this.secondary, this.primary ]);
2012  
2013              if ( this.options.items )
2014                  this.set( this.options.items, { silent: true });
2015  
2016              if ( ! this.options.silent )
2017                  this.render();
2018  
2019              if ( selection )
2020                  selection.on( 'add remove reset', this.refresh, this );
2021              if ( library )
2022                  library.on( 'add remove reset', this.refresh, this );
2023          },
2024  
2025          dispose: function() {
2026              if ( this.selection )
2027                  this.selection.off( null, null, this );
2028              if ( this.library )
2029                  this.library.off( null, null, this );
2030              return media.View.prototype.dispose.apply( this, arguments );
2031          },
2032  
2033          ready: function() {
2034              this.refresh();
2035          },
2036  
2037          set: function( id, view, options ) {
2038              var list;
2039              options = options || {};
2040  
2041              // Accept an object with an `id` : `view` mapping.
2042              if ( _.isObject( id ) ) {
2043                  _.each( id, function( view, id ) {
2044                      this.set( id, view, { silent: true });
2045                  }, this );
2046  
2047              } else {
2048                  if ( ! ( view instanceof Backbone.View ) ) {
2049                      view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
2050                      view = new media.view.Button( view ).render();
2051                  }
2052  
2053                  view.controller = view.controller || this.controller;
2054  
2055                  this._views[ id ] = view;
2056  
2057                  list = view.options.priority < 0 ? 'secondary' : 'primary';
2058                  this[ list ].set( id, view, options );
2059              }
2060  
2061              if ( ! options.silent )
2062                  this.refresh();
2063  
2064              return this;
2065          },
2066  
2067          get: function( id ) {
2068              return this._views[ id ];
2069          },
2070  
2071          unset: function( id, options ) {
2072              delete this._views[ id ];
2073              this.primary.unset( id, options );
2074              this.secondary.unset( id, options );
2075  
2076              if ( ! options || ! options.silent )
2077                  this.refresh();
2078              return this;
2079          },
2080  
2081          refresh: function() {
2082              var state = this.controller.state(),
2083                  library = state.get('library'),
2084                  selection = state.get('selection');
2085  
2086              _.each( this._views, function( button ) {
2087                  if ( ! button.model || ! button.options || ! button.options.requires )
2088                      return;
2089  
2090                  var requires = button.options.requires,
2091                      disabled = false;
2092  
2093                  // Prevent insertion of attachments if any of them are still uploading
2094                  disabled = _.some( selection.models, function( attachment ) {
2095                      return attachment.get('uploading') === true;
2096                  });
2097  
2098                  if ( requires.selection && selection && ! selection.length )
2099                      disabled = true;
2100                  else if ( requires.library && library && ! library.length )
2101                      disabled = true;
2102  
2103                  button.model.set( 'disabled', disabled );
2104              });
2105          }
2106      });
2107  
2108      // wp.media.view.Toolbar.Select
2109      // ----------------------------
2110      media.view.Toolbar.Select = media.view.Toolbar.extend({
2111          initialize: function() {
2112              var options = this.options;
2113  
2114              _.bindAll( this, 'clickSelect' );
2115  
2116              _.defaults( options, {
2117                  event: 'select',
2118                  state: false,
2119                  reset: true,
2120                  close: true,
2121                  text:  l10n.select,
2122  
2123                  // Does the button rely on the selection?
2124                  requires: {
2125                      selection: true
2126                  }
2127              });
2128  
2129              options.items = _.defaults( options.items || {}, {
2130                  select: {
2131                      style:    'primary',
2132                      text:     options.text,
2133                      priority: 80,
2134                      click:    this.clickSelect,
2135                      requires: options.requires
2136                  }
2137              });
2138  
2139              media.view.Toolbar.prototype.initialize.apply( this, arguments );
2140          },
2141  
2142          clickSelect: function() {
2143              var options = this.options,
2144                  controller = this.controller;
2145  
2146              if ( options.close )
2147                  controller.close();
2148  
2149              if ( options.event )
2150                  controller.state().trigger( options.event );
2151  
2152              if ( options.state )
2153                  controller.setState( options.state );
2154  
2155              if ( options.reset )
2156                  controller.reset();
2157          }
2158      });
2159  
2160      // wp.media.view.Toolbar.Embed
2161      // ---------------------------
2162      media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
2163          initialize: function() {
2164              _.defaults( this.options, {
2165                  text: l10n.insertIntoPost,
2166                  requires: false
2167              });
2168  
2169              media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
2170          },
2171  
2172          refresh: function() {
2173              var url = this.controller.state().props.get('url');
2174              this.get('select').model.set( 'disabled', ! url || url === 'http://' );
2175  
2176              media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
2177          }
2178      });
2179  
2180      /**
2181       * wp.media.view.Button
2182       */
2183      media.view.Button = media.View.extend({
2184          tagName:    'a',
2185          className:  'media-button',
2186          attributes: { href: '#' },
2187  
2188          events: {
2189              'click': 'click'
2190          },
2191  
2192          defaults: {
2193              text:     '',
2194              style:    '',
2195              size:     'large',
2196              disabled: false
2197          },
2198  
2199          initialize: function() {
2200              // Create a model with the provided `defaults`.
2201              this.model = new Backbone.Model( this.defaults );
2202  
2203              // If any of the `options` have a key from `defaults`, apply its
2204              // value to the `model` and remove it from the `options object.
2205              _.each( this.defaults, function( def, key ) {
2206                  var value = this.options[ key ];
2207                  if ( _.isUndefined( value ) )
2208                      return;
2209  
2210                  this.model.set( key, value );
2211                  delete this.options[ key ];
2212              }, this );
2213  
2214              this.model.on( 'change', this.render, this );
2215          },
2216  
2217          render: function() {
2218              var classes = [ 'button', this.className ],
2219                  model = this.model.toJSON();
2220  
2221              if ( model.style )
2222                  classes.push( 'button-' + model.style );
2223  
2224              if ( model.size )
2225                  classes.push( 'button-' + model.size );
2226  
2227              classes = _.uniq( classes.concat( this.options.classes ) );
2228              this.el.className = classes.join(' ');
2229  
2230              this.$el.attr( 'disabled', model.disabled );
2231              this.$el.text( this.model.get('text') );
2232  
2233              return this;
2234          },
2235  
2236          click: function( event ) {
2237              if ( '#' === this.attributes.href )
2238                  event.preventDefault();
2239  
2240              if ( this.options.click && ! this.model.get('disabled') )
2241                  this.options.click.apply( this, arguments );
2242          }
2243      });
2244  
2245      /**
2246       * wp.media.view.ButtonGroup
2247       */
2248      media.view.ButtonGroup = media.View.extend({
2249          tagName:   'div',
2250          className: 'button-group button-large media-button-group',
2251  
2252          initialize: function() {
2253              this.buttons = _.map( this.options.buttons || [], function( button ) {
2254                  if ( button instanceof Backbone.View )
2255                      return button;
2256                  else
2257                      return new media.view.Button( button ).render();
2258              });
2259  
2260              delete this.options.buttons;
2261  
2262              if ( this.options.classes )
2263                  this.$el.addClass( this.options.classes );
2264          },
2265  
2266          render: function() {
2267              this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
2268              return this;
2269          }
2270      });
2271  
2272      /**
2273       * wp.media.view.PriorityList
2274       */
2275  
2276      media.view.PriorityList = media.View.extend({
2277          tagName:   'div',
2278  
2279          initialize: function() {
2280              this._views = {};
2281  
2282              this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
2283              delete this.options.views;
2284  
2285              if ( ! this.options.silent )
2286                  this.render();
2287          },
2288  
2289          set: function( id, view, options ) {
2290              var priority, views, index;
2291  
2292              options = options || {};
2293  
2294              // Accept an object with an `id` : `view` mapping.
2295              if ( _.isObject( id ) ) {
2296                  _.each( id, function( view, id ) {
2297                      this.set( id, view );
2298                  }, this );
2299                  return this;
2300              }
2301  
2302              if ( ! (view instanceof Backbone.View) )
2303                  view = this.toView( view, id, options );
2304  
2305              view.controller = view.controller || this.controller;
2306  
2307              this.unset( id );
2308  
2309              priority = view.options.priority || 10;
2310              views = this.views.get() || [];
2311  
2312              _.find( views, function( existing, i ) {
2313                  if ( existing.options.priority > priority ) {
2314                      index = i;
2315                      return true;
2316                  }
2317              });
2318  
2319              this._views[ id ] = view;
2320              this.views.add( view, {
2321                  at: _.isNumber( index ) ? index : views.length || 0
2322              });
2323  
2324              return this;
2325          },
2326  
2327          get: function( id ) {
2328              return this._views[ id ];
2329          },
2330  
2331          unset: function( id ) {
2332              var view = this.get( id );
2333  
2334              if ( view )
2335                  view.remove();
2336  
2337              delete this._views[ id ];
2338              return this;
2339          },
2340  
2341          toView: function( options ) {
2342              return new media.View( options );
2343          }
2344      });
2345  
2346      /**
2347       * wp.media.view.MenuItem
2348       */
2349      media.view.MenuItem = media.View.extend({
2350          tagName:   'a',
2351          className: 'media-menu-item',
2352  
2353          attributes: {
2354              href: '#'
2355          },
2356  
2357          events: {
2358              'click': '_click'
2359          },
2360  
2361          _click: function( event ) {
2362              var clickOverride = this.options.click;
2363  
2364              if ( event )
2365                  event.preventDefault();
2366  
2367              if ( clickOverride )
2368                  clickOverride.call( this );
2369              else
2370                  this.click();
2371          },
2372  
2373          click: function() {
2374              var state = this.options.state;
2375              if ( state )
2376                  this.controller.setState( state );
2377          },
2378  
2379          render: function() {
2380              var options = this.options;
2381  
2382              if ( options.text )
2383                  this.$el.text( options.text );
2384              else if ( options.html )
2385                  this.$el.html( options.html );
2386  
2387              return this;
2388          }
2389      });
2390  
2391      /**
2392       * wp.media.view.Menu
2393       */
2394      media.view.Menu = media.view.PriorityList.extend({
2395          tagName:   'div',
2396          className: 'media-menu',
2397          property:  'state',
2398          ItemView:  media.view.MenuItem,
2399          region:    'menu',
2400  
2401          toView: function( options, id ) {
2402              options = options || {};
2403              options[ this.property ] = options[ this.property ] || id;
2404              return new this.ItemView( options ).render();
2405          },
2406  
2407          ready: function() {
2408              media.view.PriorityList.prototype.ready.apply( this, arguments );
2409              this.visibility();
2410          },
2411  
2412          set: function() {
2413              media.view.PriorityList.prototype.set.apply( this, arguments );
2414              this.visibility();
2415          },
2416  
2417          unset: function() {
2418              media.view.PriorityList.prototype.unset.apply( this, arguments );
2419              this.visibility();
2420          },
2421  
2422          visibility: function() {
2423              var region = this.region,
2424                  view = this.controller[ region ].get(),
2425                  views = this.views.get(),
2426                  hide = ! views || views.length < 2;
2427  
2428              if ( this === view )
2429                  this.controller.$el.toggleClass( 'hide-' + region, hide );
2430          },
2431  
2432          select: function( id ) {
2433              var view = this.get( id );
2434  
2435              if ( ! view )
2436                  return;
2437  
2438              this.deselect();
2439              view.$el.addClass('active');
2440          },
2441  
2442          deselect: function() {
2443              this.$el.children().removeClass('active');
2444          }
2445      });
2446  
2447      /**
2448       * wp.media.view.RouterItem
2449       */
2450      media.view.RouterItem = media.view.MenuItem.extend({
2451          click: function() {
2452              var contentMode = this.options.contentMode;
2453              if ( contentMode )
2454                  this.controller.content.mode( contentMode );
2455          }
2456      });
2457  
2458      /**
2459       * wp.media.view.Router
2460       */
2461      media.view.Router = media.view.Menu.extend({
2462          tagName:   'div',
2463          className: 'media-router',
2464          property:  'contentMode',
2465          ItemView:  media.view.RouterItem,
2466          region:    'router',
2467  
2468          initialize: function() {
2469              this.controller.on( 'content:render', this.update, this );
2470              media.view.Menu.prototype.initialize.apply( this, arguments );
2471          },
2472  
2473          update: function() {
2474              var mode = this.controller.content.mode();
2475              if ( mode )
2476                  this.select( mode );
2477          }
2478      });
2479  
2480  
2481      /**
2482       * wp.media.view.Sidebar
2483       */
2484      media.view.Sidebar = media.view.PriorityList.extend({
2485          className: 'media-sidebar'
2486      });
2487  
2488      /**
2489       * wp.media.view.Attachment
2490       */
2491      media.view.Attachment = media.View.extend({
2492          tagName:   'li',
2493          className: 'attachment',
2494          template:  media.template('attachment'),
2495  
2496          events: {
2497              'click .attachment-preview':      'toggleSelectionHandler',
2498              'change [data-setting]':          'updateSetting',
2499              'change [data-setting] input':    'updateSetting',
2500              'change [data-setting] select':   'updateSetting',
2501              'change [data-setting] textarea': 'updateSetting',
2502              'click .close':                   'removeFromLibrary',
2503              'click .check':                   'removeFromSelection',
2504              'click a':                        'preventDefault'
2505          },
2506  
2507          buttons: {},
2508  
2509          initialize: function() {
2510              var selection = this.options.selection;
2511  
2512              this.model.on( 'change:sizes change:uploading', this.render, this );
2513              this.model.on( 'change:title', this._syncTitle, this );
2514              this.model.on( 'change:caption', this._syncCaption, this );
2515              this.model.on( 'change:percent', this.progress, this );
2516  
2517              // Update the selection.
2518              this.model.on( 'add', this.select, this );
2519              this.model.on( 'remove', this.deselect, this );
2520              if ( selection )
2521                  selection.on( 'reset', this.updateSelect, this );
2522  
2523              // Update the model's details view.
2524              this.model.on( 'selection:single selection:unsingle', this.details, this );
2525              this.details( this.model, this.controller.state().get('selection') );
2526          },
2527  
2528          dispose: function() {
2529              var selection = this.options.selection;
2530  
2531              // Make sure all settings are saved before removing the view.
2532              this.updateAll();
2533  
2534              if ( selection )
2535                  selection.off( null, null, this );
2536  
2537              media.View.prototype.dispose.apply( this, arguments );
2538              return this;
2539          },
2540  
2541          render: function() {
2542              var options = _.defaults( this.model.toJSON(), {
2543                      orientation:   'landscape',
2544                      uploading:     false,
2545                      type:          '',
2546                      subtype:       '',
2547                      icon:          '',
2548                      filename:      '',
2549                      caption:       '',
2550                      title:         '',
2551                      dateFormatted: '',
2552                      width:         '',
2553                      height:        '',
2554                      compat:        false,
2555                      alt:           '',
2556                      description:   ''
2557                  });
2558  
2559              options.buttons  = this.buttons;
2560              options.describe = this.controller.state().get('describe');
2561  
2562              if ( 'image' === options.type )
2563                  options.size = this.imageSize();
2564  
2565              options.can = {};
2566              if ( options.nonces ) {
2567                  options.can.remove = !! options.nonces['delete'];
2568                  options.can.save = !! options.nonces.update;
2569              }
2570  
2571              if ( this.controller.state().get('allowLocalEdits') )
2572                  options.allowLocalEdits = true;
2573  
2574              this.views.detach();
2575              this.$el.html( this.template( options ) );
2576  
2577              this.$el.toggleClass( 'uploading', options.uploading );
2578              if ( options.uploading )
2579                  this.$bar = this.$('.media-progress-bar div');
2580              else
2581                  delete this.$bar;
2582  
2583              // Check if the model is selected.
2584              this.updateSelect();
2585  
2586              // Update the save status.
2587              this.updateSave();
2588  
2589              this.views.render();
2590  
2591              return this;
2592          },
2593  
2594          progress: function() {
2595              if ( this.$bar && this.$bar.length )
2596                  this.$bar.width( this.model.get('percent') + '%' );
2597          },
2598  
2599          toggleSelectionHandler: function( event ) {
2600              var method;
2601  
2602              if ( event.shiftKey )
2603                  method = 'between';
2604              else if ( event.ctrlKey || event.metaKey )
2605                  method = 'toggle';
2606  
2607              this.toggleSelection({
2608                  method: method
2609              });
2610          },
2611  
2612          toggleSelection: function( options ) {
2613              var collection = this.collection,
2614                  selection = this.options.selection,
2615                  model = this.model,
2616                  method = options && options.method,
2617                  single, models, singleIndex, modelIndex;
2618  
2619              if ( ! selection )
2620                  return;
2621  
2622              single = selection.single();
2623              method = _.isUndefined( method ) ? selection.multiple : method;
2624  
2625              // If the `method` is set to `between`, select all models that
2626              // exist between the current and the selected model.
2627              if ( 'between' === method && single && selection.multiple ) {
2628                  // If the models are the same, short-circuit.
2629                  if ( single === model )
2630                      return;
2631  
2632                  singleIndex = collection.indexOf( single );
2633                  modelIndex  = collection.indexOf( this.model );
2634  
2635                  if ( singleIndex < modelIndex )
2636                      models = collection.models.slice( singleIndex, modelIndex + 1 );
2637                  else
2638                      models = collection.models.slice( modelIndex, singleIndex + 1 );
2639  
2640                  selection.add( models ).single( model );
2641                  return;
2642  
2643              // If the `method` is set to `toggle`, just flip the selection
2644              // status, regardless of whether the model is the single model.
2645              } else if ( 'toggle' === method ) {
2646                  selection[ this.selected() ? 'remove' : 'add' ]( model ).single( model );
2647                  return;
2648              }
2649  
2650              if ( method !== 'add' )
2651                  method = 'reset';
2652  
2653              if ( this.selected() ) {
2654                  // If the model is the single model, remove it.
2655                  // If it is not the same as the single model,
2656                  // it now becomes the single model.
2657                  selection[ single === model ? 'remove' : 'single' ]( model );
2658              } else {
2659                  // If the model is not selected, run the `method` on the
2660                  // selection. By default, we `reset` the selection, but the
2661                  // `method` can be set to `add` the model to the selection.
2662                  selection[ method ]( model ).single( model );
2663              }
2664          },
2665  
2666          updateSelect: function() {
2667              this[ this.selected() ? 'select' : 'deselect' ]();
2668          },
2669  
2670          selected: function() {
2671              var selection = this.options.selection;
2672              if ( selection )
2673                  return !! selection.get( this.model.cid );
2674          },
2675  
2676          select: function( model, collection ) {
2677              var selection = this.options.selection;
2678  
2679              // Check if a selection exists and if it's the collection provided.
2680              // If they're not the same collection, bail; we're in another
2681              // selection's event loop.
2682              if ( ! selection || ( collection && collection !== selection ) )
2683                  return;
2684  
2685              this.$el.addClass('selected');
2686          },
2687  
2688          deselect: function( model, collection ) {
2689              var selection = this.options.selection;
2690  
2691              // Check if a selection exists and if it's the collection provided.
2692              // If they're not the same collection, bail; we're in another
2693              // selection's event loop.
2694              if ( ! selection || ( collection && collection !== selection ) )
2695                  return;
2696  
2697              this.$el.removeClass('selected');
2698          },
2699  
2700          details: function( model, collection ) {
2701              var selection = this.options.selection,
2702                  details;
2703  
2704              if ( selection !== collection )
2705                  return;
2706  
2707              details = selection.single();
2708              this.$el.toggleClass( 'details', details === this.model );
2709          },
2710  
2711          preventDefault: function( event ) {
2712              event.preventDefault();
2713          },
2714  
2715          imageSize: function( size ) {
2716              var sizes = this.model.get('sizes');
2717  
2718              size = size || 'medium';
2719  
2720              // Use the provided image size if possible.
2721              if ( sizes && sizes[ size ] ) {
2722                  return _.clone( sizes[ size ] );
2723              } else {
2724                  return {
2725                      url:         this.model.get('url'),
2726                      width:       this.model.get('width'),
2727                      height:      this.model.get('height'),
2728                      orientation: this.model.get('orientation')
2729                  };
2730              }
2731          },
2732  
2733          updateSetting: function( event ) {
2734              var $setting = $( event.target ).closest('[data-setting]'),
2735                  setting, value;
2736  
2737              if ( ! $setting.length )
2738                  return;
2739  
2740              setting = $setting.data('setting');
2741              value   = event.target.value;
2742  
2743              if ( this.model.get( setting ) !== value )
2744                  this.save( setting, value );
2745          },
2746  
2747          // Pass all the arguments to the model's save method.
2748          //
2749          // Records the aggregate status of all save requests and updates the
2750          // view's classes accordingly.
2751          save: function() {
2752              var view = this,
2753                  save = this._save = this._save || { status: 'ready' },
2754                  request = this.model.save.apply( this.model, arguments ),
2755                  requests = save.requests ? $.when( request, save.requests ) : request;
2756  
2757              // If we're waiting to remove 'Saved.', stop.
2758              if ( save.savedTimer )
2759                  clearTimeout( save.savedTimer );
2760  
2761              this.updateSave('waiting');
2762              save.requests = requests;
2763              requests.always( function() {
2764                  // If we've performed another request since this one, bail.
2765                  if ( save.requests !== requests )
2766                      return;
2767  
2768                  view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
2769                  save.savedTimer = setTimeout( function() {
2770                      view.updateSave('ready');
2771                      delete save.savedTimer;
2772                  }, 2000 );
2773              });
2774  
2775          },
2776  
2777          updateSave: function( status ) {
2778              var save = this._save = this._save || { status: 'ready' };
2779  
2780              if ( status && status !== save.status ) {
2781                  this.$el.removeClass( 'save-' + save.status );
2782                  save.status = status;
2783              }
2784  
2785              this.$el.addClass( 'save-' + save.status );
2786              return this;
2787          },
2788  
2789          updateAll: function() {
2790              var $settings = this.$('[data-setting]'),
2791                  model = this.model,
2792                  changed;
2793  
2794              changed = _.chain( $settings ).map( function( el ) {
2795                  var $input = $('input, textarea, select, [value]', el ),
2796                      setting, value;
2797  
2798                  if ( ! $input.length )
2799                      return;
2800  
2801                  setting = $(el).data('setting');
2802                  value = $input.val();
2803  
2804                  // Record the value if it changed.
2805                  if ( model.get( setting ) !== value )
2806                      return [ setting, value ];
2807              }).compact().object().value();
2808  
2809              if ( ! _.isEmpty( changed ) )
2810                  model.save( changed );
2811          },
2812  
2813          removeFromLibrary: function( event ) {
2814              // Stop propagation so the model isn't selected.
2815              event.stopPropagation();
2816  
2817              this.collection.remove( this.model );
2818          },
2819  
2820          removeFromSelection: function( event ) {
2821              var selection = this.options.selection;
2822              if ( ! selection )
2823                  return;
2824  
2825              // Stop propagation so the model isn't selected.
2826              event.stopPropagation();
2827  
2828              selection.remove( this.model );
2829          }
2830      });
2831  
2832      // Ensure settings remain in sync between attachment views.
2833      _.each({
2834          caption: '_syncCaption',
2835          title:   '_syncTitle'
2836      }, function( method, setting ) {
2837          media.view.Attachment.prototype[ method ] = function( model, value ) {
2838              var $setting = this.$('[data-setting="' + setting + '"]');
2839  
2840              if ( ! $setting.length )
2841                  return this;
2842  
2843              // If the updated value is in sync with the value in the DOM, there
2844              // is no need to re-render. If we're currently editing the value,
2845              // it will automatically be in sync, suppressing the re-render for
2846              // the view we're editing, while updating any others.
2847              if ( value === $setting.find('input, textarea, select, [value]').val() )
2848                  return this;
2849  
2850              return this.render();
2851          };
2852      });
2853  
2854      /**
2855       * wp.media.view.Attachment.Library
2856       */
2857      media.view.Attachment.Library = media.view.Attachment.extend({
2858          buttons: {
2859              check: true
2860          }
2861      });
2862  
2863      /**
2864       * wp.media.view.Attachment.EditLibrary
2865       */
2866      media.view.Attachment.EditLibrary = media.view.Attachment.extend({
2867          buttons: {
2868              close: true
2869          }
2870      });
2871  
2872      /**
2873       * wp.media.view.Attachments
2874       */
2875      media.view.Attachments = media.View.extend({
2876          tagName:   'ul',
2877          className: 'attachments',
2878  
2879          cssTemplate: media.template('attachments-css'),
2880  
2881          events: {
2882              'scroll': 'scroll'
2883          },
2884  
2885          initialize: function() {
2886              this.el.id = _.uniqueId('__attachments-view-');
2887  
2888              _.defaults( this.options, {
2889                  refreshSensitivity: 200,
2890                  refreshThreshold:   3,
2891                  AttachmentView:     media.view.Attachment,
2892                  sortable:           false,
2893                  resize:             true
2894              });
2895  
2896              this._viewsByCid = {};
2897  
2898              this.collection.on( 'add', function( attachment ) {
2899                  this.views.add( this.createAttachmentView( attachment ), {
2900                      at: this.collection.indexOf( attachment )
2901                  });
2902              }, this );
2903  
2904              this.collection.on( 'remove', function( attachment ) {
2905                  var view = this._viewsByCid[ attachment.cid ];
2906                  delete this._viewsByCid[ attachment.cid ];
2907  
2908                  if ( view )
2909                      view.remove();
2910              }, this );
2911  
2912              this.collection.on( 'reset', this.render, this );
2913  
2914              // Throttle the scroll handler.
2915              this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
2916  
2917              this.initSortable();
2918  
2919              _.bindAll( this, 'css' );
2920              this.model.on( 'change:edge change:gutter', this.css, this );
2921              this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity );
2922              if ( this.options.resize )
2923                  $(window).on( 'resize.attachments', this._resizeCss );
2924              this.css();
2925          },
2926  
2927          dispose: function() {
2928              this.collection.props.off( null, null, this );
2929              $(window).off( 'resize.attachments', this._resizeCss );
2930              media.View.prototype.dispose.apply( this, arguments );
2931          },
2932  
2933          css: function() {
2934              var $css = $( '#' + this.el.id + '-css' );
2935  
2936              if ( $css.length )
2937                  $css.remove();
2938  
2939              media.view.Attachments.$head().append( this.cssTemplate({
2940                  id:     this.el.id,
2941                  edge:   this.edge(),
2942                  gutter: this.model.get('gutter')
2943              }) );
2944          },
2945  
2946          edge: function() {
2947              var edge = this.model.get('edge'),
2948                  gutter, width, columns;
2949  
2950              if ( ! this.$el.is(':visible') )
2951                  return edge;
2952  
2953              gutter  = this.model.get('gutter') * 2;
2954              width   = this.$el.width() - gutter;
2955              columns = Math.ceil( width / ( edge + gutter ) );
2956              edge = Math.floor( ( width - ( columns * gutter ) ) / columns );
2957              return edge;
2958          },
2959  
2960          initSortable: function() {
2961              var collection = this.collection;
2962  
2963              if ( ! this.options.sortable || ! $.fn.sortable )
2964                  return;
2965  
2966              this.$el.sortable( _.extend({
2967                  // If the `collection` has a `comparator`, disable sorting.
2968                  disabled: !! collection.comparator,
2969  
2970                  // Prevent attachments from being dragged outside the bounding
2971                  // box of the list.
2972                  containment: this.$el,
2973  
2974                  // Change the position of the attachment as soon as the
2975                  // mouse pointer overlaps a thumbnail.
2976                  tolerance: 'pointer',
2977  
2978                  // Record the initial `index` of the dragged model.
2979                  start: function( event, ui ) {
2980                      ui.item.data('sortableIndexStart', ui.item.index());
2981                  },
2982  
2983                  // Update the model's index in the collection.
2984                  // Do so silently, as the view is already accurate.
2985                  update: function( event, ui ) {
2986                      var model = collection.at( ui.item.data('sortableIndexStart') ),
2987                          comparator = collection.comparator;
2988  
2989                      // Temporarily disable the comparator to prevent `add`
2990                      // from re-sorting.
2991                      delete collection.comparator;
2992  
2993                      // Silently shift the model to its new index.
2994                      collection.remove( model, {
2995                          silent: true
2996                      }).add( model, {
2997                          silent: true,
2998                          at:     ui.item.index()
2999                      });
3000  
3001                      // Restore the comparator.
3002                      collection.comparator = comparator;
3003  
3004                      // Fire the `reset` event to ensure other collections sync.
3005                      collection.trigger( 'reset', collection );
3006  
3007                      // If the collection is sorted by menu order,
3008                      // update the menu order.
3009                      collection.saveMenuOrder();
3010                  }
3011              }, this.options.sortable ) );
3012  
3013              // If the `orderby` property is changed on the `collection`,
3014              // check to see if we have a `comparator`. If so, disable sorting.
3015              collection.props.on( 'change:orderby', function() {
3016                  this.$el.sortable( 'option', 'disabled', !! collection.comparator );
3017              }, this );
3018  
3019              this.collection.props.on( 'change:orderby', this.refreshSortable, this );
3020              this.refreshSortable();
3021          },
3022  
3023          refreshSortable: function() {
3024              if ( ! this.options.sortable || ! $.fn.sortable )
3025                  return;
3026  
3027              // If the `collection` has a `comparator`, disable sorting.
3028              var collection = this.collection,
3029                  orderby = collection.props.get('orderby'),
3030                  enabled = 'menuOrder' === orderby || ! collection.comparator;
3031  
3032              this.$el.sortable( 'option', 'disabled', ! enabled );
3033          },
3034  
3035          createAttachmentView: function( attachment ) {
3036              var view = new this.options.AttachmentView({
3037                  controller: this.controller,
3038                  model:      attachment,
3039                  collection: this.collection,
3040                  selection:  this.options.selection
3041              });
3042  
3043              return this._viewsByCid[ attachment.cid ] = view;
3044          },
3045  
3046          prepare: function() {
3047              // Create all of the Attachment views, and replace
3048              // the list in a single DOM operation.
3049              if ( this.collection.length ) {
3050                  this.views.set( this.collection.map( this.createAttachmentView, this ) );
3051  
3052              // If there are no elements, clear the views and load some.
3053              } else {
3054                  this.views.unset();
3055                  this.collection.more().done( this.scroll );
3056              }
3057          },
3058  
3059          ready: function() {
3060              // Trigger the scroll event to check if we're within the
3061              // threshold to query for additional attachments.
3062              this.scroll();
3063          },
3064  
3065          scroll: function() {
3066              // @todo: is this still necessary?
3067              if ( ! this.$el.is(':visible') )
3068                  return;
3069  
3070              if ( this.collection.hasMore() && this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
3071                  this.collection.more().done( this.scroll );
3072              }
3073          }
3074      }, {
3075          $head: (function() {
3076              var $head;
3077              return function() {
3078                  return $head = $head || $('head');
3079              };
3080          }())
3081      });
3082  
3083      /**
3084       * wp.media.view.Search
3085       */
3086      media.view.Search = media.View.extend({
3087          tagName:   'input',
3088          className: 'search',
3089  
3090          attributes: {
3091              type:        'search',
3092              placeholder: l10n.search
3093          },
3094  
3095          events: {
3096              'input':  'search',
3097              'keyup':  'search',
3098              'change': 'search',
3099              'search': 'search'
3100          },
3101  
3102          render: function() {
3103              this.el.value = this.model.escape('search');
3104              return this;
3105          },
3106  
3107          search: function( event ) {
3108              if ( event.target.value )
3109                  this.model.set( 'search', event.target.value );
3110              else
3111                  this.model.unset('search');
3112          }
3113      });
3114  
3115      /**
3116       * wp.media.view.AttachmentFilters
3117       */
3118      media.view.AttachmentFilters = media.View.extend({
3119          tagName:   'select',
3120          className: 'attachment-filters',
3121  
3122          events: {
3123              change: 'change'
3124          },
3125  
3126          keys: [],
3127  
3128          initialize: function() {
3129              this.createFilters();
3130              _.extend( this.filters, this.options.filters );
3131  
3132              // Build `<option>` elements.
3133              this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
3134                  return {
3135                      el: $('<option></option>').val(value).text(filter.text)[0],
3136                      priority: filter.priority || 50
3137                  };
3138              }, this ).sortBy('priority').pluck('el').value() );
3139  
3140              this.model.on( 'change', this.select, this );
3141              this.select();
3142          },
3143  
3144          createFilters: function() {
3145              this.filters = {};
3146          },
3147  
3148          change: function() {
3149              var filter = this.filters[ this.el.value ];
3150  
3151              if ( filter )
3152                  this.model.set( filter.props );
3153          },
3154  
3155          select: function() {
3156              var model = this.model,
3157                  value = 'all',
3158                  props = model.toJSON();
3159  
3160              _.find( this.filters, function( filter, id ) {
3161                  var equal = _.all( filter.props, function( prop, key ) {
3162                      return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
3163                  });
3164  
3165                  if ( equal )
3166                      return value = id;
3167              });
3168  
3169              this.$el.val( value );
3170          }
3171      });
3172  
3173      media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
3174          createFilters: function() {
3175              var type = this.model.get('type'),
3176                  types = media.view.settings.mimeTypes,
3177                  text;
3178  
3179              if ( types && type )
3180                  text = types[ type ];
3181  
3182              this.filters = {
3183                  all: {
3184                      text:  text || l10n.allMediaItems,
3185                      props: {
3186                          uploadedTo: null,
3187                          orderby: 'date',
3188                          order:   'DESC'
3189                      },
3190                      priority: 10
3191                  },
3192  
3193                  uploaded: {
3194                      text:  l10n.uploadedToThisPost,
3195                      props: {
3196                          uploadedTo: media.view.settings.post.id,
3197                          orderby: 'menuOrder',
3198                          order:   'ASC'
3199                      },
3200                      priority: 20
3201                  }
3202              };
3203          }
3204      });
3205  
3206      media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
3207          createFilters: function() {
3208              var filters = {};
3209  
3210              _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
3211                  filters[ key ] = {
3212                      text: text,
3213                      props: {
3214                          type:    key,
3215                          uploadedTo: null,
3216                          orderby: 'date',
3217                          order:   'DESC'
3218                      }
3219                  };
3220              });
3221  
3222              filters.all = {
3223                  text:  l10n.allMediaItems,
3224                  props: {
3225                      type:    null,
3226                      uploadedTo: null,
3227                      orderby: 'date',
3228                      order:   'DESC'
3229                  },
3230                  priority: 10
3231              };
3232  
3233              filters.uploaded = {
3234                  text:  l10n.uploadedToThisPost,
3235                  props: {
3236                      type:    null,
3237                      uploadedTo: media.view.settings.post.id,
3238                      orderby: 'menuOrder',
3239                      order:   'ASC'
3240                  },
3241                  priority: 20
3242              };
3243  
3244              this.filters = filters;
3245          }
3246      });
3247  
3248  
3249  
3250      /**
3251       * wp.media.view.AttachmentsBrowser
3252       */
3253      media.view.AttachmentsBrowser = media.View.extend({
3254          tagName:   'div',
3255          className: 'attachments-browser',
3256  
3257          initialize: function() {
3258              _.defaults( this.options, {
3259                  filters: false,
3260                  search:  true,
3261                  display: false,
3262  
3263                  AttachmentView: media.view.Attachment.Library
3264              });
3265  
3266              this.createToolbar();
3267              this.updateContent();
3268              this.createSidebar();
3269  
3270              this.collection.on( 'add remove reset', this.updateContent, this );
3271          },
3272  
3273          dispose: function() {
3274              this.options.selection.off( null, null, this );
3275              media.View.prototype.dispose.apply( this, arguments );
3276              return this;
3277          },
3278  
3279          createToolbar: function() {
3280              var filters, FiltersConstructor;
3281  
3282              this.toolbar = new media.view.Toolbar({
3283                  controller: this.controller
3284              });
3285  
3286              this.views.add( this.toolbar );
3287  
3288              filters = this.options.filters;
3289              if ( 'uploaded' === filters )
3290                  FiltersConstructor = media.view.AttachmentFilters.Uploaded;
3291              else if ( 'all' === filters )
3292                  FiltersConstructor = media.view.AttachmentFilters.All;
3293  
3294              if ( FiltersConstructor ) {
3295                  this.toolbar.set( 'filters', new FiltersConstructor({
3296                      controller: this.controller,
3297                      model:      this.collection.props,
3298                      priority:   -80
3299                  }).render() );
3300              }
3301  
3302              if ( this.options.search ) {
3303                  this.toolbar.set( 'search', new media.view.Search({
3304                      controller: this.controller,
3305                      model:      this.collection.props,
3306                      priority:   60
3307                  }).render() );
3308              }
3309  
3310              if ( this.options.dragInfo ) {
3311                  this.toolbar.set( 'dragInfo', new media.View({
3312                      el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
3313                      priority: -40
3314                  }) );
3315              }
3316          },
3317  
3318          updateContent: function() {
3319              var view = this;
3320  
3321              if( ! this.attachments )
3322                  this.createAttachments();
3323  
3324              if ( ! this.collection.length ) {
3325                  this.collection.more().done( function() {
3326                      if ( ! view.collection.length )
3327                          view.createUploader();
3328                  });
3329              }
3330          },
3331  
3332          removeContent: function() {
3333              _.each(['attachments','uploader'], function( key ) {
3334                  if ( this[ key ] ) {
3335                      this[ key ].remove();
3336                      delete this[ key ];
3337                  }
3338              }, this );
3339          },
3340  
3341          createUploader: function() {
3342              this.removeContent();
3343  
3344              this.uploader = new media.view.UploaderInline({
3345                  controller: this.controller,
3346                  status:     false,
3347                  message:    l10n.noItemsFound
3348              });
3349  
3350              this.views.add( this.uploader );
3351          },
3352  
3353          createAttachments: function() {
3354              this.removeContent();
3355  
3356              this.attachments = new media.view.Attachments({
3357                  controller: this.controller,
3358                  collection: this.collection,
3359                  selection:  this.options.selection,
3360                  model:      this.model,
3361                  sortable:   this.options.sortable,
3362  
3363                  // The single `Attachment` view to be used in the `Attachments` view.
3364                  AttachmentView: this.options.AttachmentView
3365              });
3366  
3367              this.views.add( this.attachments );
3368          },
3369  
3370          createSidebar: function() {
3371              var options = this.options,
3372                  selection = options.selection,
3373                  sidebar = this.sidebar = new media.view.Sidebar({
3374                      controller: this.controller
3375                  });
3376  
3377              this.views.add( sidebar );
3378  
3379              if ( this.controller.uploader ) {
3380                  sidebar.set( 'uploads', new media.view.UploaderStatus({
3381                      controller: this.controller,
3382                      priority:   40
3383                  }) );
3384              }
3385  
3386              selection.on( 'selection:single', this.createSingle, this );
3387              selection.on( 'selection:unsingle', this.disposeSingle, this );
3388  
3389              if ( selection.single() )
3390                  this.createSingle();
3391          },
3392  
3393          createSingle: function() {
3394              var sidebar = this.sidebar,
3395                  single = this.options.selection.single();
3396  
3397              sidebar.set( 'details', new media.view.Attachment.Details({
3398                  controller: this.controller,
3399                  model:      single,
3400                  priority:   80
3401              }) );
3402  
3403              sidebar.set( 'compat', new media.view.AttachmentCompat({
3404                  controller: this.controller,
3405                  model:      single,
3406                  priority:   120
3407              }) );
3408  
3409              if ( this.options.display ) {
3410                  sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
3411                      controller:   this.controller,
3412                      model:        this.model.display( single ),
3413                      attachment:   single,
3414                      priority:     160,
3415                      userSettings: this.model.get('displayUserSettings')
3416                  }) );
3417              }
3418          },
3419  
3420          disposeSingle: function() {
3421              var sidebar = this.sidebar;
3422              sidebar.unset('details');
3423              sidebar.unset('compat');
3424              sidebar.unset('display');
3425          }
3426      });
3427  
3428      /**
3429       * wp.media.view.Selection
3430       */
3431      media.view.Selection = media.View.extend({
3432          tagName:   'div',
3433          className: 'media-selection',
3434          template:  media.template('media-selection'),
3435  
3436          events: {
3437              'click .edit-selection':  'edit',
3438              'click .clear-selection': 'clear'
3439          },
3440  
3441          initialize: function() {
3442              _.defaults( this.options, {
3443                  editable:  false,
3444                  clearable: true
3445              });
3446  
3447              this.attachments = new media.view.Attachments.Selection({
3448                  controller: this.controller,
3449                  collection: this.collection,
3450                  selection:  this.collection,
3451                  model:      new Backbone.Model({
3452                      edge:   40,
3453                      gutter: 5
3454                  })
3455              });
3456  
3457              this.views.set( '.selection-view', this.attachments );
3458              this.collection.on( 'add remove reset', this.refresh, this );
3459              this.controller.on( 'content:activate', this.refresh, this );
3460          },
3461  
3462          ready: function() {
3463              this.refresh();
3464          },
3465  
3466          refresh: function() {
3467              // If the selection hasn't been rendered, bail.
3468              if ( ! this.$el.children().length )
3469                  return;
3470  
3471              var collection = this.collection,
3472                  editing = 'edit-selection' === this.controller.content.mode();
3473  
3474              // If nothing is selected, display nothing.
3475              this.$el.toggleClass( 'empty', ! collection.length );
3476              this.$el.toggleClass( 'one', 1 === collection.length );
3477              this.$el.toggleClass( 'editing', editing );
3478  
3479              this.$('.count').text( l10n.selected.replace('%d', collection.length) );
3480          },
3481  
3482          edit: function( event ) {
3483              event.preventDefault();
3484              if ( this.options.editable )
3485                  this.options.editable.call( this, this.collection );
3486          },
3487  
3488          clear: function( event ) {
3489              event.preventDefault();
3490              this.collection.reset();
3491          }
3492      });
3493  
3494  
3495      /**
3496       * wp.media.view.Attachment.Selection
3497       */
3498      media.view.Attachment.Selection = media.view.Attachment.extend({
3499          className: 'attachment selection',
3500  
3501          // On click, just select the model, instead of removing the model from
3502          // the selection.
3503          toggleSelection: function() {
3504              this.options.selection.single( this.model );
3505          }
3506      });
3507  
3508      /**
3509       * wp.media.view.Attachments.Selection
3510       */
3511      media.view.Attachments.Selection = media.view.Attachments.extend({
3512          events: {},
3513          initialize: function() {
3514              _.defaults( this.options, {
3515                  sortable:   true,
3516                  resize:     false,
3517  
3518                  // The single `Attachment` view to be used in the `Attachments` view.
3519                  AttachmentView: media.view.Attachment.Selection
3520              });
3521              return media.view.Attachments.prototype.initialize.apply( this, arguments );
3522          }
3523      });
3524  
3525      /**
3526       * wp.media.view.Attachments.EditSelection
3527       */
3528      media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
3529          buttons: {
3530              close: true
3531          }
3532      });
3533  
3534  
3535      /**
3536       * wp.media.view.Settings
3537       */
3538      media.view.Settings = media.View.extend({
3539          events: {
3540              'click button':    'updateHandler',
3541              'change input':    'updateHandler',
3542              'change select':   'updateHandler',
3543              'change textarea': 'updateHandler'
3544          },
3545  
3546          initialize: function() {
3547              this.model = this.model || new Backbone.Model();
3548              this.model.on( 'change', this.updateChanges, this );
3549          },
3550  
3551          prepare: function() {
3552              return _.defaults({
3553                  model: this.model.toJSON()
3554              }, this.options );
3555          },
3556  
3557          render: function() {
3558              media.View.prototype.render.apply( this, arguments );
3559              // Select the correct values.
3560              _( this.model.attributes ).chain().keys().each( this.update, this );
3561              return this;
3562          },
3563  
3564          update: function( key ) {
3565              var value = this.model.get( key ),
3566                  $setting = this.$('[data-setting="' + key + '"]'),
3567                  $buttons, $value;
3568  
3569              // Bail if we didn't find a matching setting.
3570              if ( ! $setting.length )
3571                  return;
3572  
3573              // Attempt to determine how the setting is rendered and update
3574              // the selected value.
3575  
3576              // Handle dropdowns.
3577              if ( $setting.is('select') ) {
3578                  $value = $setting.find('[value="' + value + '"]');
3579  
3580                  if ( $value.length ) {
3581                      $setting.find('option').prop( 'selected', false );
3582                      $value.prop( 'selected', true );
3583                  } else {
3584                      // If we can't find the desired value, record what *is* selected.
3585                      this.model.set( key, $setting.find(':selected').val() );
3586                  }
3587  
3588  
3589              // Handle button groups.
3590              } else if ( $setting.hasClass('button-group') ) {
3591                  $buttons = $setting.find('button').removeClass('active');
3592                  $buttons.filter( '[value="' + value + '"]' ).addClass('active');
3593  
3594              // Handle text inputs and textareas.
3595              } else if ( $setting.is('input[type="text"], textarea') ) {
3596                  if ( ! $setting.is(':focus') )
3597                      $setting.val( value );
3598  
3599              // Handle checkboxes.
3600              } else if ( $setting.is('input[type="checkbox"]') ) {
3601                  $setting.attr( 'checked', !! value );
3602              }
3603          },
3604  
3605          updateHandler: function( event ) {
3606              var $setting = $( event.target ).closest('[data-setting]'),
3607                  value = event.target.value,
3608                  userSetting;
3609  
3610              event.preventDefault();
3611  
3612              if ( ! $setting.length )
3613                  return;
3614  
3615              // Use the correct value for checkboxes.
3616              if ( $setting.is('input[type="checkbox"]') )
3617                  value = $setting[0].checked;
3618  
3619              // Update the corresponding setting.
3620              this.model.set( $setting.data('setting'), value );
3621  
3622              // If the setting has a corresponding user setting,
3623              // update that as well.
3624              if ( userSetting = $setting.data('userSetting') )
3625                  setUserSetting( userSetting, value );
3626          },
3627  
3628          updateChanges: function( model ) {
3629              if ( model.hasChanged() )
3630                  _( model.changed ).chain().keys().each( this.update, this );
3631          }
3632      });
3633  
3634      /**
3635       * wp.media.view.Settings.AttachmentDisplay
3636       */
3637      media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
3638          className: 'attachment-display-settings',
3639          template:  media.template('attachment-display-settings'),
3640  
3641          initialize: function() {
3642              var attachment = this.options.attachment;
3643  
3644              _.defaults( this.options, {
3645                  userSettings: false
3646              });
3647  
3648              media.view.Settings.prototype.initialize.apply( this, arguments );
3649              this.model.on( 'change:link', this.updateLinkTo, this );
3650  
3651              if ( attachment )
3652                  attachment.on( 'change:uploading', this.render, this );
3653          },
3654  
3655          dispose: function() {
3656              var attachment = this.options.attachment;
3657              if ( attachment )
3658                  attachment.off( null, null, this );
3659  
3660              media.view.Settings.prototype.dispose.apply( this, arguments );
3661          },
3662  
3663          render: function() {
3664              var attachment = this.options.attachment;
3665              if ( attachment ) {
3666                  _.extend( this.options, {
3667                      sizes: attachment.get('sizes'),
3668                      type:  attachment.get('type')
3669                  });
3670              }
3671  
3672              media.view.Settings.prototype.render.call( this );
3673              this.updateLinkTo();
3674              return this;
3675          },
3676  
3677          updateLinkTo: function() {
3678              var linkTo = this.model.get('link'),
3679                  $input = this.$('.link-to-custom'),
3680                  attachment = this.options.attachment;
3681  
3682              if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
3683                  $input.hide();
3684                  return;
3685              }
3686  
3687              if ( attachment ) {
3688                  if ( 'post' === linkTo ) {
3689                      $input.val( attachment.get('link') );
3690                  } else if ( 'file' === linkTo ) {
3691                      $input.val( attachment.get('url') );
3692                  } else if ( ! this.model.get('linkUrl') ) {
3693                      $input.val('http://');
3694                  }
3695  
3696                  $input.prop( 'readonly', 'custom' !== linkTo );
3697              }
3698  
3699              $input.show();
3700  
3701              // If the input is visible, focus and select its contents.
3702              if ( $input.is(':visible') )
3703                  $input.focus()[0].select();
3704          }
3705      });
3706  
3707      /**
3708       * wp.media.view.Settings.Gallery
3709       */
3710      media.view.Settings.Gallery = media.view.Settings.extend({
3711          className: 'gallery-settings',
3712          template:  media.template('gallery-settings')
3713      });
3714  
3715      /**
3716       * wp.media.view.Attachment.Details
3717       */
3718      media.view.Attachment.Details = media.view.Attachment.extend({
3719          tagName:   'div',
3720          className: 'attachment-details',
3721          template:  media.template('attachment-details'),
3722  
3723          events: {
3724              'change [data-setting]':          'updateSetting',
3725              'change [data-setting] input':    'updateSetting',
3726              'change [data-setting] select':   'updateSetting',
3727              'change [data-setting] textarea': 'updateSetting',
3728              'click .delete-attachment':       'deleteAttachment',
3729              'click .edit-attachment':         'editAttachment',
3730              'click .refresh-attachment':      'refreshAttachment'
3731          },
3732  
3733          initialize: function() {
3734              this.focusManager = new media.view.FocusManager({
3735                  el: this.el
3736              });
3737  
3738              media.view.Attachment.prototype.initialize.apply( this, arguments );
3739          },
3740  
3741          render: function() {
3742              media.view.Attachment.prototype.render.apply( this, arguments );
3743              this.focusManager.focus();
3744              return this;
3745          },
3746  
3747          deleteAttachment: function( event ) {
3748              event.preventDefault();
3749  
3750              if ( confirm( l10n.warnDelete ) )
3751                  this.model.destroy();
3752          },
3753  
3754          editAttachment: function() {
3755              this.$el.addClass('needs-refresh');
3756          },
3757  
3758          refreshAttachment: function( event ) {
3759              this.$el.removeClass('needs-refresh');
3760              event.preventDefault();
3761              this.model.fetch();
3762          }
3763      });
3764  
3765      /**
3766       * wp.media.view.AttachmentCompat
3767       */
3768      media.view.AttachmentCompat = media.View.extend({
3769          tagName:   'form',
3770          className: 'compat-item',
3771  
3772          events: {
3773              'submit':          'preventDefault',
3774              'change input':    'save',
3775              'change select':   'save',
3776              'change textarea': 'save'
3777          },
3778  
3779          initialize: function() {
3780              this.focusManager = new media.view.FocusManager({
3781                  el: this.el
3782              });
3783  
3784              this.model.on( 'change:compat', this.render, this );
3785          },
3786  
3787          dispose: function() {
3788              if ( this.$(':focus').length )
3789                  this.save();
3790  
3791              return media.View.prototype.dispose.apply( this, arguments );
3792          },
3793  
3794          render: function() {
3795              var compat = this.model.get('compat');
3796              if ( ! compat || ! compat.item )
3797                  return;
3798  
3799              this.views.detach();
3800              this.$el.html( compat.item );
3801              this.views.render();
3802  
3803              this.focusManager.focus();
3804              return this;
3805          },
3806  
3807          preventDefault: function( event ) {
3808              event.preventDefault();
3809          },
3810  
3811          save: function( event ) {
3812              var data = {};
3813  
3814              if ( event )
3815                  event.preventDefault();
3816  
3817              _.each( this.$el.serializeArray(), function( pair ) {
3818                  data[ pair.name ] = pair.value;
3819              });
3820  
3821              this.model.saveCompat( data );
3822          }
3823      });
3824  
3825      /**
3826       * wp.media.view.Iframe
3827       */
3828      media.view.Iframe = media.View.extend({
3829          className: 'media-iframe',
3830  
3831          render: function() {
3832              this.views.detach();
3833              this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
3834              this.views.render();
3835              return this;
3836          }
3837      });
3838  
3839      /**
3840       * wp.media.view.Embed
3841       */
3842      media.view.Embed = media.View.extend({
3843          className: 'media-embed',
3844  
3845          initialize: function() {
3846              this.url = new media.view.EmbedUrl({
3847                  controller: this.controller,
3848                  model:      this.model.props
3849              }).render();
3850  
3851              this.views.set([ this.url ]);
3852              this.refresh();
3853              this.model.on( 'change:type', this.refresh, this );
3854              this.model.on( 'change:loading', this.loading, this );
3855          },
3856  
3857          settings: function( view ) {
3858              if ( this._settings )
3859                  this._settings.remove();
3860              this._settings = view;
3861              this.views.add( view );
3862          },
3863  
3864          refresh: function() {
3865              var type = this.model.get('type'),
3866                  constructor;
3867  
3868              if ( 'image' === type )
3869                  constructor = media.view.EmbedImage;
3870              else if ( 'link' === type )
3871                  constructor = media.view.EmbedLink;
3872              else
3873                  return;
3874  
3875              this.settings( new constructor({
3876                  controller: this.controller,
3877                  model:      this.model.props,
3878                  priority:   40
3879              }) );
3880          },
3881  
3882          loading: function() {
3883              this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
3884          }
3885      });
3886  
3887      /**
3888       * wp.media.view.EmbedUrl
3889       */
3890      media.view.EmbedUrl = media.View.extend({
3891          tagName:   'label',
3892          className: 'embed-url',
3893  
3894          events: {
3895              'input':  'url',
3896              'keyup':  'url',
3897              'change': 'url'
3898          },
3899  
3900          initialize: function() {
3901              this.$input = $('<input/>').attr( 'type', 'text' ).val( this.model.get('url') );
3902              this.input = this.$input[0];
3903  
3904              this.spinner = $('<span class="spinner" />')[0];
3905              this.$el.append([ this.input, this.spinner ]);
3906  
3907              this.model.on( 'change:url', this.render, this );
3908          },
3909  
3910          render: function() {
3911              var $input = this.$input;
3912  
3913              if ( $input.is(':focus') )
3914                  return;
3915  
3916              this.input.value = this.model.get('url') || 'http://';
3917              media.View.prototype.render.apply( this, arguments );
3918              return this;
3919          },
3920  
3921          ready: function() {
3922              this.focus();
3923          },
3924  
3925          url: function( event ) {
3926              this.model.set( 'url', event.target.value );
3927          },
3928  
3929          focus: function() {
3930              var $input = this.$input;
3931              // If the input is visible, focus and select its contents.
3932              if ( $input.is(':visible') )
3933                  $input.focus()[0].select();
3934          }
3935      });
3936  
3937      /**
3938       * wp.media.view.EmbedLink
3939       */
3940      media.view.EmbedLink = media.view.Settings.extend({
3941          className: 'embed-link-settings',
3942          template:  media.template('embed-link-settings')
3943      });
3944  
3945      /**
3946       * wp.media.view.EmbedImage
3947       */
3948      media.view.EmbedImage =  media.view.Settings.AttachmentDisplay.extend({
3949          className: 'embed-image-settings',
3950          template:  media.template('embed-image-settings'),
3951  
3952          initialize: function() {
3953              media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
3954              this.model.on( 'change:url', this.updateImage, this );
3955          },
3956  
3957          updateImage: function() {
3958              this.$('img').attr( 'src', this.model.get('url') );
3959          }
3960      });
3961  }(jQuery));


Generated: Tue Mar 25 01:41:18 2014 WordPress honlapkészítés: online1.hu