~canonical-sysadmins/wordpress/4.7.2

« back to all changes in this revision

Viewing changes to wp-admin/js/customize-controls.js

  • Committer: Barry Price
  • Date: 2016-08-17 04:50:12 UTC
  • mfrom: (1.1.18 upstream)
  • Revision ID: barry.price@canonical.com-20160817045012-qfui81zhqnqv2ba9
Merge WP4.6 from upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
                        this.id = id;
28
28
                        this.transport = this.transport || 'refresh';
29
29
                        this._dirty = options.dirty || false;
 
30
                        this.notifications = new api.Values({ defaultConstructor: api.Notification });
30
31
 
31
32
                        // Whenever the setting's value changes, refresh the preview.
32
33
                        this.bind( this.preview );
42
43
                                case 'postMessage':
43
44
                                        return this.previewer.send( 'setting', [ this.id, this() ] );
44
45
                        }
 
46
                },
 
47
 
 
48
                /**
 
49
                 * Find controls associated with this setting.
 
50
                 *
 
51
                 * @since 4.6.0
 
52
                 * @returns {wp.customize.Control[]} Controls associated with setting.
 
53
                 */
 
54
                findControls: function() {
 
55
                        var setting = this, controls = [];
 
56
                        api.control.each( function( control ) {
 
57
                                _.each( control.settings, function( controlSetting ) {
 
58
                                        if ( controlSetting.id === setting.id ) {
 
59
                                                controls.push( control );
 
60
                                        }
 
61
                                } );
 
62
                        } );
 
63
                        return controls;
45
64
                }
46
65
        });
47
66
 
1478
1497
                        control.priority = new api.Value();
1479
1498
                        control.active = new api.Value();
1480
1499
                        control.activeArgumentsQueue = [];
 
1500
                        control.notifications = new api.Values({ defaultConstructor: api.Notification });
1481
1501
 
1482
1502
                        control.elements = [];
1483
1503
 
1541
1561
 
1542
1562
                                        control.setting = control.settings['default'] || null;
1543
1563
 
 
1564
                                        // Add setting notifications to the control notification.
 
1565
                                        _.each( control.settings, function( setting ) {
 
1566
                                                setting.notifications.bind( 'add', function( settingNotification ) {
 
1567
                                                        var controlNotification, code, params;
 
1568
                                                        code = setting.id + ':' + settingNotification.code;
 
1569
                                                        params = _.extend(
 
1570
                                                                {},
 
1571
                                                                settingNotification,
 
1572
                                                                {
 
1573
                                                                        setting: setting.id
 
1574
                                                                }
 
1575
                                                        );
 
1576
                                                        controlNotification = new api.Notification( code, params );
 
1577
                                                        control.notifications.add( controlNotification.code, controlNotification );
 
1578
                                                } );
 
1579
                                                setting.notifications.bind( 'remove', function( settingNotification ) {
 
1580
                                                        control.notifications.remove( setting.id + ':' + settingNotification.code );
 
1581
                                                } );
 
1582
                                        } );
 
1583
 
1544
1584
                                        control.embed();
1545
1585
                                }) );
1546
1586
                        }
1547
1587
 
1548
1588
                        // After the control is embedded on the page, invoke the "ready" method.
1549
1589
                        control.deferred.embedded.done( function () {
 
1590
                                /*
 
1591
                                 * Note that this debounced/deferred rendering is needed for two reasons:
 
1592
                                 * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
 
1593
                                 * 2) Improve performance when adding/removing multiple notifications at a time.
 
1594
                                 */
 
1595
                                var debouncedRenderNotifications = _.debounce( function renderNotifications() {
 
1596
                                        control.renderNotifications();
 
1597
                                } );
 
1598
                                control.notifications.bind( 'add', function( notification ) {
 
1599
                                        wp.a11y.speak( notification.message, 'assertive' );
 
1600
                                        debouncedRenderNotifications();
 
1601
                                } );
 
1602
                                control.notifications.bind( 'remove', debouncedRenderNotifications );
 
1603
                                control.renderNotifications();
 
1604
 
1550
1605
                                control.ready();
1551
1606
                        });
