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