HEX
Server: Apache
System: Linux webm003.cluster123.gra.hosting.ovh.net 6.18.39-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Tue Jul 21 12:03:15 CEST 2026 x86_64
User: vanessatli (59366)
PHP: 5.4.45
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
File: /home/vanessatli/www/wp-content/plugins/themegrill-demo-importer/assets/js/admin/demo-importer.js
/* global _demoImporterSettings */
window.wp = window.wp || {};

( function( $ ) {

// Set up our namespace...
	var demos, l10n, route;
	demos = wp.demos = wp.demos || {};

// Store the demo data and settings for organized and quick access
// demos.data.settings, demos.data.demos, demos.data.l10n
	demos.data = _demoImporterSettings;
	l10n = demos.data.l10n;
	route = _demoImporterSettings.routes;

// Shortcut for isNew check
	demos.isNew = !! demos.data.settings.isNew;

// Setup app structure
	_.extend( demos, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });

	demos.Model = Backbone.Model.extend({
		// Adds attributes to the default data coming through the demos api
		// Map `id` to `slug` for shared code
		initialize: function() {
			var description;

			// Set the attributes
			this.set({
				// slug is for installation, id is for existing.
				id: this.get( 'slug' ) || this.get( 'id' )
			});

			// Map `section.description` to `description`
			// as the API sometimes returns it differently
			if ( this.has( 'sections' ) ) {
				description = this.get( 'sections' ).description;
				this.set({ description: description });
			}
		}
	});

// Main view controller for demo importer
// Unifies and renders all available views
	demos.view.Appearance = wp.Backbone.View.extend({

		el: '#wpbody-content .wrap .theme-browser',

		window: $( window ),
		// Pagination instance
		page: 0,

		// Sets up a throttler for binding to 'scroll'
		initialize: function( options ) {
			// Scroller checks how far the scroll position is
			_.bindAll( this, 'scroller' );

			this.SearchView = options.SearchView ? options.SearchView : demos.view.Search;
			// Bind to the scroll event and throttle
			// the results from this.scroller
			this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
		},

		// Main render control
		render: function() {
			// Setup the main demo view
			// with the current demo collection
			this.view = new demos.view.Demos({
				collection: this.collection,
				parent: this
			});

			// Render search form.
			this.search();

			this.$el.removeClass( 'search-loading' );

			// Render and append
			this.view.render();
			this.$el.empty().append( this.view.el ).addClass( 'rendered' );
		},

		// Defines search element container
		searchContainer: $( '.search-form' ),

		// Search input and view
		// for current demo collection
		search: function() {
			var view,
				self = this;

			// Don't render the search if there is only one demo
			if ( demos.data.demos.length === 1 ) {
				return;
			}

			view = new this.SearchView({
				collection: self.collection,
				parent: this
			});
			self.SearchView = view;

			// Render and append after screen title
			view.render();
			this.searchContainer
				.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
				.append( view.el )
				.on( 'submit', function( event ) {
					event.preventDefault();
				});
		},

		// Checks when the user gets close to the bottom
		// of the page and triggers a demo:scroll event
		scroller: function() {
			var self = this,
				bottom, threshold;

			bottom = this.window.scrollTop() + self.window.height();
			threshold = self.$el.offset().top + self.$el.outerHeight( false ) - self.window.height();
			threshold = Math.round( threshold * 0.9 );

			if ( bottom > threshold ) {
				// this.trigger( 'demo:scroll' );
			}
		}
	});

// Set up the Collection for our demo data
// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
	demos.Collection = Backbone.Collection.extend({

		model: demos.Model,

		// Search terms
		terms: '',

		// Controls searching on the current theme collection
		// and triggers an update event
		doSearch: function( value ) {

			// Don't do anything if we've already done this search
			// Useful because the Search handler fires multiple times per keystroke
			if ( this.terms === value ) {
				return;
			}

			// Updates terms with the value passed
			this.terms = value;

			// If we have terms, run a search...
			if ( this.terms.length > 0 ) {
				this.search( this.terms );
			}

			// If search is blank, show all demos
			// Useful for resetting the views when you clean the input
			if ( this.terms === '' ) {
				this.reset( demos.data.demos );
				$( 'body' ).removeClass( 'no-results' );
			}

			// Trigger a 'demos:update' event
			this.trigger( 'demos:update' );
		},

		// Performs a search within the collection
		// @uses RegExp
		search: function( term ) {
			var match, results, haystack, name, description, author;

			// Start with a full collection
			this.reset( demos.data.demos, { silent: true } );

			// Escape the term string for RegExp meta characters
			term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );

			// Consider spaces as word delimiters and match the whole string
			// so matching terms can be combined
			term = term.replace( / /g, ')(?=.*' );
			match = new RegExp( '^(?=.*' + term + ').+', 'i' );

			// Find results
			// _.filter and .test
			results = this.filter( function( data ) {
				name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
				description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
				author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );

				haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );

				if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
					data.set( 'displayAuthor', true );
				}

				return match.test( haystack );
			});

			if ( results.length === 0 ) {
				this.trigger( 'query:empty' );
			} else {
				$( 'body' ).removeClass( 'no-results' );
			}

			this.reset( results );
		},

		// Paginates the collection with a helper method
		// that slices the collection
		paginate: function( instance ) {
			var collection = this;
			instance = instance || 0;

			// // Demos per instance are set at 20
			// collection = _( collection.rest( 20 * instance ) );
			// collection = _( collection.first( 20 ) );

			return collection;
		},

		count: false,

		// Handles requests for more demos
		// and caches results
		//
		// When we are missing a cache object we fire an apiCall()
		// which triggers events of `query:success` or `query:fail`
		query: function( request ) {
			/**
			 * @static
			 * @type Array
			 */
			var queries = this.queries,
				self = this,
				query, isPaginated, count;

			// Store current query request args
			// for later use with the event `demo:end`
			this.currentQuery.request = request;

			// Search the query cache for matches.
			query = _.find( queries, function( query ) {
				return _.isEqual( query.request, request );
			});

			// If the request matches the stored currentQuery.request
			// it means we have a paginated request.
			isPaginated = _.has( request, 'page' );

			// Reset the internal api page counter for non paginated queries.
			if ( ! isPaginated ) {
				this.currentQuery.page = 1;
			}

			// Otherwise, send a new API call and add it to the cache.
			if ( ! query && ! isPaginated ) {
				query = this.apiCall( request ).done( function( data ) {

					// Update the collection with the queried data.
					if ( data.demos ) {
						self.reset( data.demos );
						count = data.info.results;
						// Store the results and the query request
						queries.push( { demos: data.demos, request: request, total: count } );
					}

					// Trigger a collection refresh event
					// and a `query:success` event with a `count` argument.
					self.trigger( 'demos:update' );
					self.trigger( 'query:success', count );

					if ( data.demos && data.demos.length === 0 ) {
						self.trigger( 'query:empty' );
					}

				}).fail( function() {
					self.trigger( 'query:fail' );
				});
			} else {
				// If it's a paginated request we need to fetch more demos...
				if ( isPaginated ) {
					return this.apiCall( request, isPaginated ).done( function( data ) {
						// Add the new demos to the current collection
						// @todo update counter
						self.add( data.demos );
						self.trigger( 'query:success' );

						// We are done loading demos for now.
						self.loadingDemos = false;

					}).fail( function() {
						self.trigger( 'query:fail' );
					});
				}

				if ( query.demos.length === 0 ) {
					self.trigger( 'query:empty' );
				} else {
					$( 'body' ).removeClass( 'no-results' );
				}

				// Only trigger an update event since we already have the demos
				// on our cached object
				if ( _.isNumber( query.total ) ) {
					this.count = query.total;
				}

				this.reset( query.demos );
				if ( ! query.total ) {
					this.count = this.length;
				}

				this.trigger( 'demos:update' );
				this.trigger( 'query:success', this.count );
			}
		},

		// Local cache array for API queries
		queries: [],

		// Keep track of current query so we can handle pagination
		currentQuery: {
			page: 1,
			request: {}
		},

		// Send request to api.github.com/repos/themegrill-demo-pack
		apiCall: function( request, paginated ) {
			return wp.ajax.send( 'query-demos', {
				data: {
					// Request data
					request: _.extend({
						per_page: 100
					}, request )
				},

				beforeSend: function() {
					if ( ! paginated ) {
						// Spin it
						$( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
					}
				}
			});
		},

		// Static status controller for when we are loading demos.
		loadingDemos: false
	});

