/* eslint complexity: ["error", 100] */ // current complexity is the only way to achieve desired results, pull request solutions appreciated. /* global _mainwpThemeSettings, confirm */ window.wp = window.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: $(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('
'); }, // Defines search element container searchContainer: $('#wpbody'), // init container // Search input and view // for current theme collection search: function () { let 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 }); // Render and append after screen title view.render(); this.searchContainer .append(view.el) .append(''); }, // Checks when the user gets close to the bottom // of the mage and triggers a theme:scroll event scroller: function () { let 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 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.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, ''); // 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) { let 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 */ let 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) { 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('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 // @devtodo 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('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 (e) { 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) { let self = this; 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', self.model.cid); }, preventExpand: function () { this.touchDrag = true; }, preview: function (event) { let self = this, 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', 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'); self.current = current; return self.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 () { 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 self = this, 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, 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.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(this.model.collection.length - 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 () { let self = this; self.trigger('theme:next', self.model.cid); return false; }, previousTheme: function () { let 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) { 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 four 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) { let self = this; // Set up parent this.parent = options.parent; // Set current view to [grid] this.setView('grid'); // Move the active theme to the beginning of the collection self.currentTheme(); // When the collection is updated by user input... this.listenTo(self.collection, 'update', function () { self.parent.page = 0; self.currentTheme(); self.render(this); }); // 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; } // 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.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) { let self = this; self.instance = self.collection.paginate(page); // If we have no more themes bail if (self.instance.length === 0) { // Fire a no-more-themes event. 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 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.data.settings.canInstall) { this.$el.append('

' + l10n.addNew + '

'); } this.parent.page++; }, // Grabs current theme and puts it at the beginning of the collection currentTheme: function () { let 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) { let self = this; // 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(); 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) { let 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); // 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 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) { let 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 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 () { let 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' }, // 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 () { let 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'); $('.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('

' + l10n.error + '

'); }); 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('' + name + ''); }); // 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 }); $(this).addClass('mainwp_action_down'); $('a.browse-themes').removeClass('mainwp_action_down'); $('#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 }); $(this).addClass('mainwp_action_down'); $('a.upload').removeClass('mainwp_action_down'); $('#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'), 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: { '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 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) { request.theme = slug; self.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', function (sort) { if (!sort) { sort = 'featured'; } self.view.sort(sort); self.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($) { window.tb_position = function () { let 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) { 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 () { window.tb_position(); }); });