Make WordPress Core

Ticket #27404: 27404-6-8-16.patch

File 27404-6-8-16.patch, 34.2 KB (added by rramo012, 8 years ago)
  • 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                                          * Filters 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                                 * Filters 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 );
     486
     487                                $section_args = array(
     488                                        'title' => $title,
     489                                        'description' => $description,
     490                                        'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
     491                                        'panel' => 'widgets',
     492                                        'sidebar_id' => $sidebar_id,
     493                                );
     494
     495                                /**
     496                                 * Filter Customizer widget section arguments for a given sidebar.
     497                                 *
     498                                 * @since 3.9.0
     499                                 *
     500                                 * @param array      $section_args Array of Customizer widget section arguments.
     501                                 * @param string     $section_id   Customizer section ID.
     502                                 * @param int|string $sidebar_id   Sidebar ID.
     503                                 */
     504                                $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
     505
     506                                $section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
     507                                $this->manager->add_section( $section );
     508
     509                                $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
     510                                        'section'    => $section_id,
     511                                        'sidebar_id' => $sidebar_id,
     512                                        'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
     513                                ) );
     514                                $new_setting_ids[] = $setting_id;
     515
     516                                $this->manager->add_control( $control );
    480517                        }
    481518
    482519                        // Add a control for each active widget (located in a sidebar).
    483520                        foreach ( $sidebar_widget_ids as $i => $widget_id ) {
    484521
    485522                                // Skip widgets that may have gone away due to a plugin being deactivated.
    486                                 if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) {
     523                                if ( ! isset( $wp_registered_widgets[$widget_id] ) ) {
    487524                                        continue;
    488525                                }
    489526
     
    502539                                        'height'         => $wp_registered_widget_controls[$widget_id]['height'],
    503540                                        'is_wide'        => $this->is_wide_widget( $widget_id ),
    504541                                ) );
     542
    505543                                $this->manager->add_control( $control );
    506544                        }
    507545                }
     
    717755                        </div>'
    718756                );
    719757
     758                $inactive_widget_tpl = str_replace(
     759                        array( '{delete}', '{add}' ),
     760                        array(
     761                                __( 'Delete' ),
     762                                __( 'Add' )
     763                        ),
     764                        '<div id="saved-widget-<%- id %>" data-id-base="<%- idBase %>" data-widget-id="<%- id %>" class="widget-title saved-widget widget-tpl" tabindex="0">
     765                                <h3><%- type %><span class="in-widget-title"></span></h3>
     766                                <div class="saved-widget-controls">
     767                                        <a href="#" class="add-saved-widget" data-is-saved-widget="1" data-widget-id="<%- id %>"
     768                                        >{add}</a> &#124; <a href="#" class="delete-widget-permanently">{delete}</a>
     769                                </div>
     770                        </div>'
     771                );
     772
     773                $inactive_sidebar = array(
     774                        'inactive-sidebar' => array(
     775                                'name' => __( 'Save For Later' ),
     776                                'id' => 'wp_inactive_widgets',
     777                                'description' => __( 'Save widgets to the Inactive Sidebar to use later.' ),
     778                                'class' => 'inactive-sidebar',
     779                                'before_widget' => '<aside id="%1$s" class="widget %2$s">',
     780                                'after_widget' => '</aside>',
     781                                'before_title' => '<h1 class="widget-title">',
     782                                'after_title' => '</h1>',
     783                        ),
     784                );
     785
     786                $all_sidebars = array_values( array_merge( $wp_registered_sidebars, $inactive_sidebar ) );
     787
    720788                $settings = array(
    721                         'registeredSidebars'   => array_values( $wp_registered_sidebars ),
     789                        'registeredSidebars'   => $all_sidebars,
    722790                        'registeredWidgets'    => $wp_registered_widgets,
    723791                        'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
    724792                        'l10n' => array(
     
    726794                                'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
    727795                                'removeBtnLabel'   => __( 'Remove' ),
    728796                                'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
     797                                'moveBtnLabel'     => __( 'Move' ),
     798                                'moveBtnTooltip'   => __( 'Reorder the widgets in this sidebar.' ),
    729799                                'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
    730800                                'widgetMovedUp'    => __( 'Widget moved up' ),
    731801                                'widgetMovedDown'  => __( 'Widget moved down' ),
     
    734804                                'reorderModeOff'   => __( 'Reorder mode closed' ),
    735805                                'reorderLabelOn'   => esc_attr__( 'Reorder widgets' ),
    736806                                'reorderLabelOff'  => esc_attr__( 'Close reorder mode' ),
     807                                'itemDeleted'       => __( 'Widget deleted' ),
    737808                        ),
    738809                        'tpl' => array(
    739810                                'widgetReorderNav' => $widget_reorder_nav_tpl,
    740811                                'moveWidgetArea'   => $move_widget_area_tpl,
     812                                'inactiveWidget'   => $inactive_widget_tpl,
    741813                        ),
    742814                        'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
    743815                );
     
    781853                        </div>
    782854                        <div id="available-widgets-list">
    783855                        <?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">
     856                                <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">
    785857                                        <?php echo $available_widget['control_tpl']; ?>
    786858                                </div>
    787859                        <?php endforeach; ?>
     
    868940                foreach ( $widget_ids as $widget_id ) {
    869941                        $sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
    870942                }
     943
    871944                return $sanitized_widget_ids;
    872945        }
    873946
    874947        /**
     948         * Create an list of saved widgets that correspond to a widget type.
     949         *
     950         * @since 4.6.0
     951         * @access protected
     952         *
     953         * @global array $wp_registered_widget_controls
     954         *
     955         * @return array List of saved widget by type.
     956         */
     957        protected function get_sorted_saved_widgets() {
     958                global $wp_registered_widget_controls;
     959
     960                $sidebars_widgets = wp_get_sidebars_widgets();
     961
     962                $sorted_saved_widgets = array();
     963                foreach ( $sidebars_widgets as $sidebars_widget ) {
     964                        foreach( $sidebars_widget as $widget ) {
     965
     966                                if ( isset ( $wp_registered_widget_controls[ $widget ]['id_base'] ) ) {
     967                                        $current_base_id = $wp_registered_widget_controls[ $widget ]['id_base'];
     968                                        $sorted_saved_widgets[ $current_base_id ][] = array(
     969                                                'id' => $widget,
     970                                                'type' => $wp_registered_widget_controls[ $widget ]['name'],
     971                                        );
     972                                }
     973
     974                        }
     975                }
     976
     977                return $sorted_saved_widgets;
     978        }
     979
     980        /**
    875981         * Builds up an index of all available widgets for use in Backbone models.
    876982         *
    877983         * @since 3.9.0
     
    8941000                global $wp_registered_widgets, $wp_registered_widget_controls;
    8951001                require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
    8961002
     1003                $sorted_saved_widgets = $this->get_sorted_saved_widgets();
    8971004                $sort = $wp_registered_widgets;
    8981005                usort( $sort, array( $this, '_sort_name_callback' ) );
    8991006                $done = array();
     
    9501057                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
    9511058                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
    9521059                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
     1060                                'saved_widgets' => isset( $sorted_saved_widgets[ $id_base ] ) ? $sorted_saved_widgets[ $id_base ] : array(),
    9531061                        ) );
    9541062
    9551063                        $available_widgets[] = $available_widget;
     
    12571365         * @return array|void Sanitized widget instance.
    12581366         */
    12591367        public function sanitize_widget_instance( $value ) {
    1260                 if ( $value === array() ) {
     1368                if ( $value === array() || false === $value ) {
    12611369                        return $value;
    12621370                }
    12631371
     
    13471455                global $wp_registered_widget_updates, $wp_registered_widget_controls;
    13481456
    13491457                $setting_id = $this->get_setting_id( $widget_id );
    1350 
    13511458                /*
    13521459                 * Make sure that other setting changes have previewed since this widget
    13531460                 * may depend on them (e.g. Menus being present for Custom Menu widget).
     
    13961503                        }
    13971504                }
    13981505
     1506                // If deleting widget.
     1507                $delete_widget_val = $this->get_post_value( 'delete_widget' );
     1508                $is_widget_delete = ! empty( $delete_widget_val );
     1509
     1510                if ( $is_widget_delete ) {
     1511                        // Set post values needed in widget update_callback.
     1512                        $_POST[ 'sidebar' ] = 'wp_inactive_widgets';
     1513                        $_POST[ 'widget-' . $parsed_id['id_base'] ] = array();
     1514                        $_POST[ 'the-widget-id' ] = $widget_id;
     1515                        $_POST[ 'delete_widget' ] = '1';
     1516
     1517                        $post_values_added = array( 'sidebar', 'widget-' . $parsed_id[ 'id_base' ], 'the-widget-id', 'delete_widget' );
     1518                        $added_input_vars = array_merge( $added_input_vars, $post_values_added );
     1519
     1520                        /** This action is documented in wp-admin/widgets.php */
     1521                        do_action( 'delete_widget', $widget_id, $_POST[ 'sidebar' ], $parsed_id['id_base'] );
     1522                }
     1523
    13991524                // Invoke the widget update callback.
    14001525                foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
    14011526                        if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
     
    14281553
    14291554                // Obtain the widget instance.
    14301555                $option = $this->get_captured_option( $option_name );
    1431                 if ( null !== $parsed_id['number'] ) {
     1556                if ( $is_widget_delete ) {
     1557                        $instance = array( 'multidimensional_delete' => true );
     1558                } else if ( null !== $parsed_id['number'] ) {
    14321559                        $instance = $option[ $parsed_id['number'] ];
    14331560                } else {
    14341561                        $instance = $option;
     
    14451572                // Obtain the widget control with the updated instance in place.
    14461573                ob_start();
    14471574                $form = $wp_registered_widget_controls[ $widget_id ];
    1448                 if ( $form ) {
     1575                if ( $form && false === $is_widget_delete ) {
    14491576                        call_user_func_array( $form['callback'], $form['params'] );
    14501577                }
    14511578                $form = ob_get_clean();
  • src/wp-includes/class-wp-customize-setting.php

     
    845845
    846846                $result = $this->multidimensional( $root, $keys, true );
    847847
    848                 if ( isset( $result ) )
    849                         $result['node'][ $result['key'] ] = $value;
     848                if ( isset( $result ) ) {
    850849
     850                        if ( ! empty( $value['multidimensional_delete'] ) ) {
     851                                unset( $result['node'][ $result['key'] ] );
     852                        } else {
     853                                $result['node'][ $result['key'] ] = $value;
     854                        }
     855
     856                }
     857
    851858                return $root;
    852859        }
    853860
  • 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;
     
    478689                        if ( control.widgetControlEmbedded ) {
    479690                                return;
    480691                        }
     692
    481693                        control.widgetControlEmbedded = true;
    482694
    483695                        widgetControl = $( control.params.widget_control );
     
    491703                        control._setupReorderUI();
    492704                        control._setupHighlightEffects();
    493705                        control._setupUpdateUI();
     706                        control._setupMoveUI();
    494707                        control._setupRemoveUI();
    495708                },
    496709
     
    9041117                },
    9051118
    9061119                /**
    907                  * Set up event handlers for widget removal
     1120                 * Set up event handlers for widget move.
     1121                 *
     1122                 * @since 4.6.0
    9081123                 */
     1124                _setupMoveUI: function() {
     1125                        var self = this, $moveBtn, $reorderToggle, replaceDeleteWithMove;
     1126
     1127                        // Configure move button
     1128                        $moveBtn = this.container.find( 'a.widget-control-remove' );
     1129
     1130                        $moveBtn.on( 'click', function ( e ) {
     1131                                e.preventDefault();
     1132
     1133                                $reorderToggle = $( this ).closest( '.ui-sortable' ).find( '.reorder-toggle' );
     1134
     1135                                $reorderToggle.trigger( 'click' );
     1136                                self.openWidgetMoveArea();
     1137                        } );
     1138
     1139                        // Replace the Delete link with Move.
     1140                        replaceDeleteWithMove = function () {
     1141                                $moveBtn.text( l10n.moveBtnLabel ); // wp_widget_control() outputs the link as "Close"
     1142                                $moveBtn.attr( 'title', l10n.moveBtnTooltip );
     1143                        };
     1144
     1145                        replaceDeleteWithMove();
     1146                },
     1147
     1148                /**
     1149                 * Set up event handlers for widget removal.
     1150                 */
    9091151                _setupRemoveUI: function() {
    910                         var self = this, $removeBtn, replaceDeleteWithRemove;
     1152                        var self = this, $closeBtn;
    9111153
    9121154                        // Configure remove button
    913                         $removeBtn = this.container.find( 'a.widget-control-remove' );
    914                         $removeBtn.on( 'click', function( e ) {
     1155                        $closeBtn = this.container.find( 'a.widget-control-close' );
     1156                        $closeBtn.on( 'click', function( e ) {
    9151157                                e.preventDefault();
    9161158
    9171159                                // Find an adjacent element to add focus to when this widget goes away
     
    9261168
    9271169                                self.container.slideUp( function() {
    9281170                                        var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
    929                                                 sidebarWidgetIds, i;
     1171                                                sidebarWidgetIds, i, inactiveWidgets, inactiveWidgetSetting;
    9301172
    9311173                                        if ( ! sidebarsWidgetsControl ) {
    9321174                                                return;
     
    9411183                                        sidebarWidgetIds.splice( i, 1 );
    9421184                                        sidebarsWidgetsControl.setting( sidebarWidgetIds );
    9431185
     1186                                        // Add widget to inactive widgets.
     1187                                        inactiveWidgetSetting = api( 'sidebars_widgets[wp_inactive_widgets]' );
     1188                                        inactiveWidgets = inactiveWidgetSetting();
     1189                                        inactiveWidgets.push( self.params.widget_id );
     1190                                        inactiveWidgetSetting.set( [] ).set( _( inactiveWidgets ).unique() );
     1191
    9441192                                        $adjacentFocusTarget.focus(); // keyboard accessibility
     1193
     1194                                        // Remove display none in case its added again.
     1195                                        self.container.show();
    9451196                                } );
    9461197                        } );
    9471198
    948                         replaceDeleteWithRemove = function() {
    949                                 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
    950                                 $removeBtn.attr( 'title', l10n.removeBtnTooltip );
    951                         };
    952 
    953                         if ( this.params.is_new ) {
    954                                 api.bind( 'saved', replaceDeleteWithRemove );
    955                         } else {
    956                                 replaceDeleteWithRemove();
    957                         }
     1199                        $closeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Close"
     1200                        $closeBtn.attr( 'title', l10n.removeBtnTooltip );
    9581201                },
    9591202
    9601203                /**
     
    11131356                        params.nonce = api.settings.nonce['update-widget'];
    11141357                        params.theme = api.settings.theme.stylesheet;
    11151358                        params.customized = wp.customize.previewer.query().customized;
     1359                        params.delete_widget = ( false === self.setting() ) ? 1 : 0;
    11161360
    11171361                        data = $.param( params );
    11181362                        $inputs = this._getInputs( $widgetContent );
     
    11261370
    11271371                        if ( instanceOverride ) {
    11281372                                data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
    1129                         } else {
     1373                        } else if ( $inputs.length ) {
    11301374                                data += '&' + $inputs.serialize();
    11311375                        }
     1376
    11321377                        data += '&' + $widgetContent.find( '~ :input' ).serialize();
    11331378
    11341379                        if ( this._previousUpdateRequest ) {
     
    12091454                                         * preview finishing loading.
    12101455                                         */
    12111456                                        isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
    1212                                         if ( isChanged ) {
     1457                                        if ( isChanged || false === self.setting() ) {
    12131458                                                self.isWidgetUpdating = true; // suppress triggering another updateWidget
    12141459                                                self.setting( r.data.instance );
    12151460                                                self.isWidgetUpdating = false;
     
    13261571                 */
    13271572                onChangeExpanded: function ( expanded, args ) {
    13281573                        var self = this, $widget, $inside, complete, prevComplete;
    1329 
    13301574                        self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI.
    13311575                        if ( expanded ) {
    13321576                                self.embedWidgetContent();
     
    14851729                        }
    14861730
    14871731                        $moveWidgetArea.toggleClass( 'active', showOrHide );
     1732                        self.toggleSaveForLater();
    14881733                },
    14891734
    14901735                /**
     1736                 * Open widget move area if control is not active already.
     1737                 *
     1738                 * @since 4.6.0
     1739                 */
     1740                openWidgetMoveArea: function() {
     1741                        var self = this, $moveWidgetArea;
     1742
     1743                        $moveWidgetArea = this.container.find( '.move-widget-area' );
     1744
     1745                        if( ! $moveWidgetArea.hasClass( 'active' ) ) {
     1746                                self.toggleWidgetMoveArea();
     1747                        }
     1748                },
     1749
     1750                /**
     1751                 * Toggle visibility of the Save For Later/Inactive Widget Sidebar section.
     1752                 *
     1753                 * @since 4.6.0
     1754                 */
     1755                toggleSaveForLater: function() {
     1756                        var $moveWidgetArea, $saveForLater;
     1757
     1758                        $moveWidgetArea = this.container.find( '.move-widget-area' );
     1759                        $saveForLater = this.container.find( 'li[data-id="wp_inactive_widgets"]' );
     1760
     1761                        if( $moveWidgetArea.hasClass( 'active' ) ) {
     1762                                $saveForLater.show();
     1763                        } else {
     1764                                $saveForLater.hide();
     1765                        }
     1766                },
     1767
     1768                /**
    14911769                 * Highlight the widget control and section
    14921770                 */
    14931771                highlightSectionAndControl: function() {
     
    17081986                                                        return;
    17091987                                                }
    17101988
    1711                                                 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
    1712 
    1713                                                 // Detect if widget control was dragged to another sidebar
    1714                                                 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
    1715 
    1716                                                 // Delete any widget form controls for removed widgets
    1717                                                 if ( removedControl && ! wasDraggedToAnotherSidebar ) {
    1718                                                         api.control.remove( removedControl.id );
    1719                                                         removedControl.container.remove();
    1720                                                 }
    1721 
    17221989                                                // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
    17231990                                                // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
    1724                                                 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
     1991                                                if ( api.Widgets.savedWidgetIds[removedWidgetId] && 'sidebars_widgets[wp_inactive_widgets]' !== self.id ) {
    17251992                                                        inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
    17261993                                                        inactiveWidgets.push( removedWidgetId );
    17271994                                                        api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
     
    19902257                                setting.set( {} ); // mark dirty, changing from '' to {}
    19912258                        }
    19922259
    1993                         controlConstructor = api.controlConstructor[controlType];
    1994                         widgetFormControl = new controlConstructor( settingId, {
    1995                                 params: {
    1996                                         settings: {
    1997                                                 'default': settingId
     2260                        widgetFormControl = api.control( settingId );
     2261                        if ( ! widgetFormControl ) {
     2262
     2263                                controlConstructor = api.controlConstructor[controlType];
     2264                                widgetFormControl = new controlConstructor( settingId, {
     2265                                        params: {
     2266                                                settings: {
     2267                                                        'default': settingId
     2268                                                },
     2269                                                content: controlContainer,
     2270                                                sidebar_id: self.params.sidebar_id,
     2271                                                widget_id: widgetId,
     2272                                                widget_id_base: widget.get( 'id_base' ),
     2273                                                type: controlType,
     2274                                                is_new: ! isExistingWidget,
     2275                                                width: widget.get( 'width' ),
     2276                                                height: widget.get( 'height' ),
     2277                                                is_wide: widget.get( 'is_wide' ),
     2278                                                active: true
    19982279                                        },
    1999                                         content: controlContainer,
    2000                                         sidebar_id: self.params.sidebar_id,
    2001                                         widget_id: widgetId,
    2002                                         widget_id_base: widget.get( 'id_base' ),
    2003                                         type: controlType,
    2004                                         is_new: ! isExistingWidget,
    2005                                         width: widget.get( 'width' ),
    2006                                         height: widget.get( 'height' ),
    2007                                         is_wide: widget.get( 'is_wide' ),
    2008                                         active: true
    2009                                 },
    2010                                 previewer: self.setting.previewer
    2011                         } );
    2012                         api.control.add( settingId, widgetFormControl );
     2280                                        previewer: self.setting.previewer
     2281                                } );
     2282                                api.control.add( settingId, widgetFormControl );
     2283                        }
    20132284
    20142285                        // Make sure widget is removed from the other sidebars
    20152286                        api.each( function( otherSetting ) {
     
    20252296                                        i = _.indexOf( otherSidebarWidgets, widgetId );
    20262297
    20272298                                if ( -1 !== i ) {
    2028                                         otherSidebarWidgets.splice( i );
     2299                                        otherSidebarWidgets.splice( i, 1 );
    20292300                                        otherSetting( otherSidebarWidgets );
    20302301                                }
    20312302                        } );
  • src/wp-admin/css/customize-controls.css

     
    16261626        #available-widgets-list {
    16271627                top: 140px;
    16281628        }
     1629
     1630        .wp-customizer #available-widgets .saved-widget-controls {
     1631                visibility: visible;
     1632        }
    16291633}
  • 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        }