Make WordPress Core

Changeset 34563


Ignore:
Timestamp:
09/25/2015 09:01:46 PM (11 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.