angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

theme.js (55671B)


      1 /**
      2  * @output wp-admin/js/theme.js
      3  */
      4 
      5 /* global _wpThemeSettings, confirm, tb_position */
      6 window.wp = window.wp || {};
      7 
      8 ( function($) {
      9 
     10 // Set up our namespace...
     11 var themes, l10n;
     12 themes = wp.themes = wp.themes || {};
     13 
     14 // Store the theme data and settings for organized and quick access.
     15 // themes.data.settings, themes.data.themes, themes.data.l10n.
     16 themes.data = _wpThemeSettings;
     17 l10n = themes.data.l10n;
     18 
     19 // Shortcut for isInstall check.
     20 themes.isInstall = !! themes.data.settings.isInstall;
     21 
     22 // Setup app structure.
     23 _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
     24 
     25 themes.Model = Backbone.Model.extend({
     26 	// Adds attributes to the default data coming through the .org themes api.
     27 	// Map `id` to `slug` for shared code.
     28 	initialize: function() {
     29 		var description;
     30 
     31 		if ( this.get( 'slug' ) ) {
     32 			// If the theme is already installed, set an attribute.
     33 			if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
     34 				this.set({ installed: true });
     35 			}
     36 
     37 			// If the theme is active, set an attribute.
     38 			if ( themes.data.activeTheme === this.get( 'slug' ) ) {
     39 				this.set({ active: true });
     40 			}
     41 		}
     42 
     43 		// Set the attributes.
     44 		this.set({
     45 			// `slug` is for installation, `id` is for existing.
     46 			id: this.get( 'slug' ) || this.get( 'id' )
     47 		});
     48 
     49 		// Map `section.description` to `description`
     50 		// as the API sometimes returns it differently.
     51 		if ( this.has( 'sections' ) ) {
     52 			description = this.get( 'sections' ).description;
     53 			this.set({ description: description });
     54 		}
     55 	}
     56 });
     57 
     58 // Main view controller for themes.php.
     59 // Unifies and renders all available views.
     60 themes.view.Appearance = wp.Backbone.View.extend({
     61 
     62 	el: '#wpbody-content .wrap .theme-browser',
     63 
     64 	window: $( window ),
     65 	// Pagination instance.
     66 	page: 0,
     67 
     68 	// Sets up a throttler for binding to 'scroll'.
     69 	initialize: function( options ) {
     70 		// Scroller checks how far the scroll position is.
     71 		_.bindAll( this, 'scroller' );
     72 
     73 		this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
     74 		// Bind to the scroll event and throttle
     75 		// the results from this.scroller.
     76 		this.window.on( 'scroll', _.throttle( this.scroller, 300 ) );
     77 	},
     78 
     79 	// Main render control.
     80 	render: function() {
     81 		// Setup the main theme view
     82 		// with the current theme collection.
     83 		this.view = new themes.view.Themes({
     84 			collection: this.collection,
     85 			parent: this
     86 		});
     87 
     88 		// Render search form.
     89 		this.search();
     90 
     91 		this.$el.removeClass( 'search-loading' );
     92 
     93 		// Render and append.
     94 		this.view.render();
     95 		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
     96 	},
     97 
     98 	// Defines search element container.
     99 	searchContainer: $( '.search-form' ),
    100 
    101 	// Search input and view
    102 	// for current theme collection.
    103 	search: function() {
    104 		var view,
    105 			self = this;
    106 
    107 		// Don't render the search if there is only one theme.
    108 		if ( themes.data.themes.length === 1 ) {
    109 			return;
    110 		}
    111 
    112 		view = new this.SearchView({
    113 			collection: self.collection,
    114 			parent: this
    115 		});
    116 		self.SearchView = view;
    117 
    118 		// Render and append after screen title.
    119 		view.render();
    120 		this.searchContainer
    121 			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
    122 			.append( view.el )
    123 			.on( 'submit', function( event ) {
    124 				event.preventDefault();
    125 			});
    126 	},
    127 
    128 	// Checks when the user gets close to the bottom
    129 	// of the mage and triggers a theme:scroll event.
    130 	scroller: function() {
    131 		var self = this,
    132 			bottom, threshold;
    133 
    134 		bottom = this.window.scrollTop() + self.window.height();
    135 		threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
    136 		threshold = Math.round( threshold * 0.9 );
    137 
    138 		if ( bottom > threshold ) {
    139 			this.trigger( 'theme:scroll' );
    140 		}
    141 	}
    142 });
    143 
    144 // Set up the Collection for our theme data.
    145 // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
    146 themes.Collection = Backbone.Collection.extend({
    147 
    148 	model: themes.Model,
    149 
    150 	// Search terms.
    151 	terms: '',
    152 
    153 	// Controls searching on the current theme collection
    154 	// and triggers an update event.
    155 	doSearch: function( value ) {
    156 
    157 		// Don't do anything if we've already done this search.
    158 		// Useful because the Search handler fires multiple times per keystroke.
    159 		if ( this.terms === value ) {
    160 			return;
    161 		}
    162 
    163 		// Updates terms with the value passed.
    164 		this.terms = value;
    165 
    166 		// If we have terms, run a search...
    167 		if ( this.terms.length > 0 ) {
    168 			this.search( this.terms );
    169 		}
    170 
    171 		// If search is blank, show all themes.
    172 		// Useful for resetting the views when you clean the input.
    173 		if ( this.terms === '' ) {
    174 			this.reset( themes.data.themes );
    175 			$( 'body' ).removeClass( 'no-results' );
    176 		}
    177 
    178 		// Trigger a 'themes:update' event.
    179 		this.trigger( 'themes:update' );
    180 	},
    181 
    182 	/**
    183 	 * Performs a search within the collection.
    184 	 *
    185 	 * @uses RegExp
    186 	 */
    187 	search: function( term ) {
    188 		var match, results, haystack, name, description, author;
    189 
    190 		// Start with a full collection.
    191 		this.reset( themes.data.themes, { silent: true } );
    192 
    193 		// Trim the term.
    194 		term = term.trim();
    195 
    196 		// Escape the term string for RegExp meta characters.
    197 		term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
    198 
    199 		// Consider spaces as word delimiters and match the whole string
    200 		// so matching terms can be combined.
    201 		term = term.replace( / /g, ')(?=.*' );
    202 		match = new RegExp( '^(?=.*' + term + ').+', 'i' );
    203 
    204 		// Find results.
    205 		// _.filter() and .test().
    206 		results = this.filter( function( data ) {
    207 			name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
    208 			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
    209 			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
    210 
    211 			haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
    212 
    213 			if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
    214 				data.set( 'displayAuthor', true );
    215 			}
    216 
    217 			return match.test( haystack );
    218 		});
    219 
    220 		if ( results.length === 0 ) {
    221 			this.trigger( 'query:empty' );
    222 		} else {
    223 			$( 'body' ).removeClass( 'no-results' );
    224 		}
    225 
    226 		this.reset( results );
    227 	},
    228 
    229 	// Paginates the collection with a helper method
    230 	// that slices the collection.
    231 	paginate: function( instance ) {
    232 		var collection = this;
    233 		instance = instance || 0;
    234 
    235 		// Themes per instance are set at 20.
    236 		collection = _( collection.rest( 20 * instance ) );
    237 		collection = _( collection.first( 20 ) );
    238 
    239 		return collection;
    240 	},
    241 
    242 	count: false,
    243 
    244 	/*
    245 	 * Handles requests for more themes and caches results.
    246 	 *
    247 	 *
    248 	 * When we are missing a cache object we fire an apiCall()
    249 	 * which triggers events of `query:success` or `query:fail`.
    250 	 */
    251 	query: function( request ) {
    252 		/**
    253 		 * @static
    254 		 * @type Array
    255 		 */
    256 		var queries = this.queries,
    257 			self = this,
    258 			query, isPaginated, count;
    259 
    260 		// Store current query request args
    261 		// for later use with the event `theme:end`.
    262 		this.currentQuery.request = request;
    263 
    264 		// Search the query cache for matches.
    265 		query = _.find( queries, function( query ) {
    266 			return _.isEqual( query.request, request );
    267 		});
    268 
    269 		// If the request matches the stored currentQuery.request
    270 		// it means we have a paginated request.
    271 		isPaginated = _.has( request, 'page' );
    272 
    273 		// Reset the internal api page counter for non-paginated queries.
    274 		if ( ! isPaginated ) {
    275 			this.currentQuery.page = 1;
    276 		}
    277 
    278 		// Otherwise, send a new API call and add it to the cache.
    279 		if ( ! query && ! isPaginated ) {
    280 			query = this.apiCall( request ).done( function( data ) {
    281 
    282 				// Update the collection with the queried data.
    283 				if ( data.themes ) {
    284 					self.reset( data.themes );
    285 					count = data.info.results;
    286 					// Store the results and the query request.
    287 					queries.push( { themes: data.themes, request: request, total: count } );
    288 				}
    289 
    290 				// Trigger a collection refresh event
    291 				// and a `query:success` event with a `count` argument.
    292 				self.trigger( 'themes:update' );
    293 				self.trigger( 'query:success', count );
    294 
    295 				if ( data.themes && data.themes.length === 0 ) {
    296 					self.trigger( 'query:empty' );
    297 				}
    298 
    299 			}).fail( function() {
    300 				self.trigger( 'query:fail' );
    301 			});
    302 		} else {
    303 			// If it's a paginated request we need to fetch more themes...
    304 			if ( isPaginated ) {
    305 				return this.apiCall( request, isPaginated ).done( function( data ) {
    306 					// Add the new themes to the current collection.
    307 					// @todo Update counter.
    308 					self.add( data.themes );
    309 					self.trigger( 'query:success' );
    310 
    311 					// We are done loading themes for now.
    312 					self.loadingThemes = false;
    313 
    314 				}).fail( function() {
    315 					self.trigger( 'query:fail' );
    316 				});
    317 			}
    318 
    319 			if ( query.themes.length === 0 ) {
    320 				self.trigger( 'query:empty' );
    321 			} else {
    322 				$( 'body' ).removeClass( 'no-results' );
    323 			}
    324 
    325 			// Only trigger an update event since we already have the themes
    326 			// on our cached object.
    327 			if ( _.isNumber( query.total ) ) {
    328 				this.count = query.total;
    329 			}
    330 
    331 			this.reset( query.themes );
    332 			if ( ! query.total ) {
    333 				this.count = this.length;
    334 			}
    335 
    336 			this.trigger( 'themes:update' );
    337 			this.trigger( 'query:success', this.count );
    338 		}
    339 	},
    340 
    341 	// Local cache array for API queries.
    342 	queries: [],
    343 
    344 	// Keep track of current query so we can handle pagination.
    345 	currentQuery: {
    346 		page: 1,
    347 		request: {}
    348 	},
    349 
    350 	// Send request to api.wordpress.org/themes.
    351 	apiCall: function( request, paginated ) {
    352 		return wp.ajax.send( 'query-themes', {
    353 			data: {
    354 				// Request data.
    355 				request: _.extend({
    356 					per_page: 100
    357 				}, request)
    358 			},
    359 
    360 			beforeSend: function() {
    361 				if ( ! paginated ) {
    362 					// Spin it.
    363 					$( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
    364 				}
    365 			}
    366 		});
    367 	},
    368 
    369 	// Static status controller for when we are loading themes.
    370 	loadingThemes: false
    371 });
    372 
    373 // This is the view that controls each theme item
    374 // that will be displayed on the screen.
    375 themes.view.Theme = wp.Backbone.View.extend({
    376 
    377 	// Wrap theme data on a div.theme element.
    378 	className: 'theme',
    379 
    380 	// Reflects which theme view we have.
    381 	// 'grid' (default) or 'detail'.
    382 	state: 'grid',
    383 
    384 	// The HTML template for each element to be rendered.
    385 	html: themes.template( 'theme' ),
    386 
    387 	events: {
    388 		'click': themes.isInstall ? 'preview': 'expand',
    389 		'keydown': themes.isInstall ? 'preview': 'expand',
    390 		'touchend': themes.isInstall ? 'preview': 'expand',
    391 		'keyup': 'addFocus',
    392 		'touchmove': 'preventExpand',
    393 		'click .theme-install': 'installTheme',
    394 		'click .update-message': 'updateTheme'
    395 	},
    396 
    397 	touchDrag: false,
    398 
    399 	initialize: function() {
    400 		this.model.on( 'change', this.render, this );
    401 	},
    402 
    403 	render: function() {
    404 		var data = this.model.toJSON();
    405 
    406 		// Render themes using the html template.
    407 		this.$el.html( this.html( data ) ).attr( 'data-slug', data.id );
    408 
    409 		// Renders active theme styles.
    410 		this.activeTheme();
    411 
    412 		if ( this.model.get( 'displayAuthor' ) ) {
    413 			this.$el.addClass( 'display-author' );
    414 		}
    415 	},
    416 
    417 	// Adds a class to the currently active theme
    418 	// and to the overlay in detailed view mode.
    419 	activeTheme: function() {
    420 		if ( this.model.get( 'active' ) ) {
    421 			this.$el.addClass( 'active' );
    422 		}
    423 	},
    424 
    425 	// Add class of focus to the theme we are focused on.
    426 	addFocus: function() {
    427 		var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');
    428 
    429 		$('.theme.focus').removeClass('focus');
    430 		$themeToFocus.addClass('focus');
    431 	},
    432 
    433 	// Single theme overlay screen.
    434 	// It's shown when clicking a theme.
    435 	expand: function( event ) {
    436 		var self = this;
    437 
    438 		event = event || window.event;
    439 
    440 		// 'Enter' and 'Space' keys expand the details view when a theme is :focused.
    441 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
    442 			return;
    443 		}
    444 
    445 		// Bail if the user scrolled on a touch device.
    446 		if ( this.touchDrag === true ) {
    447 			return this.touchDrag = false;
    448 		}
    449 
    450 		// Prevent the modal from showing when the user clicks
    451 		// one of the direct action buttons.
    452 		if ( $( event.target ).is( '.theme-actions a' ) ) {
    453 			return;
    454 		}
    455 
    456 		// Prevent the modal from showing when the user clicks one of the direct action buttons.
    457 		if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
    458 			return;
    459 		}
    460 
    461 		// Set focused theme to current element.
    462 		themes.focusedTheme = this.$el;
    463 
    464 		this.trigger( 'theme:expand', self.model.cid );
    465 	},
    466 
    467 	preventExpand: function() {
    468 		this.touchDrag = true;
    469 	},
    470 
    471 	preview: function( event ) {
    472 		var self = this,
    473 			current, preview;
    474 
    475 		event = event || window.event;
    476 
    477 		// Bail if the user scrolled on a touch device.
    478 		if ( this.touchDrag === true ) {
    479 			return this.touchDrag = false;
    480 		}
    481 
    482 		// Allow direct link path to installing a theme.
    483 		if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
    484 			return;
    485 		}
    486 
    487 		// 'Enter' and 'Space' keys expand the details view when a theme is :focused.
    488 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
    489 			return;
    490 		}
    491 
    492 		// Pressing Enter while focused on the buttons shouldn't open the preview.
    493 		if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
    494 			return;
    495 		}
    496 
    497 		event.preventDefault();
    498 
    499 		event = event || window.event;
    500 
    501 		// Set focus to current theme.
    502 		themes.focusedTheme = this.$el;
    503 
    504 		// Construct a new Preview view.
    505 		themes.preview = preview = new themes.view.Preview({
    506 			model: this.model
    507 		});
    508 
    509 		// Render the view and append it.
    510 		preview.render();
    511 		this.setNavButtonsState();
    512 
    513 		// Hide previous/next navigation if there is only one theme.
    514 		if ( this.model.collection.length === 1 ) {
    515 			preview.$el.addClass( 'no-navigation' );
    516 		} else {
    517 			preview.$el.removeClass( 'no-navigation' );
    518 		}
    519 
    520 		// Append preview.
    521 		$( 'div.wrap' ).append( preview.el );
    522 
    523 		// Listen to our preview object
    524 		// for `theme:next` and `theme:previous` events.
    525 		this.listenTo( preview, 'theme:next', function() {
    526 
    527 			// Keep local track of current theme model.
    528 			current = self.model;
    529 
    530 			// If we have ventured away from current model update the current model position.
    531 			if ( ! _.isUndefined( self.current ) ) {
    532 				current = self.current;
    533 			}
    534 
    535 			// Get next theme model.
    536 			self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
    537 
    538 			// If we have no more themes, bail.
    539 			if ( _.isUndefined( self.current ) ) {
    540 				self.options.parent.parent.trigger( 'theme:end' );
    541 				return self.current = current;
    542 			}
    543 
    544 			preview.model = self.current;
    545 
    546 			// Render and append.
    547 			preview.render();
    548 			this.setNavButtonsState();
    549 			$( '.next-theme' ).trigger( 'focus' );
    550 		})
    551 		.listenTo( preview, 'theme:previous', function() {
    552 
    553 			// Keep track of current theme model.
    554 			current = self.model;
    555 
    556 			// Bail early if we are at the beginning of the collection.
    557 			if ( self.model.collection.indexOf( self.current ) === 0 ) {
    558 				return;
    559 			}
    560 
    561 			// If we have ventured away from current model update the current model position.
    562 			if ( ! _.isUndefined( self.current ) ) {
    563 				current = self.current;
    564 			}
    565 
    566 			// Get previous theme model.
    567 			self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
    568 
    569 			// If we have no more themes, bail.
    570 			if ( _.isUndefined( self.current ) ) {
    571 				return;
    572 			}
    573 
    574 			preview.model = self.current;
    575 
    576 			// Render and append.
    577 			preview.render();
    578 			this.setNavButtonsState();
    579 			$( '.previous-theme' ).trigger( 'focus' );
    580 		});
    581 
    582 		this.listenTo( preview, 'preview:close', function() {
    583 			self.current = self.model;
    584 		});
    585 
    586 	},
    587 
    588 	// Handles .disabled classes for previous/next buttons in theme installer preview.
    589 	setNavButtonsState: function() {
    590 		var $themeInstaller = $( '.theme-install-overlay' ),
    591 			current = _.isUndefined( this.current ) ? this.model : this.current,
    592 			previousThemeButton = $themeInstaller.find( '.previous-theme' ),
    593 			nextThemeButton = $themeInstaller.find( '.next-theme' );
    594 
    595 		// Disable previous at the zero position.
    596 		if ( 0 === this.model.collection.indexOf( current ) ) {
    597 			previousThemeButton
    598 				.addClass( 'disabled' )
    599 				.prop( 'disabled', true );
    600 
    601 			nextThemeButton.trigger( 'focus' );
    602 		}
    603 
    604 		// Disable next if the next model is undefined.
    605 		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
    606 			nextThemeButton
    607 				.addClass( 'disabled' )
    608 				.prop( 'disabled', true );
    609 
    610 			previousThemeButton.trigger( 'focus' );
    611 		}
    612 	},
    613 
    614 	installTheme: function( event ) {
    615 		var _this = this;
    616 
    617 		event.preventDefault();
    618 
    619 		wp.updates.maybeRequestFilesystemCredentials( event );
    620 
    621 		$( document ).on( 'wp-theme-install-success', function( event, response ) {
    622 			if ( _this.model.get( 'id' ) === response.slug ) {
    623 				_this.model.set( { 'installed': true } );
    624 			}
    625 		} );
    626 
    627 		wp.updates.installTheme( {
    628 			slug: $( event.target ).data( 'slug' )
    629 		} );
    630 	},
    631 
    632 	updateTheme: function( event ) {
    633 		var _this = this;
    634 
    635 		if ( ! this.model.get( 'hasPackage' ) ) {
    636 			return;
    637 		}
    638 
    639 		event.preventDefault();
    640 
    641 		wp.updates.maybeRequestFilesystemCredentials( event );
    642 
    643 		$( document ).on( 'wp-theme-update-success', function( event, response ) {
    644 			_this.model.off( 'change', _this.render, _this );
    645 			if ( _this.model.get( 'id' ) === response.slug ) {
    646 				_this.model.set( {
    647 					hasUpdate: false,
    648 					version: response.newVersion
    649 				} );
    650 			}
    651 			_this.model.on( 'change', _this.render, _this );
    652 		} );
    653 
    654 		wp.updates.updateTheme( {
    655 			slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
    656 		} );
    657 	}
    658 });
    659 
    660 // Theme Details view.
    661 // Sets up a modal overlay with the expanded theme data.
    662 themes.view.Details = wp.Backbone.View.extend({
    663 
    664 	// Wrap theme data on a div.theme element.
    665 	className: 'theme-overlay',
    666 
    667 	events: {
    668 		'click': 'collapse',
    669 		'click .delete-theme': 'deleteTheme',
    670 		'click .left': 'previousTheme',
    671 		'click .right': 'nextTheme',
    672 		'click #update-theme': 'updateTheme',
    673 		'click .toggle-auto-update': 'autoupdateState'
    674 	},
    675 
    676 	// The HTML template for the theme overlay.
    677 	html: themes.template( 'theme-single' ),
    678 
    679 	render: function() {
    680 		var data = this.model.toJSON();
    681 		this.$el.html( this.html( data ) );
    682 		// Renders active theme styles.
    683 		this.activeTheme();
    684 		// Set up navigation events.
    685 		this.navigation();
    686 		// Checks screenshot size.
    687 		this.screenshotCheck( this.$el );
    688 		// Contain "tabbing" inside the overlay.
    689 		this.containFocus( this.$el );
    690 	},
    691 
    692 	// Adds a class to the currently active theme
    693 	// and to the overlay in detailed view mode.
    694 	activeTheme: function() {
    695 		// Check the model has the active property.
    696 		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
    697 	},
    698 
    699 	// Set initial focus and constrain tabbing within the theme browser modal.
    700 	containFocus: function( $el ) {
    701 
    702 		// Set initial focus on the primary action control.
    703 		_.delay( function() {
    704 			$( '.theme-overlay' ).trigger( 'focus' );
    705 		}, 100 );
    706 
    707 		// Constrain tabbing within the modal.
    708 		$el.on( 'keydown.wp-themes', function( event ) {
    709 			var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
    710 				$lastFocusable = $el.find( '.theme-actions a:visible' ).last();
    711 
    712 			// Check for the Tab key.
    713 			if ( 9 === event.which ) {
    714 				if ( $firstFocusable[0] === event.target && event.shiftKey ) {
    715 					$lastFocusable.trigger( 'focus' );
    716 					event.preventDefault();
    717 				} else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
    718 					$firstFocusable.trigger( 'focus' );
    719 					event.preventDefault();
    720 				}
    721 			}
    722 		});
    723 	},
    724 
    725 	// Single theme overlay screen.
    726 	// It's shown when clicking a theme.
    727 	collapse: function( event ) {
    728 		var self = this,
    729 			scroll;
    730 
    731 		event = event || window.event;
    732 
    733 		// Prevent collapsing detailed view when there is only one theme available.
    734 		if ( themes.data.themes.length === 1 ) {
    735 			return;
    736 		}
    737 
    738 		// Detect if the click is inside the overlay and don't close it
    739 		// unless the target was the div.back button.
    740 		if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
    741 
    742 			// Add a temporary closing class while overlay fades out.
    743 			$( 'body' ).addClass( 'closing-overlay' );
    744 
    745 			// With a quick fade out animation.
    746 			this.$el.fadeOut( 130, function() {
    747 				// Clicking outside the modal box closes the overlay.
    748 				$( 'body' ).removeClass( 'closing-overlay' );
    749 				// Handle event cleanup.
    750 				self.closeOverlay();
    751 
    752 				// Get scroll position to avoid jumping to the top.
    753 				scroll = document.body.scrollTop;
    754 
    755 				// Clean the URL structure.
    756 				themes.router.navigate( themes.router.baseUrl( '' ) );
    757 
    758 				// Restore scroll position.
    759 				document.body.scrollTop = scroll;
    760 
    761 				// Return focus to the theme div.
    762 				if ( themes.focusedTheme ) {
    763 					themes.focusedTheme.find('.more-details').trigger( 'focus' );
    764 				}
    765 			});
    766 		}
    767 	},
    768 
    769 	// Handles .disabled classes for next/previous buttons.
    770 	navigation: function() {
    771 
    772 		// Disable Left/Right when at the start or end of the collection.
    773 		if ( this.model.cid === this.model.collection.at(0).cid ) {
    774 			this.$el.find( '.left' )
    775 				.addClass( 'disabled' )
    776 				.prop( 'disabled', true );
    777 		}
    778 		if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
    779 			this.$el.find( '.right' )
    780 				.addClass( 'disabled' )
    781 				.prop( 'disabled', true );
    782 		}
    783 	},
    784 
    785 	// Performs the actions to effectively close
    786 	// the theme details overlay.
    787 	closeOverlay: function() {
    788 		$( 'body' ).removeClass( 'modal-open' );
    789 		this.remove();
    790 		this.unbind();
    791 		this.trigger( 'theme:collapse' );
    792 	},
    793 
    794 	// Set state of the auto-update settings link after it has been changed and saved.
    795 	autoupdateState: function() {
    796 		var callback,
    797 			_this = this;
    798 
    799 		// Support concurrent clicks in different Theme Details overlays.
    800 		callback = function( event, data ) {
    801 			var autoupdate;
    802 			if ( _this.model.get( 'id' ) === data.asset ) {
    803 				autoupdate = _this.model.get( 'autoupdate' );
    804 				autoupdate.enabled = 'enable' === data.state;
    805 				_this.model.set( { autoupdate: autoupdate } );
    806 				$( document ).off( 'wp-auto-update-setting-changed', callback );
    807 			}
    808 		};
    809 
    810 		// Triggered in updates.js
    811 		$( document ).on( 'wp-auto-update-setting-changed', callback );
    812 	},
    813 
    814 	updateTheme: function( event ) {
    815 		var _this = this;
    816 		event.preventDefault();
    817 
    818 		wp.updates.maybeRequestFilesystemCredentials( event );
    819 
    820 		$( document ).on( 'wp-theme-update-success', function( event, response ) {
    821 			if ( _this.model.get( 'id' ) === response.slug ) {
    822 				_this.model.set( {
    823 					hasUpdate: false,
    824 					version: response.newVersion
    825 				} );
    826 			}
    827 			_this.render();
    828 		} );
    829 
    830 		wp.updates.updateTheme( {
    831 			slug: $( event.target ).data( 'slug' )
    832 		} );
    833 	},
    834 
    835 	deleteTheme: function( event ) {
    836 		var _this = this,
    837 		    _collection = _this.model.collection,
    838 		    _themes = themes;
    839 		event.preventDefault();
    840 
    841 		// Confirmation dialog for deleting a theme.
    842 		if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
    843 			return;
    844 		}
    845 
    846 		wp.updates.maybeRequestFilesystemCredentials( event );
    847 
    848 		$( document ).one( 'wp-theme-delete-success', function( event, response ) {
    849 			_this.$el.find( '.close' ).trigger( 'click' );
    850 			$( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
    851 				$( this ).remove();
    852 				_themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
    853 
    854 				$( '.wp-filter-search' ).val( '' );
    855 				_collection.doSearch( '' );
    856 				_collection.remove( _this.model );
    857 				_collection.trigger( 'themes:update' );
    858 			} );
    859 		} );
    860 
    861 		wp.updates.deleteTheme( {
    862 			slug: this.model.get( 'id' )
    863 		} );
    864 	},
    865 
    866 	nextTheme: function() {
    867 		var self = this;
    868 		self.trigger( 'theme:next', self.model.cid );
    869 		return false;
    870 	},
    871 
    872 	previousTheme: function() {
    873 		var self = this;
    874 		self.trigger( 'theme:previous', self.model.cid );
    875 		return false;
    876 	},
    877 
    878 	// Checks if the theme screenshot is the old 300px width version
    879 	// and adds a corresponding class if it's true.
    880 	screenshotCheck: function( el ) {
    881 		var screenshot, image;
    882 
    883 		screenshot = el.find( '.screenshot img' );
    884 		image = new Image();
    885 		image.src = screenshot.attr( 'src' );
    886 
    887 		// Width check.
    888 		if ( image.width && image.width <= 300 ) {
    889 			el.addClass( 'small-screenshot' );
    890 		}
    891 	}
    892 });
    893 
    894 // Theme Preview view.
    895 // Sets up a modal overlay with the expanded theme data.
    896 themes.view.Preview = themes.view.Details.extend({
    897 
    898 	className: 'wp-full-overlay expanded',
    899 	el: '.theme-install-overlay',
    900 
    901 	events: {
    902 		'click .close-full-overlay': 'close',
    903 		'click .collapse-sidebar': 'collapse',
    904 		'click .devices button': 'previewDevice',
    905 		'click .previous-theme': 'previousTheme',
    906 		'click .next-theme': 'nextTheme',
    907 		'keyup': 'keyEvent',
    908 		'click .theme-install': 'installTheme'
    909 	},
    910 
    911 	// The HTML template for the theme preview.
    912 	html: themes.template( 'theme-preview' ),
    913 
    914 	render: function() {
    915 		var self = this,
    916 			currentPreviewDevice,
    917 			data = this.model.toJSON(),
    918 			$body = $( document.body );
    919 
    920 		$body.attr( 'aria-busy', 'true' );
    921 
    922 		this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
    923 
    924 		currentPreviewDevice = this.$el.data( 'current-preview-device' );
    925 		if ( currentPreviewDevice ) {
    926 			self.tooglePreviewDeviceButtons( currentPreviewDevice );
    927 		}
    928 
    929 		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
    930 
    931 		this.$el.fadeIn( 200, function() {
    932 			$body.addClass( 'theme-installer-active full-overlay-active' );
    933 		});
    934 
    935 		this.$el.find( 'iframe' ).one( 'load', function() {
    936 			self.iframeLoaded();
    937 		});
    938 	},
    939 
    940 	iframeLoaded: function() {
    941 		this.$el.addClass( 'iframe-ready' );
    942 		$( document.body ).attr( 'aria-busy', 'false' );
    943 	},
    944 
    945 	close: function() {
    946 		this.$el.fadeOut( 200, function() {
    947 			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
    948 
    949 			// Return focus to the theme div.
    950 			if ( themes.focusedTheme ) {
    951 				themes.focusedTheme.find('.more-details').trigger( 'focus' );
    952 			}
    953 		}).removeClass( 'iframe-ready' );
    954 
    955 		// Restore the previous browse tab if available.
    956 		if ( themes.router.selectedTab ) {
    957 			themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
    958 			themes.router.selectedTab = false;
    959 		} else {
    960 			themes.router.navigate( themes.router.baseUrl( '' ) );
    961 		}
    962 		this.trigger( 'preview:close' );
    963 		this.undelegateEvents();
    964 		this.unbind();
    965 		return false;
    966 	},
    967 
    968 	collapse: function( event ) {
    969 		var $button = $( event.currentTarget );
    970 		if ( 'true' === $button.attr( 'aria-expanded' ) ) {
    971 			$button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
    972 		} else {
    973 			$button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
    974 		}
    975 
    976 		this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
    977 		return false;
    978 	},
    979 
    980 	previewDevice: function( event ) {
    981 		var device = $( event.currentTarget ).data( 'device' );
    982 
    983 		this.$el
    984 			.removeClass( 'preview-desktop preview-tablet preview-mobile' )
    985 			.addClass( 'preview-' + device )
    986 			.data( 'current-preview-device', device );
    987 
    988 		this.tooglePreviewDeviceButtons( device );
    989 	},
    990 
    991 	tooglePreviewDeviceButtons: function( newDevice ) {
    992 		var $devices = $( '.wp-full-overlay-footer .devices' );
    993 
    994 		$devices.find( 'button' )
    995 			.removeClass( 'active' )
    996 			.attr( 'aria-pressed', false );
    997 
    998 		$devices.find( 'button.preview-' + newDevice )
    999 			.addClass( 'active' )
   1000 			.attr( 'aria-pressed', true );
   1001 	},
   1002 
   1003 	keyEvent: function( event ) {
   1004 		// The escape key closes the preview.
   1005 		if ( event.keyCode === 27 ) {
   1006 			this.undelegateEvents();
   1007 			this.close();
   1008 		}
   1009 		// The right arrow key, next theme.
   1010 		if ( event.keyCode === 39 ) {
   1011 			_.once( this.nextTheme() );
   1012 		}
   1013 
   1014 		// The left arrow key, previous theme.
   1015 		if ( event.keyCode === 37 ) {
   1016 			this.previousTheme();
   1017 		}
   1018 	},
   1019 
   1020 	installTheme: function( event ) {
   1021 		var _this   = this,
   1022 		    $target = $( event.target );
   1023 		event.preventDefault();
   1024 
   1025 		if ( $target.hasClass( 'disabled' ) ) {
   1026 			return;
   1027 		}
   1028 
   1029 		wp.updates.maybeRequestFilesystemCredentials( event );
   1030 
   1031 		$( document ).on( 'wp-theme-install-success', function() {
   1032 			_this.model.set( { 'installed': true } );
   1033 		} );
   1034 
   1035 		wp.updates.installTheme( {
   1036 			slug: $target.data( 'slug' )
   1037 		} );
   1038 	}
   1039 });
   1040 
   1041 // Controls the rendering of div.themes,
   1042 // a wrapper that will hold all the theme elements.
   1043 themes.view.Themes = wp.Backbone.View.extend({
   1044 
   1045 	className: 'themes wp-clearfix',
   1046 	$overlay: $( 'div.theme-overlay' ),
   1047 
   1048 	// Number to keep track of scroll position
   1049 	// while in theme-overlay mode.
   1050 	index: 0,
   1051 
   1052 	// The theme count element.
   1053 	count: $( '.wrap .theme-count' ),
   1054 
   1055 	// The live themes count.
   1056 	liveThemeCount: 0,
   1057 
   1058 	initialize: function( options ) {
   1059 		var self = this;
   1060 
   1061 		// Set up parent.
   1062 		this.parent = options.parent;
   1063 
   1064 		// Set current view to [grid].
   1065 		this.setView( 'grid' );
   1066 
   1067 		// Move the active theme to the beginning of the collection.
   1068 		self.currentTheme();
   1069 
   1070 		// When the collection is updated by user input...
   1071 		this.listenTo( self.collection, 'themes:update', function() {
   1072 			self.parent.page = 0;
   1073 			self.currentTheme();
   1074 			self.render( this );
   1075 		} );
   1076 
   1077 		// Update theme count to full result set when available.
   1078 		this.listenTo( self.collection, 'query:success', function( count ) {
   1079 			if ( _.isNumber( count ) ) {
   1080 				self.count.text( count );
   1081 				self.announceSearchResults( count );
   1082 			} else {
   1083 				self.count.text( self.collection.length );
   1084 				self.announceSearchResults( self.collection.length );
   1085 			}
   1086 		});
   1087 
   1088 		this.listenTo( self.collection, 'query:empty', function() {
   1089 			$( 'body' ).addClass( 'no-results' );
   1090 		});
   1091 
   1092 		this.listenTo( this.parent, 'theme:scroll', function() {
   1093 			self.renderThemes( self.parent.page );
   1094 		});
   1095 
   1096 		this.listenTo( this.parent, 'theme:close', function() {
   1097 			if ( self.overlay ) {
   1098 				self.overlay.closeOverlay();
   1099 			}
   1100 		} );
   1101 
   1102 		// Bind keyboard events.
   1103 		$( 'body' ).on( 'keyup', function( event ) {
   1104 			if ( ! self.overlay ) {
   1105 				return;
   1106 			}
   1107 
   1108 			// Bail if the filesystem credentials dialog is shown.
   1109 			if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
   1110 				return;
   1111 			}
   1112 
   1113 			// Pressing the right arrow key fires a theme:next event.
   1114 			if ( event.keyCode === 39 ) {
   1115 				self.overlay.nextTheme();
   1116 			}
   1117 
   1118 			// Pressing the left arrow key fires a theme:previous event.
   1119 			if ( event.keyCode === 37 ) {
   1120 				self.overlay.previousTheme();
   1121 			}
   1122 
   1123 			// Pressing the escape key fires a theme:collapse event.
   1124 			if ( event.keyCode === 27 ) {
   1125 				self.overlay.collapse( event );
   1126 			}
   1127 		});
   1128 	},
   1129 
   1130 	// Manages rendering of theme pages
   1131 	// and keeping theme count in sync.
   1132 	render: function() {
   1133 		// Clear the DOM, please.
   1134 		this.$el.empty();
   1135 
   1136 		// If the user doesn't have switch capabilities or there is only one theme
   1137 		// in the collection, render the detailed view of the active theme.
   1138 		if ( themes.data.themes.length === 1 ) {
   1139 
   1140 			// Constructs the view.
   1141 			this.singleTheme = new themes.view.Details({
   1142 				model: this.collection.models[0]
   1143 			});
   1144 
   1145 			// Render and apply a 'single-theme' class to our container.
   1146 			this.singleTheme.render();
   1147 			this.$el.addClass( 'single-theme' );
   1148 			this.$el.append( this.singleTheme.el );
   1149 		}
   1150 
   1151 		// Generate the themes using page instance
   1152 		// while checking the collection has items.
   1153 		if ( this.options.collection.size() > 0 ) {
   1154 			this.renderThemes( this.parent.page );
   1155 		}
   1156 
   1157 		// Display a live theme count for the collection.
   1158 		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
   1159 		this.count.text( this.liveThemeCount );
   1160 
   1161 		/*
   1162 		 * In the theme installer the themes count is already announced
   1163 		 * because `announceSearchResults` is called on `query:success`.
   1164 		 */
   1165 		if ( ! themes.isInstall ) {
   1166 			this.announceSearchResults( this.liveThemeCount );
   1167 		}
   1168 	},
   1169 
   1170 	// Iterates through each instance of the collection
   1171 	// and renders each theme module.
   1172 	renderThemes: function( page ) {
   1173 		var self = this;
   1174 
   1175 		self.instance = self.collection.paginate( page );
   1176 
   1177 		// If we have no more themes, bail.
   1178 		if ( self.instance.size() === 0 ) {
   1179 			// Fire a no-more-themes event.
   1180 			this.parent.trigger( 'theme:end' );
   1181 			return;
   1182 		}
   1183 
   1184 		// Make sure the add-new stays at the end.
   1185 		if ( ! themes.isInstall && page >= 1 ) {
   1186 			$( '.add-new-theme' ).remove();
   1187 		}
   1188 
   1189 		// Loop through the themes and setup each theme view.
   1190 		self.instance.each( function( theme ) {
   1191 			self.theme = new themes.view.Theme({
   1192 				model: theme,
   1193 				parent: self
   1194 			});
   1195 
   1196 			// Render the views...
   1197 			self.theme.render();
   1198 			// ...and append them to div.themes.
   1199 			self.$el.append( self.theme.el );
   1200 
   1201 			// Binds to theme:expand to show the modal box
   1202 			// with the theme details.
   1203 			self.listenTo( self.theme, 'theme:expand', self.expand, self );
   1204 		});
   1205 
   1206 		// 'Add new theme' element shown at the end of the grid.
   1207 		if ( ! themes.isInstall && themes.data.settings.canInstall ) {
   1208 			this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' );
   1209 		}
   1210 
   1211 		this.parent.page++;
   1212 	},
   1213 
   1214 	// Grabs current theme and puts it at the beginning of the collection.
   1215 	currentTheme: function() {
   1216 		var self = this,
   1217 			current;
   1218 
   1219 		current = self.collection.findWhere({ active: true });
   1220 
   1221 		// Move the active theme to the beginning of the collection.
   1222 		if ( current ) {
   1223 			self.collection.remove( current );
   1224 			self.collection.add( current, { at:0 } );
   1225 		}
   1226 	},
   1227 
   1228 	// Sets current view.
   1229 	setView: function( view ) {
   1230 		return view;
   1231 	},
   1232 
   1233 	// Renders the overlay with the ThemeDetails view.
   1234 	// Uses the current model data.
   1235 	expand: function( id ) {
   1236 		var self = this, $card, $modal;
   1237 
   1238 		// Set the current theme model.
   1239 		this.model = self.collection.get( id );
   1240 
   1241 		// Trigger a route update for the current model.
   1242 		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
   1243 
   1244 		// Sets this.view to 'detail'.
   1245 		this.setView( 'detail' );
   1246 		$( 'body' ).addClass( 'modal-open' );
   1247 
   1248 		// Set up the theme details view.
   1249 		this.overlay = new themes.view.Details({
   1250 			model: self.model
   1251 		});
   1252 
   1253 		this.overlay.render();
   1254 
   1255 		if ( this.model.get( 'hasUpdate' ) ) {
   1256 			$card  = $( '[data-slug="' + this.model.id + '"]' );
   1257 			$modal = $( this.overlay.el );
   1258 
   1259 			if ( $card.find( '.updating-message' ).length ) {
   1260 				$modal.find( '.notice-warning h3' ).remove();
   1261 				$modal.find( '.notice-warning' )
   1262 					.removeClass( 'notice-large' )
   1263 					.addClass( 'updating-message' )
   1264 					.find( 'p' ).text( wp.updates.l10n.updating );
   1265 			} else if ( $card.find( '.notice-error' ).length ) {
   1266 				$modal.find( '.notice-warning' ).remove();
   1267 			}
   1268 		}
   1269 
   1270 		this.$overlay.html( this.overlay.el );
   1271 
   1272 		// Bind to theme:next and theme:previous triggered by the arrow keys.
   1273 		// Keep track of the current model so we can infer an index position.
   1274 		this.listenTo( this.overlay, 'theme:next', function() {
   1275 			// Renders the next theme on the overlay.
   1276 			self.next( [ self.model.cid ] );
   1277 
   1278 		})
   1279 		.listenTo( this.overlay, 'theme:previous', function() {
   1280 			// Renders the previous theme on the overlay.
   1281 			self.previous( [ self.model.cid ] );
   1282 		});
   1283 	},
   1284 
   1285 	/*
   1286 	 * This method renders the next theme on the overlay modal
   1287 	 * based on the current position in the collection.
   1288 	 *
   1289 	 * @params [model cid]
   1290 	 */
   1291 	next: function( args ) {
   1292 		var self = this,
   1293 			model, nextModel;
   1294 
   1295 		// Get the current theme.
   1296 		model = self.collection.get( args[0] );
   1297 		// Find the next model within the collection.
   1298 		nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
   1299 
   1300 		// Sanity check which also serves as a boundary test.
   1301 		if ( nextModel !== undefined ) {
   1302 
   1303 			// We have a new theme...
   1304 			// Close the overlay.
   1305 			this.overlay.closeOverlay();
   1306 
   1307 			// Trigger a route update for the current model.
   1308 			self.theme.trigger( 'theme:expand', nextModel.cid );
   1309 
   1310 		}
   1311 	},
   1312 
   1313 	/*
   1314 	 * This method renders the previous theme on the overlay modal
   1315 	 * based on the current position in the collection.
   1316 	 *
   1317 	 * @params [model cid]
   1318 	 */
   1319 	previous: function( args ) {
   1320 		var self = this,
   1321 			model, previousModel;
   1322 
   1323 		// Get the current theme.
   1324 		model = self.collection.get( args[0] );
   1325 		// Find the previous model within the collection.
   1326 		previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
   1327 
   1328 		if ( previousModel !== undefined ) {
   1329 
   1330 			// We have a new theme...
   1331 			// Close the overlay.
   1332 			this.overlay.closeOverlay();
   1333 
   1334 			// Trigger a route update for the current model.
   1335 			self.theme.trigger( 'theme:expand', previousModel.cid );
   1336 
   1337 		}
   1338 	},
   1339 
   1340 	// Dispatch audible search results feedback message.
   1341 	announceSearchResults: function( count ) {
   1342 		if ( 0 === count ) {
   1343 			wp.a11y.speak( l10n.noThemesFound );
   1344 		} else {
   1345 			wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
   1346 		}
   1347 	}
   1348 });
   1349 
   1350 // Search input view controller.
   1351 themes.view.Search = wp.Backbone.View.extend({
   1352 
   1353 	tagName: 'input',
   1354 	className: 'wp-filter-search',
   1355 	id: 'wp-filter-search-input',
   1356 	searching: false,
   1357 
   1358 	attributes: {
   1359 		placeholder: l10n.searchPlaceholder,
   1360 		type: 'search',
   1361 		'aria-describedby': 'live-search-desc'
   1362 	},
   1363 
   1364 	events: {
   1365 		'input': 'search',
   1366 		'keyup': 'search',
   1367 		'blur': 'pushState'
   1368 	},
   1369 
   1370 	initialize: function( options ) {
   1371 
   1372 		this.parent = options.parent;
   1373 
   1374 		this.listenTo( this.parent, 'theme:close', function() {
   1375 			this.searching = false;
   1376 		} );
   1377 
   1378 	},
   1379 
   1380 	search: function( event ) {
   1381 		// Clear on escape.
   1382 		if ( event.type === 'keyup' && event.which === 27 ) {
   1383 			event.target.value = '';
   1384 		}
   1385 
   1386 		// Since doSearch is debounced, it will only run when user input comes to a rest.
   1387 		this.doSearch( event );
   1388 	},
   1389 
   1390 	// Runs a search on the theme collection.
   1391 	doSearch: function( event ) {
   1392 		var options = {};
   1393 
   1394 		this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
   1395 
   1396 		// if search is initiated and key is not return.
   1397 		if ( this.searching && event.which !== 13 ) {
   1398 			options.replace = true;
   1399 		} else {
   1400 			this.searching = true;
   1401 		}
   1402 
   1403 		// Update the URL hash.
   1404 		if ( event.target.value ) {
   1405 			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
   1406 		} else {
   1407 			themes.router.navigate( themes.router.baseUrl( '' ) );
   1408 		}
   1409 	},
   1410 
   1411 	pushState: function( event ) {
   1412 		var url = themes.router.baseUrl( '' );
   1413 
   1414 		if ( event.target.value ) {
   1415 			url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
   1416 		}
   1417 
   1418 		this.searching = false;
   1419 		themes.router.navigate( url );
   1420 
   1421 	}
   1422 });
   1423 
   1424 /**
   1425  * Navigate router.
   1426  *
   1427  * @since 4.9.0
   1428  *
   1429  * @param {string} url - URL to navigate to.
   1430  * @param {Object} state - State.
   1431  * @return {void}
   1432  */
   1433 function navigateRouter( url, state ) {
   1434 	var router = this;
   1435 	if ( Backbone.history._hasPushState ) {
   1436 		Backbone.Router.prototype.navigate.call( router, url, state );
   1437 	}
   1438 }
   1439 
   1440 // Sets up the routes events for relevant url queries.
   1441 // Listens to [theme] and [search] params.
   1442 themes.Router = Backbone.Router.extend({
   1443 
   1444 	routes: {
   1445 		'themes.php?theme=:slug': 'theme',
   1446 		'themes.php?search=:query': 'search',
   1447 		'themes.php?s=:query': 'search',
   1448 		'themes.php': 'themes',
   1449 		'': 'themes'
   1450 	},
   1451 
   1452 	baseUrl: function( url ) {
   1453 		return 'themes.php' + url;
   1454 	},
   1455 
   1456 	themePath: '?theme=',
   1457 	searchPath: '?search=',
   1458 
   1459 	search: function( query ) {
   1460 		$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
   1461 	},
   1462 
   1463 	themes: function() {
   1464 		$( '.wp-filter-search' ).val( '' );
   1465 	},
   1466 
   1467 	navigate: navigateRouter
   1468 
   1469 });
   1470 
   1471 // Execute and setup the application.
   1472 themes.Run = {
   1473 	init: function() {
   1474 		// Initializes the blog's theme library view.
   1475 		// Create a new collection with data.
   1476 		this.themes = new themes.Collection( themes.data.themes );
   1477 
   1478 		// Set up the view.
   1479 		this.view = new themes.view.Appearance({
   1480 			collection: this.themes
   1481 		});
   1482 
   1483 		this.render();
   1484 
   1485 		// Start debouncing user searches after Backbone.history.start().
   1486 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
   1487 	},
   1488 
   1489 	render: function() {
   1490 
   1491 		// Render results.
   1492 		this.view.render();
   1493 		this.routes();
   1494 
   1495 		if ( Backbone.History.started ) {
   1496 			Backbone.history.stop();
   1497 		}
   1498 		Backbone.history.start({
   1499 			root: themes.data.settings.adminUrl,
   1500 			pushState: true,
   1501 			hashChange: false
   1502 		});
   1503 	},
   1504 
   1505 	routes: function() {
   1506 		var self = this;
   1507 		// Bind to our global thx object
   1508 		// so that the object is available to sub-views.
   1509 		themes.router = new themes.Router();
   1510 
   1511 		// Handles theme details route event.
   1512 		themes.router.on( 'route:theme', function( slug ) {
   1513 			self.view.view.expand( slug );
   1514 		});
   1515 
   1516 		themes.router.on( 'route:themes', function() {
   1517 			self.themes.doSearch( '' );
   1518 			self.view.trigger( 'theme:close' );
   1519 		});
   1520 
   1521 		// Handles search route event.
   1522 		themes.router.on( 'route:search', function() {
   1523 			$( '.wp-filter-search' ).trigger( 'keyup' );
   1524 		});
   1525 
   1526 		this.extraRoutes();
   1527 	},
   1528 
   1529 	extraRoutes: function() {
   1530 		return false;
   1531 	}
   1532 };
   1533 
   1534 // Extend the main Search view.
   1535 themes.view.InstallerSearch =  themes.view.Search.extend({
   1536 
   1537 	events: {
   1538 		'input': 'search',
   1539 		'keyup': 'search'
   1540 	},
   1541 
   1542 	terms: '',
   1543 
   1544 	// Handles Ajax request for searching through themes in public repo.
   1545 	search: function( event ) {
   1546 
   1547 		// Tabbing or reverse tabbing into the search input shouldn't trigger a search.
   1548 		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
   1549 			return;
   1550 		}
   1551 
   1552 		this.collection = this.options.parent.view.collection;
   1553 
   1554 		// Clear on escape.
   1555 		if ( event.type === 'keyup' && event.which === 27 ) {
   1556 			event.target.value = '';
   1557 		}
   1558 
   1559 		this.doSearch( event.target.value );
   1560 	},
   1561 
   1562 	doSearch: function( value ) {
   1563 		var request = {};
   1564 
   1565 		// Don't do anything if the search terms haven't changed.
   1566 		if ( this.terms === value ) {
   1567 			return;
   1568 		}
   1569 
   1570 		// Updates terms with the value passed.
   1571 		this.terms = value;
   1572 
   1573 		request.search = value;
   1574 
   1575 		/*
   1576 		 * Intercept an [author] search.
   1577 		 *
   1578 		 * If input value starts with `author:` send a request
   1579 		 * for `author` instead of a regular `search`.
   1580 		 */
   1581 		if ( value.substring( 0, 7 ) === 'author:' ) {
   1582 			request.search = '';
   1583 			request.author = value.slice( 7 );
   1584 		}
   1585 
   1586 		/*
   1587 		 * Intercept a [tag] search.
   1588 		 *
   1589 		 * If input value starts with `tag:` send a request
   1590 		 * for `tag` instead of a regular `search`.
   1591 		 */
   1592 		if ( value.substring( 0, 4 ) === 'tag:' ) {
   1593 			request.search = '';
   1594 			request.tag = [ value.slice( 4 ) ];
   1595 		}
   1596 
   1597 		$( '.filter-links li > a.current' )
   1598 			.removeClass( 'current' )
   1599 			.removeAttr( 'aria-current' );
   1600 
   1601 		$( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
   1602 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
   1603 
   1604 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
   1605 		// or searching the local cache.
   1606 		this.collection.query( request );
   1607 
   1608 		// Set route.
   1609 		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
   1610 	}
   1611 });
   1612 
   1613 themes.view.Installer = themes.view.Appearance.extend({
   1614 
   1615 	el: '#wpbody-content .wrap',
   1616 
   1617 	// Register events for sorting and filters in theme-navigation.
   1618 	events: {
   1619 		'click .filter-links li > a': 'onSort',
   1620 		'click .theme-filter': 'onFilter',
   1621 		'click .drawer-toggle': 'moreFilters',
   1622 		'click .filter-drawer .apply-filters': 'applyFilters',
   1623 		'click .filter-group [type="checkbox"]': 'addFilter',
   1624 		'click .filter-drawer .clear-filters': 'clearFilters',
   1625 		'click .edit-filters': 'backToFilters',
   1626 		'click .favorites-form-submit' : 'saveUsername',
   1627 		'keyup #wporg-username-input': 'saveUsername'
   1628 	},
   1629 
   1630 	// Initial render method.
   1631 	render: function() {
   1632 		var self = this;
   1633 
   1634 		this.search();
   1635 		this.uploader();
   1636 
   1637 		this.collection = new themes.Collection();
   1638 
   1639 		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
   1640 		this.listenTo( this, 'theme:end', function() {
   1641 
   1642 			// Make sure we are not already loading.
   1643 			if ( self.collection.loadingThemes ) {
   1644 				return;
   1645 			}
   1646 
   1647 			// Set loadingThemes to true and bump page instance of currentQuery.
   1648 			self.collection.loadingThemes = true;
   1649 			self.collection.currentQuery.page++;
   1650 
   1651 			// Use currentQuery.page to build the themes request.
   1652 			_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
   1653 			self.collection.query( self.collection.currentQuery.request );
   1654 		});
   1655 
   1656 		this.listenTo( this.collection, 'query:success', function() {
   1657 			$( 'body' ).removeClass( 'loading-content' );
   1658 			$( '.theme-browser' ).find( 'div.error' ).remove();
   1659 		});
   1660 
   1661 		this.listenTo( this.collection, 'query:fail', function() {
   1662 			$( 'body' ).removeClass( 'loading-content' );
   1663 			$( '.theme-browser' ).find( 'div.error' ).remove();
   1664 			$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
   1665 			$( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
   1666 				e.preventDefault();
   1667 				$( 'input.wp-filter-search' ).trigger( 'input' );
   1668 			} );
   1669 		});
   1670 
   1671 		if ( this.view ) {
   1672 			this.view.remove();
   1673 		}
   1674 
   1675 		// Sets up the view and passes the section argument.
   1676 		this.view = new themes.view.Themes({
   1677 			collection: this.collection,
   1678 			parent: this
   1679 		});
   1680 
   1681 		// Reset pagination every time the install view handler is run.
   1682 		this.page = 0;
   1683 
   1684 		// Render and append.
   1685 		this.$el.find( '.themes' ).remove();
   1686 		this.view.render();
   1687 		this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
   1688 	},
   1689 
   1690 	// Handles all the rendering of the public theme directory.
   1691 	browse: function( section ) {
   1692 		// Create a new collection with the proper theme data
   1693 		// for each section.
   1694 		this.collection.query( { browse: section } );
   1695 	},
   1696 
   1697 	// Sorting navigation.
   1698 	onSort: function( event ) {
   1699 		var $el = $( event.target ),
   1700 			sort = $el.data( 'sort' );
   1701 
   1702 		event.preventDefault();
   1703 
   1704 		$( 'body' ).removeClass( 'filters-applied show-filters' );
   1705 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
   1706 
   1707 		// Bail if this is already active.
   1708 		if ( $el.hasClass( this.activeClass ) ) {
   1709 			return;
   1710 		}
   1711 
   1712 		this.sort( sort );
   1713 
   1714 		// Trigger a router.navigate update.
   1715 		themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
   1716 	},
   1717 
   1718 	sort: function( sort ) {
   1719 		this.clearSearch();
   1720 
   1721 		// Track sorting so we can restore the correct tab when closing preview.
   1722 		themes.router.selectedTab = sort;
   1723 
   1724 		$( '.filter-links li > a, .theme-filter' )
   1725 			.removeClass( this.activeClass )
   1726 			.removeAttr( 'aria-current' );
   1727 
   1728 		$( '[data-sort="' + sort + '"]' )
   1729 			.addClass( this.activeClass )
   1730 			.attr( 'aria-current', 'page' );
   1731 
   1732 		if ( 'favorites' === sort ) {
   1733 			$( 'body' ).addClass( 'show-favorites-form' );
   1734 		} else {
   1735 			$( 'body' ).removeClass( 'show-favorites-form' );
   1736 		}
   1737 
   1738 		this.browse( sort );
   1739 	},
   1740 
   1741 	// Filters and Tags.
   1742 	onFilter: function( event ) {
   1743 		var request,
   1744 			$el = $( event.target ),
   1745 			filter = $el.data( 'filter' );
   1746 
   1747 		// Bail if this is already active.
   1748 		if ( $el.hasClass( this.activeClass ) ) {
   1749 			return;
   1750 		}
   1751 
   1752 		$( '.filter-links li > a, .theme-section' )
   1753 			.removeClass( this.activeClass )
   1754 			.removeAttr( 'aria-current' );
   1755 		$el
   1756 			.addClass( this.activeClass )
   1757 			.attr( 'aria-current', 'page' );
   1758 
   1759 		if ( ! filter ) {
   1760 			return;
   1761 		}
   1762 
   1763 		// Construct the filter request
   1764 		// using the default values.
   1765 		filter = _.union( [ filter, this.filtersChecked() ] );
   1766 		request = { tag: [ filter ] };
   1767 
   1768 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
   1769 		// or searching the local cache.
   1770 		this.collection.query( request );
   1771 	},
   1772 
   1773 	// Clicking on a checkbox to add another filter to the request.
   1774 	addFilter: function() {
   1775 		this.filtersChecked();
   1776 	},
   1777 
   1778 	// Applying filters triggers a tag request.
   1779 	applyFilters: function( event ) {
   1780 		var name,
   1781 			tags = this.filtersChecked(),
   1782 			request = { tag: tags },
   1783 			filteringBy = $( '.filtered-by .tags' );
   1784 
   1785 		if ( event ) {
   1786 			event.preventDefault();
   1787 		}
   1788 
   1789 		if ( ! tags ) {
   1790 			wp.a11y.speak( l10n.selectFeatureFilter );
   1791 			return;
   1792 		}
   1793 
   1794 		$( 'body' ).addClass( 'filters-applied' );
   1795 		$( '.filter-links li > a.current' )
   1796 			.removeClass( 'current' )
   1797 			.removeAttr( 'aria-current' );
   1798 
   1799 		filteringBy.empty();
   1800 
   1801 		_.each( tags, function( tag ) {
   1802 			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
   1803 			filteringBy.append( '<span class="tag">' + name + '</span>' );
   1804 		});
   1805 
   1806 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
   1807 		// or searching the local cache.
   1808 		this.collection.query( request );
   1809 	},
   1810 
   1811 	// Save the user's WordPress.org username and get his favorite themes.
   1812 	saveUsername: function ( event ) {
   1813 		var username = $( '#wporg-username-input' ).val(),
   1814 			nonce = $( '#wporg-username-nonce' ).val(),
   1815 			request = { browse: 'favorites', user: username },
   1816 			that = this;
   1817 
   1818 		if ( event ) {
   1819 			event.preventDefault();
   1820 		}
   1821 
   1822 		// Save username on enter.
   1823 		if ( event.type === 'keyup' && event.which !== 13 ) {
   1824 			return;
   1825 		}
   1826 
   1827 		return wp.ajax.send( 'save-wporg-username', {
   1828 			data: {
   1829 				_wpnonce: nonce,
   1830 				username: username
   1831 			},
   1832 			success: function () {
   1833 				// Get the themes by sending Ajax POST request to api.wordpress.org/themes
   1834 				// or searching the local cache.
   1835 				that.collection.query( request );
   1836 			}
   1837 		} );
   1838 	},
   1839 
   1840 	/**
   1841 	 * Get the checked filters.
   1842 	 *
   1843 	 * @return {Array} of tags or false
   1844 	 */
   1845 	filtersChecked: function() {
   1846 		var items = $( '.filter-group' ).find( ':checkbox' ),
   1847 			tags = [];
   1848 
   1849 		_.each( items.filter( ':checked' ), function( item ) {
   1850 			tags.push( $( item ).prop( 'value' ) );
   1851 		});
   1852 
   1853 		// When no filters are checked, restore initial state and return.
   1854 		if ( tags.length === 0 ) {
   1855 			$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
   1856 			$( '.filter-drawer .clear-filters' ).hide();
   1857 			$( 'body' ).removeClass( 'filters-applied' );
   1858 			return false;
   1859 		}
   1860 
   1861 		$( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
   1862 		$( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
   1863 
   1864 		return tags;
   1865 	},
   1866 
   1867 	activeClass: 'current',
   1868 
   1869 	/**
   1870 	 * When users press the "Upload Theme" button, show the upload form in place.
   1871 	 */
   1872 	uploader: function() {
   1873 		var uploadViewToggle = $( '.upload-view-toggle' ),
   1874 			$body = $( document.body );
   1875 
   1876 		uploadViewToggle.on( 'click', function() {
   1877 			// Toggle the upload view.
   1878 			$body.toggleClass( 'show-upload-view' );
   1879 			// Toggle the `aria-expanded` button attribute.
   1880 			uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
   1881 		});
   1882 	},
   1883 
   1884 	// Toggle the full filters navigation.
   1885 	moreFilters: function( event ) {
   1886 		var $body = $( 'body' ),
   1887 			$toggleButton = $( '.drawer-toggle' );
   1888 
   1889 		event.preventDefault();
   1890 
   1891 		if ( $body.hasClass( 'filters-applied' ) ) {
   1892 			return this.backToFilters();
   1893 		}
   1894 
   1895 		this.clearSearch();
   1896 
   1897 		themes.router.navigate( themes.router.baseUrl( '' ) );
   1898 		// Toggle the feature filters view.
   1899 		$body.toggleClass( 'show-filters' );
   1900 		// Toggle the `aria-expanded` button attribute.
   1901 		$toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
   1902 	},
   1903 
   1904 	/**
   1905 	 * Clears all the checked filters.
   1906 	 *
   1907 	 * @uses filtersChecked()
   1908 	 */
   1909 	clearFilters: function( event ) {
   1910 		var items = $( '.filter-group' ).find( ':checkbox' ),
   1911 			self = this;
   1912 
   1913 		event.preventDefault();
   1914 
   1915 		_.each( items.filter( ':checked' ), function( item ) {
   1916 			$( item ).prop( 'checked', false );
   1917 			return self.filtersChecked();
   1918 		});
   1919 	},
   1920 
   1921 	backToFilters: function( event ) {
   1922 		if ( event ) {
   1923 			event.preventDefault();
   1924 		}
   1925 
   1926 		$( 'body' ).removeClass( 'filters-applied' );
   1927 	},
   1928 
   1929 	clearSearch: function() {
   1930 		$( '#wp-filter-search-input').val( '' );
   1931 	}
   1932 });
   1933 
   1934 themes.InstallerRouter = Backbone.Router.extend({
   1935 	routes: {
   1936 		'theme-install.php?theme=:slug': 'preview',
   1937 		'theme-install.php?browse=:sort': 'sort',
   1938 		'theme-install.php?search=:query': 'search',
   1939 		'theme-install.php': 'sort'
   1940 	},
   1941 
   1942 	baseUrl: function( url ) {
   1943 		return 'theme-install.php' + url;
   1944 	},
   1945 
   1946 	themePath: '?theme=',
   1947 	browsePath: '?browse=',
   1948 	searchPath: '?search=',
   1949 
   1950 	search: function( query ) {
   1951 		$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
   1952 	},
   1953 
   1954 	navigate: navigateRouter
   1955 });
   1956 
   1957 
   1958 themes.RunInstaller = {
   1959 
   1960 	init: function() {
   1961 		// Set up the view.
   1962 		// Passes the default 'section' as an option.
   1963 		this.view = new themes.view.Installer({
   1964 			section: 'popular',
   1965 			SearchView: themes.view.InstallerSearch
   1966 		});
   1967 
   1968 		// Render results.
   1969 		this.render();
   1970 
   1971 		// Start debouncing user searches after Backbone.history.start().
   1972 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
   1973 	},
   1974 
   1975 	render: function() {
   1976 
   1977 		// Render results.
   1978 		this.view.render();
   1979 		this.routes();
   1980 
   1981 		if ( Backbone.History.started ) {
   1982 			Backbone.history.stop();
   1983 		}
   1984 		Backbone.history.start({
   1985 			root: themes.data.settings.adminUrl,
   1986 			pushState: true,
   1987 			hashChange: false
   1988 		});
   1989 	},
   1990 
   1991 	routes: function() {
   1992 		var self = this,
   1993 			request = {};
   1994 
   1995 		// Bind to our global `wp.themes` object
   1996 		// so that the router is available to sub-views.
   1997 		themes.router = new themes.InstallerRouter();
   1998 
   1999 		// Handles `theme` route event.
   2000 		// Queries the API for the passed theme slug.
   2001 		themes.router.on( 'route:preview', function( slug ) {
   2002 
   2003 			// Remove existing handlers.
   2004 			if ( themes.preview ) {
   2005 				themes.preview.undelegateEvents();
   2006 				themes.preview.unbind();
   2007 			}
   2008 
   2009 			// If the theme preview is active, set the current theme.
   2010 			if ( self.view.view.theme && self.view.view.theme.preview ) {
   2011 				self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
   2012 				self.view.view.theme.preview();
   2013 			} else {
   2014 
   2015 				// Select the theme by slug.
   2016 				request.theme = slug;
   2017 				self.view.collection.query( request );
   2018 				self.view.collection.trigger( 'update' );
   2019 
   2020 				// Open the theme preview.
   2021 				self.view.collection.once( 'query:success', function() {
   2022 					$( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
   2023 				});
   2024 
   2025 			}
   2026 		});
   2027 
   2028 		/*
   2029 		 * Handles sorting / browsing routes.
   2030 		 * Also handles the root URL triggering a sort request
   2031 		 * for `popular`, the default view.
   2032 		 */
   2033 		themes.router.on( 'route:sort', function( sort ) {
   2034 			if ( ! sort ) {
   2035 				sort = 'popular';
   2036 				themes.router.navigate( themes.router.baseUrl( '?browse=popular' ), { replace: true } );
   2037 			}
   2038 			self.view.sort( sort );
   2039 
   2040 			// Close the preview if open.
   2041 			if ( themes.preview ) {
   2042 				themes.preview.close();
   2043 			}
   2044 		});
   2045 
   2046 		// The `search` route event. The router populates the input field.
   2047 		themes.router.on( 'route:search', function() {
   2048 			$( '.wp-filter-search' ).trigger( 'focus' ).trigger( 'keyup' );
   2049 		});
   2050 
   2051 		this.extraRoutes();
   2052 	},
   2053 
   2054 	extraRoutes: function() {
   2055 		return false;
   2056 	}
   2057 };
   2058 
   2059 // Ready...
   2060 $( function() {
   2061 	if ( themes.isInstall ) {
   2062 		themes.RunInstaller.init();
   2063 	} else {
   2064 		themes.Run.init();
   2065 	}
   2066 
   2067 	// Update the return param just in time.
   2068 	$( document.body ).on( 'click', '.load-customize', function() {
   2069 		var link = $( this ), urlParser = document.createElement( 'a' );
   2070 		urlParser.href = link.prop( 'href' );
   2071 		urlParser.search = $.param( _.extend(
   2072 			wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
   2073 			{
   2074 				'return': window.location.href
   2075 			}
   2076 		) );
   2077 		link.prop( 'href', urlParser.href );
   2078 	});
   2079 
   2080 	$( '.broken-themes .delete-theme' ).on( 'click', function() {
   2081 		return confirm( _wpThemeSettings.settings.confirmDelete );
   2082 	});
   2083 });
   2084 
   2085 })( jQuery );
   2086 
   2087 // Align theme browser thickbox.
   2088 jQuery( function($) {
   2089 	window.tb_position = function() {
   2090 		var tbWindow = $('#TB_window'),
   2091 			width = $(window).width(),
   2092 			H = $(window).height(),
   2093 			W = ( 1040 < width ) ? 1040 : width,
   2094 			adminbar_height = 0;
   2095 
   2096 		if ( $('#wpadminbar').length ) {
   2097 			adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 );
   2098 		}
   2099 
   2100 		if ( tbWindow.length >= 1 ) {
   2101 			tbWindow.width( W - 50 ).height( H - 45 - adminbar_height );
   2102 			$('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height );
   2103 			tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'});
   2104 			if ( typeof document.body.style.maxWidth !== 'undefined' ) {
   2105 				tbWindow.css({'top': 20 + adminbar_height + 'px', 'margin-top': '0'});
   2106 			}
   2107 		}
   2108 	};
   2109 
   2110 	$(window).on( 'resize', function(){ tb_position(); });
   2111 });