WordPress.org

Make WordPress Core

Ticket #27404: 27404-5-2-16.patch

File 27404-5-2-16.patch, 31.9 KB (added by rramo012, 21 months ago)
  • src/wp-admin/css/customize-controls.css

     
    16651665        #available-widgets-list { 
    16661666                top: 140px; 
    16671667        } 
     1668 
     1669        .wp-customizer #available-widgets .saved-widget-controls { 
     1670                visibility: visible; 
     1671        } 
    16681672} 
  • src/wp-admin/css/customize-widgets.css

     
    212212        display: block; 
    213213} 
    214214 
     215#customize-theme-controls .widget-control-remove:hover { 
     216        color: #0096dd; 
     217} 
     218#customize-theme-controls .widget-control-close { 
     219        color: #a00; 
     220} 
     221#customize-theme-controls .widget-control-close:hover { 
     222        color: #f00; 
     223} 
     224 
    215225/** 
    216226 * Styles for new widget addition panel 
    217227 */ 
     
    265275        opacity: 0.4; 
    266276} 
    267277 
     278#available-widgets .widget-tpl { 
     279        padding-right: 35px; 
     280} 
    268281 
     282#available-widgets .saved-widget { 
     283        margin-left: 48px; 
     284        border-left: 1px #e4e4e4 solid; 
     285        padding: 10px 20px 10px; 
     286        background-color: white; 
     287        display: none; 
     288        cursor: default; 
     289} 
     290 
     291#available-widgets .saved-widget:last-of-type { 
     292        margin-bottom: 40px; 
     293} 
     294 
     295#available-widgets .saved-widget h3 { 
     296        font-size: 1em; 
     297        margin: 0; 
     298} 
     299 
     300#available-widgets .saved-widget a { 
     301        text-decoration: none; 
     302} 
     303 
     304#available-widgets .saved-widget-controls { 
     305        visibility: hidden; 
     306} 
     307 
     308#available-widgets .saved-widget.selected .saved-widget-controls, 
     309#available-widgets .saved-widget:hover .saved-widget-controls { 
     310        visibility: visible; 
     311} 
     312 
     313#available-widgets .delete-widget-permanently { 
     314        color: #a00; 
     315} 
     316 
     317#available-widgets .delete-widget-permanently:hover { 
     318        color: #f00; 
     319} 
     320 
     321#available-widgets .widget-tpl.expanded a.widget-action:after { 
     322        content: "\f142"; 
     323} 
     324 
     325#available-widgets .has-inactive-widgets .widget-action { 
     326        display: block; 
     327        position: relative; 
     328        top: 50%; 
     329        width: 20px; 
     330        margin: 0 auto; 
     331        -webkit-transform: translateY(-50%); 
     332        -ms-transform: translateY(-50%); 
     333        transform: translateY(-50%); 
     334} 
     335 
     336#available-widgets .has-inactive-widgets .widget-title-action { 
     337        display: block; 
     338        position: absolute; 
     339        right: 0; 
     340        top: 0; 
     341        height:100%; 
     342        width: 30px; 
     343        background-color: #eee; 
     344} 
     345 
     346#available-widgets .saved-widget.widget-title:before { 
     347        display: none; 
     348} 
     349 
     350#available-widgets .widget-tpl.expanded .widget-title-action, 
     351#available-widgets .has-inactive-widgets .widget-title-action:hover { 
     352        border-left: 1px #e4e4e4 solid; 
     353        background-color: #FFFFFF; 
     354} 
     355 
     356#available-widgets .has-inactive-widgets .widget-title-action:hover .widget-action { 
     357        color: #555d66; 
     358} 
     359 
     360#available-widgets .has-inactive-widgets .widget-top a.widget-action:after { 
     361        margin: 0 auto; 
     362} 
     363 
    269364/** 
    270365 * Widget Icon styling 
    271366 * No plurals in naming. 
     
    445540        .widget-reorder-nav span:before { 
    446541                line-height: 39px; 
    447542        } 
     543        #available-widgets .widget-top { 
     544                position: static; 
     545        } 
    448546        #customize-theme-controls .widget-area-select li { 
    449547                padding: 9px 15px 11px 42px; 
    450548        } 
  • src/wp-admin/js/customize-widgets.js

     
    153153                        'search #widgets-search': 'search', 
    154154                        'focus .widget-tpl' : 'focus', 
    155155                        'click .widget-tpl' : '_submit', 
     156                        'click .add-saved-widget' : '_submit', 
     157                        'click .delete-widget-permanently' : '_delete', 
     158                        'click .widget-tpl .widget-title-action' : '_toggleInactive', 
    156159                        'keypress .widget-tpl' : '_submit', 
    157160                        'keydown' : 'keyboardAccessible' 
    158161                }, 
    159162 
     163                // Inactive widget template to be used in available widgets. 
     164                inactiveWidgetTpl: null, 
     165 
    160166                // Cache current selected widget 
    161167                selected: null, 
    162168 
     
    175181 
    176182                        this.updateList(); 
    177183 
     184                        this.inactiveWidgetTpl = _.template( api.Widgets.data.tpl.inactiveWidget ); 
     185 
     186                        this._createInactiveWidgets(); 
     187 
    178188                        // If the available widgets panel is open and the customize controls are 
    179189                        // interacted with (i.e. available widgets panel is blurred) then close the 
    180190                        // available widgets panel. Also close on back button click. 
     
    214224                                        this.select( firstVisible ); 
    215225                                } 
    216226                        } 
     227 
     228                        // Collapse inactive widgets upon searching. 
     229                        this.$el.find( '.saved-widget' ).hide(); 
     230                        this.$el.find( '.widget-tpl.expanded' ).removeClass( 'expanded' ); 
     231 
    217232                }, 
    218233 
    219234                // Changes visibility of available widgets 
     
    227242                        } ); 
    228243                }, 
    229244 
     245                /** 
     246                 * Create the list of inactive widgets to be added to the available widgets sidebar onload. 
     247                 * 
     248                 * @since 4.6.0 
     249                 */ 
     250                _createInactiveWidgets : function () { 
     251                        var self = this; 
     252 
     253                        _( api.Widgets.data.availableWidgets ).each( function ( availableWidget )  { 
     254                                _( availableWidget.saved_widgets ).each( function ( savedWidget )  { 
     255                                        self.addInactiveWidget( { 
     256                                                id: savedWidget.id, 
     257                                                type: savedWidget.type, 
     258                                                idBase: availableWidget.id_base, 
     259                                        } ); 
     260                                }); 
     261                        }); 
     262                }, 
     263 
     264                /** 
     265                 * Adds a single inactive widget to the available widgets sidebar. 
     266                 * 
     267                 * @since 4.6.0 
     268                 */ 
     269                addInactiveWidget : function ( templateArgs ) { 
     270                        var $inactiveWidget = $( this.inactiveWidgetTpl( templateArgs ) ); 
     271 
     272                        if ( 0 === this.$el.find( '.saved-widget[data-widget-id="' + templateArgs.id + '"]' ).length ) { 
     273                                this.$el.find( '[data-id-base="' + templateArgs.idBase + '"]' ).first().after( $inactiveWidget ); 
     274                        } 
     275 
     276                        return $inactiveWidget; 
     277                }, 
     278 
     279                /** 
     280                 * Show inactive widgets and hide active widgets. 
     281                 * 
     282                 * @since 4.6.0 
     283                 */ 
     284                updateVisibleSavedWidgets : function () { 
     285                        var self = this; 
     286 
     287                        // Remove state classes. 
     288                        this.$el.find( '.widget-tpl.has-inactive-widgets, .widget-tpl.expanded' ) 
     289                                .removeClass( 'has-inactive-widgets' ) 
     290                                .removeClass( 'expanded' ); 
     291 
     292                        // Hide all inactive widgets 
     293                        this.$el.find( '.saved-widget' ) 
     294                                .hide() 
     295                                .removeClass( 'inactive-widget' ); 
     296 
     297                        _( api( 'sidebars_widgets[wp_inactive_widgets]' )() ).each( function( widgetId ) { 
     298                                var $savedWidget  = self.$el.find( '#saved-widget-' + widgetId ), 
     299                                        parsedWidgetId = parseWidgetId( widgetId ), 
     300                                        widgetControlId = 'widget_' + parsedWidgetId.id_base + '[' + parsedWidgetId.number + ']', 
     301                                        widgetControl =  api.control( widgetControlId ), 
     302                                        widgetSetting =  api( widgetControlId ), 
     303                                        title = ( widgetSetting ) ? widgetSetting().title : null, 
     304                                        $inWidgetTitle, 
     305                                        availableWidget; 
     306 
     307                                if ( ! widgetControl ) { 
     308                                        return; 
     309                                } 
     310 
     311                                // If inactive widget HTML does not exist, create it. 
     312                                if ( 0 === $savedWidget.length ) { 
     313                                        availableWidget = api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } ); 
     314                                        $savedWidget = self.addInactiveWidget({ 
     315                                                id: widgetId, 
     316                                                type: availableWidget.attributes.name, 
     317                                                idBase: parsedWidgetId.id_base, 
     318                                        }); 
     319                                } 
     320 
     321                                $savedWidget.addClass( 'inactive-widget' ); 
     322 
     323                                self.$el.find( '.widget-tpl[data-id-base="' + parsedWidgetId.id_base + '"]' ) 
     324                                        .not( '.saved-widget' ) 
     325                                        .addClass( 'has-inactive-widgets' ); 
     326 
     327                                $inWidgetTitle = $savedWidget.find( '.in-widget-title' ); 
     328                                if ( title ) { 
     329                                        $inWidgetTitle.text( ': ' + title ); 
     330                                } else { 
     331                                        $inWidgetTitle.text( '' ); 
     332                                } 
     333                        } ); 
     334                }, 
     335 
     336                /** 
     337                 * Show or hide the list of inactive widgets per widget id base. 
     338                 * 
     339                 * @since 4.6.0 
     340                 */ 
     341                toggleInactive : function ( idBase ) { 
     342                        var $widgetTpl = this.$el.find( '.has-inactive-widgets[data-id-base="' + idBase + '"]' ), 
     343                                $inactiveWidgets = this.$el.find( '.saved-widget[data-id-base="' + idBase + '"].inactive-widget' ); 
     344 
     345                        $widgetTpl.toggleClass( 'expanded' ); 
     346                        $inactiveWidgets.stop().slideToggle( 'fast' ); 
     347 
     348                        if ( $widgetTpl.hasClass('expanded') ) { 
     349                                this.select( $inactiveWidgets.first().focus() ); 
     350                        } else { 
     351                                this.select( $widgetTpl.focus() ); 
     352                        } 
     353                }, 
     354 
     355                /** 
     356                 * Slides down a list of active widgets per widget type. 
     357                 * 
     358                 * @since 4.6.0 
     359                 */ 
     360                _toggleInactive : function ( event ) { 
     361                        event.stopPropagation(); 
     362                        event.preventDefault(); 
     363 
     364                        var $widgetTpl = $( event.currentTarget ).closest( '.widget-tpl' ); 
     365                        this.toggleInactive( $widgetTpl.data('id-base') ); 
     366                }, 
     367 
     368                /** 
     369                 * Permanently delete a widget. 
     370                 * 
     371                 * @since 4.6.0 
     372                 */ 
     373                _delete : function ( event ) { 
     374                        var self = this, 
     375                                $currentTarget = $( event.currentTarget ), 
     376                                $savedWidgets = $currentTarget.closest( '.saved-widget' ), 
     377                                widgetId = $savedWidgets.data( 'widget-id' ), 
     378                                idBase = $savedWidgets.data( 'id-base' ), 
     379                                settingId = widgetIdToSettingId( widgetId ), 
     380                                control = api.Widgets.getWidgetFormControlForWidget( widgetId ), 
     381                                deleteCallback, 
     382                                inactiveSidebarWidgets, 
     383                                inactiveWidgets, 
     384                                i; 
     385 
     386                        // Remove from sidebar. 
     387                        inactiveWidgets = api( 'sidebars_widgets[wp_inactive_widgets]' ); 
     388 
     389                        inactiveSidebarWidgets = inactiveWidgets().slice(); 
     390                        i = _.indexOf( inactiveSidebarWidgets, widgetId ); 
     391                        if ( -1 !== i ) { 
     392                                inactiveSidebarWidgets.splice( i, 1 ); 
     393                                inactiveWidgets.set( inactiveSidebarWidgets ); 
     394                        } 
     395 
     396                        // Embed control to allow unembedded controls to trigger update calls. 
     397                        if ( ! control.widgetControlEmdedded ) { 
     398                                control.embedWidgetControl(); 
     399                        } 
     400                        api( settingId ).set( false ); 
     401 
     402                        deleteCallback = function () { 
     403                                var $remainingWidgets; 
     404 
     405                                $( this ).remove(); 
     406                                api.control.remove( control.id ); 
     407                                control.container.remove(); 
     408 
     409                                // Remove drop down if widgets no longer exist 
     410                                $remainingWidgets = self.$el.find( '.saved-widget[data-id-base="' + idBase + '"].inactive-widget' ); 
     411                                if ( ! $remainingWidgets.length ) { 
     412                                        self.$el.find( '.has-inactive-widgets[data-id-base="' + idBase + '"]' ) 
     413                                                .removeClass( 'has-inactive-widgets expanded' ); 
     414                                } 
     415 
     416                                wp.a11y.speak( l10n.itemDeleted ); 
     417                        }; 
     418 
     419                        $currentTarget.closest( '.saved-widget' ).slideUp( 'fast' , deleteCallback ); 
     420                }, 
     421 
    230422                // Highlights a widget 
    231423                select: function( widgetTpl ) { 
    232424                        this.selected = $( widgetTpl ); 
     
    251443 
    252444                // Adds a selected widget to the sidebar 
    253445                submit: function( widgetTpl ) { 
    254                         var widgetId, widget, widgetFormControl; 
     446                        var widgetId, widget, widgetFormControl, isSavedWidget, addWidgetId; 
    255447 
    256448                        if ( ! widgetTpl ) { 
    257449                                widgetTpl = this.selected; 
     
    263455 
    264456                        this.select( widgetTpl ); 
    265457 
    266                         widgetId = $( this.selected ).data( 'widget-id' ); 
     458                        widgetId = widgetTpl.data( 'widget-id' ); 
     459                        isSavedWidget = widgetTpl.data( 'is-saved-widget' ); 
     460 
    267461                        widget = this.collection.findWhere( { id: widgetId } ); 
    268                         if ( ! widget ) { 
     462                        if ( ( ! widget && ! isSavedWidget ) || widgetTpl.hasClass( 'saved-widget' ) ) { 
    269463                                return; 
    270464                        } 
    271465 
    272                         widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) ); 
     466                        addWidgetId = ( isSavedWidget ) ? widgetId : widget.get( 'id_base' ); 
     467                        widgetFormControl = this.currentSidebarControl.addWidget( addWidgetId ); 
     468 
    273469                        if ( widgetFormControl ) { 
    274470                                widgetFormControl.focus(); 
    275471                        } 
     
    281477                open: function( sidebarControl ) { 
    282478                        this.currentSidebarControl = sidebarControl; 
    283479 
     480                        // Create inactive widget controls that were moved to the inactive sidebar. 
     481                        this.updateVisibleSavedWidgets(); 
     482 
    284483                        // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens 
    285484                        _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) { 
    286485                                if ( control.params.is_wide ) { 
     
    316515                        this.$search.val( '' ); 
    317516                }, 
    318517 
    319                 // Add keyboard accessiblity to the panel 
     518                // Add keyboard accessibility to the panel 
    320519                keyboardAccessible: function( event ) { 
    321520                        var isEnter = ( event.which === 13 ), 
    322521                                isEsc = ( event.which === 27 ), 
    323522                                isDown = ( event.which === 40 ), 
    324523                                isUp = ( event.which === 38 ), 
     524                                isLeft = ( event.which === 37 ), 
     525                                isRight = ( event.which === 39 ), 
    325526                                isTab = ( event.which === 9 ), 
    326527                                isShift = ( event.shiftKey ), 
    327528                                selected = null, 
    328                                 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ), 
    329                                 lastVisible = this.$el.find( '> .widget-tpl:visible:last' ), 
     529                                firstVisible = this.$el.find( '.widget-tpl:visible:first' ), 
     530                                lastVisible = this.$el.find( '.widget-tpl:visible:last' ), 
    330531                                isSearchFocused = $( event.target ).is( this.$search ), 
    331532                                isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' ); 
    332533 
     
    356557                                return; 
    357558                        } 
    358559 
     560                        // Toggle display when the user presses left or right on widget template with inactive widgets. 
     561                        if ( isLeft || isRight ) { 
     562                                if ( this.selected && (this.selected.hasClass( 'has-inactive-widgets' ) || this.selected.hasClass( 'saved-widget' ) ) ) { 
     563                                        var idBase = this.selected.data('id-base'); 
     564                                        this.toggleInactive( idBase ); 
     565                                } 
     566 
     567                                return; 
     568                        } 
     569 
    359570                        // If enter pressed but nothing entered, don't do anything 
    360571                        if ( isEnter && ! this.$search.val() ) { 
    361572                                return; 
     
    490701                        control._setupReorderUI(); 
    491702                        control._setupHighlightEffects(); 
    492703                        control._setupUpdateUI(); 
     704                        control._setupMoveUI(); 
    493705                        control._setupRemoveUI(); 
    494706                }, 
    495707 
     
    9031115                }, 
    9041116 
    9051117                /** 
    906                  * Set up event handlers for widget removal 
     1118                 * Set up event handlers for widget move. 
     1119                 * 
     1120                 * @since 4.6.0 
    9071121                 */ 
     1122                _setupMoveUI: function() { 
     1123                        var self = this, $moveBtn, $reorderToggle, replaceDeleteWithMove; 
     1124 
     1125                        // Configure move button 
     1126                        $moveBtn = this.container.find( 'a.widget-control-remove' ); 
     1127 
     1128                        $moveBtn.on( 'click', function ( e ) { 
     1129                                e.preventDefault(); 
     1130 
     1131                                $reorderToggle = $( this ).closest( '.ui-sortable' ).find( '.reorder-toggle' ); 
     1132 
     1133                                $reorderToggle.trigger( 'click' ); 
     1134                                self.openWidgetMoveArea(); 
     1135                        } ); 
     1136 
     1137                        // Replace the Delete link with Move. 
     1138                        replaceDeleteWithMove = function () { 
     1139                                $moveBtn.text( l10n.moveBtnLabel ); // wp_widget_control() outputs the link as "Close" 
     1140                                $moveBtn.attr( 'title', l10n.moveBtnTooltip ); 
     1141                        }; 
     1142 
     1143                        replaceDeleteWithMove(); 
     1144                }, 
     1145 
     1146                /** 
     1147                 * Set up event handlers for widget removal. 
     1148                 */ 
    9081149                _setupRemoveUI: function() { 
    909                         var self = this, $removeBtn, replaceDeleteWithRemove; 
     1150                        var self = this, $closeBtn; 
    9101151 
    9111152                        // Configure remove button 
    912                         $removeBtn = this.container.find( 'a.widget-control-remove' ); 
    913                         $removeBtn.on( 'click', function( e ) { 
     1153                        $closeBtn = this.container.find( 'a.widget-control-close' ); 
     1154                        $closeBtn.on( 'click', function( e ) { 
    9141155                                e.preventDefault(); 
    9151156 
    9161157                                // Find an adjacent element to add focus to when this widget goes away 
     
    9251166 
    9261167                                self.container.slideUp( function() { 
    9271168                                        var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ), 
    928                                                 sidebarWidgetIds, i; 
     1169                                                sidebarWidgetIds, i, inactiveWidgets, inactiveWidgetSetting; 
    9291170 
    9301171                                        if ( ! sidebarsWidgetsControl ) { 
    9311172                                                return; 
     
    9401181                                        sidebarWidgetIds.splice( i, 1 ); 
    9411182                                        sidebarsWidgetsControl.setting( sidebarWidgetIds ); 
    9421183 
     1184                                        // Add widget to inactive widgets. 
     1185                                        inactiveWidgetSetting = api( 'sidebars_widgets[wp_inactive_widgets]' ); 
     1186                                        inactiveWidgets = inactiveWidgetSetting(); 
     1187                                        inactiveWidgets.push( self.params.widget_id ); 
     1188                                        inactiveWidgetSetting.set( [] ).set( _( inactiveWidgets ).unique() ); 
     1189 
    9431190                                        $adjacentFocusTarget.focus(); // keyboard accessibility 
     1191 
     1192                                        // Remove display none in case its added again. 
     1193                                        self.container.show(); 
    9441194                                } ); 
    9451195                        } ); 
    9461196 
    947                         replaceDeleteWithRemove = function() { 
    948                                 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete" 
    949                                 $removeBtn.attr( 'title', l10n.removeBtnTooltip ); 
    950                         }; 
    951  
    952                         if ( this.params.is_new ) { 
    953                                 api.bind( 'saved', replaceDeleteWithRemove ); 
    954                         } else { 
    955                                 replaceDeleteWithRemove(); 
    956                         } 
     1197                        $closeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Close" 
     1198                        $closeBtn.attr( 'title', l10n.removeBtnTooltip ); 
    9571199                }, 
    9581200 
    9591201                /** 
     
    11121354                        params.nonce = api.settings.nonce['update-widget']; 
    11131355                        params.theme = api.settings.theme.stylesheet; 
    11141356                        params.customized = wp.customize.previewer.query().customized; 
     1357                        params.delete_widget = ( false === self.setting() ) ? 1 : 0; 
    11151358 
    11161359                        data = $.param( params ); 
    11171360                        $inputs = this._getInputs( $widgetContent ); 
     
    11251368 
    11261369                        if ( instanceOverride ) { 
    11271370                                data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } ); 
    1128                         } else { 
     1371                        } else if ( $inputs.length ) { 
    11291372                                data += '&' + $inputs.serialize(); 
    11301373                        } 
    11311374                        data += '&' + $widgetContent.find( '~ :input' ).serialize(); 
     
    12081451                                         * preview finishing loading. 
    12091452                                         */ 
    12101453                                        isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance ); 
    1211                                         if ( isChanged ) { 
     1454                                        if ( isChanged || false === self.setting() ) { 
    12121455                                                self.isWidgetUpdating = true; // suppress triggering another updateWidget 
    12131456                                                self.setting( r.data.instance ); 
    12141457                                                self.isWidgetUpdating = false; 
     
    14841727                        } 
    14851728 
    14861729                        $moveWidgetArea.toggleClass( 'active', showOrHide ); 
     1730                        self.toggleSaveForLater(); 
    14871731                }, 
    14881732 
    14891733                /** 
     1734                 * Open widget move area if control is not active already. 
     1735                 * 
     1736                 * @since 4.6.0 
     1737                 */ 
     1738                openWidgetMoveArea: function() { 
     1739                        var self = this, $moveWidgetArea; 
     1740 
     1741                        $moveWidgetArea = this.container.find( '.move-widget-area' ); 
     1742 
     1743                        if( ! $moveWidgetArea.hasClass( 'active' ) ) { 
     1744                                self.toggleWidgetMoveArea(); 
     1745                        } 
     1746                }, 
     1747 
     1748                /** 
     1749                 * Toggle visibility of the Save For Later/Inactive Widget Sidebar section. 
     1750                 * 
     1751                 * @since 4.6.0 
     1752                 */ 
     1753                toggleSaveForLater: function() { 
     1754                        var $moveWidgetArea, $saveForLater; 
     1755 
     1756                        $moveWidgetArea = this.container.find( '.move-widget-area' ); 
     1757                        $saveForLater = this.container.find( 'li[data-id="wp_inactive_widgets"]' ); 
     1758 
     1759                        if( $moveWidgetArea.hasClass( 'active' ) ) { 
     1760                                $saveForLater.show(); 
     1761                        } else { 
     1762                                $saveForLater.hide(); 
     1763                        } 
     1764                }, 
     1765 
     1766                /** 
    14901767                 * Highlight the widget control and section 
    14911768                 */ 
    14921769                highlightSectionAndControl: function() { 
     
    17071984                                                        return; 
    17081985                                                } 
    17091986 
    1710                                                 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId ); 
    1711  
    1712                                                 // Detect if widget control was dragged to another sidebar 
    1713                                                 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] ); 
    1714  
    1715                                                 // Delete any widget form controls for removed widgets 
    1716                                                 if ( removedControl && ! wasDraggedToAnotherSidebar ) { 
    1717                                                         api.control.remove( removedControl.id ); 
    1718                                                         removedControl.container.remove(); 
    1719                                                 } 
    1720  
    17211987                                                // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved 
    17221988                                                // This prevents the inactive widgets sidebar from overflowing with throwaway widgets 
    1723                                                 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) { 
     1989                                                if ( api.Widgets.savedWidgetIds[removedWidgetId] && 'sidebars_widgets[wp_inactive_widgets]' !== self.id ) { 
    17241990                                                        inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice(); 
    17251991                                                        inactiveWidgets.push( removedWidgetId ); 
    17261992                                                        api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() ); 
     
    19892255                                setting.set( {} ); // mark dirty, changing from '' to {} 
    19902256                        } 
    19912257 
    1992                         controlConstructor = api.controlConstructor[controlType]; 
    1993                         widgetFormControl = new controlConstructor( settingId, { 
    1994                                 params: { 
    1995                                         settings: { 
    1996                                                 'default': settingId 
     2258                        widgetFormControl = api.control( settingId ); 
     2259                        if ( ! widgetFormControl ) { 
     2260 
     2261                                controlConstructor = api.controlConstructor[controlType]; 
     2262                                widgetFormControl = new controlConstructor( settingId, { 
     2263                                        params: { 
     2264                                                settings: { 
     2265                                                        'default': settingId 
     2266                                                }, 
     2267                                                content: controlContainer, 
     2268                                                sidebar_id: self.params.sidebar_id, 
     2269                                                widget_id: widgetId, 
     2270                                                widget_id_base: widget.get( 'id_base' ), 
     2271                                                type: controlType, 
     2272                                                is_new: ! isExistingWidget, 
     2273                                                width: widget.get( 'width' ), 
     2274                                                height: widget.get( 'height' ), 
     2275                                                is_wide: widget.get( 'is_wide' ), 
     2276                                                active: true 
    19972277                                        }, 
    1998                                         content: controlContainer, 
    1999                                         sidebar_id: self.params.sidebar_id, 
    2000                                         widget_id: widgetId, 
    2001                                         widget_id_base: widget.get( 'id_base' ), 
    2002                                         type: controlType, 
    2003                                         is_new: ! isExistingWidget, 
    2004                                         width: widget.get( 'width' ), 
    2005                                         height: widget.get( 'height' ), 
    2006                                         is_wide: widget.get( 'is_wide' ), 
    2007                                         active: true 
    2008                                 }, 
    2009                                 previewer: self.setting.previewer 
    2010                         } ); 
    2011                         api.control.add( settingId, widgetFormControl ); 
     2278                                        previewer: self.setting.previewer 
     2279                                } ); 
     2280                                api.control.add( settingId, widgetFormControl ); 
     2281                        } 
    20122282 
    20132283                        // Make sure widget is removed from the other sidebars 
    20142284                        api.each( function( otherSetting ) { 
     
    20242294                                        i = _.indexOf( otherSidebarWidgets, widgetId ); 
    20252295 
    20262296                                if ( -1 !== i ) { 
    2027                                         otherSidebarWidgets.splice( i ); 
     2297                                        otherSidebarWidgets.splice( i, 1 ); 
    20282298                                        otherSetting( otherSidebarWidgets ); 
    20292299                                } 
    20302300                        } ); 
  • src/wp-includes/class-wp-customize-setting.php

     
    782782 
    783783                $result = $this->multidimensional( $root, $keys, true ); 
    784784 
    785                 if ( isset( $result ) ) 
    786                         $result['node'][ $result['key'] ] = $value; 
     785                if ( isset( $result ) ) { 
    787786 
     787                        if ( ! empty( $value['multidimensional_delete'] ) ) { 
     788                                unset( $result['node'][ $result['key'] ] ); 
     789                        } else { 
     790                                $result['node'][ $result['key'] ] = $value; 
     791                        } 
     792 
     793                } 
     794 
    788795                return $root; 
    789796        } 
    790797 
  • src/wp-includes/class-wp-customize-widgets.php

     
    444444 
    445445                                // Add section to contain controls. 
    446446                                $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id ); 
    447                                 if ( $is_active_sidebar ) { 
    448447 
    449                                         $section_args = array( 
    450                                                 'title' => $wp_registered_sidebars[ $sidebar_id ]['name'], 
    451                                                 'description' => $wp_registered_sidebars[ $sidebar_id ]['description'], 
    452                                                 'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ), 
    453                                                 'panel' => 'widgets', 
    454                                                 'sidebar_id' => $sidebar_id, 
    455                                         ); 
     448                                if ( $is_inactive_widgets ) { 
     449                                        $title = 'Inactive Widgets'; 
     450                                        $description = $title; 
     451                                } else { 
     452                                        $title = $wp_registered_sidebars[ $sidebar_id ]['name']; 
     453                                        $description = $wp_registered_sidebars[ $sidebar_id ]['description']; 
     454                                } 
    456455 
    457                                         /** 
    458                                          * Filter Customizer widget section arguments for a given sidebar. 
    459                                          * 
    460                                          * @since 3.9.0 
    461                                          * 
    462                                          * @param array      $section_args Array of Customizer widget section arguments. 
    463                                          * @param string     $section_id   Customizer section ID. 
    464                                          * @param int|string $sidebar_id   Sidebar ID. 
    465                                          */ 
    466                                         $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id ); 
     456                                $section_args = array( 
     457                                        'title' => $title, 
     458                                        'description' => $description, 
     459                                        'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ), 
     460                                        'panel' => 'widgets', 
     461                                        'sidebar_id' => $sidebar_id, 
     462                                ); 
    467463 
    468                                         $section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args ); 
    469                                         $this->manager->add_section( $section ); 
     464                                /** 
     465                                 * Filter Customizer widget section arguments for a given sidebar. 
     466                                 * 
     467                                 * @since 3.9.0 
     468                                 * 
     469                                 * @param array      $section_args Array of Customizer widget section arguments. 
     470                                 * @param string     $section_id   Customizer section ID. 
     471                                 * @param int|string $sidebar_id   Sidebar ID. 
     472                                 */ 
     473                                $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id ); 
    470474 
    471                                         $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array( 
    472                                                 'section'    => $section_id, 
    473                                                 'sidebar_id' => $sidebar_id, 
    474                                                 'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end. 
    475                                         ) ); 
    476                                         $new_setting_ids[] = $setting_id; 
     475                                $section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args ); 
     476                                $this->manager->add_section( $section ); 
    477477 
    478                                         $this->manager->add_control( $control ); 
    479                                 } 
     478                                $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array( 
     479                                        'section'    => $section_id, 
     480                                        'sidebar_id' => $sidebar_id, 
     481                                        'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end. 
     482                                ) ); 
     483                                $new_setting_ids[] = $setting_id; 
     484 
     485                                $this->manager->add_control( $control ); 
    480486                        } 
    481487 
    482488                        // Add a control for each active widget (located in a sidebar). 
     
    483489                        foreach ( $sidebar_widget_ids as $i => $widget_id ) { 
    484490 
    485491                                // Skip widgets that may have gone away due to a plugin being deactivated. 
    486                                 if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) { 
     492                                if ( ! isset( $wp_registered_widgets[$widget_id] ) ) { 
    487493                                        continue; 
    488494                                } 
    489495 
     
    717723                        </div>' 
    718724                ); 
    719725 
     726                $inactive_widget_tpl = str_replace( 
     727                        array( '{delete}', '{add}' ), 
     728                        array( 
     729                                __( 'Delete' ), 
     730                                __( 'Add' ) 
     731                        ), 
     732                        '<div id="saved-widget-<%- id %>" data-id-base="<%- idBase %>" data-widget-id="<%- id %>" class="widget-title saved-widget widget-tpl" tabindex="0"> 
     733                                <h3><%- type %><span class="in-widget-title"></span></h3> 
     734                                <div class="saved-widget-controls"> 
     735                                        <a href="#" class="add-saved-widget" data-is-saved-widget="1" data-widget-id="<%- id %>" 
     736                                        >{add}</a> &#124; <a href="#" class="delete-widget-permanently">{delete}</a> 
     737                                </div> 
     738                        </div>' 
     739                ); 
     740 
     741                $inactive_sidebar = array( 
     742                        'inactive-sidebar' => array( 
     743                                'name' => __( 'Save For Later' ), 
     744                                'id' => 'wp_inactive_widgets', 
     745                                'description' => __( 'Save widgets to the Inactive Sidebar to use later.' ), 
     746                                'class' => 'inactive-sidebar', 
     747                                'before_widget' => '<aside id="%1$s" class="widget %2$s">', 
     748                                'after_widget' => '</aside>', 
     749                                'before_title' => '<h1 class="widget-title">', 
     750                                'after_title' => '</h1>', 
     751                        ), 
     752                ); 
     753 
     754                $all_sidebars = array_values( array_merge( $wp_registered_sidebars, $inactive_sidebar ) ); 
     755 
    720756                $settings = array( 
    721                         'registeredSidebars'   => array_values( $wp_registered_sidebars ), 
     757                        'registeredSidebars'   => $all_sidebars, 
    722758                        'registeredWidgets'    => $wp_registered_widgets, 
    723759                        'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets 
    724760                        'l10n' => array( 
     
    726762                                'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ), 
    727763                                'removeBtnLabel'   => __( 'Remove' ), 
    728764                                'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ), 
     765                                'moveBtnLabel'     => __( 'Move' ), 
     766                                'moveBtnTooltip'   => __( 'Reorder the widgets in this sidebar.' ), 
    729767                                'error'            => __( 'An error has occurred. Please reload the page and try again.' ), 
    730768                                'widgetMovedUp'    => __( 'Widget moved up' ), 
    731769                                'widgetMovedDown'  => __( 'Widget moved down' ), 
     
    734772                                'reorderModeOff'   => __( 'Reorder mode closed' ), 
    735773                                'reorderLabelOn'   => esc_attr__( 'Reorder widgets' ), 
    736774                                'reorderLabelOff'  => esc_attr__( 'Close reorder mode' ), 
     775                                'itemDeleted'       => __( 'Widget deleted' ), 
    737776                        ), 
    738777                        'tpl' => array( 
    739778                                'widgetReorderNav' => $widget_reorder_nav_tpl, 
    740779                                'moveWidgetArea'   => $move_widget_area_tpl, 
     780                                'inactiveWidget'   => $inactive_widget_tpl, 
    741781                        ), 
    742782                        'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(), 
    743783                ); 
     
    781821                        </div> 
    782822                        <div id="available-widgets-list"> 
    783823                        <?php foreach ( $this->get_available_widgets() as $available_widget ): ?> 
    784                                 <div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0"> 
     824                                <div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-id-base="<?php echo esc_attr( $available_widget['id_base'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0"> 
    785825                                        <?php echo $available_widget['control_tpl']; ?> 
    786826                                </div> 
    787827                        <?php endforeach; ?> 
     
    872912        } 
    873913 
    874914        /** 
     915         * Create an list of saved widgets that correspond to a widget type. 
     916         * 
     917         * @since 4.6.0 
     918         * @access protected 
     919         * 
     920         * @global array $wp_registered_widget_controls 
     921         * 
     922         * @return array List of saved widget by type. 
     923         */ 
     924        protected function get_sorted_saved_widgets() { 
     925                global $wp_registered_widget_controls; 
     926 
     927                $sidebars_widgets = wp_get_sidebars_widgets(); 
     928 
     929                $sorted_saved_widgets = array(); 
     930                foreach ( $sidebars_widgets as $sidebars_widget ) { 
     931                        foreach( $sidebars_widget as $widget ) { 
     932 
     933                                if ( isset ( $wp_registered_widget_controls[ $widget ]['id_base'] ) ) { 
     934                                        $current_base_id = $wp_registered_widget_controls[ $widget ]['id_base']; 
     935                                        $sorted_saved_widgets[ $current_base_id ][] = array( 
     936                                                'id' => $widget, 
     937                                                'type' => $wp_registered_widget_controls[ $widget ]['name'], 
     938                                        ); 
     939                                } 
     940 
     941                        } 
     942                } 
     943 
     944                return $sorted_saved_widgets; 
     945        } 
     946 
     947        /** 
    875948         * Builds up an index of all available widgets for use in Backbone models. 
    876949         * 
    877950         * @since 3.9.0 
     
    894967                global $wp_registered_widgets, $wp_registered_widget_controls; 
    895968                require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number() 
    896969 
     970                $sorted_saved_widgets = $this->get_sorted_saved_widgets(); 
    897971                $sort = $wp_registered_widgets; 
    898972                usort( $sort, array( $this, '_sort_name_callback' ) ); 
    899973                $done = array(); 
     
    9501024                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'], 
    9511025                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'], 
    9521026                                'is_wide'      => $this->is_wide_widget( $widget['id'] ), 
     1027                                'saved_widgets' => isset( $sorted_saved_widgets[ $id_base ] ) ? $sorted_saved_widgets[ $id_base ] : array(), 
    9531028                        ) ); 
    9541029 
    9551030                        $available_widgets[] = $available_widget; 
     
    12571332         * @return array|void Sanitized widget instance. 
    12581333         */ 
    12591334        public function sanitize_widget_instance( $value ) { 
    1260                 if ( $value === array() ) { 
     1335                if ( $value === array() || false === $value ) { 
    12611336                        return $value; 
    12621337                } 
    12631338 
     
    13961471                        } 
    13971472                } 
    13981473 
     1474                // If deleting widget. 
     1475                $is_widget_delete = ! empty( $this->get_post_value( 'delete_widget' ) ); 
     1476 
     1477                if ( $is_widget_delete ) { 
     1478                        // Set post values needed in widget update_callback. 
     1479                        $_POST[ 'sidebar' ] = 'wp_inactive_widgets'; 
     1480                        $_POST[ 'widget-' . $parsed_id['id_base'] ] = array(); 
     1481                        $_POST[ 'the-widget-id' ] = $widget_id; 
     1482                        $_POST[ 'delete_widget' ] = '1'; 
     1483 
     1484                        $post_values_added = array( 'sidebar', 'widget-' . $parsed_id[ 'id_base' ], 'the-widget-id', 'delete_widget' ); 
     1485                        $added_input_vars = array_merge( $added_input_vars, $post_values_added ); 
     1486 
     1487                        /** This action is documented in wp-admin/widgets.php */ 
     1488                        do_action( 'delete_widget', $widget_id, $_POST[ 'sidebar' ], $parsed_id['id_base'] ); 
     1489                } 
     1490 
    13991491                // Invoke the widget update callback. 
    14001492                foreach ( (array) $wp_registered_widget_updates as $name => $control ) { 
    14011493                        if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) { 
     
    14281520 
    14291521                // Obtain the widget instance. 
    14301522                $option = $this->get_captured_option( $option_name ); 
    1431                 if ( null !== $parsed_id['number'] ) { 
     1523                if ( $is_widget_delete ) { 
     1524                        $instance = array( 'multidimensional_delete' => true ); 
     1525                } else if ( null !== $parsed_id['number'] ) { 
    14321526                        $instance = $option[ $parsed_id['number'] ]; 
    14331527                } else { 
    14341528                        $instance = $option; 
     
    14451539                // Obtain the widget control with the updated instance in place. 
    14461540                ob_start(); 
    14471541                $form = $wp_registered_widget_controls[ $widget_id ]; 
    1448                 if ( $form ) { 
     1542                if ( $form && false === $is_widget_delete ) { 
    14491543                        call_user_func_array( $form['callback'], $form['params'] ); 
    14501544                } 
    14511545                $form = ob_get_clean();