diff options
Diffstat (limited to 'srcs/wordpress/wp-admin/js/theme.js')
| -rw-r--r-- | srcs/wordpress/wp-admin/js/theme.js | 2070 |
1 files changed, 2070 insertions, 0 deletions
diff --git a/srcs/wordpress/wp-admin/js/theme.js b/srcs/wordpress/wp-admin/js/theme.js new file mode 100644 index 0000000..74a90cc --- /dev/null +++ b/srcs/wordpress/wp-admin/js/theme.js @@ -0,0 +1,2070 @@ +/** + * @output wp-admin/js/theme.js + */ + +/* global _wpThemeSettings, confirm, tb_position */ +window.wp = window.wp || {}; + +( function($) { + +// Set up our namespace... +var themes, l10n; +themes = wp.themes = wp.themes || {}; + +// Store the theme data and settings for organized and quick access +// themes.data.settings, themes.data.themes, themes.data.l10n +themes.data = _wpThemeSettings; +l10n = themes.data.l10n; + +// Shortcut for isInstall check +themes.isInstall = !! themes.data.settings.isInstall; + +// Setup app structure +_.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template }); + +themes.Model = Backbone.Model.extend({ + // Adds attributes to the default data coming through the .org themes api + // Map `id` to `slug` for shared code + initialize: function() { + var description; + + // If theme is already installed, set an attribute. + if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) { + this.set({ installed: true }); + } + + // 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 themes.php +// Unifies and renders all available views +themes.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 : themes.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 theme view + // with the current theme collection + this.view = new themes.view.Themes({ + 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 theme collection + search: function() { + var view, + self = this; + + // Don't render the search if there is only one theme + if ( themes.data.themes.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 mage and triggers a theme: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( 'theme:scroll' ); + } + } +}); + +// Set up the Collection for our theme data +// @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ... +themes.Collection = Backbone.Collection.extend({ + + model: themes.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 themes + // Useful for resetting the views when you clean the input + if ( this.terms === '' ) { + this.reset( themes.data.themes ); + $( 'body' ).removeClass( 'no-results' ); + } + + // Trigger a 'themes:update' event + this.trigger( 'themes: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( themes.data.themes, { silent: true } ); + + // Trim the term + term = term.trim(); + + // 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; + + // Themes per instance are set at 20 + collection = _( collection.rest( 20 * instance ) ); + collection = _( collection.first( 20 ) ); + + return collection; + }, + + count: false, + + // Handles requests for more themes + // 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 `theme: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.themes ) { + self.reset( data.themes ); + count = data.info.results; + // Store the results and the query request + queries.push( { themes: data.themes, request: request, total: count } ); + } + + // Trigger a collection refresh event + // and a `query:success` event with a `count` argument. + self.trigger( 'themes:update' ); + self.trigger( 'query:success', count ); + + if ( data.themes && data.themes.length === 0 ) { + self.trigger( 'query:empty' ); + } + + }).fail( function() { + self.trigger( 'query:fail' ); + }); + } else { + // If it's a paginated request we need to fetch more themes... + if ( isPaginated ) { + return this.apiCall( request, isPaginated ).done( function( data ) { + // Add the new themes to the current collection + // @todo update counter + self.add( data.themes ); + self.trigger( 'query:success' ); + + // We are done loading themes for now. + self.loadingThemes = false; + + }).fail( function() { + self.trigger( 'query:fail' ); + }); + } + + if ( query.themes.length === 0 ) { + self.trigger( 'query:empty' ); + } else { + $( 'body' ).removeClass( 'no-results' ); + } + + // Only trigger an update event since we already have the themes + // on our cached object + if ( _.isNumber( query.total ) ) { + this.count = query.total; + } + + this.reset( query.themes ); + if ( ! query.total ) { + this.count = this.length; + } + + this.trigger( 'themes: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.wordpress.org/themes + apiCall: function( request, paginated ) { + return wp.ajax.send( 'query-themes', { + 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 themes. + loadingThemes: false +}); + +// This is the view that controls each theme item +// that will be displayed on the screen +themes.view.Theme = wp.Backbone.View.extend({ + + // 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': themes.isInstall ? 'preview': 'expand', + 'keydown': themes.isInstall ? 'preview': 'expand', + 'touchend': themes.isInstall ? 'preview': 'expand', + 'keyup': 'addFocus', + 'touchmove': 'preventExpand', + 'click .theme-install': 'installTheme', + 'click .update-message': 'updateTheme' + }, + + touchDrag: false, + + initialize: function() { + this.model.on( 'change', this.render, this ); + }, + + render: function() { + var data = this.model.toJSON(); + + // Render themes 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 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' ); + } + }, + + // Add class of focus to the theme we are focused on. + addFocus: function() { + var $themeToFocus = ( $( ':focus' ).hasClass( 'theme' ) ) ? $( ':focus' ) : $(':focus').parents('.theme'); + + $('.theme.focus').removeClass('focus'); + $themeToFocus.addClass('focus'); + }, + + // Single theme overlay screen + // It's shown when clicking a theme + expand: function( event ) { + var self = this; + + event = event || window.event; + + // 'enter' and 'space' keys expand the details view when a theme is :focused + if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) { + return; + } + + // Bail if the user scrolled on a touch device + if ( this.touchDrag === true ) { + return this.touchDrag = false; + } + + // Prevent the modal from showing when the user clicks + // one of the direct action buttons + if ( $( event.target ).is( '.theme-actions a' ) ) { + return; + } + + // Prevent the modal from showing when the user clicks one of the direct action buttons. + if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) { + return; + } + + // Set focused theme to current element + themes.focusedTheme = this.$el; + + this.trigger( 'theme:expand', self.model.cid ); + }, + + 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 theme. + if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) { + return; + } + + // 'enter' and 'space' keys expand the details view when a theme 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 theme. + themes.focusedTheme = this.$el; + + // Construct a new Preview view. + themes.preview = preview = new themes.view.Preview({ + model: this.model + }); + + // Render the view and append it. + preview.render(); + this.setNavButtonsState(); + + // Hide previous/next navigation if there is only one theme + 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 `theme:next` and `theme:previous` events. + this.listenTo( preview, 'theme:next', function() { + + // Keep local track of current theme 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 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 ) ) { + self.options.parent.parent.trigger( 'theme:end' ); + return self.current = current; + } + + preview.model = self.current; + + // Render and append. + preview.render(); + this.setNavButtonsState(); + $( '.next-theme' ).focus(); + }) + .listenTo( preview, 'theme:previous', function() { + + // Keep track of current theme 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 theme installer preview + setNavButtonsState: function() { + var $themeInstaller = $( '.theme-install-overlay' ), + current = _.isUndefined( this.current ) ? this.model : this.current, + previousThemeButton = $themeInstaller.find( '.previous-theme' ), + nextThemeButton = $themeInstaller.find( '.next-theme' ); + + // Disable previous at the zero position + if ( 0 === this.model.collection.indexOf( current ) ) { + previousThemeButton + .addClass( 'disabled' ) + .prop( 'disabled', true ); + + nextThemeButton.focus(); + } + + // Disable next if the next model is undefined + if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) { + nextThemeButton + .addClass( 'disabled' ) + .prop( 'disabled', true ); + + previousThemeButton.focus(); + } + }, + + installTheme: function( event ) { + var _this = this; + + event.preventDefault(); + + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).on( 'wp-theme-install-success', function( event, response ) { + if ( _this.model.get( 'id' ) === response.slug ) { + _this.model.set( { 'installed': true } ); + } + } ); + + wp.updates.installTheme( { + slug: $( event.target ).data( 'slug' ) + } ); + }, + + updateTheme: function( event ) { + var _this = this; + + if ( ! this.model.get( 'hasPackage' ) ) { + return; + } + + event.preventDefault(); + + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).on( 'wp-theme-update-success', function( event, response ) { + _this.model.off( 'change', _this.render, _this ); + if ( _this.model.get( 'id' ) === response.slug ) { + _this.model.set( { + hasUpdate: false, + version: response.newVersion + } ); + } + _this.model.on( 'change', _this.render, _this ); + } ); + + wp.updates.updateTheme( { + slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' ) + } ); + } +}); + +// 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 + className: 'theme-overlay', + + events: { + 'click': 'collapse', + 'click .delete-theme': 'deleteTheme', + 'click .left': 'previousTheme', + 'click .right': 'nextTheme', + 'click #update-theme': 'updateTheme' + }, + + // 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(); + // Checks screenshot size + this.screenshotCheck( this.$el ); + // Contain "tabbing" inside the overlay + this.containFocus( this.$el ); + }, + + // 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 + this.$el.toggleClass( 'active', this.model.get( 'active' ) ); + }, + + // Set initial focus and constrain tabbing within the theme browser modal. + containFocus: function( $el ) { + + // Set initial focus on the primary action control. + _.delay( function() { + $( '.theme-overlay' ).focus(); + }, 100 ); + + // Constrain tabbing within the modal. + $el.on( 'keydown.wp-themes', function( event ) { + var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(), + $lastFocusable = $el.find( '.theme-actions a:visible' ).last(); + + // Check for the Tab key. + if ( 9 === event.which ) { + if ( $firstFocusable[0] === event.target && event.shiftKey ) { + $lastFocusable.focus(); + event.preventDefault(); + } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) { + $firstFocusable.focus(); + event.preventDefault(); + } + } + }); + }, + + // Single theme overlay screen + // It's shown when clicking a theme + collapse: function( event ) { + var self = this, + scroll; + + event = event || window.event; + + // Prevent collapsing detailed view when there is only one theme available + if ( themes.data.themes.length === 1 ) { + return; + } + + // 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( '.close' ) || 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( '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( themes.router.baseUrl( '' ) ); + + // Restore scroll position + document.body.scrollTop = scroll; + + // Return focus to the theme div + if ( themes.focusedTheme ) { + themes.focusedTheme.focus(); + } + }); + } + }, + + // Handles .disabled classes for next/previous buttons + navigation: function() { + + // Disable Left/Right when at the start or end of the collection + if ( this.model.cid === this.model.collection.at(0).cid ) { + this.$el.find( '.left' ) + .addClass( 'disabled' ) + .prop( 'disabled', true ); + } + if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) { + this.$el.find( '.right' ) + .addClass( 'disabled' ) + .prop( 'disabled', true ); + } + }, + + // Performs the actions to effectively close + // the theme details overlay + closeOverlay: function() { + $( 'body' ).removeClass( 'modal-open' ); + this.remove(); + this.unbind(); + this.trigger( 'theme:collapse' ); + }, + + updateTheme: function( event ) { + var _this = this; + event.preventDefault(); + + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).on( 'wp-theme-update-success', function( event, response ) { + if ( _this.model.get( 'id' ) === response.slug ) { + _this.model.set( { + hasUpdate: false, + version: response.newVersion + } ); + } + _this.render(); + } ); + + wp.updates.updateTheme( { + slug: $( event.target ).data( 'slug' ) + } ); + }, + + deleteTheme: function( event ) { + var _this = this, + _collection = _this.model.collection, + _themes = themes; + event.preventDefault(); + + // Confirmation dialog for deleting a theme. + if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).one( 'wp-theme-delete-success', function( event, response ) { + _this.$el.find( '.close' ).trigger( 'click' ); + $( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() { + $( this ).remove(); + _themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) ); + + $( '.wp-filter-search' ).val( '' ); + _collection.doSearch( '' ); + _collection.remove( _this.model ); + _collection.trigger( 'themes:update' ); + } ); + } ); + + wp.updates.deleteTheme( { + slug: this.model.get( 'id' ) + } ); + }, + + nextTheme: function() { + var self = this; + self.trigger( 'theme:next', self.model.cid ); + return false; + }, + + previousTheme: function() { + var self = this; + self.trigger( 'theme:previous', self.model.cid ); + return false; + }, + + // Checks if the theme screenshot is the old 300px width version + // and adds a corresponding class if it's true + screenshotCheck: function( el ) { + var screenshot, image; + + screenshot = el.find( '.screenshot img' ); + image = new Image(); + image.src = screenshot.attr( 'src' ); + + // Width check + if ( image.width && image.width <= 300 ) { + el.addClass( 'small-screenshot' ); + } + } +}); + +// Theme Preview view +// Set ups a modal overlay with the expanded theme data +themes.view.Preview = themes.view.Details.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': 'previousTheme', + 'click .next-theme': 'nextTheme', + 'keyup': 'keyEvent', + 'click .theme-install': 'installTheme' + }, + + // The HTML template for the theme preview + html: themes.template( 'theme-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 ); + } + + themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } ); + + this.$el.fadeIn( 200, function() { + $body.addClass( 'theme-installer-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( 'theme-installer-active full-overlay-active' ); + + // Return focus to the theme div + if ( themes.focusedTheme ) { + themes.focusedTheme.focus(); + } + }).removeClass( 'iframe-ready' ); + + // Restore the previous browse tab if available. + if ( themes.router.selectedTab ) { + themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) ); + themes.router.selectedTab = false; + } else { + themes.router.navigate( themes.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 theme + if ( event.keyCode === 39 ) { + _.once( this.nextTheme() ); + } + + // The left arrow key, previous theme + if ( event.keyCode === 37 ) { + this.previousTheme(); + } + }, + + installTheme: function( event ) { + var _this = this, + $target = $( event.target ); + event.preventDefault(); + + if ( $target.hasClass( 'disabled' ) ) { + return; + } + + wp.updates.maybeRequestFilesystemCredentials( event ); + + $( document ).on( 'wp-theme-install-success', function() { + _this.model.set( { 'installed': true } ); + } ); + + wp.updates.installTheme( { + slug: $target.data( 'slug' ) + } ); + } +}); + +// Controls the rendering of div.themes, +// a wrapper that will hold all the theme elements +themes.view.Themes = 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 theme count element + count: $( '.wrap .theme-count' ), + + // The live themes count + liveThemeCount: 0, + + 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, 'themes:update', function() { + self.parent.page = 0; + self.currentTheme(); + self.render( this ); + } ); + + // Update theme 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, 'theme:scroll', function() { + self.renderThemes( self.parent.page ); + }); + + this.listenTo( this.parent, 'theme:close', function() { + if ( self.overlay ) { + self.overlay.closeOverlay(); + } + } ); + + // Bind keyboard events. + $( 'body' ).on( 'keyup', function( event ) { + if ( ! self.overlay ) { + return; + } + + // Bail if the filesystem credentials dialog is shown. + if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) { + return; + } + + // Pressing the right arrow key fires a theme:next event + if ( event.keyCode === 39 ) { + self.overlay.nextTheme(); + } + + // Pressing the left arrow key fires a theme:previous event + if ( event.keyCode === 37 ) { + self.overlay.previousTheme(); + } + + // Pressing the escape key fires a theme:collapse event + if ( event.keyCode === 27 ) { + self.overlay.collapse( event ); + } + }); + }, + + // Manages rendering of theme pages + // and keeping theme count in sync + render: function() { + // Clear the DOM, please + this.$el.empty(); + + // If the user doesn't have switch capabilities + // or there is only one theme in the collection + // render the detailed view of the active theme + if ( themes.data.themes.length === 1 ) { + + // Constructs the view + this.singleTheme = new themes.view.Details({ + model: this.collection.models[0] + }); + + // Render and apply a 'single-theme' class to our container + this.singleTheme.render(); + this.$el.addClass( 'single-theme' ); + this.$el.append( this.singleTheme.el ); + } + + // Generate the themes + // Using page instance + // While checking the collection has items + if ( this.options.collection.size() > 0 ) { + this.renderThemes( this.parent.page ); + } + + // Display a live theme count for the collection + this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length; + this.count.text( this.liveThemeCount ); + + /* + * In the theme installer the themes count is already announced + * because `announceSearchResults` is called on `query:success`. + */ + if ( ! themes.isInstall ) { + this.announceSearchResults( this.liveThemeCount ); + } + }, + + // 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.size() === 0 ) { + // Fire a no-more-themes event. + this.parent.trigger( 'theme:end' ); + return; + } + + // Make sure the add-new stays at the end + if ( ! themes.isInstall && page >= 1 ) { + $( '.add-new-theme' ).remove(); + } + + // Loop through the themes and setup each theme view + self.instance.each( function( theme ) { + self.theme = new themes.view.Theme({ + model: theme, + parent: self + }); + + // 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 + if ( ! themes.isInstall && themes.data.settings.canInstall ) { + this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + |
