Make WordPress Core

Ticket #27404: 27404-1-30-17.patch

File 27404-1-30-17.patch, 34.2 KB (added by rramo012, 8 years ago)

Refresh

  • src/wp-includes/class-wp-customize-widgets.php

     
    447447
    448448                                // Add section to contain controls.
    449449                                $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
    450                                 if ( $is_active_sidebar ) {
    451450
    452                                         $section_args = array(
    453                                                 'title' => $wp_registered_sidebars[ $sidebar_id ]['name'],
    454                                                 'description' => $wp_registered_sidebars[ $sidebar_id ]['description'],
    455                                                 'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
    456                                                 'panel' => 'widgets',
    457                                                 'sidebar_id' => $sidebar_id,
    458                                         );
     451                                if ( $is_inactive_widgets ) {
     452                                        $title = 'Inactive Widgets';
     453                                        $description = $title;
     454                                } else {
     455                                        $title = $wp_registered_sidebars[ $sidebar_id ]['name'];
     456                                        $description = $wp_registered_sidebars[ $sidebar_id ]['description'];
     457                                }
    459458
    460                                         /**
    461                                          * Filters Customizer widget section arguments for a given sidebar.
    462                                          *
    463                                          * @since 3.9.0
    464                                          *
    465                                          * @param array      $section_args Array of Customizer widget section arguments.
    466                                          * @param string     $section_id   Customizer section ID.
    467                                          * @param int|string $sidebar_id   Sidebar ID.
    468                                          */
    469                                         $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
     459                                $section_args = array(
     460                                        'title' => $title,
     461                                        'description' => $description,
     462                                        'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
     463                                        'panel' => 'widgets',
     464                                        'sidebar_id' => $sidebar_id,
     465                                );
    470466
    471                                         $section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
    472                                         $this->manager->add_section( $section );
     467                                /**
     468                                 * Filters Customizer widget section arguments for a given sidebar.
     469                                 *
     470                                 * @since 3.9.0
     471                                 *
     472                                 * @param array      $section_args Array of Customizer widget section arguments.
     473                                 * @param string     $section_id   Customizer section ID.
     474                                 * @param int|string $sidebar_id   Sidebar ID.
     475                                 */
     476                                $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
    473477
    474                                         $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
    475                                                 'section'    => $section_id,
    476                                                 'sidebar_id' => $sidebar_id,
    477                                                 'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
    478                                         ) );
    479                                         $new_setting_ids[] = $setting_id;
     478                                $section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
     479                                $this->manager->add_section( $section );
    480480
    481                                         $this->manager->add_control( $control );
    482                                 }
     481                                $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
     482                                        'section'    => $section_id,
     483                                        'sidebar_id' => $sidebar_id,
     484                                        'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
     485                                ) );
     486                                $new_setting_ids[] = $setting_id;
     487
     488                                $this->manager->add_control( $control );
     489
     490                                $section_args = array(
     491                                        'title' => $title,
     492                                        'description' => $description,
     493                                        'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
     494                                        'panel' => 'widgets',
     495                                        'sidebar_id' => $sidebar_id,
     496                                );
     497
     498                                /**
     499                                 * Filter Customizer widget section arguments for a given sidebar.
     500                                 *
     501                                 * @since 3.9.0
     502                                 *
     503                                 * @param array      $section_args Array of Customizer widget section arguments.
     504                                 * @param string     $section_id   Customizer section ID.
     505                                 * @param int|string $sidebar_id   Sidebar ID.
     506                                 */
     507                                $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
     508
     509                                $section = new WP_Customize_Sidebar_Section( $this->manager, $section_id, $section_args );
     510                                $this->manager->add_section( $section );
     511
     512                                $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
     513                                        'section'    => $section_id,
     514                                        'sidebar_id' => $sidebar_id,
     515                                        'priority'   => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
     516                                ) );
     517                                $new_setting_ids[] = $setting_id;
     518
     519                                $this->manager->add_control( $control );
    483520                        }
    484521
    485522                        // Add a control for each active widget (located in a sidebar).
    486523                        foreach ( $sidebar_widget_ids as $i => $widget_id ) {
    487524
    488525                                // Skip widgets that may have gone away due to a plugin being deactivated.
    489                                 if ( ! $is_active_sidebar || ! isset( $wp_registered_widgets[$widget_id] ) ) {
     526                                if ( ! isset( $wp_registered_widgets[$widget_id] ) ) {
    490527                                        continue;
    491528                                }
    492529
     
    505542                                        'height'         => $wp_registered_widget_controls[$widget_id]['height'],
    506543                                        'is_wide'        => $this->is_wide_widget( $widget_id ),
    507544                                ) );
     545
    508546                                $this->manager->add_control( $control );
    509547                        }
    510548                }
     
    720758                        </div>'
    721759                );
    722760
     761                $inactive_widget_tpl = str_replace(
     762                        array( '{delete}', '{add}' ),
     763                        array(
     764                                __( 'Delete' ),
     765                                __( 'Add' )
     766                        ),
     767                        '<div id="saved-widget-<%- id %>" data-id-base="<%- idBase %>" data-widget-id="<%- id %>" class="widget-title saved-widget widget-tpl" tabindex="0">
     768                                <h3><%- type %><span class="in-widget-title"></span></h3>
     769                                <div class="saved-widget-controls">
     770                                        <a href="#" class="add-saved-widget" data-is-saved-widget="1" data-widget-id="<%- id %>"
     771                                        >{add}</a> &#124; <a href="#" class="delete-widget-permanently">{delete}</a>
     772                                </div>
     773                        </div>'
     774                );
     775
     776                $inactive_sidebar = array(
     777                        'inactive-sidebar' => array(
     778                                'name' => __( 'Save For Later' ),
     779                                'id' => 'wp_inactive_widgets',
     780                                'description' => __( 'Save widgets to the Inactive Sidebar to use later.' ),
     781                                'class' => 'inactive-sidebar',
     782                                'before_widget' => '<aside id="%1$s" class="widget %2$s">',
     783                                'after_widget' => '</aside>',
     784                                'before_title' => '<h1 class="widget-title">',
     785                                'after_title' => '</h1>',
     786                        ),
     787                );
     788
     789                $all_sidebars = array_values( array_merge( $wp_registered_sidebars, $inactive_sidebar ) );
     790
    723791                $settings = array(
    724                         'registeredSidebars'   => array_values( $wp_registered_sidebars ),
     792                        'registeredSidebars'   => $all_sidebars,
    725793                        'registeredWidgets'    => $wp_registered_widgets,
    726794                        'availableWidgets'     => $available_widgets, // @todo Merge this with registered_widgets
    727795                        'l10n' => array(
     
    729797                                'saveBtnTooltip'   => __( 'Save and preview changes before publishing them.' ),
    730798                                'removeBtnLabel'   => __( 'Remove' ),
    731799                                'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
     800                                'moveBtnLabel'     => __( 'Move' ),
     801                                'moveBtnTooltip'   => __( 'Reorder the widgets in this sidebar.' ),
    732802                                'error'            => __( 'An error has occurred. Please reload the page and try again.' ),
    733803                                'widgetMovedUp'    => __( 'Widget moved up' ),
    734804                                'widgetMovedDown'  => __( 'Widget moved down' ),
     
    736806                                'reorderModeOn'    => __( 'Reorder mode enabled' ),
    737807                                'reorderModeOff'   => __( 'Reorder mode closed' ),
    738808                                'reorderLabelOn'   => esc_attr__( 'Reorder widgets' ),
     809                                'reorderLabelOff'  => esc_attr__( 'Close reorder mode' ),
     810                                'itemDeleted'       => __( 'Widget deleted' ),
    739811                                'widgetsFound'     => __( 'Number of widgets found: %d' ),
    740812                                'noWidgetsFound'   => __( 'No widgets found.' ),
    741813                        ),
    742814                        'tpl' => array(
    743815                                'widgetReorderNav' => $widget_reorder_nav_tpl,
    744816                                'moveWidgetArea'   => $move_widget_area_tpl,
     817                                'inactiveWidget'   => $inactive_widget_tpl,
    745818                        ),
    746819                        'selectiveRefreshableWidgets' => $this->get_selective_refreshable_widgets(),
    747820                );
     
    788861                        </div>
    789862                        <div id="available-widgets-list">
    790863                        <?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
    791                                 <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">
     864                                <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">
    792865                                        <?php echo $available_widget['control_tpl']; ?>
    793866                                </div>
    794867                        <?php endforeach; ?>
     
    876949                foreach ( $widget_ids as $widget_id ) {
    877950                        $sanitized_widget_ids[] = preg_replace( '/[^a-z0-9_\-]/', '', $widget_id );
    878951                }
     952
    879953                return $sanitized_widget_ids;
    880954        }
    881955
    882956        /**
     957         * Create an list of saved widgets that correspond to a widget type.
     958         *
     959         * @since 4.8.0
     960         * @access protected
     961         *
     962         * @global array $wp_registered_widget_controls
     963         *
     964         * @return array List of saved widget by type.
     965         */
     966        protected function get_sorted_saved_widgets() {
     967                global $wp_registered_widget_controls;
     968
     969                $sidebars_widgets = wp_get_sidebars_widgets();
     970
     971                $sorted_saved_widgets = array();
     972                foreach ( $sidebars_widgets as $sidebars_widget ) {
     973                        foreach( $sidebars_widget as $widget ) {
     974
     975                                if ( isset ( $wp_registered_widget_controls[ $widget ]['id_base'] ) ) {
     976                                        $current_base_id = $wp_registered_widget_controls[ $widget ]['id_base'];
     977                                        $sorted_saved_widgets[ $current_base_id ][] = array(
     978                                                'id' => $widget,
     979                                                'type' => $wp_registered_widget_controls[ $widget ]['name'],
     980                                        );
     981                                }
     982
     983                        }
     984                }
     985
     986                return $sorted_saved_widgets;
     987        }
     988
     989        /**
    883990         * Builds up an index of all available widgets for use in Backbone models.
    884991         *
    885992         * @since 3.9.0
     
    9021009                global $wp_registered_widgets, $wp_registered_widget_controls;
    9031010                require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
    9041011
     1012                $sorted_saved_widgets = $this->get_sorted_saved_widgets();
    9051013                $sort = $wp_registered_widgets;
    9061014                usort( $sort, array( $this, '_sort_name_callback' ) );
    9071015                $done = array();
     
    9581066                                'width'        => $wp_registered_widget_controls[$widget['id']]['width'],
    9591067                                'height'       => $wp_registered_widget_controls[$widget['id']]['height'],
    9601068                                'is_wide'      => $this->is_wide_widget( $widget['id'] ),
     1069                                'saved_widgets' => isset( $sorted_saved_widgets[ $id_base ] ) ? $sorted_saved_widgets[ $id_base ] : array(),
    9611070                        ) );
    9621071
    9631072                        $available_widgets[] = $available_widget;
     
    12701379         * @return array|void Sanitized widget instance.
    12711380         */
    12721381        public function sanitize_widget_instance( $value ) {
    1273                 if ( $value === array() ) {
     1382                if ( $value === array() || false === $value ) {
    12741383                        return $value;
    12751384                }
    12761385
     
    13601469                global $wp_registered_widget_updates, $wp_registered_widget_controls;
    13611470
    13621471                $setting_id = $this->get_setting_id( $widget_id );
    1363 
    13641472                /*
    13651473                 * Make sure that other setting changes have previewed since this widget
    13661474                 * may depend on them (e.g. Menus being present for Custom Menu widget).
     
    14091517                        }
    14101518                }
    14111519
     1520                // If deleting widget.
     1521                $delete_widget_val = $this->get_post_value( 'delete_widget' );
     1522                $is_widget_delete = ! empty( $delete_widget_val );
     1523
     1524                if ( $is_widget_delete ) {
     1525                        // Set post values needed in widget update_callback.
     1526                        $_POST[ 'sidebar' ] = 'wp_inactive_widgets';
     1527                        $_POST[ 'widget-' . $parsed_id['id_base'] ] = array();
     1528                        $_POST[ 'the-widget-id' ] = $widget_id;
     1529                        $_POST[ 'delete_widget' ] = '1';
     1530
     1531                        $post_values_added = array( 'sidebar', 'widget-' . $parsed_id[ 'id_base' ], 'the-widget-id', 'delete_widget' );
     1532                        $added_input_vars = array_merge( $added_input_vars, $post_values_added );
     1533
     1534                        /** This action is documented in wp-admin/widgets.php */
     1535                        do_action( 'delete_widget', $widget_id, $_POST[ 'sidebar' ], $parsed_id['id_base'] );
     1536                }
     1537
    14121538                // Invoke the widget update callback.
    14131539                foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
    14141540                        if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
     
    14411567
    14421568                // Obtain the widget instance.
    14431569                $option = $this->get_captured_option( $option_name );
    1444                 if ( null !== $parsed_id['number'] ) {
     1570                if ( $is_widget_delete ) {
     1571                        $instance = array( 'multidimensional_delete' => true );
     1572                } else if ( null !== $parsed_id['number'] ) {
    14451573                        $instance = $option[ $parsed_id['number'] ];
    14461574                } else {
    14471575                        $instance = $option;
     
    14581586                // Obtain the widget control with the updated instance in place.
    14591587                ob_start();
    14601588                $form = $wp_registered_widget_controls[ $widget_id ];
    1461                 if ( $form ) {
     1589                if ( $form && false === $is_widget_delete ) {
    14621590                        call_user_func_array( $form['callback'], $form['params'] );
    14631591                }
    14641592                $form = ob_get_clean();
  • src/wp-includes/class-wp-customize-setting.php

     
    868868
    869869                $result = $this->multidimensional( $root, $keys, true );
    870870
    871                 if ( isset( $result ) )
    872                         $result['node'][ $result['key'] ] = $value;
     871                if ( isset( $result ) ) {
    873872
     873                        if ( ! empty( $value['multidimensional_delete'] ) ) {
     874                                unset( $result['node'][ $result['key'] ] );
     875                        } else {
     876                                $result['node'][ $result['key'] ] = $value;
     877                        }
     878
     879                }
     880
    874881                return $root;
    875882        }
    876883
  • src/wp-admin/js/customize-widgets.js

     
    150150                        'keyup #widgets-search': 'search',
    151151                        'focus .widget-tpl' : 'focus',
    152152                        'click .widget-tpl' : '_submit',
     153                        'click .add-saved-widget' : '_submit',
     154                        'click .delete-widget-permanently' : '_delete',
     155                        'click .widget-tpl .widget-title-action' : '_toggleInactive',
    153156                        'keypress .widget-tpl' : '_submit',
    154157                        'keydown' : 'keyboardAccessible'
    155158                },
    156159
     160                // Inactive widget template to be used in available widgets.
     161                inactiveWidgetTpl: null,
     162
    157163                // Cache current selected widget
    158164                selected: null,
    159165
     
    176182
    177183                        this.updateList();
    178184
     185                        this.inactiveWidgetTpl = _.template( api.Widgets.data.tpl.inactiveWidget );
     186
     187                        this._createInactiveWidgets();
     188
    179189                        // Set the initial search count to the number of available widgets.
    180190                        this.searchMatchesCount = this.collection.length;
    181191
     
    228238                                }
    229239                        }
    230240
     241                        // Collapse inactive widgets upon searching.
     242                        this.$el.find( '.saved-widget' ).hide();
     243                        this.$el.find( '.widget-tpl.expanded' ).removeClass( 'expanded' );
     244
    231245                        // Toggle the clear search results button.
    232246                        if ( '' !== event.target.value ) {
    233247                                this.$clearResults.addClass( 'is-visible' );
     
    270284                        } );
    271285                },
    272286
     287                /**
     288                 * Create the list of inactive widgets to be added to the available widgets sidebar onload.
     289                 *
     290                 * @since 4.8.0
     291                 */
     292                _createInactiveWidgets : function () {
     293                        var self = this;
     294
     295                        _( api.Widgets.data.availableWidgets ).each( function ( availableWidget )  {
     296                                _( availableWidget.saved_widgets ).each( function ( savedWidget )  {
     297                                        self.addInactiveWidget( {
     298                                                id: savedWidget.id,
     299                                                type: savedWidget.type,
     300                                                idBase: availableWidget.id_base,
     301                                        } );
     302                                });
     303                        });
     304                },
     305
     306                /**
     307                 * Adds a single inactive widget to the available widgets sidebar.
     308                 *
     309                 * @since 4.8.0
     310                 */
     311                addInactiveWidget : function ( templateArgs ) {
     312                        var $inactiveWidget = $( this.inactiveWidgetTpl( templateArgs ) );
     313
     314                        if ( 0 === this.$el.find( '.saved-widget[data-widget-id="' + templateArgs.id + '"]' ).length ) {
     315                                this.$el.find( '[data-id-base="' + templateArgs.idBase + '"]' ).first().after( $inactiveWidget );
     316                        }
     317
     318                        return $inactiveWidget;
     319                },
     320
     321                /**
     322                 * Show inactive widgets and hide active widgets.
     323                 *
     324                 * @since 4.8.0
     325                 */
     326                updateVisibleSavedWidgets : function () {
     327                        var self = this;
     328
     329                        // Remove state classes.
     330                        this.$el.find( '.widget-tpl.has-inactive-widgets, .widget-tpl.expanded' )
     331                                .removeClass( 'has-inactive-widgets' )
     332                                .removeClass( 'expanded' );
     333
     334                        // Hide all inactive widgets
     335                        this.$el.find( '.saved-widget' )
     336                                .hide()
     337                                .removeClass( 'inactive-widget' );
     338
     339                        _( api( 'sidebars_widgets[wp_inactive_widgets]' )() ).each( function( widgetId ) {
     340                                var $savedWidget  = self.$el.find( '#saved-widget-' + widgetId ),
     341                                        parsedWidgetId = parseWidgetId( widgetId ),
     342                                        widgetControlId = 'widget_' + parsedWidgetId.id_base + '[' + parsedWidgetId.number + ']',
     343                                        widgetControl =  api.control( widgetControlId ),
     344                                        widgetSetting =  api( widgetControlId ),
     345                                        title = ( widgetSetting ) ? widgetSetting().title : null,
     346                                        $inWidgetTitle,
     347                                        availableWidget;
     348
     349                                if ( ! widgetControl ) {
     350                                        return;
     351                                }
     352
     353                                // If inactive widget HTML does not exist, create it.
     354                                if ( 0 === $savedWidget.length ) {
     355                                        availableWidget = api.Widgets.availableWidgets.findWhere( { id_base: parsedWidgetId.id_base } );
     356                                        $savedWidget = self.addInactiveWidget({
     357                                                id: widgetId,
     358                                                type: availableWidget.attributes.name,
     359                                                idBase: parsedWidgetId.id_base,
     360                                        });
     361                                }
     362
     363                                $savedWidget.addClass( 'inactive-widget' );
     364
     365                                self.$el.find( '.widget-tpl[data-id-base="' + parsedWidgetId.id_base + '"]' )
     366                                        .not( '.saved-widget' )
     367                                        .addClass( 'has-inactive-widgets' );
     368
     369                                $inWidgetTitle = $savedWidget.find( '.in-widget-title' );
     370                                if ( title ) {
     371                                        $inWidgetTitle.text( ': ' + title );
     372                                } else {
     373                                        $inWidgetTitle.text( '' );
     374                                }
     375                        } );
     376                },
     377
     378                /**
     379                 * Show or hide the list of inactive widgets per widget id base.
     380                 *
     381                 * @since 4.8.0
     382                 */
     383                toggleInactive : function ( idBase ) {
     384                        var $widgetTpl = this.$el.find( '.has-inactive-widgets[data-id-base="' + idBase + '"]' ),
     385                                $inactiveWidgets = this.$el.find( '.saved-widget[data-id-base="' + idBase + '"].inactive-widget' );
     386
     387                        $widgetTpl.toggleClass( 'expanded' );
     388                        $inactiveWidgets.stop().slideToggle( 'fast' );
     389
     390                        if ( $widgetTpl.hasClass('expanded') ) {
     391                                this.select( $inactiveWidgets.first().focus() );
     392                        } else {
     393                                this.select( $widgetTpl.focus() );
     394                        }
     395                },
     396
     397                /**
     398                 * Slides down a list of active widgets per widget type.
     399                 *
     400                 * @since 4.8.0
     401                 */
     402                _toggleInactive : function ( event ) {
     403                        event.stopPropagation();
     404                        event.preventDefault();
     405
     406                        var $widgetTpl = $( event.currentTarget ).closest( '.widget-tpl' );
     407                        this.toggleInactive( $widgetTpl.data('id-base') );
     408                },
     409
     410                /**
     411                 * Permanently delete a widget.
     412                 *
     413                 * @since 4.8.0
     414                 */
     415                _delete : function ( event ) {
     416                        var self = this,
     417                                $currentTarget = $( event.currentTarget ),
     418                                $savedWidgets = $currentTarget.closest( '.saved-widget' ),
     419                                widgetId = $savedWidgets.data( 'widget-id' ),
     420                                idBase = $savedWidgets.data( 'id-base' ),
     421                                settingId = widgetIdToSettingId( widgetId ),
     422                                control = api.Widgets.getWidgetFormControlForWidget( widgetId ),
     423                                deleteCallback,
     424                                inactiveSidebarWidgets,
     425                                inactiveWidgets,
     426                                i;
     427
     428                        // Remove from sidebar.
     429                        inactiveWidgets = api( 'sidebars_widgets[wp_inactive_widgets]' );
     430
     431                        inactiveSidebarWidgets = inactiveWidgets().slice();
     432                        i = _.indexOf( inactiveSidebarWidgets, widgetId );
     433                        if ( -1 !== i ) {
     434                                inactiveSidebarWidgets.splice( i, 1 );
     435                                inactiveWidgets.set( inactiveSidebarWidgets );
     436                        }
     437
     438                        // Embed control to allow unembedded controls to trigger update calls.
     439                        if ( ! control.widgetControlEmdedded ) {
     440                                control.embedWidgetControl();
     441                        }
     442                        api( settingId ).set( false );
     443
     444                        deleteCallback = function () {
     445                                var $remainingWidgets;
     446
     447                                $( this ).remove();
     448                                api.control.remove( control.id );
     449                                control.container.remove();
     450
     451                                // Remove drop down if widgets no longer exist
     452                                $remainingWidgets = self.$el.find( '.saved-widget[data-id-base="' + idBase + '"].inactive-widget' );
     453                                if ( ! $remainingWidgets.length ) {
     454                                        self.$el.find( '.has-inactive-widgets[data-id-base="' + idBase + '"]' )
     455                                                .removeClass( 'has-inactive-widgets expanded' );
     456                                }
     457
     458                                wp.a11y.speak( l10n.itemDeleted );
     459                        };
     460
     461                        $currentTarget.closest( '.saved-widget' ).slideUp( 'fast' , deleteCallback );
     462                },
     463
    273464                // Highlights a widget
    274465                select: function( widgetTpl ) {
    275466                        this.selected = $( widgetTpl );
     
    294485
    295486                // Adds a selected widget to the sidebar
    296487                submit: function( widgetTpl ) {
    297                         var widgetId, widget, widgetFormControl;
     488                        var widgetId, widget, widgetFormControl, isSavedWidget, addWidgetId;
    298489
    299490                        if ( ! widgetTpl ) {
    300491                                widgetTpl = this.selected;
     
    306497
    307498                        this.select( widgetTpl );
    308499
    309                         widgetId = $( this.selected ).data( 'widget-id' );
     500                        widgetId = widgetTpl.data( 'widget-id' );
     501                        isSavedWidget = widgetTpl.data( 'is-saved-widget' );
     502
    310503                        widget = this.collection.findWhere( { id: widgetId } );
    311                         if ( ! widget ) {
     504                        if ( ( ! widget && ! isSavedWidget ) || widgetTpl.hasClass( 'saved-widget' ) ) {
    312505                                return;
    313506                        }
    314507
    315                         widgetFormControl = this.currentSidebarControl.addWidget( widget.get( 'id_base' ) );
     508                        addWidgetId = ( isSavedWidget ) ? widgetId : widget.get( 'id_base' );
     509                        widgetFormControl = this.currentSidebarControl.addWidget( addWidgetId );
     510
    316511                        if ( widgetFormControl ) {
    317512                                widgetFormControl.focus();
    318513                        }
     
    324519                open: function( sidebarControl ) {
    325520                        this.currentSidebarControl = sidebarControl;
    326521
     522                        // Create inactive widget controls that were moved to the inactive sidebar.
     523                        this.updateVisibleSavedWidgets();
     524
    327525                        // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens
    328526                        _( this.currentSidebarControl.getWidgetFormControls() ).each( function( control ) {
    329527                                if ( control.params.is_wide ) {
     
    359557                        this.$search.val( '' );
    360558                },
    361559
    362                 // Add keyboard accessiblity to the panel
     560                // Add keyboard accessibility to the panel
    363561                keyboardAccessible: function( event ) {
    364562                        var isEnter = ( event.which === 13 ),
    365563                                isEsc = ( event.which === 27 ),
    366564                                isDown = ( event.which === 40 ),
    367565                                isUp = ( event.which === 38 ),
     566                                isLeft = ( event.which === 37 ),
     567                                isRight = ( event.which === 39 ),
    368568                                isTab = ( event.which === 9 ),
    369569                                isShift = ( event.shiftKey ),
    370570                                selected = null,
    371                                 firstVisible = this.$el.find( '> .widget-tpl:visible:first' ),
    372                                 lastVisible = this.$el.find( '> .widget-tpl:visible:last' ),
     571                                firstVisible = this.$el.find( '.widget-tpl:visible:first' ),
     572                                lastVisible = this.$el.find( '.widget-tpl:visible:last' ),
    373573                                isSearchFocused = $( event.target ).is( this.$search ),
    374574                                isLastWidgetFocused = $( event.target ).is( '.widget-tpl:visible:last' );
    375575
     
    399599                                return;
    400600                        }
    401601
     602                        // Toggle display when the user presses left or right on widget template with inactive widgets.
     603                        if ( isLeft || isRight ) {
     604                                if ( this.selected && (this.selected.hasClass( 'has-inactive-widgets' ) || this.selected.hasClass( 'saved-widget' ) ) ) {
     605                                        var idBase = this.selected.data('id-base');
     606                                        this.toggleInactive( idBase );
     607                                }
     608
     609                                return;
     610                        }
     611
    402612                        // If enter pressed but nothing entered, don't do anything
    403613                        if ( isEnter && ! this.$search.val() ) {
    404614                                return;
     
    521731                        if ( control.widgetControlEmbedded ) {
    522732                                return;
    523733                        }
     734
    524735                        control.widgetControlEmbedded = true;
    525736
    526737                        widgetControl = $( control.params.widget_control );
     
    534745                        control._setupReorderUI();
    535746                        control._setupHighlightEffects();
    536747                        control._setupUpdateUI();
     748                        control._setupMoveUI();
    537749                        control._setupRemoveUI();
    538750                },
    539751
     
    9471159                },
    9481160
    9491161                /**
    950                  * Set up event handlers for widget removal
     1162                 * Set up event handlers for widget move.
     1163                 *
     1164                 * @since 4.8.0
    9511165                 */
     1166                _setupMoveUI: function() {
     1167                        var self = this, $moveBtn, $reorderToggle, replaceDeleteWithMove;
     1168
     1169                        // Configure move button
     1170                        $moveBtn = this.container.find( 'a.widget-control-remove' );
     1171
     1172                        $moveBtn.on( 'click', function ( e ) {
     1173                                e.preventDefault();
     1174
     1175                                $reorderToggle = $( this ).closest( '.ui-sortable' ).find( '.reorder-toggle' );
     1176
     1177                                $reorderToggle.trigger( 'click' );
     1178                                self.openWidgetMoveArea();
     1179                        } );
     1180
     1181                        // Replace the Delete link with Move.
     1182                        replaceDeleteWithMove = function () {
     1183                                $moveBtn.text( l10n.moveBtnLabel ); // wp_widget_control() outputs the link as "Close"
     1184                                $moveBtn.attr( 'title', l10n.moveBtnTooltip );
     1185                        };
     1186
     1187                        replaceDeleteWithMove();
     1188                },
     1189
     1190                /**
     1191                 * Set up event handlers for widget removal.
     1192                 */
    9521193                _setupRemoveUI: function() {
    953                         var self = this, $removeBtn, replaceDeleteWithRemove;
     1194                        var self = this, $closeBtn;
    9541195
    9551196                        // Configure remove button
    956                         $removeBtn = this.container.find( 'a.widget-control-remove' );
    957                         $removeBtn.on( 'click', function( e ) {
     1197                        $closeBtn = this.container.find( 'a.widget-control-close' );
     1198                        $closeBtn.on( 'click', function( e ) {
    9581199                                e.preventDefault();
    9591200
    9601201                                // Find an adjacent element to add focus to when this widget goes away
     
    9691210
    9701211                                self.container.slideUp( function() {
    9711212                                        var sidebarsWidgetsControl = api.Widgets.getSidebarWidgetControlContainingWidget( self.params.widget_id ),
    972                                                 sidebarWidgetIds, i;
     1213                                                sidebarWidgetIds, i, inactiveWidgets, inactiveWidgetSetting;
    9731214
    9741215                                        if ( ! sidebarsWidgetsControl ) {
    9751216                                                return;
     
    9841225                                        sidebarWidgetIds.splice( i, 1 );
    9851226                                        sidebarsWidgetsControl.setting( sidebarWidgetIds );
    9861227
     1228                                        // Add widget to inactive widgets.
     1229                                        inactiveWidgetSetting = api( 'sidebars_widgets[wp_inactive_widgets]' );
     1230                                        inactiveWidgets = inactiveWidgetSetting();
     1231                                        inactiveWidgets.push( self.params.widget_id );
     1232                                        inactiveWidgetSetting.set( [] ).set( _( inactiveWidgets ).unique() );
     1233
    9871234                                        $adjacentFocusTarget.focus(); // keyboard accessibility
     1235
     1236                                        // Remove display none in case its added again.
     1237                                        self.container.show();
    9881238                                } );
    9891239                        } );
    9901240
    991                         replaceDeleteWithRemove = function() {
    992                                 $removeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Delete"
    993                                 $removeBtn.attr( 'title', l10n.removeBtnTooltip );
    994                         };
    995 
    996                         if ( this.params.is_new ) {
    997                                 api.bind( 'saved', replaceDeleteWithRemove );
    998                         } else {
    999                                 replaceDeleteWithRemove();
    1000                         }
     1241                        $closeBtn.text( l10n.removeBtnLabel ); // wp_widget_control() outputs the link as "Close"
     1242                        $closeBtn.attr( 'title', l10n.removeBtnTooltip );
    10011243                },
    10021244
    10031245                /**
     
    11561398                        params.nonce = api.settings.nonce['update-widget'];
    11571399                        params.customize_theme = api.settings.theme.stylesheet;
    11581400                        params.customized = wp.customize.previewer.query().customized;
     1401                        params.delete_widget = ( false === self.setting() ) ? 1 : 0;
    11591402
    11601403                        data = $.param( params );
    11611404                        $inputs = this._getInputs( $widgetContent );
     
    11691412
    11701413                        if ( instanceOverride ) {
    11711414                                data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instanceOverride ) } );
    1172                         } else {
     1415                        } else if ( $inputs.length ) {
    11731416                                data += '&' + $inputs.serialize();
    11741417                        }
     1418
    11751419                        data += '&' + $widgetContent.find( '~ :input' ).serialize();
    11761420
    11771421                        if ( this._previousUpdateRequest ) {
     
    12521496                                         * preview finishing loading.
    12531497                                         */
    12541498                                        isChanged = ! isLiveUpdateAborted && ! _( self.setting() ).isEqual( r.data.instance );
    1255                                         if ( isChanged ) {
     1499                                        if ( isChanged || false === self.setting() ) {
    12561500                                                self.isWidgetUpdating = true; // suppress triggering another updateWidget
    12571501                                                self.setting( r.data.instance );
    12581502                                                self.isWidgetUpdating = false;
     
    15341778                        }
    15351779
    15361780                        $moveWidgetArea.toggleClass( 'active', showOrHide );
     1781                        self.toggleSaveForLater();
    15371782                },
    15381783
    15391784                /**
     1785                 * Open widget move area if control is not active already.
     1786                 *
     1787                 * @since 4.8.0
     1788                 */
     1789                openWidgetMoveArea: function() {
     1790                        var self = this, $moveWidgetArea;
     1791
     1792                        $moveWidgetArea = this.container.find( '.move-widget-area' );
     1793
     1794                        if( ! $moveWidgetArea.hasClass( 'active' ) ) {
     1795                                self.toggleWidgetMoveArea();
     1796                        }
     1797                },
     1798
     1799                /**
     1800                 * Toggle visibility of the Save For Later/Inactive Widget Sidebar section.
     1801                 *
     1802                 * @since 4.8.0
     1803                 */
     1804                toggleSaveForLater: function() {
     1805                        var $moveWidgetArea, $saveForLater;
     1806
     1807                        $moveWidgetArea = this.container.find( '.move-widget-area' );
     1808                        $saveForLater = this.container.find( 'li[data-id="wp_inactive_widgets"]' );
     1809
     1810                        if( $moveWidgetArea.hasClass( 'active' ) ) {
     1811                                $saveForLater.show();
     1812                        } else {
     1813                                $saveForLater.hide();
     1814                        }
     1815                },
     1816
     1817                /**
    15401818                 * Highlight the widget control and section
    15411819                 */
    15421820                highlightSectionAndControl: function() {
     
    17572035                                                        return;
    17582036                                                }
    17592037
    1760                                                 removedControl = api.Widgets.getWidgetFormControlForWidget( removedWidgetId );
    1761 
    1762                                                 // Detect if widget control was dragged to another sidebar
    1763                                                 wasDraggedToAnotherSidebar = removedControl && $.contains( document, removedControl.container[0] ) && ! $.contains( self.$sectionContent[0], removedControl.container[0] );
    1764 
    1765                                                 // Delete any widget form controls for removed widgets
    1766                                                 if ( removedControl && ! wasDraggedToAnotherSidebar ) {
    1767                                                         api.control.remove( removedControl.id );
    1768                                                         removedControl.container.remove();
    1769                                                 }
    1770 
    17712038                                                // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
    17722039                                                // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
    1773                                                 if ( api.Widgets.savedWidgetIds[removedWidgetId] ) {
     2040                                                if ( api.Widgets.savedWidgetIds[removedWidgetId] && 'sidebars_widgets[wp_inactive_widgets]' !== self.id ) {
    17742041                                                        inactiveWidgets = api.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
    17752042                                                        inactiveWidgets.push( removedWidgetId );
    17762043                                                        api.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactiveWidgets ).unique() );
     
    20392306                                setting.set( {} ); // mark dirty, changing from '' to {}
    20402307                        }
    20412308
    2042                         controlConstructor = api.controlConstructor[controlType];
    2043                         widgetFormControl = new controlConstructor( settingId, {
    2044                                 params: {
    2045                                         settings: {
    2046                                                 'default': settingId
     2309                        widgetFormControl = api.control( settingId );
     2310                        if ( ! widgetFormControl ) {
     2311
     2312                                controlConstructor = api.controlConstructor[controlType];
     2313                                widgetFormControl = new controlConstructor( settingId, {
     2314                                        params: {
     2315                                                settings: {
     2316                                                        'default': settingId
     2317                                                },
     2318                                                content: controlContainer,
     2319                                                sidebar_id: self.params.sidebar_id,
     2320                                                widget_id: widgetId,
     2321                                                widget_id_base: widget.get( 'id_base' ),
     2322                                                type: controlType,
     2323                                                is_new: ! isExistingWidget,
     2324                                                width: widget.get( 'width' ),
     2325                                                height: widget.get( 'height' ),
     2326                                                is_wide: widget.get( 'is_wide' ),
     2327                                                active: true
    20472328                                        },
    2048                                         content: controlContainer,
    2049                                         sidebar_id: self.params.sidebar_id,
    2050                                         widget_id: widgetId,
    2051                                         widget_id_base: widget.get( 'id_base' ),
    2052                                         type: controlType,
    2053                                         is_new: ! isExistingWidget,
    2054                                         width: widget.get( 'width' ),
    2055                                         height: widget.get( 'height' ),
    2056                                         is_wide: widget.get( 'is_wide' ),
    2057                                         active: true
    2058                                 },
    2059                                 previewer: self.setting.previewer
    2060                         } );
    2061                         api.control.add( settingId, widgetFormControl );
     2329                                        previewer: self.setting.previewer
     2330                                } );
     2331                                api.control.add( settingId, widgetFormControl );
     2332                        }
    20622333
    20632334                        // Make sure widget is removed from the other sidebars
    20642335                        api.each( function( otherSetting ) {
     
    20742345                                        i = _.indexOf( otherSidebarWidgets, widgetId );
    20752346
    20762347                                if ( -1 !== i ) {
    2077                                         otherSidebarWidgets.splice( i );
     2348                                        otherSidebarWidgets.splice( i, 1 );
    20782349                                        otherSetting( otherSidebarWidgets );
    20792350                                }
    20802351                        } );
  • src/wp-admin/css/customize-controls.css

     
    19711971        #available-menu-items-search .search-icon {
    19721972                top: 85px; /* 70 section title height + 13 container padding +1 input margin +1 input border */
    19731973        }
     1974
     1975        .wp-customizer #available-widgets .saved-widget-controls {
     1976                visibility: visible;
     1977        }
    19741978}
  • src/wp-admin/css/customize-widgets.css

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