1552
1607
                },
1589
1644
                ready: function() {},
1590
1645
 
1591
1646
                /**
 
1647
                 * Get the element inside of a control's container that contains the validation error message.
 
1648
                 *
 
1649
                 * Control subclasses may override this to return the proper container to render notifications into.
 
1650
                 * Injects the notification container for existing controls that lack the necessary container,
 
1651
                 * including special handling for nav menu items and widgets.
 
1652
                 *
 
1653
                 * @since 4.6.0
 
1654
                 * @returns {jQuery} Setting validation message element.
 
1655
                 * @this {wp.customize.Control}
 
1656
                 */
 
1657
                getNotificationsContainerElement: function() {
 
1658
                        var control = this, controlTitle, notificationsContainer;
 
1659
 
 
1660
                        notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
 
1661
                        if ( notificationsContainer.length ) {
 
1662
                                return notificationsContainer;
 
1663
                        }
 
1664
 
 
1665
                        notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
 
1666
 
 
1667
                        if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
 
1668
                                control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
 
1669
                        } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
 
1670
                                control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
 
1671
                        } else {
 
1672
                                controlTitle = control.container.find( '.customize-control-title' );
 
1673
                                if ( controlTitle.length ) {
 
1674
                                        controlTitle.after( notificationsContainer );
 
1675
                                } else {
 
1676
                                        control.container.prepend( notificationsContainer );
 
1677
                                }
 
1678
                        }
 
1679
                        return notificationsContainer;
 
1680
                },
 
1681
 
 
1682
                /**
 
1683
                 * Render notifications.
 
1684
                 *
 
1685
                 * Renders the `control.notifications` into the control's container.
 
1686
                 * Control subclasses may override this method to do their own handling
 
1687
                 * of rendering notifications.
 
1688
                 *
 
1689
                 * @since 4.6.0
 
1690
                 * @this {wp.customize.Control}
 
1691
                 */
 
1692
                renderNotifications: function() {
 
1693
                        var control = this, container, notifications, hasError = false;
 
1694
                        container = control.getNotificationsContainerElement();
 
1695
                        if ( ! container || ! container.length ) {
 
1696
                                return;
 
1697
                        }
 
1698
                        notifications = [];
 
1699
                        control.notifications.each( function( notification ) {
 
1700
                                notifications.push( notification );
 
1701
                                if ( 'error' === notification.type ) {
 
1702
                                        hasError = true;
 
1703
                                }
 
1704
                        } );
 
1705
 
 
1706
                        if ( 0 === notifications.length ) {
 
1707
                                container.stop().slideUp( 'fast' );
 
1708
                        } else {
 
1709
                                container.stop().slideDown( 'fast', null, function() {
 
1710
                                        $( this ).css( 'height', 'auto' );
 
1711
                                } );
 
1712
                        }
 
1713
 
 
1714
                        if ( ! control.notificationsTemplate ) {
 
1715
                                control.notificationsTemplate = wp.template( 'customize-control-notifications' );
 
1716
                        }
 
1717
 
 
1718
                        control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
 
1719
                        control.container.toggleClass( 'has-error', hasError );
 
1720
                        container.empty().append( $.trim(
 
1721
                                control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
 
1722
                        ) );
 
1723
                },
 
1724
 
 
1725
                /**
1592
1726
                 * Normal controls do not expand, so just expand its parent
1593
1727
                 *
1594
1728
                 * @param {Object} [params]
1794
1928
                                        control.pausePlayer();
1795
1929
                                });
1796
1930
 
1797
 
                        control.setting.bind( function( value ) {
1798
 
 
1799
 
                                // Send attachment information to the preview for possible use in `postMessage` transport.
1800
 
                                wp.media.attachment( value ).fetch().done( function() {
1801
 
                                        wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
 
1931
                        /**
 
1932
                         * Set attachment data and render content.
 
1933
                         *
 
1934
                         * Note that BackgroundImage.prototype.ready applies this ready method
 
1935
                         * to itself. Since BackgroundImage is an UploadControl, the value
 
1936
                         * is the attachment URL instead of the attachment ID. In this case
 
1937
                         * we skip fetching the attachment data because we have no ID available,
 
1938
                         * and it is the responsibility of the UploadControl to set the control's
 
1939
                         * attachmentData before calling the renderContent method.
 
1940
                         *
 
1941
                         * @param {number|string} value Attachment
 
1942
                         */
 
