1542
1562
control.setting = control.settings['default'] || null;
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;
1571
settingNotification,
1576
controlNotification = new api.Notification( code, params );
1577
control.notifications.add( controlNotification.code, controlNotification );
1579
setting.notifications.bind( 'remove', function( settingNotification ) {
1580
control.notifications.remove( setting.id + ':' + settingNotification.code );
1544
1584
control.embed();
1548
1588
// After the control is embedded on the page, invoke the "ready" method.
1549
1589
control.deferred.embedded.done( function () {
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.
1595
var debouncedRenderNotifications = _.debounce( function renderNotifications() {
1596
control.renderNotifications();
1598
control.notifications.bind( 'add', function( notification ) {
1599
wp.a11y.speak( notification.message, 'assertive' );
1600
debouncedRenderNotifications();
1602
control.notifications.bind( 'remove', debouncedRenderNotifications );
1603
control.renderNotifications();
1550
1605
control.ready();
1589
1644
ready: function() {},
1647
* Get the element inside of a control's container that contains the validation error message.
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.
1654
* @returns {jQuery} Setting validation message element.
1655
* @this {wp.customize.Control}
1657
getNotificationsContainerElement: function() {
1658
var control = this, controlTitle, notificationsContainer;
1660
notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
1661
if ( notificationsContainer.length ) {
1662
return notificationsContainer;
1665
notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
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 );
1672
controlTitle = control.container.find( '.customize-control-title' );
1673
if ( controlTitle.length ) {
1674
controlTitle.after( notificationsContainer );
1676
control.container.prepend( notificationsContainer );
1679
return notificationsContainer;
1683
* Render notifications.
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.
1690
* @this {wp.customize.Control}
1692
renderNotifications: function() {
1693
var control = this, container, notifications, hasError = false;
1694
container = control.getNotificationsContainerElement();
1695
if ( ! container || ! container.length ) {
1699
control.notifications.each( function( notification ) {
1700
notifications.push( notification );
1701
if ( 'error' === notification.type ) {
1706
if ( 0 === notifications.length ) {
1707
container.stop().slideUp( 'fast' );
1709
container.stop().slideDown( 'fast', null, function() {
1710
$( this ).css( 'height', 'auto' );
1714
if ( ! control.notificationsTemplate ) {
1715
control.notificationsTemplate = wp.template( 'customize-control-notifications' );
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 ) } )
1592
1726
* Normal controls do not expand, so just expand its parent
1594
1728
* @param {Object} [params]
1794
1928
control.pausePlayer();
1797
control.setting.bind( function( value ) {
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 );
1932
* Set attachment data and render content.
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.
1941
* @param {number|string} value Attachment
1943
function setAttachmentDataAndRenderContent( value ) {
1944
var hasAttachmentData = $.Deferred();
1946
if ( control.extended( api.UploadControl ) ) {
1947
hasAttachmentData.resolve();
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();
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();
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 );
1969
hasAttachmentData.done( function() {
1970
control.renderContent();
1804
// Re-render whenever the control's setting changes.
1805
control.renderContent();
1974
// Ensure attachment data is initially set (for dynamically-instantiated controls).
1975
setAttachmentDataAndRenderContent( control.setting() );
1977
// Update the attachment data and re-render the control when the setting changes.
1978
control.setting.bind( setAttachmentDataAndRenderContent );
1809
1981
pausePlayer: function () {
3239
3419
themes: api.ThemesSection
3423
* Handle setting_validities in an error response for the customize-save request.
3425
* Add notifications to the settings and focus on the first control that has an invalid setting.
3430
* @param {object} args
3431
* @param {object} args.settingValidities
3432
* @param {boolean} [args.focusInvalidControl=false]
3435
api._handleSettingValidities = function handleSettingValidities( args ) {
3436
var invalidSettingControls, invalidSettings = [], wasFocused = false;
3438
// Find the controls that correspond to each invalid setting.
3439
_.each( args.settingValidities, function( validity, settingId ) {
3440
var setting = api( settingId );
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;
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 );
3453
if ( needsReplacement ) {
3454
setting.notifications.remove( code );
3457
if ( ! setting.notifications.has( notification.code ) ) {
3458
setting.notifications.add( code, notification );
3460
invalidSettings.push( setting.id );
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 );
3473
if ( args.focusInvalidControl ) {
3474
invalidSettingControls = api.findControlsForSettings( invalidSettings );
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();
3491
// Focus on the first invalid control.
3492
if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
3493
_.values( invalidSettingControls )[0][0].focus();
3499
* Find all controls associated with the given settings.
3502
* @param {string[]} settingIds Setting IDs.
3503
* @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
3505
api.findControlsForSettings = function findControlsForSettings( settingIds ) {
3506
var controls = {}, settingControls;
3507
_.each( _.unique( settingIds ), function( settingId ) {
3508
var setting = api( settingId );
3510
settingControls = setting.findControls();
3511
if ( settingControls && settingControls.length > 0 ) {
3512
controls[ settingId ] = settingControls;
3520
* Sort panels, sections, controls by priorities. Hide empty sections and panels.
3524
api.reflowPaneContents = _.bind( function () {
3526
var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3528
if ( document.activeElement ) {
3529
activeElement = $( document.activeElement );
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 );
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 );
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 );
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 );
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 ] );
3578
api.section.each( function ( section ) {
3579
var value = section.active();
3580
section.active.callbacks.fireWith( section.active, [ value, value ] );
3583
// Restore focus if there was a reflow and there was an active (focused) element
3584
if ( wasReflowed && activeElement ) {
3585
activeElement.focus();
3587
api.trigger( 'pane-contents-reflowed' );
3242
3590
$( function() {
3243
3591
api.settings = window._wpCustomizeSettings;
3244
3592
api.l10n = window._wpCustomizeControlsL10n;
3332
3675
var self = this,
3333
3676
processing = api.state( 'processing' ),
3334
3677
submitWhenDoneProcessing,
3679
modifiedWhileSaving = {},
3680
invalidSettings = [],
3337
3683
body.addClass( 'saving' );
3685
function captureSettingModifiedDuringSave( setting ) {
3686
modifiedWhileSaving[ setting.id ] = true;
3688
api.bind( 'change', captureSettingModifiedDuringSave );
3339
3690
submit = function () {
3340
3691
var request, query;
3694
* Block saving if there are any settings that are marked as
3695
* invalid from the client (not from the server). Focus on
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 );
3705
invalidControls = api.findControlsForSettings( invalidSettings );
3706
if ( ! _.isEmpty( invalidControls ) ) {
3707
_.values( invalidControls )[0][0].focus();
3708
body.removeClass( 'saving' );
3709
api.unbind( 'change', captureSettingModifiedDuringSave );
3341
3713
query = $.extend( self.query(), {
3342
3714
nonce: self.nonce.save
3344
3716
request = wp.ajax.post( 'customize_save', query );
3718
// Disable save button during the save request.
3719
saveBtn.prop( 'disabled', true );
3346
3721
api.trigger( 'save', request );
3348
3723
request.always( function () {
3349
3724
body.removeClass( 'saving' );
3725
saveBtn.prop( 'disabled', false );
3726
api.unbind( 'change', captureSettingModifiedDuringSave );
3352
3729
request.fail( function ( response ) {
3366
3743
self.preview.iframe.show();
3747
if ( response.setting_validities ) {
3748
api._handleSettingValidities( {
3749
settingValidities: response.setting_validities,
3750
focusInvalidControl: true
3369
3754
api.trigger( 'error', response );
3372
3757
request.done( function( response ) {
3373
// Clear setting dirty states
3374
api.each( function ( value ) {
3375
value._dirty = false;
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;
3378
3766
api.previewer.send( 'saved', response );
3768
if ( response.setting_validities ) {
3769
api._handleSettingValidities( {
3770
settingValidities: response.setting_validities,
3771
focusInvalidControl: true
3380
3775
api.trigger( 'saved', response );
3777
// Restore the global dirty state if any settings were modified during save.
3778
if ( ! _.isEmpty( modifiedWhileSaving ) ) {
3779
api.state( 'saved' ).set( false );
3477
* Sort panels, sections, controls by priorities. Hide empty sections and panels.
3481
api.reflowPaneContents = _.bind( function () {
3483
var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3485
if ( document.activeElement ) {
3486
activeElement = $( document.activeElement );
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 );
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 );
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 );
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 );
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 ] );
3535
api.section.each( function ( section ) {
3536
var value = section.active();
3537
section.active.callbacks.fireWith( section.active, [ value, value ] );
3540
// Restore focus if there was a reflow and there was an active (focused) element
3541
if ( wasReflowed && activeElement ) {
3542
activeElement.focus();
3544
api.trigger( 'pane-contents-reflowed' );
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 );
3554
3888
// Check if preview url is valid and load the preview frame.
3633
3968
overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
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 = [];
3975
if ( 27 !== event.which ) { // Esc.
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 );
3985
api.section.each( function( section ) {
3986
if ( section.expanded() ) {
3987
expandedSections.push( section );
3990
api.panel.each( function( panel ) {
3991
if ( panel.expanded() ) {
3992
expandedPanels.push( panel );
3996
// Skip collapsing expanded controls if there are no expanded sections.
3997
if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
3998
expandedControls.length = 0;
4001
// Collapse the most granular expanded object.
4002
collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
4003
if ( collapsedObject ) {
4004
collapsedObject.collapse();
4005
event.preventDefault();
4009
$( '.customize-controls-preview-toggle' ).on( 'click', function() {
3641
4010
overlay.toggleClass( 'preview-only' );
3642
event.preventDefault();
3645
4013
// Previewed device bindings.