// This is the view that controls each demo item
// that will be displayed on the screen
	demos.view.Demo = wp.Backbone.View.extend({

		// Wrap demo data on a div.theme element
		className: 'theme',

		// Reflects which demo view we have
		// 'grid' (default) or 'detail'
		state: 'grid',

		// The HTML template for each element to be rendered
		html: demos.template( 'demo' ),

		events: {
			'click': 'preview',
			'keydown': 'preview',
			'touchend': 'preview',
			'keyup': 'addFocus',
			'touchmove': 'preventExpand',
			'click .demo-import': 'importDemo'
		},

		touchDrag: false,

		initialize: function() {
			this.model.on( 'change', this.render, this );
		},

		render: function() {
			var data = this.model.toJSON();

			// Render demos using the html template
			this.$el.html( this.html( data ) ).attr({
				tabindex: 0,
				'aria-describedby' : data.id + '-action ' + data.id + '-name',
				'data-slug': data.id
			});

			// Renders active demo styles
			this.activeDemo();

			if ( this.model.get( 'displayAuthor' ) ) {
				this.$el.addClass( 'display-author' );
			}
		},

		// Adds a class to the currently active demo
		// and to the overlay in detailed view mode
		activeDemo: function() {
			if ( this.model.get( 'active' ) ) {
				this.$el.addClass( 'active' );
			}
		},

		// Add class of focus to the demo we are focused on.
		addFocus: function() {
			var $demoToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme');

			$('.theme.focus').removeClass('focus');
			$demoToFocus.addClass('focus');
		},

		preventExpand: function() {
			this.touchDrag = true;
		},

		preview: function( event ) {
			var self = this,
				current, preview;

			event = event || window.event;

			// Bail if the user scrolled on a touch device
			if ( this.touchDrag === true ) {
				return this.touchDrag = false;
			}

			// Allow direct link path to installing a demo.
			if ( $( event.target ).not( '.install-demo-preview' ).parents( '.theme-actions' ).length ) {
				return;
			}

			// 'enter' and 'space' keys expand the details view when a demo is :focused
			if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
				return;
			}

			// pressing enter while focused on the buttons shouldn't open the preview
			if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
				return;
			}

			event.preventDefault();

			event = event || window.event;

			// Set focus to current demo.
			demos.focusedDemo = this.$el;

			// Construct a new Preview view.
			demos.preview = preview = new demos.view.Preview({
				model: this.model
			});

			// Render the view and append it.
			preview.render();
			this.setNavButtonsState();

			// Hide previous/next navigation if there is only one demo
			if ( this.model.collection.length === 1 ) {
				preview.$el.addClass( 'no-navigation' );
			} else {
				preview.$el.removeClass( 'no-navigation' );
			}

			// Append preview
			$( 'div.wrap' ).append( preview.el );

			// Listen to our preview object
			// for `demo:next` and `demo:previous` events.
			this.listenTo( preview, 'demo:next', function() {

				// Keep local track of current demo model.
				current = self.model;

				// If we have ventured away from current model update the current model position.
				if ( ! _.isUndefined( self.current ) ) {
					current = self.current;
				}

				// Get next demo model.
				self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );

				// If we have no more themes, bail.
				if ( _.isUndefined( self.current ) ) {
					self.options.parent.parent.trigger( 'demo:end' );
					return self.current = current;
				}

				preview.model = self.current;

				// Render and append.
				preview.render();
				this.setNavButtonsState();
				$( '.next-theme' ).focus();
			})
				.listenTo( preview, 'demo:previous', function() {

					// Keep track of current demo model.
					current = self.model;

					// Bail early if we are at the beginning of the collection
					if ( self.model.collection.indexOf( self.current ) === 0 ) {
						return;
					}

					// If we have ventured away from current model update the current model position.
					if ( ! _.isUndefined( self.current ) ) {
						current = self.current;
					}

					// Get previous theme model.
					self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );

					// If we have no more themes, bail.
					if ( _.isUndefined( self.current ) ) {
						return;
					}

					preview.model = self.current;

					// Render and append.
					preview.render();
					this.setNavButtonsState();
					$( '.previous-theme' ).focus();
				});

			this.listenTo( preview, 'preview:close', function() {
				self.current = self.model;
			});
		},

		// Handles .disabled classes for previous/next buttons in demo installer preview
		setNavButtonsState: function() {
			var $demoInstaller = $( '.theme-install-overlay' ),
				current = _.isUndefined( this.current ) ? this.model : this.current,
				previousDemoButton = $demoInstaller.find( '.previous-theme' ),
				nextDemoButton = $demoInstaller.find( '.next-theme' );

			// Disable previous at the zero position
			if ( 0 === this.model.collection.indexOf( current ) ) {
				previousDemoButton
					.addClass( 'disabled' )
					.prop( 'disabled', true );

				nextDemoButton.focus();
			}

			// Disable next if the next model is undefined
			if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
				nextDemoButton
					.addClass( 'disabled' )
					.prop( 'disabled', true );

				previousDemoButton.focus();
			}
		},

		importDemo: function( event ) {
			var _this       = this,
				$target     = $( event.target ),
				pluginsList = $( event.target ).data( 'plugins' );
			event.preventDefault();

			if ( $target.hasClass( 'disabled' ) || $target.hasClass( 'updating-message' ) ) {
				return;
			}

			$.confirm( {
				title : '',
				content: '<div class="demo-import-confirm-message">' + ( _this.model.get( 'ceAddonNotice' ) ? wp.demos.data.settings.ceAddonNotice : '' )  + wp.demos.data.settings.confirmImport + '</div>',
				boxWidth: '50%',
				useBootstrap: false,
				buttons: {
					confirm: {
						text: wp.demos.data.l10n.confirmMsg,
						keys: ['enter'],
						btnClass: _this.model.get( 'ceAddonNotice' ) ? 'disabled demo-import-confirm-button' : 'demo-import-confirm-button',
						action: function(){
							_this.processImport( $target, pluginsList, _this );
						}
					},
					cancel: {
						btnClass: 'demo-import-cancel-button',
						action: function() {
							return;
						}
					}
				}
			} );

		},
		processImport: function ( $target, pluginsList, _this ) {
			wp.updates.maybeRequestFilesystemCredentials( event );

			$( document ).trigger( 'wp-plugin-bulk-install', pluginsList );

			// Find all the plugins which are required.
			$.each( pluginsList, function( plugin_slug, plugin_data ) {
				if ( ! plugin_data.is_active ) {
					// Add it to the queue.
					wp.updates.queue.push( {
						action: 'install-plugin',
						data:   {
							plugin: plugin_data.slug,
							name: plugin_data.name,
							slug: plugin_slug,
							demo: $target.data( 'slug' )
						}
					} );
				}
			} );

			$( document ).on( 'wp-demo-import-success', function( event, response ) {
				if ( _this.model.get( 'id' ) === response.slug ) {
					_this.render();
					_this.model.set( { 'imported': true } );
				}
			} );

			// Add it to the queue.
			wp.updates.queue.push( {
				action: 'import-demo',
				data: {
					slug: $target.data( 'slug' )
				}
			} );

			// Check the queue, now that the event handlers have been added.
			wp.updates.queueChecker();
		}
	});

