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 );