1943
                        function setAttachmentDataAndRenderContent( value ) {
 
1944
                                var hasAttachmentData = $.Deferred();
 
1945
 
 
1946
                                if ( control.extended( api.UploadControl ) ) {
 
1947
                                        hasAttachmentData.resolve();
 
1948
                                } else {
 
1949
                                        value = parseInt( value, 10 );
 
1950
                                        if ( _.isNaN( value ) || value <= 0 ) {
 
1951
                                                delete control.params.attachment;
 
1952
                                                hasAttachmentData.resolve();
 
1953
                                        } else if ( control.params.attachment && control.params.attachment.id === value ) {
 
1954
                                                hasAttachmentData.resolve();
 
1955
                                        }
 
1956
                                }
 
1957
 
 
1958
                                // Fetch the attachment data.
 
1959
                                if ( 'pending' === hasAttachmentData.state() ) {
 
1960
                                        wp.media.attachment( value ).fetch().done( function() {
 
1961
                                                control.params.attachment = this.attributes;
 
1962
                                                hasAttachmentData.resolve();
 
1963
 
 
1964
                                                // Send attachment information to the preview for possible use in `postMessage` transport.
 
1965
                                                wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
 
1966
                                        } );
 
1967
                                }
 
1968
 
 
1969
                                hasAttachmentData.done( function() {
 
1970
                                        control.renderContent();
1802
1971
                                } );
1803
 
 
1804
 
                                // Re-render whenever the control's setting changes.
1805
 
                                control.renderContent();
1806
 
                        } );
 
1972
                        }
 
1973
 
 
1974
                        // Ensure attachment data is initially set (for dynamically-instantiated controls).
 
1975
                        setAttachmentDataAndRenderContent( control.setting() );
 
1976
 
 
1977
                        // Update the attachment data and re-render the control when the setting changes.
 
1978
                        control.setting.bind( setAttachmentDataAndRenderContent );
1807
1979
                },
1808
1980
 
1809
1981
                pausePlayer: function () {
2271
2443
                                        controller.setImageFromAttachment( croppedImage );
2272
2444
                                        controller.frame.close();
2273
2445
                                } ).fail( function() {
2274
 
                                        controller.trigger('content:error:crop');
 
2446
                                        controller.frame.trigger('content:error:crop');
2275
2447
                                } );
2276
2448
                        } else {
2277
2449
                                this.frame.setState( 'cropper' );
2802
2974
                                                }
2803
2975
                                        } );
2804
2976
                                } );
 
2977
 
 
2978
                                if ( data.settingValidities ) {
 
2979
                                        api._handleSettingValidities( {
 
2980
                                                settingValidities: data.settingValidities,
 
2981
                                                focusInvalidControl: false
 
2982
                                        } );
 
2983
                                }
2805
2984
                        } );
2806
2985
 
2807
2986
                        this.request = $.ajax( this.previewUrl(), {
3223
3402
                }
3224
3403
        });
3225
3404
 
 
3405
        api.settingConstructor = {};
3226
3406
        api.controlConstructor = {
3227
3407
                color:         api.ColorControl,
3228
3408
                media:         api.MediaControl,
3239
3419
                themes: api.ThemesSection
3240
3420
        };