// Theme Preview view
// Set ups a modal overlay with the expanded demo data
	demos.view.Preview = wp.Backbone.View.extend({

		className: 'wp-full-overlay expanded',
		el: '.theme-install-overlay',

		events: {
			'click .close-full-overlay': 'close',
			'click .collapse-sidebar': 'collapse',
			'click .devices button': 'previewDevice',
			'click .previous-theme': 'previousDemo',
			'click .next-theme': 'nextDemo',
			'keyup': 'keyEvent',
			'click .demo-import': 'importDemo'
		},

		// The HTML template for the demo preview
		html: demos.template( 'demo-preview' ),

		render: function() {
			var self = this,
				currentPreviewDevice,
				data = this.model.toJSON(),
				$body = $( document.body );

			$body.attr( 'aria-busy', 'true' );

			this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );

			currentPreviewDevice = this.$el.data( 'current-preview-device' );
			if ( currentPreviewDevice ) {
				self.tooglePreviewDeviceButtons( currentPreviewDevice );
			}

			demos.router.navigate( demos.router.baseUrl( demos.router.demoPath + this.model.get( 'id' ) ), { replace: false } );

			this.$el.fadeIn( 200, function() {
				$body.addClass( 'demo-importer-active full-overlay-active' );
			});

			this.$el.find( 'iframe' ).one( 'load', function() {
				self.iframeLoaded();
			});
		},

		iframeLoaded: function() {
			this.$el.addClass( 'iframe-ready' );
			$( document.body ).attr( 'aria-busy', 'false' );
		},

		close: function() {
			this.$el.fadeOut( 200, function() {
				$( 'body' ).removeClass( 'demo-importer-active full-overlay-active' );

				// Return focus to the demo div
				if ( demos.focusedDemo ) {
					demos.focusedDemo.focus();
				}
			}).removeClass( 'iframe-ready' );

			// Restore the previous browse tab if available.
			if ( demos.router.selectedTab ) {
				demos.router.navigate( demos.router.baseUrl( '&browse=' + demos.router.selectedTab ) );
			} else {
				demos.router.navigate( demos.router.baseUrl( '' ) );
			}
			this.trigger( 'preview:close' );
			this.undelegateEvents();
			this.unbind();
			return false;
		},

		collapse: function( event ) {
			var $button = $( event.currentTarget );
			if ( 'true' === $button.attr( 'aria-expanded' ) ) {
				$button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
			} else {
				$button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
			}

			this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
			return false;
		},

		previewDevice: function( event ) {
			var device = $( event.currentTarget ).data( 'device' );

			this.$el
				.removeClass( 'preview-desktop preview-tablet preview-mobile' )
				.addClass( 'preview-' + device )
				.data( 'current-preview-device', device );

			this.tooglePreviewDeviceButtons( device );
		},

		tooglePreviewDeviceButtons: function( newDevice ) {
			var $devices = $( '.wp-full-overlay-footer .devices' );

			$devices.find( 'button' )
				.removeClass( 'active' )
				.attr( 'aria-pressed', false );

			$devices.find( 'button.preview-' + newDevice )
				.addClass( 'active' )
				.attr( 'aria-pressed', true );
		},

		keyEvent: function( event ) {
			// The escape key closes the preview
			if ( event.keyCode === 27 ) {
				this.undelegateEvents();
				this.close();
			}
			// The right arrow key, next demo
			if ( event.keyCode === 39 ) {
				_.once( this.nextDemo() );
			}

			// The left arrow key, previous demo
			if ( event.keyCode === 37 ) {
				this.previousDemo();
			}
		},

		nextDemo: function() {
			var self = this;
			self.trigger( 'demo:next', self.model.cid );
			return false;
		},

		previousDemo: function() {
			var self = this;
			self.trigger( 'demo:previous', self.model.cid );
			return false;
		},

		importDemo: function( event ) {
			var _this = this,
				$target       = $( event.target ),
				pluginsList   = $( '.plugins-list-table' ).find( '#the-list tr' ),
				success       = 0,
				error         = 0,
				errorMessages = [];

			event.preventDefault();

			if ( $target.hasClass( 'disabled' ) || $target.hasClass( 'updating-message' ) ) {
				return;
			}

			$.confirm( {
				title : '',
				content: '<div class="demo-import-confirm-message">' + wp.demos.data.settings.confirmImport + '</div>',
				boxWidth: '50%',
				useBootstrap: false,
				buttons: {
					confirm: {
						text: wp.demos.data.l10n.confirmMsg,
						keys: ['enter'],
						btnClass: 'demo-import-confirm-button',
						action: function(){
							_this.processImport( $target, pluginsList, success, error, errorMessages, _this );
						}
					},
					cancel: {
						btnClass: 'demo-import-cancel-button',
						action: function() {
							return;
						}
					}
				},
				onContentReady: function () {
					$( 'body' ).addClass( 'demo-import-message-popup' );
				},
				onDestroy: function () {
					$( 'body' ).removeClass( 'demo-import-message-popup' );
				}
			} );
		},

		processImport: function ( $target, pluginsList, success, error, errorMessages, _this ) {
			wp.updates.maybeRequestFilesystemCredentials( event );

			// Disable the next and previous demo.
			$( '.theme-install-overlay' ).find( '.next-theme, .previous-theme' ).addClass( 'disabled' );

			$( document ).trigger( 'wp-plugin-bulk-install', pluginsList );

			// Find all the plugins which are required.
			pluginsList.each( function( index, element ) {
				var $itemRow = $( element );

				// Only add inactive items to the update queue.
				if ( ! $itemRow.hasClass( 'inactive' ) || $itemRow.find( 'notice-error' ).length ) {
					return;
				}

				// Add it to the queue.
				wp.updates.queue.push( {
					action: 'install-plugin',
					data:   {
						plugin: $itemRow.data( 'plugin' ),
						slug: $itemRow.data( 'slug' ),
						demo: $target.data( 'slug' )
					}
				} );
			} );

			// Scroll to bulk plugin install area.
			$( document ).on( 'wp-plugin-bulk-installing', function() {
				$( '.wp-full-overlay-sidebar-content' ).animate( { scrollTop: $( document ).height() } );
			} );

			// Display bulk notification for install of plugin.
			$( document ).on( 'wp-plugin-bulk-install-success wp-plugin-bulk-install-error', function( event, response ) {
				var $itemRow = $( '[data-slug="' + response.slug + '"]' ),
					$bulkActionNotice, itemName;

				if ( 'wp-' + response.install + '-bulk-install-success' === event.type ) {
					success++;
				} else {
					itemName = response.pluginName ? response.pluginName : $itemRow.find( '.plugin-name' ).text();

					error++;
					errorMessages.push( itemName + ': ' + response.errorMessage );
				}

				wp.updates.adminNotice = wp.template( 'wp-bulk-installs-admin-notice' );

				// Remove previous error messages, if any.
				$( '.plugins-details .bulk-action-notice' ).remove();

				$( '.plugins-details .plugins-info' ).after( wp.updates.adminNotice( {
					id:            'bulk-action-notice',
					className:     'bulk-action-notice notice-alt',
					successes:     success,
					errors:        error,
					errorMessages: errorMessages,
					type:          response.install
				} ) );

				$bulkActionNotice = $( '#bulk-action-notice' ).on( 'click', 'button', function() {
					// $( this ) is the clicked button, no need to get it again.
					$( this )
						.toggleClass( 'bulk-action-errors-collapsed' )
						.attr( 'aria-expanded', ! $( this ).hasClass( 'bulk-action-errors-collapsed' ) );
					// Show the errors list.
					$bulkActionNotice.find( '.bulk-action-errors' ).toggleClass( 'hidden' );
				} );

				if ( ! wp.updates.queue.length ) {
					if ( error > 0 ) {
						$target
							.removeClass( 'updating-message' )
							.text( $target.data( 'originaltext' ) );
					} else {
						_this.model.set( { requiredPlugins: false } );

						// Disable the next and previous demo.
						$( '.theme-install-overlay' ).find( '.next-theme, .previous-theme' ).addClass( 'disabled' );
					}
				}
			} );

			// Reset admin notice template after #bulk-action-notice was added.
			$( document ).on( 'wp-updates-notice-added', function() {
				wp.updates.adminNotice = wp.template( 'wp-updates-admin-notice' );
			} );

			$( document ).on( 'wp-demo-import-success', function( event, response ) {
				if ( _this.model.get( 'id' ) === response.slug ) {
					_this.model.set( { 'imported': true } );
				}
			} );

			// Add it to the queue.
			wp.updates.queue.push( {
				action: 'import-demo',
				data: {
					slug: $target.data( 'slug' )
				}
			} );

			// Check the queue, now that the event handlers have been added.
			wp.updates.queueChecker();
		}
	});

