WordPress.org

Make WordPress Core

Changeset 41807


Ignore:
Timestamp:
10/10/2017 07:08:51 AM (8 months 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.