Make WordPress Core

Ticket #28709: 28709.wip.4.diff

File 28709.wip.4.diff, 30.5 KB (added by westonruter, 10 years ago)

https://github.com/xwpco/wordpress-develop/pull/29

  • 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..fbfe90e 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         *
     52         * @constructor
     53         * @augments wp.customize.Class
     54         */
     55        Container = api.Class.extend({
     56                slideSpeed: 150,
     57
     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                                if ( active && self.isContextuallyActive() ) {
     71                                        self.onActivate();
     72                                        // @todo Trigger 'activated' event?
     73                                } else {
     74                                        self.onDeactivate();
     75                                        // @todo Trigger 'deactivated' event?
     76                                }
     77                        });
     78                        self.expanded.bind( function ( expanded ) {
     79                                if ( expanded ) {
     80                                        self.onExpand();
     81                                        // @todo Trigger 'expanded' event?
     82                                } else {
     83                                        self.onCollapse();
     84                                        // @todo Trigger 'collapsed' event?
     85                                }
     86                        });
     87
     88                        self.attachEvents();
     89
     90                        bubbleChildValueChanges( self, [ 'priority', 'active' ] );
     91
     92                        self.priority.set( self.params.priority || 100 );
     93                        self.active.set( true ); // @todo pass from params, eventually from active_callback when defining panel/section
     94                        self.expanded.set( false ); // @todo True if deeplinking?
     95                },
     96
     97                /**
     98                 * Get the child models associated with this parent, sorting them by their priority Value.
     99                 *
     100                 * @param {String} parentType
     101                 * @param {String} childType
     102                 * @returns {Array}
     103                 */
     104                _children: function ( parentType, childType ) {
     105                        var parent = this,
     106                                children = [];
     107                        api[ childType ].each( function ( child ) {
     108                                if ( child[ parentType ].get() === parent.id ) {
     109                                        children.push( child );
     110                                }
     111                        } );
     112                        children.sort( function ( a, b ) {
     113                                return a.priority() - b.priority();
     114                        } );
     115                        return children;
     116                },
     117
     118                /**
     119                 * Change to the section's active state.
     120                 *
     121                 * @param {Boolean} active
     122                 */
     123                toggle: function ( active ) {
     124                        // @todo
     125                        if ( active ) {
     126                                this.deactivate();
     127                        } else {
     128                                this.activate();
     129                        }
     130                },
     131
     132                isContextuallyActive: function () {
     133                        throw new Error( 'Must override with subclass.' );
     134                },
     135
     136                /**
     137                 *
     138                 */
     139                activate: function () {
     140                        this.active.set( true );
     141                        // @todo If it was already active, then re-trigger the events
     142                },
     143
     144                /**
     145                 *
     146                 */
     147                onActivate: function () {
     148                        // @todo Prevent this from proceeding if there are no active controls; as soon as a control becomes active, then this would automatically show
     149                        this.container.stop( true, true ).slideDown(); // @todo Trigger 'activated' event when complete?
     150                },
     151
     152                /**
     153                 *
     154                 */
     155                deactivate: function () {
     156                        this.active.set( false );
     157                        // @todo If it was already inactive, then re-trigger the events
     158                },
     159
     160                /**
     161                 *
     162                 */
     163                onDeactivate: function () {
     164                        this.onCollapse();
     165                        this.container.stop( true, true ).slideUp(); // @todo Trigger 'deactivated' event when complete?
     166                },
     167
     168                /**
     169                 *
     170                 */
     171                expand: function () {
     172                        this.expanded.set( true );
     173                },
     174
     175                /**
     176                 * Show the accordion section contents and collapse all other sections at the same time
     177                 */
     178                onExpand: function () {
     179                        throw new Error( 'onCollapse method must be overridden' );
     180                },
     181
     182                /**
     183                 *
     184                 */
     185                collapse: function () {
     186                        this.expanded( false );
     187                },
     188
     189                /**
     190                 *
     191                 */
     192                onCollapse: function () {
     193                        throw new Error( 'onCollapse method must be overridden' );
     194                },
     195
     196                /**
     197                 *
     198                 */
     199                focus: function () {
     200                        throw new Error( 'focus method must be overridden' );
     201                }
     202        });
     203
     204        /**
     205         * @constructor
     206         * @augments wp.customize.Class
     207         */
     208        api.Section = Container.extend({
     209
     210                /**
     211                 * @param {String} id
     212                 * @param {Array} options
     213                 */
     214                initialize: function ( id, options ) {
     215                        var section = this;
     216                        Container.prototype.initialize.call( section, id, options );
     217
     218                        section.panel = new api.Value();
     219                        section.panel.bind( function ( id ) {
     220                                $( section.container ).toggleClass( 'control-subsection', !! id );
     221                        });
     222                        section.panel.set( section.params.panel || '' );
     223                        bubbleChildValueChanges( section, [ 'panel' ] );
     224                },
     225
     226                /**
     227                 *
     228                 */
     229                embed: function ( readyCallback ) {
     230                        var panel_id,
     231                                section = this;
     232
     233                        panel_id = this.panel.get();
     234                        if ( ! panel_id ) {
     235                                $( '#customize-theme-controls > ul' ).append( section.container );
     236                                readyCallback();
     237                        } else {
     238                                api.panel( panel_id, function ( panel ) {
     239                                        panel.embed();
     240                                        panel.container.find( 'ul:first' ).append( section.container );
     241                                        readyCallback();
     242                                } );
     243                        }
     244                },
     245
     246                /**
     247                 * Add behaviors for the accordion section
     248                 */
     249                attachEvents: function () {
     250                        var section = this;
     251
     252                        // Expand/Collapse accordion sections on click.
     253                        section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
     254                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     255                                        return;
     256                                }
     257                                e.preventDefault(); // Keep this AFTER the key filter above
     258
     259                                if ( section.expanded() ) {
     260                                        section.collapse();
     261                                } else {
     262                                        section.expand();
     263                                }
     264                        });
     265                },
     266
     267                /**
     268                 * Return whether this section has any active controls.
     269                 *
     270                 * @returns {boolean}
     271                 */
     272                isContextuallyActive: function () {
     273                        var section = this,
     274                                controls = section.controls(),
     275                                activeCount = 0;
     276                        _( controls ).each( function ( control ) {
     277                                if ( control.active() ) {
     278                                        activeCount += 1;
     279                                }
     280                        } );
     281                        return ( activeCount !== 0 );
     282                },
     283
     284                /**
     285                 * Get the controls that are associated with this section, sorted by their priority Value.
     286                 *
     287                 * @returns {Array}
     288                 */
     289                controls: function () {
     290                        return this._children( 'section', 'control' );
     291                },
     292
     293                /**
     294                 * Show the accordion section contents and collapse all other sections at the same time
     295                 */
     296                onExpand: function () {
     297                        var section = this,
     298                                content = section.container.find( '.accordion-section-content' );
     299
     300                        if ( section.panel() ) {
     301                                api.panel( section.panel() ).expand();
     302                        }
     303
     304                        api.section.each( function ( otherSection ) {
     305                                if ( otherSection !== section ) {
     306                                        otherSection.collapse();
     307                                }
     308                        });
     309
     310                        content.stop().slideDown( section.slideSpeed );
     311                        section.container.addClass( 'open' );
     312                },
     313
     314                /**
     315                 *
     316                 */
     317                onCollapse: function () {
     318                        var section = this,
     319                                content = section.container.find( '.accordion-section-content' );
     320
     321                        section.container.removeClass( 'open' );
     322                        content.slideUp( section.slideSpeed );
     323                },
     324
     325                /**
     326                 * Bring the containing panel into view and then expand this section and bring it into view
     327                 *
     328                 * @todo This is an alias for expand(); do we need it?
     329                 */
     330                focus: function () {
     331                        var section = this;
     332                        // @todo What if it is not active? Return false?
     333                        section.expand();
     334                }
     335        });
     336
     337        /**
     338         * @constructor
     339         * @augments wp.customize.Class
     340         */
     341        api.Panel = Container.extend({
     342                initialize: function ( id, options ) {
     343                        var panel = this;
     344                        Container.prototype.initialize.call( panel, id, options );
     345                },
     346
     347                /**
     348                 *
     349                 */
     350                embed: function ( readyCallback ) {
     351                        $( '#customize-theme-controls > ul' ).append( this.container );
     352                        if ( readyCallback ) {
     353                                readyCallback();
     354                        }
     355                },
     356
     357                /**
     358                 *
     359                 */
     360                attachEvents: function () {
     361                        var meta, panel = this;
     362
     363                        // Expand/Collapse accordion sections on click.
     364                        panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) {
     365                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     366                                        return;
     367                                }
     368                                e.preventDefault(); // Keep this AFTER the key filter above
     369
     370                                if ( ! panel.expanded() ) {
     371                                        panel.expand();
     372                                }
     373                        });
     374
     375                        meta = panel.container.find( '.panel-meta:first' );
     376
     377                        meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
     378                                if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     379                                        return;
     380                                }
     381                                e.preventDefault(); // Keep this AFTER the key filter above
     382
     383                                if ( meta.hasClass( 'cannot-expand' ) ) {
     384                                        return;
     385                                }
     386
     387                                var content = meta.find( '.accordion-section-content:first' );
     388                                if ( meta.hasClass( 'open' ) ) {
     389                                        meta.toggleClass( 'open' );
     390                                        content.slideUp( 150 );
     391                                } else {
     392                                        content.slideDown( 150 );
     393                                        meta.toggleClass( 'open' );
     394                                }
     395                        });
     396
     397                },
     398
     399                /**
     400                 * Get the sections that are associated with this panel, sorted by their priority Value.
     401                 *
     402                 * @returns {Array}
     403                 */
     404                sections: function () {
     405                        return this._children( 'panel', 'section' );
     406                },
     407
     408                /**
     409                 * Return whether this section has any active sections.
     410                 *
     411                 * @returns {boolean}
     412                 */
     413                isContextuallyActive: function () {
     414                        var panel = this,
     415                                sections = panel.sections(),
     416                                activeCount = 0;
     417                        _( sections ).each( function ( section ) {
     418                                if ( section.active() && section.isContextuallyActive() ) {
     419                                        activeCount += 1;
     420                                }
     421                        } );
     422                        return ( activeCount !== 0 );
     423                },
     424
     425                /**
     426                 *
     427                 */
     428                onExpand: function () {
     429
     430                        var position, scroll,
     431                                panel = this,
     432                                section = panel.container.closest( '.accordion-section' ),
     433                                overlay = section.closest( '.wp-full-overlay' ),
     434                                container = section.closest( '.accordion-container' ),
     435                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
     436                                backBtn = overlay.find( '.control-panel-back' ),
     437                                content = section.find( '.control-panel-content' );
     438
     439                        // Collapse any sibling sections/panels
     440                        api.section.each( function ( section ) {
     441                                if ( ! section.panel() ) {
     442                                        section.collapse(); // @todo If any sections are open, then the position calculation below will fire too early
     443                                }
     444                        });
     445                        api.panel.each( function ( otherPanel ) {
     446                                if ( panel !== otherPanel ) {
     447                                        otherPanel.collapse(); // @todo the position calculation below probably will fire too early
     448                                }
     449                        });
     450
     451                        content.show( 0, function() {
     452                                position = content.offset().top;
     453                                scroll = container.scrollTop();
     454                                content.css( 'margin-top', ( 45 - position - scroll ) );
     455                                section.addClass( 'current-panel' );
     456                                overlay.addClass( 'in-sub-panel' );
     457                                container.scrollTop( 0 );
     458                        } );
     459                        topPanel.attr( 'tabindex', '-1' );
     460                        backBtn.attr( 'tabindex', '0' );
     461                        backBtn.focus();
     462                },
     463
     464                /**
     465                 *
     466                 */
     467                onCollapse: function () {
     468
     469                        var panel = this,
     470                                section = panel.container.closest( '.accordion-section' ),
     471                                overlay = section.closest( '.wp-full-overlay' ),
     472                                container = section.closest( '.accordion-container' ),
     473                                siblings = container.find( '.open' ),
     474                                topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
     475                                backBtn = overlay.find( '.control-panel-back' ),
     476                                panelTitle = section.find( '.accordion-section-title' ).first(),
     477                                content = section.find( '.control-panel-content' );
     478
     479                        siblings.removeClass( 'open' );
     480                        section.removeClass( 'current-panel' );
     481                        overlay.removeClass( 'in-sub-panel' );
     482                        content.delay( 180 ).hide( 0, function() {
     483                                content.css( 'margin-top', 'inherit' ); // Reset
     484                        } );
     485                        topPanel.attr( 'tabindex', '0' );
     486                        backBtn.attr( 'tabindex', '-1' );
     487                        panelTitle.focus();
     488                        container.scrollTop( 0 );
     489                },
     490
     491                /**
     492                 * Bring the containing panel into view and then expand this section and bring it into view
     493                 */
     494                focus: function () {
     495                        var panel = this;
     496                        // @todo What if it is not active? Return false?
     497                        panel.expand();
     498                }
     499
     500                // @todo Need to first exit out of the Panel
     501        });
     502
     503        /**
    34504         * @constructor
    35505         * @augments wp.customize.Class
    36506         */
     
    44514
    45515                        this.id = id;
    46516                        this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
    47                         this.container = $( this.selector );
     517                        this.container = this.params.content ? $( this.params.content ) : $( this.selector );
     518
     519                        this.section = new api.Value( this.params.section );
     520                        this.priority = new api.Value( this.params.priority || 10 );
    48521                        this.active = new api.Value( this.params.active );
    49522
    50523                        settings = $.map( this.params.settings, function( value ) {
     
    60533                                }
    61534
    62535                                control.setting = control.settings['default'] || null;
    63                                 control.ready();
     536                                control.embed( function () {
     537                                        control.ready();
     538                                } );
    64539                        }) );
    65540
    66541                        control.elements = [];
     
    74549
    75550                                if ( node.is(':radio') ) {
    76551                                        name = node.prop('name');
    77                                         if ( radios[ name ] )
     552                                        if ( radios[ name ] ) {
    78553                                                return;
     554                                        }
    79555
    80556                                        radios[ name ] = true;
    81557                                        node = nodes.filter( '[name="' + name + '"]' );
     
    93569                                control.toggle( active );
    94570                        } );
    95571                        control.toggle( control.active() );
     572
     573                        bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
     574                },
     575
     576                /**
     577                 * @param {Function} [readyCallback] Callback to fire when the embedding is done.
     578                 */
     579                embed: function ( readyCallback ) {
     580                        var section_id,
     581                                control = this;
     582
     583                        section_id = control.section.get();
     584                        if ( ! section_id ) {
     585                                throw new Error( 'A control must have an associated section.' );
     586                        }
     587
     588                        // Defer until the associated section is available
     589                        api.section( section_id, function ( section ) {
     590                                section.embed( function () {
     591                                        section.container.find( 'ul:first' ).append( control.container );
     592                                        readyCallback();
     593                                } );
     594                        } );
    96595                },
    97596
    98597                /**
     
    101600                ready: function() {},
    102601
    103602                /**
    104                  * Callback for change to the control's active state.
    105                  *
    106                  * Override function for custom behavior for the control being active/inactive.
     603                 * Bring the containing section and panel into view and then this control into view, focusing on the first input
     604                 */
     605                focus: function () {
     606                        throw new Error( 'Not implemented yet' );
     607                },
     608
     609                /**
     610                 * Change to the control's active state.
    107611                 *
    108612                 * @param {Boolean} active
    109613                 */
     
    5751079
    5761080        // Create the collection of Control objects.
    5771081        api.control = new api.Values({ defaultConstructor: api.Control });
     1082        api.section = new api.Values({ defaultConstructor: api.Section });
     1083        api.panel = new api.Values({ defaultConstructor: api.Panel });
    5781084
    5791085        /**
    5801086         * @constructor
     
    9791485                image:  api.ImageControl,
    9801486                header: api.HeaderControl
    9811487        };
     1488        api.panelConstructor = {};
     1489        api.sectionConstructor = {};
    9821490
    9831491        $( function() {
    9841492                api.settings = window._wpCustomizeSettings;
     
    10091517                        }
    10101518                });
    10111519
     1520                // Expand/Collapse the main customizer customize info
     1521                $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) {
     1522                        if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     1523                                return;
     1524                        }
     1525                        e.preventDefault(); // Keep this AFTER the key filter above
     1526
     1527                        var section = $( this ).parent(),
     1528                                content = section.find( '.accordion-section-content:first' );
     1529
     1530                        if ( section.hasClass( 'cannot-expand' ) ) {
     1531                                return;
     1532                        }
     1533
     1534                        if ( section.hasClass( 'open' ) ) {
     1535                                section.toggleClass( 'open' );
     1536                                content.slideUp( 150 );
     1537                        } else {
     1538                                content.slideDown( 150 );
     1539                                section.toggleClass( 'open' );
     1540                        }
     1541                });
     1542
    10121543                // Initialize Previewer
    10131544                api.previewer = new api.Previewer({
    10141545                        container:   '#customize-preview',
     
    11021633                        $.extend( this.nonce, nonce );
    11031634                });
    11041635
     1636                // Create Settings
    11051637                $.each( api.settings.settings, function( id, data ) {
    11061638                        api.create( id, id, data.value, {
    11071639                                transport: data.transport,
     
    11091641                        } );
    11101642                });
    11111643
     1644                // Create Panels
     1645                $.each( api.settings.panels, function ( id, data ) {
     1646                        var constructor = api.panelConstructor[ data.type ] || api.Panel,
     1647                                panel;
     1648
     1649                        panel = new constructor( id, {
     1650                                params: data
     1651                        } );
     1652                        api.panel.add( id, panel );
     1653                });
     1654
     1655                // Create Sections
     1656                $.each( api.settings.sections, function ( id, data ) {
     1657                        var constructor = api.sectionConstructor[ data.type ] || api.Section,
     1658                                section;
     1659
     1660                        section = new constructor( id, {
     1661                                params: data
     1662                        } );
     1663                        api.section.add( id, section );
     1664                });
     1665
     1666                // Create Controls
     1667                // @todo factor this out
    11121668                $.each( api.settings.controls, function( id, data ) {
    11131669                        var constructor = api.controlConstructor[ data.type ] || api.Control,
    11141670                                control;
    11151671
    1116                         control = api.control.add( id, new constructor( id, {
     1672                        control = new constructor( id, {
    11171673                                params: data,
    11181674                                previewer: api.previewer
    1119                         } ) );
     1675                        } );
     1676                        api.control.add( id, control );
    11201677                });
    11211678
     1679                /**
     1680                 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
     1681                 */
     1682                api.reflowPaneContents = _.bind( function () {
     1683
     1684                        var appendContainer, activeElement, rootNodes = [];
     1685
     1686                        if ( document.activeElement ) {
     1687                                activeElement = $( document.activeElement );
     1688                        }
     1689
     1690                        api.panel.each( function ( panel ) {
     1691                                var sections = panel.sections();
     1692                                rootNodes.push( panel );
     1693                                appendContainer = panel.container.find( 'ul:first' );
     1694                                _.chain( sections ).reverse().each( function ( section ) {
     1695                                        appendContainer.append( section.container );
     1696                                } );
     1697                        } );
     1698
     1699                        api.section.each( function ( section ) {
     1700                                var controls = section.controls();
     1701                                if ( ! section.panel() ) {
     1702                                        rootNodes.push( section );
     1703                                }
     1704                                appendContainer = section.container.find( 'ul:first' );
     1705                                _.chain( controls ).reverse().each( function ( control ) {
     1706                                        appendContainer.append( control.container );
     1707                                } );
     1708                        } );
     1709
     1710                        // Sort the root elements
     1711                        rootNodes.sort( function ( a, b ) {
     1712                                return a.priority() - b.priority();
     1713                        } );
     1714                        appendContainer = $( '#customize-theme-controls > ul' );
     1715                        _.chain( rootNodes ).each( function ( rootNode ) {
     1716                                appendContainer.append( rootNode.container );
     1717                        } );
     1718
     1719                        // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
     1720                        api.panel.each( function ( panel ) {
     1721                                var value = panel.active();
     1722                                panel.active.callbacks.fireWith( panel.active, [ value, value ] );
     1723                        } );
     1724                        api.section.each( function ( section ) {
     1725                                var value = section.active();
     1726                                section.active.callbacks.fireWith( section.active, [ value, value ] );
     1727                        } );
     1728
     1729                        if ( activeElement ) {
     1730                                activeElement.focus();
     1731                        }
     1732                }, api );
     1733                api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
     1734                $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
     1735                        values.bind( 'add', api.reflowPaneContents );
     1736                        values.bind( 'change', api.reflowPaneContents );
     1737                        values.bind( 'remove', api.reflowPaneContents );
     1738                } );
     1739                api.bind( 'ready', api.reflowPaneContents );
     1740
    11221741                // Check if preview url is valid and load the preview frame.
    11231742                if ( api.previewer.previewUrl() ) {
    11241743                        api.previewer.refresh();
     
    11831802                        event.preventDefault();
    11841803                });
    11851804
     1805                // Go back to the top-level Customizer accordion.
     1806                $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {
     1807                        if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key
     1808                                return;
     1809                        }
     1810
     1811                        e.preventDefault(); // Keep this AFTER the key filter above
     1812                        api.panel.each( function ( panel ) {
     1813                                panel.collapse();
     1814                        });
     1815                });
     1816
    11861817                closeBtn.keydown( function( event ) {
    11871818                        if ( 9 === event.which ) // tab
    11881819                                return;
  • 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..d38d22f 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 { 
    219220
    220221                $this->json['type'] = $this->type;
    221222                $this->json['active'] = $this->active();
     223                $this->json['section'] = $this->section;
     224                $this->json['content'] = $this->get_content();
     225        }
     226
     227        /**
     228         * Get the data to export to the client via JSON.
     229         *
     230         * @since 4.1.0
     231         *
     232         * @return array
     233         */
     234        public function json() {
     235                $this->to_json();
     236                return $this->json;
    222237        }
    223238
    224239        /**
    class WP_Customize_Control { 
    242257        }
    243258
    244259        /**
     260         * Get the control's content for insertion into the Customizer pane.
     261         *
     262         * @since 4.1.0
     263         *
     264         * @return string
     265         */
     266        public final function get_content() {
     267                ob_start();
     268                $this->maybe_render();
     269                $template = trim( ob_get_contents() );
     270                ob_end_clean();
     271                return $template;
     272        }
     273
     274        /**
    245275         * Check capabilities and render the control.
    246276         *
    247277         * @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;