3241
3421
 
 
3422
        /**
 
3423
         * Handle setting_validities in an error response for the customize-save request.
 
3424
         *
 
3425
         * Add notifications to the settings and focus on the first control that has an invalid setting.
 
3426
         *
 
3427
         * @since 4.6.0
 
3428
         * @private
 
3429
         *
 
3430
         * @param {object}  args
 
3431
         * @param {object}  args.settingValidities
 
3432
         * @param {boolean} [args.focusInvalidControl=false]
 
3433
         * @returns {void}
 
3434
         */
 
3435
        api._handleSettingValidities = function handleSettingValidities( args ) {
 
3436
                var invalidSettingControls, invalidSettings = [], wasFocused = false;
 
3437
 
 
3438
                // Find the controls that correspond to each invalid setting.
 
3439
                _.each( args.settingValidities, function( validity, settingId ) {
 
3440
                        var setting = api( settingId );
 
3441
                        if ( setting ) {
 
3442
 
 
3443
                                // Add notifications for invalidities.
 
3444
                                if ( _.isObject( validity ) ) {
 
3445
                                        _.each( validity, function( params, code ) {
 
3446
                                                var notification = new api.Notification( code, params ), existingNotification, needsReplacement = false;
 
3447
 
 
3448
                                                // Remove existing notification if already exists for code but differs in parameters.
 
3449
                                                existingNotification = setting.notifications( notification.code );
 
3450
                                                if ( existingNotification ) {
 
3451
                                                        needsReplacement = ( notification.type !== existingNotification.type ) || ! _.isEqual( notification.data, existingNotification.data );
 
3452
                                                }
 
3453
                                                if ( needsReplacement ) {
 
3454
                                                        setting.notifications.remove( code );
 
3455
                                                }
 
3456
 
 
3457
                                                if ( ! setting.notifications.has( notification.code ) ) {
 
3458
                                                        setting.notifications.add( code, notification );
 
3459
                                                }
 
3460
                                                invalidSettings.push( setting.id );
 
3461
                                        } );
 
3462
                                }
 
3463
 
 
3464
                                // Remove notification errors that are no longer valid.
 
3465
                                setting.notifications.each( function( notification ) {
 
3466
                                        if ( 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
 
3467
                                                setting.notifications.remove( notification.code );
 
3468
                                        }
 
3469
                                } );
 
3470
                        }
 
3471
                } );
 
3472
 
 
3473
                if ( args.focusInvalidControl ) {
 
3474
                        invalidSettingControls = api.findControlsForSettings( invalidSettings );
 
3475
 
 
3476
                        // Focus on the first control that is inside of an expanded section (one that is visible).
 
3477
                        _( _.values( invalidSettingControls ) ).find( function( controls ) {
 
3478
                                return _( controls ).find( function( control ) {
 
3479
                                        var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
 
3480
                                        if ( isExpanded && control.expanded ) {
 
3481
                                                isExpanded = control.expanded();
 
3482
                                        }
 
3483
                                        if ( isExpanded ) {
 
3484
                                                control.focus();
 
3485
                                                wasFocused = true;
 
3486
                                        }
 
3487
                                        return wasFocused;
 
3488
                                } );
 
3489
                        } );
 
3490
 
 
3491
                        // Focus on the first invalid control.
 
3492
                        if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
 
3493
                                _.values( invalidSettingControls )[0][0].focus();
 
3494
                        }
 
3495
                }
 
3496
        };
 
3497
 
 
3498
        /**
 
3499
         * Find all controls associated with the given settings.
 
3500
         *
 
3501
         * @since 4.6.0
 
3502
         * @param {string[]} settingIds Setting IDs.
 
3503
         * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
 
3504
         */
 
3505
        api.findControlsForSettings = function findControlsForSettings( settingIds ) {
 
3506
                var controls = {}, settingControls;
 
3507
                _.each( _.unique( settingIds ), function( settingId ) {
 
3508
                        var setting = api( settingId );
 
3509
                        if ( setting ) {
 
3510
                                settingControls = setting.findControls();
 
3511
                                if ( settingControls && settingControls.length > 0 ) {
 
3512
                                        controls[ settingId ] = settingControls;
 
3513
                                }
 
3514
                        }
 
3515
                } );
 
3516
                return controls;
 
3517
        };
 
