X-Git-Url: https://scripts.mit.edu/gitweb/autoinstalls/wordpress.git/blobdiff_plain/7f1521bf193b382565eb753043c161f4cb3fcda7..4ea0dca21bda49aab5ccb91ec12bb4ef5924ed3e:/wp-admin/js/customize-widgets.js?ds=sidebyside diff --git a/wp-admin/js/customize-widgets.js b/wp-admin/js/customize-widgets.js index 729e6a8f..25c1eb5b 100644 --- a/wp-admin/js/customize-widgets.js +++ b/wp-admin/js/customize-widgets.js @@ -8,6 +8,7 @@ l10n; api.Widgets = api.Widgets || {}; + api.Widgets.savedWidgetIds = {}; // Link settings api.Widgets.data = _wpCustomizeWidgetsSettings || {}; @@ -33,7 +34,7 @@ multi_number: null, name: null, id_base: null, - transport: 'refresh', + transport: null, params: [], width: null, height: null, @@ -69,8 +70,7 @@ this.search( this.terms ); } - // If search is blank, show all themes - // Useful for resetting the views when you clean the input + // If search is blank, set all the widgets as they matched the search to reset the views. if ( this.terms === '' ) { this.each( function ( widget ) { widget.set( 'search_matched', true ); @@ -148,8 +148,6 @@ events: { 'input #widgets-search': 'search', 'keyup #widgets-search': 'search', - 'change #widgets-search': 'search', - 'search #widgets-search': 'search', 'focus .widget-tpl' : 'focus', 'click .widget-tpl' : '_submit', 'keypress .widget-tpl' : '_submit', @@ -162,28 +160,40 @@ // Cache sidebar control which has opened panel currentSidebarControl: null, $search: null, + $clearResults: null, + searchMatchesCount: null, initialize: function() { var self = this; this.$search = $( '#widgets-search' ); + this.$clearResults = this.$el.find( '.clear-results' ); + _.bindAll( this, 'close' ); this.listenTo( this.collection, 'change', this.updateList ); this.updateList(); + // Set the initial search count to the number of available widgets. + this.searchMatchesCount = this.collection.length; + // 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, .customize-overlay-close' ).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(); } } ); + // Clear the search results and trigger a `keyup` event to fire a new search. + this.$clearResults.on( 'click', function() { + self.$search.val( '' ).focus().trigger( 'keyup' ); + } ); + // Close the panel if the URL in the preview changes api.previewer.bind( 'url', this.close ); }, @@ -193,6 +203,10 @@ var firstVisible; this.collection.doSearch( event.target.value ); + // Update the search matches count. + this.updateSearchMatchesCount(); + // Announce how many search results. + this.announceSearchMatches(); // Remove a widget from being selected if it is no longer visible if ( this.selected && ! this.selected.is( ':visible' ) ) { @@ -213,8 +227,38 @@ this.select( firstVisible ); } } + + // Toggle the clear search results button. + if ( '' !== event.target.value ) { + this.$clearResults.addClass( 'is-visible' ); + } else if ( '' === event.target.value ) { + this.$clearResults.removeClass( 'is-visible' ); + } + + // Set a CSS class on the search container when there are no search results. + if ( ! this.searchMatchesCount ) { + this.$el.addClass( 'no-widgets-found' ); + } else { + this.$el.removeClass( 'no-widgets-found' ); + } }, + // Update the count of the available widgets that have the `search_matched` attribute. + updateSearchMatchesCount: function() { + this.searchMatchesCount = this.collection.where({ search_matched: true }).length; + }, + + // Send a message to the aria-live region to announce how many search results. + announceSearchMatches: _.debounce( function() { + var message = l10n.widgetsFound.replace( '%d', this.searchMatchesCount ) ; + + if ( ! this.searchMatchesCount ) { + message = l10n.noWidgetsFound; + } + + wp.a11y.speak( message ); + }, 500 ), + // Changes visibility of available widgets updateList: function() { this.collection.each( function( widget ) { @@ -366,7 +410,7 @@ this.close( { returnFocus: true } ); } - if ( isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) { + if ( this.currentSidebarControl && isTab && ( isShift && isSearchFocused || ! isShift && isLastWidgetFocused ) ) { this.currentSidebarControl.container.find( '.add-new-widget' ).focus(); event.preventDefault(); } @@ -417,37 +461,105 @@ /** * @since 4.1.0 */ - initialize: function ( id, options ) { + initialize: function( id, options ) { var control = this; - api.Control.prototype.initialize.call( control, id, options ); - control.expanded = new api.Value(); + + control.widgetControlEmbedded = false; + control.widgetContentEmbedded = false; + control.expanded = new api.Value( false ); control.expandedArgumentsQueue = []; - control.expanded.bind( function ( expanded ) { + control.expanded.bind( function( expanded ) { var args = control.expandedArgumentsQueue.shift(); args = $.extend( {}, control.defaultExpandedArguments, args ); control.onChangeExpanded( expanded, args ); }); - control.expanded.set( false ); + control.altNotice = true; + + api.Control.prototype.initialize.call( control, id, options ); }, /** - * Set up the control + * 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', [ this.container.find( '.widget:first' ) ] ); + $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] ); + }, /** @@ -456,8 +568,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; @@ -597,7 +707,7 @@ */ _setupReorderUI: function() { var self = this, selectSidebarItem, $moveWidgetArea, - $reorderNav, updateAvailableSidebars; + $reorderNav, updateAvailableSidebars, template; /** * select the provided sidebar list item in the move widget area @@ -615,8 +725,10 @@ * Add the widget reordering elements to the widget control */ this.container.find( '.widget-title-action' ).after( $( api.Widgets.data.tpl.widgetReorderNav ) ); - $moveWidgetArea = $( - _.template( api.Widgets.data.tpl.moveWidgetArea, { + + + template = _.template( api.Widgets.data.tpl.moveWidgetArea ); + $moveWidgetArea = $( template( { sidebars: _( api.Widgets.registeredSidebars.toArray() ).pluck( 'attributes' ) } ) ); @@ -766,7 +878,7 @@ $saveBtn = this.container.find( '.widget-control-save' ); $saveBtn.val( l10n.saveBtnLabel ); $saveBtn.attr( 'title', l10n.saveBtnTooltip ); - $saveBtn.removeClass( 'button-primary' ).addClass( 'button-secondary' ); + $saveBtn.removeClass( 'button-primary' ); $saveBtn.on( 'click', function( e ) { e.preventDefault(); self.updateWidget( { disable_form: true } ); // @todo disable_form is unused? @@ -786,12 +898,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(); } } ); @@ -894,7 +1005,7 @@ * comparing the loaded form with the sanitized form, whose fields will * be aligned to copy the sanitized over. The elements returned by this * are passed into this._getInputsSignature(), and they are iterated - * over when copying sanitized values over to the the form loaded. + * over when copying sanitized values over to the form loaded. * * @param {jQuery} container element in which to look for inputs * @returns {jQuery} inputs @@ -1009,6 +1120,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, @@ -1039,8 +1153,9 @@ params = {}; params.action = 'update-widget'; params.wp_customize = 'on'; - params.nonce = api.Widgets.data.nonce; - params.theme = api.settings.theme.stylesheet; + params.nonce = api.settings.nonce['update-widget']; + params.customize_theme = api.settings.theme.stylesheet; + params.customized = wp.customize.previewer.query().customized; data = $.param( params ); $inputs = this._getInputs( $widgetContent ); @@ -1253,7 +1368,12 @@ * @param {Object} args merged on top of this.defaultActiveArguments */ onChangeExpanded: function ( expanded, args ) { - var self = this, $widget, $inside, complete, prevComplete; + var self = this, $widget, $inside, complete, prevComplete, expandControl; + + self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. + if ( expanded ) { + self.embedWidgetContent(); + } // If the expanded state is unchanged only manipulate container expanded states if ( args.unchanged ) { @@ -1268,9 +1388,7 @@ $widget = this.container.find( 'div.widget:first' ); $inside = $widget.find( '.widget-inside:first' ); - if ( expanded ) { - - self.expandControlSection(); + expandControl = function() { // Close all other widget controls before expanding this one api.control.each( function( otherControl ) { @@ -1300,6 +1418,16 @@ self.container.trigger( 'expand' ); self.container.addClass( 'expanding' ); + }; + + if ( expanded ) { + if ( api.section.has( self.section() ) ) { + api.section( self.section() ).expand( { + completeCallback: expandControl + } ); + } else { + expandControl(); + } } else { complete = function() { @@ -1429,6 +1557,77 @@ } } ); + /** + * 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 * @@ -1604,6 +1803,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; @@ -1641,11 +1841,7 @@ /** * 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 ); } ); }, @@ -1656,18 +1852,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(); } } ); @@ -1685,7 +1881,7 @@ } }); - if ( ! widgetControls.length ) { + if ( 0 === widgetControls.length || ( 1 === api.Widgets.registeredSidebars.length && widgetControls.length <= 1 ) ) { this.container.find( '.reorder-toggle' ).hide(); return; } else { @@ -1721,6 +1917,10 @@ * @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' ) ) { @@ -1735,17 +1935,23 @@ formControl.collapse(); } ); - this.$sectionContent.find( '.first-widget .move-widget' ).focus(); - this.$sectionContent.find( '.add-new-widget' ).prop( 'tabIndex', -1 ); + 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 { - this.$sectionContent.find( '.add-new-widget' ).prop( 'tabIndex', 0 ); + 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 + * @since 3.9.0 * @return {wp.customize.controlConstructor.widget_form[]} */ getWidgetFormControls: function() { @@ -1826,7 +2032,7 @@ isExistingWidget = api.has( settingId ); if ( ! isExistingWidget ) { settingArgs = { - transport: 'refresh', + transport: api.Widgets.data.selectiveRefreshableWidgets[ widget.get( 'id_base' ) ] ? 'postMessage' : 'refresh', previewer: this.setting.previewer }; setting = api.create( settingId, settingId, '', settingArgs ); @@ -1847,7 +2053,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 } ); @@ -1891,7 +2098,10 @@ } } ); - // Register models for custom section and control types + // Register models for custom panel, section, and control types + $.extend( api.panelConstructor, { + widgets: api.Widgets.WidgetsPanel + }); $.extend( api.sectionConstructor, { sidebar: api.Widgets.SidebarSection }); @@ -1900,11 +2110,6 @@ sidebar_widgets: api.Widgets.SidebarControl }); - // 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. */ @@ -1984,6 +2189,58 @@ return foundControl; }; + /** + * Initialize Edit Menu button in Nav Menu widget. + */ + $( document ).on( 'widget-added', function( event, widgetContainer ) { + var parsedWidgetId, widgetControl, navMenuSelect, editMenuButton; + parsedWidgetId = parseWidgetId( widgetContainer.find( '> .widget-inside > .form > .widget-id' ).val() ); + if ( 'nav_menu' !== parsedWidgetId.id_base ) { + return; + } + widgetControl = api.control( 'widget_nav_menu[' + String( parsedWidgetId.number ) + ']' ); + if ( ! widgetControl ) { + return; + } + navMenuSelect = widgetContainer.find( 'select[name*="nav_menu"]' ); + editMenuButton = widgetContainer.find( '.edit-selected-nav-menu > button' ); + if ( 0 === navMenuSelect.length || 0 === editMenuButton.length ) { + return; + } + navMenuSelect.on( 'change', function() { + if ( api.section.has( 'nav_menu[' + navMenuSelect.val() + ']' ) ) { + editMenuButton.parent().show(); + } else { + editMenuButton.parent().hide(); + } + }); + editMenuButton.on( 'click', function() { + var section = api.section( 'nav_menu[' + navMenuSelect.val() + ']' ); + if ( section ) { + focusConstructWithBreadcrumb( section, widgetControl ); + } + } ); + } ); + + /** + * Focus (expand) one construct and then focus on another construct after the first is collapsed. + * + * This overrides the back button to serve the purpose of breadcrumb navigation. + * + * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} focusConstruct - The object to initially focus. + * @param {wp.customize.Section|wp.customize.Panel|wp.customize.Control} returnConstruct - The object to return focus. + */ + function focusConstructWithBreadcrumb( focusConstruct, returnConstruct ) { + focusConstruct.focus(); + function onceCollapsed( isExpanded ) { + if ( ! isExpanded ) { + focusConstruct.expanded.unbind( onceCollapsed ); + returnConstruct.focus(); + } + } + focusConstruct.expanded.bind( onceCollapsed ); + } + /** * @param {String} widgetId * @returns {Object}