Make WordPress Core

Changeset 34563


Ignore:
Timestamp:
09/25/2015 09:01:46 PM (8 years ago)
Author:
westonruter
Message:

Customizer: Defer embedding widget controls to improve DOM performance and initial load time.

The Menu Customizer feature includes a performance technique whereby the controls for nav menu items are only embedded into the DOM once the containing menu section is expanded. This commit implements the same DOM deferral for widgets but goes a step further than just embedding the controls once the widget area's Customizer section is expanded: it also defers the embedding of the widget control's form until the widget is expanded, at which point the widget-added event also fires to allow any additional widget initialization to be done. The deferred DOM embedding can speed up initial load time by 10x or more. This DOM deferral also yields a reduction in overall memory usage in the browser process.

Includes changes to wp_widget_control() to facilitate separating out the widget form from the surrounding accordion container; also includes unit tests for this previously-untested function. Also included are initial QUnit tests (finally) for widgets in the Customizer.

Fixes #33901.

Location:
trunk
Files:
2 added
8 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/widgets.php

    r34017 r34563  
    182182    $add_new = isset($sidebar_args['_add']) ? $sidebar_args['_add'] : '';
    183183
     184    $before_form = isset( $sidebar_args['before_form'] ) ? $sidebar_args['before_form'] : '<form method="post">';
     185    $after_form = isset( $sidebar_args['after_form'] ) ? $sidebar_args['after_form'] : '</form>';
     186    $before_widget_content = isset( $sidebar_args['before_widget_content'] ) ? $sidebar_args['before_widget_content'] : '<div class="widget-content">';
     187    $after_widget_content = isset( $sidebar_args['after_widget_content'] ) ? $sidebar_args['after_widget_content'] : '</div>';
     188
    184189    $query_arg = array( 'editwidget' => $widget['id'] );
    185190    if ( $add_new ) {
     
    226231
    227232    <div class="widget-inside">
    228     <form method="post">
    229     <div class="widget-content">
    230 <?php
    231     if ( isset($control['callback']) )
     233    <?php echo $before_form; ?>
     234    <?php echo $before_widget_content; ?>
     235    <?php
     236    if ( isset( $control['callback'] ) ) {
    232237        $has_form = call_user_func_array( $control['callback'], $control['params'] );
    233     else
    234         echo "\t\t<p>" . __('There are no options for this widget.') . "</p>\n"; ?>
    235     </div>
     238    } else {
     239        echo "\t\t<p>" . __('There are no options for this widget.') . "</p>\n";
     240    }
     241    ?>
     242    <?php echo $after_widget_content; ?>
    236243    <input type="hidden" name="widget-id" class="widget-id" value="<?php echo esc_attr($id_format); ?>" />
    237244    <input type="hidden" name="id_base" class="id_base" value="<?php echo esc_attr($id_base); ?>" />
     
    253260        <br class="clear" />
    254261    </div>
    255     </form>
     262    <?php echo $after_form; ?>
    256263    </div>
    257264
  • trunk/src/wp-admin/js/customize-widgets.js

    r33599 r34563  
    418418         * @since 4.1.0
    419419         */
    420         initialize: function ( id, options ) {
     420        initialize: function( id, options ) {
    421421            var control = this;
    422             api.Control.prototype.initialize.call( control, id, options );
    423             control.expanded = new api.Value();
     422
     423            control.widgetControlEmbedded = false;
     424            control.widgetContentEmbedded = false;
     425            control.expanded = new api.Value( false );
    424426            control.expandedArgumentsQueue = [];
    425             control.expanded.bind( function ( expanded ) {
     427            control.expanded.bind( function( expanded ) {
    426428                var args = control.expandedArgumentsQueue.shift();
    427429                args = $.extend( {}, control.defaultExpandedArguments, args );
    428430                control.onChangeExpanded( expanded, args );
    429431            });
    430             control.expanded.set( false );
    431         },
    432 
    433         /**
    434          * Set up the control
     432
     433            api.Control.prototype.initialize.call( control, id, options );
     434        },
     435
     436        /**
     437         * Set up the control.
     438         *
     439         * @since 3.9.0
    435440         */
    436441        ready: function() {
    437             this._setupModel();
    438             this._setupWideWidget();
    439             this._setupControlToggle();
    440             this._setupWidgetTitle();
    441             this._setupReorderUI();
    442             this._setupHighlightEffects();
    443             this._setupUpdateUI();
    444             this._setupRemoveUI();
     442            var control = this;
     443
     444            /*
     445             * Embed a placeholder once the section is expanded. The full widget
     446             * form content will be embedded once the control itself is expanded,
     447             * and at this point the widget-added event will be triggered.
     448             */
     449            if ( ! control.section() ) {
     450                control.embedWidgetControl();
     451            } else {
     452                api.section( control.section(), function( section ) {
     453                    var onExpanded = function( isExpanded ) {
     454                        if ( isExpanded ) {
     455                            control.embedWidgetControl();
     456                            section.expanded.unbind( onExpanded );
     457                        }
     458                    };
     459                    if ( section.expanded() ) {
     460                        onExpanded( true );
     461                    } else {
     462                        section.expanded.bind( onExpanded );
     463                    }
     464                } );
     465            }
     466        },
     467
     468        /**
     469         * Embed the .widget element inside the li container.
     470         *
     471         * @since 4.4.0
     472         */
     473        embedWidgetControl: function() {
     474            var control = this, widgetControl;
     475
     476            if ( control.widgetControlEmbedded ) {
     477                return;
     478            }
     479            control.widgetControlEmbedded = true;
     480
     481            widgetControl = $( control.params.widget_control );
     482            control.container.append( widgetControl );
     483
     484            control._setupModel();
     485            control._setupWideWidget();
     486            control._setupControlToggle();
     487
     488            control._setupWidgetTitle();
     489            control._setupReorderUI();
     490            control._setupHighlightEffects();
     491            control._setupUpdateUI();
     492            control._setupRemoveUI();
     493        },
     494
     495        /**
     496         * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event.
     497         *
     498         * @since 4.4.0
     499         */
     500        embedWidgetContent: function() {
     501            var control = this, widgetContent;
     502
     503            control.embedWidgetControl();
     504            if ( control.widgetContentEmbedded ) {
     505                return;
     506            }
     507            control.widgetContentEmbedded = true;
     508
     509            widgetContent = $( control.params.widget_content );
     510            control.container.find( '.widget-content:first' ).append( widgetContent );
    445511
    446512            /*
     
    448514             * listeners and dynamic UI elements.
    449515             */
    450             $( document ).trigger( 'widget-added', [ this.container.find( '.widget:first' ) ] );
     516            $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] );
     517
    451518        },
    452519
     
    10091076                updateNumber, params, data, $inputs, processing, jqxhr, isChanged;
    10101077
     1078            // The updateWidget logic requires that the form fields to be fully present.
     1079            self.embedWidgetContent();
     1080
    10111081            args = $.extend( {
    10121082                instance: null,
     
    12551325        onChangeExpanded: function ( expanded, args ) {
    12561326            var self = this, $widget, $inside, complete, prevComplete;
     1327
     1328            self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
     1329            if ( expanded ) {
     1330                self.embedWidgetContent();
     1331            }
    12571332
    12581333            // If the expanded state is unchanged only manipulate container expanded states
  • trunk/src/wp-includes/class-wp-customize-control.php

    r33734 r34563  
    14881488    public $is_wide = false;
    14891489
     1490    /**
     1491     * Gather control params for exporting to JavaScript.
     1492     *
     1493     * @global array $wp_registered_widgets
     1494     */
    14901495    public function to_json() {
     1496        global $wp_registered_widgets;
     1497
    14911498        parent::to_json();
    14921499        $exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide' );
     
    14941501            $this->json[ $key ] = $this->$key;
    14951502        }
    1496     }
    1497 
    1498     /**
    1499      *
    1500      * @global array $wp_registered_widgets
    1501      */
    1502     public function render_content() {
    1503         global $wp_registered_widgets;
     1503
     1504        // Get the widget_control and widget_content.
    15041505        require_once ABSPATH . '/wp-admin/includes/widgets.php';
    15051506
     
    15151516
    15161517        $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
    1517         echo $this->manager->widgets->get_widget_control( $args );
    1518     }
     1518        $widget_control_parts = $this->manager->widgets->get_widget_control_parts( $args );
     1519
     1520        $this->json['widget_control'] = $widget_control_parts['control'];
     1521        $this->json['widget_content'] = $widget_control_parts['content'];
     1522    }
     1523
     1524    /**
     1525     * Override render_content to be no-op since content is exported via to_json for deferred embedding.
     1526     */
     1527    public function render_content() {}
    15191528
    15201529    /**
  • trunk/src/wp-includes/class-wp-customize-widgets.php

    r33535 r34563  
    899899     */
    900900    public function get_widget_control( $args ) {
     901        $args[0]['before_form'] = '<div class="form">';
     902        $args[0]['after_form'] = '</div><!-- .form -->';
     903        $args[0]['before_widget_content'] = '<div class="widget-content">';
     904        $args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
    901905        ob_start();
    902 
    903906        call_user_func_array( 'wp_widget_control', $args );
    904         $replacements = array(
    905             '<form method="post">' => '<div class="form">',
    906             '</form>' => '</div><!-- .form -->',
    907         );
    908 
    909907        $control_tpl = ob_get_clean();
    910 
    911         $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
    912 
    913908        return $control_tpl;
     909    }
     910
     911    /**
     912     * Get the widget control markup parts.
     913     *
     914     * @since 4.4.0
     915     * @access public
     916     *
     917     * @param array $args Widget control arguments.
     918     * @return array {
     919     *     @type string $control  Markup for widget control wrapping form.
     920     *     @type string $content  The contents of the widget form itself.
     921     * }
     922     */
     923    public function get_widget_control_parts( $args ) {
     924        $args[0]['before_widget_content'] = '<div class="widget-content">';
     925        $args[0]['after_widget_content'] = '</div><!-- .widget-content -->';
     926        $control_markup = $this->get_widget_control( $args );
     927
     928        $content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] );
     929        $content_end_pos = strrpos( $control_markup, $args[0]['after_widget_content'] );
     930
     931        $control = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) );
     932        $control .= substr( $control_markup, $content_end_pos );
     933        $content = trim( substr(
     934            $control_markup,
     935            $content_start_pos + strlen( $args[0]['before_widget_content'] ),
     936            $content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] )
     937        ) );
     938
     939        return compact( 'control', 'content' );
    914940    }
    915941
  • trunk/tests/phpunit/tests/customize/widgets.php

    r31622 r34563  
    196196        $this->assertEquals( $unsanitized_from_js, $new_categories_instance );
    197197    }
     198
     199    /**
     200     * Get the widget control args for tests.
     201     *
     202     * @return array
     203     */
     204    function get_test_widget_control_args() {
     205        global $wp_registered_widgets;
     206        require_once ABSPATH . '/wp-admin/includes/widgets.php';
     207        $widget_id = 'search-2';
     208        $widget = $wp_registered_widgets[ $widget_id ];
     209        $args = array(
     210            'widget_id' => $widget['id'],
     211            'widget_name' => $widget['name'],
     212        );
     213        $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
     214        return $args;
     215    }
     216
     217    /**
     218     * @see WP_Customize_Widgets::get_widget_control()
     219     */
     220    function test_get_widget_control() {
     221        $this->do_customize_boot_actions();
     222        $widget_control = $this->manager->widgets->get_widget_control( $this->get_test_widget_control_args() );
     223
     224        $this->assertContains( '<div class="form">', $widget_control );
     225        $this->assertContains( '<div class="widget-content">', $widget_control );
     226        $this->assertContains( '<input type="hidden" name="id_base" class="id_base" value="search"', $widget_control );
     227        $this->assertContains( '<input class="widefat"', $widget_control );
     228    }
     229
     230    /**
     231     * @see WP_Customize_Widgets::get_widget_control_parts()
     232     */
     233    function test_get_widget_control_parts() {
     234        $this->do_customize_boot_actions();
     235        $widget_control_parts = $this->manager->widgets->get_widget_control_parts( $this->get_test_widget_control_args() );
     236        $this->assertArrayHasKey( 'content', $widget_control_parts );
     237        $this->assertArrayHasKey( 'control', $widget_control_parts );
     238
     239        $this->assertContains( '<div class="form">', $widget_control_parts['control'] );
     240        $this->assertContains( '<div class="widget-content">', $widget_control_parts['control'] );
     241        $this->assertContains( '<input type="hidden" name="id_base" class="id_base" value="search"', $widget_control_parts['control'] );
     242        $this->assertNotContains( '<input class="widefat"', $widget_control_parts['control'] );
     243        $this->assertContains( '<input class="widefat"', $widget_control_parts['content'] );
     244    }
     245
     246    /**
     247     * @see WP_Widget_Form_Customize_Control::json()
     248     */
     249    function test_wp_widget_form_customize_control_json() {
     250        $this->do_customize_boot_actions();
     251        $control = $this->manager->get_control( 'widget_search[2]' );
     252        $params = $control->json();
     253
     254        $this->assertEquals( 'widget_form', $params['type'] );
     255        $this->assertRegExp( '#^<li[^>]+>\s+</li>$#', $params['content'] );
     256        $this->assertRegExp( '#^<div[^>]*class=\'widget\'[^>]*#s', $params['widget_control'] );
     257        $this->assertContains( '<div class="widget-content"></div>', $params['widget_control'] );
     258        $this->assertNotContains( '<input class="widefat"', $params['widget_control'] );
     259        $this->assertContains( '<input class="widefat"', $params['widget_content'] );
     260        $this->assertEquals( 'search-2', $params['widget_id'] );
     261        $this->assertEquals( 'search', $params['widget_id_base'] );
     262        $this->assertArrayHasKey( 'sidebar_id', $params );
     263        $this->assertArrayHasKey( 'width', $params );
     264        $this->assertArrayHasKey( 'height', $params );
     265        $this->assertInternalType( 'bool', $params['is_wide'] );
     266
     267    }
    198268}
  • trunk/tests/phpunit/tests/widgets.php

    r34465 r34563  
    307307        $result = dynamic_sidebar( 'Sidebar 1' );
    308308        ob_end_clean();
    309          
     309
    310310        $this->assertFalse( $result );
    311311    }
    312312
     313    /**
     314     * @see wp_widget_control()
     315     */
     316    function test_wp_widget_control() {
     317        global $wp_registered_widgets;
     318
     319        wp_widgets_init();
     320        require_once ABSPATH . '/wp-admin/includes/widgets.php';
     321        $widget_id = 'search-2';
     322        $widget = $wp_registered_widgets[ $widget_id ];
     323        $params = array(
     324            'widget_id' => $widget['id'],
     325            'widget_name' => $widget['name'],
     326        );
     327        $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $params, 1 => $widget['params'][0] ) );
     328
     329        ob_start();
     330        call_user_func_array( 'wp_widget_control', $args );
     331        $control = ob_get_clean();
     332        $this->assertNotEmpty( $control );
     333
     334        $this->assertContains( '<div class="widget-top">', $control );
     335        $this->assertContains( '<div class="widget-title-action">', $control );
     336        $this->assertContains( '<div class="widget-title">', $control );
     337        $this->assertContains( '<form method="post">', $control );
     338        $this->assertContains( '<div class="widget-content">', $control );
     339        $this->assertContains( '<input class="widefat"', $control );
     340        $this->assertContains( '<input type="hidden" name="id_base" class="id_base" value="search"', $control );
     341        $this->assertContains( '<div class="widget-control-actions">', $control );
     342        $this->assertContains( '<div class="alignleft">', $control );
     343        $this->assertContains( 'widget-control-remove', $control );
     344        $this->assertContains( 'widget-control-close', $control );
     345        $this->assertContains( '<div class="alignright">', $control );
     346        $this->assertContains( '<input type="submit" name="savewidget"', $control );
     347
     348        $param_overrides = array(
     349            'before_form' => '<!-- before_form -->',
     350            'after_form' => '<!-- after_form -->',
     351            'before_widget_content' => '<!-- before_widget_content -->',
     352            'after_widget_content' => '<!-- after_widget_content -->',
     353        );
     354        $params = array_merge( $params, $param_overrides );
     355        $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $params, 1 => $widget['params'][0] ) );
     356
     357        ob_start();
     358        call_user_func_array( 'wp_widget_control', $args );
     359        $control = ob_get_clean();
     360        $this->assertNotEmpty( $control );
     361        $this->assertNotContains( '<form method="post">', $control );
     362        $this->assertNotContains( '<div class="widget-content">', $control );
     363
     364        foreach ( $param_overrides as $contained ) {
     365            $this->assertContains( $contained, $control );
     366        }
     367    }
    313368}
  • trunk/tests/qunit/fixtures/customize-settings.js

    r33528 r34563  
    11window.wp = window.wp || {};
    2 window.wp.customize = window.wp.customize || { get: function(){} };
     2window.wp.customize = window.wp.customize || { get: function() {} };
    33
    44var customizerRootElement;
    55customizerRootElement = jQuery( '<div id="customize-theme-controls"><ul></ul></div>' );
    6 customizerRootElement.css( { position: 'absolute', left: -10000, top: -10000 } ); // remove from view
     6customizerRootElement.css( { position: 'absolute', left: -10000, top: -10000 } ); // Remove from view.
    77jQuery( document.body ).append( customizerRootElement );
    88
  • trunk/tests/qunit/index.html

    r33528 r34563  
    2626            <script src="fixtures/customize-settings.js"></script>
    2727            <script src="fixtures/customize-menus.js"></script>
     28            <script src="fixtures/customize-widgets.js"></script>
    2829        </div>
    2930        <p><a href="editor">TinyMCE tests</a></p>
     
    4546        <script src="../../src/wp-admin/js/nav-menu.js"></script>
    4647        <script src="../../src/wp-admin/js/customize-nav-menus.js"></script>
     48        <script src="../../src/wp-admin/js/customize-widgets.js"></script>
    4749        <script src="../../src/wp-admin/js/word-count.js"></script>
    4850
     
    5557        <script src="wp-admin/js/customize-controls-utils.js"></script>
    5658        <script src="wp-admin/js/customize-nav-menus.js"></script>
     59        <script src="wp-admin/js/customize-widgets.js"></script>
    5760        <script src="wp-admin/js/word-count.js"></script>
    5861
     
    268271                <button type="button" class="menus-move-up">Move up</button><button type="button" class="menus-move-down">Move down</button><button type="button" class="menus-move-left">Move one level up</button><button type="button" class="menus-move-right">Move one level down</button>         </div>
    269272        </script>
     273
     274        <script type="text/html" id="tmpl-customize-section-sidebar">
     275            <li id="accordion-section-{{ data.id }}" class="accordion-section control-section control-section-{{ data.type }}">
     276            <h3 class="accordion-section-title" tabindex="0">
     277                {{ data.title }}
     278                <span class="screen-reader-text">Press return or enter to open</span>
     279            </h3>
     280            <ul class="accordion-section-content">
     281                <li class="customize-section-description-container">
     282                    <div class="customize-section-title">
     283                        <button class="customize-section-back" tabindex="-1">
     284                            <span class="screen-reader-text">Back</span>
     285                        </button>
     286                        <h3>
     287                            <span class="customize-action">
     288                                {{{ data.customizeAction }}}
     289                            </span>
     290                            {{ data.title }}
     291                        </h3>
     292                    </div>
     293                    <# if ( data.description ) { #>
     294                        <div class="description customize-section-description">
     295                            {{{ data.description }}}
     296                        </div>
     297                    <# } #>
     298                </li>
     299            </ul>
     300            </li>
     301        </script>
     302
    270303        <div hidden>
    271304            <div id="available-menu-items" class="accordion-container">
     
    376409            </div><!-- end nav menu templates -->
    377410
    378 
     411        <div hidden>
     412            <div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
     413                <div id="available-widgets">
     414                    <div class="customize-section-title">
     415                        <button class="customize-section-back" tabindex="-1">
     416                            <span class="screen-reader-text">Back</span>
     417                        </button>
     418                        <h3>
     419                            <span class="customize-action">Customizing &#9656; Widgets</span>
     420                            Add a Widget                </h3>
     421                    </div>
     422                    <div id="available-widgets-filter">
     423                        <label class="screen-reader-text" for="widgets-search">Search Widgets</label>
     424                        <input type="search" id="widgets-search" placeholder="Search widgets&hellip;" />
     425                    </div>
     426                    <div id="available-widgets-list">
     427                                    <div id="widget-tpl-search-2" data-widget-id="search-2" class="widget-tpl search-2" tabindex="0">
     428                            <div id='widget-11_search-__i__' class='widget'>    <div class="widget-top">
     429            <div class="widget-title-action">
     430                <a class="widget-action hide-if-no-js" href="#available-widgets"></a>
     431                <a class="widget-control-edit hide-if-js" href="/wp-admin/customize.php?editwidget=search-2&#038;addnew=1&#038;num=3&#038;base=search">
     432                    <span class="edit">Edit</span>
     433                    <span class="add">Add</span>
     434                    <span class="screen-reader-text">Search</span>
     435                </a>
     436            </div>
     437            <div class="widget-title"><h4>Search<span class="in-widget-title"></span></h4></div>
     438            </div>
     439
     440            <div class="widget-inside">
     441            <div class="form">
     442            <div class="widget-content">
     443                <p><label for="widget-search-__i__-title">Title: <input class="widefat" id="widget-search-__i__-title" name="widget-search[__i__][title]" type="text" value="" /></label></p>
     444            </div>
     445            <input type="hidden" name="widget-id" class="widget-id" value="search-__i__" />
     446            <input type="hidden" name="id_base" class="id_base" value="search" />
     447            <input type="hidden" name="widget-width" class="widget-width" value="250" />
     448            <input type="hidden" name="widget-height" class="widget-height" value="200" />
     449            <input type="hidden" name="widget_number" class="widget_number" value="2" />
     450            <input type="hidden" name="multi_number" class="multi_number" value="3" />
     451            <input type="hidden" name="add_new" class="add_new" value="multi" />
     452
     453            <div class="widget-control-actions">
     454                <div class="alignleft">
     455                <a class="widget-control-remove" href="#remove">Delete</a> |
     456                <a class="widget-control-close" href="#close">Close</a>
     457                </div>
     458                <div class="alignright">
     459                    <input type="submit" name="savewidget" id="widget-search-__i__-savewidget" class="button button-primary widget-control-save right" value="Save"  />         <span class="spinner"></span>
     460                </div>
     461                <br class="clear" />
     462            </div>
     463            </div><!-- .form -->
     464            </div>
     465
     466            <div class="widget-description">
     467        A search form for your site.
     468            </div>
     469        </div>              </div>
     470                    </div><!-- #available-widgets-list -->
     471                </div><!-- #available-widgets -->
     472            </div><!-- #widgets-left -->
     473        </div><!-- end widget templates -->
    379474        <script src="../../src/wp-includes/js/tinymce/tinymce.js"></script>
    380475        <script src="editor/js/utils.js"></script>
Note: See TracChangeset for help on using the changeset viewer.