Make WordPress Core

Ticket #28709: 28709.wip.7.diff

File 28709.wip.7.diff, 58.7 KB (added by westonruter, 10 years ago)

https://github.com/xwpco/wordpress-develop/compare/2a5c56abda35b47cb946a11d36b31940ca030b69...5169fd3c115a8af92891e819180cd7ebcb64177c

  • src/wp-admin/customize.php

    diff --git src/wp-admin/customize.php src/wp-admin/customize.php
    index 7828ee4..85caff4 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' ); 
    249243                ),
    250244                'settings' => array(),
    251245                'controls' => array(),
     246                'panels'   => array(),
     247                'sections' => array(),
    252248                'nonce'    => array(
    253249                        'save'    => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ),
    254250                        'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() )
    do_action( 'customize_controls_print_scripts' ); 
    263259                );
    264260        }
    265261
    266         // Prepare Customize Control objects to pass to Javascript.
     262        // Prepare Customize Control objects to pass to JavaScript.
    267263        foreach ( $wp_customize->controls() as $id => $control ) {
    268                 $control->to_json();
    269                 $settings['controls'][ $id ] = $control->json;
     264                $settings['controls'][ $id ] = $control->json();
     265        }
     266
     267        // Prepare Customize Section objects to pass to JavaScript.
     268        foreach ( $wp_customize->sections() as $id => $section ) {
     269                $settings['sections'][ $id ] = $section->json();
     270        }
     271
     272        // Prepare Customize Panel objects to pass to JavaScript.
     273        foreach ( $wp_customize->panels() as $id => $panel ) {
     274                $settings['panels'][ $id ] = $panel->json();
     275                foreach ( $panel->sections as $section_id => $section ) {
     276                        $settings['sections'][ $section_id ] = $section->json();
     277                }
    270278        }
    271279
    272280        ?>
  • 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 fad223e..0a12f5b 100644
     
    11/* globals _wpCustomizeHeader, _wpMediaViewsL10n */
    22(function( exports, $ ){
    3         var api = wp.customize;
     3        var bubbleChildValueChanges, Container, api = wp.customize;
    44
    55        /**
    66         * @constructor
     
    3131        });
    3232
    3333        /**
     34         * Watch all changes to Value properties, and bubble changes to parent Values instance
     35         *
     36         * @param {wp.customize.Class} instance
     37         * @param {Array} properties  The names of the Value instances to watch.
     38         */
     39        bubbleChildValueChanges = function ( instance, properties ) {
     40                $.each( properties, function ( i, key ) {
     41                        instance[ key ].bind( function ( to, from ) {
     42                                if ( instance.parent && to !== from ) {
     43                                        instance.parent.trigger( 'change', instance );
     44                                }
     45                        } );
     46                } );
     47        };
     48
     49        /**
     50         * Base class for Panel and Section
     51         *
    3452         * @constructor
    3553         * @augments wp.customize.Class
    3654         */
    37         api.Control = api.Class.extend({
    38                 initialize: function( id, options ) {
    39                         var control = this,
    40                                 nodes, radios, settings;
     55        Container = api.Class.extend({
     56                defaultActiveArguments: { duration: null },
     57                defaultExpandedArguments: { duration: 150 },
     58
     59                initialize: function ( id, options ) {
     60                        var container = this;
     61                        container.id = id;
     62                        container.params = {};
     63                        $.extend( container, options || {} );
     64                        container.container = $( container.params.content );
     65
     66                        container.priority = new api.Value();
     67                        container.active = new api.Value();
     68                        container.activeArgumentsQueue = [];
     69                        container.expanded = new api.Value();
     70                        container.expandedArgumentsQueue = [];
     71
     72                        container.active.bind( function ( active ) {
     73                                var args = container.activeArgumentsQueue.shift();
     74                                args = $.extend( {}, container.defaultActiveArguments, args );
     75                                active = ( active && container.isContextuallyActive() );
     76                                container.onToggleActive( active, args );
     77                                // @todo trigger 'activated' and 'deactivated' events based on the expanded param?
     78                        });
     79                        container.expanded.bind( function ( expanded ) {
     80                                var args = container.expandedArgumentsQueue.shift();
     81                                args = $.extend( {}, container.defaultExpandedArguments, args );
     82                                container.onToggleExpanded( expanded, args );
     83                                // @todo trigger 'expanded' and 'collapsed' events based on the expanded param?
     84                        });
    4185
    42                         this.params = {};
    43                         $.extend( this, options || {} );
     86                        container.attachEvents();
    4487
    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 );
     88                        bubbleChildValueChanges( container, [ 'priority', 'active' ] );
    4989
    50                         settings = $.map( this.params.settings, function( value ) {
    51                                 return value;
     90                        container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
     91                        container.active.set( container.params.active );
     92                        container.expanded.set( false ); // @todo True if deeplinking?
     93                },
     94
     95                /**
     96                 * Get the child models associated with this parent, sorting them by their priority Value.
     97                 *
     98                 * @param {String} parentType
     99                 * @param {String} childType
     100                 * @returns {Array}
     101                 */
     102                _children: function ( parentType, childType ) {
     103                        var parent = this,
     104                                children = [];
     105                        api[ childType ].each( function ( child ) {
     106                                if ( child[ parentType ].get() === parent.id ) {
     107                                        children.push( child );
     108                                }
     109                        } );
     110                        children.sort( function ( a, b ) {
     111                                return a.priority() - b.priority();
     112                        } );
     113                        return children;
     114                },
     115
     116                /**
     117                 * To override by subclass, to return whether the container has active children.
     118                 */
     119                isContextuallyActive: function () {
     120                        throw new Error( 'Must override with subclass.' );
     121                },
     122
     123                /**
     124                 * Handle changes to the active state.
     125                 * This does not change the active state, it merely handles the behavior
     126                 * for when it does change.
     127                 *
     128                 * To override by subclass, update the container's UI to reflect the provided active state.
     129                 *
     130                 * @param {Boolean} active
     131                 * @param {Object} args  merged on top of this.defaultActiveArguments
     132                 */
     133                onToggleActive: function ( active, args ) {
     134                        if ( active ) {
     135                                this.container.stop( true, true ).slideDown( args.duration ); // @todo pass args.completeCallback
     136                        } else {
     137                                this.container.stop( true, true ).slideUp( args.duration ); // @todo pass args.completeCallback
     138                        }
     139                },
     140
     141                /**
     142                 * @param {Object} [params]
     143                 */
     144                activate: function ( params ) {
     145                        this.activeArgumentsQueue.push( params || {} );
     146                        this.active.set( true );
     147                },
     148
     149                /**
     150                 * @param {Object} [params]
     151                 */
     152                deactivate: function ( params ) {
     153                        this.activeArgumentsQueue.push( params || {} );
     154                        this.active.set( false );
     155                },
     156
     157                /**
     158                 * To override by subclass, update the container's UI to reflect the provided active state.
     159                 */
     160                onToggleExpanded: function () {
     161                        throw new Error( 'Must override with subclass.' );
     162                },
     163
     164                /**
     165                 * @param {Object} [params]
     166                 */
     167                expand: function ( params ) {
     168                        this.expandedArgumentsQueue.push( params || {} );
     169                        this.expanded.set( true );
     170                },
     171
     172                /**
     173                 * @param {Object} [params]
     174                 */
     175                collapse: function ( params ) {
     176                        this.expandedArgumentsQueue.push( params || {} );
     177                        this.expanded( false );
     178                },
     179
     180                /**
     181                 *
     182                 */
     183                focus: function () {
     184                        throw new Error( 'focus method must be overridden' );
     185                }
     186        });
     187
     188        /**
     189         * @constructor
     190         * @augments wp.customize.Class
     191         */
     192        api.Section = Container.extend({
     193
     194                /**
     195                 * @param {String} id
     196                 * @param {Array} options
     197                 */
     198                initialize: function ( id, options ) {
     199                        var section = this;
     200                        Container.prototype.initialize.call( section, id, options );
     201
     202                        section.panel = new api.Value();
     203                        section.panel.bind( function ( id ) {
     204                                $( section.container ).toggleClass( 'control-subsection', !! id );
    52205                        });
     206                        section.panel.set( section.params.panel || '' );
     207                        bubbleChildValueChanges( section, [ 'panel' ] );
     208                },
    53209
    54                         api.apply( api, settings.concat( function() {
    55                                 var key;
     210                /**
     211                 *
     212                 */
     213                embed: function ( readyCallback ) {
     214                        var panel_id,
     215                                section = this;
     216
     217                        panel_id = this.panel.get();
     218                        if ( ! panel_id ) {
     219                                $( '#customize-theme-controls > ul' ).append( section.container );
     220                                readyCallback();
     221                        } else {
     222                                api.panel( panel_id, function ( panel ) {
     223                                        panel.embed();
     224                                        panel.container.find( 'ul:first' ).append( section.container );
     225                                        readyCallback();
     226                                } );
     227                        }
     228                },
    56229
    57                                 control.settings = {};
    58                                 for ( key in control.params.settings ) {
    59                                         control.settings[ key ] = api( control.params.settings[ key ] );
     230                /**
     231                 * Add behaviors for the accordion section
     232                 */
     233                attachEvents: function () {
     234                        var section = this;
     235
     236                        // Expand/Collapse accordion sections on click.
     237                        section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
     238                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     239                                        return;
    60240                                }
     241                                e.preventDefault(); // Keep this AFTER the key filter above
    61242
    62                                 control.setting = control.settings['default'] || null;
    63                                 control.ready();
    64                         }) );
     243                                if ( section.expanded() ) {
     244                                        section.collapse();
     245                                } else {
     246                                        section.expand();
     247                                }
     248                        });
     249                },
     250
     251                /**
     252                 * Return whether this section has any active controls.
     253                 *
     254                 * @returns {boolean}
     255                 */
     256                isContextuallyActive: function () {
     257                        var section = this,
     258                                controls = section.controls(),
     259                                activeCount = 0;
     260                        _( controls ).each( function ( control ) {
     261                                if ( control.active() ) {
     262                                        activeCount += 1;
     263                                }
     264                        } );
     265                        return ( activeCount !== 0 );
     266                },
     267
     268                /**
     269                 * Get the controls that are associated with this section, sorted by their priority Value.
     270                 *
     271                 * @returns {Array}
     272                 */
     273                controls: function () {
     274                        return this._children( 'section', 'control' );
     275                },
     276
     277                /**
     278                 * Update UI to reflect expanded state
     279                 *
     280                 * @param {Boolean} expanded
     281                 * @param {Object} args
     282                 */
     283                onToggleExpanded: function ( expanded, args ) {
     284                        var section = this,
     285                                content = section.container.find( '.accordion-section-content' );
     286
     287                        if ( expanded ) {
     288
     289                                if ( section.panel() ) {
     290                                        api.panel( section.panel() ).expand();
     291                                }
     292
     293                                api.section.each( function ( otherSection ) {
     294                                        if ( otherSection !== section ) {
     295                                                otherSection.collapse( { duration : 0 } );
     296                                        }
     297                                });
     298
     299                                content.stop().slideDown( args.duration ); // @todo pass args.completeCallback
     300                                section.container.addClass( 'open' );
     301                        } else {
     302
     303                                section.container.removeClass( 'open' );
     304                                content.slideUp( args.duration ); // @todo pass args.completeCallback
     305                        }
     306                },
     307
     308                /**
     309                 * Bring the containing panel into view and then expand this section and bring it into view
     310                 *
     311                 * @todo This is an alias for expand(); do we need it?
     312                 */
     313                focus: function () {
     314                        var section = this;
     315                        // @todo What if it is not active? Return false?
     316                        section.expand();
     317                }
     318        });
     319
     320        /**
     321         * @constructor
     322         * @augments wp.customize.Class
     323         */
     324        api.Panel = Container.extend({
     325                initialize: function ( id, options ) {
     326                        var panel = this;
     327                        Container.prototype.initialize.call( panel, id, options );
     328                },
     329
     330                /**
     331                 *
     332                 */
     333                embed: function ( readyCallback ) {
     334                        $( '#customize-theme-controls > ul' ).append( this.container );
     335                        if ( readyCallback ) {
     336                                readyCallback();
     337                        }
     338                },
     339
     340                /**
     341                 *
     342                 */
     343                attachEvents: function () {
     344                        var meta, panel = this;
     345
     346                        // Expand/Collapse accordion sections on click.
     347                        panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
     348                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     349                                        return;
     350                                }
     351                                e.preventDefault(); // Keep this AFTER the key filter above
     352
     353                                if ( ! panel.expanded() ) {
     354                                        panel.expand();
     355                                }
     356                        });
     357
     358                        meta = panel.container.find( '.panel-meta:first' );
     359
     360                        meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
     361                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     362                                        return;
     363                                }
     364                                e.preventDefault(); // Keep this AFTER the key filter above
     365
     366                                if ( meta.hasClass( 'cannot-expand' ) ) {
     367                                        return;
     368                                }
     369
     370                                var content = meta.find( '.accordion-section-content:first' );
     371                                if ( meta.hasClass( 'open' ) ) {
     372                                        meta.toggleClass( 'open' );
     373                                        content.slideUp( 150 );
     374                                } else {
     375                                        content.slideDown( 150 );
     376                                        meta.toggleClass( 'open' );
     377                                }
     378                        });
     379
     380                },
     381
     382                /**
     383                 * Get the sections that are associated with this panel, sorted by their priority Value.
     384                 *
     385                 * @returns {Array}
     386                 */
     387                sections: function () {
     388                        return this._children( 'panel', 'section' );
     389                },
     390
     391                /**
     392                 * Return whether this section has any active sections.
     393                 *
     394                 * @returns {boolean}
     395                 */
     396                isContextuallyActive: function () {
     397                        var panel = this,
     398                                sections = panel.sections(),
     399                                activeCount = 0;
     400                        _( sections ).each( function ( section ) {
     401                                if ( section.active() && section.isContextuallyActive() ) {
     402                                        activeCount += 1;
     403                                }
     404                        } );
     405                        return ( activeCount !== 0 );
     406                },
     407
     408                /**
     409                 * Update UI to reflect expanded state
     410                 *
     411                 * @param {Boolean} expanded
     412                 */
     413                onToggleExpanded: function ( expanded ) {
     414                        // Note: there is a second argument 'args' passed
     415                        var position, scroll,
     416                                panel = this,
     417                                section = panel.container.closest( '.accordion-section' ),
     418                                overlay = section.closest( '.wp-full-overlay' ),
     419                                container = section.closest( '.accordion-container' ),
     420                                siblings = container.find( '.open' ),
     421                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
     422                                backBtn = overlay.find( '.control-panel-back' ),
     423                                panelTitle = section.find( '.accordion-section-title' ).first(),
     424                                content = section.find( '.control-panel-content' );
     425
     426                        if ( expanded ) {
     427
     428                                // Collapse any sibling sections/panels
     429                                api.section.each( function ( section ) {
     430                                        if ( ! section.panel() ) {
     431                                                section.collapse( { duration: 0 } );
     432                                        }
     433                                });
     434                                api.panel.each( function ( otherPanel ) {
     435                                        if ( panel !== otherPanel ) {
     436                                                otherPanel.collapse( { duration: 0 } );
     437                                        }
     438                                });
     439
     440                                content.show( 0, function() {
     441                                        position = content.offset().top;
     442                                        scroll = container.scrollTop();
     443                                        content.css( 'margin-top', ( 45 - position - scroll ) );
     444                                        section.addClass( 'current-panel' );
     445                                        overlay.addClass( 'in-sub-panel' );
     446                                        container.scrollTop( 0 );
     447                                } );
     448                                topPanel.attr( 'tabindex', '-1' );
     449                                backBtn.attr( 'tabindex', '0' );
     450                                backBtn.focus();
     451                        } else {
     452                                siblings.removeClass( 'open' );
     453                                section.removeClass( 'current-panel' );
     454                                overlay.removeClass( 'in-sub-panel' );
     455                                content.delay( 180 ).hide( 0, function() {
     456                                        content.css( 'margin-top', 'inherit' ); // Reset
     457                                } );
     458                                topPanel.attr( 'tabindex', '0' );
     459                                backBtn.attr( 'tabindex', '-1' );
     460                                panelTitle.focus();
     461                                container.scrollTop( 0 );
     462                        }
     463                },
     464
     465                /**
     466                 * Bring the containing panel into view and then expand this section and bring it into view
     467                 */
     468                focus: function () {
     469                        var panel = this;
     470                        // @todo What if it is not active? Return false?
     471                        panel.expand();
     472                }
     473
     474                // @todo Need to first exit out of the Panel
     475        });
     476
     477        /**
     478         * @constructor
     479         * @augments wp.customize.Class
     480         */
     481        api.Control = api.Class.extend({
     482                defaultActiveArguments: { duration: null },
     483
     484                initialize: function( id, options ) {
     485                        var control = this,
     486                                nodes, radios, settings;
     487
     488                        control.params = {};
     489                        $.extend( control, options || {} );
     490
     491                        control.id = id;
     492                        control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
     493                        control.container = control.params.content ? $( control.params.content ) : $( control.selector );
     494
     495                        control.section = new api.Value();
     496                        control.priority = new api.Value();
     497                        control.active = new api.Value();
     498                        control.activeArgumentsQueue = [];
    65499
    66500                        control.elements = [];
    67501
    68                         nodes  = this.container.find('[data-customize-setting-link]');
     502                        nodes  = control.container.find('[data-customize-setting-link]');
    69503                        radios = {};
    70504
    71505                        nodes.each( function() {
    72                                 var node = $(this),
     506                                var node = $( this ),
    73507                                        name;
    74508
    75                                 if ( node.is(':radio') ) {
    76                                         name = node.prop('name');
    77                                         if ( radios[ name ] )
     509                                if ( node.is( ':radio' ) ) {
     510                                        name = node.prop( 'name' );
     511                                        if ( radios[ name ] ) {
    78512                                                return;
     513                                        }
    79514
    80515                                        radios[ name ] = true;
    81516                                        node = nodes.filter( '[name="' + name + '"]' );
    82517                                }
    83518
    84                                 api( node.data('customizeSettingLink'), function( setting ) {
     519                                api( node.data( 'customizeSettingLink' ), function( setting ) {
    85520                                        var element = new api.Element( node );
    86521                                        control.elements.push( element );
    87522                                        element.sync( setting );
     
    90525                        });
    91526
    92527                        control.active.bind( function ( active ) {
    93                                 control.toggle( active );
     528                                var args = control.activeArgumentsQueue.shift();
     529                                args = $.extend( {}, control.defaultActiveArguments, args );
     530                                control.onToggleActive( active, args );
     531                        } );
     532
     533                        control.section.set( control.params.section );
     534                        control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
     535                        control.active.set( control.params.active );
     536
     537                        bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
     538
     539                        // Associate this control with its settings when they are created
     540                        settings = $.map( control.params.settings, function( value ) {
     541                                return value;
     542                        });
     543                        api.apply( api, settings.concat( function () {
     544                                var key;
     545
     546                                control.settings = {};
     547                                for ( key in control.params.settings ) {
     548                                        control.settings[ key ] = api( control.params.settings[ key ] );
     549                                }
     550
     551                                control.setting = control.settings['default'] || null;
     552                                control.embed( function () {
     553                                        control.ready();
     554                                });
     555                        }) );
     556                },
     557
     558                /**
     559                 * @param {Function} [readyCallback] Callback to fire when the embedding is done.
     560                 */
     561                embed: function ( readyCallback ) {
     562                        var section_id,
     563                                control = this;
     564
     565                        section_id = control.section.get();
     566                        if ( ! section_id ) {
     567                                throw new Error( 'A control must have an associated section.' );
     568                        }
     569
     570                        // Defer until the associated section is available
     571                        api.section( section_id, function ( section ) {
     572                                section.embed( function () {
     573                                        section.container.find( 'ul:first' ).append( control.container );
     574                                        readyCallback();
     575                                } );
    94576                        } );
    95                         control.toggle( control.active() );
    96577                },
    97578
    98579                /**
     
    101582                ready: function() {},
    102583
    103584                /**
    104                  * Callback for change to the control's active state.
    105                  *
    106                  * Override function for custom behavior for the control being active/inactive.
     585                 * Bring the containing section and panel into view and then this control into view, focusing on the first input
     586                 */
     587                focus: function () {
     588                        throw new Error( 'Not implemented yet' );
     589                },
     590
     591                /**
     592                 * Update UI in response to a change in the control's active state.
     593                 * This does not change the active state, it merely handles the behavior
     594                 * for when it does change.
    107595                 *
    108596                 * @param {Boolean} active
     597                 * @param {Object} args  merged on top of this.defaultActiveArguments
    109598                 */
    110                 toggle: function ( active ) {
     599                onToggleActive: function ( active, args ) {
    111600                        if ( active ) {
    112                                 this.container.slideDown();
     601                                this.container.slideDown( args.duration );
    113602                        } else {
    114                                 this.container.slideUp();
     603                                this.container.slideUp( args.duration );
    115604                        }
    116605                },
    117606
     607                /**
     608                 * @deprecated alias of onToggleActive
     609                 */
     610                toggle: function ( active ) {
     611                        return this.onToggleActive( active, this.defaultActiveArguments );
     612                },
     613
     614                /**
     615                 * Shorthand way to enable the active state.
     616                 */
     617                activate: function () {
     618                        this.active.set( true );
     619                },
     620
     621                /**
     622                 * Shorthand way to disable the active state.
     623                 */
     624                deactivate: function () {
     625                        this.active.set( false );
     626                },
     627
    118628                dropdownInit: function() {
    119629                        var control      = this,
    120630                                statuses     = this.container.find('.dropdown-status'),
     
    5751085
    5761086        // Create the collection of Control objects.
    5771087        api.control = new api.Values({ defaultConstructor: api.Control });
     1088        api.section = new api.Values({ defaultConstructor: api.Section });
     1089        api.panel = new api.Values({ defaultConstructor: api.Panel });
    5781090
    5791091        /**
    5801092         * @constructor
     
    6101122                                loaded = false,
    6111123                                ready  = false;
    6121124
    613                         if ( this._ready )
     1125                        if ( this._ready ) {
    6141126                                this.unbind( 'ready', this._ready );
     1127                        }
    6151128
    6161129                        this._ready = function() {
    6171130                                ready = true;
    6181131
    619                                 if ( loaded )
     1132                                if ( loaded ) {
    6201133                                        deferred.resolveWith( self );
     1134                                }
    6211135                        };
    6221136
    6231137                        this.bind( 'ready', this._ready );
    6241138
    6251139                        this.bind( 'ready', function ( data ) {
    626                                 if ( ! data || ! data.activeControls ) {
     1140                                if ( ! data ) {
    6271141                                        return;
    6281142                                }
    6291143
    630                                 $.each( data.activeControls, function ( id, active ) {
    631                                         var control = api.control( id );
    632                                         if ( control ) {
    633                                                 control.active( active );
     1144                                var constructs = {
     1145                                        panel: data.activePanels,
     1146                                        section: data.activeSections,
     1147                                        control: data.activeControls
     1148                                };
     1149
     1150                                $.each( constructs, function ( type, activeConstructs ) {
     1151                                        if ( activeConstructs ) {
     1152                                                $.each( activeConstructs, function ( id, active ) {
     1153                                                        var construct = api[ type ]( id );
     1154                                                        if ( construct ) {
     1155                                                                construct.active( active );
     1156                                                        }
     1157                                                } );
    6341158                                        }
    6351159                                } );
     1160
    6361161                        } );
    6371162
    6381163                        this.request = $.ajax( this.previewUrl(), {
     
    6541179
    6551180                                // Check if the location response header differs from the current URL.
    6561181                                // If so, the request was redirected; try loading the requested page.
    657                                 if ( location && location != self.previewUrl() ) {
     1182                                if ( location && location !== self.previewUrl() ) {
    6581183                                        deferred.rejectWith( self, [ 'redirect', location ] );
    6591184                                        return;
    6601185                                }
     
    9791504                image:  api.ImageControl,
    9801505                header: api.HeaderControl
    9811506        };
     1507        api.panelConstructor = {};
     1508        api.sectionConstructor = {};
    9821509
    9831510        $( function() {
    9841511                api.settings = window._wpCustomizeSettings;
     
    10091536                        }
    10101537                });
    10111538
     1539                // Expand/Collapse the main customizer customize info
     1540                $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
     1541                        if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     1542                                return;
     1543                        }
     1544                        e.preventDefault(); // Keep this AFTER the key filter above
     1545
     1546                        var section = $( this ).parent(),
     1547                                content = section.find( '.accordion-section-content:first' );
     1548
     1549                        if ( section.hasClass( 'cannot-expand' ) ) {
     1550                                return;
     1551                        }
     1552
     1553                        if ( section.hasClass( 'open' ) ) {
     1554                                section.toggleClass( 'open' );
     1555                                content.slideUp( 150 );
     1556                        } else {
     1557                                content.slideDown( 150 );
     1558                                section.toggleClass( 'open' );
     1559                        }
     1560                });
     1561
    10121562                // Initialize Previewer
    10131563                api.previewer = new api.Previewer({
    10141564                        container:   '#customize-preview',
     
    11021652                        $.extend( this.nonce, nonce );
    11031653                });
    11041654
     1655                // Create Settings
    11051656                $.each( api.settings.settings, function( id, data ) {
    11061657                        api.create( id, id, data.value, {
    11071658                                transport: data.transport,
     
    11091660                        } );
    11101661                });
    11111662
     1663                // Create Panels
     1664                $.each( api.settings.panels, function ( id, data ) {
     1665                        var constructor = api.panelConstructor[ data.type ] || api.Panel,
     1666                                panel;
     1667
     1668                        panel = new constructor( id, {
     1669                                params: data
     1670                        } );
     1671                        api.panel.add( id, panel );
     1672                });
     1673
     1674                // Create Sections
     1675                $.each( api.settings.sections, function ( id, data ) {
     1676                        var constructor = api.sectionConstructor[ data.type ] || api.Section,
     1677                                section;
     1678
     1679                        section = new constructor( id, {
     1680                                params: data
     1681                        } );
     1682                        api.section.add( id, section );
     1683                });
     1684
     1685                // Create Controls
     1686                // @todo factor this out
    11121687                $.each( api.settings.controls, function( id, data ) {
    11131688                        var constructor = api.controlConstructor[ data.type ] || api.Control,
    11141689                                control;
    11151690
    1116                         control = api.control.add( id, new constructor( id, {
     1691                        control = new constructor( id, {
    11171692                                params: data,
    11181693                                previewer: api.previewer
    1119                         } ) );
     1694                        } );
     1695                        api.control.add( id, control );
    11201696                });
    11211697
     1698                /**
     1699                 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
     1700                 */
     1701                api.reflowPaneContents = _.bind( function () {
     1702
     1703                        var appendContainer, activeElement, rootNodes = [];
     1704
     1705                        if ( document.activeElement ) {
     1706                                activeElement = $( document.activeElement );
     1707                        }
     1708
     1709                        api.panel.each( function ( panel ) {
     1710                                var sections = panel.sections();
     1711                                rootNodes.push( panel );
     1712                                appendContainer = panel.container.find( 'ul:first' );
     1713                                // @todo Skip doing any DOM manipulation if the ordering is already correct
     1714                                _( sections ).each( function ( section ) {
     1715                                        appendContainer.append( section.container );
     1716                                } );
     1717                        } );
     1718
     1719                        api.section.each( function ( section ) {
     1720                                var controls = section.controls();
     1721                                if ( ! section.panel() ) {
     1722                                        rootNodes.push( section );
     1723                                }
     1724                                appendContainer = section.container.find( 'ul:first' );
     1725                                // @todo Skip doing any DOM manipulation if the ordering is already correct
     1726                                _( controls ).each( function ( control ) {
     1727                                        appendContainer.append( control.container );
     1728                                } );
     1729                        } );
     1730
     1731                        // Sort the root elements
     1732                        rootNodes.sort( function ( a, b ) {
     1733                                return a.priority() - b.priority();
     1734                        } );
     1735                        appendContainer = $( '#customize-theme-controls > ul' );
     1736                        // @todo Skip doing any DOM manipulation if the ordering is already correct
     1737                        _( rootNodes ).each( function ( rootNode ) {
     1738                                appendContainer.append( rootNode.container );
     1739                        } );
     1740
     1741                        // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
     1742                        api.panel.each( function ( panel ) {
     1743                                var value = panel.active();
     1744                                panel.active.callbacks.fireWith( panel.active, [ value, value ] );
     1745                        } );
     1746                        api.section.each( function ( section ) {
     1747                                var value = section.active();
     1748                                section.active.callbacks.fireWith( section.active, [ value, value ] );
     1749                        } );
     1750
     1751                        if ( activeElement ) {
     1752                                activeElement.focus();
     1753                        }
     1754                }, api );
     1755                api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
     1756                $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
     1757                        values.bind( 'add', api.reflowPaneContents );
     1758                        values.bind( 'change', api.reflowPaneContents );
     1759                        values.bind( 'remove', api.reflowPaneContents );
     1760                } );
     1761                api.bind( 'ready', api.reflowPaneContents );
     1762
    11221763                // Check if preview url is valid and load the preview frame.
    11231764                if ( api.previewer.previewUrl() ) {
    11241765                        api.previewer.refresh();
     
    11831824                        event.preventDefault();
    11841825                });
    11851826
     1827                // Go back to the top-level Customizer accordion.
     1828                $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
     1829                        if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     1830                                return;
     1831                        }
     1832
     1833                        e.preventDefault(); // Keep this AFTER the key filter above
     1834                        api.panel.each( function ( panel ) {
     1835                                panel.collapse();
     1836                        });
     1837                });
     1838
    11861839                closeBtn.keydown( function( event ) {
    11871840                        if ( 9 === event.which ) // tab
    11881841                                return;
  • 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..607926b 100644
     
    404404         * @augments wp.customize.Control
    405405         */
    406406        api.Widgets.WidgetControl = api.Control.extend({
     407                defaultExpandedArguments: {},
     408
     409                initialize: function ( id, options ) {
     410                        var control = this;
     411                        api.Control.prototype.initialize.call( control, id, options );
     412                        control.expanded = new api.Value();
     413                        control.expandedArgumentsQueue = [];
     414                        control.expanded.bind( function ( expanded ) {
     415                                var args = control.expandedArgumentsQueue.shift();
     416                                args = $.extend( {}, control.defaultExpandedArguments, args );
     417                                control.onToggleExpanded( expanded, args );
     418                        });
     419                        control.expanded.set( false );
     420                },
     421
    407422                /**
    408423                 * Set up the control
    409424                 */
     
    529544                                if ( sidebarWidgetsControl.isReordering ) {
    530545                                        return;
    531546                                }
    532                                 self.toggleForm();
     547                                self.expanded( ! self.expanded() );
    533548                        } );
    534549
    535550                        $closeBtn = this.container.find( '.widget-control-close' );
    536551                        $closeBtn.on( 'click', function( e ) {
    537552                                e.preventDefault();
    538                                 self.collapseForm();
     553                                self.collapse();
    539554                                self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
    540555                        } );
    541556                },
     
    778793                 *
    779794                 * @param {Boolean} active
    780795                 */
    781                 toggle: function ( active ) {
     796                onToggleActive: function ( active ) {
     797                        // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
    782798                        this.container.toggleClass( 'widget-rendered', active );
    783799                },
    784800
     
    11011117                 * Expand the accordion section containing a control
    11021118                 */
    11031119                expandControlSection: function() {
    1104                         var $section = this.container.closest( '.accordion-section' );
     1120                        api.section( this.section() ).expand();
     1121                },
    11051122
    1106                         if ( ! $section.hasClass( 'open' ) ) {
    1107                                 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
    1108                         }
     1123                /**
     1124                 * Expand the widget form control
     1125                 */
     1126                expand: function () {
     1127                        this.expanded( true );
    11091128                },
    11101129
    11111130                /**
    11121131                 * Expand the widget form control
     1132                 *
     1133                 * @deprecated alias of expand()
    11131134                 */
    11141135                expandForm: function() {
    1115                         this.toggleForm( true );
     1136                        this.expand();
     1137                },
     1138
     1139                /**
     1140                 * Collapse the widget form control
     1141                 */
     1142                collapse: function () {
     1143                        this.expanded( false );
    11161144                },
    11171145
    11181146                /**
    11191147                 * Collapse the widget form control
     1148                 *
     1149                 * @deprecated alias of expand()
    11201150                 */
    11211151                collapseForm: function() {
    1122                         this.toggleForm( false );
     1152                        this.collapse();
    11231153                },
    11241154
    11251155                /**
    11261156                 * Expand or collapse the widget control
    11271157                 *
     1158                 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
     1159                 *
    11281160                 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
    11291161                 */
    11301162                toggleForm: function( showOrHide ) {
    1131                         var self = this, $widget, $inside, complete;
     1163                        if ( typeof showOrHide === 'undefined' ) {
     1164                                showOrHide = ! this.expanded();
     1165                        }
     1166                        this.expanded( showOrHide );
     1167                },
     1168
     1169                /**
     1170                 * Respond to change in the expanded state.
     1171                 *
     1172                 * @param {Boolean} expanded
     1173                 * @param {Object} args  merged on top of this.defaultActiveArguments
     1174                 */
     1175                onToggleExpanded: function ( expanded, args ) {
    11321176
     1177                        var self = this, $widget, $inside, complete;
    11331178                        $widget = this.container.find( 'div.widget:first' );
    11341179                        $inside = $widget.find( '.widget-inside:first' );
    1135                         if ( typeof showOrHide === 'undefined' ) {
    1136                                 showOrHide = ! $inside.is( ':visible' );
    1137                         }
    11381180
    1139                         // Already expanded or collapsed, so noop
    1140                         if ( $inside.is( ':visible' ) === showOrHide ) {
    1141                                 return;
    1142                         }
     1181                        if ( expanded ) {
     1182
     1183                                self.expandControlSection();
    11431184
    1144                         if ( showOrHide ) {
    11451185                                // Close all other widget controls before expanding this one
    11461186                                api.control.each( function( otherControl ) {
    11471187                                        if ( self.params.type === otherControl.params.type && self !== otherControl ) {
    1148                                                 otherControl.collapseForm();
     1188                                                otherControl.collapse();
    11491189                                        }
    11501190                                } );
    11511191
     
    11641204                                self.container.trigger( 'expand' );
    11651205                                self.container.addClass( 'expanding' );
    11661206                        } else {
     1207
    11671208                                complete = function() {
    11681209                                        self.container.removeClass( 'collapsing' );
    11691210                                        self.container.removeClass( 'expanded' );
     
    11891230                 * the first input in the control
    11901231                 */
    11911232                focus: function() {
    1192                         this.expandControlSection();
    1193                         this.expandForm();
     1233                        this.expand();
    11941234                        this.container.find( '.widget-content :focusable:first' ).focus();
    11951235                },
    11961236
     
    13251365                                registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
    13261366
    13271367                        this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
    1328                                 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
     1368                                var widgetFormControls, removedWidgetIds, priority;
    13291369
    13301370                                removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
    13311371
     
    13501390                                widgetFormControls.sort( function( a, b ) {
    13511391                                        var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
    13521392                                                bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
     1393                                        return aIndex - bIndex;
     1394                                });
    13531395
    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 );
     1396                                priority = 0;
     1397                                _( widgetFormControls ).each( function ( control ) {
     1398                                        control.priority( priority );
     1399                                        control.section( self.section() );
     1400                                        priority += 1;
     1401                                });
     1402                                self.priority( priority ); // Make sure sidebar control remains at end
    13681403
    13691404                                // Re-sort widget form controls (including widgets form other sidebars newly moved here)
    13701405                                self._applyCardinalOrderClassNames();
     
    14341469                        // Update the model with whether or not the sidebar is rendered
    14351470                        self.active.bind( function ( active ) {
    14361471                                registeredSidebar.set( 'is_rendered', active );
     1472                                api.section( self.section.get() ).active( active );
    14371473                        } );
    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                         }
     1474                        api.section( self.section.get() ).active( self.active() );
    14671475                },
    14681476
    14691477                /**
     
    14961504                        /**
    14971505                         * Expand other Customizer sidebar section when dragging a control widget over it,
    14981506                         * allowing the control to be dropped into another section
     1507                         *
     1508                         * @todo The wp.customize.Section API should accomodate forcing a single accordion open
    14991509                         */
    15001510                        this.$controlSection.find( '.accordion-section-title' ).droppable({
    15011511                                accept: '.customize-control-widget_form',
     
    15481558                 * Add classes to the widget_form controls to assist with styling
    15491559                 */
    15501560                _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 );
     1561                        var widgetControls = [];
     1562                        _.each( this.setting(), function ( widgetId ) {
     1563                                var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
     1564                                if ( widgetControl ) {
     1565                                        widgetControls.push( widgetControl );
     1566                                }
     1567                        });
     1568
     1569                        if ( ! widgetControls.length ) {
     1570                                return;
     1571                        }
     1572
     1573                        $( widgetControls ).each( function () {
     1574                                $( this.container )
     1575                                        .removeClass( 'first-widget' )
     1576                                        .removeClass( 'last-widget' )
     1577                                        .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
     1578                        });
    15551579
    1556                         this.$sectionContent.find( '.customize-control-widget_form:first' )
     1580                        _.first( widgetControls ).container
    15571581                                .addClass( 'first-widget' )
    15581582                                .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
    15591583
    1560                         this.$sectionContent.find( '.customize-control-widget_form:last' )
     1584                        _.last( widgetControls ).container
    15611585                                .addClass( 'last-widget' )
    15621586                                .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
    15631587                },
     
    15841608
    15851609                        if ( showOrHide ) {
    15861610                                _( this.getWidgetFormControls() ).each( function( formControl ) {
    1587                                         formControl.collapseForm();
     1611                                        formControl.collapse();
    15881612                                } );
    15891613
    15901614                                this.$sectionContent.find( '.first-widget .move-widget' ).focus();
     
    16511675
    16521676                        $widget = $( controlHtml );
    16531677
     1678                        // @todo need to pass this in as the control's 'content' property
    16541679                        $control = $( '<li/>' )
    16551680                                .addClass( 'customize-control' )
    16561681                                .addClass( 'customize-control-' + controlType )
     
    16741699                        }
    16751700                        $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
    16761701
     1702                        // @todo Eliminate this
    16771703                        this.container.after( $control );
    16781704
    16791705                        // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
     
    17331759
    17341760                        $control.slideDown( function() {
    17351761                                if ( isExistingWidget ) {
    1736                                         widgetFormControl.expandForm();
     1762                                        widgetFormControl.expand();
    17371763                                        widgetFormControl.updateWidget( {
    17381764                                                instance: widgetFormControl.setting(),
    17391765                                                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 7937d2d..129a85f 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;
    221223                $this->json['active'] = $this->active();
     224                $this->json['section'] = $this->section;
     225                $this->json['content'] = $this->get_content();
     226        }
     227
     228        /**
     229         * Get the data to export to the client via JSON.
     230         *
     231         * @since 4.1.0
     232         *
     233         * @return array
     234         */
     235        public function json() {
     236                $this->to_json();
     237                return $this->json;
    222238        }
    223239
    224240        /**
    class WP_Customize_Control { 
    242258        }
    243259
    244260        /**
     261         * Get the control's content for insertion into the Customizer pane.
     262         *
     263         * @since 4.1.0
     264         *
     265         * @return string
     266         */
     267        public final function get_content() {
     268                ob_start();
     269                $this->maybe_render();
     270                $template = trim( ob_get_contents() );
     271                ob_end_clean();
     272                return $template;
     273        }
     274
     275        /**
    245276         * Check capabilities and render the control.
    246277         *
    247278         * @since 3.4.0
    class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control { 
    10311062/**
    10321063 * Widget Area Customize Control Class
    10331064 *
     1065 * @since 3.9.0
    10341066 */
    10351067class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
    10361068        public $type = 'sidebar_widgets';
    class WP_Widget_Area_Customize_Control extends WP_Customize_Control { 
    10721104
    10731105/**
    10741106 * Widget Form Customize Control Class
     1107 *
     1108 * @since 3.9.0
    10751109 */
    10761110class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
    10771111        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 febd8bc..ac70bdb 100644
    final class WP_Customize_Manager { 
    491491                $settings = array(
    492492                        'values'  => array(),
    493493                        'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
     494                        'activePanels' => array(),
     495                        'activeSections' => array(),
    494496                        'activeControls' => array(),
    495497                );
    496498
    final class WP_Customize_Manager { 
    504506                foreach ( $this->settings as $id => $setting ) {
    505507                        $settings['values'][ $id ] = $setting->js_value();
    506508                }
     509                foreach ( $this->panels as $id => $panel ) {
     510                        $settings['activePanels'][ $id ] = $panel->active();
     511                }
     512                foreach ( $this->sections as $id => $section ) {
     513                        $settings['activeSections'][ $id ] = $section->active();
     514                }
    507515                foreach ( $this->controls as $id => $control ) {
    508516                        $settings['activeControls'][ $id ] = $control->active();
    509517                }
    final class WP_Customize_Manager { 
    878886
    879887                        if ( ! $section->panel ) {
    880888                                // Top-level section.
    881                                 $sections[] = $section;
     889                                $sections[ $section->id ] = $section;
    882890                        } else {
    883891                                // This section belongs to a panel.
    884892                                if ( isset( $this->panels [ $section->panel ] ) ) {
    885                                         $this->panels[ $section->panel ]->sections[] = $section;
     893                                        $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
    886894                                }
    887895                        }
    888896                }
    final class WP_Customize_Manager { 
    899907                                continue;
    900908                        }
    901909
    902                         usort( $panel->sections, array( $this, '_cmp_priority' ) );
    903                         $panels[] = $panel;
     910                        uasort( $panel->sections, array( $this, '_cmp_priority' ) );
     911                        $panels[ $panel->id ] = $panel;
    904912                }
    905913                $this->panels = $panels;
    906914
  • 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..c253940
    - +  
     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                        staticProp: 'staticPropValue'
     16                }
     17        );
     18        test( 'FooSuperClass is a function ', function () {
     19                equal( typeof FooSuperClass, 'function' );
     20        });
     21        test( 'FooSuperClass prototype has protoProp', function () {
     22                equal( FooSuperClass.prototype.protoProp, 'protoPropValue' );
     23        });
     24        test( 'FooSuperClass does not have protoProp', function () {
     25                equal( typeof FooSuperClass.protoProp, 'undefined' );
     26        });
     27        test( 'FooSuperClass has staticProp', function () {
     28                equal( FooSuperClass.staticProp, 'staticPropValue' );
     29        });
     30        test( 'FooSuperClass prototype does not have staticProp', function () {
     31                equal( typeof FooSuperClass.prototype.staticProp, 'undefined' );
     32        });
     33
     34        foo = new FooSuperClass( { instanceProp: 'instancePropValue' } );
     35        test( 'FooSuperClass instance foo extended Class', function () {
     36                equal( foo.extended( wp.customize.Class ), true );
     37        });
     38        test( 'foo instance has protoProp', function () {
     39                equal( foo.protoProp, 'protoPropValue' );
     40        });
     41        test( 'foo instance does not have staticProp', function () {
     42                equal( typeof foo.staticProp, 'undefined' );
     43        });
     44        test( 'FooSuperClass instance foo ran initialize() and has supplied instanceProp', function () {
     45                equal( foo.instanceProp, 'instancePropValue' );
     46        });
     47
     48        // @todo Test Class.constructor() manipulation
     49        // @todo Test Class.applicator?
     50        // @todo do we test object.instance?
     51
     52
     53        module( 'Customize Base: Subclass' );
     54
     55        BarSubClass = FooSuperClass.extend(
     56                {
     57                        initialize: function ( instanceProps ) {
     58                                FooSuperClass.prototype.initialize.call( this, instanceProps );
     59                                this.subInstanceProp = 'subInstancePropValue';
     60                        },
     61                        subProtoProp: 'subProtoPropValue'
     62                },
     63                {
     64                        subStaticProp: 'subStaticPropValue'
     65                }
     66        );
     67        test( 'BarSubClass prototype has subProtoProp', function () {
     68                equal( BarSubClass.prototype.subProtoProp, 'subProtoPropValue' );
     69        });
     70        test( 'BarSubClass prototype has parent FooSuperClass protoProp', function () {
     71                equal( BarSubClass.prototype.protoProp, 'protoPropValue' );
     72        });
     73
     74        bar = new BarSubClass( { instanceProp: 'instancePropValue' } );
     75        test( 'BarSubClass instance bar its initialize() and parent initialize() run', function () {
     76                equal( bar.instanceProp, 'instancePropValue' );
     77                equal( bar.subInstanceProp, 'subInstancePropValue' );
     78        });
     79
     80        test( 'BarSubClass instance bar extended FooSuperClass', function () {
     81                equal( bar.extended( FooSuperClass ), true );
     82        });
     83
     84});