3518
 
 
3519
        /**
 
3520
         * Sort panels, sections, controls by priorities. Hide empty sections and panels.
 
3521
         *
 
3522
         * @since 4.1.0
 
3523
         */
 
3524
        api.reflowPaneContents = _.bind( function () {
 
3525
 
 
3526
                var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
 
3527
 
 
3528
                if ( document.activeElement ) {
 
3529
                        activeElement = $( document.activeElement );
 
3530
                }
 
3531
 
 
3532
                // Sort the sections within each panel
 
3533
                api.panel.each( function ( panel ) {
 
3534
                        var sections = panel.sections(),
 
3535
                                sectionContainers = _.pluck( sections, 'container' );
 
3536
                        rootNodes.push( panel );
 
3537
                        appendContainer = panel.container.find( 'ul:first' );
 
3538
                        if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
 
3539
                                _( sections ).each( function ( section ) {
 
3540
                                        appendContainer.append( section.container );
 
3541
                                } );
 
3542
                                wasReflowed = true;
 
3543
                        }
 
3544
                } );
 
3545
 
 
3546
                // Sort the controls within each section
 
3547
                api.section.each( function ( section ) {
 
3548
                        var controls = section.controls(),
 
3549
                                controlContainers = _.pluck( controls, 'container' );
 
3550
                        if ( ! section.panel() ) {
 
3551
                                rootNodes.push( section );
 
3552
                        }
 
3553
                        appendContainer = section.container.find( 'ul:first' );
 
3554
                        if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
 
3555
                                _( controls ).each( function ( control ) {
 
3556
                                        appendContainer.append( control.container );
 
3557
                                } );
 
3558
                                wasReflowed = true;
 
3559
                        }
 
3560
                } );
 
3561
 
 
3562
                // Sort the root panels and sections
 
3563
                rootNodes.sort( api.utils.prioritySort );
 
3564
                rootContainers = _.pluck( rootNodes, 'container' );
 
3565
                appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
 
3566
                if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
 
3567
                        _( rootNodes ).each( function ( rootNode ) {
 
3568
                                appendContainer.append( rootNode.container );
 
3569
                        } );
 
3570
                        wasReflowed = true;
 
3571
                }
 
3572
 
 
3573
                // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
 
3574
                api.panel.each( function ( panel ) {
 
3575
                        var value = panel.active();
 
3576
                        panel.active.callbacks.fireWith( panel.active, [ value, value ] );
 
3577
                } );
 
3578
                api.section.each( function ( section ) {
 
3579
                        var value = section.active();
 
3580
                        section.active.callbacks.fireWith( section.active, [ value, value ] );
 
3581
                } );
 
3582
 
 
3583
                // Restore focus if there was a reflow and there was an active (focused) element
 
3584
                if ( wasReflowed && activeElement ) {
 
3585
                        activeElement.focus();
 
3586
                }
 
3587
                api.trigger( 'pane-contents-reflowed' );
 
3588
        }, api );
 
3589
 
3242
3590
        $( function() {
3243
3591
                api.settings = window._wpCustomizeSettings;
3244
3592
                api.l10n = window._wpCustomizeControlsL10n;
3272
3620
                });
3273
3621
 
3274
3622
                // Expand/Collapse the main customizer customize info.
3275
 
                $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
3276
 
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3277
 
                                return;
3278
 
                        }
3279
 
                        event.preventDefault(); // Keep this AFTER the key filter above
3280
 
 
 
3623
                $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
3281
3624
                        var section = $( this ).closest( '.accordion-section' ),
3282
3625
                                content = section.find( '.customize-panel-description:first' );
3283
3626
 
3332
3675
                                var self = this,
3333
3676
                                        processing = api.state( 'processing' ),
3334
3677
                                        submitWhenDoneProcessing,
3335
 
                                        submit;
 
3678
                                        submit,
 
3679
                                        modifiedWhileSaving = {},
 
