# mainwp/6.2.1/assets/js/mainwp-theme.js

MainWP Dashboard: Self-hosted WordPress Management for Agencies, version 6.2.1. 1,753 lines.

- Page: https://pluginprobe.com/plugins/mainwp/6.2.1/code/assets/js/mainwp-theme.js
- Raw: https://pluginprobe.com/plugins/mainwp/6.2.1/raw/assets/js/mainwp-theme.js
- Modified: 2026-09-09T15:30:44+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/mainwp/6.2.1/code/assets/js/mainwp-theme.js#L10-L20`.

```javascript
/* eslint complexity: ["error", 100] */
// current complexity is the only way to achieve desired results, pull request solutions appreciated.

/* global _mainwpThemeSettings, confirm */
globalThis.wp = globalThis.wp || {};

(function ($) {

    // Set up our namespace...
    let 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 = _mainwpThemeSettings;
    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 () {
            let 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 .mainwp-browse-themes',

        window: $(globalThis.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();

            // Render and append
            this.view.render();
            this.$el.empty().append(this.view.el).addClass('rendered');
            this.$el.append('<br class="clear"/>');
        },

        // Defines search element container
        searchContainer: $('#wpbody'), // init container

        // Search input and view
        // for current theme collection
        search: function () {
            // Don't render the search if there is only one theme
            if (themes.data.themes.length === 1) {
                return;
            }

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

            // Render and append after screen title
            view.render();
            this.searchContainer
                .append(view.el)
                .append('<i class="search icon"></i>');
        },

        // Checks when the user gets close to the bottom
        // of the mage and triggers a theme:scroll event
        scroller: function () {
            const bottom = this.window.scrollTop() + this.window.height();
            let threshold = this.$el.offset().top + this.$el.outerHeight(false) - this.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 an 'update' event
            this.trigger('update');
        },

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

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

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

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

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

                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 = 0) {
             // Themes per instance are set at 20
            let collection = _(this.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) {

            let queries = this.queries;
            let query, isPaginated, count;

            // Store current query request args
            this.currentQuery.request = request;

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

            isPaginated = _.has(request, 'page');

            if (!isPaginated) {
                this.currentQuery.page = 1;
            }

            if (!query && !isPaginated) {

                this.apiCall(request)
                    .done((data) => {

                        if (data.themes) {
                            this.reset(data.themes);
                            count = data.info.results;

                            queries.push({
                                themes: data.themes,
                                request: request,
                                total: count
                            });
                        }

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

                        if (data?.themes?.length === 0) {
                            this.trigger('query:empty');
                        }

                    })
                    .fail(() => {
                        this.trigger('query:fail');
                    });

            } else {

                if (isPaginated) {
                    return this.apiCall(request, isPaginated)
                        .done((data) => {

                            this.add(data.themes);
                            this.trigger('query:success');

                            this.loadingThemes = false;

                        })
                        .fail(() => {
                            this.trigger('query:fail');
                        });
                }

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

                if (_.isNumber(query.total)) {
                    this.count = query.total;
                }

                this.reset(query.themes);

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

                this.trigger('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,
                        fields: {
                            description: true,
                            tested: true,
                            requires: true,
                            rating: true,
                            downloaded: true,
                            downloadLink: true,
                            last_updated: true,
                            homepage: true,
                            num_ratings: true
                        }
                    }, request)
                },

                beforeSend: function () {
                    if (!paginated) {
                        // Spin it
                        $('body').addClass('loading-content').removeClass('no-results');
                    }
                }
            }).done(function (response) {
                if (response?.themes) {
                    let favThemes = jQuery('#mainwp-favorites-themes').attr('favorites-themes');
                    if ('' !== favThemes) {
                        try {
                            let decoded_favThemes = JSON.parse(favThemes);
                            response.themes.forEach(function (part, index) {
                                if (decoded_favThemes.hasOwnProperty(this[index].slug)) {
                                    this[index].added_fav = 1;
                                } else {
                                    this[index].added_fav = 0;
                                }
                            }, response.themes); // use second param as this.
                        } catch {
                            console.log('Invalid favorites themes data.');
                        }
                    }
                }
            });
        },

        // 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 card',

        // 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'
        },

        touchDrag: false,

        render: function () {
            let 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'
            });

            // Renders active theme styles
            this.activeTheme();

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

            if (this.model.get('installed')) {
                this.$el.addClass('is-installed');
            }
        },

        // 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 () {
            let $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) {

            event = event || window.event; // NOSONAR - compatible.

            // '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) {
                this.touchDrag = false;
                return this.touchDrag;
            }

            // Prevent the modal from showing when the user clicks
            // one of the direct action buttons
            if ($(event.target).is('.theme-actions a')) {
                return;
            }

            // Set focused theme to current element
            themes.focusedTheme = this.$el;

            this.trigger('theme:expand', this.model.cid);
        },

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

        preview: function (event) {
            let current, preview;

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

            // Allow direct link path to installing a theme.
            if ($(event.target).hasClass('button-primary')) {
                return;
            }

            if ($(event.target).is('.mainwp-theme-lnks input[type="radio"]')) {
                return;
            }
            if (!$(event.target).is('.mainwp-theme-preview')) {
                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; // NOSONAR - compatible.

            // Set focus to current theme.
            themes.focusedTheme = this.$el;

            // Construct a new Preview view.
            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.mainwp-content-wrap').append(preview.el);

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

                // Keep local track of current theme model.
                current = this.model;

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

                // Get next theme model.
                this.current = this.model.collection.at(this.model.collection.indexOf(current) + 1);

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

                preview.model = this.current;

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

                    // Keep track of current theme model.
                    current = this.model;

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

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

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

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

                    preview.model = this.current;

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

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

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

            // Disable previous at the zero position
            if (0 === this.model.collection.indexOf(current)) {
                $themeInstaller.find('.previous-theme').addClass('disabled');
            }

            // Disable next if the next model is undefined
            if (_.isUndefined(this.model.collection.at(this.model.collection.indexOf(current) + 1))) {
                $themeInstaller.find('.next-theme').addClass('disabled');
            }
        }
    });

    // 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'
        },

        // The HTML template for the theme overlay
        html: themes.template('theme-single'),

        render: function () {
            let 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'));
        },

        // Keeps :focus within the theme details elements
        containFocus: function ($el) {
            let $target;

            // Move focus to the primary action
            _.delay(function () {
                $('.theme-wrap a.button-primary:visible').trigger('focus');
            }, 500);

            $el.on('keydown.wp-themes', function (event) {

                // Tab key
                if (event.which === 9) {
                    $target = $(event.target);

                    // Keep focus within the overlay by making the last link on theme actions
                    // switch focus to button.left on tabbing and vice versa
                    if ($target.is('button.left') && event.shiftKey) {
                        $el.find('.theme-actions a:last-child').trigger('focus');
                        event.preventDefault();
                    } else if ($target.is('.theme-actions a:last-child')) {
                        $el.find('button.left').trigger('focus');
                        event.preventDefault();
                    }
                }
            });
        },

        // Single theme overlay screen
        // It's shown when clicking a theme
        collapse: function (event) {
            let scroll;
            event = event || window.event; // NOSONAR - compatible.

            // 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, () => {
                    // Clicking outside the modal box closes the overlay
                    $('body').removeClass('closing-overlay');
                    // Handle event cleanup
                    this.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.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');
            }
            if (this.model.cid === this.model.collection.at(-1).cid) {
                this.$el.find('.right').addClass('disabled');
            }
        },

        // Performs the actions to effectively close
        // the theme details overlay
        closeOverlay: function () {
            $('body').removeClass('modal-open');
            this.remove();
            this.unbind();
            this.trigger('theme:collapse');
        },

        // Confirmation dialog for deleting a theme
        deleteTheme: function () {
            return confirm(themes.data.settings.confirmDelete);
        },

        nextTheme: function () {
            this.trigger('theme:next', this.model.cid);
            return false;
        },

        previousTheme: function () {
            this.trigger('theme:previous', this.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) {
            let 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 .previous-theme': 'previousTheme',
            'click .next-theme': 'nextTheme',
            'keyup': 'keyEvent'
        },

        // The HTML template for the theme preview
        html: themes.template('theme-preview'),

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

            this.$el.html(this.html(data));

            themes.router.navigate(themes.router.baseUrl(themes.router.themePath + this.model.get('id')), { replace: true });

            this.$el.fadeIn(200, function () {
                $('body').addClass('theme-installer-active full-overlay-active');
                $('.close-full-overlay').trigger('focus');
            });
        },

        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.trigger('focus');
                }
            });

            themes.router.navigate(themes.router.baseUrl(''));
            this.trigger('preview:close');
            this.undelegateEvents();
            this.unbind();
            return false;
        },

        collapse: function (event) {
            let $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;
        },

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

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

        className: 'themes ui three cards',
        $overlay: $('div.theme-overlay'),

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

        // The theme count element
        count: $('.wp-core-ui .theme-count'),

        // The live themes count
        liveThemeCount: 0,

        initialize: function (options) {

            // 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
            this.currentTheme();

            // When the collection is updated by user input...
            this.listenTo(this.collection, 'update',  () => {
                this.parent.page = 0;
                this.currentTheme();
                this.render(this);
            });

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

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

            this.listenTo(this.parent, 'theme:scroll', () => {
                this.renderThemes(this.parent.page);
            });

            this.listenTo(this.parent, 'theme:close', () => {
                if (this.overlay) {
                    this.overlay.closeOverlay();
                }
            });

            // Bind keyboard events.
            $('body').on('keyup', (event) => {
                if (!this.overlay) {
                    return;
                }

                // Pressing the right arrow key fires a theme:next event
                if (event.keyCode === 39) {
                    this.overlay.nextTheme();
                }

                // Pressing the left arrow key fires a theme:previous event
                if (event.keyCode === 37) {
                    this.overlay.previousTheme();
                }

                // Pressing the escape key fires a theme:collapse event
                if (event.keyCode === 27) {
                    this.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.length > 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);

            this.announceSearchResults(this.liveThemeCount);
        },

        // Iterates through each instance of the collection
        // and renders each theme module
        renderThemes: function (page) {

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

            // If we have no more themes bail
            if (this.instance.length === 0) {
                this.parent.trigger('theme:end');
                return;
            }

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

            // Loop through the themes and setup each theme view
            this.instance.each((theme) => {

                this.theme = new themes.view.Theme({
                    model: theme,
                    parent: this
                });

                // Render the view
                this.theme.render();

                // Append to container
                this.$el.append(this.theme.el);

                // Bind expand event
                this.listenTo(this.theme, 'theme:expand', this.expand);
            });

            // 'Add new theme' element shown at the end of the grid
            if (themes.data.settings.canInstall) {
                this.$el.append(
                    '<div class="theme add-new-theme">' +
                        '<a href="' + themes.data.settings.installURI + '">' +
                            '<div class="theme-screenshot"><span></span></div>' +
                            '<h3 class="theme-name">' + l10n.addNew + '</h3>' +
                        '</a>' +
                    '</div>'
                );
            }

            this.parent.page++;
        },

        // Grabs current theme and puts it at the beginning of the collection
        currentTheme: function () {
            const current = this.collection.findWhere({ active: true });

            // Move the active theme to the beginning of the collection
            if (current) {
                this.collection.remove(current);
                this.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) {
            // Set the current theme model
            this.model = this.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: this.model
            });

            this.overlay.render();
            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', () => {
                // Renders the next theme on the overlay
                this.next([this.model.cid]);
            })
                .listenTo(this.overlay, 'theme:previous', () => {
                    // Renders the previous theme on the overlay
                    this.previous([this.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) {
            let model, nextModel;

            // Get the current theme
            model = this.collection.get(args[0]);
            // Find the next model within the collection
            nextModel = this.collection.at(this.collection.indexOf(model) + 1);

            // Sanity 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
                this.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) {
            let model, previousModel;

            // Get the current theme
            model = this.collection.get(args[0]);
            // Find the previous model within the collection
            previousModel = this.collection.at(this.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
                this.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 fluid prompt',
        id: 'wp-filter-search-input',
        searching: false,


        attributes: {
            placeholder: 'Search themes...',
            type: 'text',
            '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: _.debounce(function (event) {
            let options = {};

            this.collection.doSearch(event.target.value);

            // 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(''));
            }
        }, 500),

        pushState: function (event) {
            let url = themes.router.baseUrl('');

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

            this.searching = false;
            themes.router.navigate(url);

        }
    });

    // 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);
        },

        themes: function () {
            $('.wp-filter-search').val('');
        },

        navigate: function () {
            if (Backbone.history._hasPushState) {
                Backbone.Router.prototype.navigate.apply(this, arguments);
            }
        }

    });

    // 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();
            this.routes();

            Backbone.history.start({
                root: themes.data.settings.adminUrl,
                pushState: true,
                hashChange: false
            });
        },
        routes: function () {

            // Bind to our global themes object
            themes.router = new themes.Router();

            // Handles theme details route event
            themes.router.on('route:theme', (slug) => {
                this.view.view.expand(slug);
            });

            themes.router.on('route:themes', () => {
                this.themes.doSearch('');
                this.view.trigger('theme:close');
            });

            // Handles search route event
            themes.router.on('route:search', () => {
                $('.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'
        },

        // 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: _.debounce(function (value) {
            let request = {};

            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');
            $('body').removeClass('show-filters filters-applied');

            // 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 + value), { replace: true });
        }, 500)
    });

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

        el: '#wpbody-content .mainwp-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 .filtered-by': 'backToFilters'
        },

        // Initial render method
        render: function () {

            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',  () => {

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

                // Set loadingThemes to true and bump page instance of currentQuery.
                this.collection.loadingThemes = true;
                this.collection.currentQuery.page++;

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

            this.listenTo(this.collection, 'query:success', function () {
                $('body').removeClass('loading-content');
                $('.mainwp-browse-themes').find('div.error').remove();
                $('.card .ui.star.rating').rating(); // for adding to favorites
            });

            this.listenTo(this.collection, 'query:fail', function () {
                $('body').removeClass('loading-content');
                $('.mainwp-browse-themes').find('div.error').remove();
                $('.mainwp-browse-themes').find('div.themes').before('<div class="error"><p>' + l10n.error + '</p></div>');
            });

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

            // Set ups 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('.mainwp-browse-themes').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
            this.collection.query({ browse: section });
        },

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

            event.preventDefault();

            $('body').removeClass('filters-applied show-filters');

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

            this.sort(sort);

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

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

            $('.filter-links li > a, .theme-filter').removeClass(this.activeClass);
            $('[data-sort="' + sort + '"]').addClass(this.activeClass);

            this.browse(sort);
        },

        // Filters and Tags
        onFilter: function (event) {
            let 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);
            $el.addClass(this.activeClass);

            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) {
            let name,
                tags = this.filtersChecked(),
                request = { tag: tags },
                filteringBy = $('.filtered-by .tags');

            if (event) {
                event.preventDefault();
            }

            $('body').addClass('filters-applied');
            $('.filter-links li > a.current').removeClass('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);
        },

        // Get the checked filters
        // @return {array} of tags or false
        filtersChecked: function () {
            let 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 [];
            }

            $('.filter-drawer .apply-filters').find('span').text(tags.length);
            $('.filter-drawer .clear-filters').css('display', 'inline-block');

            return tags;
        },

        activeClass: 'current',

        // Overwrite search container class to append search
        // in new location
        searchContainer: $('#mainwp-search-themes-input-container'),

        uploader: function () {
            $('a.upload').on('click', function (event) {
                event.preventDefault();
                $('.mainwp-bulk-install-showhide-content').hide();
                $('.mainwp-upload-theme').show();
                themes.router.navigate(themes.router.baseUrl('&upload'), { replace: true });
                $('a.mainwp-bulk-install-tabs-header-btn').removeClass('green');
                $(this).addClass('green');
                $('#mainwp_theme_bulk_install_btn').attr('bulk-action', 'upload');
            });
            $('a.browse-themes').on('click', function (event) {
                event.preventDefault();
                $('.mainwp-bulk-install-showhide-content').hide();
                $('#mainwp-search-themes-input-container').show();
                $('#theme-filter').show();
                themes.router.navigate(themes.router.baseUrl(''), { replace: true });
                $('a.mainwp-bulk-install-tabs-header-btn').removeClass('green');
                $(this).addClass('green');
                $('#mainwp_theme_bulk_install_btn').attr('bulk-action', 'install');
            });
        },

        // Toggle the full filters navigation
        moreFilters: function (event) {
            event.preventDefault();

            if ($('body').hasClass('filters-applied')) {
                return this.backToFilters();
            }

            // If the filters section is opened and filters are checked
            // run the relevant query collapsing to filtered-by state
            if ($('body').hasClass('show-filters') && this.filtersChecked()) {
                return this.addFilter();
            }

            this.clearSearch();

            themes.router.navigate(themes.router.baseUrl(''));
            $('body').toggleClass('show-filters');
        },

        // Clears all the checked filters
        // @uses filtersChecked()
        clearFilters: function (event) {
            let items = $('.filter-group').find(':checkbox')
            event.preventDefault();

            _.each(items.filter(':checked'), (item) => {
                $(item).prop('checked', false);
                return this.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: {
            'admin.php?page=ThemesInstall&theme=:slug': 'preview',
            'admin.php?page=ThemesInstall&browse=:sort': 'sort',
            'admin.php?page=ThemesInstall&upload': 'upload',
            'admin.php?page=ThemesInstall&search=:query': 'search',
            'admin.php?page=ThemesInstall': 'sort'
        },

        baseUrl: function (url) {
            return 'admin.php?page=ThemesInstall' + url;
        },

        themePath: '&theme=',
        browsePath: '&browse=',
        searchPath: '&search=',

        search: function (query) {
            $('.wp-filter-search').val(query);
        },

        navigate: function () {
            if (Backbone.history._hasPushState) {
                Backbone.Router.prototype.navigate.apply(this, arguments);
            }
        }
    });


    themes.RunInstaller = {

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

            // Render results
            this.render();

        },

        render: function () {

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

            Backbone.history.start({
                root: themes.data.settings.adminUrl,
                pushState: true,
                hashChange: false
            });
        },

        routes: function () {
            let 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',  (slug)  => {
                request.theme = slug;
                this.view.collection.query(request);
            });

            // Handles sorting / browsing routes
            // Also handles the root URL triggering a sort request
            // for `featured`, the default view
            themes.router.on('route:sort', (sort) => {
                if (!sort) {
                    sort = 'featured';
                }

                this.view.sort(sort);
                this.view.trigger('theme:close');
            });

            // Support the `upload` route by going straight to upload section
            themes.router.on('route:upload', function () {
                $('a.upload').trigger('click');
            });

            // 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...
    $(document).ready(function () {
        if (themes.isInstall) {
            themes.RunInstaller.init();
        } else {
            themes.Run.init();
        }

        $('.broken-themes .delete-theme').on('click', function () {
            return confirm(_mainwpThemeSettings.settings.confirmDelete);
        });
    });

})(jQuery);

// Align theme browser thickbox
jQuery(function($) {

    globalThis.tb_position = function () {
        let tbWindow = $('#TB_window'),
            width = $(globalThis).width(),
            H = $(globalThis).height(),
            W = Math.min(1040, width),
            adminbar_height = 0;

        if ($('#wpadminbar').length) {
            adminbar_height =  Number.parseInt($('#wpadminbar').css('height'), 10);
        }

        if (tbWindow.length) {
            tbWindow.width(W - 50).height(H - 45 - adminbar_height);
            $('#TB_iframeContent').width(W - 50).height(H - 75 - adminbar_height);
            tbWindow.css({ 'margin-left': '-' +  Number.parseInt(((W - 50) / 2), 10) + 'px' });
            if ('maxWidth' in document.body.style) {
                tbWindow.css({
                    top: `${20 + adminbar_height}px`,
                    marginTop: 0
                });
            }
        }
    };

    $(globalThis).on('resize', function () {
        globalThis.tb_position();
    });
});

```
