Make WordPress Core

Ticket #28709: 28709.wip.5.diff

File 28709.wip.5.diff, 39.8 KB (added by westonruter, 10 years ago)

https://github.com/xwpco/wordpress-develop/compare/cdf7b96c9ea88f962b707434cebca993d9f56bba...c7816e1e777f25e0b52a3935b0b765455a11a8cc

  • src/wp-admin/customize.php

    diff --git src/wp-admin/customize.php src/wp-admin/customize.php
    index 7828ee4..109bc07 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' ); 
    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..63e14e8 100644
     
    5858                });
    5959        });
    6060
    61         var sectionContent = $( '.accordion-section-content' );
    62 
    6361        /**
    6462         * Close the current accordion section and open a new one.
    6563         *
     
    6967        function accordionSwitch ( el ) {
    7068                var section = el.closest( '.accordion-section' ),
    7169                        siblings = section.closest( '.accordion-container' ).find( '.open' ),
    72                         content = section.find( sectionContent );
     70                        content = section.find( '.accordion-section-content' );
    7371
    7472                // This section has no content and cannot be expanded.
    7573                if ( section.hasClass( 'cannot-expand' ) ) {
     
    8785                        content.toggle( true ).slideToggle( 150 );
    8886                } else {
    8987                        siblings.removeClass( 'open' );
    90                         siblings.find( sectionContent ).show().slideUp( 150 );
     88                        siblings.find( '.accordion-section-content' ).show().slideUp( 150 );
    9189                        content.toggle( false ).slideToggle( 150 );
    9290                        section.toggleClass( 'open' );
    9391                }
     
    125123                } else {
    126124                        // Close all open sections in any accordion level.
    127125                        siblings.removeClass( 'open' );
    128                         siblings.find( sectionContent ).show().slideUp( 0 );
     126                        siblings.find( '.accordion-section-content' ).show().slideUp( 0 );
    129127                        content.show( 0, function() {
    130128                                position = content.offset().top;
    131129                                scroll = container.scrollTop();
  • 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..a0688a7 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                slideSpeed: 150,
    4157
    42                         this.params = {};
    43                         $.extend( this, options || {} );
     58                initialize: function ( id, options ) {
     59                        var self = this;
     60                        self.id = id;
     61                        self.params = {};
     62                        $.extend( self, options || {} );
     63                        self.container = $( self.params.content );
     64
     65                        self.priority = new api.Value();
     66                        self.active = new api.Value();
     67                        self.expanded = new api.Value();
     68
     69                        self.active.bind( function ( active ) {
     70                                self.toggleActive( active && self.isContextuallyActive() );
     71                                // @todo trigger 'activatged' and 'deactivated' events based on the expanded param?
     72                        });
     73                        self.expanded.bind( function ( expanded ) {
     74                                self.toggleExpanded( expanded );
     75                                // @todo trigger 'expanded' and 'collapsed' events based on the expanded param?
     76                        });
    4477
    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 );
     78                        self.attachEvents();
    4979
    50                         settings = $.map( this.params.settings, function( value ) {
    51                                 return value;
     80                        bubbleChildValueChanges( self, [ 'priority', 'active' ] );
     81
     82                        self.priority.set( isNaN( self.params.priority ) ? 100 : self.params.priority );
     83                        self.active.set( true ); // @todo pass from params, eventually from active_callback when defining panel/section
     84                        self.expanded.set( false ); // @todo True if deeplinking?
     85                },
     86
     87                /**
     88                 * Get the child models associated with this parent, sorting them by their priority Value.
     89                 *
     90                 * @param {String} parentType
     91                 * @param {String} childType
     92                 * @returns {Array}
     93                 */
     94                _children: function ( parentType, childType ) {
     95                        var parent = this,
     96                                children = [];
     97                        api[ childType ].each( function ( child ) {
     98                                if ( child[ parentType ].get() === parent.id ) {
     99                                        children.push( child );
     100                                }
     101                        } );
     102                        children.sort( function ( a, b ) {
     103                                return a.priority() - b.priority();
     104                        } );
     105                        return children;
     106                },
     107
     108                /**
     109                 * To override by subclass, to return whether the container has active children.
     110                 */
     111                isContextuallyActive: function () {
     112                        throw new Error( 'Must override with subclass.' );
     113                },
     114
     115                /**
     116                 * Handle changes to the active state.
     117                 * This does not change the active state, it merely handles the behavior
     118                 * for when it does change.
     119                 *
     120                 * To override by subclass, update the container's UI to reflect the provided active state.
     121                 *
     122                 * @param {Boolean} active
     123                 */
     124                toggleActive: function ( active ) {
     125                        if ( active ) {
     126                                this.container.stop( true, true ).slideDown();
     127                        } else {
     128                                this.container.stop( true, true ).slideUp();
     129                        }
     130                },
     131
     132                /**
     133                 *
     134                 */
     135                activate: function () {
     136                        this.active.set( true );
     137                },
     138
     139                /**
     140                 *
     141                 */
     142                deactivate: function () {
     143                        this.active.set( false );
     144                },
     145
     146                /**
     147                 * To override by subclass, update the container's UI to reflect the provided active state.
     148                 */
     149                toggleExpanded: function () {
     150                        throw new Error( 'Must override with subclass.' );
     151                },
     152
     153                /**
     154                 *
     155                 */
     156                expand: function () {
     157                        this.expanded.set( true );
     158                },
     159
     160                /**
     161                 *
     162                 */
     163                collapse: function () {
     164                        this.expanded( false );
     165                },
     166
     167                /**
     168                 *
     169                 */
     170                focus: function () {
     171                        throw new Error( 'focus method must be overridden' );
     172                }
     173        });
     174
     175        /**
     176         * @constructor
     177         * @augments wp.customize.Class
     178         */
     179        api.Section = Container.extend({
     180
     181                /**
     182                 * @param {String} id
     183                 * @param {Array} options
     184                 */
     185                initialize: function ( id, options ) {
     186                        var section = this;
     187                        Container.prototype.initialize.call( section, id, options );
     188
     189                        section.panel = new api.Value();
     190                        section.panel.bind( function ( id ) {
     191                                $( section.container ).toggleClass( 'control-subsection', !! id );
    52192                        });
     193                        section.panel.set( section.params.panel || '' );
     194                        bubbleChildValueChanges( section, [ 'panel' ] );
     195                },
    53196
    54                         api.apply( api, settings.concat( function() {
    55                                 var key;
     197                /**
     198                 *
     199                 */
     200                embed: function ( readyCallback ) {
     201                        var panel_id,
     202                                section = this;
     203
     204                        panel_id = this.panel.get();
     205                        if ( ! panel_id ) {
     206                                $( '#customize-theme-controls > ul' ).append( section.container );
     207                                readyCallback();
     208                        } else {
     209                                api.panel( panel_id, function ( panel ) {
     210                                        panel.embed();
     211                                        panel.container.find( 'ul:first' ).append( section.container );
     212                                        readyCallback();
     213                                } );
     214                        }
     215                },
    56216
    57                                 control.settings = {};
    58                                 for ( key in control.params.settings ) {
    59                                         control.settings[ key ] = api( control.params.settings[ key ] );
     217                /**
     218                 * Add behaviors for the accordion section
     219                 */
     220                attachEvents: function () {
     221                        var section = this;
     222
     223                        // Expand/Collapse accordion sections on click.
     224                        section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
     225                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     226                                        return;
    60227                                }
     228                                e.preventDefault(); // Keep this AFTER the key filter above
    61229
    62                                 control.setting = control.settings['default'] || null;
    63                                 control.ready();
    64                         }) );
     230                                if ( section.expanded() ) {
     231                                        section.collapse();
     232                                } else {
     233                                        section.expand();
     234                                }
     235                        });
     236                },
     237
     238                /**
     239                 * Return whether this section has any active controls.
     240                 *
     241                 * @returns {boolean}
     242                 */
     243                isContextuallyActive: function () {
     244                        var section = this,
     245                                controls = section.controls(),
     246                                activeCount = 0;
     247                        _( controls ).each( function ( control ) {
     248                                if ( control.active() ) {
     249                                        activeCount += 1;
     250                                }
     251                        } );
     252                        return ( activeCount !== 0 );
     253                },
     254
     255                /**
     256                 * Get the controls that are associated with this section, sorted by their priority Value.
     257                 *
     258                 * @returns {Array}
     259                 */
     260                controls: function () {
     261                        return this._children( 'section', 'control' );
     262                },
     263
     264                /**
     265                 * Update UI to reflect expanded state
     266                 *
     267                 * @param {Boolean} expanded
     268                 */
     269                toggleExpanded: function ( expanded ) {
     270                        var section = this,
     271                                content = section.container.find( '.accordion-section-content' );
     272
     273                        if ( expanded ) {
     274
     275                                if ( section.panel() ) {
     276                                        api.panel( section.panel() ).expand();
     277                                }
     278
     279                                api.section.each( function ( otherSection ) {
     280                                        if ( otherSection !== section ) {
     281                                                otherSection.collapse();
     282                                        }
     283                                });
     284
     285                                content.stop().slideDown( section.slideSpeed );
     286                                section.container.addClass( 'open' );
     287                        } else {
     288
     289                                section.container.removeClass( 'open' );
     290                                content.slideUp( section.slideSpeed );
     291                        }
     292                },
     293
     294                /**
     295                 * Bring the containing panel into view and then expand this section and bring it into view
     296                 *
     297                 * @todo This is an alias for expand(); do we need it?
     298                 */
     299                focus: function () {
     300                        var section = this;
     301                        // @todo What if it is not active? Return false?
     302                        section.expand();
     303                }
     304        });
     305
     306        /**
     307         * @constructor
     308         * @augments wp.customize.Class
     309         */
     310        api.Panel = Container.extend({
     311                initialize: function ( id, options ) {
     312                        var panel = this;
     313                        Container.prototype.initialize.call( panel, id, options );
     314                },
     315
     316                /**
     317                 *
     318                 */
     319                embed: function ( readyCallback ) {
     320                        $( '#customize-theme-controls > ul' ).append( this.container );
     321                        if ( readyCallback ) {
     322                                readyCallback();
     323                        }
     324                },
     325
     326                /**
     327                 *
     328                 */
     329                attachEvents: function () {
     330                        var meta, panel = this;
     331
     332                        // Expand/Collapse accordion sections on click.
     333                        panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
     334                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     335                                        return;
     336                                }
     337                                e.preventDefault(); // Keep this AFTER the key filter above
     338
     339                                if ( ! panel.expanded() ) {
     340                                        panel.expand();
     341                                }
     342                        });
     343
     344                        meta = panel.container.find( '.panel-meta:first' );
     345
     346                        meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
     347                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     348                                        return;
     349                                }
     350                                e.preventDefault(); // Keep this AFTER the key filter above
     351
     352                                if ( meta.hasClass( 'cannot-expand' ) ) {
     353                                        return;
     354                                }
     355
     356                                var content = meta.find( '.accordion-section-content:first' );
     357                                if ( meta.hasClass( 'open' ) ) {
     358                                        meta.toggleClass( 'open' );
     359                                        content.slideUp( 150 );
     360                                } else {
     361                                        content.slideDown( 150 );
     362                                        meta.toggleClass( 'open' );
     363                                }
     364                        });
     365
     366                },
     367
     368                /**
     369                 * Get the sections that are associated with this panel, sorted by their priority Value.
     370                 *
     371                 * @returns {Array}
     372                 */
     373                sections: function () {
     374                        return this._children( 'panel', 'section' );
     375                },
     376
     377                /**
     378                 * Return whether this section has any active sections.
     379                 *
     380                 * @returns {boolean}
     381                 */
     382                isContextuallyActive: function () {
     383                        var panel = this,
     384                                sections = panel.sections(),
     385                                activeCount = 0;
     386                        _( sections ).each( function ( section ) {
     387                                if ( section.active() && section.isContextuallyActive() ) {
     388                                        activeCount += 1;
     389                                }
     390                        } );
     391                        return ( activeCount !== 0 );
     392                },
     393
     394                /**
     395                 * Update UI to reflect expanded state
     396                 *
     397                 * @param {Boolean} expanded
     398                 */
     399                toggleExpanded: function ( expanded ) {
     400                        var position, scroll,
     401                                panel = this,
     402                                section = panel.container.closest( '.accordion-section' ),
     403                                overlay = section.closest( '.wp-full-overlay' ),
     404                                container = section.closest( '.accordion-container' ),
     405                                siblings = container.find( '.open' ),
     406                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
     407                                backBtn = overlay.find( '.control-panel-back' ),
     408                                panelTitle = section.find( '.accordion-section-title' ).first(),
     409                                content = section.find( '.control-panel-content' );
     410
     411                        if ( expanded ) {
     412
     413                                // Collapse any sibling sections/panels
     414                                api.section.each( function ( section ) {
     415                                        if ( ! section.panel() ) {
     416                                                section.collapse(); // @todo If any sections are open, then the position calculation below will fire too early
     417                                        }
     418                                });
     419                                api.panel.each( function ( otherPanel ) {
     420                                        if ( panel !== otherPanel ) {
     421                                                otherPanel.collapse(); // @todo the position calculation below probably will fire too early
     422                                        }
     423                                });
     424
     425                                content.show( 0, function() {
     426                                        position = content.offset().top;
     427                                        scroll = container.scrollTop();
     428                                        content.css( 'margin-top', ( 45 - position - scroll ) );
     429                                        section.addClass( 'current-panel' );
     430                                        overlay.addClass( 'in-sub-panel' );
     431                                        container.scrollTop( 0 );
     432                                } );
     433                                topPanel.attr( 'tabindex', '-1' );
     434                                backBtn.attr( 'tabindex', '0' );
     435                                backBtn.focus();
     436                        } else {
     437                                siblings.removeClass( 'open' );
     438                                section.removeClass( 'current-panel' );
     439                                overlay.removeClass( 'in-sub-panel' );
     440                                content.delay( 180 ).hide( 0, function() {
     441                                        content.css( 'margin-top', 'inherit' ); // Reset
     442                                } );
     443                                topPanel.attr( 'tabindex', '0' );
     444                                backBtn.attr( 'tabindex', '-1' );
     445                                panelTitle.focus();
     446                                container.scrollTop( 0 );
     447                        }
     448                },
     449
     450                /**
     451                 * Bring the containing panel into view and then expand this section and bring it into view
     452                 */
     453                focus: function () {
     454                        var panel = this;
     455                        // @todo What if it is not active? Return false?
     456                        panel.expand();
     457                }
     458
     459                // @todo Need to first exit out of the Panel
     460        });
     461
     462        /**
     463         * @constructor
     464         * @augments wp.customize.Class
     465         */
     466        api.Control = api.Class.extend({
     467                initialize: function( id, options ) {
     468                        var control = this,
     469                                nodes, radios, settings;
     470
     471                        control.params = {};
     472                        $.extend( control, options || {} );
     473
     474                        control.id = id;
     475                        control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
     476                        control.container = control.params.content ? $( control.params.content ) : $( control.selector );
     477
     478                        control.section = new api.Value();
     479                        control.priority = new api.Value();
     480                        control.active = new api.Value();
    65481
    66482                        control.elements = [];
    67483
    68                         nodes  = this.container.find('[data-customize-setting-link]');
     484                        nodes  = control.container.find('[data-customize-setting-link]');
    69485                        radios = {};
    70486
    71487                        nodes.each( function() {
    72                                 var node = $(this),
     488                                var node = $( this ),
    73489                                        name;
    74490
    75                                 if ( node.is(':radio') ) {
    76                                         name = node.prop('name');
    77                                         if ( radios[ name ] )
     491                                if ( node.is( ':radio' ) ) {
     492                                        name = node.prop( 'name' );
     493                                        if ( radios[ name ] ) {
    78494                                                return;
     495                                        }
    79496
    80497                                        radios[ name ] = true;
    81498                                        node = nodes.filter( '[name="' + name + '"]' );
    82499                                }
    83500
    84                                 api( node.data('customizeSettingLink'), function( setting ) {
     501                                api( node.data( 'customizeSettingLink' ), function( setting ) {
    85502                                        var element = new api.Element( node );
    86503                                        control.elements.push( element );
    87504                                        element.sync( setting );
     
    90507                        });
    91508
    92509                        control.active.bind( function ( active ) {
    93                                 control.toggle( active );
     510                                control.toggleActive( active );
     511                        } );
     512
     513                        control.section.set( control.params.section );
     514                        control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
     515                        control.active.set( control.params.active );
     516
     517                        bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
     518
     519                        // Associate this control with its settings when they are created
     520                        settings = $.map( control.params.settings, function( value ) {
     521                                return value;
     522                        });
     523                        api.apply( api, settings.concat( function () {
     524                                var key;
     525
     526                                control.settings = {};
     527                                for ( key in control.params.settings ) {
     528                                        control.settings[ key ] = api( control.params.settings[ key ] );
     529                                }
     530
     531                                control.setting = control.settings['default'] || null;
     532                                control.embed( function () {
     533                                        control.ready();
     534                                });
     535                        }) );
     536                },
     537
     538                /**
     539                 * @param {Function} [readyCallback] Callback to fire when the embedding is done.
     540                 */
     541                embed: function ( readyCallback ) {
     542                        var section_id,
     543                                control = this;
     544
     545                        section_id = control.section.get();
     546                        if ( ! section_id ) {
     547                                throw new Error( 'A control must have an associated section.' );
     548                        }
     549
     550                        // Defer until the associated section is available
     551                        api.section( section_id, function ( section ) {
     552                                section.embed( function () {
     553                                        section.container.find( 'ul:first' ).append( control.container );
     554                                        readyCallback();
     555                                } );
    94556                        } );
    95                         control.toggle( control.active() );
    96557                },
    97558
    98559                /**
     
    101562                ready: function() {},
    102563
    103564                /**
    104                  * Callback for change to the control's active state.
    105                  *
    106                  * Override function for custom behavior for the control being active/inactive.
     565                 * Bring the containing section and panel into view and then this control into view, focusing on the first input
     566                 */
     567                focus: function () {
     568                        throw new Error( 'Not implemented yet' );
     569                },
     570
     571                /**
     572                 * Update UI in response to a change in the control's active state.
     573                 * This does not change the active state, it merely handles the behavior
     574                 * for when it does change.
    107575                 *
    108576                 * @param {Boolean} active
    109577                 */
    110                 toggle: function ( active ) {
     578                toggleActive: function ( active ) {
    111579                        if ( active ) {
    112580                                this.container.slideDown();
    113581                        } else {
     
    115583                        }
    116584                },
    117585
     586                /**
     587                 * @deprecated alias of toggleActive
     588                 */
     589                toggle: function ( active ) {
     590                        return this.toggleActive( active );
     591                },
     592
     593                /**
     594                 * Shorthand way to enable the active state.
     595                 */
     596                activate: function () {
     597                        this.active.set( true );
     598                },
     599
     600                /**
     601                 * Shorthand way to disable the active state.
     602                 */
     603                deactivate: function () {
     604                        this.active.set( false );
     605                },
     606
    118607                dropdownInit: function() {
    119608                        var control      = this,
    120609                                statuses     = this.container.find('.dropdown-status'),
     
    5751064
    5761065        // Create the collection of Control objects.
    5771066        api.control = new api.Values({ defaultConstructor: api.Control });
     1067        api.section = new api.Values({ defaultConstructor: api.Section });
     1068        api.panel = new api.Values({ defaultConstructor: api.Panel });
    5781069
    5791070        /**
    5801071         * @constructor
     
    9791470                image:  api.ImageControl,
    9801471                header: api.HeaderControl
    9811472        };
     1473        api.panelConstructor = {};
     1474        api.sectionConstructor = {};
    9821475
    9831476        $( function() {
    9841477                api.settings = window._wpCustomizeSettings;
     
    10091502                        }
    10101503                });
    10111504
     1505                // Expand/Collapse the main customizer customize info
     1506                $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
     1507                        if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     1508                                return;
     1509                        }
     1510                        e.preventDefault(); // Keep this AFTER the key filter above
     1511
     1512                        var section = $( this ).parent(),
     1513                                content = section.find( '.accordion-section-content:first' );
     1514
     1515                        if ( section.hasClass( 'cannot-expand' ) ) {
     1516                                return;
     1517                        }
     1518
     1519                        if ( section.hasClass( 'open' ) ) {
     1520                                section.toggleClass( 'open' );
     1521                                content.slideUp( 150 );
     1522                        } else {
     1523                                content.slideDown( 150 );
     1524                                section.toggleClass( 'open' );
     1525                        }
     1526                });
     1527
    10121528                // Initialize Previewer
    10131529                api.previewer = new api.Previewer({
    10141530                        container:   '#customize-preview',
     
    11021618                        $.extend( this.nonce, nonce );
    11031619                });
    11041620
     1621                // Create Settings
    11051622                $.each( api.settings.settings, function( id, data ) {
    11061623                        api.create( id, id, data.value, {
    11071624                                transport: data.transport,
     
    11091626                        } );
    11101627                });
    11111628
     1629                // Create Panels
     1630                $.each( api.settings.panels, function ( id, data ) {
     1631                        var constructor = api.panelConstructor[ data.type ] || api.Panel,
     1632                                panel;
     1633
     1634                        panel = new constructor( id, {
     1635                                params: data
     1636                        } );
     1637                        api.panel.add( id, panel );
     1638                });
     1639
     1640                // Create Sections
     1641                $.each( api.settings.sections, function ( id, data ) {
     1642                        var constructor = api.sectionConstructor[ data.type ] || api.Section,
     1643                                section;
     1644
     1645                        section = new constructor( id, {
     1646                                params: data
     1647                        } );
     1648                        api.section.add( id, section );
     1649                });
     1650
     1651                // Create Controls
     1652                // @todo factor this out
    11121653                $.each( api.settings.controls, function( id, data ) {
    11131654                        var constructor = api.controlConstructor[ data.type ] || api.Control,
    11141655                                control;
    11151656
    1116                         control = api.control.add( id, new constructor( id, {
     1657                        control = new constructor( id, {
    11171658                                params: data,
    11181659                                previewer: api.previewer
    1119                         } ) );
     1660                        } );
     1661                        api.control.add( id, control );
    11201662                });
    11211663
     1664                /**
     1665                 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
     1666                 */
     1667                api.reflowPaneContents = _.bind( function () {
     1668
     1669                        var appendContainer, activeElement, rootNodes = [];
     1670
     1671                        if ( document.activeElement ) {
     1672                                activeElement = $( document.activeElement );
     1673                        }
     1674
     1675                        api.panel.each( function ( panel ) {
     1676                                var sections = panel.sections();
     1677                                rootNodes.push( panel );
     1678                                appendContainer = panel.container.find( 'ul:first' );
     1679                                // @todo Skip doing any DOM manipulation if the ordering is already correct
     1680                                _( sections ).each( function ( section ) {
     1681                                        appendContainer.append( section.container );
     1682                                } );
     1683                        } );
     1684
     1685                        api.section.each( function ( section ) {
     1686                                var controls = section.controls();
     1687                                if ( ! section.panel() ) {
     1688                                        rootNodes.push( section );
     1689                                }
     1690                                appendContainer = section.container.find( 'ul:first' );
     1691                                // @todo Skip doing any DOM manipulation if the ordering is already correct
     1692                                _( controls ).each( function ( control ) {
     1693                                        appendContainer.append( control.container );
     1694                                } );
     1695                        } );
     1696
     1697                        // Sort the root elements
     1698                        rootNodes.sort( function ( a, b ) {
     1699                                return a.priority() - b.priority();
     1700                        } );
     1701                        appendContainer = $( '#customize-theme-controls > ul' );
     1702                        // @todo Skip doing any DOM manipulation if the ordering is already correct
     1703                        _( rootNodes ).each( function ( rootNode ) {
     1704                                appendContainer.append( rootNode.container );
     1705                        } );
     1706
     1707                        // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
     1708                        api.panel.each( function ( panel ) {
     1709                                var value = panel.active();
     1710                                panel.active.callbacks.fireWith( panel.active, [ value, value ] );
     1711                        } );
     1712                        api.section.each( function ( section ) {
     1713                                var value = section.active();
     1714                                section.active.callbacks.fireWith( section.active, [ value, value ] );
     1715                        } );
     1716
     1717                        if ( activeElement ) {
     1718                                activeElement.focus();
     1719                        }
     1720                }, api );
     1721                api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
     1722                $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
     1723                        values.bind( 'add', api.reflowPaneContents );
     1724                        values.bind( 'change', api.reflowPaneContents );
     1725                        values.bind( 'remove', api.reflowPaneContents );
     1726                } );
     1727                api.bind( 'ready', api.reflowPaneContents );
     1728
    11221729                // Check if preview url is valid and load the preview frame.
    11231730                if ( api.previewer.previewUrl() ) {
    11241731                        api.previewer.refresh();
     
    11831790                        event.preventDefault();
    11841791                });
    11851792
     1793                // Go back to the top-level Customizer accordion.
     1794                $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
     1795                        if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     1796                                return;
     1797                        }
     1798
     1799                        e.preventDefault(); // Keep this AFTER the key filter above
     1800                        api.panel.each( function ( panel ) {
     1801                                panel.collapse();
     1802                        });
     1803                });
     1804
    11861805                closeBtn.keydown( function( event ) {
    11871806                        if ( 9 === event.which ) // tab
    11881807                                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..60ed1c1 100644
     
    404404         * @augments wp.customize.Control
    405405         */
    406406        api.Widgets.WidgetControl = api.Control.extend({
     407
     408                initialize: function ( id, options ) {
     409                        var control = this;
     410                        api.Control.prototype.initialize.call( control, id, options );
     411                        control.expanded = new api.Value();
     412                        control.expanded.bind( function ( expanded ) {
     413                                control.toggleExpanded( expanded );
     414                        });
     415                        control.expanded.set( false );
     416                },
     417
    407418                /**
    408419                 * Set up the control
    409420                 */
     
    529540                                if ( sidebarWidgetsControl.isReordering ) {
    530541                                        return;
    531542                                }
    532                                 self.toggleForm();
     543                                self.expanded( ! self.expanded() );
    533544                        } );
    534545
    535546                        $closeBtn = this.container.find( '.widget-control-close' );
    536547                        $closeBtn.on( 'click', function( e ) {
    537548                                e.preventDefault();
    538                                 self.collapseForm();
     549                                self.collapse();
    539550                                self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
    540551                        } );
    541552                },
     
    778789                 *
    779790                 * @param {Boolean} active
    780791                 */
    781                 toggle: function ( active ) {
     792                toggleActive: function ( active ) {
    782793                        this.container.toggleClass( 'widget-rendered', active );
    783794                },
    784795
     
    11011112                 * Expand the accordion section containing a control
    11021113                 */
    11031114                expandControlSection: function() {
    1104                         var $section = this.container.closest( '.accordion-section' );
     1115                        api.section( this.section() ).expand();
     1116                },
    11051117
    1106                         if ( ! $section.hasClass( 'open' ) ) {
    1107                                 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
    1108                         }
     1118                /**
     1119                 * Expand the widget form control
     1120                 */
     1121                expand: function () {
     1122                        this.expanded( true );
    11091123                },
    11101124
    11111125                /**
    11121126                 * Expand the widget form control
     1127                 *
     1128                 * @deprecated alias of expand()
    11131129                 */
    11141130                expandForm: function() {
    1115                         this.toggleForm( true );
     1131                        this.expand();
    11161132                },
    11171133
    11181134                /**
    11191135                 * Collapse the widget form control
    11201136                 */
     1137                collapse: function () {
     1138                        this.expanded( false );
     1139                },
     1140
     1141                /**
     1142                 * Collapse the widget form control
     1143                 *
     1144                 * @deprecated alias of expand()
     1145                 */
    11211146                collapseForm: function() {
    1122                         this.toggleForm( false );
     1147                        this.collapse();
    11231148                },
    11241149
    11251150                /**
    11261151                 * Expand or collapse the widget control
    11271152                 *
     1153                 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
     1154                 *
    11281155                 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
    11291156                 */
    11301157                toggleForm: function( showOrHide ) {
    1131                         var self = this, $widget, $inside, complete;
     1158                        if ( typeof showOrHide === 'undefined' ) {
     1159                                showOrHide = ! this.expanded();
     1160                        }
     1161                        this.expanded( showOrHide );
     1162                },
    11321163
     1164                /**
     1165                 * Respond to change in the expanded state.
     1166                 *
     1167                 * @param {Boolean} expanded
     1168                 */
     1169                toggleExpanded: function ( expanded ) {
     1170
     1171                        var self = this, $widget, $inside, complete;
    11331172                        $widget = this.container.find( 'div.widget:first' );
    11341173                        $inside = $widget.find( '.widget-inside:first' );
    1135                         if ( typeof showOrHide === 'undefined' ) {
    1136                                 showOrHide = ! $inside.is( ':visible' );
    1137                         }
    11381174
    1139                         // Already expanded or collapsed, so noop
    1140                         if ( $inside.is( ':visible' ) === showOrHide ) {
    1141                                 return;
    1142                         }
     1175                        if ( expanded ) {
     1176
     1177                                self.expandControlSection();
    11431178
    1144                         if ( showOrHide ) {
    11451179                                // Close all other widget controls before expanding this one
    11461180                                api.control.each( function( otherControl ) {
    11471181                                        if ( self.params.type === otherControl.params.type && self !== otherControl ) {
    1148                                                 otherControl.collapseForm();
     1182                                                otherControl.collapse();
    11491183                                        }
    11501184                                } );
    11511185
     
    11641198                                self.container.trigger( 'expand' );
    11651199                                self.container.addClass( 'expanding' );
    11661200                        } else {
     1201
    11671202                                complete = function() {
    11681203                                        self.container.removeClass( 'collapsing' );
    11691204                                        self.container.removeClass( 'expanded' );
     
    11891224                 * the first input in the control
    11901225                 */
    11911226                focus: function() {
    1192                         this.expandControlSection();
    1193                         this.expandForm();
     1227                        this.expand();
    11941228                        this.container.find( '.widget-content :focusable:first' ).focus();
    11951229                },
    11961230
     
    13251359                                registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id );
    13261360
    13271361                        this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
    1328                                 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
     1362                                var widgetFormControls, removedWidgetIds, priority;
    13291363
    13301364                                removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
    13311365
     
    13501384                                widgetFormControls.sort( function( a, b ) {
    13511385                                        var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
    13521386                                                bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
     1387                                        return aIndex - bIndex;
     1388                                });
    13531389
    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 );
     1390                                priority = 0;
     1391                                _( widgetFormControls ).each( function ( control ) {
     1392                                        control.priority( priority );
     1393                                        control.section( self.section() );
     1394                                        priority += 1;
     1395                                });
     1396                                self.priority( priority ); // Make sure sidebar control remains at end
    13681397
    13691398                                // Re-sort widget form controls (including widgets form other sidebars newly moved here)
    13701399                                self._applyCardinalOrderClassNames();
     
    14381467                },
    14391468
    14401469                /**
    1441                  * Show the sidebar section when it becomes visible.
     1470                 * Respond to change in active state to show the sidebar section.
    14421471                 *
    1443                  * Overrides api.Control.toggle()
     1472                 * Overrides api.Control.toggleActive()
    14441473                 *
    14451474                 * @param {Boolean} active
    14461475                 */
    1447                 toggle: function ( active ) {
     1476                toggleActive: function ( active ) {
     1477                        // @todo this seems wrong. It seems we should be linking this.active with api.section( this.section.get() ).active
     1478
    14481479                        var $section, sectionSelector;
    14491480
    14501481                        sectionSelector = '#accordion-section-sidebar-widgets-' + this.params.sidebar_id;
     
    15841615
    15851616                        if ( showOrHide ) {
    15861617                                _( this.getWidgetFormControls() ).each( function( formControl ) {
    1587                                         formControl.collapseForm();
     1618                                        formControl.collapse();
    15881619                                } );
    15891620
    15901621                                this.$sectionContent.find( '.first-widget .move-widget' ).focus();
     
    16511682
    16521683                        $widget = $( controlHtml );
    16531684
     1685                        // @todo need to pass this in as the control's 'content' property
    16541686                        $control = $( '<li/>' )
    16551687                                .addClass( 'customize-control' )
    16561688                                .addClass( 'customize-control-' + controlType )
     
    16741706                        }
    16751707                        $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
    16761708
     1709                        // @todo Eliminate this
    16771710                        this.container.after( $control );
    16781711
    16791712                        // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
     
    17331766
    17341767                        $control.slideDown( function() {
    17351768                                if ( isExistingWidget ) {
    1736                                         widgetFormControl.expandForm();
     1769                                        widgetFormControl.expand();
    17371770                                        widgetFormControl.updateWidget( {
    17381771                                                instance: widgetFormControl.setting(),
    17391772                                                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..47d925b 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
  • 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..5041f82 100644
    final class WP_Customize_Manager { 
    878878
    879879                        if ( ! $section->panel ) {
    880880                                // Top-level section.
    881                                 $sections[] = $section;
     881                                $sections[ $section->id ] = $section;
    882882                        } else {
    883883                                // This section belongs to a panel.
    884884                                if ( isset( $this->panels [ $section->panel ] ) ) {
    885                                         $this->panels[ $section->panel ]->sections[] = $section;
     885                                        $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
    886886                                }
    887887                        }
    888888                }
    final class WP_Customize_Manager { 
    899899                                continue;
    900900                        }
    901901
    902                         usort( $panel->sections, array( $this, '_cmp_priority' ) );
    903                         $panels[] = $panel;
     902                        uasort( $panel->sections, array( $this, '_cmp_priority' ) );
     903                        $panels[ $panel->id ] = $panel;
    904904                }
    905905                $this->panels = $panels;
    906906
  • 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..9ba0295 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        /**
    8693         * Constructor.
    8794         *
    8895         * Any supplied $args override class property defaults.
    class WP_Customize_Panel { 
    110117        }
    111118
    112119        /**
     120         * Gather the parameters passed to client JavaScript via JSON.
     121         *
     122         * @since 4.1.0
     123         *
     124         * @return array The array to be exported to the client as JSON
     125         */
     126        public function json() {
     127                $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) );
     128                $array['content'] = $this->get_content();
     129                return $array;
     130        }
     131
     132        /**
    113133         * Checks required user capabilities and whether the theme has the
    114134         * feature support required by the panel.
    115135         *
    class WP_Customize_Panel { 
    130150        }
    131151
    132152        /**
     153         * Get the panel's content template for insertion into the Customizer pane.
     154         *
     155         * @since 4.1.0
     156         *
     157         * @return string
     158         */
     159        public final function get_content() {
     160                ob_start();
     161                $this->maybe_render();
     162                $template = trim( ob_get_contents() );
     163                ob_end_clean();
     164                return $template;
     165        }
     166
     167        /**
    133168         * Check capabilities and render the panel.
    134169         *
    135170         * @since 4.0.0
    class WP_Customize_Panel { 
    189224         */
    190225        protected function render_content() {
    191226                ?>
    192                 <li class="accordion-section control-section<?php if ( empty( $this->description ) ) echo ' cannot-expand'; ?>">
     227                <li class="panel-meta accordion-section control-section<?php if ( empty( $this->description ) ) { echo ' cannot-expand'; } ?>">
    193228                        <div class="accordion-section-title" tabindex="0">
    194229                                <span class="preview-notice"><?php
    195230                                        /* translators: %s is the site/panel title in the Customizer */
    class WP_Customize_Panel { 
    203238                        <?php endif; ?>
    204239                </li>
    205240                <?php
    206                 foreach ( $this->sections as $section ) {
    207                         $section->maybe_render();
    208                 }
    209241        }
    210242}
  • 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..a905f32 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        /**
    95102         * Constructor.
    96103         *
    97104         * Any supplied $args override class property defaults.
    class WP_Customize_Section { 
    118125        }
    119126
    120127        /**
     128         * Gather the parameters passed to client JavaScript via JSON.
     129         *
     130         * @since 4.1.0
     131         *
     132         * @return array The array to be exported to the client as JSON
     133         */
     134        public function json() {
     135                $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'panel', 'type' ) );
     136                $array['content'] = $this->get_content();
     137                return $array;
     138        }
     139
     140        /**
    121141         * Checks required user capabilities and whether the theme has the
    122142         * feature support required by the section.
    123143         *
    class WP_Customize_Section { 
    136156        }
    137157
    138158        /**
     159         * Get the section's content template for insertion into the Customizer pane.
     160         *
     161         * @since 4.1.0
     162         *
     163         * @return string
     164         */
     165        public final function get_content() {
     166                ob_start();
     167                $this->maybe_render();
     168                $template = trim( ob_get_contents() );
     169                ob_end_clean();
     170                return $template;
     171        }
     172
     173        /**
    139174         * Check capabilities and render the section.
    140175         *
    141176         * @since 3.4.0
    class WP_Customize_Section { 
    172207         */
    173208        protected function render() {
    174209                $classes = 'control-section accordion-section';
    175                 if ( $this->panel ) {
    176                         $classes .= ' control-subsection';
    177                 }
    178210                ?>
    179211                <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>">
    180212                        <h3 class="accordion-section-title" tabindex="0">
    class WP_Customize_Section { 
    183215                        </h3>
    184216                        <ul class="accordion-section-content">
    185217                                <?php if ( ! empty( $this->description ) ) : ?>
    186                                 <li><p class="description customize-section-description"><?php echo $this->description; ?></p></li>
     218                                        <li class="customize-section-description-container">
     219                                                <p class="description customize-section-description"><?php echo $this->description; ?></p>
     220                                        </li>
    187221                                <?php endif; ?>
    188                                 <?php
    189                                 foreach ( $this->controls as $control )
    190                                         $control->maybe_render();
    191                                 ?>
    192222                        </ul>
    193223                </li>
    194224                <?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;