3680
                                        invalidSettings = [],
 
3681
                                        invalidControls;
3336
3682
 
3337
3683
                                body.addClass( 'saving' );
3338
3684
 
 
3685
                                function captureSettingModifiedDuringSave( setting ) {
 
3686
                                        modifiedWhileSaving[ setting.id ] = true;
 
3687
                                }
 
3688
                                api.bind( 'change', captureSettingModifiedDuringSave );
 
3689
 
3339
3690
                                submit = function () {
3340
3691
                                        var request, query;
 
3692
 
 
3693
                                        /*
 
3694
                                         * Block saving if there are any settings that are marked as
 
3695
                                         * invalid from the client (not from the server). Focus on
 
3696
                                         * the control.
 
3697
                                         */
 
3698
                                        api.each( function( setting ) {
 
3699
                                                setting.notifications.each( function( notification ) {
 
3700
                                                        if ( 'error' === notification.type && ( ! notification.data || ! notification.data.from_server ) ) {
 
3701
                                                                invalidSettings.push( setting.id );
 
3702
                                                        }
 
3703
                                                } );
 
3704
                                        } );
 
3705
                                        invalidControls = api.findControlsForSettings( invalidSettings );
 
3706
                                        if ( ! _.isEmpty( invalidControls ) ) {
 
3707
                                                _.values( invalidControls )[0][0].focus();
 
3708
                                                body.removeClass( 'saving' );
 
3709
                                                api.unbind( 'change', captureSettingModifiedDuringSave );
 
3710
                                                return;
 
3711
                                        }
 
3712
 
3341
3713
                                        query = $.extend( self.query(), {
3342
3714
                                                nonce:  self.nonce.save
3343
3715
                                        } );
3344
3716
                                        request = wp.ajax.post( 'customize_save', query );
3345
3717
 
 
3718
                                        // Disable save button during the save request.
 
3719
                                        saveBtn.prop( 'disabled', true );
 
3720
 
3346
3721
                                        api.trigger( 'save', request );
3347
3722
 
3348
3723
                                        request.always( function () {
3349
3724
                                                body.removeClass( 'saving' );
 
3725
                                                saveBtn.prop( 'disabled', false );
 
3726
                                                api.unbind( 'change', captureSettingModifiedDuringSave );
3350
3727
                                        } );
3351
3728
 
3352
3729
                                        request.fail( function ( response ) {
3366
3743
                                                                self.preview.iframe.show();
3367
3744
                                                        } );
3368
3745
                                                }
 
3746
 
 
3747
                                                if ( response.setting_validities ) {
 
3748
                                                        api._handleSettingValidities( {
 
3749
                                                                settingValidities: response.setting_validities,
 
3750
                                                                focusInvalidControl: true
 
3751
                                                        } );
 
3752
                                                }
 
3753
 
3369
3754
                                                api.trigger( 'error', response );
3370
3755
                                        } );
3371
3756
 
3372
3757
                                        request.done( function( response ) {
3373
 
                                                // Clear setting dirty states
3374
 
                                                api.each( function ( value ) {
3375
 
                                                        value._dirty = false;
 
3758
 
 
3759
                                                // Clear setting dirty states, if setting wasn't modified while saving.
 
3760
                                                api.each( function( setting ) {
 
3761
                                                        if ( ! modifiedWhileSaving[ setting.id ] ) {
 
3762
                                                                setting._dirty = false;
 
3763
                                                        }
3376
3764
                                                } );
3377
3765
 
3378
3766
                                                api.previewer.send( 'saved', response );
3379
3767
 
 
3768
                                                if ( response.setting_validities ) {
 
3769
                                                        api._handleSettingValidities( {
 
3770
                                                                settingValidities: response.setting_validities,
 
3771
                                                                focusInvalidControl: true
 
3772
                                                        } );
 
3773
                                                }
 
3774
 
3380
3775
                                                api.trigger( 'saved', response );
 
3776
 
 
3777
                                                // Restore the global dirty state if any settings were modified during save.
 
3778
                                                if ( ! _.isEmpty( modifiedWhileSaving ) ) {
 
3779
                                                        api.state( 'saved' ).set( false );
 
3780
                                                }
3381
3781
                                        } );
3382
3782
                                };
3383
3783
 
3410
3810
 
3411
3811
                // Create Settings
3412
3812
                $.each( api.settings.settings, function( id, data ) {
3413
 
                        api.create( id, id, data.value, {
 
3813
                        var constructor = api.settingConstructor[ data.type ] || api.Setting,
 
3814
                                setting;
 
3815
 
 
3816
                        setting = new constructor( id, data.value, {
3414
3817
                                transport: data.transport,
3415
3818
                                previewer: api.previewer,
3416
3819
                                dirty: !! data.dirty
3417
3820
                        } );
 
3821
                        api.add( id, setting );
3418
3822
                });
3419
3823
 
3420
3824
                // Create Panels
3473
3877
                        });
3474
3878
                });