// Controls the rendering of div.themes,
// a wrapper that will hold all the theme elements
	demos.view.Demos = wp.Backbone.View.extend({

		className: 'themes wp-clearfix',
		$overlay: $( 'div.theme-overlay' ),

		// Number to keep track of scroll position
		// while in theme-overlay mode
		index: 0,

		// The demo count element
		count: $( '.wrap .demo-count' ),

		// The live demos count
		liveDemoCount: 0,

		initialize: function( options ) {
			var self = this;

			// Set up parent
			this.parent = options.parent;

			// Set current view to [grid]
			this.setView( 'grid' );

			// Move the imported demo to the beginning of the collection
			self.importedDemo();

			// When the collection is updated by user input...
			this.listenTo( self.collection, 'demos:update', function() {
				self.parent.page = 0;
				self.importedDemo();
				self.render( this );
			} );

			// Update demo count to full result set when available.
			this.listenTo( self.collection, 'query:success', function( count ) {
				if ( _.isNumber( count ) ) {
					self.count.text( count );
					self.announceSearchResults( count );
				} else {
					self.count.text( self.collection.length );
					self.announceSearchResults( self.collection.length );
				}
			});

			this.listenTo( self.collection, 'query:empty', function() {
				$( 'body' ).addClass( 'no-results' );
			});

			this.listenTo( this.parent, 'demo:scroll', function() {
				self.renderDemos( self.parent.page );
			});
		},

		// Manages rendering of demo pages
		// and keeping demo count in sync
		render: function() {
			// Clear the DOM, please
			this.$el.empty();

			// Generate the demos
			// Using page instance
			// While checking the collection has items
			if ( this.options.collection.size() > 0 ) {
				this.renderDemos( this.parent.page );
			}

			// Display a live demo count for the collection
			this.liveDemoCount = this.collection.count ? this.collection.count : this.collection.length;
			this.count.text( this.liveDemoCount );
		},

		// Iterates through each instance of the collection
		// and renders each demo module
		renderDemos: function( page ) {
			var self = this;

			self.instance = self.collection.paginate( page );

			// If we have no more demos bail
			if ( self.instance.size() === 0 ) {
				// Fire a no-more-demos event.
				this.parent.trigger( 'demo:end' );
				return;
			}

			// Make sure the add-new stays at the end
			if ( demos.isNew && page >= 1 ) {
				$( '.add-new-theme' ).remove();
			}

			// Loop through the demos and setup each demo view
			self.instance.each( function( demo ) {
				self.demo = new demos.view.Demo({
					model: demo,
					parent: self
				});

				// Render the views...
				self.demo.render();
				// and append them to div.themes
				self.$el.append( self.demo.el );
			});

			// 'Suggest us!' element shown at the end of the grid
			if ( demos.isNew && demos.data.settings.suggestURI ) {
				this.$el.append( '<div class="theme add-new-theme"><a href="' + demos.data.settings.suggestURI + '" target="blank"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">' + l10n.suggestNew + '</h2></a></div>' );
			}

			this.parent.page++;
		},

		// Grabs imported demo and puts it at the beginning of the collection
		importedDemo: function() {
			var self = this,
				current;

			current = self.collection.findWhere({ active: true });

			// Move the imported demo to the beginning of the collection
			if ( current ) {
				self.collection.remove( current );
				self.collection.add( current, { at:0 } );
			}
		},

		// Sets current view
		setView: function( view ) {
			return view;
		},

		// Dispatch audible search results feedback message
		announceSearchResults: function( count ) {
			if ( 0 === count ) {
				wp.a11y.speak( l10n.noDemosFound );
			} else {
				wp.a11y.speak( l10n.demosFound.replace( '%d', count ) );
			}
		}
	});

