Make WordPress Core

Changeset 41807


Ignore:
Timestamp:
10/10/2017 07:08:51 AM (9 years ago)
Author:
westonruter
Message:

Customize: Improve behavior and extensibility of theme loading and searching.

  • Introduce WP_Customize_Themes_Section::$filter_type, which has built-in functionality for local and remote filtering. When this set to local, all themes are assumed to be loaded from Ajax when the section is first loaded, and subsequent searching/filtering is applied to the loaded collection of themes within the section. This is how the core "Installed" section behaves - third-party sources with limited numbers of themes may consider leveraging this implementation. When this is set to remote, searching and filtering always triggers a new remote query via Ajax. The core "WordPress.org" section uses this approach, as it has over 5000 themes to search.
  • Refactor filterSearch() to accept a raw term string as input. This enables a feature filter to be used on a section where filter_type is local.
  • Refactor filter() on a theme control to check for an array of terms. Also sort the results by the number of matches. Rather than searching for an exact match, this will now search for each word in a search distinctly, allowing things like tags to rank in search results more accurately.
  • Split loadControls() into two functions for themes section JS: loadThemes() to initiate and manage an Ajax request and loadControls() to create theme controls based on the results of the Ajax call. If third-party sections need to change the way controls are loaded, such as by using a custom control subclass of WP_Customize_Theme_Control, this allows them to use the core logic for managing the Ajax call and only override the actual control-creation process.
  • Introduce customize_load_themes filter to facilitate loading themes from third-party sources (or modifying the results of the core sections).
  • Bring significant improvements to the installed themes search filter.

Props celloexpressions.
Amends [41648].
See #37661.
Fixes #42049.