3475
3879
 
3476
 
                /**
3477
 
                 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3478
 
                 *
3479
 
                 * @since 4.1.0
3480
 
                 */
3481
 
                api.reflowPaneContents = _.bind( function () {
3482
 
 
3483
 
                        var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3484
 
 
3485
 
                        if ( document.activeElement ) {
3486
 
                                activeElement = $( document.activeElement );
3487
 
                        }
3488
 
 
3489
 
                        // Sort the sections within each panel
3490
 
                        api.panel.each( function ( panel ) {
3491
 
                                var sections = panel.sections(),
3492
 
                                        sectionContainers = _.pluck( sections, 'container' );
3493
 
                                rootNodes.push( panel );
3494
 
                                appendContainer = panel.container.find( 'ul:first' );
3495
 
                                if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3496
 
                                        _( sections ).each( function ( section ) {
3497
 
                                                appendContainer.append( section.container );
3498
 
                                        } );
3499
 
                                        wasReflowed = true;
3500
 
                                }
3501
 
                        } );
3502
 
 
3503
 
                        // Sort the controls within each section
3504
 
                        api.section.each( function ( section ) {
3505
 
                                var controls = section.controls(),
3506
 
                                        controlContainers = _.pluck( controls, 'container' );
3507
 
                                if ( ! section.panel() ) {
3508
 
                                        rootNodes.push( section );
3509
 
                                }
3510
 
                                appendContainer = section.container.find( 'ul:first' );
3511
 
                                if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3512
 
                                        _( controls ).each( function ( control ) {
3513
 
                                                appendContainer.append( control.container );
3514
 
                                        } );
3515
 
                                        wasReflowed = true;
3516
 
                                }
3517
 
                        } );
3518
 
 
3519
 
                        // Sort the root panels and sections
3520
 
                        rootNodes.sort( api.utils.prioritySort );
3521
 
                        rootContainers = _.pluck( rootNodes, 'container' );
3522
 
                        appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3523
 
                        if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3524
 
                                _( rootNodes ).each( function ( rootNode ) {
3525
 
                                        appendContainer.append( rootNode.container );
3526
 
                                } );
3527
 
                                wasReflowed = true;
3528
 
                        }
3529
 
 
3530
 
                        // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3531
 
                        api.panel.each( function ( panel ) {
3532
 
                                var value = panel.active();
3533
 
                                panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3534
 
                        } );
3535
 
                        api.section.each( function ( section ) {
3536
 
                                var value = section.active();
3537
 
                                section.active.callbacks.fireWith( section.active, [ value, value ] );
3538
 
                        } );
3539
 
 
3540
 
                        // Restore focus if there was a reflow and there was an active (focused) element
3541
 
                        if ( wasReflowed && activeElement ) {
3542
 
                                activeElement.focus();
3543
 
                        }
3544
 
                        api.trigger( 'pane-contents-reflowed' );
3545
 
                }, api );
3546
3880
                api.bind( 'ready', api.reflowPaneContents );
3547
 
                api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3548