// Search input view controller.
	demos.view.Search = wp.Backbone.View.extend({

		tagName: 'input',
		className: 'wp-filter-search',
		id: 'wp-filter-search-input',
		searching: false,

		attributes: {
			placeholder: l10n.searchPlaceholder,
			type: 'search',
			'aria-describedby': 'live-search-desc'
		},

		events: {
			'input': 'search',
			'keyup': 'search',
			'blur': 'pushState'
		},

		initialize: function( options ) {

			this.parent = options.parent;

			this.listenTo( this.parent, 'demo:close', function() {
				this.searching = false;
			} );
		},

		search: function( event ) {
			// Clear on escape.
			if ( event.type === 'keyup' && event.which === 27 ) {
				event.target.value = '';
			}

			// Since doSearch is debounced, it will only run when user input comes to a rest.
			this.doSearch( event );
		},

		// Runs a search on the demo collection.
		doSearch: function( event ) {
			var options = {};

			this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );

			// if search is initiated and key is not return
			if ( this.searching && event.which !== 13 ) {
				options.replace = true;
			} else {
				this.searching = true;
			}

			// Update the URL hash
			if ( event.target.value ) {
				demos.router.navigate( demos.router.baseUrl( demos.router.searchPath + event.target.value ), options );
			} else {
				demos.router.navigate( demos.router.baseUrl( '' ) );
			}
		},

		pushState: function( event ) {
			var url = demos.router.baseUrl( '' );

			if ( event.target.value ) {
				url = demos.router.baseUrl( demos.router.searchPath + encodeURIComponent( event.target.value ) );
			}

			this.searching = false;
			demos.router.navigate( url );
		}
	});

	/**
	 * Navigate router.
	 *
	 * @since 1.5.0
	 *
	 * @param {string} url - URL to navigate to.
	 * @param {object} state - State.
	 * @returns {void}
	 */
	function navigateRouter( url, state ) {
		var router = this;
		if ( Backbone.history._hasPushState ) {
			Backbone.Router.prototype.navigate.call( router, url, state );
		}
	}

