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