diff --git a/wp-admin/includes/theme.php b/wp-admin/includes/theme.php index ccfad6b169..94e811606a 100644 --- a/wp-admin/includes/theme.php +++ b/wp-admin/includes/theme.php @@ -345,3 +345,60 @@ function themes_api( $action, $args = null ) { */ return apply_filters( 'themes_api_result', $res, $action, $args ); } + +/** + * Prepare themes for JavaScript. + * + * @since 3.8.0 + * + * @param array $themes Optional. Array of WP_Theme objects to prepare. + * Defaults to all allowed themes. + * + * @return array An associative array of theme data. + */ +function wp_prepare_themes_for_js( $themes = null ) { + if ( null === $themes ) { + $themes = wp_get_themes( array( 'allowed' => true ) ); + } + + $prepared_themes = array(); + $current_theme = get_stylesheet(); + + $updates = array(); + if ( current_user_can( 'update_themes' ) ) { + $updates = get_site_transient( 'update_themes' ); + $updates = $updates->response; + } + + foreach( $themes as $slug => $theme ) { + $parent = false; + if ( $theme->parent() ) { + $parent = $theme->parent()->display( 'Name' ); + } + + $encoded_slug = urlencode( $slug ); + + $prepared_themes[] = array( + 'id' => $slug, + 'name' => $theme->display( 'Name' ), + 'screenshot' => array( $theme->get_screenshot() ), // @todo multiple + 'description' => $theme->display( 'Description' ), + 'author' => $theme->get( 'Author' ), + 'authorURI' => $theme->get( 'AuthorURI' ), + 'version' => $theme->get( 'Version' ), + 'tags' => $theme->get( 'Tags' ), + 'parent' => $parent, + 'active' => $slug === $current_theme, + 'hasUpdate' => isset( $updates[ $slug ] ), + 'update' => 'New version available', // @todo complete this + 'actions' => array( + 'activate' => wp_nonce_url( 'themes.php?action=activate&stylesheet=' . $encoded_slug, 'switch-theme_' . $slug ), + 'customize'=> admin_url( 'customize.php?theme=' . $encoded_slug ), + 'delete' => wp_nonce_url( 'themes.php?action=delete&stylesheet=' . $encoded_slug, 'delete-theme_' . $slug ), + ), + ); + } + // var_dump( $prepared_themes ); + + return $prepared_themes; +} \ No newline at end of file diff --git a/wp-admin/js/theme-install.js b/wp-admin/js/theme-install.js new file mode 100644 index 0000000000..0899ab72f0 --- /dev/null +++ b/wp-admin/js/theme-install.js @@ -0,0 +1,279 @@ +/** + * Theme Browsing + * + * Controls visibility of theme details on manage and install themes pages. + */ +jQuery( function($) { + $('#availablethemes').on( 'click', '.theme-detail', function (event) { + var theme = $(this).closest('.available-theme'), + details = theme.find('.themedetaildiv'); + + if ( ! details.length ) { + details = theme.find('.install-theme-info .theme-details'); + details = details.clone().addClass('themedetaildiv').appendTo( theme ).hide(); + } + + details.toggle(); + event.preventDefault(); + }); +}); + +/** + * Theme Browser Thickbox + * + * Aligns theme browser thickbox. + */ +var tb_position; +jQuery(document).ready( function($) { + tb_position = function() { + var tbWindow = $('#TB_window'), width = $(window).width(), H = $(window).height(), W = ( 1040 < width ) ? 1040 : width, adminbar_height = 0; + + if ( $('body.admin-bar').length ) + adminbar_height = 28; + + if ( tbWindow.size() ) { + tbWindow.width( W - 50 ).height( H - 45 - adminbar_height ); + $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height ); + tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'}); + if ( typeof document.body.style.maxWidth != 'undefined' ) + tbWindow.css({'top': 20 + adminbar_height + 'px','margin-top':'0'}); + }; + }; + + $(window).resize(function(){ tb_position(); }); +}); + +/** + * Theme Install + * + * Displays theme previews on theme install pages. + */ +jQuery( function($) { + if( ! window.postMessage ) + return; + + var preview = $('#theme-installer'), + info = preview.find('.install-theme-info'), + panel = preview.find('.wp-full-overlay-main'), + body = $( document.body ); + + preview.on( 'click', '.close-full-overlay', function( event ) { + preview.fadeOut( 200, function() { + panel.empty(); + body.removeClass('theme-installer-active full-overlay-active'); + }); + event.preventDefault(); + }); + + preview.on( 'click', '.collapse-sidebar', function( event ) { + preview.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); + event.preventDefault(); + }); + + $('#availablethemes').on( 'click', '.install-theme-preview', function( event ) { + var src; + + info.html( $(this).closest('.installable-theme').find('.install-theme-info').html() ); + src = info.find( '.theme-preview-url' ).val(); + panel.html( ''); + preview.fadeIn( 200, function() { + body.addClass('theme-installer-active full-overlay-active'); + }); + event.preventDefault(); + }); +}); + +var ThemeViewer; + +(function($){ + ThemeViewer = function( args ) { + + function init() { + $( '#filter-click, #mini-filter-click' ).unbind( 'click' ).click( function() { + $( '#filter-click' ).toggleClass( 'current' ); + $( '#filter-box' ).slideToggle(); + $( '#current-theme' ).slideToggle( 300 ); + return false; + }); + + $( '#filter-box :checkbox' ).unbind( 'click' ).click( function() { + var count = $( '#filter-box :checked' ).length, + text = $( '#filter-click' ).text(); + + if ( text.indexOf( '(' ) != -1 ) + text = text.substr( 0, text.indexOf( '(' ) ); + + if ( count == 0 ) + $( '#filter-click' ).text( text ); + else + $( '#filter-click' ).text( text + ' (' + count + ')' ); + }); + + /* $('#filter-box :submit').unbind( 'click' ).click(function() { + var features = []; + $('#filter-box :checked').each(function() { + features.push($(this).val()); + }); + + listTable.update_rows({'features': features}, true, function() { + $( '#filter-click' ).toggleClass( 'current' ); + $( '#filter-box' ).slideToggle(); + $( '#current-theme' ).slideToggle( 300 ); + }); + + return false; + }); */ + } + + // These are the functions we expose + var api = { + init: init + }; + + return api; + } +})(jQuery); + +jQuery( document ).ready( function($) { + theme_viewer = new ThemeViewer(); + theme_viewer.init(); +}); + + +/** + * Class that provides infinite scroll for Themes admin screens + * + * @since 3.4 + * + * @uses ajaxurl + * @uses list_args + * @uses theme_list_args + * @uses $('#_ajax_fetch_list_nonce').val() +* */ +var ThemeScroller; +(function($){ + ThemeScroller = { + querying: false, + scrollPollingDelay: 500, + failedRetryDelay: 4000, + outListBottomThreshold: 300, + + /** + * Initializer + * + * @since 3.4 + * @access private + */ + init: function() { + var self = this; + + // Get out early if we don't have the required arguments. + if ( typeof ajaxurl === 'undefined' || + typeof list_args === 'undefined' || + typeof theme_list_args === 'undefined' ) { + $('.pagination-links').show(); + return; + } + + // Handle inputs + this.nonce = $('#_ajax_fetch_list_nonce').val(); + this.nextPage = ( theme_list_args.paged + 1 ); + + // Cache jQuery selectors + this.$outList = $('#availablethemes'); + this.$spinner = $('div.tablenav.bottom').children( '.spinner' ); + this.$window = $(window); + this.$document = $(document); + + /** + * If there are more pages to query, then start polling to track + * when user hits the bottom of the current page + */ + if ( theme_list_args.total_pages >= this.nextPage ) + this.pollInterval = + setInterval( function() { + return self.poll(); + }, this.scrollPollingDelay ); + }, + + /** + * Checks to see if user has scrolled to bottom of page. + * If so, requests another page of content from self.ajax(). + * + * @since 3.4 + * @access private + */ + poll: function() { + var bottom = this.$document.scrollTop() + this.$window.innerHeight(); + + if ( this.querying || + ( bottom < this.$outList.height() - this.outListBottomThreshold ) ) + return; + + this.ajax(); + }, + + /** + * Applies results passed from this.ajax() to $outList + * + * @since 3.4 + * @access private + * + * @param results Array with results from this.ajax() query. + */ + process: function( results ) { + if ( results === undefined ) { + clearInterval( this.pollInterval ); + return; + } + + if ( this.nextPage > theme_list_args.total_pages ) + clearInterval( this.pollInterval ); + + if ( this.nextPage <= ( theme_list_args.total_pages + 1 ) ) + this.$outList.append( results.rows ); + }, + + /** + * Queries next page of themes + * + * @since 3.4 + * @access private + */ + ajax: function() { + var self = this; + + this.querying = true; + + var query = { + action: 'fetch-list', + paged: this.nextPage, + s: theme_list_args.search, + tab: theme_list_args.tab, + type: theme_list_args.type, + _ajax_fetch_list_nonce: this.nonce, + 'features[]': theme_list_args.features, + 'list_args': list_args + }; + + this.$spinner.show(); + $.getJSON( ajaxurl, query ) + .done( function( response ) { + self.nextPage++; + self.process( response ); + self.$spinner.hide(); + self.querying = false; + }) + .fail( function() { + self.$spinner.hide(); + self.querying = false; + setTimeout( function() { self.ajax(); }, self.failedRetryDelay ); + }); + } + } + + $(document).ready( function($) { + ThemeScroller.init(); + }); + +})(jQuery); diff --git a/wp-admin/js/theme.js b/wp-admin/js/theme.js index 0899ab72f0..0d4543dd86 100644 --- a/wp-admin/js/theme.js +++ b/wp-admin/js/theme.js @@ -1,279 +1,718 @@ -/** - * Theme Browsing - * - * Controls visibility of theme details on manage and install themes pages. - */ -jQuery( function($) { - $('#availablethemes').on( 'click', '.theme-detail', function (event) { - var theme = $(this).closest('.available-theme'), - details = theme.find('.themedetaildiv'); +/* global _wpThemeSettings, confirm */ +window.wp = window.wp || {}; - if ( ! details.length ) { - details = theme.find('.install-theme-info .theme-details'); - details = details.clone().addClass('themedetaildiv').appendTo( theme ).hide(); - } +( function($) { - details.toggle(); - event.preventDefault(); - }); -}); +// Set up our namespace... +var themes = wp.themes = wp.themes || {}; -/** - * Theme Browser Thickbox - * - * Aligns theme browser thickbox. - */ -var tb_position; -jQuery(document).ready( function($) { - tb_position = function() { - var tbWindow = $('#TB_window'), width = $(window).width(), H = $(window).height(), W = ( 1040 < width ) ? 1040 : width, adminbar_height = 0; +// Store the theme data and settings for organized and quick access +// themes.data.settings, themes.data.themes, themes.data.i18n +themes.data = _wpThemeSettings; - if ( $('body.admin-bar').length ) - adminbar_height = 28; +// Setup app structure +_.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template }); - if ( tbWindow.size() ) { - tbWindow.width( W - 50 ).height( H - 45 - adminbar_height ); - $('#TB_iframeContent').width( W - 50 ).height( H - 75 - adminbar_height ); - tbWindow.css({'margin-left': '-' + parseInt( ( ( W - 50 ) / 2 ), 10 ) + 'px'}); - if ( typeof document.body.style.maxWidth != 'undefined' ) - tbWindow.css({'top': 20 + adminbar_height + 'px','margin-top':'0'}); - }; - }; +themes.model = Backbone.Model.extend({}); - $(window).resize(function(){ tb_position(); }); -}); +// Main view controller for themes.php +// Unifies and renders all available views +// +// Hooks to #appearance and organizes the views to be rendered +themes.view.Appearance = wp.Backbone.View.extend({ -/** - * Theme Install - * - * Displays theme previews on theme install pages. - */ -jQuery( function($) { - if( ! window.postMessage ) - return; + // Set DOM container + // {#appearance} by default + el: themes.data.settings.container, - var preview = $('#theme-installer'), - info = preview.find('.install-theme-info'), - panel = preview.find('.wp-full-overlay-main'), - body = $( document.body ); + window: $( window ), + // Pagination instance + page: 0, - preview.on( 'click', '.close-full-overlay', function( event ) { - preview.fadeOut( 200, function() { - panel.empty(); - body.removeClass('theme-installer-active full-overlay-active'); + events: { + 'click .themes-bulk-edit': 'editMode', + 'click .theme .delete-theme': 'deleteTheme' + }, + + // Sets up a throttler for binding to 'scroll' + initialize: function() { + var self = this; + + // Keep a boolean check so that we don't run + // too much code on every event trigger + this.window.bind( 'scroll.themes', function() { + this.throttle = true; }); - event.preventDefault(); - }); - preview.on( 'click', '.collapse-sidebar', function( event ) { - preview.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); - event.preventDefault(); - }); + setInterval( function() { + if ( this.throttle ) { + // Once the case is the case, the action occurs and the fact is no more + this.throttle = false; + self.scroller(); + } + }, 300 ); + }, - $('#availablethemes').on( 'click', '.install-theme-preview', function( event ) { - var src; - - info.html( $(this).closest('.installable-theme').find('.install-theme-info').html() ); - src = info.find( '.theme-preview-url' ).val(); - panel.html( ''); - preview.fadeIn( 200, function() { - body.addClass('theme-installer-active full-overlay-active'); + // Main render control + render: function() { + // Setup the main theme view + // with the current theme collection + this.view = new themes.view.Themes({ + collection: this.collection, + parent: this }); - event.preventDefault(); - }); -}); + // Render and append + this.view.render(); + this.$el.append( this.view.el ); -var ThemeViewer; + // Search form + this.search(); + }, -(function($){ - ThemeViewer = function( args ) { + // Search input and view + // for current theme collection + search: function() { + var view, + self = this; - function init() { - $( '#filter-click, #mini-filter-click' ).unbind( 'click' ).click( function() { - $( '#filter-click' ).toggleClass( 'current' ); - $( '#filter-box' ).slideToggle(); - $( '#current-theme' ).slideToggle( 300 ); - return false; - }); + view = new themes.view.Search({ collection: self.collection }); - $( '#filter-box :checkbox' ).unbind( 'click' ).click( function() { - var count = $( '#filter-box :checked' ).length, - text = $( '#filter-click' ).text(); + // Render and append after screen title + view.render(); + self.$el.find( '> h2' ).after( view.el ); + }, - if ( text.indexOf( '(' ) != -1 ) - text = text.substr( 0, text.indexOf( '(' ) ); + // Checks when the user gets close to the bottom + // of the mage and triggers a theme:scroll event + scroller: function() { + var self = this, + bottom, threshold; - if ( count == 0 ) - $( '#filter-click' ).text( text ); - else - $( '#filter-click' ).text( text + ' (' + count + ')' ); - }); + 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 ); - /* $('#filter-box :submit').unbind( 'click' ).click(function() { - var features = []; - $('#filter-box :checked').each(function() { - features.push($(this).val()); - }); - - listTable.update_rows({'features': features}, true, function() { - $( '#filter-click' ).toggleClass( 'current' ); - $( '#filter-box' ).slideToggle(); - $( '#current-theme' ).slideToggle( 300 ); - }); - - return false; - }); */ + if ( bottom > threshold ) { + this.trigger( 'theme:scroll' ); } + }, - // These are the functions we expose - var api = { - init: init - }; + // Enters edit mode that allows easy access to deleting themes + editMode: function() { + $( 'body' ).toggleClass( 'edit-mode' ); + this.$el.find( '.themes-bulk-edit' ).toggleClass( 'mp6-text-highlight' ); + }, - return api; + deleteTheme: function() { + return confirm( themes.data.settings.confirmDelete ); } -})(jQuery); - -jQuery( document ).ready( function($) { - theme_viewer = new ThemeViewer(); - theme_viewer.init(); }); +// Set up the Collection for our theme data +// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ... +themes.Collection = Backbone.Collection.extend({ -/** - * Class that provides infinite scroll for Themes admin screens - * - * @since 3.4 - * - * @uses ajaxurl - * @uses list_args - * @uses theme_list_args - * @uses $('#_ajax_fetch_list_nonce').val() -* */ -var ThemeScroller; -(function($){ - ThemeScroller = { - querying: false, - scrollPollingDelay: 500, - failedRetryDelay: 4000, - outListBottomThreshold: 300, + model: themes.model, - /** - * Initializer - * - * @since 3.4 - * @access private - */ - init: function() { - var self = this; + // Search terms + terms: '', - // Get out early if we don't have the required arguments. - if ( typeof ajaxurl === 'undefined' || - typeof list_args === 'undefined' || - typeof theme_list_args === 'undefined' ) { - $('.pagination-links').show(); - return; + // Controls searching on the current theme collection + // and triggers an update event + doSearch: function( value ) { + + // 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 themes + // Useful for resetting the views when you clean the input + if ( this.terms === '' ) { + this.reset( themes.data.themes ); + } + + // Trigger an 'update' event + this.trigger( 'update' ); + }, + // Performs a search within the collection + // @uses RegExp + search: function( term ) { + var self = this, + match, results, haystack; + + // Start with a full collection + self.reset( themes.data.themes ); + + // The RegExp object to match + // + // Consider spaces as word delimiters and match the whole string + // so matching terms can be combined + term = term.replace( ' ', ')(?=.*' ); + match = new RegExp( '^(?=.*' + term + ').+', 'i' ); + + // Find results + // _.filter and .test + results = self.filter( function( data ) { + haystack = _.union( data.get( 'name' ), data.get( 'author' ), data.get( 'tags' ) ); + + if ( match.test( data.get( 'author' ) ) ) { + data.set( 'displayAuthor', true ); } - // Handle inputs - this.nonce = $('#_ajax_fetch_list_nonce').val(); - this.nextPage = ( theme_list_args.paged + 1 ); + return match.test( haystack ); + }); - // Cache jQuery selectors - this.$outList = $('#availablethemes'); - this.$spinner = $('div.tablenav.bottom').children( '.spinner' ); - this.$window = $(window); - this.$document = $(document); + self.reset( results ); + }, - /** - * If there are more pages to query, then start polling to track - * when user hits the bottom of the current page - */ - if ( theme_list_args.total_pages >= this.nextPage ) - this.pollInterval = - setInterval( function() { - return self.poll(); - }, this.scrollPollingDelay ); - }, + // Paginates the collection with a helper method + // that slices the collection + paginate: function( instance ) { + var collection = this; + instance = instance || 0; - /** - * Checks to see if user has scrolled to bottom of page. - * If so, requests another page of content from self.ajax(). - * - * @since 3.4 - * @access private - */ - poll: function() { - var bottom = this.$document.scrollTop() + this.$window.innerHeight(); + // Themes per instance are set at 15 + collection = _( collection.rest( 15 * instance ) ); + collection = _( collection.first( 15 ) ); - if ( this.querying || - ( bottom < this.$outList.height() - this.outListBottomThreshold ) ) - return; + return collection; + } +}); - this.ajax(); - }, +// This is the view that controls each theme item +// that will be displayed on the screen +themes.view.Theme = wp.Backbone.View.extend({ - /** - * Applies results passed from this.ajax() to $outList - * - * @since 3.4 - * @access private - * - * @param results Array with results from this.ajax() query. - */ - process: function( results ) { - if ( results === undefined ) { - clearInterval( this.pollInterval ); - return; + // Wrap theme data on a div.theme element + className: 'theme', + + // Reflects which theme view we have + // 'grid' (default) or 'detail' + state: 'grid', + + // The HTML template for each element to be rendered + html: themes.template( 'theme' ), + + events: { + 'click': 'expand' + }, + + render: function() { + var data = this.model.toJSON(); + // Render themes using the html template + this.$el.html( this.html( data ) ); + // Renders active theme styles + this.activeTheme(); + + if ( this.model.get( 'displayAuthor' ) ) { + this.$el.addClass( 'display-author' ); + } + }, + + // Adds a class to the currently active theme + // and to the overlay in detailed view mode + activeTheme: function() { + if ( this.model.get( 'active' ) ) { + this.$el.addClass( 'active' ); + this.$el.find( '.theme-name' ).addClass( 'mp6-primary' ); + $( '#theme-overlay' ).addClass( 'active' ); + } + }, + + // Single theme overlay screen + // It's shown when clicking a theme + expand: function( event ) { + var self = this; + + event = event || window.event; + + // Prevent the modal from showing when the user clicks + // one of the direct action buttons + if ( $( event.target ).is( '.theme-actions a, .delete-theme' ) ) { + return; + } + + this.trigger( 'theme:expand', self.model.cid ); + } +}); + +// Theme Details view +// Set ups a modal overlay with the expanded theme data +themes.view.Details = wp.Backbone.View.extend({ + + // Wrap theme data on a div.theme element + id: 'theme-overlay', + + events: { + 'click': 'collapse', + 'click .delete-theme': 'deleteTheme', + 'click .left': 'previousTheme', + 'click .right': 'nextTheme' + }, + + // The HTML template for the theme overlay + html: themes.template( 'theme-single' ), + + render: function() { + var data = this.model.toJSON(); + + this.$el.html( this.html( data ) ); + // Renders active theme styles + this.activeTheme(); + // Set up navigation events + this.navigation(); + }, + + // Adds a class to the currently active theme + // and to the overlay in detailed view mode + activeTheme: function() { + // Check the model has the active property + if ( this.model.get( 'active' ) ) { + this.$el.addClass( 'active' ); + } else { + $( '#theme-overlay' ).removeClass( 'active' ); + } + }, + + // Single theme overlay screen + // It's shown when clicking a theme + collapse: function( event ) { + var self = this, + scroll; + + event = event || window.event; + + // Detect if the click is inside the overlay + // and don't close it unless the target was + // the div.back button + if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( 'div.back' ) || event.keyCode === 27 ) { + + // Add a temporary closing class while overlay fades out + $( 'body' ).addClass( 'closing-overlay' ); + + // With a quick fade out animation + this.$el.fadeOut( 130, function() { + // Clicking outside the modal box closes the overlay + $( 'body' ).removeClass( 'theme-overlay closing-overlay' ); + // Handle event cleanup + self.closeOverlay(); + + // Get scroll position to avoid jumping to the top + scroll = document.body.scrollTop; + + // Clean the url structure + themes.router.navigate( '' ); + + // Restore scroll position + document.body.scrollTop = scroll; + }); + } + }, + + // Handles arrow keys navigation for the overlay + // Triggers theme:next and theme:previous events + navigation: function() { + var self = this; + + $( 'body' ).on( 'keyup', function( event ) { + + // Pressing the right arrow key fires a theme:next event + if ( event.keyCode === 39 ) { + self.trigger( 'theme:next', self.model.cid ); } - if ( this.nextPage > theme_list_args.total_pages ) - clearInterval( this.pollInterval ); + // Pressing the left arrow key fires a theme:previous event + if ( event.keyCode === 37 ) { + self.trigger( 'theme:previous', self.model.cid ); + } - if ( this.nextPage <= ( theme_list_args.total_pages + 1 ) ) - this.$outList.append( results.rows ); - }, + // Pressing the escape key closes the theme details panel + if ( event.keyCode === 27 ) { + self.collapse(); + } + }); + }, - /** - * Queries next page of themes - * - * @since 3.4 - * @access private - */ - ajax: function() { - var self = this; + // Performs the actions to effectively close + // the theme details overlay + closeOverlay: function() { + this.remove(); + this.unbind(); + this.trigger( 'theme:collapse' ); + }, - this.querying = true; + // Setups an image gallery using the theme screenshots supplied by a theme + screenshotGallery: function() { + var screenshots = $( '#theme-screenshots' ), + current, img; - var query = { - action: 'fetch-list', - paged: this.nextPage, - s: theme_list_args.search, - tab: theme_list_args.tab, - type: theme_list_args.type, - _ajax_fetch_list_nonce: this.nonce, - 'features[]': theme_list_args.features, - 'list_args': list_args - }; + screenshots.find( 'div.first' ).next().addClass( 'selected' ); - this.$spinner.show(); - $.getJSON( ajaxurl, query ) - .done( function( response ) { - self.nextPage++; - self.process( response ); - self.$spinner.hide(); - self.querying = false; - }) - .fail( function() { - self.$spinner.hide(); - self.querying = false; - setTimeout( function() { self.ajax(); }, self.failedRetryDelay ); - }); - } + // Clicking on a screenshot thumbnail drops it + // at the top of the stack in a larger size + screenshots.on( 'click', 'div.thumb', function() { + current = $( this ); + img = $( this ).find( 'img' ).clone(); + + current.siblings( '.first' ).html( img ); + current.siblings( '.selected' ).removeClass( 'selected' ); + current.addClass( 'selected' ); + }); + }, + + // Confirmation dialoge for deleting a theme + deleteTheme: function() { + return confirm( themes.data.settings.confirmDelete ); + }, + + nextTheme: function() { + var self = this; + self.trigger( 'theme:next', self.model.cid ); + }, + + previousTheme: function() { + var self = this; + self.trigger( 'theme:previous', self.model.cid ); } +}); - $(document).ready( function($) { - ThemeScroller.init(); - }); +// Controls the rendering of div#themes, +// a wrapper that will hold all the theme elements +themes.view.Themes = wp.Backbone.View.extend({ -})(jQuery); + id: 'themes', + + // Number to keep track of scroll position + // while in theme-overlay mode + index: 0, + + // The theme count element + count: $( '#theme-count' ), + + initialize: function( options ) { + var self = this; + + // Set up parent + this.parent = options.parent; + + // Set current view to [grid] + this.setView( 'grid' ); + + // Move the active theme to the beginning of the collection + self.currentTheme(); + + // When the collection is updated by user input... + this.listenTo( self.collection, 'update', function() { + self.parent.page = 0; + self.currentTheme(); + self.render( this ); + }); + + this.listenTo( this.parent, 'theme:scroll', function() { + self.renderThemes( self.parent.page ); + }); + }, + + // Manages rendering of theme pages + // and keeping theme count in sync + render: function() { + // Clear the DOM, please + this.$el.html( '' ); + + // Generate the themes + // Using page instance + this.renderThemes( this.parent.page ); + + // Display a live theme count for the collection + this.count.text( this.collection.length ); + }, + + // Iterates through each instance of the collection + // and renders each theme module + renderThemes: function( page ) { + var self = this; + + self.instance = self.collection.paginate( page ); + + // If we have no more themes bail + if ( self.instance.length === 0 ) { + return; + } + + // Make sure the add-new stays at the end + if ( page >= 1 ) { + $( '#add-new' ).remove(); + } + + // Loop through the themes and setup each theme view + self.instance.each( function( theme ) { + self.theme = new themes.view.Theme({ + model: theme + }); + + // Render the views... + self.theme.render(); + // and append them to div#themes + self.$el.append( self.theme.el ); + + // Binds to theme:expand to show the modal box + // with the theme details + self.listenTo( self.theme, 'theme:expand', self.expand, self ); + }); + + // 'Add new theme' element shown at the end of the grid + this.$el.append( '
' ); + + this.parent.page++; + }, + + // Grabs current theme and puts it at the beginning of the collection + currentTheme: function() { + var self = this, + current; + + current = self.collection.findWhere({ active: true }); + + // Move the active theme 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; + }, + + // Renders the overlay with the ThemeDetails view + // Uses the current model data + expand: function( id ) { + var self = this; + + // Set the current theme model + this.model = self.collection.get( id ); + + // Trigger a route update for the current model + themes.router.navigate( 'theme/' + this.model.id ); + + // Sets this.view to 'detail' + this.setView( 'detail' ); + $( 'body' ).addClass( 'theme-overlay' ); + + // Set up the theme details view + this.overlay = new themes.view.Details({ + model: self.model, + className: 'theme-' + self.model.id + }); + + this.overlay.render(); + this.$el.append( this.overlay.el ); + + this.overlay.screenshotGallery(); + + // Resets counter whenever the overlay is opened + this.index = 0; + + // Bind to theme:next and theme:previous + // triggered by the arrow keys + // + // The index keep track of where we are at + // any given time + this.listenTo( this.overlay, 'theme:next', function() { + // Bump the index number to keep track of how far + // we should go for the next theme + self.index++; + // Renders the next theme on the overlay + self.next( [ self.model.cid, self.index ] ); + self.overlay.screenshotGallery(); + + }) + .listenTo( this.overlay, 'theme:previous', function() { + // Decrease the index number to keep track of how far + // we should go for the previous theme + self.index--; + // Renders the previous theme on the overlay + self.previous( [ self.model.cid, self.index ] ); + self.overlay.screenshotGallery(); + }); + + }, + + // This method renders the next theme on the overlay modal + // based on the current position in the collection + // @params [model cid] and [index] + next: function( args ) { + var self = this, + model, nextModel; + + // Get the current theme + model = self.collection.get( args[0] ); + // Get the next one + nextModel = self.collection.at( self.collection.indexOf( model ) + args[1] ); + + // Sanity check which also serves as a boundary test + if ( nextModel !== undefined ) { + + // We have a new theme... + // Clean the overlay + this.overlay.$el.html(''); + + // Create the view + this.nextTheme = new themes.view.Details({ + model: nextModel, + className: 'theme-' + nextModel.id + }); + + // Trigger a route update for the current model + themes.router.navigate( 'theme/' + nextModel.id ); + + // Render and fill this.overlay with the new html + this.nextTheme.render(); + return this.overlay.$el.html( this.nextTheme.el ); + } + + // If we got this far it means we have no other themes + // so move back the counter to keep it sane + self.index--; + }, + + // This method renders the previous theme on the overlay modal + // based on the current position in the collection + // @params [model cid] and [index] + previous: function( args ) { + var self = this, + model, previousModel; + + // Get the current theme + model = self.collection.get( args[0] ); + previousModel = self.collection.at( self.collection.indexOf( model ) + args[1] ); + + if ( previousModel !== undefined ) { + + // We have a new theme... + // Clean the overlay + this.overlay.$el.html( '' ); + + // Create the view + this.previousTheme = new themes.view.Details({ + model: previousModel, + className: 'theme-' + previousModel.id + }); + + // Trigger a route update for the current model + themes.router.navigate( 'theme/' + previousModel.id ); + + // Render and fill this.overlay with the new html + this.previousTheme.render(); + return this.overlay.$el.html( this.previousTheme.el ); + } + + // If we got this far it means we have no other themes + // so move back the counter to keep it sane + self.index++; + } +}); + +// Search input view controller +// renders #search-form +themes.view.Search = wp.Backbone.View.extend({ + + className: 'search-form', + + // 'keyup' triggers search + events: { + 'keyup #theme-search': 'search' + }, + + // Grabs template file + html: themes.template( 'theme-search' ), + + // Render the search form + render: function() { + var self = this; + self.$el.html( self.html ); + }, + + // Runs a search on the theme collection + // bind on 'keyup' event + search: function() { + this.collection.doSearch( $( '#theme-search' ).val() ); + } +}); + +// Sets up the routes events for relevant url queries +// Listens to [theme] and [search] params +themes.routes = Backbone.Router.extend({ + + routes: { + 'search/:query': 'search', + 'theme/:slug': 'theme' + }, + + // Set the search input value based on url + search: function( query ) { + $( '#theme-search' ).val( query ); + } +}); + +// Make routes easily extendable +_.extend( themes.routes, themes.data.settings.extraRoutes ); + +// Execute and setup the application +themes.Run = { + init: function() { + // Initializes the blog's theme library view + // Create a new collection with data + this.themes = new themes.Collection( themes.data.themes ); + + // Set up the view + this.view = new themes.view.Appearance({ + collection: this.themes + }); + + this.render(); + }, + + render: function() { + // Render results + this.view.render(); + + // Calls the routes functionality + this.routes(); + + // Set ups history with pushState and our root + Backbone.history.start({ root: themes.data.settings.root }); + }, + + routes: function() { + var self = this; + // Bind to our global thx object + // so that the object is available to sub-views + themes.router = new themes.routes(); + + // Handles theme details route event + themes.router.on( 'route:theme', function( slug ) { + self.view.view.expand( slug ); + }); + + // Handles search route event + themes.router.on( 'route:search', function( query ) { + self.themes.doSearch( query ); + }); + } +}; + +// Ready... +jQuery( document ).ready( + + // Bring on the themes + _.bind( themes.Run.init, themes.Run ) + +); + +})( jQuery ); diff --git a/wp-admin/themes.php b/wp-admin/themes.php index edb0cad96f..41bd85ac1e 100644 --- a/wp-admin/themes.php +++ b/wp-admin/themes.php @@ -12,10 +12,6 @@ require_once( dirname( __FILE__ ) . '/admin.php' ); if ( !current_user_can('switch_themes') && !current_user_can('edit_theme_options') ) wp_die( __( 'Cheatin’ uh?' ) ); -$wp_list_table = _get_list_table('WP_Themes_List_Table'); - -$_SERVER['REQUEST_URI'] = remove_query_arg( array( 's', 'features', '_ajax_fetch_list_nonce', '_wp_http_referer', 'paged' ), $_SERVER['REQUEST_URI'] ); - if ( current_user_can( 'switch_themes' ) && isset($_GET['action'] ) ) { if ( 'activate' == $_GET['action'] ) { check_admin_referer('switch-theme_' . $_GET['stylesheet']); @@ -36,8 +32,6 @@ if ( current_user_can( 'switch_themes' ) && isset($_GET['action'] ) ) { } } -$wp_list_table->prepare_items(); - $title = __('Manage Themes'); $parent_file = 'themes.php'; @@ -66,8 +60,6 @@ if ( current_user_can( 'install_themes' ) ) { ) ); } -add_thickbox(); - endif; // switch_themes if ( current_user_can( 'edit_theme_options' ) ) { @@ -90,21 +82,42 @@ get_current_screen()->set_help_sidebar( '' . __('Support Forums') . '
' ); +if ( current_user_can( 'switch_themes' ) ) { + $themes = wp_prepare_themes_for_js(); +} else { + $themes = wp_prepare_themes_for_js( array( wp_get_theme() ) ); +} + +wp_localize_script( 'theme', '_wpThemeSettings', array( + 'themes' => $themes, + 'settings' => array( + 'install_uri' => admin_url( 'theme-install.php' ), + 'customizeURI' => ( current_user_can( 'edit_theme_options' ) ) ? wp_customize_url() : null, + 'confirmDelete' => __( "Are you sure you want to delete this theme?\n\nClick 'Cancel' to go back, 'OK' to confirm the delete." ), + 'root' => '/wp-admin/themes.php', + 'container' => '#appearance', + 'extraRoutes' => '', + ), + 'i18n' => array( + 'add_new' => __( 'Add New Theme' ), + ), +) ); + +wp_enqueue_style( 'theme' ); wp_enqueue_script( 'theme' ); wp_enqueue_script( 'customize-loader' ); require_once( ABSPATH . 'wp-admin/admin-header.php' ); ?> -display('Description'); ?>
- parent() ) { - printf( '' . __( 'This child theme requires its parent theme, %2$s.' ) . '
', - __( 'http://codex.wordpress.org/Child_Themes' ), - $ct->parent()->display( 'Name' ) ); - } ?> - -