Wordpress - How to influence the information displayed on widget inside wp-admin

Background:

The reason why filtering with dynamic_sidebar_params doesn't work with HTML is because WordPress strips HTML from Widget Heading in wp_widget_control() function like this:

$widget_title = esc_html( strip_tags( $sidebar_args['widget_name'] ) );

WordPress also strips HTML in default JavaScript in wp-admin/js/widgets.js

So without a customised solution, there is no default filter or option either with PHP or with JavaScript to achieve what you want.

Custom PHP Solution:

A custom PHP solution is possible that'll only work in wp-admin -> Appearance -> Widgets, but not in Customizer -> Widgets.

WordPress adds wp_widget_control() function (that generates Widget Control UI) with dynamic_sidebar_params hook, so it's possible to override it using this filter hook. However, in customizer, WordPress calls wp_widget_control() function directly, so this solution will not work for the customizer.

The solution works like the following (add this code in a custom plugin):

add_filter( 'dynamic_sidebar_params', 'wpse261042_list_widget_controls_dynamic_sidebar', 20 );
function wpse261042_list_widget_controls_dynamic_sidebar( $params ) {
    global $wp_registered_widgets;
    // we only want this in wp-admin (may need different check to enable page builder)
    if( is_admin() ) {
        if ( is_array( $params ) && is_array( $params[0] ) && $params[0]['id'] !== 'wp_inactive_widgets' ) {
            $widget_id = $params[0]['widget_id'];
            if ( $wp_registered_widgets[$widget_id]['callback'] ===  'wp_widget_control' ) {
                // here we are replacing wp_widget_control() function 
                // with our custom wpse261042_widget_control() function
                $wp_registered_widgets[$widget_id]['callback'] = 'wpse261042_widget_control';
            }
        }
    }
    return $params;
}

function wpse261042_widget_control( $sidebar_args ) {
    // here copy everything from the core wp_widget_control() function
    // and change only the part related to heading as you need 
} 

As I said before, this solution doesn't work for the customizer and more likely to require future updates as we are overriding a core function.

Custom JavaScript Solution (Recommended):

Fortunately, it's possible to customize this behaviour with JavaScript. WordPress updates Widget Control Heading with JavaScript anyway. To do that WordPress keeps a placeholder in-widget-title CSS class and updates it with the widget title field value from JavaScript CODE. We can manipulate this to achieve our goal.

Related Core JS files:

First you need to know that WordPress loads wp-admin/js/customize-widgets.js file (with customize-widgets handle) in wp-admin -> Customize -> Widgets (customizer) and wp-admin/js/widgets.js file (with admin-widgets handle) in wp-admin -> Appearance -> Widgets to manipulate Widget Control UI. These two files do similar operations for Widget UI markups and Widget Heading UI manipulation, but a lot of different things as well. So we need to consider that for our JavaScript based solution.

Considerations for Customizer:

Customizer doesn't load Widget UI markup immediately after page load, instead, it loads with JavaScript when the corresponding Widgets -> Sidebar panel is open. So we need to manipulate the Widget UI after WordPress loads it. For example, since customizer CODE is event based, I've used the following line of CODE to set the event handler at the correct moment:

section.expanded.bind( onExpanded );

Also, customizer used AJAX to load changes immediately, that's why I've used the following line to tap into the data change:

control.setting.bind( updateTitle );

Also, I needed to tap into widget-added event with the following line of CODE:

$( document ).on( 'widget-added', add_widget );

Common for Customizer & wp-admin -> Appearance -> Widgets:

Both the above mentioned JavaScript files trigger widget-updated event when an widget is updated. Although customizer does it immediately with AJAX, while traditional Widget admin does it after you Click the Save button. I've used the following line of CODE for this:

$( document ).on( 'widget-updated', modify_widget );

Considerations for wp-admin -> Appearance -> Widgets:

Contrary to the customizer, traditional Widgets admin loads the Widget Control UI with PHP, so I've traversed the UI HTML to make the initial changes like this:

$( '#widgets-right div.widgets-sortables div.widget' ).each( function() { // code } ); 

Custom Plugin CODE:

Following is a complete Plugin with JavaScript based solution that will work both in wp-admin -> Appearance -> Widgets and Customizer -> Widgets:

wpse-widget-control.php Plugin PHP file:

<?php
/**
 *  Plugin Name: Widget Control
 *  Plugin URI: https://wordpress.stackexchange.com/questions/261042/how-to-influence-the-information-displayed-on-widget-inside-wp-admin
 *  Description: Display additional info on Widget Heading in wp-admin & customizer using JS
 *  Author: Fayaz
 *  Version: 1.0
 *  Author URI: http://fmy.me/
 */

    if( is_admin() ) {
        add_action( 'current_screen', 'wpse261042_widget_screen' );
    }

    function wpse261042_widget_screen() {
        $currentScreen = get_current_screen();
        if( $currentScreen->id === 'customize' ) {
            add_action( 'customize_controls_enqueue_scripts', 'wpse261042_customizer_widgets', 99 );
        }
        else if( $currentScreen->id === 'widgets' ) {
            add_action( 'admin_enqueue_scripts', 'wpse261042_admin_widgets', 99 );
        }
    }

    function wpse261042_customizer_widgets() {
        wp_enqueue_script( 'custom-widget-heading', plugin_dir_url( __FILE__ ) . 'custom-widget-heading.js', array( 'jquery', 'customize-widgets' ) );
    }

    function wpse261042_admin_widgets() {
        wp_enqueue_script( 'custom-widget-heading', plugin_dir_url( __FILE__ ) . 'custom-widget-heading.js', array( 'jquery', 'admin-widgets' ) );
    }

custom-widget-heading.js JavaScript file:

(function( wp, $ ) {
    var compare = {
        // field to compare
        field: 'title',
        // value to be compared with
        value: 'yes',
        // heading if compare.value matches with compare.field value
        heading: ' <i>(mobile/desktop)</i> ',
        // alternate heading
        alt_heading: ' <i>(desktop only)</i> ',
        // WP adds <span class="in-widget-title"></span> in each widget heading by default
        heading_selector: '.in-widget-title'
    };

    var widgetIdSelector = '> .widget-inside > form > .widget-id';
    // heading is added as prefix of already existing heading, modify this as needed
    function modify_heading( $elm, isMain ) {
        var html = $elm.html();
        if ( html.indexOf( compare.heading ) == -1 && html.indexOf( compare.alt_heading ) == -1 ) {
            if( isMain ) {
                $elm.html( compare.heading + html );
            }
            else {
                $elm.html( compare.alt_heading + html );
            }
        }
    };
    function parseFieldSelector( widgetId ) {
        return 'input[name="' + widgetIdToFieldPrefix( widgetId ) + '[' + compare.field + ']"]';
    };

    // @note: change this function if you don't want custom Heading change to appear for all the widgets.
    // If field variable is empty, then that means that field doesn't exist for that widget.
    // So use this logic if you don't want the custom heading manipulation if the field doesn't exist for a widget
    function modify_widget( evt, $widget, content ) {
        var field = null;
        var field_value = '';
        if( content ) {
            field = $( content ).find( parseFieldSelector( $widget.find( widgetIdSelector ).val() ) );
        }
        else {
            field = $widget.find( parseFieldSelector( $widget.find( widgetIdSelector ).val() ) );
        }
        if( field ) {
            field_value = ( ( field.attr( 'type' ) != 'radio' && field.attr( 'type' ) != 'checkbox' )
                          || field.is( ':checked' ) ) ? field.val() : '';
        }
        modify_heading( $widget.find( compare.heading_selector ), field_value == compare.value );
    }

    function parseWidgetId( widgetId ) {
        var matches, parsed = {
            number: null,
            id_base: null
        };
        matches = widgetId.match( /^(.+)-(\d+)$/ );
        if ( matches ) {
            parsed.id_base = matches[1];
            parsed.number = parseInt( matches[2], 10 );
        } else {
            parsed.id_base = widgetId;
        }
        return parsed;
    }
    function widgetIdToSettingId( widgetId ) {
        var parsed = parseWidgetId( widgetId ), settingId;
        settingId = 'widget_' + parsed.id_base;
        if ( parsed.number ) {
            settingId += '[' + parsed.number + ']';
        }
        return settingId;
    }
    function widgetIdToFieldPrefix( widgetId ) {
        var parsed = parseWidgetId( widgetId ), settingId;
        settingId = 'widget-' + parsed.id_base;
        if ( parsed.number ) {
            settingId += '[' + parsed.number + ']';
        }
        return settingId;
    }
    var api = wp.customize;
    if( api ) {
        // We ate in the customizer
        widgetIdSelector = '> .widget-inside > .form > .widget-id';
        api.bind( 'ready', function() {
            function add_widget( evt, $widget ) {
                var control;
                control = api.control( widgetIdToSettingId( $widget.find( widgetIdSelector ).val() ) );

                function updateTitle( evt ) {
                    modify_widget( null, $widget );
                };
                if ( control ) {
                    control.setting.bind( updateTitle );
                }
                updateTitle();
            };
            api.control.each( function ( control ) {
                if( control.id &&  0 === control.id.indexOf( 'widget_' ) ) {
                    api.section( control.section.get(), function( section ) {
                        function onExpanded( isExpanded ) {
                            if ( isExpanded ) {
                                section.expanded.unbind( onExpanded );
                                modify_widget( null, control.container.find( '.widget' ), control.params.widget_content );
                            }
                        };
                        if ( section.expanded() ) {
                            onExpanded( true );
                        } else {
                            section.expanded.bind( onExpanded );
                        }
                    } );
                }
            } );
            $( document ).on( 'widget-added', add_widget );
        } );
    }
    else {
        // We are in wp-admin -> Appearance -> Widgets
        // Use proper condition and CODE if you want to target any page builder
        // that doesn't use WP Core Widget Markup or Core JavaScript
        $( window ).on( 'load', function() {
            $( '#widgets-right div.widgets-sortables div.widget' ).each( function() {
                modify_widget( 'non-customizer', $( this ) );
            } );
            $( document ).on( 'widget-added', modify_widget );
        } );
    }
    $( document ).on( 'widget-updated', modify_widget );
})( window.wp, jQuery );

Plugin Usage:

Note: With the above sample Plugin CODE as it is, if you set the title of a Widget to yes, then it'll show (mobile/desktop) within the Widget Control UI heading, all the other Widget's will have (desktop only) in the Heading. In the customizer the change will be immediate, in wp-admin -> widgets the change will show after you save the changes. Of course you can change this behaviour by modifying the CODE (in JavaScript) to do the heading change for a different field_name or to only show that specific heading to some widgets and not to all of them.

For example, say you have a field named use_mobile, and you want to set the heading to (mobile/desktop) when it's set to yes. Then set the compare variable to something like:

var compare = {
    field: 'use_mobile',
    value: 'yes',
    heading: ' <i>(mobile/desktop)</i> ',
    alt_heading: ' <i>(desktop only)</i> ',
    heading_selector: '.in-widget-title'
};

Also, if you want to change the entire heading (instead of just .in-widget-title), then you may change the heading_selector setting along with the correct markup for heading & alt_heading to do so. The possibilities are endless, but make sure WordPress core CODE doesn't produce any error if you want to be too much creative with the resulting markup.

Page builder integration:

Whether or not either of these solutions will work for a page builder will depend on that page builder implementation. If it uses WordPress supplied methods to load Widget Control UI then it should work without any modification, otherwise similar (but modified) implication may be possible for page builders as well.


Let's first see whether it is possible to change the information that is displayed in the widget titles in admin. This list is generated by wp_list_widget_controls, which calls on dynamic_sidebar, which contains a filter dynamic_sidebar_params to change the parameters in the controls, including the title. Let's try that:

add_filter ('dynamic_sidebar_params','wpse261042_change_widget_title');
function wpse261042_change_widget_title ($params) {
  $string = ' Added info';
  $params[0]['widget_name'] .= $string;
  return $params;
  }

The $string is not exactly in the place where you point at, but I'd say it's good enough.

Now, we need to replace $string with some information from inside the current widget. Luckily, we know which widget we are in, because $params also contains the widget_id. I'll refer to this answer for an explanation how you use the widget_id to retrieve widget data. Here we go:

 // we need to separate the widget name and instance from the id
 $widget_id = $params[0]['widget_id'];
 $widget_instance = strrchr ($widget_id, '-');
 $wlen = strlen ($widget_id);
 $ilen = strlen ($widget_instance);
 $widget_name = substr ($widget_id,0,$wlen-$ilen);
 $widget_instance = substr ($widget_instance,1);
 // get the data
 $widget_instances = get_option('widget_' . $widget_name);
 $data = $widget_instances[$widget_instance];

Now the array $data contains the instances of the widget and you can choose which one you want to pass to $string in the function.