X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/41578db67d72562346e4dbb2a14889b23d522813..03f2fa83c13c1b532284205fa7efcab9b8b2c41f:/wp-admin/js/customize-widgets.js diff --git a/wp-admin/js/customize-widgets.js b/wp-admin/js/customize-widgets.js index 47505dd3..820e8e3c 100644 --- a/wp-admin/js/customize-widgets.js +++ b/wp-admin/js/customize-widgets.js @@ -5,9 +5,10 @@ // Set up our namespace... var api = wp.customize, - l10n, OldPreviewer; + l10n; api.Widgets = api.Widgets || {}; + api.Widgets.savedWidgetIds = {}; // Link settings api.Widgets.data = _wpCustomizeWidgetsSettings || {}; @@ -176,8 +177,8 @@ // If the available widgets panel is open and the customize controls are // interacted with (i.e. available widgets panel is blurred) then close the - // available widgets panel. - $( '#customize-controls' ).on( 'click keydown', function( e ) { + // available widgets panel. Also close on back button click. + $( '#customize-controls, #available-widgets .customize-section-title' ).on( 'click keydown', function( e ) { var isAddNewBtn = $( e.target ).is( '.add-new-widget, .add-new-widget *' ); if ( $( 'body' ).hasClass( 'adding-widget' ) && ! isAddNewBtn ) { self.close(); @@ -185,7 +186,7 @@ } ); // Close the panel if the URL in the preview changes - api.Widgets.Previewer.bind( 'url', this.close ); + api.previewer.bind( 'url', this.close ); }, // Performs a search and handles selected widget @@ -250,7 +251,7 @@ // Adds a selected widget to the sidebar submit: function( widgetTpl ) { - var widgetId, widget; + var widgetId, widget, widgetFormControl; if ( ! widgetTpl ) { widgetTpl = this.selected; @@ -268,7 +269,10 @@ return; } - this.currentSidebarControl.addWidget( widget.get( 'id_base' ) ); + widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) ); + if ( widgetFormControl ) { + widgetFormControl.focus(); + } this.close(); }, @@ -291,7 +295,9 @@ // Reset search this.collection.doSearch( '' ); - this.$search.focus(); + if ( ! api.settings.browser.mobile ) { + this.$search.focus(); + } }, // Closes the panel @@ -316,10 +322,13 @@ isEsc = ( event.which === 27 ), isDown = ( event.which === 40 ), isUp = ( event.which === 38 ), + isTab = ( event.which === 9 ), + isShift = ( event.shiftKey ), selected = null, firstVisible = this.$el.find( '> .widget-tpl:visible:first' ), lastVisible = this.$el.find( '> .widget-tpl:visible:last' ), - isSearchFocused = $( event.target ).is( this.$search ); + isSearchFocused = $( event.target ).is( this.$search ), + isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' ); if ( isDown || isUp ) { if ( isDown ) { @@ -357,6 +366,11 @@ } else if ( isEsc ) { this.close( { returnFocus: true } ); } + + if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) { + this.currentSidebarControl.container.find( '.add-new-widget' ).focus(); + event.preventDefault(); + } } }); @@ -396,18 +410,112 @@ * @augments wp.customize.Control */ api.Widgets.WidgetControl = api.Control.extend({ + defaultExpandedArguments: { + duration: 'fast', + completeCallback: $.noop + }, + /** - * Set up the control + * @since 4.1.0 + */ + initialize: function( id, options ) { + var control = this; + + control.widgetControlEmbedded = false; + control.widgetContentEmbedded = false; + control.expanded = new api.Value( false ); + control.expandedArgumentsQueue = []; + control.expanded.bind( function( expanded ) { + var args = control.expandedArgumentsQueue.shift(); + args = $.extend( {}, control.defaultExpandedArguments, args ); + control.onChangeExpanded( expanded, args ); + }); + + api.Control.prototype.initialize.call( control, id, options ); + }, + + /** + * Set up the control. + * + * @since 3.9.0 */ ready: function() { - this._setupModel(); - this._setupWideWidget(); - this._setupControlToggle(); - this._setupWidgetTitle(); - this._setupReorderUI(); - this._setupHighlightEffects(); - this._setupUpdateUI(); - this._setupRemoveUI(); + var control = this; + + /* + * Embed a placeholder once the section is expanded. The full widget + * form content will be embedded once the control itself is expanded, + * and at this point the widget-added event will be triggered. + */ + if ( ! control.section() ) { + control.embedWidgetControl(); + } else { + api.section( control.section(), function( section ) { + var onExpanded = function( isExpanded ) { + if ( isExpanded ) { + control.embedWidgetControl(); + section.expanded.unbind( onExpanded ); + } + }; + if ( section.expanded() ) { + onExpanded( true ); + } else { + section.expanded.bind( onExpanded ); + } + } ); + } + }, + + /** + * Embed the .widget element inside the li container. + * + * @since 4.4.0 + */ + embedWidgetControl: function() { + var control = this, widgetControl; + + if ( control.widgetControlEmbedded ) { + return; + } + control.widgetControlEmbedded = true; + + widgetControl = $( control.params.widget_control ); + control.container.append( widgetControl ); + + control._setupModel(); + control._setupWideWidget(); + control._setupControlToggle(); + + control._setupWidgetTitle(); + control._setupReorderUI(); + control._setupHighlightEffects(); + control._setupUpdateUI(); + control._setupRemoveUI(); + }, + + /** + * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event. + * + * @since 4.4.0 + */ + embedWidgetContent: function() { + var control = this, widgetContent; + + control.embedWidgetControl(); + if ( control.widgetContentEmbedded ) { + return; + } + control.widgetContentEmbedded = true; + + widgetContent = $( control.params.widget_content ); + control.container.find( '.widget-content:first' ).append( widgetContent ); + + /* + * Trigger widget-added event so that plugins can attach any event + * listeners and dynamic UI elements. + */ + $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] ); + }, /** @@ -416,8 +524,6 @@ _setupModel: function() { var self = this, rememberSavedWidgetId; - api.Widgets.savedWidgetIds = api.Widgets.savedWidgetIds || []; - // Remember saved widgets so we know which to trash (move to inactive widgets sidebar) rememberSavedWidgetId = function() { api.Widgets.savedWidgetIds[self.params.widget_id] = true; @@ -521,13 +627,13 @@ if ( sidebarWidgetsControl.isReordering ) { return; } - self.toggleForm(); + self.expanded( ! self.expanded() ); } ); $closeBtn = this.container.find( '.widget-control-close' ); $closeBtn.on( 'click', function( e ) { e.preventDefault(); - self.collapseForm(); + self.collapse(); self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility } ); }, @@ -586,7 +692,8 @@ * Update available sidebars when their rendered state changes */ updateAvailableSidebars = function() { - var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem; + var $sidebarItems = $moveWidgetArea.find( 'li' ), selfSidebarItem, + renderedSidebarCount = 0; selfSidebarItem = $sidebarItems.filter( function(){ return $( this ).data( 'id' ) === self.params.sidebar_id; @@ -594,18 +701,28 @@ $sidebarItems.each( function() { var li = $( this ), - sidebarId, - sidebar; + sidebarId, sidebar, sidebarIsRendered; sidebarId = li.data( 'id' ); sidebar = api.Widgets.registeredSidebars.get( sidebarId ); + sidebarIsRendered = sidebar.get( 'is_rendered' ); - li.toggle( sidebar.get( 'is_rendered' ) ); + li.toggle( sidebarIsRendered ); - if ( li.hasClass( 'selected' ) && ! sidebar.get( 'is_rendered' ) ) { + if ( sidebarIsRendered ) { + renderedSidebarCount += 1; + } + + if ( li.hasClass( 'selected' ) && ! sidebarIsRendered ) { selectSidebarItem( selfSidebarItem ); } } ); + + if ( renderedSidebarCount > 1 ) { + self.container.find( '.move-widget' ).show(); + } else { + self.container.find( '.move-widget' ).hide(); + } }; updateAvailableSidebars(); @@ -615,7 +732,9 @@ * Handle clicks for up/down/move on the reorder nav */ $reorderNav = this.container.find( '.widget-reorder-nav' ); - $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function( event ) { + $reorderNav.find( '.move-widget, .move-widget-down, .move-widget-up' ).each( function() { + $( this ).prepend( self.container.find( '.widget-title' ).text() + ': ' ); + } ).on( 'click keypress', function( event ) { if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { return; } @@ -634,8 +753,10 @@ if ( isMoveUp ) { self.moveUp(); + wp.a11y.speak( l10n.widgetMovedUp ); } else { self.moveDown(); + wp.a11y.speak( l10n.widgetMovedDown ); } $( this ).focus(); // re-focus after the container was moved @@ -645,11 +766,11 @@ /** * Handle selecting a sidebar to move to */ - this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( e ) { + this.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function( event ) { if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) { return; } - e.preventDefault(); + event.preventDefault(); selectSidebarItem( $( this ) ); } ); @@ -681,7 +802,7 @@ }, /** - * Highlight widgets in preview when interacted with in the customizer + * Highlight widgets in preview when interacted with in the Customizer */ _setupHighlightEffects: function() { var self = this; @@ -731,12 +852,11 @@ // Handle widgets that support live previews $widgetContent.on( 'change input propertychange', ':input', function( e ) { - if ( self.liveUpdateMode ) { - if ( e.type === 'change' ) { - self.updateWidget(); - } else if ( this.checkValidity && this.checkValidity() ) { - updateWidgetDebounced(); - } + if ( ! self.liveUpdateMode ) { + return; + } + if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) { + updateWidgetDebounced(); } } ); @@ -745,19 +865,12 @@ self.container.removeClass( 'previewer-loading' ); } ); - api.Widgets.Previewer.bind( 'widget-updated', function( updatedWidgetId ) { + api.previewer.bind( 'widget-updated', function( updatedWidgetId ) { if ( updatedWidgetId === self.params.widget_id ) { self.container.removeClass( 'previewer-loading' ); } } ); - // Update widget control to indicate whether it is currently rendered - api.Widgets.Previewer.bind( 'rendered-widgets', function( renderedWidgets ) { - var isRendered = !! renderedWidgets[self.params.widget_id]; - - self.container.toggleClass( 'widget-rendered', isRendered ); - } ); - formSyncHandler = api.Widgets.formSyncHandlers[ this.params.widget_id_base ]; if ( formSyncHandler ) { $( document ).on( 'widget-synced', function( e, widget ) { @@ -768,6 +881,25 @@ } }, + /** + * Update widget control to indicate whether it is currently rendered. + * + * Overrides api.Control.toggle() + * + * @since 4.1.0 + * + * @param {Boolean} active + * @param {Object} args + * @param {Callback} args.completeCallback + */ + onChangeActive: function ( active, args ) { + // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments + this.container.toggleClass( 'widget-rendered', active ); + if ( args.completeCallback ) { + args.completeCallback(); + } + }, + /** * Set up event handlers for widget removal */ @@ -862,19 +994,50 @@ }, /** - * Get the property that represents the state of an input. + * Get the state for an input depending on its type. * - * @param {jQuery|DOMElement} input - * @returns {string} + * @param {jQuery|Element} input + * @returns {string|boolean|array|*} * @private */ - _getInputStatePropertyName: function( input ) { - var $input = $( input ); + _getInputState: function( input ) { + input = $( input ); + if ( input.is( ':radio, :checkbox' ) ) { + return input.prop( 'checked' ); + } else if ( input.is( 'select[multiple]' ) ) { + return input.find( 'option:selected' ).map( function () { + return $( this ).val(); + } ).get(); + } else { + return input.val(); + } + }, - if ( $input.is( ':radio, :checkbox' ) ) { - return 'checked'; + /** + * Update an input's state based on its type. + * + * @param {jQuery|Element} input + * @param {string|boolean|array|*} state + * @private + */ + _setInputState: function ( input, state ) { + input = $( input ); + if ( input.is( ':radio, :checkbox' ) ) { + input.prop( 'checked', state ); + } else if ( input.is( 'select[multiple]' ) ) { + if ( ! $.isArray( state ) ) { + state = []; + } else { + // Make sure all state items are strings since the DOM value is a string + state = _.map( state, function ( value ) { + return String( value ); + } ); + } + input.find( 'option' ).each( function () { + $( this ).prop( 'selected', -1 !== _.indexOf( state, String( this.value ) ) ); + } ); } else { - return 'value'; + input.val( state ); } }, @@ -911,6 +1074,9 @@ var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent, updateNumber, params, data, $inputs, processing, jqxhr, isChanged; + // The updateWidget logic requires that the form fields to be fully present. + self.embedWidgetContent(); + args = $.extend( { instance: null, complete: null, @@ -943,6 +1109,7 @@ params.wp_customize = 'on'; params.nonce = api.Widgets.data.nonce; params.theme = api.settings.theme.stylesheet; + params.customized = wp.customize.previewer.query().customized; data = $.param( params ); $inputs = this._getInputs( $widgetContent ); @@ -951,9 +1118,7 @@ // we know if it got sanitized; if there is no difference in the sanitized value, // then we do not need to touch the UI and mess up the user's ongoing editing. $inputs.each( function() { - var input = $( this ), - property = self._getInputStatePropertyName( this ); - input.data( 'state' + updateNumber, input.prop( property ) ); + $( this ).data( 'state' + updateNumber, self._getInputState( this ) ); } ); if ( instanceOverride ) { @@ -963,7 +1128,11 @@ } data += '&' + $widgetContent.find( '~ :input' ).serialize(); + if ( this._previousUpdateRequest ) { + this._previousUpdateRequest.abort(); + } jqxhr = $.post( wp.ajax.settings.url, data ); + this._previousUpdateRequest = jqxhr; jqxhr.done( function( r ) { var message, sanitizedForm, $sanitizedInputs, hasSameInputsInResponse, @@ -971,17 +1140,17 @@ // Check if the user is logged out. if ( '0' === r ) { - api.Widgets.Previewer.preview.iframe.hide(); - api.Widgets.Previewer.login().done( function() { + api.previewer.preview.iframe.hide(); + api.previewer.login().done( function() { self.updateWidget( args ); - api.Widgets.Previewer.preview.iframe.show(); + api.previewer.preview.iframe.show(); } ); return; } // Check for cheaters. if ( '-1' === r ) { - api.Widgets.Previewer.cheatin(); + api.previewer.cheatin(); return; } @@ -1002,16 +1171,15 @@ $inputs.each( function( i ) { var $input = $( this ), $sanitizedInput = $( $sanitizedInputs[i] ), - property = self._getInputStatePropertyName( this ), submittedState, sanitizedState, canUpdateState; submittedState = $input.data( 'state' + updateNumber ); - sanitizedState = $sanitizedInput.prop( property ); + sanitizedState = self._getInputState( $sanitizedInput ); $input.data( 'sanitized', sanitizedState ); - canUpdateState = ( submittedState !== sanitizedState && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) ); + canUpdateState = ( ! _.isEqual( submittedState, sanitizedState ) && ( args.ignoreActiveElement || ! $input.is( document.activeElement ) ) ); if ( canUpdateState ) { - $input.prop( property, sanitizedState ); + self._setInputState( $input, sanitizedState ); } } ); @@ -1087,51 +1255,103 @@ * Expand the accordion section containing a control */ expandControlSection: function() { - var $section = this.container.closest( '.accordion-section' ); - - if ( ! $section.hasClass( 'open' ) ) { - $section.find( '.accordion-section-title:first' ).trigger( 'click' ); - } + api.Control.prototype.expand.call( this ); }, + /** + * @since 4.1.0 + * + * @param {Boolean} expanded + * @param {Object} [params] + * @returns {Boolean} false if state already applied + */ + _toggleExpanded: api.Section.prototype._toggleExpanded, + + /** + * @since 4.1.0 + * + * @param {Object} [params] + * @returns {Boolean} false if already expanded + */ + expand: api.Section.prototype.expand, + /** * Expand the widget form control + * + * @deprecated 4.1.0 Use this.expand() instead. */ expandForm: function() { - this.toggleForm( true ); + this.expand(); }, + /** + * @since 4.1.0 + * + * @param {Object} [params] + * @returns {Boolean} false if already collapsed + */ + collapse: api.Section.prototype.collapse, + /** * Collapse the widget form control + * + * @deprecated 4.1.0 Use this.collapse() instead. */ collapseForm: function() { - this.toggleForm( false ); + this.collapse(); }, /** * Expand or collapse the widget control * + * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) + * * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility */ toggleForm: function( showOrHide ) { - var self = this, $widget, $inside, complete; - - $widget = this.container.find( 'div.widget:first' ); - $inside = $widget.find( '.widget-inside:first' ); if ( typeof showOrHide === 'undefined' ) { - showOrHide = ! $inside.is( ':visible' ); + showOrHide = ! this.expanded(); + } + this.expanded( showOrHide ); + }, + + /** + * Respond to change in the expanded state. + * + * @param {Boolean} expanded + * @param {Object} args merged on top of this.defaultActiveArguments + */ + onChangeExpanded: function ( expanded, args ) { + var self = this, $widget, $inside, complete, prevComplete; + + self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. + if ( expanded ) { + self.embedWidgetContent(); } - // Already expanded or collapsed, so noop - if ( $inside.is( ':visible' ) === showOrHide ) { + // If the expanded state is unchanged only manipulate container expanded states + if ( args.unchanged ) { + if ( expanded ) { + api.Control.prototype.expand.call( self, { + completeCallback: args.completeCallback + }); + } return; } - if ( showOrHide ) { + $widget = this.container.find( 'div.widget:first' ); + $inside = $widget.find( '.widget-inside:first' ); + + if ( expanded ) { + + if ( self.section() && api.section( self.section() ) ) { + self.expandControlSection(); + } + // Close all other widget controls before expanding this one api.control.each( function( otherControl ) { if ( self.params.type === otherControl.params.type && self !== otherControl ) { - otherControl.collapseForm(); + otherControl.collapse(); } } ); @@ -1140,29 +1360,44 @@ self.container.addClass( 'expanded' ); self.container.trigger( 'expanded' ); }; + if ( args.completeCallback ) { + prevComplete = complete; + complete = function () { + prevComplete(); + args.completeCallback(); + }; + } if ( self.params.is_wide ) { - $inside.fadeIn( 'fast', complete ); + $inside.fadeIn( args.duration, complete ); } else { - $inside.slideDown( 'fast', complete ); + $inside.slideDown( args.duration, complete ); } self.container.trigger( 'expand' ); self.container.addClass( 'expanding' ); } else { + complete = function() { self.container.removeClass( 'collapsing' ); self.container.removeClass( 'expanded' ); self.container.trigger( 'collapsed' ); }; + if ( args.completeCallback ) { + prevComplete = complete; + complete = function () { + prevComplete(); + args.completeCallback(); + }; + } self.container.trigger( 'collapse' ); self.container.addClass( 'collapsing' ); if ( self.params.is_wide ) { - $inside.fadeOut( 'fast', complete ); + $inside.fadeOut( args.duration, complete ); } else { - $inside.slideUp( 'fast', function() { + $inside.slideUp( args.duration, function() { $widget.css( { width:'', margin:'' } ); complete(); } ); @@ -1170,16 +1405,6 @@ } }, - /** - * Expand the containing sidebar section, expand the form, and focus on - * the first input in the control - */ - focus: function() { - this.expandControlSection(); - this.expandForm(); - this.container.find( '.widget-content :focusable:first' ).focus(); - }, - /** * Get the position (index) of the widget in the containing sidebar * @@ -1280,16 +1505,115 @@ } } ); + /** + * wp.customize.Widgets.WidgetsPanel + * + * Customizer panel containing the widget area sections. + * + * @since 4.4.0 + */ + api.Widgets.WidgetsPanel = api.Panel.extend({ + + /** + * Add and manage the display of the no-rendered-areas notice. + * + * @since 4.4.0 + */ + ready: function () { + var panel = this; + + api.Panel.prototype.ready.call( panel ); + + panel.deferred.embedded.done(function() { + var panelMetaContainer, noRenderedAreasNotice, shouldShowNotice; + panelMetaContainer = panel.container.find( '.panel-meta' ); + noRenderedAreasNotice = $( '
', { + 'class': 'no-widget-areas-rendered-notice' + }); + noRenderedAreasNotice.append( $( '', { + text: l10n.noAreasRendered + } ) ); + panelMetaContainer.append( noRenderedAreasNotice ); + + shouldShowNotice = function() { + return ( 0 === _.filter( panel.sections(), function( section ) { + return section.active(); + } ).length ); + }; + + /* + * Set the initial visibility state for rendered notice. + * Update the visibility of the notice whenever a reflow happens. + */ + noRenderedAreasNotice.toggle( shouldShowNotice() ); + api.previewer.deferred.active.done( function () { + noRenderedAreasNotice.toggle( shouldShowNotice() ); + }); + api.bind( 'pane-contents-reflowed', function() { + var duration = ( 'resolved' === api.previewer.deferred.active.state() ) ? 'fast' : 0; + if ( shouldShowNotice() ) { + noRenderedAreasNotice.slideDown( duration ); + } else { + noRenderedAreasNotice.slideUp( duration ); + } + }); + }); + }, + + /** + * Allow an active widgets panel to be contextually active even when it has no active sections (widget areas). + * + * This ensures that the widgets panel appears even when there are no + * sidebars displayed on the URL currently being previewed. + * + * @since 4.4.0 + * + * @returns {boolean} + */ + isContextuallyActive: function() { + var panel = this; + return panel.active(); + } + }); + + /** + * wp.customize.Widgets.SidebarSection + * + * Customizer section representing a widget area widget + * + * @since 4.1.0 + */ + api.Widgets.SidebarSection = api.Section.extend({ + + /** + * Sync the section's active state back to the Backbone model's is_rendered attribute + * + * @since 4.1.0 + */ + ready: function () { + var section = this, registeredSidebar; + api.Section.prototype.ready.call( this ); + registeredSidebar = api.Widgets.registeredSidebars.get( section.params.sidebarId ); + section.active.bind( function ( active ) { + registeredSidebar.set( 'is_rendered', active ); + }); + registeredSidebar.set( 'is_rendered', section.active() ); + } + }); + /** * wp.customize.Widgets.SidebarControl * * Customizer control for widgets. * Note that 'sidebar_widgets' must match the WP_Widget_Area_Customize_Control::$type * + * @since 3.9.0 + * * @constructor * @augments wp.customize.Control */ api.Widgets.SidebarControl = api.Control.extend({ + /** * Set up the control */ @@ -1307,11 +1631,10 @@ * Update ordering of widget control forms when the setting is updated */ _setupModel: function() { - var self = this, - registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id ); + var self = this; this.setting.bind( function( newWidgetIds, oldWidgetIds ) { - var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds; + var widgetFormControls, removedWidgetIds, priority; removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); @@ -1336,21 +1659,16 @@ widgetFormControls.sort( function( a, b ) { var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); + return aIndex - bIndex; + }); - if ( aIndex === bIndex ) { - return 0; - } - - return aIndex < bIndex ? -1 : 1; - } ); - - // Append the controls to put them in the right order - finalControlContainers = _( widgetFormControls ).map( function( widgetFormControls ) { - return widgetFormControls.container[0]; - } ); - - $sidebarWidgetsAddControl = self.$sectionContent.find( '.customize-control-sidebar_widgets' ); - $sidebarWidgetsAddControl.before( finalControlContainers ); + priority = 0; + _( widgetFormControls ).each( function ( control ) { + control.priority( priority ); + control.section( self.section() ); + priority += 1; + }); + self.priority( priority ); // Make sure sidebar control remains at end // Re-sort widget form controls (including widgets form other sidebars newly moved here) self._applyCardinalOrderClassNames(); @@ -1416,34 +1734,6 @@ } ); } ); - - // Update the model with whether or not the sidebar is rendered - api.Widgets.Previewer.bind( 'rendered-sidebars', function( renderedSidebars ) { - var isRendered = !! renderedSidebars[self.params.sidebar_id]; - - registeredSidebar.set( 'is_rendered', isRendered ); - } ); - - // Show the sidebar section when it becomes visible - registeredSidebar.on( 'change:is_rendered', function( ) { - var sectionSelector = '#accordion-section-sidebar-widgets-' + this.get( 'id' ), $section; - - $section = $( sectionSelector ); - if ( this.get( 'is_rendered' ) ) { - $section.stop().slideDown( function() { - $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow - } ); - - } else { - // Make sure that hidden sections get closed first - if ( $section.hasClass( 'open' ) ) { - // it would be nice if accordionSwitch() in accordion.js was public - $section.find( '.accordion-section-title' ).trigger( 'click' ); - } - - $section.stop().slideUp(); - } - } ); }, /** @@ -1461,6 +1751,7 @@ items: '> .customize-control-widget_form', handle: '.widget-top', axis: 'y', + tolerance: 'pointer', connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)', update: function() { var widgetContainerIds = self.$sectionContent.sortable( 'toArray' ), widgetIds; @@ -1474,29 +1765,31 @@ } ); /** - * Expand other customizer sidebar section when dragging a control widget over it, + * Expand other Customizer sidebar section when dragging a control widget over it, * allowing the control to be dropped into another section */ this.$controlSection.find( '.accordion-section-title' ).droppable({ accept: '.customize-control-widget_form', over: function() { - if ( ! self.$controlSection.hasClass( 'open' ) ) { - self.$controlSection.addClass( 'open' ); - self.$sectionContent.toggle( false ).slideToggle( 150, function() { - self.$sectionContent.sortable( 'refreshPositions' ); - } ); - } + var section = api.section( self.section.get() ); + section.expand({ + allowMultiple: true, // Prevent the section being dragged from to be collapsed + completeCallback: function () { + // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed + api.section.each( function ( otherSection ) { + if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { + otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); + } + } ); + } + }); } }); /** * Keyboard-accessible reordering */ - this.container.find( '.reorder-toggle' ).on( 'click keydown', function( event ) { - if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar - return; - } - + this.container.find( '.reorder-toggle' ).on( 'click', function() { self.toggleReordering( ! self.isReordering ); } ); }, @@ -1507,18 +1800,18 @@ _setupAddition: function() { var self = this; - this.container.find( '.add-new-widget' ).on( 'click keydown', function( event ) { - if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar - return; - } + this.container.find( '.add-new-widget' ).on( 'click', function() { + var addNewWidgetBtn = $( this ); if ( self.$sectionContent.hasClass( 'reordering' ) ) { return; } if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) { + addNewWidgetBtn.attr( 'aria-expanded', 'true' ); api.Widgets.availableWidgetsPanel.open( self ); } else { + addNewWidgetBtn.attr( 'aria-expanded', 'false' ); api.Widgets.availableWidgetsPanel.close(); } } ); @@ -1528,16 +1821,33 @@ * Add classes to the widget_form controls to assist with styling */ _applyCardinalOrderClassNames: function() { - this.$sectionContent.find( '.customize-control-widget_form' ) - .removeClass( 'first-widget' ) - .removeClass( 'last-widget' ) - .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); + var widgetControls = []; + _.each( this.setting(), function ( widgetId ) { + var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); + if ( widgetControl ) { + widgetControls.push( widgetControl ); + } + }); + + if ( ! widgetControls.length ) { + this.container.find( '.reorder-toggle' ).hide(); + return; + } else { + this.container.find( '.reorder-toggle' ).show(); + } - this.$sectionContent.find( '.customize-control-widget_form:first' ) + $( widgetControls ).each( function () { + $( this.container ) + .removeClass( 'first-widget' ) + .removeClass( 'last-widget' ) + .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); + }); + + _.first( widgetControls ).container .addClass( 'first-widget' ) .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); - this.$sectionContent.find( '.customize-control-widget_form:last' ) + _.last( widgetControls ).container .addClass( 'last-widget' ) .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); }, @@ -1551,8 +1861,14 @@ * Enable/disable the reordering UI * * @param {Boolean} showOrHide to enable/disable reordering + * + * @todo We should have a reordering state instead and rename this to onChangeReordering */ toggleReordering: function( showOrHide ) { + var addNewWidgetBtn = this.$sectionContent.find( '.add-new-widget' ), + reorderBtn = this.container.find( '.reorder-toggle' ), + widgetsTitle = this.$sectionContent.find( '.widget-title' ); + showOrHide = Boolean( showOrHide ); if ( showOrHide === this.$sectionContent.hasClass( 'reordering' ) ) { @@ -1564,26 +1880,37 @@ if ( showOrHide ) { _( this.getWidgetFormControls() ).each( function( formControl ) { - formControl.collapseForm(); + formControl.collapse(); } ); + + addNewWidgetBtn.attr({ 'tabindex': '-1', 'aria-hidden': 'true' }); + reorderBtn.attr( 'aria-label', l10n.reorderLabelOff ); + wp.a11y.speak( l10n.reorderModeOn ); + // Hide widget titles while reordering: title is already in the reorder controls. + widgetsTitle.attr( 'aria-hidden', 'true' ); + } else { + addNewWidgetBtn.removeAttr( 'tabindex aria-hidden' ); + reorderBtn.attr( 'aria-label', l10n.reorderLabelOn ); + wp.a11y.speak( l10n.reorderModeOff ); + widgetsTitle.attr( 'aria-hidden', 'false' ); } }, /** + * Get the widget_form Customize controls associated with the current sidebar. + * + * @since 3.9 * @return {wp.customize.controlConstructor.widget_form[]} */ getWidgetFormControls: function() { - var formControls; + var formControls = []; - formControls = _( this.setting() ).map( function( widgetId ) { + _( this.setting() ).each( function( widgetId ) { var settingId = widgetIdToSettingId( widgetId ), formControl = api.control( settingId ); - - if ( ! formControl ) { - return; + if ( formControl ) { + formControls.push( formControl ); } - - return formControl; } ); return formControls; @@ -1594,12 +1921,12 @@ * @returns {object|false} widget_form control instance, or false on error */ addWidget: function( widgetId ) { - var self = this, controlHtml, $widget, controlType = 'widget_form', $control, controlConstructor, + var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, parsedWidgetId = parseWidgetId( widgetId ), widgetNumber = parsedWidgetId.number, widgetIdBase = parsedWidgetId.id_base, widget = api.Widgets.availableWidgets.findWhere( {id_base: widgetIdBase} ), - settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs; + settingId, isExistingWidget, widgetFormControl, sidebarWidgets, settingArgs, setting; if ( ! widget ) { return false; @@ -1626,30 +1953,28 @@ $widget = $( controlHtml ); - $control = $( '
  • ' ) + controlContainer = $( '
  • ' ) .addClass( 'customize-control' ) .addClass( 'customize-control-' + controlType ) .append( $widget ); // Remove icon which is visible inside the panel - $control.find( '> .widget-icon' ).remove(); + controlContainer.find( '> .widget-icon' ).remove(); if ( widget.get( 'is_multi' ) ) { - $control.find( 'input[name="widget_number"]' ).val( widgetNumber ); - $control.find( 'input[name="multi_number"]' ).val( widgetNumber ); + controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); + controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); } - widgetId = $control.find( '[name="widget-id"]' ).val(); + widgetId = controlContainer.find( '[name="widget-id"]' ).val(); - $control.hide(); // to be slid-down below + controlContainer.hide(); // to be slid-down below settingId = 'widget_' + widget.get( 'id_base' ); if ( widget.get( 'is_multi' ) ) { settingId += '[' + widgetNumber + ']'; } - $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); - - this.container.after( $control ); + controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget) isExistingWidget = api.has( settingId ); @@ -1658,7 +1983,8 @@ transport: 'refresh', previewer: this.setting.previewer }; - api.create( settingId, settingId, {}, settingArgs ); + setting = api.create( settingId, settingId, '', settingArgs ); + setting.set( {} ); // mark dirty, changing from '' to {} } controlConstructor = api.controlConstructor[controlType]; @@ -1667,6 +1993,7 @@ settings: { 'default': settingId }, + content: controlContainer, sidebar_id: self.params.sidebar_id, widget_id: widgetId, widget_id_base: widget.get( 'id_base' ), @@ -1674,7 +2001,8 @@ is_new: ! isExistingWidget, width: widget.get( 'width' ), height: widget.get( 'height' ), - is_wide: widget.get( 'is_wide' ) + is_wide: widget.get( 'is_wide' ), + active: true }, previewer: self.setting.previewer } ); @@ -1706,49 +2034,34 @@ this.setting( sidebarWidgets ); } - $control.slideDown( function() { + controlContainer.slideDown( function() { if ( isExistingWidget ) { - widgetFormControl.expandForm(); widgetFormControl.updateWidget( { - instance: widgetFormControl.setting(), - complete: function( error ) { - if ( error ) { - throw error; - } - widgetFormControl.focus(); - } + instance: widgetFormControl.setting() } ); - } else { - widgetFormControl.focus(); } } ); - $( document ).trigger( 'widget-added', [ $widget ] ); - return widgetFormControl; } } ); - /** - * Extends wp.customizer.controlConstructor with control constructor for - * widget_form and sidebar_widgets. - */ + // Register models for custom panel, section, and control types + $.extend( api.panelConstructor, { + widgets: api.Widgets.WidgetsPanel + }); + $.extend( api.sectionConstructor, { + sidebar: api.Widgets.SidebarSection + }); $.extend( api.controlConstructor, { widget_form: api.Widgets.WidgetControl, sidebar_widgets: api.Widgets.SidebarControl }); - /** - * Capture the instance of the Previewer since it is private - */ - OldPreviewer = api.Previewer; - api.Previewer = OldPreviewer.extend({ - initialize: function( params, options ) { - api.Widgets.Previewer = this; - OldPreviewer.prototype.initialize.call( this, params, options ); - this.bind( 'refresh', this.refresh ); - } - } ); + // Refresh the nonce if login sends updated nonces over. + api.bind( 'nonce-refresh', function( nonces ) { + api.Widgets.data.nonce = nonces['update-widget']; + }); /** * Init Customizer for widgets. @@ -1760,10 +2073,10 @@ }); // Highlight widget control - api.Widgets.Previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); + api.previewer.bind( 'highlight-widget-control', api.Widgets.highlightWidgetFormControl ); // Open and focus widget control - api.Widgets.Previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); + api.previewer.bind( 'focus-widget-control', api.Widgets.focusWidgetFormControl ); } ); /**