3881
                $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3549
 
                        values.bind( 'add', api.reflowPaneContents );
3550
 
                        values.bind( 'change', api.reflowPaneContents );
3551
 
                        values.bind( 'remove', api.reflowPaneContents );
 
3882
                        var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
 
3883
                        values.bind( 'add', debouncedReflowPaneContents );
 
3884
                        values.bind( 'change', debouncedReflowPaneContents );
 
3885
                        values.bind( 'remove', debouncedReflowPaneContents );
3552
3886
                } );
3553
3887
 
3554
3888
                // Check if preview url is valid and load the preview frame.
3595
3929
                        });
3596
3930
 
3597
3931
                        activated.bind( function( to ) {
3598
 
                                if ( to )
 
3932
                                if ( to ) {
3599
3933
                                        api.trigger( 'activated' );
 
3934
                                }
3600
3935
                        });
3601
3936
 
3602
3937
                        // Expose states to the API.
3633
3968
                        overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3634
3969
                });
3635
3970
 
3636
 
                $( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
3637
 
                        if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
 
3971
                // Keyboard shortcuts - esc to exit section/panel.
 
3972
                $( 'body' ).on( 'keydown', function( event ) {
 
3973
                        var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
 
3974
 
 
3975
                        if ( 27 !== event.which ) { // Esc.
3638
3976
                                return;
3639
3977
                        }
3640
3978
 
 
3979
                        // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
 
3980
                        api.control.each( function( control ) {
 
3981
                                if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
 
3982
                                        expandedControls.push( control );
 
3983
                                }
 
3984
                        });
 
3985
                        api.section.each( function( section ) {
 
3986
                                if ( section.expanded() ) {
 
3987
                                        expandedSections.push( section );
 
3988
                                }
 
3989
                        });
 
3990
                        api.panel.each( function( panel ) {
 
3991
                                if ( panel.expanded() ) {
 
3992
                                        expandedPanels.push( panel );
 
3993
                                }
 
3994
                        });
 
3995
 
 
3996
                        // Skip collapsing expanded controls if there are no expanded sections.
 
3997
                        if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
 
3998
                                expandedControls.length = 0;
 
3999
                        }
 
4000
 
 
4001
                        // Collapse the most granular expanded object.
 
4002
                        collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
 
4003
                        if ( collapsedObject ) {
 
4004
                                collapsedObject.collapse();
 
4005
                                event.preventDefault();
 
4006
                        }
 
4007
                });
 
4008
 
 
4009
                $( '.customize-controls-preview-toggle' ).on( 'click', function() {
3641
4010
                        overlay.toggleClass( 'preview-only' );
3642
 
                        event.preventDefault();
3643
4011
                });
3644
4012
 
3645
4013
                // Previewed device bindings.
3732
4100
                        });
3733
4101
                } );
3734
4102
 
3735
 
                /*
3736
 
                 * When activated, let the loader handle redirecting the page.
3737
 
                 * If no loader exists, redirect the page ourselves (if a url exists).
3738
 
                 */
3739
 
                api.bind( 'activated', function() {
3740
 
                        if ( parent.targetWindow() )
3741
 
                                parent.send( 'activated', api.settings.url.activated );
3742
 
                        else if ( api.settings.url.activated )
3743
 
                                window.location = api.settings.url.activated;
3744
 
                });
3745
 
 
3746
4103
                // Pass titles to the parent
3747
4104
                api.bind( 'title', function( newTitle ) {
3748
4105
                        parent.send( 'title', newTitle );
3822
4179
                        });
3823
4180
                });
3824
4181
 
 
4182
                // Update the setting validities.
 
4183
                api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
 
4184
                        api._handleSettingValidities( {
 
4185
                                settingValidities: settingValidities,
 
4186
                                focusInvalidControl: false
 
4187
                        } );
 
4188
                } );
 
4189
 
3825
4190
                // Focus on the control that is associated with the given setting.
3826
4191
                api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
3827
4192
                        var matchedControl;