WordPress.org

Make WordPress Core

Changeset 30102


Ignore:
Timestamp:
10/29/2014 10:50:21 PM (5 years ago)
Author:
ocean90
Message:

Improve/introduce Customizer JavaScript models for Controls, Sections, and Panels.

  • Introduce models for panels and sections.
  • Introduce API to expand and focus a control, section or panel.
  • Allow deep-linking to panels, sections, and controls inside of the Customizer.
  • Clean up accordion.js, removing all Customizer-specific logic.
  • Add initial unit tests for wp.customize.Class in customize-base.js.

https://make.wordpress.org/core/2014/10/27/toward-a-complete-javascript-api-for-the-customizer/ provides an overview of how to use the JavaScript API.

props westonruter, celloexpressions, ryankienstra.
see #28032, #28579, #28580, #28650, #28709, #29758.
fixes #29529.

Location:
trunk
Files:
1 added
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/customize.php

    r30055 r30102  
    5454wp_enqueue_style( 'customize-controls' );
    5555
    56 wp_enqueue_script( 'accordion' );
    57 
    5856/**
    5957 * Enqueue Customizer control scripts.
     
    131129
    132130        <div id="widgets-right"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat -->
    133         <div class="wp-full-overlay-sidebar-content accordion-container" tabindex="-1">
     131        <div class="wp-full-overlay-sidebar-content" tabindex="-1">
    134132            <div id="customize-info" class="accordion-section <?php if ( $cannot_expand ) echo ' cannot-expand'; ?>">
    135133                <div class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0">
     
    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>
     
    253247        'settings' => array(),
    254248        'controls' => array(),
     249        'panels'   => array(),
     250        'sections' => array(),
    255251        'nonce'    => array(
    256252            'save'    => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ),
    257253            'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() )
    258254        ),
     255        'autofocus' => array(),
    259256    );
    260257
     
    267264    }
    268265
    269     // Prepare Customize Control objects to pass to Javascript.
     266    // Prepare Customize Control objects to pass to JavaScript.
    270267    foreach ( $wp_customize->controls() as $id => $control ) {
    271         $control->to_json();
    272         $settings['controls'][ $id ] = $control->json;
     268        $settings['controls'][ $id ] = $control->json();
     269    }
     270
     271    // Prepare Customize Section objects to pass to JavaScript.
     272    foreach ( $wp_customize->sections() as $id => $section ) {
     273        $settings['sections'][ $id ] = $section->json();
     274    }
     275
     276    // Prepare Customize Panel objects to pass to JavaScript.
     277    foreach ( $wp_customize->panels() as $id => $panel ) {
     278        $settings['panels'][ $id ] = $panel->json();
     279        foreach ( $panel->sections as $section_id => $section ) {
     280            $settings['sections'][ $section_id ] = $section->json();
     281        }
     282    }
     283
     284    // Pass to frontend the Customizer construct being deeplinked
     285    if ( isset( $_GET['autofocus'] ) && is_array( $_GET['autofocus'] ) ) {
     286        $autofocus = wp_unslash( $_GET['autofocus'] );
     287        foreach ( $autofocus as $type => $id ) {
     288            if ( isset( $settings[ $type . 's' ][ $id ] ) ) {
     289                $settings['autofocus'][ $type ] = $id;
     290            }
     291        }
    273292    }
    274293
  • trunk/src/wp-admin/js/accordion.js

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

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

    r29903 r30102  
    405405     */
    406406    api.Widgets.WidgetControl = api.Control.extend({
     407        defaultExpandedArguments: {
     408            duration: 'fast'
     409        },
     410
     411        initialize: function ( id, options ) {
     412            var control = this;
     413            api.Control.prototype.initialize.call( control, id, options );
     414            control.expanded = new api.Value();
     415            control.expandedArgumentsQueue = [];
     416            control.expanded.bind( function ( expanded ) {
     417                var args = control.expandedArgumentsQueue.shift();
     418                args = $.extend( {}, control.defaultExpandedArguments, args );
     419                control.onChangeExpanded( expanded, args );
     420            });
     421            control.expanded.set( false );
     422        },
     423
    407424        /**
    408425         * Set up the control
     
    530547                    return;
    531548                }
    532                 self.toggleForm();
     549                self.expanded( ! self.expanded() );
    533550            } );
    534551
     
    536553            $closeBtn.on( 'click', function( e ) {
    537554                e.preventDefault();
    538                 self.collapseForm();
     555                self.collapse();
    539556                self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
    540557            } );
     
    778795         *
    779796         * @param {Boolean} active
    780          */
    781         toggle: function ( active ) {
     797         * @param {Object} args
     798         */
     799        onChangeActive: function ( active, args ) {
     800            // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments
    782801            this.container.toggleClass( 'widget-rendered', active );
     802            if ( args.completeCallback ) {
     803                args.completeCallback();
     804            }
    783805        },
    784806
     
    11021124         */
    11031125        expandControlSection: function() {
    1104             var $section = this.container.closest( '.accordion-section' );
    1105 
    1106             if ( ! $section.hasClass( 'open' ) ) {
    1107                 $section.find( '.accordion-section-title:first' ).trigger( 'click' );
    1108             }
    1109         },
     1126            api.Control.prototype.expand.call( this );
     1127        },
     1128
     1129        /**
     1130         * @param {Boolean} expanded
     1131         * @param {Object} [params]
     1132         * @returns {Boolean} false if state already applied
     1133         */
     1134        _toggleExpanded: api.Section.prototype._toggleExpanded,
     1135
     1136        /**
     1137         * @param {Object} [params]
     1138         * @returns {Boolean} false if already expanded
     1139         */
     1140        expand: api.Section.prototype.expand,
    11101141
    11111142        /**
    11121143         * Expand the widget form control
     1144         *
     1145         * @deprecated alias of expand()
    11131146         */
    11141147        expandForm: function() {
    1115             this.toggleForm( true );
    1116         },
     1148            this.expand();
     1149        },
     1150
     1151        /**
     1152         * @param {Object} [params]
     1153         * @returns {Boolean} false if already collapsed
     1154         */
     1155        collapse: api.Section.prototype.collapse,
    11171156
    11181157        /**
    11191158         * Collapse the widget form control
     1159         *
     1160         * @deprecated alias of expand()
    11201161         */
    11211162        collapseForm: function() {
    1122             this.toggleForm( false );
     1163            this.collapse();
    11231164        },
    11241165
     
    11261167         * Expand or collapse the widget control
    11271168         *
     1169         * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide )
     1170         *
    11281171         * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility
    11291172         */
    11301173        toggleForm: function( showOrHide ) {
    1131             var self = this, $widget, $inside, complete;
     1174            if ( typeof showOrHide === 'undefined' ) {
     1175                showOrHide = ! this.expanded();
     1176            }
     1177            this.expanded( showOrHide );
     1178        },
     1179
     1180        /**
     1181         * Respond to change in the expanded state.
     1182         *
     1183         * @param {Boolean} expanded
     1184         * @param {Object} args  merged on top of this.defaultActiveArguments
     1185         */
     1186        onChangeExpanded: function ( expanded, args ) {
     1187            var self = this, $widget, $inside, complete, prevComplete;
     1188
     1189            // If the expanded state is unchanged only manipulate container expanded states
     1190            if ( args.unchanged ) {
     1191                if ( expanded ) {
     1192                    api.Control.prototype.expand.call( self, {
     1193                        completeCallback:  args.completeCallback
     1194                    });
     1195                }
     1196                return;
     1197            }
    11321198
    11331199            $widget = this.container.find( 'div.widget:first' );
    11341200            $inside = $widget.find( '.widget-inside:first' );
    1135             if ( typeof showOrHide === 'undefined' ) {
    1136                 showOrHide = ! $inside.is( ':visible' );
    1137             }
    1138 
    1139             // Already expanded or collapsed, so noop
    1140             if ( $inside.is( ':visible' ) === showOrHide ) {
    1141                 return;
    1142             }
    1143 
    1144             if ( showOrHide ) {
     1201
     1202            if ( expanded ) {
     1203
     1204                self.expandControlSection();
     1205
    11451206                // Close all other widget controls before expanding this one
    11461207                api.control.each( function( otherControl ) {
    11471208                    if ( self.params.type === otherControl.params.type && self !== otherControl ) {
    1148                         otherControl.collapseForm();
     1209                        otherControl.collapse();
    11491210                    }
    11501211                } );
     
    11551216                    self.container.trigger( 'expanded' );
    11561217                };
     1218                if ( args.completeCallback ) {
     1219                    prevComplete = complete;
     1220                    complete = function () {
     1221                        prevComplete();
     1222                        args.completeCallback();
     1223                    };
     1224                }
    11571225
    11581226                if ( self.params.is_wide ) {
    1159                     $inside.fadeIn( 'fast', complete );
     1227                    $inside.fadeIn( args.duration, complete );
    11601228                } else {
    1161                     $inside.slideDown( 'fast', complete );
     1229                    $inside.slideDown( args.duration, complete );
    11621230                }
    11631231
     
    11651233                self.container.addClass( 'expanding' );
    11661234            } else {
     1235
    11671236                complete = function() {
    11681237                    self.container.removeClass( 'collapsing' );
     
    11701239                    self.container.trigger( 'collapsed' );
    11711240                };
     1241                if ( args.completeCallback ) {
     1242                    prevComplete = complete;
     1243                    complete = function () {
     1244                        prevComplete();
     1245                        args.completeCallback();
     1246                    };
     1247                }
    11721248
    11731249                self.container.trigger( 'collapse' );
     
    11751251
    11761252                if ( self.params.is_wide ) {
    1177                     $inside.fadeOut( 'fast', complete );
     1253                    $inside.fadeOut( args.duration, complete );
    11781254                } else {
    1179                     $inside.slideUp( 'fast', function() {
     1255                    $inside.slideUp( args.duration, function() {
    11801256                        $widget.css( { width:'', margin:'' } );
    11811257                        complete();
     
    11831259                }
    11841260            }
    1185         },
    1186 
    1187         /**
    1188          * Expand the containing sidebar section, expand the form, and focus on
    1189          * the first input in the control
    1190          */
    1191         focus: function() {
    1192             this.expandControlSection();
    1193             this.expandForm();
    1194             this.container.find( '.widget-content :focusable:first' ).focus();
    11951261        },
    11961262
     
    13051371     */
    13061372    api.Widgets.SidebarControl = api.Control.extend({
     1373
    13071374        /**
    13081375         * Set up the control
     
    13261393
    13271394            this.setting.bind( function( newWidgetIds, oldWidgetIds ) {
    1328                 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;
     1395                var widgetFormControls, removedWidgetIds, priority;
    13291396
    13301397                removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds );
     
    13511418                    var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ),
    13521419                        bIndex = _.indexOf( newWidgetIds, b.params.widget_id );
    1353 
    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 );
     1420                    return aIndex - bIndex;
     1421                });
     1422
     1423                priority = 0;
     1424                _( widgetFormControls ).each( function ( control ) {
     1425                    control.priority( priority );
     1426                    control.section( self.section() );
     1427                    priority += 1;
     1428                });
     1429                self.priority( priority ); // Make sure sidebar control remains at end
    13681430
    13691431                // Re-sort widget form controls (including widgets form other sidebars newly moved here)
     
    14351497            self.active.bind( function ( active ) {
    14361498                registeredSidebar.set( 'is_rendered', active );
    1437             } );
    1438         },
    1439 
    1440         /**
    1441          * Show the sidebar section when it becomes visible.
    1442          *
    1443          * Overrides api.Control.toggle()
    1444          *
    1445          * @param {Boolean} active
    1446          */
    1447         toggle: function ( active ) {
    1448             var $section, sectionSelector;
    1449 
    1450             sectionSelector = '#accordion-section-sidebar-widgets-' + this.params.sidebar_id;
    1451             $section = $( sectionSelector );
    1452 
    1453             if ( active ) {
    1454                 $section.stop().slideDown( function() {
    1455                     $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
    1456                 } );
    1457 
    1458             } else {
    1459                 // Make sure that hidden sections get closed first
    1460                 if ( $section.hasClass( 'open' ) ) {
    1461                     // it would be nice if accordionSwitch() in accordion.js was public
    1462                     $section.find( '.accordion-section-title' ).trigger( 'click' );
    1463                 }
    1464 
    1465                 $section.stop().slideUp();
    1466             }
     1499                api.section( self.section.get() ).active( active );
     1500            } );
     1501            api.section( self.section.get() ).active( self.active() );
    14671502        },
    14681503
     
    15011536                accept: '.customize-control-widget_form',
    15021537                over: function() {
    1503                     if ( ! self.$controlSection.hasClass( 'open' ) ) {
    1504                         self.$controlSection.addClass( 'open' );
    1505                         self.$sectionContent.toggle( false ).slideToggle( 150, function() {
    1506                             self.$sectionContent.sortable( 'refreshPositions' );
    1507                         } );
    1508                     }
     1538                    var section = api.section( self.section.get() );
     1539                    section.expand({
     1540                        allowMultiple: true, // Prevent the section being dragged from to be collapsed
     1541                        completeCallback: function () {
     1542                            // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed
     1543                            api.section.each( function ( otherSection ) {
     1544                                if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) {
     1545                                    otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' );
     1546                                }
     1547                            } );
     1548                        }
     1549                    });
    15091550                }
    15101551            });
     
    15491590         */
    15501591        _applyCardinalOrderClassNames: function() {
    1551             this.$sectionContent.find( '.customize-control-widget_form' )
    1552                 .removeClass( 'first-widget' )
    1553                 .removeClass( 'last-widget' )
    1554                 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
    1555 
    1556             this.$sectionContent.find( '.customize-control-widget_form:first' )
     1592            var widgetControls = [];
     1593            _.each( this.setting(), function ( widgetId ) {
     1594                var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId );
     1595                if ( widgetControl ) {
     1596                    widgetControls.push( widgetControl );
     1597                }
     1598            });
     1599
     1600            if ( ! widgetControls.length ) {
     1601                return;
     1602            }
     1603
     1604            $( widgetControls ).each( function () {
     1605                $( this.container )
     1606                    .removeClass( 'first-widget' )
     1607                    .removeClass( 'last-widget' )
     1608                    .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
     1609            });
     1610
     1611            _.first( widgetControls ).container
    15571612                .addClass( 'first-widget' )
    15581613                .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
    15591614
    1560             this.$sectionContent.find( '.customize-control-widget_form:last' )
     1615            _.last( widgetControls ).container
    15611616                .addClass( 'last-widget' )
    15621617                .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
     
    15721627         *
    15731628         * @param {Boolean} showOrHide to enable/disable reordering
     1629         *
     1630         * @todo We should have a reordering state instead and rename this to onChangeReordering
    15741631         */
    15751632        toggleReordering: function( showOrHide ) {
     
    15851642            if ( showOrHide ) {
    15861643                _( this.getWidgetFormControls() ).each( function( formControl ) {
    1587                     formControl.collapseForm();
     1644                    formControl.collapse();
    15881645                } );
    15891646
     
    16201677         */
    16211678        addWidget: function( widgetId ) {
    1622             var self = this, controlHtml, $widget, controlType = 'widget_form', $control, controlConstructor,
     1679            var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor,
    16231680                parsedWidgetId = parseWidgetId( widgetId ),
    16241681                widgetNumber = parsedWidgetId.number,
     
    16521709            $widget = $( controlHtml );
    16531710
    1654             $control = $( '<li/>' )
     1711            controlContainer = $( '<li/>' )
    16551712                .addClass( 'customize-control' )
    16561713                .addClass( 'customize-control-' + controlType )
     
    16581715
    16591716            // Remove icon which is visible inside the panel
    1660             $control.find( '> .widget-icon' ).remove();
     1717            controlContainer.find( '> .widget-icon' ).remove();
    16611718
    16621719            if ( widget.get( 'is_multi' ) ) {
    1663                 $control.find( 'input[name="widget_number"]' ).val( widgetNumber );
    1664                 $control.find( 'input[name="multi_number"]' ).val( widgetNumber );
    1665             }
    1666 
    1667             widgetId = $control.find( '[name="widget-id"]' ).val();
    1668 
    1669             $control.hide(); // to be slid-down below
     1720                controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber );
     1721                controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber );
     1722            }
     1723
     1724            widgetId = controlContainer.find( '[name="widget-id"]' ).val();
     1725
     1726            controlContainer.hide(); // to be slid-down below
    16701727
    16711728            settingId = 'widget_' + widget.get( 'id_base' );
     
    16731730                settingId += '[' + widgetNumber + ']';
    16741731            }
    1675             $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
    1676 
    1677             this.container.after( $control );
     1732            controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
    16781733
    16791734            // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
     
    16931748                        'default': settingId
    16941749                    },
     1750                    content: controlContainer,
    16951751                    sidebar_id: self.params.sidebar_id,
    16961752                    widget_id: widgetId,
     
    17321788            }
    17331789
    1734             $control.slideDown( function() {
     1790            controlContainer.slideDown( function() {
    17351791                if ( isExistingWidget ) {
    1736                     widgetFormControl.expandForm();
     1792                    widgetFormControl.expand();
    17371793                    widgetFormControl.updateWidget( {
    17381794                        instance: widgetFormControl.setting(),
  • trunk/src/wp-includes/class-wp-customize-control.php

    r30087 r30102  
    7575
    7676    /**
     77     * @deprecated It is better to just call the json() method
    7778     * @access public
    7879     * @var array
     
    219220
    220221        $this->json['type']        = $this->type;
     222        $this->json['priority']    = $this->priority;
     223        $this->json['active']      = $this->active();
     224        $this->json['section']     = $this->section;
     225        $this->json['content']     = $this->get_content();
    221226        $this->json['label']       = $this->label;
    222227        $this->json['description'] = $this->description;
    223         $this->json['active']      = $this->active();
     228    }
     229
     230    /**
     231     * Get the data to export to the client via JSON.
     232     *
     233     * @since 4.1.0
     234     *
     235     * @return array
     236     */
     237    public function json() {
     238        $this->to_json();
     239        return $this->json;
    224240    }
    225241
     
    242258
    243259        return true;
     260    }
     261
     262    /**
     263     * Get the control's content for insertion into the Customizer pane.
     264     *
     265     * @since 4.1.0
     266     *
     267     * @return string
     268     */
     269    public final function get_content() {
     270        ob_start();
     271        $this->maybe_render();
     272        $template = trim( ob_get_contents() );
     273        ob_end_clean();
     274        return $template;
    244275    }
    245276
     
    10741105 * Widget Area Customize Control Class
    10751106 *
     1107 * @since 3.9.0
    10761108 */
    10771109class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
     
    11151147/**
    11161148 * Widget Form Customize Control Class
     1149 *
     1150 * @since 3.9.0
    11171151 */
    11181152class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r30055 r30102  
    499499            'values'  => array(),
    500500            'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
     501            'activePanels' => array(),
     502            'activeSections' => array(),
    501503            'activeControls' => array(),
    502504        );
     
    511513        foreach ( $this->settings as $id => $setting ) {
    512514            $settings['values'][ $id ] = $setting->js_value();
     515        }
     516        foreach ( $this->panels as $id => $panel ) {
     517            $settings['activePanels'][ $id ] = $panel->active();
     518        }
     519        foreach ( $this->sections as $id => $section ) {
     520            $settings['activeSections'][ $id ] = $section->active();
    513521        }
    514522        foreach ( $this->controls as $id => $control ) {
     
    912920            if ( ! $section->panel ) {
    913921                // Top-level section.
    914                 $sections[] = $section;
     922                $sections[ $section->id ] = $section;
    915923            } else {
    916924                // This section belongs to a panel.
    917925                if ( isset( $this->panels [ $section->panel ] ) ) {
    918                     $this->panels[ $section->panel ]->sections[] = $section;
     926                    $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
    919927                }
    920928            }
     
    933941            }
    934942
    935             usort( $panel->sections, array( $this, '_cmp_priority' ) );
    936             $panels[] = $panel;
     943            uasort( $panel->sections, array( $this, '_cmp_priority' ) );
     944            $panels[ $panel->id ] = $panel;
    937945        }
    938946        $this->panels = $panels;
  • trunk/src/wp-includes/class-wp-customize-panel.php

    r29950 r30102  
    8484
    8585    /**
     86     * @since 4.1.0
     87     * @access public
     88     * @var string
     89     */
     90    public $type;
     91
     92    /**
     93     * Callback.
     94     *
     95     * @since 4.1.0
     96     * @access public
     97     *
     98     * @see WP_Customize_Section::active()
     99     *
     100     * @var callable Callback is called with one argument, the instance of
     101     *               WP_Customize_Section, and returns bool to indicate whether
     102     *               the section is active (such as it relates to the URL
     103     *               currently being previewed).
     104     */
     105    public $active_callback = '';
     106
     107    /**
    86108     * Constructor.
    87109     *
     
    104126        $this->manager = $manager;
    105127        $this->id = $id;
     128        if ( empty( $this->active_callback ) ) {
     129            $this->active_callback = array( $this, 'active_callback' );
     130        }
    106131
    107132        $this->sections = array(); // Users cannot customize the $sections array.
    108133
    109134        return $this;
     135    }
     136
     137    /**
     138     * Check whether panel is active to current Customizer preview.
     139     *
     140     * @since 4.1.0
     141     * @access public
     142     *
     143     * @return bool Whether the panel is active to the current preview.
     144     */
     145    public final function active() {
     146        $panel = $this;
     147        $active = call_user_func( $this->active_callback, $this );
     148
     149        /**
     150         * Filter response of WP_Customize_Panel::active().
     151         *
     152         * @since 4.1.0
     153         *
     154         * @param bool                 $active  Whether the Customizer panel is active.
     155         * @param WP_Customize_Panel $panel WP_Customize_Panel instance.
     156         */
     157        $active = apply_filters( 'customize_panel_active', $active, $panel );
     158
     159        return $active;
     160    }
     161
     162    /**
     163     * Default callback used when invoking WP_Customize_Panel::active().
     164     *
     165     * Subclasses can override this with their specific logic, or they may
     166     * provide an 'active_callback' argument to the constructor.
     167     *
     168     * @since 4.1.0
     169     * @access public
     170     *
     171     * @return bool Always true.
     172     */
     173    public function active_callback() {
     174        return true;
     175    }
     176
     177    /**
     178     * Gather the parameters passed to client JavaScript via JSON.
     179     *
     180     * @since 4.1.0
     181     *
     182     * @return array The array to be exported to the client as JSON
     183     */
     184    public function json() {
     185        $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) );
     186        $array['content'] = $this->get_content();
     187        $array['active'] = $this->active();
     188        return $array;
    110189    }
    111190
     
    128207
    129208        return true;
     209    }
     210
     211    /**
     212     * Get the panel's content template for insertion into the Customizer pane.
     213     *
     214     * @since 4.1.0
     215     *
     216     * @return string
     217     */
     218    public final function get_content() {
     219        ob_start();
     220        $this->maybe_render();
     221        $template = trim( ob_get_contents() );
     222        ob_end_clean();
     223        return $template;
    130224    }
    131225
     
    190284    protected function render_content() {
    191285        ?>
    192         <li class="accordion-section control-section<?php if ( empty( $this->description ) ) echo ' cannot-expand'; ?>">
     286        <li class="panel-meta accordion-section control-section<?php if ( empty( $this->description ) ) { echo ' cannot-expand'; } ?>">
    193287            <div class="accordion-section-title" tabindex="0">
    194288                <span class="preview-notice"><?php
     
    204298        </li>
    205299        <?php
    206         foreach ( $this->sections as $section ) {
    207             $section->maybe_render();
    208         }
    209300    }
    210301}
  • trunk/src/wp-includes/class-wp-customize-section.php

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

    r29907 r30102  
    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;
  • trunk/src/wp-includes/js/customize-preview.js

    r29451 r30102  
    108108
    109109        preview.send( 'ready', {
     110            activePanels: api.settings.activePanels,
     111            activeSections: api.settings.activeSections,
    110112            activeControls: api.settings.activeControls
    111113        } );
  • trunk/tests/qunit/index.html

    r27847 r30102  
    99  <script src="../../src/wp-includes/js/backbone.min.js"></script>
    1010  <script src="../../src/wp-includes/js/zxcvbn.min.js"></script>
    11    
     11
    1212  <!-- QUnit -->
    1313  <link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
     
    2929    <!-- Tested files -->
    3030    <script src="../../src/wp-admin/js/password-strength-meter.js"></script>
     31    <script src="../../src/wp-includes/js/customize-base.js"></script>
    3132    <script src="../../src/wp-includes/js/customize-models.js"></script>
    3233    <script src="../../src/wp-includes/js/shortcode.js"></script>
     
    3435    <!-- Unit tests -->
    3536    <script src="wp-admin/js/password-strength-meter.js"></script>
     37    <script src="wp-admin/js/customize-base.js"></script>
    3638    <script src="wp-admin/js/customize-header.js"></script>
    3739    <script src="wp-includes/js/shortcode.js"></script>
     
    3941</body>
    4042</html>
    41 
Note: See TracChangeset for help on using the changeset viewer.