// Sets up the routes events for relevant url queries
// Listens to [demo] and [search] params
	demos.Router = Backbone.Router.extend({

		routes: route,

		baseUrl: function( url ) {
			return _demoImporterSettings.baseURL + url;
		},

		demoPath: '&demo=',
		browsePath: '&browse=',
		searchPath: '&search=',

		search: function( query ) {
			$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
		},

		navigate: navigateRouter
	});

// Extend the main Search view
	demos.view.InstallerSearch = demos.view.Search.extend({

		events: {
			'input': 'search',
			'keyup': 'search'
		},

		terms: '',

		// Handles Ajax request for searching through demos in public repo
		search: function( event ) {

			// Tabbing or reverse tabbing into the search input shouldn't trigger a search
			if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
				return;
			}

			this.collection = this.options.parent.view.collection;

			// Clear on escape.
			if ( event.type === 'keyup' && event.which === 27 ) {
				event.target.value = '';
			}

			this.doSearch( event.target.value );
		},

		doSearch: function( value ) {
			var request = {};

			// Don't do anything if the search terms haven't changed.
			if ( this.terms === value ) {
				return;
			}

			// Updates terms with the value passed.
			this.terms = value;

			request.search = value;

			$( '.filter-links li > a.current' )
				.removeClass( 'current' )
				.removeAttr( 'aria-current' );

			this.collection.doSearch( value.replace( /\+/g, ' ' ) );

			// Set route
			demos.router.navigate( demos.router.baseUrl( demos.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
		}
	});

	demos.view.Installer = demos.view.Appearance.extend({

		el: '#wpbody-content .wrap',

		// Register events for sorting and filters in demo-navigation
		events: {
			'click .filter-links li > a': 'onSort'
		},

		// Initial render method
		render: function() {
			var self = this;

			this.search();

			this.collection = new demos.Collection();

			// Bump `collection.currentQuery.page` and request more demos if we hit the end of the page.
			this.listenTo( this, 'demo:end', function() {

				// Make sure we are not already loading
				if ( self.collection.loadingDemos ) {
					return;
				}

				// Set loadingDemos to true and bump page instance of currentQuery.
				self.collection.loadingDemos = true;
				self.collection.currentQuery.page++;

				// Use currentQuery.page to build the demos request.
				_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
				self.collection.query( self.collection.currentQuery.request );
			});

			this.listenTo( this.collection, 'query:success', function() {
				$( 'body' ).removeClass( 'loading-content' );
				$( '.theme-browser' ).find( 'div.error' ).remove();
			});

			this.listenTo( this.collection, 'query:fail', function() {
				$( 'body' ).removeClass( 'loading-content' );
				$( '.theme-browser' ).find( 'div.error' ).remove();
				$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
				$( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
					e.preventDefault();
					$( 'input.wp-filter-search' ).trigger( 'input' );
				} );
			});

			if ( this.view ) {
				this.view.remove();
			}

			// Set ups the view and passes the section argument
			this.view = new demos.view.Demos({
				collection: this.collection,
				parent: this
			});

			// Reset pagination every time the install view handler is run
			this.page = 0;

			// Render and append
			this.$el.find( '.themes' ).remove();
			this.view.render();
			this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
		},

		// Handles all the rendering of the public demo directory
		browse: function( section, builder ) {
			// Create a new collection with the proper demo data
			// for each section
			this.collection.query( { browse: section, builder: builder } );
		},

		// Sorting navigation
		onSort: function( event ) {
			var $el = $( event.target ),
				sort = $el.data( 'sort' ),
				type = $el.data( 'type' );

			event.preventDefault();

			// Restore the previous browse tab if available.
			sort = sort ? sort : demos.router.selectedTab;
			type = type ? type : demos.router.selectedType;

			// Bail if this is already active
			if ( $el.hasClass( this.activeClass ) ) {
				return;
			}

			this.sort( sort, type );

			// Trigger a router.naviagte update
			demos.router.navigate( demos.router.baseUrl( demos.router.browsePath + sort ) );
		},

		sort: function( sort, type ) {
			this.clearSearch();

			// Track sorting so we can restore the correct tab when closing preview.
			demos.router.selectedTab  = sort;
			demos.router.selectedType = type;

			$( '.filter-links li > a' )
				.removeClass( this.activeClass )
				.removeAttr( 'aria-current' );

			$( '[data-sort="' + sort + '"]' )
				.addClass( this.activeClass )
				.attr( 'aria-current', 'page' );

			$( '[data-type="' + type + '"]' )
				.addClass( this.activeClass )
				.attr( 'aria-current', 'page' );

			this.browse( sort, type );
		},

		activeClass: 'current',

		clearSearch: function() {
			$( '#wp-filter-search-input' ).val( '' );
		}
	});

