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