Make WordPress Core

Ticket #28709: 28709.12.diff

File 28709.12.diff, 74.1 KB (added by westonruter, 10 years ago)

https://github.com/xwpco/wordpress-develop/compare/f714ff4...619799f4

  • src/wp-admin/customize.php

    diff --git src/wp-admin/customize.php src/wp-admin/customize.php
    index 1d79598..50ee0ad 100644
    do_action( 'customize_controls_init' ); 
    5353wp_enqueue_script( 'customize-controls' );
    5454wp_enqueue_style( 'customize-controls' );
    5555
    56 wp_enqueue_script( 'accordion' );
    57 
    5856/**
    5957 * Enqueue Customizer control scripts.
    6058 *
    do_action( 'customize_controls_print_scripts' ); 
    130128                ?>
    131129
    132130                <div id="widgets-right"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat -->
    133                 <div class="wp-full-overlay-sidebar-content accordion-container" tabindex="-1">
     131                <div class="wp-full-overlay-sidebar-content" tabindex="-1">
    134132                        <div id="customize-info" class="accordion-section <?php if ( $cannot_expand ) echo ' cannot-expand'; ?>">
    135133                                <div class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0">
    136134                                        <span class="preview-notice"><?php
    do_action( 'customize_controls_print_scripts' ); 
    160158                                <?php endif; ?>
    161159                        </div>
    162160
    163                         <div id="customize-theme-controls"><ul>
    164                                 <?php
    165                                 foreach ( $wp_customize->containers() as $container ) {
    166                                         $container->maybe_render();
    167                                 }
    168                                 ?>
    169                         </ul></div>
     161                        <div id="customize-theme-controls">
     162                                <ul><?php // Panels and sections are managed here via JavaScript ?></ul>
     163                        </div>
    170164                </div>
    171165                </div>
    172166
    do_action( 'customize_controls_print_scripts' ); 
    252246                ),
    253247                'settings' => array(),
    254248                'controls' => array(),
     249                'panels'   => array(),
     250                'sections' => array(),
    255251                'nonce'    => array(
    256252                        'save'    => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ),
    257253                        'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() )
    258254                ),
     255                'autofocus' => array(),
    259256        );
    260257
    261258        // Prepare Customize Setting objects to pass to Javascript.
    do_action( 'customize_controls_print_scripts' ); 
    266263                );
    267264        }
    268265
    269         // Prepare Customize Control objects to pass to Javascript.
     266        // Prepare Customize Control objects to pass to JavaScript.
    270267        foreach ( $wp_customize->controls() as $id => $control ) {
    271                 $control->to_json();
    272                 $settings['controls'][ $id ] = $control->json;
     268                $settings['controls'][ $id ] = $control->json();
     269        }
     270
     271        // Prepare Customize Section objects to pass to JavaScript.
     272        foreach ( $wp_customize->sections() as $id => $section ) {
     273                $settings['sections'][ $id ] = $section->json();
     274        }
     275
     276        // Prepare Customize Panel objects to pass to JavaScript.
     277        foreach ( $wp_customize->panels() as $id => $panel ) {
     278                $settings['panels'][ $id ] = $panel->json();
     279                foreach ( $panel->sections as $section_id => $section ) {
     280                        $settings['sections'][ $section_id ] = $section->json();
     281                }
     282        }
     283
     284        // Pass to frontend the Customizer construct being deeplinked
     285        if ( isset( $_GET['autofocus'] ) && is_array( $_GET['autofocus'] ) ) {
     286                $autofocus = wp_unslash( $_GET['autofocus'] );
     287                foreach ( $autofocus as $type => $id ) {
     288                        if ( isset( $settings[ $type . 's' ][ $id ] ) ) {
     289                                $settings['autofocus'][ $type ] = $id;
     290                        }
     291                }
    273292        }
    274293
    275294        ?>
  • src/wp-admin/js/accordion.js

    diff --git src/wp-admin/js/accordion.js src/wp-admin/js/accordion.js
    index 6cb1c1c..1769d27 100644
     
    2525 *
    2626 * Note that any appropriate tags may be used, as long as the above classes are present.
    2727 *
    28  * In addition to the standard accordion behavior, this file includes JS for the
    29  * Customizer's "Panel" functionality.
    30  *
    3128 * @since 3.6.0.
    3229 */
    3330
     
    4643                        accordionSwitch( $( this ) );
    4744                });
    4845
    49                 // Go back to the top-level Customizer accordion.
    50                 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
    51                         if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
    52                                 return;
    53                         }
    54 
    55                         e.preventDefault(); // Keep this AFTER the key filter above
    56 
    57                         panelSwitch( $( '.current-panel' ) );
    58                 });
    5946        });
    6047
    61         var sectionContent = $( '.accordion-section-content' );
    62 
    6348        /**
    6449         * Close the current accordion section and open a new one.
    6550         *
     
    6954        function accordionSwitch ( el ) {
    7055                var section = el.closest( '.accordion-section' ),
    7156                        siblings = section.closest( '.accordion-container' ).find( '.open' ),
    72                         content = section.find( sectionContent );
     57                        content = section.find( '.accordion-section-content' );
    7358
    7459                // This section has no content and cannot be expanded.
    7560                if ( section.hasClass( 'cannot-expand' ) ) {
    7661                        return;
    7762                }
    7863
    79                 // Slide into a sub-panel instead of accordioning (Customizer-specific).
    80                 if ( section.hasClass( 'control-panel' ) ) {
    81                         panelSwitch( section );
    82                         return;
    83                 }
    84 
    8564                if ( section.hasClass( 'open' ) ) {
    8665                        section.toggleClass( 'open' );
    8766                        content.toggle( true ).slideToggle( 150 );
    8867                } else {
    8968                        siblings.removeClass( 'open' );
    90                         siblings.find( sectionContent ).show().slideUp( 150 );
     69                        siblings.find( '.accordion-section-content' ).show().slideUp( 150 );
    9170                        content.toggle( false ).slideToggle( 150 );
    9271                        section.toggleClass( 'open' );
    9372                }
    9473        }
    9574
    96         /**
    97          * Slide into an accordion sub-panel.
    98          *
    99          * For the Customizer-specific panel functionality
    100          *
    101          * @param {Object} panel Title element or back button of the accordion panel to toggle.
    102          * @since 4.0.0
    103          */
    104         function panelSwitch( panel ) {
    105                 var position, scroll,
    106                         section = panel.closest( '.accordion-section' ),
    107                         overlay = section.closest( '.wp-full-overlay' ),
    108                         container = section.closest( '.accordion-container' ),
    109                         siblings = container.find( '.open' ),
    110                         topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
    111                         backBtn = overlay.find( '.control-panel-back' ),
    112                         panelTitle = section.find( '.accordion-section-title' ).first(),
    113                         content = section.find( '.control-panel-content' );
    114 
    115                 if ( section.hasClass( 'current-panel' ) ) {
    116                         section.toggleClass( 'current-panel' );
    117                         overlay.toggleClass( 'in-sub-panel' );
    118                         content.delay( 180 ).hide( 0, function() {
    119                                 content.css( 'margin-top', 'inherit' ); // Reset
    120                         } );
    121                         topPanel.attr( 'tabindex', '0' );
    122                         backBtn.attr( 'tabindex', '-1' );
    123                         panelTitle.focus();
    124                         container.scrollTop( 0 );
    125                 } else {
    126                         // Close all open sections in any accordion level.
    127                         siblings.removeClass( 'open' );
    128                         siblings.find( sectionContent ).show().slideUp( 0 );
    129                         content.show( 0, function() {
    130                                 position = content.offset().top;
    131                                 scroll = container.scrollTop();
    132                                 content.css( 'margin-top', ( 45 - position - scroll ) );
    133                                 section.toggleClass( 'current-panel' );
    134                                 overlay.toggleClass( 'in-sub-panel' );
    135                                 container.scrollTop( 0 );
    136                         } );
    137                         topPanel.attr( 'tabindex', '-1' );
    138                         backBtn.attr( 'tabindex', '0' );
    139                         backBtn.focus();
    140                 }
    141         }
    142 
    14375})(jQuery);
  • src/wp-admin/js/customize-controls.js

    diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
    index 363e965..275424e 100644
     
    11/* globals _wpCustomizeHeader, _wpMediaViewsL10n */
    22(function( exports, $ ){
    3         var api = wp.customize;
     3        var bubbleChildValueChanges, Container, focus, isKeydownButNotEnterEvent, areElementListsEqual, api = wp.customize;
     4
     5        // @todo Move private helper functions to wp.customize.utils so they can be unit tested
    46
    57        /**
    68         * @constructor
     
    3133        });
    3234
    3335        /**
     36         * Watch all changes to Value properties, and bubble changes to parent Values instance
     37         *
     38         * @param {wp.customize.Class} instance
     39         * @param {Array} properties  The names of the Value instances to watch.
     40         */
     41        bubbleChildValueChanges = function ( instance, properties ) {
     42                $.each( properties, function ( i, key ) {
     43                        instance[ key ].bind( function ( to, from ) {
     44                                if ( instance.parent && to !== from ) {
     45                                        instance.parent.trigger( 'change', instance );
     46                                }
     47                        } );
     48                } );
     49        };
     50
     51        /**
     52         * Expand a panel, section, or control and focus on the first focusable element.
     53         *
     54         * @param {Object} [params]
     55         */
     56        focus = function ( params ) {
     57                var construct, completeCallback, focus;
     58                construct = this;
     59                params = params || {};
     60                focus = function () {
     61                        construct.container.find( ':focusable:first' ).focus();
     62                        construct.container[0].scrollIntoView( true );
     63                };
     64                if ( params.completeCallback ) {
     65                        completeCallback = params.completeCallback;
     66                        params.completeCallback = function () {
     67                                focus();
     68                                completeCallback();
     69                        };
     70                } else {
     71                        params.completeCallback = focus;
     72                }
     73                if ( construct.expand ) {
     74                        construct.expand( params );
     75                } else {
     76                        params.completeCallback();
     77                }
     78        };
     79
     80        /**
     81         * Return whether the supplied Event object is for a keydown event but not the Enter key.
     82         *
     83         * @param {jQuery.Event} event
     84         * @returns {boolean}
     85         */
     86        isKeydownButNotEnterEvent = function ( event ) {
     87                return ( 'keydown' === event.type && 13 !== event.which );
     88        };
     89
     90        /**
     91         * Return whether the two lists of elements are the same and are in the same order.
     92         *
     93         * @param {Array|jQuery} listA
     94         * @param {Array|jQuery} listB
     95         * @returns {boolean}
     96         */
     97        areElementListsEqual = function ( listA, listB ) {
     98                var equal = (
     99                        listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
     100                        -1 === _.map( // are there any false values in the list returned by map?
     101                                _.zip( listA, listB ), // pair up each element between the two lists
     102                                function ( pair ) {
     103                                        return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
     104                                }
     105                        ).indexOf( false ) // check for presence of false in map's return value
     106                );
     107                return equal;
     108        };
     109
     110        /**
     111         * Base class for Panel and Section
     112         *
    34113         * @constructor
    35114         * @augments wp.customize.Class
    36115         */
    37         api.Control = api.Class.extend({
    38                 initialize: function( id, options ) {
    39                         var control = this,
    40                                 nodes, radios, settings;
     116        Container = api.Class.extend({
     117                defaultActiveArguments: { duration: 'fast' },
     118                defaultExpandedArguments: { duration: 'fast' },
     119
     120                initialize: function ( id, options ) {
     121                        var container = this;
     122                        container.id = id;
     123                        container.params = {};
     124                        $.extend( container, options || {} );
     125                        container.container = $( container.params.content );
     126
     127                        container.deferred = {
     128                                ready: new $.Deferred()
     129                        };
     130                        container.priority = new api.Value();
     131                        container.active = new api.Value();
     132                        container.activeArgumentsQueue = [];
     133                        container.expanded = new api.Value();
     134                        container.expandedArgumentsQueue = [];
     135
     136                        container.active.bind( function ( active ) {
     137                                var args = container.activeArgumentsQueue.shift();
     138                                args = $.extend( {}, container.defaultActiveArguments, args );
     139                                active = ( active && container.isContextuallyActive() );
     140                                container.onChangeActive( active, args );
     141                                // @todo trigger 'activated' and 'deactivated' events based on the expanded param?
     142                        });
     143                        container.expanded.bind( function ( expanded ) {
     144                                var args = container.expandedArgumentsQueue.shift();
     145                                args = $.extend( {}, container.defaultExpandedArguments, args );
     146                                container.onChangeExpanded( expanded, args );
     147                                // @todo trigger 'expanded' and 'collapsed' events based on the expanded param?
     148                        });
    41149
    42                         this.params = {};
    43                         $.extend( this, options || {} );
     150                        container.attachEvents();
    44151
    45                         this.id = id;
    46                         this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
    47                         this.container = $( this.selector );
    48                         this.active = new api.Value( this.params.active );
     152                        bubbleChildValueChanges( container, [ 'priority', 'active' ] );
    49153
    50                         settings = $.map( this.params.settings, function( value ) {
    51                                 return value;
     154                        container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
     155                        container.active.set( container.params.active );
     156                        container.expanded.set( false ); // @todo True if deeplinking?
     157                },
     158
     159                /**
     160                 * @abstract
     161                 */
     162                ready: function() {},
     163
     164                /**
     165                 * Get the child models associated with this parent, sorting them by their priority Value.
     166                 *
     167                 * @param {String} parentType
     168                 * @param {String} childType
     169                 * @returns {Array}
     170                 */
     171                _children: function ( parentType, childType ) {
     172                        var parent = this,
     173                                children = [];
     174                        api[ childType ].each( function ( child ) {
     175                                if ( child[ parentType ].get() === parent.id ) {
     176                                        children.push( child );
     177                                }
     178                        } );
     179                        children.sort( function ( a, b ) {
     180                                return a.priority() - b.priority();
     181                        } );
     182                        return children;
     183                },
     184
     185                /**
     186                 * To override by subclass, to return whether the container has active children.
     187                 * @abstract
     188                 */
     189                isContextuallyActive: function () {
     190                        throw new Error( 'Must override with subclass.' );
     191                },
     192
     193                /**
     194                 * Handle changes to the active state.
     195                 * This does not change the active state, it merely handles the behavior
     196                 * for when it does change.
     197                 *
     198                 * To override by subclass, update the container's UI to reflect the provided active state.
     199                 *
     200                 * @param {Boolean} active
     201                 * @param {Object} args  merged on top of this.defaultActiveArguments
     202                 */
     203                onChangeActive: function ( active, args ) {
     204                        var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
     205                        if ( active ) {
     206                                this.container.stop( true, true ).slideDown( duration, args.completeCallback );
     207                        } else {
     208                                this.container.stop( true, true ).slideUp( duration, args.completeCallback );
     209                        }
     210                },
     211
     212                /**
     213                 * @params {Boolean} active
     214                 * @param {Object} [params]
     215                 * @returns {Boolean} false if state already applied
     216                 */
     217                _toggleActive: function ( active, params ) {
     218                        var self = this;
     219                        params = params || {};
     220                        if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
     221                                params.unchanged = true;
     222                                self.onChangeActive( self.active.get(), params );
     223                                return false;
     224                        } else {
     225                                params.unchanged = false;
     226                                this.activeArgumentsQueue.push( params );
     227                                this.active.set( active );
     228                                return true;
     229                        }
     230                },
     231
     232                /**
     233                 * @param {Object} [params]
     234                 * @returns {Boolean} false if already active
     235                 */
     236                activate: function ( params ) {
     237                        return this._toggleActive( true, params );
     238                },
     239
     240                /**
     241                 * @param {Object} [params]
     242                 * @returns {Boolean} false if already inactive
     243                 */
     244                deactivate: function ( params ) {
     245                        return this._toggleActive( false, params );
     246                },
     247
     248                /**
     249                 * To override by subclass, update the container's UI to reflect the provided active state.
     250                 * @abstract
     251                 */
     252                onChangeExpanded: function () {
     253                        throw new Error( 'Must override with subclass.' );
     254                },
     255
     256                /**
     257                 * @param {Boolean} expanded
     258                 * @param {Object} [params]
     259                 * @returns {Boolean} false if state already applied
     260                 */
     261                _toggleExpanded: function ( expanded, params ) {
     262                        var self = this;
     263                        params = params || {};
     264                        if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
     265                                params.unchanged = true;
     266                                self.onChangeExpanded( self.expanded.get(), params );
     267                                return false;
     268                        } else {
     269                                params.unchanged = false;
     270                                this.expandedArgumentsQueue.push( params );
     271                                this.expanded.set( expanded );
     272                                return true;
     273                        }
     274                },
     275
     276                /**
     277                 * @param {Object} [params]
     278                 * @returns {Boolean} false if already expanded
     279                 */
     280                expand: function ( params ) {
     281                        return this._toggleExpanded( true, params );
     282                },
     283
     284                /**
     285                 * @param {Object} [params]
     286                 * @returns {Boolean} false if already collapsed
     287                 */
     288                collapse: function ( params ) {
     289                        return this._toggleExpanded( false, params );
     290                },
     291
     292                /**
     293                 * Bring the container into view and then expand this and bring it into view
     294                 * @param {Object} [params]
     295                 */
     296                focus: focus
     297        });
     298
     299        /**
     300         * @constructor
     301         * @augments wp.customize.Class
     302         */
     303        api.Section = Container.extend({
     304
     305                /**
     306                 * @param {String} id
     307                 * @param {Array} options
     308                 */
     309                initialize: function ( id, options ) {
     310                        var section = this;
     311                        Container.prototype.initialize.call( section, id, options );
     312
     313                        section.id = id;
     314                        section.panel = new api.Value();
     315                        section.panel.bind( function ( id ) {
     316                                $( section.container ).toggleClass( 'control-subsection', !! id );
    52317                        });
     318                        section.panel.set( section.params.panel || '' );
     319                        bubbleChildValueChanges( section, [ 'panel' ] );
    53320
    54                         api.apply( api, settings.concat( function() {
    55                                 var key;
     321                        section.embed();
     322                        section.deferred.ready.done( function () {
     323                                section.ready();
     324                        });
     325                },
    56326
    57                                 control.settings = {};
    58                                 for ( key in control.params.settings ) {
    59                                         control.settings[ key ] = api( control.params.settings[ key ] );
     327                /**
     328                 * Embed the container in the DOM when any parent panel is ready.
     329                 */
     330                embed: function () {
     331                        var section = this, inject;
     332
     333                        // Watch for changes to the panel state
     334                        inject = function ( panelId ) {
     335                                var parentContainer;
     336                                if ( panelId ) {
     337                                        // The panel has been supplied, so wait until the panel object is registered
     338                                        api.panel( panelId, function ( panel ) {
     339                                                // The panel has been registered, wait for it to become ready/initialized
     340                                                panel.deferred.ready.done( function () {
     341                                                        parentContainer = panel.container.find( 'ul:first' );
     342                                                        if ( ! section.container.parent().is( parentContainer ) ) {
     343                                                                parentContainer.append( section.container );
     344                                                        }
     345                                                        section.deferred.ready.resolve(); // @todo Better to use `embedded` instead of `ready`
     346                                                });
     347                                        } );
     348                                } else {
     349                                        // There is no panel, so embed the section in the root of the customizer
     350                                        parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
     351                                        if ( ! section.container.parent().is( parentContainer ) ) {
     352                                                parentContainer.append( section.container );
     353                                        }
     354                                        section.deferred.ready.resolve();
     355                                }
     356                        };
     357                        section.panel.bind( inject );
     358                        inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
     359                },
     360
     361                /**
     362                 * Add behaviors for the accordion section
     363                 */
     364                attachEvents: function () {
     365                        var section = this;
     366
     367                        // Expand/Collapse accordion sections on click.
     368                        section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
     369                                if ( isKeydownButNotEnterEvent( event ) ) {
     370                                        return;
    60371                                }
     372                                event.preventDefault(); // Keep this AFTER the key filter above
    61373
    62                                 control.setting = control.settings['default'] || null;
    63                                 control.renderContent( function() {
    64                                         // Don't call ready() until the content has rendered.
    65                                         control.ready();
     374                                if ( section.expanded() ) {
     375                                        section.collapse();
     376                                } else {
     377                                        section.expand();
     378                                }
     379                        });
     380                },
     381
     382                /**
     383                 * Return whether this section has any active controls.
     384                 *
     385                 * @returns {boolean}
     386                 */
     387                isContextuallyActive: function () {
     388                        var section = this,
     389                                controls = section.controls(),
     390                                activeCount = 0;
     391                        _( controls ).each( function ( control ) {
     392                                if ( control.active() ) {
     393                                        activeCount += 1;
     394                                }
     395                        } );
     396                        return ( activeCount !== 0 );
     397                },
     398
     399                /**
     400                 * Get the controls that are associated with this section, sorted by their priority Value.
     401                 *
     402                 * @returns {Array}
     403                 */
     404                controls: function () {
     405                        return this._children( 'section', 'control' );
     406                },
     407
     408                /**
     409                 * Update UI to reflect expanded state
     410                 *
     411                 * @param {Boolean} expanded
     412                 * @param {Object} args
     413                 */
     414                onChangeExpanded: function ( expanded, args ) {
     415                        var section = this,
     416                                content = section.container.find( '.accordion-section-content' ),
     417                                expand;
     418
     419                        if ( expanded ) {
     420
     421                                if ( args.unchanged ) {
     422                                        expand = args.completeCallback;
     423                                } else {
     424                                        expand = function () {
     425                                                content.stop().slideDown( args.duration, args.completeCallback );
     426                                                section.container.addClass( 'open' );
     427                                        };
     428                                }
     429
     430                                if ( ! args.allowMultiple ) {
     431                                        api.section.each( function ( otherSection ) {
     432                                                if ( otherSection !== section ) {
     433                                                        otherSection.collapse( { duration: args.duration } );
     434                                                }
     435                                        });
     436                                }
     437
     438                                if ( section.panel() ) {
     439                                        api.panel( section.panel() ).expand({
     440                                                duration: args.duration,
     441                                                completeCallback: expand
     442                                        });
     443                                } else {
     444                                        expand();
     445                                }
     446
     447                        } else {
     448                                section.container.removeClass( 'open' );
     449                                content.slideUp( args.duration, args.completeCallback );
     450                        }
     451                }
     452        });
     453
     454        /**
     455         * @constructor
     456         * @augments wp.customize.Class
     457         */
     458        api.Panel = Container.extend({
     459                initialize: function ( id, options ) {
     460                        var panel = this;
     461                        Container.prototype.initialize.call( panel, id, options );
     462                        panel.embed();
     463                        panel.deferred.ready.done( function () {
     464                                panel.ready();
     465                        });
     466                },
     467
     468                /**
     469                 * Embed the container in the DOM when any parent panel is ready.
     470                 */
     471                embed: function () {
     472                        var panel = this,
     473                                parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
     474
     475                        if ( ! panel.container.parent().is( parentContainer ) ) {
     476                                parentContainer.append( panel.container );
     477                        }
     478                        panel.deferred.ready.resolve();
     479                },
     480
     481                /**
     482                 *
     483                 */
     484                attachEvents: function () {
     485                        var meta, panel = this;
     486
     487                        // Expand/Collapse accordion sections on click.
     488                        panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
     489                                if ( isKeydownButNotEnterEvent( event ) ) {
     490                                        return;
     491                                }
     492                                event.preventDefault(); // Keep this AFTER the key filter above
     493
     494                                if ( ! panel.expanded() ) {
     495                                        panel.expand();
     496                                }
     497                        });
     498
     499                        meta = panel.container.find( '.panel-meta:first' );
     500
     501                        meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
     502                                if ( isKeydownButNotEnterEvent( event ) ) {
     503                                        return;
     504                                }
     505                                event.preventDefault(); // Keep this AFTER the key filter above
     506
     507                                if ( meta.hasClass( 'cannot-expand' ) ) {
     508                                        return;
     509                                }
     510
     511                                var content = meta.find( '.accordion-section-content:first' );
     512                                if ( meta.hasClass( 'open' ) ) {
     513                                        meta.toggleClass( 'open' );
     514                                        content.slideUp( panel.defaultExpandedArguments.duration );
     515                                } else {
     516                                        content.slideDown( panel.defaultExpandedArguments.duration );
     517                                        meta.toggleClass( 'open' );
     518                                }
     519                        });
     520
     521                },
     522
     523                /**
     524                 * Get the sections that are associated with this panel, sorted by their priority Value.
     525                 *
     526                 * @returns {Array}
     527                 */
     528                sections: function () {
     529                        return this._children( 'panel', 'section' );
     530                },
     531
     532                /**
     533                 * Return whether this panel has any active sections.
     534                 *
     535                 * @returns {boolean}
     536                 */
     537                isContextuallyActive: function () {
     538                        var panel = this,
     539                                sections = panel.sections(),
     540                                activeCount = 0;
     541                        _( sections ).each( function ( section ) {
     542                                if ( section.active() && section.isContextuallyActive() ) {
     543                                        activeCount += 1;
     544                                }
     545                        } );
     546                        return ( activeCount !== 0 );
     547                },
     548
     549                /**
     550                 * Update UI to reflect expanded state
     551                 *
     552                 * @param {Boolean} expanded
     553                 * @param {Object} args  merged with this.defaultExpandedArguments
     554                 */
     555                onChangeExpanded: function ( expanded, args ) {
     556
     557                        // Immediately call the complete callback if there were no changes
     558                        if ( args.unchanged ) {
     559                                if ( args.completeCallback ) {
     560                                        args.completeCallback();
     561                                }
     562                                return;
     563                        }
     564
     565                        // Note: there is a second argument 'args' passed
     566                        var position, scroll,
     567                                panel = this,
     568                                section = panel.container.closest( '.accordion-section' ),
     569                                overlay = section.closest( '.wp-full-overlay' ),
     570                                container = section.closest( '.accordion-container' ),
     571                                siblings = container.find( '.open' ),
     572                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
     573                                backBtn = overlay.find( '.control-panel-back' ),
     574                                panelTitle = section.find( '.accordion-section-title' ).first(),
     575                                content = section.find( '.control-panel-content' );
     576
     577                        if ( expanded ) {
     578
     579                                // Collapse any sibling sections/panels
     580                                api.section.each( function ( section ) {
     581                                        if ( ! section.panel() ) {
     582                                                section.collapse( { duration: 0 } );
     583                                        }
     584                                });
     585                                api.panel.each( function ( otherPanel ) {
     586                                        if ( panel !== otherPanel ) {
     587                                                otherPanel.collapse( { duration: 0 } );
     588                                        }
     589                                });
     590
     591                                content.show( 0, function() {
     592                                        position = content.offset().top;
     593                                        scroll = container.scrollTop();
     594                                        content.css( 'margin-top', ( 45 - position - scroll ) );
     595                                        section.addClass( 'current-panel' );
     596                                        overlay.addClass( 'in-sub-panel' );
     597                                        container.scrollTop( 0 );
     598                                        if ( args.completeCallback ) {
     599                                                args.completeCallback();
     600                                        }
    66601                                } );
    67                         }) );
     602                                topPanel.attr( 'tabindex', '-1' );
     603                                backBtn.attr( 'tabindex', '0' );
     604                                backBtn.focus();
     605                        } else {
     606                                siblings.removeClass( 'open' );
     607                                section.removeClass( 'current-panel' );
     608                                overlay.removeClass( 'in-sub-panel' );
     609                                content.delay( 180 ).hide( 0, function() {
     610                                        content.css( 'margin-top', 'inherit' ); // Reset
     611                                        if ( args.completeCallback ) {
     612                                                args.completeCallback();
     613                                        }
     614                                } );
     615                                topPanel.attr( 'tabindex', '0' );
     616                                backBtn.attr( 'tabindex', '-1' );
     617                                panelTitle.focus();
     618                                container.scrollTop( 0 );
     619                        }
     620                }
     621        });
     622
     623        /**
     624         * @constructor
     625         * @augments wp.customize.Class
     626         */
     627        api.Control = api.Class.extend({
     628                defaultActiveArguments: { duration: 'fast' },
     629
     630                initialize: function( id, options ) {
     631                        var control = this,
     632                                nodes, radios, settings;
     633
     634                        control.params = {};
     635                        $.extend( control, options || {} );
     636
     637                        control.id = id;
     638                        control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
     639                        control.templateSelector = 'customize-control-' + control.params.type + '-content';
     640                        control.container = control.params.content ? $( control.params.content ) : $( control.selector );
     641
     642                        control.deferred = {
     643                                ready: new $.Deferred()
     644                        };
     645                        control.section = new api.Value();
     646                        control.priority = new api.Value();
     647                        control.active = new api.Value();
     648                        control.activeArgumentsQueue = [];
    68649
    69650                        control.elements = [];
    70651
    71                         nodes  = this.container.find('[data-customize-setting-link]');
     652                        nodes  = control.container.find('[data-customize-setting-link]');
    72653                        radios = {};
    73654
    74655                        nodes.each( function() {
    75                                 var node = $(this),
     656                                var node = $( this ),
    76657                                        name;
    77658
    78                                 if ( node.is(':radio') ) {
    79                                         name = node.prop('name');
    80                                         if ( radios[ name ] )
     659                                if ( node.is( ':radio' ) ) {
     660                                        name = node.prop( 'name' );
     661                                        if ( radios[ name ] ) {
    81662                                                return;
     663                                        }
    82664
    83665                                        radios[ name ] = true;
    84666                                        node = nodes.filter( '[name="' + name + '"]' );
    85667                                }
    86668
    87                                 api( node.data('customizeSettingLink'), function( setting ) {
     669                                api( node.data( 'customizeSettingLink' ), function( setting ) {
    88670                                        var element = new api.Element( node );
    89671                                        control.elements.push( element );
    90672                                        element.sync( setting );
     
    93675                        });
    94676
    95677                        control.active.bind( function ( active ) {
    96                                 control.toggle( active );
     678                                var args = control.activeArgumentsQueue.shift();
     679                                args = $.extend( {}, control.defaultActiveArguments, args );
     680                                control.onChangeActive( active, args );
    97681                        } );
    98                         control.toggle( control.active() );
     682
     683                        control.section.set( control.params.section );
     684                        control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
     685                        control.active.set( control.params.active );
     686
     687                        bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
     688
     689                        // Associate this control with its settings when they are created
     690                        settings = $.map( control.params.settings, function( value ) {
     691                                return value;
     692                        });
     693                        api.apply( api, settings.concat( function () {
     694                                var key;
     695
     696                                control.settings = {};
     697                                for ( key in control.params.settings ) {
     698                                        control.settings[ key ] = api( control.params.settings[ key ] );
     699                                }
     700
     701                                control.setting = control.settings['default'] || null;
     702
     703                                control.embed();
     704                        }) );
     705
     706                        control.deferred.ready.done( function () {
     707                                control.ready();
     708                        });
     709                },
     710
     711                /**
     712                 *
     713                 */
     714                embed: function () {
     715                        var control = this,
     716                                inject;
     717
     718                        // Watch for changes to the section state
     719                        inject = function ( sectionId ) {
     720                                var parentContainer;
     721                                if ( ! sectionId ) { // @todo allow a control to be embeded without a section, for instance a control embedded in the frontend
     722                                        return;
     723                                }
     724                                // Wait for the section to be registered
     725                                api.section( sectionId, function ( section ) {
     726                                        // Wait for the section to be ready/initialized
     727                                        section.deferred.ready.done( function () {
     728                                                parentContainer = section.container.find( 'ul:first' );
     729                                                if ( ! control.container.parent().is( parentContainer ) ) {
     730                                                        parentContainer.append( control.container );
     731                                                        control.renderContent();
     732                                                }
     733                                                control.deferred.ready.resolve(); // @todo Better to use `embedded` instead of `ready`
     734                                        });
     735                                });
     736                        };
     737                        control.section.bind( inject );
     738                        inject( control.section.get() );
    99739                },
    100740
    101741                /**
     
    104744                ready: function() {},
    105745
    106746                /**
    107                  * Callback for change to the control's active state.
     747                 * Normal controls do not expand, so just expand its parent
    108748                 *
    109                  * Override function for custom behavior for the control being active/inactive.
     749                 * @param {Object} [params]
     750                 */
     751                expand: function ( params ) {
     752                        api.section( this.section() ).expand( params );
     753                },
     754
     755                /**
     756                 * Bring the containing section and panel into view and then this control into view, focusing on the first input
     757                 */
     758                focus: focus,
     759
     760                /**
     761                 * Update UI in response to a change in the control's active state.
     762                 * This does not change the active state, it merely handles the behavior
     763                 * for when it does change.
    110764                 *
    111765                 * @param {Boolean} active
     766                 * @param {Object} args  merged on top of this.defaultActiveArguments
    112767                 */
    113                 toggle: function ( active ) {
     768                onChangeActive: function ( active, args ) {
    114769                        if ( active ) {
    115                                 this.container.slideDown();
     770                                this.container.slideDown( args.duration, args.completeCallback );
    116771                        } else {
    117                                 this.container.slideUp();
     772                                this.container.slideUp( args.duration, args.completeCallback );
    118773                        }
    119774                },
    120775
     776                /**
     777                 * @deprecated alias of onChangeActive
     778                 */
     779                toggle: function ( active ) {
     780                        return this.onChangeActive( active, this.defaultActiveArguments );
     781                },
     782
     783                /**
     784                 * Shorthand way to enable the active state.
     785                 *
     786                 * @param {Object} [params]
     787                 * @returns {Boolean} false if already active
     788                 */
     789                activate: Container.prototype.activate,
     790
     791                /**
     792                 * Shorthand way to disable the active state.
     793                 *
     794                 * @param {Object} [params]
     795                 * @returns {Boolean} false if already inactive
     796                 */
     797                deactivate: Container.prototype.deactivate,
     798
    121799                dropdownInit: function() {
    122800                        var control      = this,
    123801                                statuses     = this.container.find('.dropdown-status'),
     
    132810
    133811                        // Support the .dropdown class to open/close complex elements
    134812                        this.container.on( 'click keydown', '.dropdown', function( event ) {
    135                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
     813                                if ( isKeydownButNotEnterEvent( event ) ) {
    136814                                        return;
     815                                }
    137816
    138817                                event.preventDefault();
    139818
     
    157836                /**
    158837                 * Render the control from its JS template, if it exists.
    159838                 *
    160                  * The control's container must alreasy exist in the DOM.
     839                 * The control's container must already exist in the DOM.
    161840                 */
    162                 renderContent: function( callback ) {
     841                renderContent: function () {
    163842                        var template,
    164                                 selector = 'customize-control-' + this.params.type + '-content';
     843                                control = this;
    165844
    166                         callback = callback || function(){};
    167                         if ( 0 !== $( '#tmpl-' + selector ).length ) {
    168                                 template = wp.template( selector );
    169                                 if ( template && this.container ) {
    170                                         this.container.append( template( this.params ) );
     845                        if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
     846                                template = wp.template( control.templateSelector );
     847                                if ( template && control.container ) {
     848                                        control.container.append( template( control.params ) );
    171849                                }
    172850                        }
    173                         callback();
    174851                }
    175852        });
    176853
     
    234911
    235912                        this.remover = this.container.find('.remove');
    236913                        this.remover.on( 'click keydown', function( event ) {
    237                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
     914                                if ( isKeydownButNotEnterEvent( event ) ) {
    238915                                        return;
     916                                }
    239917
    240918                                control.setting.set( control.params.removed );
    241919                                event.preventDefault();
     
    306984
    307985                        // Bind tab switch events
    308986                        this.library.children('ul').on( 'click keydown', 'li', function( event ) {
    309                                 if ( event.type === 'keydown' &&  13 !== event.which ) // enter
     987                                if ( isKeydownButNotEnterEvent( event ) ) {
    310988                                        return;
     989                                }
    311990
    312991                                var id  = $(this).data('customizeTab'),
    313992                                        tab = control.tabs[ id ];
     
    3241003
    3251004                        // Bind events to switch image urls.
    3261005                        this.library.on( 'click keydown', 'a', function( event ) {
    327                                 if ( event.type === 'keydown' && 13 !== event.which ) // enter
     1006                                if ( isKeydownButNotEnterEvent( event ) ) {
    3281007                                        return;
     1008                                }
    3291009
    3301010                                var value = $(this).data('customizeImageValue');
    3311011
     
    5971277
    5981278        // Create the collection of Control objects.
    5991279        api.control = new api.Values({ defaultConstructor: api.Control });
     1280        api.section = new api.Values({ defaultConstructor: api.Section });
     1281        api.panel = new api.Values({ defaultConstructor: api.Panel });
    6001282
    6011283        /**
    6021284         * @constructor
     
    6321314                                loaded = false,
    6331315                                ready  = false;
    6341316
    635                         if ( this._ready )
     1317                        if ( this._ready ) {
    6361318                                this.unbind( 'ready', this._ready );
     1319                        }
    6371320
    6381321                        this._ready = function() {
    6391322                                ready = true;
    6401323
    641                                 if ( loaded )
     1324                                if ( loaded ) {
    6421325                                        deferred.resolveWith( self );
     1326                                }
    6431327                        };
    6441328
    6451329                        this.bind( 'ready', this._ready );
    6461330
    6471331                        this.bind( 'ready', function ( data ) {
    648                                 if ( ! data || ! data.activeControls ) {
     1332                                if ( ! data ) {
    6491333                                        return;
    6501334                                }
    6511335
    652                                 $.each( data.activeControls, function ( id, active ) {
    653                                         var control = api.control( id );
    654                                         if ( control ) {
    655                                                 control.active( active );
     1336                                var constructs = {
     1337                                        panel: data.activePanels,
     1338                                        section: data.activeSections,
     1339                                        control: data.activeControls
     1340                                };
     1341
     1342                                $.each( constructs, function ( type, activeConstructs ) {
     1343                                        if ( activeConstructs ) {
     1344                                                $.each( activeConstructs, function ( id, active ) {
     1345                                                        var construct = api[ type ]( id );
     1346                                                        if ( construct ) {
     1347                                                                construct.active( active );
     1348                                                        }
     1349                                                } );
    6561350                                        }
    6571351                                } );
     1352
    6581353                        } );
    6591354
    6601355                        this.request = $.ajax( this.previewUrl(), {
     
    6761371
    6771372                                // Check if the location response header differs from the current URL.
    6781373                                // If so, the request was redirected; try loading the requested page.
    679                                 if ( location && location != self.previewUrl() ) {
     1374                                if ( location && location !== self.previewUrl() ) {
    6801375                                        deferred.rejectWith( self, [ 'redirect', location ] );
    6811376                                        return;
    6821377                                }
     
    8031498                                rscheme = /^https?/;
    8041499
    8051500                        $.extend( this, options || {} );
     1501                        this.deferred = {
     1502                                active: $.Deferred()
     1503                        };
    8061504
    8071505                        /*
    8081506                         * Wrap this.refresh to prevent it from hammering the servers:
     
    9341632                                        self.targetWindow( this.targetWindow() );
    9351633                                        self.channel( this.channel() );
    9361634
     1635                                        self.deferred.active.resolve();
    9371636                                        self.send( 'active' );
    9381637                                });
    9391638
     
    10011700                image:  api.ImageControl,
    10021701                header: api.HeaderControl
    10031702        };
     1703        api.panelConstructor = {};
     1704        api.sectionConstructor = {};
    10041705
    10051706        $( function() {
    10061707                api.settings = window._wpCustomizeSettings;
     
    10311732                        }
    10321733                });
    10331734
     1735                // Expand/Collapse the main customizer customize info
     1736                $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
     1737                        if ( isKeydownButNotEnterEvent( event ) ) {
     1738                                return;
     1739                        }
     1740                        event.preventDefault(); // Keep this AFTER the key filter above
     1741
     1742                        var section = $( this ).parent(),
     1743                                content = section.find( '.accordion-section-content:first' );
     1744
     1745                        if ( section.hasClass( 'cannot-expand' ) ) {
     1746                                return;
     1747                        }
     1748
     1749                        if ( section.hasClass( 'open' ) ) {
     1750                                section.toggleClass( 'open' );
     1751                                content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
     1752                        } else {
     1753                                content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
     1754                                section.toggleClass( 'open' );
     1755                        }
     1756                });
     1757
    10341758                // Initialize Previewer
    10351759                api.previewer = new api.Previewer({
    10361760                        container:   '#customize-preview',
     
    11241848                        $.extend( this.nonce, nonce );
    11251849                });
    11261850
     1851                // Create Settings
    11271852                $.each( api.settings.settings, function( id, data ) {
    11281853                        api.create( id, id, data.value, {
    11291854                                transport: data.transport,
     
    11311856                        } );
    11321857                });
    11331858
     1859                // Create Panels
     1860                $.each( api.settings.panels, function ( id, data ) {
     1861                        var constructor = api.panelConstructor[ data.type ] || api.Panel,
     1862                                panel;
     1863
     1864                        panel = new constructor( id, {
     1865                                params: data
     1866                        } );
     1867                        api.panel.add( id, panel );
     1868                });
     1869
     1870                // Create Sections
     1871                $.each( api.settings.sections, function ( id, data ) {
     1872                        var constructor = api.sectionConstructor[ data.type ] || api.Section,
     1873                                section;
     1874
     1875                        section = new constructor( id, {
     1876                                params: data
     1877                        } );
     1878                        api.section.add( id, section );
     1879                });
     1880
     1881                // Create Controls
    11341882                $.each( api.settings.controls, function( id, data ) {
    11351883                        var constructor = api.controlConstructor[ data.type ] || api.Control,
    11361884                                control;
    11371885
    1138                         control = api.control.add( id, new constructor( id, {
     1886                        control = new constructor( id, {
    11391887                                params: data,
    11401888                                previewer: api.previewer
    1141                         } ) );
     1889                        } );
     1890                        api.control.add( id, control );
     1891                });
     1892
     1893                // Focus the autofocused element
     1894                _.each( [ 'panel', 'section', 'control' ], function ( type ) {
     1895                        var instance, id = api.settings.autofocus[ type ];
     1896                        if ( id && api[ type ]( id ) ) {
     1897                                instance = api[ type ]( id );
     1898                                // Wait until the element is embedded in the DOM
     1899                                instance.deferred.ready.done( function () {
     1900                                        // Wait until the preview has activated and so active panels, sections, controls have been set
     1901                                        api.previewer.deferred.active.done( function () {
     1902                                                instance.focus();
     1903                                        });
     1904                                });
     1905                        }
    11421906                });
    11431907
     1908                /**
     1909                 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
     1910                 */
     1911                api.reflowPaneContents = _.bind( function () {
     1912
     1913                        var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
     1914
     1915                        if ( document.activeElement ) {
     1916                                activeElement = $( document.activeElement );
     1917                        }
     1918
     1919                        // Sort the sections within each panel
     1920                        api.panel.each( function ( panel ) {
     1921                                var sections = panel.sections(),
     1922                                        sectionContainers = _.pluck( sections, 'container' );
     1923                                rootNodes.push( panel );
     1924                                appendContainer = panel.container.find( 'ul:first' );
     1925                                if ( ! areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
     1926                                        _( sections ).each( function ( section ) {
     1927                                                appendContainer.append( section.container );
     1928                                        } );
     1929                                        wasReflowed = true;
     1930                                }
     1931                        } );
     1932
     1933                        // Sort the controls within each section
     1934                        api.section.each( function ( section ) {
     1935                                var controls = section.controls(),
     1936                                        controlContainers = _.pluck( controls, 'container' );
     1937                                if ( ! section.panel() ) {
     1938                                        rootNodes.push( section );
     1939                                }
     1940                                appendContainer = section.container.find( 'ul:first' );
     1941                                if ( ! areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
     1942                                        _( controls ).each( function ( control ) {
     1943                                                appendContainer.append( control.container );
     1944                                        } );
     1945                                        wasReflowed = true;
     1946                                }
     1947                        } );
     1948
     1949                        // Sort the root panels and sections
     1950                        rootNodes.sort( function ( a, b ) {
     1951                                return a.priority() - b.priority();
     1952                        } );
     1953                        rootContainers = _.pluck( rootNodes, 'container' );
     1954                        appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
     1955                        if ( ! areElementListsEqual( rootContainers, appendContainer.children() ) ) {
     1956                                _( rootNodes ).each( function ( rootNode ) {
     1957                                        appendContainer.append( rootNode.container );
     1958                                } );
     1959                                wasReflowed = true;
     1960                        }
     1961
     1962                        // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
     1963                        api.panel.each( function ( panel ) {
     1964                                var value = panel.active();
     1965                                panel.active.callbacks.fireWith( panel.active, [ value, value ] );
     1966                        } );
     1967                        api.section.each( function ( section ) {
     1968                                var value = section.active();
     1969                                section.active.callbacks.fireWith( section.active, [ value, value ] );
     1970                        } );
     1971
     1972                        // Restore focus if there was a reflow and there was an active (focused) element
     1973                        if ( wasReflowed && activeElement ) {
     1974                                activeElement.focus();
     1975                        }
     1976                }, api );
     1977                api.bind( 'ready', api.reflowPaneContents );
     1978                api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
     1979                $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
     1980                        values.bind( 'add', api.reflowPaneContents );
     1981                        values.bind( 'change', api.reflowPaneContents );
     1982                        values.bind( 'remove', api.reflowPaneContents );
     1983                } );
     1984
    11441985                // Check if preview url is valid and load the preview frame.
    11451986                if ( api.previewer.previewUrl() ) {
    11461987                        api.previewer.refresh();
     
    12052046                        event.preventDefault();
    12062047                });
    12072048
     2049                // Go back to the top-level Customizer accordion.
     2050                $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) {
     2051                        if ( isKeydownButNotEnterEvent( event ) ) {
     2052                                return;
     2053                        }
     2054
     2055                        event.preventDefault(); // Keep this AFTER the key filter above
     2056                        api.panel.each( function ( panel ) {
     2057                                panel.collapse();
     2058                        });
     2059                });
     2060
    12082061                closeBtn.keydown( function( event ) {
    12092062                        if ( 9 === event.which ) // tab
    12102063                                return;
     
    12192072                });
    12202073
    12212074                $('.collapse-sidebar').on( 'click keydown', function( event ) {
    1222                         if ( event.type === 'keydown' &&  13 !== event.which ) // enter
     2075                        if ( isKeydownButNotEnterEvent( event ) ) {
    12232076                                return;
     2077                        }
    12242078
    12252079                        overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
    12262080                        event.preventDefault();
  • src/wp-admin/js/customize-widgets.js

    diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
    index 6be9a08..8112f3e 100644
     
    404404         * @augments wp.customize.Control
    405405         */
    406406        api.Widgets.WidgetControl = api.Control.extend({
     407                defaultExpandedArguments: {
     408                        duration: 'fast'
     409                },
     410
     411                initialize: function ( id, options ) {
     412                        var control = this;
     413                        api.Control.prototype.initialize.call( control, id, options );
     414                        control.expanded = new api.Value();
     415                        control.expandedArgumentsQueue = [];
     416                        control.expanded.bind( function ( expanded ) {
     417                                var args = control.expandedArgumentsQueue.shift();
     418                                args = $.extend( {}, control.defaultExpandedArguments, args );
     419                                control.onChangeExpanded( expanded, args );
     420                        });
     421                        control.expanded.set( false );
     422                },
     423
    407424                /**
    408425                 * Set up the control
    409426                 */
     
    529546                                if ( sidebarWidgetsControl.isReordering ) {
    530547                                        return;
    531548                                }
    532                                 self.toggleForm();
     549                                self.expanded( ! self.expanded() );
    533550                        } );
    534551
    535552                        $closeBtn = this.container.find( '.widget-control-close' );
    536553                        $closeBtn.on( 'click', function( e ) {
    537554                                e.preventDefault();
    538                                 self.collapseForm();
     555                                self.collapse();
    539556                                self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
    540557                        } );
    541558                },
     
    777794                 * Overrides api.Control.toggle()
    778795                 *
    779796                 * @param {Boolean} active
     797                 * @param {Object} args
    780798                 */
    781                 toggle: function ( active ) {
     799                onChangeActive: function ( active, args ) {
     800                        // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
    782801                        this.container.toggleClass( 'widget-rendered', active );
     802                        if ( args.completeCallback ) {
     803                                args.completeCallback();
     804                        }
    783805                },
    784806
    785807                /**
     
    11011123                 * Expand the accordion section containing a control
    11021124                 */
    11031125                expandControlSection: function() {
    1104                         var $section = this.container.closest( '.accordion-section' );
    1105 
    1106                         if ( ! $section.hasClass( 'open' ) ) {
    1107                                 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
    1108                         }
     1126                        api.Control.prototype.expand.call( this );
    11091127                },
    11101128
    11111129                /**
     1130                 * @param {Boolean} expanded
     1131                 * @param {Object} [params]
     1132                 * @returns {Boolean} false if state already applied
     1133                 */
     1134                _toggleExpanded: api.Section.prototype._toggleExpanded,
     1135
     1136                /**
     1137                 * @param {Object} [params]
     1138                 * @returns {Boolean} false if already expanded
     1139                 */
     1140                expand: api.Section.prototype.expand,
     1141
     1142                /**
    11121143                 * Expand the widget form control
     1144                 *
     1145                 * @deprecated alias of expand()
    11131146                 */
    11141147                expandForm: function() {
    1115                         this.toggleForm( true );
     1148                        this.expand();
    11161149                },
    11171150
    11181151                /**
     1152                 * @param {Object} [params]
     1153                 * @returns {Boolean} false if already collapsed
     1154                 */
     1155                collapse: api.Section.prototype.collapse,
     1156
     1157                /**
    11191158                 * Collapse the widget form control
     1159                 *
     1160                 * @deprecated alias of expand()
    11201161                 */
    11211162                collapseForm: function() {
    1122                         this.toggleForm( false );
     1163                        this.collapse();
    11231164                },
    11241165
    11251166                /**
    11261167                 * Expand or collapse the widget control
    11271168                 *
     1169                 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
     1170                 *
    11281171                 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
    11291172                 */
    11301173                toggleForm: function( showOrHide ) {
    1131                         var self = this, $widget, $inside, complete;
    1132 
    1133                         $widget = this.container.find( 'div.widget:first' );
    1134                         $inside = $widget.find( '.widget-inside:first' );
    11351174                        if ( typeof showOrHide === 'undefined' ) {
    1136                                 showOrHide = ! $inside.is( ':visible' );
     1175                                showOrHide = ! this.expanded();
    11371176                        }
     1177                        this.expanded( showOrHide );
     1178                },
    11381179
    1139                         // Already expanded or collapsed, so noop
    1140                         if ( $inside.is( ':visible' ) === showOrHide ) {
     1180                /**
     1181                 * Respond to change in the expanded state.
     1182                 *
     1183                 * @param {Boolean} expanded
     1184                 * @param {Object} args  merged on top of this.defaultActiveArguments
     1185                 */
     1186                onChangeExpanded: function ( expanded, args ) {
     1187                        var self = this, $widget, $inside, complete, prevComplete;
     1188
     1189                        // If the expanded state is unchanged only manipulate container expanded states
     1190                        if ( args.unchanged ) {
     1191                                if ( expanded ) {
     1192                                        api.Control.prototype.expand.call( self, {
     1193                                                completeCallback:  args.completeCallback
     1194                                        });
     1195                                }
    11411196                                return;
    11421197                        }
    11431198
    1144                         if ( showOrHide ) {
     1199                        $widget = this.container.find( 'div.widget:first' );
     1200                        $inside = $widget.find( '.widget-inside:first' );
     1201
     1202                        if ( expanded ) {
     1203
     1204                                self.expandControlSection();
     1205
    11451206                                // Close all other widget controls before expanding this one
    11461207                                api.control.each( function( otherControl ) {
    11471208                                        if ( self.params.type === otherControl.params.type && self !== otherControl ) {
    1148                                                 otherControl.collapseForm();
     1209                                                otherControl.collapse();
    11491210                                        }
    11501211                                } );
    11511212
     
    11541215                                        self.container.addClass( 'expanded' );
    11551216                                        self.container.trigger( 'expanded' );
    11561217                                };
     1218                                if ( args.completeCallback ) {
     1219                                        prevComplete = complete;
     1220                                        complete = function () {
     1221                                                prevComplete();
     1222                                                args.completeCallback();
     1223                                        };
     1224                                }
    11571225
    11581226                                if ( self.params.is_wide ) {
    1159                                         $inside.fadeIn( 'fast', complete );
     1227                                        $inside.fadeIn( args.duration, complete );
    11601228                                } else {
    1161                                         $inside.slideDown( 'fast', complete );
     1229                                        $inside.slideDown( args.duration, complete );
    11621230                                }
    11631231
    11641232                                self.container.trigger( 'expand' );
    11651233                                self.container.addClass( 'expanding' );
    11661234                        } else {
     1235
    11671236                                complete = function() {
    11681237                                        self.container.removeClass( 'collapsing' );
    11691238                                        self.container.removeClass( 'expanded' );
    11701239                                        self.container.trigger( 'collapsed' );
    11711240                                };
     1241                                if ( args.completeCallback ) {
     1242                                        prevComplete = complete;
     1243                                        complete = function () {
     1244                                                prevComplete();
     1245                                                args.completeCallback();
     1246                                        };
     1247                                }
    11721248
    11731249                                self.container.trigger( 'collapse' );
    11741250                                self.container.addClass( 'collapsing' );
    11751251
    11761252                                if ( self.params.is_wide ) {
    1177                                         $inside.fadeOut( 'fast', complete );
     1253                                        $inside.fadeOut( args.duration, complete );
    11781254                                } else {
    1179                                         $inside.slideUp( 'fast', function() {
     1255                                        $inside.slideUp( args.duration, function() {
    11801256                                                $widget.css( { width:'', margin:'' } );
    11811257                                                complete();
    11821258                                        } );
     
    11851261                },
    11861262
    11871263                /**
    1188                  * Expand the containing sidebar section, expand the form, and focus on
    1189                  * the first input in the control
    1190                  */
    1191                 focus: function() {
    1192                         this.expandControlSection();
    1193                         this.expandForm();
    1194                         this.container.find( '.widget-content :focusable:first' ).focus();
    1195                 },
    1196 
    1197                 /**
    11981264                 * Get the position (index) of the widget in the containing sidebar
    11991265                 *
    12001266                 * @returns {Number}
     
    13041370         * @augments wp.customize.Control
    13051371         */
    13061372        api.Widgets.SidebarControl = api.Control.extend({
     1373
    13071374                /**
    13081375                 * Set up the control
    13091376                 */
     
    13251392                                registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
    13261393
    13271394                        this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
    1328                                 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
     1395                                var widgetFormControls, removedWidgetIds, priority;
    13291396
    13301397                                removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
    13311398
     
    13501417                                widgetFormControls.sort( function( a, b ) {
    13511418                                        var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
    13521419                                                bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
     1420                                        return aIndex - bIndex;
     1421                                });
    13531422
    1354                                         if ( aIndex === bIndex ) {
    1355                                                 return 0;
    1356                                         }
    1357 
    1358                                         return aIndex < bIndex ? -1 : 1;
    1359                                 } );
    1360 
    1361                                 // Append the controls to put them in the right order
    1362                                 finalControlContainers = _( widgetFormControls ).map( function( widgetFormControls ) {
    1363                                         return widgetFormControls.container[0];
    1364                                 } );
    1365 
    1366                                 $sidebarWidgetsAddControl = self.$sectionContent.find( '.customize-control-sidebar_widgets' );
    1367                                 $sidebarWidgetsAddControl.before( finalControlContainers );
     1423                                priority = 0;
     1424                                _( widgetFormControls ).each( function ( control ) {
     1425                                        control.priority( priority );
     1426                                        control.section( self.section() );
     1427                                        priority += 1;
     1428                                });
     1429                                self.priority( priority ); // Make sure sidebar control remains at end
    13681430
    13691431                                // Re-sort widget form controls (including widgets form other sidebars newly moved here)
    13701432                                self._applyCardinalOrderClassNames();
     
    14341496                        // Update the model with whether or not the sidebar is rendered
    14351497                        self.active.bind( function ( active ) {
    14361498                                registeredSidebar.set( 'is_rendered', active );
     1499                                api.section( self.section.get() ).active( active );
    14371500                        } );
    1438                 },
    1439 
    1440                 /**
    1441                  * Show the sidebar section when it becomes visible.
    1442                  *
    1443                  * Overrides api.Control.toggle()
    1444                  *
    1445                  * @param {Boolean} active
    1446                  */
    1447                 toggle: function ( active ) {
    1448                         var $section, sectionSelector;
    1449 
    1450                         sectionSelector = '#accordion-section-sidebar-widgets-' + this.params.sidebar_id;
    1451                         $section = $( sectionSelector );
    1452 
    1453                         if ( active ) {
    1454                                 $section.stop().slideDown( function() {
    1455                                         $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
    1456                                 } );
    1457 
    1458                         } else {
    1459                                 // Make sure that hidden sections get closed first
    1460                                 if ( $section.hasClass( 'open' ) ) {
    1461                                         // it would be nice if accordionSwitch() in accordion.js was public
    1462                                         $section.find( '.accordion-section-title' ).trigger( 'click' );
    1463                                 }
    1464 
    1465                                 $section.stop().slideUp();
    1466                         }
     1501                        api.section( self.section.get() ).active( self.active() );
    14671502                },
    14681503
    14691504                /**
     
    15001535                        this.$controlSection.find( '.accordion-section-title' ).droppable({
    15011536                                accept: '.customize-control-widget_form',
    15021537                                over: function() {
    1503                                         if ( ! self.$controlSection.hasClass( 'open' ) ) {
    1504                                                 self.$controlSection.addClass( 'open' );
    1505                                                 self.$sectionContent.toggle( false ).slideToggle( 150, function() {
    1506                                                         self.$sectionContent.sortable( 'refreshPositions' );
    1507                                                 } );
    1508                                         }
     1538                                        var section = api.section( self.section.get() );
     1539                                        section.expand({
     1540                                                allowMultiple: true, // Prevent the section being dragged from to be collapsed
     1541                                                completeCallback: function () {
     1542                                                        // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
     1543                                                        api.section.each( function ( otherSection ) {
     1544                                                                if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
     1545                                                                        otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
     1546                                                                }
     1547                                                        } );
     1548                                                }
     1549                                        });
    15091550                                }
    15101551                        });
    15111552
     
    15481589                 * Add classes to the widget_form controls to assist with styling
    15491590                 */
    15501591                _applyCardinalOrderClassNames: function() {
    1551                         this.$sectionContent.find( '.customize-control-widget_form' )
    1552                                 .removeClass( 'first-widget' )
    1553                                 .removeClass( 'last-widget' )
    1554                                 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
     1592                        var widgetControls = [];
     1593                        _.each( this.setting(), function ( widgetId ) {
     1594                                var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
     1595                                if ( widgetControl ) {
     1596                                        widgetControls.push( widgetControl );
     1597                                }
     1598                        });
     1599
     1600                        if ( ! widgetControls.length ) {
     1601                                return;
     1602                        }
    15551603
    1556                         this.$sectionContent.find( '.customize-control-widget_form:first' )
     1604                        $( widgetControls ).each( function () {
     1605                                $( this.container )
     1606                                        .removeClass( 'first-widget' )
     1607                                        .removeClass( 'last-widget' )
     1608                                        .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
     1609                        });
     1610
     1611                        _.first( widgetControls ).container
    15571612                                .addClass( 'first-widget' )
    15581613                                .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
    15591614
    1560                         this.$sectionContent.find( '.customize-control-widget_form:last' )
     1615                        _.last( widgetControls ).container
    15611616                                .addClass( 'last-widget' )
    15621617                                .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
    15631618                },
     
    15711626                 * Enable/disable the reordering UI
    15721627                 *
    15731628                 * @param {Boolean} showOrHide to enable/disable reordering
     1629                 *
     1630                 * @todo We should have a reordering state instead and rename this to onChangeReordering
    15741631                 */
    15751632                toggleReordering: function( showOrHide ) {
    15761633                        showOrHide = Boolean( showOrHide );
     
    15841641
    15851642                        if ( showOrHide ) {
    15861643                                _( this.getWidgetFormControls() ).each( function( formControl ) {
    1587                                         formControl.collapseForm();
     1644                                        formControl.collapse();
    15881645                                } );
    15891646
    15901647                                this.$sectionContent.find( '.first-widget .move-widget' ).focus();
     
    16191676                 * @returns {object|false} widget_form control instance, or false on error
    16201677                 */
    16211678                addWidget: function( widgetId ) {
    1622                         var self = this, controlHtml, $widget, controlType = 'widget_form', $control, controlConstructor,
     1679                        var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
    16231680                                parsedWidgetId = parseWidgetId( widgetId ),
    16241681                                widgetNumber = parsedWidgetId.number,
    16251682                                widgetIdBase = parsedWidgetId.id_base,
     
    16511708
    16521709                        $widget = $( controlHtml );
    16531710
    1654                         $control = $( '<li/>' )
     1711                        controlContainer = $( '<li/>' )
    16551712                                .addClass( 'customize-control' )
    16561713                                .addClass( 'customize-control-' + controlType )
    16571714                                .append( $widget );
    16581715
    16591716                        // Remove icon which is visible inside the panel
    1660                         $control.find( '> .widget-icon' ).remove();
     1717                        controlContainer.find( '> .widget-icon' ).remove();
    16611718
    16621719                        if ( widget.get( 'is_multi' ) ) {
    1663                                 $control.find( 'input[name="widget_number"]' ).val( widgetNumber );
    1664                                 $control.find( 'input[name="multi_number"]' ).val( widgetNumber );
     1720                                controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
     1721                                controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
    16651722                        }
    16661723
    1667                         widgetId = $control.find( '[name="widget-id"]' ).val();
     1724                        widgetId = controlContainer.find( '[name="widget-id"]' ).val();
    16681725
    1669                         $control.hide(); // to be slid-down below
     1726                        controlContainer.hide(); // to be slid-down below
    16701727
    16711728                        settingId = 'widget_' + widget.get( 'id_base' );
    16721729                        if ( widget.get( 'is_multi' ) ) {
    16731730                                settingId += '[' + widgetNumber + ']';
    16741731                        }
    1675                         $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
    1676 
    1677                         this.container.after( $control );
     1732                        controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
    16781733
    16791734                        // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
    16801735                        isExistingWidget = api.has( settingId );
     
    16921747                                        settings: {
    16931748                                                'default': settingId
    16941749                                        },
     1750                                        content: controlContainer,
    16951751                                        sidebar_id: self.params.sidebar_id,
    16961752                                        widget_id: widgetId,
    16971753                                        widget_id_base: widget.get( 'id_base' ),
     
    17311787                                this.setting( sidebarWidgets );
    17321788                        }
    17331789
    1734                         $control.slideDown( function() {
     1790                        controlContainer.slideDown( function() {
    17351791                                if ( isExistingWidget ) {
    1736                                         widgetFormControl.expandForm();
     1792                                        widgetFormControl.expand();
    17371793                                        widgetFormControl.updateWidget( {
    17381794                                                instance: widgetFormControl.setting(),
    17391795                                                complete: function( error ) {
  • src/wp-includes/class-wp-customize-control.php

    diff --git src/wp-includes/class-wp-customize-control.php src/wp-includes/class-wp-customize-control.php
    index cbb7cdd..8f24601 100644
    class WP_Customize_Control { 
    7474        public $input_attrs = array();
    7575
    7676        /**
     77         * @deprecated It is better to just call the json() method
    7778         * @access public
    7879         * @var array
    7980         */
    class WP_Customize_Control { 
    218219                }
    219220
    220221                $this->json['type']        = $this->type;
     222                $this->json['priority']    = $this->priority;
     223                $this->json['active']      = $this->active();
     224                $this->json['section']     = $this->section;
     225                $this->json['content']     = $this->get_content();
    221226                $this->json['label']       = $this->label;
    222227                $this->json['description'] = $this->description;
    223                 $this->json['active']      = $this->active();
     228        }
     229
     230        /**
     231         * Get the data to export to the client via JSON.
     232         *
     233         * @since 4.1.0
     234         *
     235         * @return array
     236         */
     237        public function json() {
     238                $this->to_json();
     239                return $this->json;
    224240        }
    225241
    226242        /**
    class WP_Customize_Control { 
    244260        }
    245261
    246262        /**
     263         * Get the control's content for insertion into the Customizer pane.
     264         *
     265         * @since 4.1.0
     266         *
     267         * @return string
     268         */
     269        public final function get_content() {
     270                ob_start();
     271                $this->maybe_render();
     272                $template = trim( ob_get_contents() );
     273                ob_end_clean();
     274                return $template;
     275        }
     276
     277        /**
    247278         * Check capabilities and render the control.
    248279         *
    249280         * @since 3.4.0
    class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control { 
    10711102/**
    10721103 * Widget Area Customize Control Class
    10731104 *
     1105 * @since 3.9.0
    10741106 */
    10751107class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
    10761108        public $type = 'sidebar_widgets';
    class WP_Widget_Area_Customize_Control extends WP_Customize_Control { 
    11121144
    11131145/**
    11141146 * Widget Form Customize Control Class
     1147 *
     1148 * @since 3.9.0
    11151149 */
    11161150class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
    11171151        public $type = 'widget_form';
  • src/wp-includes/class-wp-customize-manager.php

    diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
    index bcbf127..4d4e73e 100644
    final class WP_Customize_Manager { 
    498498                $settings = array(
    499499                        'values'  => array(),
    500500                        'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
     501                        'activePanels' => array(),
     502                        'activeSections' => array(),
    501503                        'activeControls' => array(),
    502504                );
    503505
    final class WP_Customize_Manager { 
    511513                foreach ( $this->settings as $id => $setting ) {
    512514                        $settings['values'][ $id ] = $setting->js_value();
    513515                }
     516                foreach ( $this->panels as $id => $panel ) {
     517                        $settings['activePanels'][ $id ] = $panel->active();
     518                }
     519                foreach ( $this->sections as $id => $section ) {
     520                        $settings['activeSections'][ $id ] = $section->active();
     521                }
    514522                foreach ( $this->controls as $id => $control ) {
    515523                        $settings['activeControls'][ $id ] = $control->active();
    516524                }
    final class WP_Customize_Manager { 
    911919
    912920                        if ( ! $section->panel ) {
    913921                                // Top-level section.
    914                                 $sections[] = $section;
     922                                $sections[ $section->id ] = $section;
    915923                        } else {
    916924                                // This section belongs to a panel.
    917925                                if ( isset( $this->panels [ $section->panel ] ) ) {
    918                                         $this->panels[ $section->panel ]->sections[] = $section;
     926                                        $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
    919927                                }
    920928                        }
    921929                }
    final class WP_Customize_Manager { 
    932940                                continue;
    933941                        }
    934942
    935                         usort( $panel->sections, array( $this, '_cmp_priority' ) );
    936                         $panels[] = $panel;
     943                        uasort( $panel->sections, array( $this, '_cmp_priority' ) );
     944                        $panels[ $panel->id ] = $panel;
    937945                }
    938946                $this->panels = $panels;
    939947
  • src/wp-includes/class-wp-customize-panel.php

    diff --git src/wp-includes/class-wp-customize-panel.php src/wp-includes/class-wp-customize-panel.php
    index f289cb7..201c4b9 100644
    class WP_Customize_Panel { 
    8383        public $sections;
    8484
    8585        /**
     86         * @since 4.1.0
     87         * @access public
     88         * @var string
     89         */
     90        public $type;
     91
     92        /**
     93         * Callback.
     94         *
     95         * @since 4.1.0
     96         * @access public
     97         *
     98         * @see WP_Customize_Section::active()
     99         *
     100         * @var callable Callback is called with one argument, the instance of
     101         *               WP_Customize_Section, and returns bool to indicate whether
     102         *               the section is active (such as it relates to the URL
     103         *               currently being previewed).
     104         */
     105        public $active_callback = '';
     106
     107        /**
    86108         * Constructor.
    87109         *
    88110         * Any supplied $args override class property defaults.
    class WP_Customize_Panel { 
    103125
    104126                $this->manager = $manager;
    105127                $this->id = $id;
     128                if ( empty( $this->active_callback ) ) {
     129                        $this->active_callback = array( $this, 'active_callback' );
     130                }
    106131
    107132                $this->sections = array(); // Users cannot customize the $sections array.
    108133
    class WP_Customize_Panel { 
    110135        }
    111136
    112137        /**
     138         * Check whether panel is active to current Customizer preview.
     139         *
     140         * @since 4.1.0
     141         * @access public
     142         *
     143         * @return bool Whether the panel is active to the current preview.
     144         */
     145        public final function active() {
     146                $panel = $this;
     147                $active = call_user_func( $this->active_callback, $this );
     148
     149                /**
     150                 * Filter response of WP_Customize_Panel::active().
     151                 *
     152                 * @since 4.1.0
     153                 *
     154                 * @param bool                 $active  Whether the Customizer panel is active.
     155                 * @param WP_Customize_Panel $panel WP_Customize_Panel instance.
     156                 */
     157                $active = apply_filters( 'customize_panel_active', $active, $panel );
     158
     159                return $active;
     160        }
     161
     162        /**
     163         * Default callback used when invoking WP_Customize_Panel::active().
     164         *
     165         * Subclasses can override this with their specific logic, or they may
     166         * provide an 'active_callback' argument to the constructor.
     167         *
     168         * @since 4.1.0
     169         * @access public
     170         *
     171         * @return bool Always true.
     172         */
     173        public function active_callback() {
     174                return true;
     175        }
     176
     177        /**
     178         * Gather the parameters passed to client JavaScript via JSON.
     179         *
     180         * @since 4.1.0
     181         *
     182         * @return array The array to be exported to the client as JSON
     183         */
     184        public function json() {
     185                $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) );
     186                $array['content'] = $this->get_content();
     187                $array['active'] = $this->active();
     188                return $array;
     189        }
     190
     191        /**
    113192         * Checks required user capabilities and whether the theme has the
    114193         * feature support required by the panel.
    115194         *
    class WP_Customize_Panel { 
    130209        }
    131210
    132211        /**
     212         * Get the panel's content template for insertion into the Customizer pane.
     213         *
     214         * @since 4.1.0
     215         *
     216         * @return string
     217         */
     218        public final function get_content() {
     219                ob_start();
     220                $this->maybe_render();
     221                $template = trim( ob_get_contents() );
     222                ob_end_clean();
     223                return $template;
     224        }
     225
     226        /**
    133227         * Check capabilities and render the panel.
    134228         *
    135229         * @since 4.0.0
    class WP_Customize_Panel { 
    189283         */
    190284        protected function render_content() {
    191285                ?>
    192                 <li class="accordion-section control-section<?php if ( empty( $this->description ) ) echo ' cannot-expand'; ?>">
     286                <li class="panel-meta accordion-section control-section<?php if ( empty( $this->description ) ) { echo ' cannot-expand'; } ?>">
    193287                        <div class="accordion-section-title" tabindex="0">
    194288                                <span class="preview-notice"><?php
    195289                                        /* translators: %s is the site/panel title in the Customizer */
    class WP_Customize_Panel { 
    203297                        <?php endif; ?>
    204298                </li>
    205299                <?php
    206                 foreach ( $this->sections as $section ) {
    207                         $section->maybe_render();
    208                 }
    209300        }
    210301}
  • src/wp-includes/class-wp-customize-section.php

    diff --git src/wp-includes/class-wp-customize-section.php src/wp-includes/class-wp-customize-section.php
    index d740ddb..3553285 100644
    class WP_Customize_Section { 
    9292        public $controls;
    9393
    9494        /**
     95         * @since 4.1.0
     96         * @access public
     97         * @var string
     98         */
     99        public $type;
     100
     101        /**
     102         * Callback.
     103         *
     104         * @since 4.1.0
     105         * @access public
     106         *
     107         * @see WP_Customize_Section::active()
     108         *
     109         * @var callable Callback is called with one argument, the instance of
     110         *               WP_Customize_Section, and returns bool to indicate whether
     111         *               the section is active (such as it relates to the URL
     112         *               currently being previewed).
     113         */
     114        public $active_callback = '';
     115
     116        /**
    95117         * Constructor.
    96118         *
    97119         * Any supplied $args override class property defaults.
    class WP_Customize_Section { 
    105127        public function __construct( $manager, $id, $args = array() ) {
    106128                $keys = array_keys( get_object_vars( $this ) );
    107129                foreach ( $keys as $key ) {
    108                         if ( isset( $args[ $key ] ) )
     130                        if ( isset( $args[ $key ] ) ) {
    109131                                $this->$key = $args[ $key ];
     132                        }
    110133                }
    111134
    112135                $this->manager = $manager;
    113136                $this->id = $id;
     137                if ( empty( $this->active_callback ) ) {
     138                        $this->active_callback = array( $this, 'active_callback' );
     139                }
    114140
    115141                $this->controls = array(); // Users cannot customize the $controls array.
    116142
    class WP_Customize_Section { 
    118144        }
    119145
    120146        /**
     147         * Check whether section is active to current Customizer preview.
     148         *
     149         * @since 4.1.0
     150         * @access public
     151         *
     152         * @return bool Whether the section is active to the current preview.
     153         */
     154        public final function active() {
     155                $section = $this;
     156                $active = call_user_func( $this->active_callback, $this );
     157
     158                /**
     159                 * Filter response of WP_Customize_Section::active().
     160                 *
     161                 * @since 4.1.0
     162                 *
     163                 * @param bool                 $active  Whether the Customizer section is active.
     164                 * @param WP_Customize_Section $section WP_Customize_Section instance.
     165                 */
     166                $active = apply_filters( 'customize_section_active', $active, $section );
     167
     168                return $active;
     169        }
     170
     171        /**
     172         * Default callback used when invoking WP_Customize_Section::active().
     173         *
     174         * Subclasses can override this with their specific logic, or they may
     175         * provide an 'active_callback' argument to the constructor.
     176         *
     177         * @since 4.1.0
     178         * @access public
     179         *
     180         * @return bool Always true.
     181         */
     182        public function active_callback() {
     183                return true;
     184        }
     185
     186        /**
     187         * Gather the parameters passed to client JavaScript via JSON.
     188         *
     189         * @since 4.1.0
     190         *
     191         * @return array The array to be exported to the client as JSON
     192         */
     193        public function json() {
     194                $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'panel', 'type' ) );
     195                $array['content'] = $this->get_content();
     196                $array['active'] = $this->active();
     197                return $array;
     198        }
     199
     200        /**
    121201         * Checks required user capabilities and whether the theme has the
    122202         * feature support required by the section.
    123203         *
    class WP_Customize_Section { 
    126206         * @return bool False if theme doesn't support the section or user doesn't have the capability.
    127207         */
    128208        public final function check_capabilities() {
    129                 if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) )
     209                if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) ) {
    130210                        return false;
     211                }
    131212
    132                 if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) )
     213                if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) {
    133214                        return false;
     215                }
    134216
    135217                return true;
    136218        }
    137219
    138220        /**
     221         * Get the section's content template for insertion into the Customizer pane.
     222         *
     223         * @since 4.1.0
     224         *
     225         * @return string
     226         */
     227        public final function get_content() {
     228                ob_start();
     229                $this->maybe_render();
     230                $template = trim( ob_get_contents() );
     231                ob_end_clean();
     232                return $template;
     233        }
     234
     235        /**
    139236         * Check capabilities and render the section.
    140237         *
    141238         * @since 3.4.0
    142239         */
    143240        public final function maybe_render() {
    144                 if ( ! $this->check_capabilities() )
     241                if ( ! $this->check_capabilities() ) {
    145242                        return;
     243                }
    146244
    147245                /**
    148246                 * Fires before rendering a Customizer section.
    class WP_Customize_Section { 
    172270         */
    173271        protected function render() {
    174272                $classes = 'control-section accordion-section';
    175                 if ( $this->panel ) {
    176                         $classes .= ' control-subsection';
    177                 }
    178273                ?>
    179274                <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
    180275                        <h3 class="accordion-section-title" tabindex="0">
    class WP_Customize_Section { 
    183278                        </h3>
    184279                        <ul class="accordion-section-content">
    185280                                <?php if ( ! empty( $this->description ) ) : ?>
    186                                 <li><p class="description customize-section-description"><?php echo $this->description; ?></p></li>
     281                                        <li class="customize-section-description-container">
     282                                                <p class="description customize-section-description"><?php echo $this->description; ?></p>
     283                                        </li>
    187284                                <?php endif; ?>
    188                                 <?php
    189                                 foreach ( $this->controls as $control )
    190                                         $control->maybe_render();
    191                                 ?>
    192285                        </ul>
    193286                </li>
    194287                <?php
  • src/wp-includes/js/customize-base.js

    diff --git src/wp-includes/js/customize-base.js src/wp-includes/js/customize-base.js
    index d2488dd..6ea2822 100644
    window.wp = window.wp || {}; 
    184184                        to = this.validate( to );
    185185
    186186                        // Bail if the sanitized value is null or unchanged.
    187                         if ( null === to || _.isEqual( from, to ) )
     187                        if ( null === to || _.isEqual( from, to ) ) {
    188188                                return this;
     189                        }
    189190
    190191                        this._value = to;
    191192                        this._dirty = true;
  • src/wp-includes/js/customize-preview.js

    diff --git src/wp-includes/js/customize-preview.js src/wp-includes/js/customize-preview.js
    index 6da26f4..1a82565 100644
     
    107107        });
    108108
    109109                preview.send( 'ready', {
     110                        activePanels: api.settings.activePanels,
     111                        activeSections: api.settings.activeSections,
    110112                        activeControls: api.settings.activeControls
    111113                } );
    112114
  • tests/qunit/index.html

    diff --git tests/qunit/index.html tests/qunit/index.html
    index c5afd52..ce11144 100644
     
    88  <script src="../../src/wp-includes/js/underscore.min.js"></script>
    99  <script src="../../src/wp-includes/js/backbone.min.js"></script>
    1010  <script src="../../src/wp-includes/js/zxcvbn.min.js"></script>
    11        
     11
    1212  <!-- QUnit -->
    1313  <link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
    1414  <script src="vendor/qunit.js"></script>
     
    2828
    2929    <!-- Tested files -->
    3030    <script src="../../src/wp-admin/js/password-strength-meter.js"></script>
     31    <script src="../../src/wp-includes/js/customize-base.js"></script>
    3132    <script src="../../src/wp-includes/js/customize-models.js"></script>
    3233    <script src="../../src/wp-includes/js/shortcode.js"></script>
    3334
    3435    <!-- Unit tests -->
    3536    <script src="wp-admin/js/password-strength-meter.js"></script>
     37    <script src="wp-admin/js/customize-base.js"></script>
    3638    <script src="wp-admin/js/customize-header.js"></script>
    3739    <script src="wp-includes/js/shortcode.js"></script>
    3840  </div>
    3941</body>
    4042</html>
    41 
  • new file tests/qunit/wp-admin/js/customize-base.js

    diff --git tests/qunit/wp-admin/js/customize-base.js tests/qunit/wp-admin/js/customize-base.js
    new file mode 100644
    index 0000000..1235bd5
    - +  
     1/* global wp, sinon */
     2
     3jQuery( function( $ ) {
     4        var FooSuperClass, BarSubClass, foo, bar;
     5
     6        module( 'Customize Base: Class' );
     7
     8        FooSuperClass = wp.customize.Class.extend(
     9                {
     10                        initialize: function ( instanceProps ) {
     11                                $.extend( this, instanceProps || {} );
     12                        },
     13                        protoProp: 'protoPropValue'
     14                },
     15                {
     16                        staticProp: 'staticPropValue'
     17                }
     18        );
     19        test( 'FooSuperClass is a function ', function () {
     20                equal( typeof FooSuperClass, 'function' );
     21        });
     22        test( 'FooSuperClass prototype has protoProp', function () {
     23                equal( FooSuperClass.prototype.protoProp, 'protoPropValue' );
     24        });
     25        test( 'FooSuperClass does not have protoProp', function () {
     26                equal( typeof FooSuperClass.protoProp, 'undefined' );
     27        });
     28        test( 'FooSuperClass has staticProp', function () {
     29                equal( FooSuperClass.staticProp, 'staticPropValue' );
     30        });
     31        test( 'FooSuperClass prototype does not have staticProp', function () {
     32                equal( typeof FooSuperClass.prototype.staticProp, 'undefined' );
     33        });
     34
     35        foo = new FooSuperClass( { instanceProp: 'instancePropValue' } );
     36        test( 'FooSuperClass instance foo extended Class', function () {
     37                equal( foo.extended( wp.customize.Class ), true );
     38        });
     39        test( 'foo instance has protoProp', function () {
     40                equal( foo.protoProp, 'protoPropValue' );
     41        });
     42        test( 'foo instance does not have staticProp', function () {
     43                equal( typeof foo.staticProp, 'undefined' );
     44        });
     45        test( 'FooSuperClass instance foo ran initialize() and has supplied instanceProp', function () {
     46                equal( foo.instanceProp, 'instancePropValue' );
     47        });
     48
     49        // @todo Test Class.constructor() manipulation
     50        // @todo Test Class.applicator?
     51        // @todo do we test object.instance?
     52
     53
     54        module( 'Customize Base: Subclass' );
     55
     56        BarSubClass = FooSuperClass.extend(
     57                {
     58                        initialize: function ( instanceProps ) {
     59                                FooSuperClass.prototype.initialize.call( this, instanceProps );
     60                                this.subInstanceProp = 'subInstancePropValue';
     61                        },
     62                        subProtoProp: 'subProtoPropValue'
     63                },
     64                {
     65                        subStaticProp: 'subStaticPropValue'
     66                }
     67        );
     68        test( 'BarSubClass prototype has subProtoProp', function () {
     69                equal( BarSubClass.prototype.subProtoProp, 'subProtoPropValue' );
     70        });
     71        test( 'BarSubClass prototype has parent FooSuperClass protoProp', function () {
     72                equal( BarSubClass.prototype.protoProp, 'protoPropValue' );
     73        });
     74
     75        bar = new BarSubClass( { instanceProp: 'instancePropValue' } );
     76        test( 'BarSubClass instance bar its initialize() and parent initialize() run', function () {
     77                equal( bar.instanceProp, 'instancePropValue' );
     78                equal( bar.subInstanceProp, 'subInstancePropValue' );
     79        });
     80
     81        test( 'BarSubClass instance bar extended FooSuperClass', function () {
     82                equal( bar.extended( FooSuperClass ), true );
     83        });
     84
     85});