// Execute and setup the application
	demos.RunInstaller = {

		init: function() {
			// Set up the view
			// Passes the default 'section' as an option
			this.view = new demos.view.Installer({
				section: 'all',
				SearchView: demos.view.InstallerSearch
			});

			// Render results
			this.render();

			// Start debouncing user searches after Backbone.history.start().
			this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
		},

		render: function() {

			// Render results
			this.view.render();
			this.routes();

			if ( Backbone.History.started ) {
				Backbone.history.stop();
			}
			Backbone.history.start({
				root: demos.data.settings.adminUrl,
				pushState: true,
				hashChange: false
			});
		},

		routes: function() {
			var self = this,
				request = {};

			// Bind to our global `wp.demos` object
			// so that the object is available to sub-views
			demos.router = new demos.Router();

			// Handles `demo` route event
			// Queries the API for the passed demo slug
			demos.router.on( 'route:preview', function( slug ) {

				// Remove existing handlers.
				if ( demos.preview ) {
					demos.preview.undelegateEvents();
					demos.preview.unbind();
				}

				// If the demo preview is active, set the current demo.
				if ( self.view.view.demo && self.view.view.demo.preview ) {
					self.view.view.demo.model = self.view.collection.findWhere( { 'slug': slug } );
					self.view.view.demo.preview();
				} else {

					// Select the demo by slug.
					request.demo = slug;
					self.view.collection.query( request );
					self.view.collection.trigger( 'update' );

					// Open the theme preview.
					self.view.collection.once( 'query:success', function() {
						$( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
					});
				}
			});

			// Handles sorting / browsing routes
			// Also handles the root URL triggering a sort request
			// for `all`, the default view
			demos.router.on( 'route:sort', function( sort ) {
				var type = demos.router.selectedType ? demos.router.selectedType : $( '.filter-links.pagebuilders li' ).first().find( 'a' ).data( 'type' );

				if ( ! sort || ! $( '[data-sort="' + sort + '"]' ).length ) {
					sort = 'all';
					demos.router.navigate( demos.router.baseUrl( '&browse=all' ), { replace: true } );
				}

				self.view.sort( sort, type );

				// Close the preview if open.
				if ( demos.preview ) {
					demos.preview.close();
				}
			});

			// The `search` route event. The router populates the input field.
			demos.router.on( 'route:search', function() {
				$( '.wp-filter-search' ).focus().trigger( 'keyup' );
			});

			this.extraRoutes();
		},

		extraRoutes: function() {
			return false;
		}
	};

// Ready...
	$( document ).ready( function() {
		demos.RunInstaller.init();

		// Initialize TipTip.
		$( document.body ).on( 'init_tooltips', function() {
			$( '#tiptip_holder' ).removeAttr( 'style' );
			$( '#tiptip_arrow' ).removeAttr( 'style' );
			$( '.tips' ).tipTip({ 'attribute': 'data-tip', 'defaultPosition': 'top', 'fadeIn': 50, 'fadeOut': 50, 'delay': 50 });
		} ).trigger( 'init_tooltips' );

		// Change the footer text when rating link is clicked.
		$( '.themegrill-demo-importer-rating-link' ).on( 'click', function() {
			var $this_el = $( this );

			$.post( demos.data.settings.ajaxUrl, {
				action: 'footer-text-rated'
			});

			$this_el.parent().text( $this_el.data( 'rated' ) );
		} );
	});

})( jQuery );