Location:
trunk/src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/js/customize-controls.js

    r41803 r41807  
    17181718                        });
    17191719
    1720                         // Filter-search all theme objects loaded in the section.
    1721                         section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
    1722                                         section.filterSearch( event.currentTarget );
    1723                         });
    1724 
    1725                         // Event listeners for remote wporg queries with user-entered terms.
    1726                         if ( 'wporg' === section.params.action ) {
    1727 
     1720                        if ( 'local' === section.params.filter_type ) {
     1721
     1722                                // Filter-search all theme objects loaded in the section.
     1723                                section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
     1724                                        section.filterSearch( event.currentTarget.value );
     1725                                });
     1726
     1727                        } else if ( 'remote' === section.params.filter_type ) {
     1728
     1729                                // Event listeners for remote queries with user-entered terms.
    17281730                                // Search terms.
    17291731                                debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
     
    17411743                                        section.checkTerm( section );
    17421744                                });
    1743 
    1744                                 // Toggle feature filter sections.
    1745                                 section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
    1746                                         $( e.currentTarget )
    1747                                                 .toggleClass( 'open' )
    1748                                                 .attr( 'aria-expanded', function( i, attr ) {
    1749                                                         return 'true' === attr ? 'false' : 'true';
    1750                                                 })
    1751                                                 .next( '.filter-drawer' ).slideToggle( 180, 'linear', function() {
    1752                                                         if ( 0 === section.filtersHeight ) {
    1753                                                                 section.filtersHeight = $( this ).height();
    1754 
    1755                                                                 // First time, so it's opened.
    1756                                                                 section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
    1757                                                         }
    1758                                                 });
    1759                                         if ( $( e.currentTarget ).hasClass( 'open' ) ) {
    1760                                                 section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
    1761                                         } else {
    1762                                                 section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
    1763                                         }
    1764                                 });
    1765                         }
     1745                        }
     1746
     1747                        // Toggle feature filters.
     1748                        section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
     1749                                $( e.currentTarget )
     1750                                        .toggleClass( 'open' )
     1751                                        .attr( 'aria-expanded', function( i, attr ) {
     1752                                                return 'true' === attr ? 'false' : 'true';
     1753                                        })
     1754                                        .next( '.filter-drawer' ).slideToggle( 180, 'linear', function() {
     1755                                                if ( 0 === section.filtersHeight ) {
     1756                                                        section.filtersHeight = $( this ).height();
     1757
     1758                                                        // First time, so it's opened.
     1759                                                        section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
     1760                                                }
     1761                                        });
     1762                                if ( $( e.currentTarget ).hasClass( 'open' ) ) {
     1763                                        section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + 76 );
     1764                                } else {
     1765                                        section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
     1766                                }
     1767                        });
    17661768
    17671769                        // Setup section cross-linking.
     
    18071809                                // Try to load controls if none are loaded yet.
    18081810                                if ( 0 === section.loaded ) {
    1809                                         section.loadControls();
     1811                                        section.loadThemes();
    18101812                                }
    18111813
     
    18221824
    18231825                                                        // Directly initialize an empty remote search to avoid a race condition.
    1824                                                         if ( '' === searchTerm && '' !== section.term && 'installed' !== section.params.action ) {
     1826                                                        if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) {
    18251827                                                                section.term = '';
    18261828                                                                section.initializeNewQuery( section.term, section.tags );
    18271829                                                        } else {
    1828                                                                 section.checkTerm( section );
     1830                                                                if ( 'remote' === section.params.filter_type ) {
     1831                                                                        section.checkTerm( section );
     1832                                                                } else if ( 'local' === section.params.filter_type ) {
     1833                                                                        section.filterSearch( searchTerm );
     1834                                                                }
    18291835                                                        }
    1830                                                         section.filterSearch( section.contentContainer.find( '.wp-filter-search' ).get( 0 ) );
    18311836                                                }
    18321837                                                otherSection.collapse( { duration: args.duration } );
     
    18781883                 * @returns {void}
    18791884                 */
    1880                 loadControls: function() {
     1885                loadThemes: function() {
    18811886                        var section = this, params, page, request;
    18821887
     
    18951900                        };
    18961901
    1897                         // Add fields for wporg actions.
    1898                         if ( 'wporg' === section.params.action ) {
     1902                        // Add fields for remote filtering.
     1903                        if ( 'remote' === section.params.filter_type ) {
    18991904                                params.search = section.term;
    19001905                                params.tags = section.tags;
     
    19071912                        request = wp.ajax.post( 'customize_load_themes', params );
    19081913                        request.done(function( data ) {
    1909                                 var themes = data.themes, newThemeControls;
     1914                                var themes = data.themes;
    19101915
    19111916                                // Stop and try again if the term changed while loading.
     
    19201925                                        section.nextTags = '';
    19211926                                        section.loading = false;
    1922                                         section.loadControls();
     1927                                        section.loadThemes();
    19231928                                        return;
    19241929                                }
    19251930
    19261931                                if ( 0 !== themes.length ) {
    1927                                         newThemeControls = [];
    1928 
    1929                                         // Add controls for each theme.
    1930                                         _.each( themes, function( theme ) {
    1931                                                 var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
    1932                                                         type: 'theme',
    1933                                                         section: section.params.id,
    1934                                                         theme: theme,
    1935                                                         priority: section.loaded + 1
    1936                                                 } );
    1937 
    1938                                                 api.control.add( themeControl );
    1939                                                 newThemeControls.push( themeControl );
    1940                                                 section.loaded = section.loaded + 1;
    1941                                         });
     1932
     1933                                        section.loadControls( themes, page );
    19421934
    19431935                                        if ( 1 === page ) {
     
    19511943                                                        }
    19521944                                                });
    1953                                                 if ( 'installed' !== section.params.action ) {
     1945                                                if ( 'local' !== section.params.filter_type ) {
    19541946                                                        wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
    19551947                                                }
    1956                                         } else {
    1957                                                 Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
    19581948                                        }
     1949
    19591950                                        _.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
    19601951
    1961                                         if ( 'installed' === section.params.action || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
     1952                                        if ( 'local' === section.params.filter_type || 100 > themes.length ) { // If we have less than the requested 100 themes, it's the end of the list.
    19621953                                                section.fullyLoaded = true;
    19631954                                        }
     
    19701961                                        }
    19711962                                }
    1972                                 if ( 'installed' === section.params.action ) {
     1963                                if ( 'local' === section.params.filter_type ) {
    19731964                                        section.updateCount(); // Count of visible theme controls.
    19741965                                } else {
     
    19961987
    19971988                /**
     1989                 * Loads controls into the section from data received from loadThemes().
     1990                 *
     1991                 * @since 4.9.0
     1992                 * @param {Array} themes - Array of theme data to create controls with.
     1993                 * @param {integer} page - Page of results being loaded.
     1994                 * @returns {void}
     1995                 */
     1996                loadControls: function( themes, page ) {
     1997                        var newThemeControls = [],
     1998                                section = this;
     1999
     2000                        // Add controls for each theme.
     2001                        _.each( themes, function( theme ) {
     2002                                var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
     2003                                        type: 'theme',
     2004                                        section: section.params.id,
     2005                                        theme: theme,
     2006                                        priority: section.loaded + 1
     2007                                } );
     2008
     2009                                api.control.add( themeControl );
     2010                                newThemeControls.push( themeControl );
     2011                                section.loaded = section.loaded + 1;
     2012                        });
     2013
     2014                        if ( 1 !== page ) {
     2015                                Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
     2016                        }
     2017                },
     2018
     2019                /**
    19982020                 * Determines whether more themes should be loaded, and loads them.
    19992021                 *
     
    20102032
    20112033                                if ( bottom > threshold ) {
    2012                                         section.loadControls();
     2034                                        section.loadThemes();
    20132035                                }
    20142036                        }
     
    20202042                 * @since 4.9.0
    20212043                 *
    2022                  * @param {Element} el - The search input element as a raw JS object.
     2044                 * @param {string} term - The raw search input value.
    20232045                 * @returns {void}
    20242046                 */
    2025                 filterSearch: function( el ) {
     2047                filterSearch: function( term ) {
    20262048                        var count = 0,
    20272049                                visible = false,
    20282050                                section = this,
    2029                                 noFilter = ( undefined !== api.section( 'wporg_themes' ) && 'wporg' !== section.params.action ) ? '.no-themes-local' : '.no-themes',
    2030                                 term = el.value.toLowerCase().trim().replace( '-', ' ' ),
    2031                                 controls = section.controls();
     2051                                noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes',
     2052                                controls = section.controls(),
     2053                                terms;
    20322054
    20332055                        if ( section.loading ) {
     
    20352057                        }
    20362058
     2059                        // Standardize search term format and split into an array of individual words.
     2060                        terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' );
     2061
    20372062                        _.each( controls, function( control ) {
    2038                                 visible = control.filter( term );
     2063                                visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term.
    20392064                                if ( visible ) {
    20402065                                        count = count + 1;
     
    20502075
    20512076                        section.renderScreenshots();
     2077                        api.reflowPaneContents();
    20522078
    20532079                        // Update theme count.
     
    20652091                checkTerm: function( section ) {
    20662092                        var newTerm;
    2067                         if ( 'wporg' === section.params.action ) {
     2093                        if ( 'remote' === section.params.filter_type ) {
    20682094                                newTerm = section.contentContainer.find( '.wp-filter-search' ).val();
    20692095                                if ( section.term !== newTerm ) {
     
    21052131                                        section.nextTags = tags;
    21062132                                } else {
    2107                                         section.initializeNewQuery( section.term, tags );
     2133                                        if ( 'remote' === section.params.filter_type ) {
     2134                                                section.initializeNewQuery( section.term, tags );
     2135                                        } else if ( 'local' === section.params.filter_type ) {
     2136                                                section.filterSearch( tags.join( ' ' ) );
     2137                                        }
    21082138                                }
    21092139                        }
     
    21312161                        section.screenshotQueue = null;
    21322162
    2133                         // Run a new query, with loadControls handling paging, etc.
     2163                        // Run a new query, with loadThemes handling paging, etc.
    21342164                        if ( ! section.loading ) {
    21352165                                section.term = newTerm;
    21362166                                section.tags = newTags;
    2137                                 section.loadControls();
     2167                                section.loadThemes();
    21382168                        } else {
    2139                                 section.nextTerm = newTerm; // This will reload from loadControls() with the newest term once the current batch is loaded.
    2140                                 section.nextTags = newTags; // This will reload from loadControls() with the newest tags once the current batch is loaded.
     2169                                section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded.
     2170                                section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded.
    21412171                        }
    21422172                        if ( ! section.expanded() ) {
     
    48764906                 *
    48774907                 * @since 4.2.0
     4908                 * @param {Array} terms - An array of terms to search for.
    48784909                 * @returns {boolean} Whether a theme control was activated or not.
    48794910                 */
    4880                 filter: function( term ) {
     4911                filter: function( terms ) {
    48814912                        var control = this,
     4913                                matchCount = 0,
    48824914                                haystack = control.params.theme.name + ' ' +
    48834915                                        control.params.theme.description + ' ' +
    48844916                                        control.params.theme.tags + ' ' +
    4885                                         control.params.theme.author;
     4917                                        control.params.theme.author + ' ';
    48864918                        haystack = haystack.toLowerCase().replace( '-', ' ' );
    4887                         if ( -1 !== haystack.search( term ) ) {
     4919
     4920                        // Back-compat for behavior in WordPress 4.2.0 to 4.8.X.
     4921                        if ( ! _.isArray( terms ) ) {
     4922                                terms = [ terms ];
     4923                        }
     4924
     4925                        // Always give exact name matches highest ranking.
     4926                        if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) {
     4927                                matchCount = 100;
     4928                        } else {
     4929
     4930                                // Search for and weight (by 10) complete term matches.
     4931                                matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 );
     4932
     4933                                // Search for each term individually (as whole-word and partial match) and sum weighted match counts.
     4934                                _.each( terms, function( term ) {
     4935                                        matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted.
     4936                                        matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing.
     4937                                });
     4938
     4939                                // Upper limit on match ranking.
     4940                                if ( matchCount > 99 ) {
     4941                                        matchCount = 99;
     4942                                }
     4943                        }
     4944
     4945                        if ( 0 !== matchCount ) {
    48884946                                control.activate();
     4947                                control.params.priority = 101 - matchCount; // Sort results by match count.
    48894948                                return true;
    48904949                        } else {
    4891                                 control.deactivate();
     4950                                control.deactivate(); // Hide control
     4951                                control.params.priority = 101;
    48924952                                return false;
    48934953                        }
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41802 r41807  
    44164416                                'title'       => __( 'WordPress.org themes' ),
    44174417                                'action'      => 'wporg',
     4418                                'filter_type' => 'remote',
    44184419                                'capability'  => 'install_themes',
    44194420                                'panel'       => 'themes',
     
    49484949                $theme_action = sanitize_key( $_POST['theme_action'] );
    49494950                $themes = array();
     4951                $args = array();
     4952
     4953                // Define query filters based on user input.
     4954                if ( ! array_key_exists( 'search', $_POST ) ) {
     4955                        $args['search'] = '';
     4956                } else {
     4957                        $args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
     4958                }
     4959
     4960                if ( ! array_key_exists( 'tags', $_POST ) ) {
     4961                        $args['tag'] = '';
     4962                } else {
     4963                        $args['tag'] = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['tags'] ) );
     4964                }
     4965
     4966                if ( ! array_key_exists( 'page', $_POST ) ) {
     4967                        $args['page'] = 1;
     4968                } else {
     4969                        $args['page'] = absint( $_POST['page'] );
     4970                }
    49504971
    49514972                require_once ABSPATH . 'wp-admin/includes/theme.php';
     4973
    49524974                if ( 'installed' === $theme_action ) {
     4975
     4976                        // Load all installed themes from wp_prepare_themes_for_js().
    49534977                        $themes = array( 'themes' => wp_prepare_themes_for_js() );
    49544978                        foreach ( $themes['themes'] as &$theme ) {
     
    49564980                                $theme['active'] = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme['id'] );
    49574981                        }
     4982
    49584983                } elseif ( 'wporg' === $theme_action ) {
     4984
     4985                        // Load WordPress.org themes from the .org API and normalize data to match installed theme objects.
    49594986                        if ( ! current_user_can( 'install_themes' ) ) {
    49604987                                wp_die( -1 );
     
    49624989
    49634990                        // Arguments for all queries.
    4964                         $args = array(
     4991                        $wporg_args = array(
    49654992                                'per_page' => 100,
    4966                                 'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
    49674993                                'fields' => array(
    49684994                                        'screenshot_url' => true,
     
    49805006                        );
    49815007
    4982                         // Define query filters based on user input.
    4983                         if ( ! array_key_exists( 'search', $_POST ) ) {
    4984                                 $args['search'] = '';
    4985                         } else {
    4986                                 $args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
    4987                         }
    4988 
    4989                         if ( ! array_key_exists( 'tags', $_POST ) ) {
    4990                                 $args['tag'] = '';
    4991                         } else {
    4992                                 $args['tag'] = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['tags'] ) );
    4993                         }
     5008                        $args = array_merge( $wporg_args, $args );
    49945009
    49955010                        if ( '' === $args['search'] && '' === $args['tag'] ) {
     
    50625077                        } // End foreach().
    50635078                } // End if().
     5079
     5080                /**
     5081                 * Filters the theme data loaded in the customizer.
     5082                 *
     5083                 * This allows theme data to be loading from an external source,
     5084                 * or modification of data loaded from `wp_prepare_themes_for_js()`
     5085                 * or WordPress.org via `themes_api()`.
     5086                 *
     5087                 * @since 4.9.0
     5088                 *
     5089                 * @see wp_prepare_themes_for_js()
     5090                 * @see themes_api()
     5091                 * @see WP_Customize_Manager::__construct()
     5092                 *
     5093                 * @param array                $themes  Nested array of theme data.
     5094                 * @param array                $args    List of arguments, such as page, search term, and tags to query for.
     5095                 * @param WP_Customize_Manager $manager Instance of Customize manager.
     5096                 */
     5097                $themes = apply_filters( 'customize_load_themes', $themes, $args, $this );
     5098
    50645099                wp_send_json_success( $themes );
    50655100        }
  • trunk/src/wp-includes/customize/class-wp-customize-themes-section.php

    r41709 r41807  
    3838
    3939        /**
     40         * Theme section filter type.
     41         *
     42         * Determines whether filters are applied to loaded (local) themes or by initiating a new remote query (remote).
     43         * When filtering is local, the initial themes query is not paginated by default.
     44         *
     45         * @since 4.9.0
     46         * @var string
     47         */
     48        public $filter_type = 'local';
     49
     50        /**
    4051         * Get section parameters for JS.
    4152         *
     
    4657                $exported = parent::json();
    4758                $exported['action'] = $this->action;
     59                $exported['filter_type'] = $this->filter_type;
    4860
    4961                return $exported;
Note: See TracChangeset for help on using the changeset viewer.