WordPress.org

Make WordPress Core

Ticket #32417: 32417.17.diff

File 32417.17.diff, 29.4 KB (added by gonom9, 15 months ago)

Explicitly refresh widget partial when attachment has any changes.

  • src/wp-admin/css/customize-widgets.css

    diff --git a/src/wp-admin/css/customize-widgets.css b/src/wp-admin/css/customize-widgets.css
    index 687c9f8..c859240 100644
    a b body.adding-widget #customize-preview { 
    346346#available-widgets [class*="event"] .widget-title:before,
    347347#available-widgets [class*="calendar"] .widget-title:before { content: "\f145"; top: -4px;}
    348348
     349/* media */
     350#available-widgets [class*="media"] .widget-title:before {
     351        content: "\f104";
     352}
     353
    349354/* format-image */
    350355#available-widgets [class*="image"] .widget-title:before,
    351356#available-widgets [class*="photo"] .widget-title:before,
  • src/wp-includes/class-wp-customize-manager.php

    diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php
    index 229a248..e913d7a 100644
    a b final class WP_Customize_Manager { 
    18071807                                'stylesheet' => $this->get_stylesheet(),
    18081808                                'active'     => $this->is_theme_active(),
    18091809                        ),
     1810                        'media' => array(
     1811                                /** This filter is documented in wp-includes/media.php */
     1812                                'audioLibrary' => apply_filters( 'wp_audio_shortcode_library', 'mediaelement' ),
     1813                                /** This filter is documented in wp-includes/media.php */
     1814                                'videoLibrary' => apply_filters( 'wp_video_shortcode_library', 'mediaelement' ),
     1815                        ),
    18101816                        'url' => array(
    18111817                                'self' => $self_url,
    18121818                                'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
  • new file src/wp-includes/css/wp-media-widget.css

    diff --git a/src/wp-includes/css/wp-media-widget.css b/src/wp-includes/css/wp-media-widget.css
    new file mode 100644
    index 0000000..d36ec25
    - +  
     1.media-widget-preview .button {
     2        text-align: center
     3}
     4
     5.media-widget-preview .wp-caption {
     6        max-width: 100%;
     7        margin: 0;
     8}
     9
     10.media-widget-preview .image {
     11        height: auto;
     12        max-width: 100%;
     13        cursor: pointer;
     14}
     15
     16.media-widget-preview .aligncenter {
     17        display: block;
     18        margin: 0 auto;
     19        text-align: center
     20}
  • src/wp-includes/default-widgets.php

    diff --git a/src/wp-includes/default-widgets.php b/src/wp-includes/default-widgets.php
    index 0cf5fc3..e803135 100644
    a b require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-tag-cloud.php' ); 
    4545
    4646/** WP_Nav_Menu_Widget class */
    4747require_once( ABSPATH . WPINC . '/widgets/class-wp-nav-menu-widget.php' );
     48
     49/** WP_Widget_Media class */
     50require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media.php' );
  • src/wp-includes/js/customize-selective-refresh.js

    diff --git a/src/wp-includes/js/customize-selective-refresh.js b/src/wp-includes/js/customize-selective-refresh.js
    index 4f71656..d1a7780 100644
    a b wp.customize.selectiveRefresh = ( function( $, api ) { 
    398398                 * @returns {boolean} Whether the rendering was successful and the fallback was not invoked.
    399399                 */
    400400                renderContent: function( placement ) {
    401                         var partial = this, content, newContainerElement;
     401                        var partial = this, content, mediaSettings = api.settings.media, newContainerElement;
    402402                        if ( ! placement.container ) {
    403403                                partial.fallback( new Error( 'no_container' ), [ placement ] );
    404404                                return false;
    wp.customize.selectiveRefresh = ( function( $, api ) { 
    452452                                        placement.container.html( content );
    453453                                }
    454454
     455                                // Auto-initialize media elements when they are contained in the placement
     456                                if ( $( 'audio, video', placement.container ).length > 0 ) {
     457                                        if ( wp.mediaelement && wp.mediaelement.initialize && _.contains( [ mediaSettings.audioLibrary, mediaSettings.videoLibrary ], 'mediaelement' ) ) {
     458                                                wp.mediaelement.initialize();
     459                                        }
     460                                }
     461
    455462                                placement.container.removeClass( 'customize-render-content-error' );
    456463                        } catch ( error ) {
    457464                                if ( 'undefined' !== typeof console && console.error ) {
    wp.customize.selectiveRefresh = ( function( $, api ) { 
    10281035                        } );
    10291036                } );
    10301037
     1038                api.preview.bind( 'refresh-partial', function receiveRefreshPartialMessage( partialId ) {
     1039                        var partial = self.partial( partialId );
     1040                        if ( partial ) {
     1041                                partial.refresh();
     1042                        } else {
     1043                                api.preview.send( 'refresh' );
     1044                        }
     1045                } );
    10311046        } );
    10321047
    10331048        return self;
  • new file src/wp-includes/js/wp-media-widget.js

    diff --git a/src/wp-includes/js/wp-media-widget.js b/src/wp-includes/js/wp-media-widget.js
    new file mode 100644
    index 0000000..e4d9358
    - +  
     1/**
     2 * @since 4.8.0
     3 *
     4 * @package WP_Media_Widget
     5 */
     6( function( $, l10n ) {
     7        'use strict';
     8
     9        var frame = {
     10                defaultProps: {
     11                        id:    '',
     12                        align: '',
     13                        size:  '',
     14                        link:  ''
     15                },
     16
     17                /**
     18                 * Init.
     19                 *
     20                 * @returns {void}
     21                 */
     22                init: function() {
     23                        frame.bindEvent();
     24                        wp.mediaelement.initialize();
     25                },
     26
     27                /**
     28                 * Bind event.
     29                 *
     30                 * @param {jQuery} context Element.
     31                 * @returns {void}
     32                 */
     33                bindEvent: function( context ) {
     34                        $( '.button.select-media, .image', context || '.media-widget-preview' )
     35                                .off( 'click.mediaWidget' )
     36                                .on( 'click.mediaWidget', frame.openMediaManager );
     37                },
     38
     39                /**
     40                 * Get current selection of media.
     41                 *
     42                 * @param {String} widgetId Widget ID.
     43                 * @returns {wp.media.models.Selection|null} Selection or null if no current selection.
     44                 */
     45                getSelection: function( widgetId ) {
     46                        var ids, selection;
     47                        ids = $( '#widget-' + widgetId + '-id' ).val();
     48
     49                        if ( ! ids ) {
     50                                return null;
     51                        }
     52
     53                        selection = ids.split( ',' ).reduce( function( list, id ) {
     54                                var attachment = wp.media.attachment( id );
     55                                if ( id && attachment ) {
     56                                        list.push( attachment );
     57                                }
     58                                return list;
     59                        }, [] );
     60
     61                        return new wp.media.model.Selection( selection );
     62                },
     63
     64                /**
     65                 * Open media manager.
     66                 *
     67                 * @param {jQuery.Event} event Event.
     68                 * @returns {void}
     69                 */
     70                openMediaManager: function( event ) {
     71                        var widgetFrame, widgetId, selection, prevAttachmentId;
     72
     73                        widgetId = $( event.target ).data( 'id' );
     74                        selection = frame.getSelection( widgetId );
     75
     76                        if ( selection.length > 0 ) {
     77                                prevAttachmentId = selection.first().get('id');
     78                        }
     79
     80                        // Create the media frame.
     81                        widgetFrame = wp.media( {
     82                                button: {
     83                                        text: translate( 'addToWidget', 'Add to widget' ) // Text of the submit button.
     84                                },
     85
     86                                states: new wp.media.controller.Library( {
     87                                        library:    wp.media.query( { type: [ 'image', 'audio', 'video' ] } ),
     88                                        title:      translate( 'selectMedia', 'Select Media' ), // Media frame title
     89                                        selection:  selection,
     90                                        multiple:   false,
     91                                        priority:   20,
     92                                        display:    true, // Attachment display setting
     93                                        filterable: 'all'
     94                                } )
     95                        } );
     96
     97                        // Render the attachment details.
     98                        widgetFrame.on( 'select', function() {
     99                                var attachment, props;
     100
     101                                attachment = frame.getFirstAttachment( widgetFrame );
     102                                props = frame.getDisplayProps( widgetFrame );
     103
     104                                // Only try to render the attachment details if a selection was made.
     105                                if ( props && attachment && prevAttachmentId !== attachment.id ) {
     106                                        frame.renderFormView( widgetId, props, attachment );
     107                                }
     108                        } );
     109
     110                        /*
     111                         * Try to render the form only if the selection doesn't change.
     112                         * This ensures that changes of props will reflect in the form and the preview
     113                         * even when user doesn't click the Add button.
     114                         */
     115                        widgetFrame.on( 'close', function() {
     116                                var attachment, props;
     117
     118                                attachment = frame.getFirstAttachment( widgetFrame );
     119
     120                                if ( attachment && prevAttachmentId && prevAttachmentId === attachment.id ) {
     121                                        props = frame.getDisplayProps( widgetFrame );
     122                                        frame.renderFormView( widgetId, props, attachment );
     123                                }
     124                        } );
     125
     126                        widgetFrame.open( widgetId );
     127                },
     128
     129                /**
     130                 * Get the first attachment of the selection in the widget frame.
     131                 *
     132                 * @param {wp.media.view.MediaFrame} widgetFrame Widget frame
     133                 * @return {object|null} JSON object of the attachment if it exists, otherwise null
     134                 */
     135                getAttachment: function( widgetFrame ) {
     136                        var selection = widgetFrame.state().get( 'selection' );
     137
     138                        if ( 0 === selection.length ) {
     139                                return null;
     140                        }
     141
     142                        return selection.first().toJSON();
     143                },
     144
     145                /**
     146                 * Get display props of the current selection from the widget frame.
     147                 *
     148                 * @param {wp.media.view.MediaFrame} widgetFrame Widget frame
     149                 * @return {object|null} JSON object of the props if possible, otherwise null
     150                 */
     151                 getDisplayProps: function( widgetFrame ) {
     152                        if ( 0 === widgetFrame.state().get( 'selection' ).length ) {
     153                                return null;
     154                        }
     155
     156                        return widgetFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON();
     157                 },
     158
     159                /**
     160                 * Renders the attachment details from the media modal into the widget.
     161                 *
     162                 * @param {String} widgetId Widget ID.
     163                 * @param {Object} props Attachment Display Settings (align, link, size, etc).
     164                 * @param {Object} attachment Attachment Details (title, description, caption, url, sizes, etc).
     165                 * @returns {void}
     166                 */
     167                renderFormView: function( widgetId, props, attachment ) {
     168                        var formView, serializedAttachment;
     169
     170                        // Start with container elements for the widgets page, customizer controls, and customizer preview.
     171                        formView = $( '.' + widgetId + ', #customize-control-widget_' + widgetId + ', #' + widgetId );
     172
     173                        // Bail if there is no target form
     174                        if ( ! formView.length ) {
     175                                return;
     176                        }
     177
     178                        _.extend( attachment, _.pick( props, 'link', 'size' ) );
     179
     180                        // Show/hide the widget description
     181                        formView.find( '.attachment-description' )
     182                                .toggleClass( 'hidden', ! attachment.description )
     183                                .html( attachment.description );
     184
     185                        // Set the preview content and apply responsive styles to the media.
     186                        formView.find( '.media-widget-admin-preview' )
     187                                .html( frame.renderMediaElement( widgetId, props, attachment ) )
     188                                .find( '.wp-video, .wp-caption' ).css( 'width', '100%' ).end()
     189                                .find( 'img.image' ).css( { width: '100%', height: 'auto' } );
     190
     191                        if ( _.contains( [ 'audio', 'video' ], attachment.type ) ) {
     192                                wp.mediaelement.initialize();
     193                        }
     194
     195                        frame.bindEvent( formView );
     196
     197                        // Populate form fields with selection data from the media frame.
     198                        _.each( _.keys( frame.defaultProps ), function( key ) {
     199                                formView.find( '#widget-' + widgetId + '-' + key ).val( attachment[ key ] || props[ key ] ).trigger( 'change' );
     200                        } );
     201
     202                        /*
     203                         * Force the widget's partial in the preview to refresh even when the instance was not changed.
     204                         * This ensures that changes to attachment's caption or description will be shown in the
     205                         * preview since these are not in the widget's instance state.
     206                         */
     207                        serializedAttachment = JSON.stringify( _.pick( attachment, 'id', 'title', 'caption', 'link', 'size' ) );
     208                        if ( formView.data( 'attachment' ) !== serializedAttachment && wp.customize && wp.customize.previewer ) {
     209                                wp.customize.previewer.send( 'refresh-partial', 'widget[' + widgetId + ']' );
     210                                formView.data( 'attachment', serializedAttachment );
     211                        }
     212
     213                        // Change button text
     214                        formView.find( frame.buttonId ).text( translate( 'changeMedia', 'Change Media' ) );
     215                },
     216
     217                /**
     218                 * Renders the media attachment in HTML.
     219                 *
     220                 * @param {String} widgetId Widget ID.
     221                 * @param {Object} props Attachment Display Settings (align, link, size, etc).
     222                 * @param {Object} attachment Attachment Details (title, description, caption, url, sizes, etc).
     223                 *
     224                 * @returns {String} Render media element.
     225                 */
     226                renderMediaElement: function( widgetId, props, attachment ) {
     227                        var type, renderer;
     228                        type = attachment.type || '';
     229                        renderer = 'render' + type.charAt( 0 ).toUpperCase() + type.slice( 1 );
     230
     231                        if ( 'function' === typeof frame[ renderer ] ) {
     232                                return frame[renderer]( widgetId, props, attachment );
     233                        }
     234
     235                        // In case no renderer found
     236                        return '';
     237                },
     238
     239                /**
     240                 * Renders the image attachment
     241                 *
     242                 * @param {String} widgetId Widget ID.
     243                 * @param {Object} props Attachment Display Settings (align, link, size, etc).
     244                 * @param {Object} attachment Attachment Details (title, description, caption, url, sizes, etc).
     245                 *
     246                 * @returns {String} Rendered image.
     247                 */
     248                renderImage: function( widgetId, props, attachment ) {
     249                        var image = $( '<img />' )
     250                                .addClass( 'image wp-image' + attachment.id )
     251                                .attr( {
     252                                        'data-id': widgetId,
     253                                        src:       attachment.sizes[ props.size ].url,
     254                                        title:     attachment.title,
     255                                        alt:       attachment.alt,
     256                                        width:     attachment.sizes[ props.size ].width,
     257                                        height:    attachment.sizes[ props.size ].height
     258                                } );
     259
     260                        if ( attachment.caption ) {
     261                                image = $( '<figure />' )
     262                                        .width( attachment.sizes[ props.size ].width )
     263                                        .addClass( 'wp-caption' )
     264                                        .attr( 'id', widgetId + '-caption' )
     265                                        .append( image );
     266
     267                                $( '<figcaption class="wp-caption-text" />' ).text( attachment.caption ).appendTo( image );
     268                        }
     269
     270                        return image.wrap( '<div />' ).parent().html();
     271                },
     272
     273                /**
     274                 * Renders the audio attachment.
     275                 *
     276                 * @param {String} widgetId Widget ID.
     277                 * @param {Object} props Attachment Display Settings (align, link, size, etc).
     278                 * @param {Object} attachment Attachment Details (title, description, caption, url, sizes, etc).
     279                 *
     280                 * @returns {String} Rendered audio.
     281                 */
     282                renderAudio: function( widgetId, props, attachment ) {
     283                        if ( 'embed' === props.link ) {
     284                                return wp.media.template( 'wp-media-widget-audio' )( {
     285                                        model: {
     286                                                src:    attachment.url
     287                                        }
     288                                } );
     289                        }
     290
     291                        return wp.html.string( {
     292                                tag: 'a',
     293                                content: attachment.title,
     294                                attrs: {
     295                                        href: '#'
     296                                }
     297                        } );
     298                },
     299
     300                /**
     301                 * Renders the video attachment.
     302                 *
     303                 * @param {String} widgetId Widget ID.
     304                 * @param {Object} props Attachment Display Settings (align, link, size, etc).
     305                 * @param {Object} attachment Attachment Details (title, description, caption, url, sizes, etc).
     306                 *
     307                 * @returns {String} Rendered video.
     308                 */
     309                renderVideo: function( widgetId, props, attachment ) {
     310                        if ( 'embed' === props.link ) {
     311                                return wp.media.template( 'wp-media-widget-video' )( {
     312                                        model: {
     313                                                src:    attachment.url,
     314                                                width:  attachment.width,
     315                                                height: attachment.height
     316                                        }
     317                                } );
     318                        }
     319
     320                        return wp.html.string( {
     321                                tag: 'a',
     322                                content: attachment.title,
     323                                attrs: {
     324                                        href: '#'
     325                                }
     326                        } );
     327                }
     328        };
     329
     330        /**
     331         * Translate.
     332         *
     333         * @param {string} key Key.
     334         * @param {string} defaultText Default text.
     335         * @return {string} Translated string.
     336         */
     337        function translate( key, defaultText ) {
     338                return l10n[ key ] || defaultText;
     339        }
     340
     341        $( document )
     342                .ready( frame.init )
     343                .on( 'widget-added widget-updated', frame.init );
     344
     345        window.wp = window.wp || {};
     346        window.wp.MediaWidget = frame;
     347} )( jQuery, window._mediaWidgetL10n || {} );
  • src/wp-includes/script-loader.php

    diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php
    index b2e24d6..8cbddef 100644
    a b function wp_default_scripts( &$scripts ) { 
    761761                $scripts->add( 'media-gallery', "/wp-admin/js/media-gallery$suffix.js", array('jquery'), false, 1 );
    762762
    763763                $scripts->add( 'svg-painter', '/wp-admin/js/svg-painter.js', array( 'jquery' ), false, 1 );
     764
     765                // Media Widget.
     766                $scripts->add( 'wp-media-widget', '/wp-includes/js/wp-media-widget.js', array( 'jquery', 'media-models', 'media-views' ), false, 1 );
     767                did_action( 'init' ) && $scripts->localize( 'wp-media-widget', '_mediaWidgetL10n', array(
     768                        'selectMedia' => __( 'Select Media' ),
     769                        'changeMedia' => __( 'Change Media' ),
     770                        'addToWidget' => __( 'Add to widget' ),
     771                ) );
    764772        }
    765773}
    766774
    function wp_default_styles( &$styles ) { 
    865873        $styles->add( 'media-views',          "/wp-includes/css/media-views$suffix.css", array( 'buttons', 'dashicons', 'wp-mediaelement' ) );
    866874        $styles->add( 'wp-pointer',           "/wp-includes/css/wp-pointer$suffix.css", array( 'dashicons' ) );
    867875        $styles->add( 'customize-preview',    "/wp-includes/css/customize-preview$suffix.css", array( 'dashicons' ) );
     876        $styles->add( 'wp-media-widget',      "/wp-includes/css/wp-media-widget$suffix.css", array( 'media-views' ) );
    868877        $styles->add( 'wp-embed-template-ie', "/wp-includes/css/wp-embed-template-ie$suffix.css" );
    869878        $styles->add_data( 'wp-embed-template-ie', 'conditional', 'lte IE 8' );
    870879
  • src/wp-includes/widgets.php

    diff --git a/src/wp-includes/widgets.php b/src/wp-includes/widgets.php
    index 1abadfb..ad36710 100644
    a b function wp_widgets_init() { 
    14661466
    14671467        register_widget('WP_Nav_Menu_Widget');
    14681468
     1469        register_widget('WP_Widget_Media');
     1470
    14691471        /**
    14701472         * Fires after all default WordPress widgets have been registered.
    14711473         *
  • new file src/wp-includes/widgets/class-wp-widget-media.php

    diff --git a/src/wp-includes/widgets/class-wp-widget-media.php b/src/wp-includes/widgets/class-wp-widget-media.php
    new file mode 100644
    index 0000000..7207d70
    - +  
     1<?php
     2/**
     3 * Widget API: WP_Media_Widget class
     4 *
     5 * @package WordPress
     6 * @subpackage Widgets
     7 * @since 4.8.0
     8 */
     9
     10/**
     11 * Core class that implements a media widget.
     12 *
     13 * @since 4.8.0
     14 *
     15 * @see WP_Widget
     16 */
     17class WP_Widget_Media extends WP_Widget {
     18
     19        /**
     20         * Default instance.
     21         *
     22         * @var array
     23         */
     24        private $default_instance = array(
     25                'id'          => '',
     26                'title'       => '',
     27                'link'        => '',
     28                'align'       => 'none',
     29        );
     30
     31        /**
     32         * Constructor.
     33         *
     34         * @since 4.8.0
     35         * @access public
     36         *
     37         * @param string $id_base         Optional Base ID for the widget, lowercase and unique. If left empty,
     38         *                                a portion of the widget's class name will be used Has to be unique.
     39         * @param string $name            Optional. Name for the widget displayed on the configuration page.
     40         *                                Default empty.
     41         * @param array  $widget_options  Optional. Widget options. See wp_register_sidebar_widget() for
     42         *                                information on accepted arguments. Default empty array.
     43         * @param array  $control_options Optional. Widget control options. See wp_register_widget_control()
     44         *                                for information on accepted arguments. Default empty array.
     45         */
     46        public function __construct( $id_base = '', $name = '', $widget_options = array(), $control_options = array() ) {
     47                $widget_opts = wp_parse_args( $widget_options, array(
     48                        'classname' => 'widget_media',
     49                        'description' => __( 'An image, video, or audio file.' ),
     50                        'customize_selective_refresh' => true,
     51                ) );
     52
     53                $control_opts = wp_parse_args( $control_options, array() );
     54
     55                parent::__construct(
     56                        $id_base ? $id_base : 'wp-media-widget', // @todo This should just be 'media'.
     57                        $name ? $name : __( 'Media' ),
     58                        $widget_opts,
     59                        $control_opts
     60                );
     61
     62                if ( is_customize_preview() ) {
     63                        $this->enqueue_mediaelement_script();
     64                }
     65
     66                add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
     67                add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
     68        }
     69
     70        /**
     71         * Displays the widget on the front-end.
     72         *
     73         * @since 4.8.0
     74         * @access public
     75         *
     76         * @see WP_Widget::widget()
     77         *
     78         * @param array $args     Display arguments including before_title, after_title, before_widget, and after_widget.
     79         * @param array $instance Saved setting from the database.
     80         */
     81        public function widget( $args, $instance ) {
     82                $output = $args['before_widget'];
     83
     84                $instance = array_merge( $this->default_instance, $instance );
     85
     86                if ( $instance['title'] ) {
     87                        $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
     88                        $output .= $args['before_title'] . $title . $args['after_title'];
     89                }
     90
     91                // Render the media.
     92                $attachment = $instance['id'] ? get_post( $instance['id'] ) : null;
     93                if ( $attachment ) {
     94                        $output .= $this->render_media( $attachment, $args['widget_id'], $instance );
     95                        $output .= $this->get_responsive_style( $attachment, $args['widget_id'], $instance );
     96                }
     97
     98                $output .= $args['after_widget'];
     99
     100                echo $output;
     101        }
     102
     103        /**
     104         * Sanitizes the widget form values as they are saved.
     105         *
     106         * @since 4.8.0
     107         * @access public
     108         *
     109         * @see WP_Widget::update()
     110         *
     111         * @param array $new_instance Values just sent to be saved.
     112         * @param array $old_instance Previously saved values from database.
     113         * @return array Updated safe values to be saved.
     114         */
     115        public function update( $new_instance, $old_instance ) {
     116                $instance = $old_instance;
     117
     118                // ID and title.
     119                $instance['id']    = (int) $new_instance['id'];
     120                $instance['title'] = sanitize_text_field( $new_instance['title'] );
     121
     122                // Everything else.
     123                $instance['align'] = sanitize_text_field( $new_instance['align'] );
     124                $instance['size']  = sanitize_text_field( $new_instance['size'] );
     125                $instance['link']  = sanitize_text_field( $new_instance['link'] );
     126
     127                return $instance;
     128        }
     129
     130        /**
     131         * Get type of a media attachment
     132         *
     133         * @since 4.8.0
     134         * @access private
     135         * @todo Why private? What about plugins that extend? Should they be able to easily call the parent method?
     136         *
     137         * @param WP_Post $attachment Attachment object.
     138         * @return String type string such as image, audio and video. Returns empty string for unknown type
     139         */
     140        private function get_typeof_media( $attachment ) {
     141                if ( wp_attachment_is_image( $attachment ) ) {
     142                        return 'image';
     143                }
     144
     145                if ( wp_attachment_is( 'audio', $attachment ) ) {
     146                        return 'audio';
     147                }
     148
     149                if ( wp_attachment_is( 'video', $attachment ) ) {
     150                        return 'video';
     151                }
     152
     153                // Unknown media type.
     154                return '';
     155        }
     156
     157        /**
     158         * Renders a single media attachment
     159         *
     160         * @since 4.8.0
     161         * @access public
     162         *
     163         * @param WP_Post $attachment Attachment object.
     164         * @param string  $widget_id  Widget ID.
     165         * @param array   $instance   Current widget instance arguments.
     166         * @return string
     167         */
     168        public function render_media( $attachment, $widget_id, $instance ) {
     169                $output = '';
     170                $renderer = 'render_' . $this->get_typeof_media( $attachment );
     171
     172                if ( method_exists( $this, $renderer ) ) {
     173                        $output .= call_user_func( array( $this, $renderer ), $attachment, $widget_id, $instance );
     174                }
     175
     176                return $output;
     177        }
     178
     179        /**
     180         * Renders an image attachment preview.
     181         *
     182         * @since 4.8.0
     183         * @access private
     184         *
     185         * @param WP_Post $attachment Attachment object.
     186         * @param string  $widget_id  Widget ID.
     187         * @param array   $instance   Current widget instance arguments.
     188         * @return string
     189         */
     190        private function render_image( $attachment, $widget_id, $instance ) {
     191                $has_caption   = ( ! empty( $attachment->post_excerpt ) );
     192
     193                $img_attrs = array(
     194                        'data-id' => $widget_id,
     195                        'title'   => $attachment->post_title,
     196                        'class'   => 'image wp-image-' . $attachment->ID,
     197                        'style'   => 'width: 100%; height: auto;',
     198                );
     199
     200                if ( ! $has_caption ) {
     201                        $img_attrs['class'] .= ' align' . $instance['align'];
     202                }
     203
     204                $image = wp_get_attachment_image( $attachment->ID, $instance['size'], false, $img_attrs );
     205
     206                if ( ! $has_caption ) {
     207                        return $image;
     208                }
     209
     210                $fig_attrs = array(
     211                        'id'      => $widget_id . '-caption',
     212                        'width'   => get_option( $instance['size'] . '_size_w' ),
     213                        'align'   => $instance['align'],
     214                        'caption' => $attachment->post_excerpt,
     215                );
     216
     217                $figure = img_caption_shortcode( $fig_attrs, $image );
     218
     219                return $figure;
     220        }
     221
     222        /**
     223         * Renders an audio attachment preview.
     224         *
     225         * @since 4.8.0
     226         * @access private
     227         *
     228         * @param WP_Post $attachment Attachment object.
     229         * @param string  $widget_id  Widget ID.
     230         * @param array   $instance   Current widget instance arguments.
     231         * @return string
     232         */
     233        private function render_audio( $attachment, $widget_id, $instance ) {
     234                unset( $widget_id );
     235                if ( in_array( $instance['link'], array( 'file', 'post' ), true ) ) {
     236                        return $this->create_link_for( $attachment, $instance['link'] );
     237                }
     238
     239                return wp_audio_shortcode( array(
     240                        'src' => wp_get_attachment_url( $attachment->ID ),
     241                ) );
     242        }
     243
     244        /**
     245         * Renders a video attachment preview.
     246         *
     247         * @since 4.8.0
     248         * @access private
     249         *
     250         * @param WP_Post $attachment Attachment object.
     251         * @param string  $widget_id  Widget ID.
     252         * @param array   $instance   Current widget instance arguments.
     253         * @return string
     254         */
     255        private function render_video( $attachment, $widget_id, $instance ) {
     256                unset( $widget_id );
     257                if ( in_array( $instance['link'], array( 'file', 'post' ), true ) ) {
     258                        return $this->create_link_for( $attachment, $instance['link'] );
     259                }
     260
     261                return wp_video_shortcode( array(
     262                        'src' => wp_get_attachment_url( $attachment->ID ),
     263                ) );
     264        }
     265
     266        /**
     267         * Get styles for responsifying the widget
     268         *
     269         * @since 4.8.0
     270         * @access private
     271         *
     272         * @param WP_Post $attachment Attachment object.
     273         * @param string  $widget_id  Widget ID.
     274         * @return string styles for responsive media
     275         */
     276        private function get_responsive_style( $attachment, $widget_id ) {
     277                if ( wp_attachment_is( 'audio', $attachment ) ) {
     278                        return;
     279                }
     280
     281                $output = '<style type="text/css">';
     282
     283                if ( wp_attachment_is_image( $attachment ) ) {
     284                        $output .= "#{$widget_id}-caption{ width: 100% !important; }";
     285                }
     286
     287                if ( wp_attachment_is( 'video', $attachment ) ) {
     288                        $output .= "#{$widget_id} .wp-video{ width: 100% !important; }";
     289                }
     290
     291                $output .= '</style>';
     292
     293                return $output;
     294        }
     295
     296
     297        /**
     298         * Creates and returns a link for an attachment
     299         *
     300         * @param WP_Post $attachment Attachment object.
     301         * @param string  $type       link type.
     302         * @return string
     303         */
     304        private function create_link_for( $attachment, $type = '' ) {
     305                $url = '#';
     306                if ( 'file' === $type ) {
     307                        $url = wp_get_attachment_url( $attachment->ID );
     308                } elseif ( 'post' === $type ) {
     309                        $url = get_attachment_link( $attachment->ID );
     310                }
     311
     312                return '<a href="' . esc_url( $url ) . '">' . $attachment->post_title . '</a>';
     313        }
     314
     315        /**
     316         * Outputs the settings update form.
     317         *
     318         * @since 4.8.0
     319         * @access public
     320         *
     321         * @param array $saved_instance Current settings.
     322         * @return void
     323         */
     324        public function form( $saved_instance ) {
     325                $defaults = array(
     326                        'title'  => '',
     327                        // Attachment props.
     328                        'id'     => '',
     329                        'align'  => '',
     330                        'size'   => '',
     331                        'link'   => '',
     332                );
     333
     334                $instance   = wp_parse_args( (array) $saved_instance, $defaults );
     335                $attachment = empty( $instance['id'] ) ? null : get_post( $instance['id'] );
     336                $widget_id  = $this->id;
     337                ?>
     338                <div class="<?php echo esc_attr( $widget_id ); ?> media-widget-preview">
     339                        <p>
     340                                <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php esc_html_e( 'Title:' ); ?></label>
     341                                <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo esc_attr( $instance['title'] ); ?>" />
     342                        </p>
     343
     344                        <p>
     345                                <?php esc_html_e( 'Add an image, video, or audio to your sidebar.' ); ?>
     346                        </p>
     347
     348                        <div class="media-widget-admin-preview" id="<?php echo esc_attr( $widget_id ); ?>">
     349                        <?php
     350                        if ( $attachment ) {
     351                                echo $this->render_media( $attachment, $widget_id, $instance );
     352                                echo $this->get_responsive_style( $attachment, $widget_id, $instance );
     353                        }
     354                        ?>
     355                        </div>
     356
     357                        <p>
     358                                <button type="button" data-id="<?php echo esc_attr( $widget_id ); ?>" class="button select-media widefat">
     359                                        <?php $attachment ? esc_html_e( 'Change Media' ) : esc_html_e( 'Select Media' ); ?>
     360                                </button>
     361                        </p>
     362
     363                        <?php
     364                        // Use hidden form fields to capture the attachment details from the media manager.
     365                        unset( $instance['title'] );
     366                        ?>
     367
     368                        <?php foreach ( $instance as $name => $value ) : ?>
     369                                <input type="hidden" id="<?php echo esc_attr( $this->get_field_id( $name ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( $name ) ); ?>" value="<?php echo esc_attr( $value ); ?>" />
     370                        <?php endforeach; ?>
     371                </div>
     372                <?php
     373        }
     374
     375        /**
     376         * Registers the stylesheet for handling the widget in the back-end.
     377         *
     378         * @since 4.8.0
     379         * @access public
     380         */
     381        public function enqueue_admin_styles() {
     382                wp_enqueue_style( 'wp-media-widget' );
     383        }
     384
     385        /**
     386         * Registers the scripts for handling the widget in the back-end.
     387         *
     388         * @since 4.8.0
     389         * @access public
     390         */
     391        public function enqueue_admin_scripts() {
     392                global $pagenow;
     393
     394                // Bail if we are not in the widgets or customize screens.
     395                if ( 'widgets.php' !== $pagenow && ! is_customize_preview() ) {
     396                        return;
     397                }
     398
     399                // Load the required media files for the media manager.
     400                wp_enqueue_media();
     401
     402                wp_enqueue_script( 'wp-media-widget' );
     403
     404                add_action( 'admin_print_footer_scripts', array( $this, 'admin_print_footer_scripts' ) );
     405        }
     406
     407        /**
     408         * Prints footer scripts.
     409         *
     410         * @since 4.8.0
     411         * @access public
     412         */
     413        public function admin_print_footer_scripts() {
     414                ?>
     415                <script type="text/html" id="tmpl-wp-media-widget-audio">
     416                <?php wp_underscore_audio_template() ?>
     417                </script>
     418
     419                <script type="text/html" id="tmpl-wp-media-widget-video">
     420                <?php wp_underscore_video_template() ?>
     421                </script>
     422
     423                <?php
     424        }
     425
     426        /**
     427         * Enqueue media element script and style if in need.
     428         *
     429         * This ensures the first instance of the media widget can properly handle media elements.
     430         *
     431         * @since 4.8.0
     432         * @access private
     433         */
     434        private function enqueue_mediaelement_script() {
     435                /** This filter is documented in wp-includes/media.php */
     436                $audio_library = apply_filters( 'wp_audio_shortcode_library', 'mediaelement' );
     437
     438                /** This filter is documented in wp-includes/media.php */
     439                $video_library = apply_filters( 'wp_video_shortcode_library', 'mediaelement' );
     440
     441                if ( 'mediaelement' !== $audio_library && 'mediaelement' !== $video_library ) {
     442                        return;
     443                }
     444
     445                wp_enqueue_style( 'wp-mediaelement' );
     446                wp_enqueue_script( 'wp-mediaelement' );
     447        }
     448}