<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[40640] trunk: Widgets: Introduce media widgets for images, audio, and video with extensible base for additional media widgets in the future.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/40640">40640</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/40640","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-05-11 21:10:54 +0000 (Thu, 11 May 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Widgets: Introduce media widgets for images, audio, and video with extensible base for additional media widgets in the future.

The last time a new widget was introduced, Vuvuzelas were a thing, Angry Birds started taking over phones, and WordPress stopped shipping with Kubrick. Seven years and 17 releases without new widgets have been enough, time to spice up your sidebar!

Props westonruter, melchoyce, obenland, timmydcrawford, adamsilverstein, gonom9, wonderboymusic, Fab1en, DrewAPicture, sirbrillig, joen, matias, samikeijonen, afercia, celloexpressions, designsimply, michelleweber, ranh, kjellr, karmatosed.
Fixes <a href="https://core.trac.wordpress.org/ticket/32417">#32417</a>, <a href="https://core.trac.wordpress.org/ticket/39993">#39993</a>, <a href="https://core.trac.wordpress.org/ticket/39994">#39994</a>, <a href="https://core.trac.wordpress.org/ticket/39995">#39995</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsswidgetscss">trunk/src/wp-admin/css/widgets.css</a></li>
<li><a href="#trunksrcwpcontentthemestwentytenstylecss">trunk/src/wp-content/themes/twentyten/style.css</a></li>
<li><a href="#trunksrcwpincludesdefaultwidgetsphp">trunk/src/wp-includes/default-widgets.php</a></li>
<li><a href="#trunksrcwpincludesjscustomizeselectiverefreshjs">trunk/src/wp-includes/js/customize-selective-refresh.js</a></li>
<li><a href="#trunksrcwpincludesmediatemplatephp">trunk/src/wp-includes/media-template.php</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunksrcwpincludeswidgetsphp">trunk/src/wp-includes/widgets.php</a></li>
<li><a href="#trunktestsphpunittestswidgetstextwidgetphp">trunk/tests/phpunit/tests/widgets/text-widget.php</a></li>
<li><a href="#trunktestsqunitindexhtml">trunk/tests/qunit/index.html</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpadminjswidgetsmediaaudiowidgetjs">trunk/src/wp-admin/js/widgets/media-audio-widget.js</a></li>
<li><a href="#trunksrcwpadminjswidgetsmediaimagewidgetjs">trunk/src/wp-admin/js/widgets/media-image-widget.js</a></li>
<li><a href="#trunksrcwpadminjswidgetsmediavideowidgetjs">trunk/src/wp-admin/js/widgets/media-video-widget.js</a></li>
<li><a href="#trunksrcwpadminjswidgetsmediawidgetsjs">trunk/src/wp-admin/js/widgets/media-widgets.js</a></li>
<li><a href="#trunksrcwpincludeswidgetsclasswpwidgetmediaaudiophp">trunk/src/wp-includes/widgets/class-wp-widget-media-audio.php</a></li>
<li><a href="#trunksrcwpincludeswidgetsclasswpwidgetmediaimagephp">trunk/src/wp-includes/widgets/class-wp-widget-media-image.php</a></li>
<li><a href="#trunksrcwpincludeswidgetsclasswpwidgetmediavideophp">trunk/src/wp-includes/widgets/class-wp-widget-media-video.php</a></li>
<li><a href="#trunksrcwpincludeswidgetsclasswpwidgetmediaphp">trunk/src/wp-includes/widgets/class-wp-widget-media.php</a></li>
<li><a href="#trunktestsphpunitdataimagescanola150x150jpg">trunk/tests/phpunit/data/images/canola-150x150.jpg</a></li>
<li><a href="#trunktestsphpunitdataimagescanola300x225jpg">trunk/tests/phpunit/data/images/canola-300x225.jpg</a></li>
<li><a href="#trunktestsphpunitdatauploadssmallaudiomp3">trunk/tests/phpunit/data/uploads/small-audio.mp3</a></li>
<li><a href="#trunktestsphpunitdatauploadssmallvideomp4">trunk/tests/phpunit/data/uploads/small-video.mp4</a></li>
<li><a href="#trunktestsphpunittestswidgetsmediaaudiowidgetphp">trunk/tests/phpunit/tests/widgets/media-audio-widget.php</a></li>
<li><a href="#trunktestsphpunittestswidgetsmediaimagewidgetphp">trunk/tests/phpunit/tests/widgets/media-image-widget.php</a></li>
<li><a href="#trunktestsphpunittestswidgetsmediavideowidgetphp">trunk/tests/phpunit/tests/widgets/media-video-widget.php</a></li>
<li><a href="#trunktestsphpunittestswidgetsmediawidgetphp">trunk/tests/phpunit/tests/widgets/media-widget.php</a></li>
<li>trunk/tests/qunit/wp-admin/js/widgets/</li>
<li><a href="#trunktestsqunitwpadminjswidgetstestmediaimagewidgetjs">trunk/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js</a></li>
<li><a href="#trunktestsqunitwpadminjswidgetstestmediavideowidgetjs">trunk/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js</a></li>
<li><a href="#trunktestsqunitwpadminjswidgetstestmediawidgetsjs">trunk/tests/qunit/wp-admin/js/widgets/test-media-widgets.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadmincsswidgetscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/widgets.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/widgets.css        2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-admin/css/widgets.css  2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -55,6 +55,110 @@
</span><span class="cx" style="display: block; padding: 0 10px">        color: #a0a5aa;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* Media Widgets */
+.wp-core-ui .media-widget-control.selected .placeholder,
+.wp-core-ui .media-widget-control.selected .not-selected,
+.wp-core-ui .media-widget-control .selected {
+       display: none;
+}
+
+.media-widget-control.selected .selected {
+       display: inline-block;
+}
+
+.media-widget-buttons {
+       text-align: left;
+       margin-bottom: 10px;
+}
+
+.media-widget-control .media-widget-buttons .button {
+       margin-left: 8px;
+       width: auto;
+}
+.media-widget-control:not(.selected) .media-widget-buttons .button,
+.media-widget-buttons .button:first-child {
+       margin-left: 0;
+}
+
+.media-widget-control .placeholder {
+       border: 1px dashed #b4b9be;
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       cursor: default;
+       line-height: 20px;
+       padding: 9px 0;
+       position: relative;
+       text-align: center;
+       width: 100%;
+}
+
+.media-widget-control .media-widget-preview {
+       text-align: center;
+}
+.media-widget-control .media-widget-preview .notice {
+       text-align: initial;
+}
+.media-frame .media-widget-embed-notice p code,
+.media-widget-control .notice p code {
+       padding: 0 3px 0 0;
+}
+.media-frame .media-widget-embed-notice {
+       margin-top: 16px;
+}
+.media-widget-control .media-widget-preview img {
+       max-width: 100%;
+}
+.media-widget-control .media-widget-preview .wp-video-shortcode {
+       background: #000;
+}
+
+.media-frame.media-widget .media-toolbar-secondary {
+       min-width: 300px;
+}
+
+.media-frame.media-widget .image-details .embed-media-settings .setting.align,
+.media-frame.media-widget .attachment-display-settings .setting.align,
+.media-frame.media-widget .embed-media-settings .setting.align,
+.media-frame.media-widget .embed-link-settings .setting.link-text,
+.media-frame.media-widget .replace-attachment,
+.media-frame.media-widget .checkbox-setting.autoplay {
+       display: none;
+}
+
+.media-widget-video-preview {
+       width: 100%;
+}
+
+.media-widget-video-link {
+       display: inline-block;
+       min-height: 132px;
+       width: 100%;
+       background: black;
+}
+
+.media-widget-video-link .dashicons {
+       font: normal 60px/1 'dashicons';
+       position: relative;
+       width: 100%;
+       top: -90px;
+       color: white;
+       text-decoration: none;
+}
+
+.media-widget-video-link.no-poster .dashicons {
+       top: 30px;
+}
+
+.media-frame #embed-url-field.invalid {
+       border: 1px solid #f00;
+}
+
+.wp-customizer .mejs-controls a:focus > .mejs-offscreen,
+.widgets-php .mejs-controls a:focus > .mejs-offscreen {
+       z-index: 2;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /* Widget Dragging Helpers */
</span><span class="cx" style="display: block; padding: 0 10px"> .widget.ui-draggable-dragging {
</span><span class="cx" style="display: block; padding: 0 10px">        min-width: 100%;
</span></span></pre></div>
<a id="trunksrcwpadminjswidgetsmediaaudiowidgetjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/js/widgets/media-audio-widget.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/widgets/media-audio-widget.js                               (rev 0)
+++ trunk/src/wp-admin/js/widgets/media-audio-widget.js 2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,150 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* eslint consistent-this: [ "error", "control" ] */
+(function( component ) {
+       'use strict';
+
+       var AudioWidgetModel, AudioWidgetControl, AudioDetailsMediaFrame;
+
+       /**
+        * Custom audio details frame that removes the replace-audio state.
+        *
+        * @class AudioDetailsMediaFrame
+        * @constructor
+        */
+       AudioDetailsMediaFrame = wp.media.view.MediaFrame.AudioDetails.extend({
+
+               /**
+                * Create the default states.
+                *
+                * @returns {void}
+                */
+               createStates: function createStates() {
+                       this.states.add([
+                               new wp.media.controller.AudioDetails( {
+                                       media: this.media
+                               } ),
+
+                               new wp.media.controller.MediaLibrary( {
+                                       type: 'audio',
+                                       id: 'add-audio-source',
+                                       title: wp.media.view.l10n.audioAddSourceTitle,
+                                       toolbar: 'add-audio-source',
+                                       media: this.media,
+                                       menu: false
+                               } )
+                       ]);
+               }
+       });
+
+       /**
+        * Audio widget model.
+        *
+        * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports.
+        *
+        * @class AudioWidgetModel
+        * @constructor
+        */
+       AudioWidgetModel = component.MediaWidgetModel.extend( {} );
+
+       /**
+        * Audio widget control.
+        *
+        * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports.
+        *
+        * @class AudioWidgetModel
+        * @constructor
+        */
+       AudioWidgetControl = component.MediaWidgetControl.extend( {
+
+               /**
+                * Show display settings.
+                *
+                * @type {boolean}
+                */
+               showDisplaySettings: false,
+
+               /**
+                * Map model props to media frame props.
+                *
+                * @param {Object} modelProps - Model props.
+                * @returns {Object} Media frame props.
+                */
+               mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
+                       var control = this, mediaFrameProps;
+                       mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps );
+                       mediaFrameProps.link = 'embed';
+                       return mediaFrameProps;
+               },
+
+               /**
+                * Render preview.
+                *
+                * @returns {void}
+                */
+               renderPreview: function renderPreview() {
+                       var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl;
+                       attachmentId = control.model.get( 'attachment_id' );
+                       attachmentUrl = control.model.get( 'url' );
+
+                       if ( ! attachmentId && ! attachmentUrl ) {
+                               return;
+                       }
+
+                       previewContainer = control.$el.find( '.media-widget-preview' );
+                       previewTemplate = wp.template( 'wp-media-widget-audio-preview' );
+
+                       previewContainer.html( previewTemplate( {
+                               model: {
+                                       attachment_id: control.model.get( 'attachment_id' ),
+                                       src: attachmentUrl
+                               },
+                               error: control.model.get( 'error' )
+                       } ) );
+                       wp.mediaelement.initialize();
+               },
+
+               /**
+                * Open the media audio-edit frame to modify the selected item.
+                *
+                * @returns {void}
+                */
+               editMedia: function editMedia() {
+                       var control = this, mediaFrame, metadata, updateCallback;
+
+                       metadata = control.mapModelToMediaFrameProps( control.model.toJSON() );
+
+                       // Set up the media frame.
+                       mediaFrame = new AudioDetailsMediaFrame({
+                               frame: 'audio',
+                               state: 'audio-details',
+                               metadata: metadata
+                       } );
+                       wp.media.frame = mediaFrame;
+                       mediaFrame.$el.addClass( 'media-widget' );
+
+                       updateCallback = function( mediaFrameProps ) {
+
+                               // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+                               control.selectedAttachment.set( mediaFrameProps );
+
+                               control.model.set( _.extend(
+                                       control.model.defaults(),
+                                       control.mapMediaToModelProps( mediaFrameProps ),
+                                       { error: false }
+                               ) );
+                       };
+
+                       mediaFrame.state( 'audio-details' ).on( 'update', updateCallback );
+                       mediaFrame.state( 'replace-audio' ).on( 'replace', updateCallback );
+                       mediaFrame.on( 'close', function() {
+                               mediaFrame.detach();
+                       });
+
+                       mediaFrame.open();
+               }
+       } );
+
+       // Exports.
+       component.controlConstructors.media_audio = AudioWidgetControl;
+       component.modelConstructors.media_audio = AudioWidgetModel;
+
+})( wp.mediaWidgets );
</ins></span></pre></div>
<a id="trunksrcwpadminjswidgetsmediaimagewidgetjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/js/widgets/media-image-widget.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/widgets/media-image-widget.js                               (rev 0)
+++ trunk/src/wp-admin/js/widgets/media-image-widget.js 2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,129 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* eslint consistent-this: [ "error", "control" ] */
+(function( component, $ ) {
+       'use strict';
+
+       var ImageWidgetModel, ImageWidgetControl;
+
+       /**
+        * Image widget model.
+        *
+        * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports.
+        *
+        * @class ImageWidgetModel
+        * @constructor
+        */
+       ImageWidgetModel = component.MediaWidgetModel.extend({});
+
+       /**
+        * Image widget control.
+        *
+        * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports.
+        *
+        * @class ImageWidgetModel
+        * @constructor
+        */
+       ImageWidgetControl = component.MediaWidgetControl.extend({
+
+               /**
+                * Render preview.
+                *
+                * @returns {void}
+                */
+               renderPreview: function renderPreview() {
+                       var control = this, previewContainer, previewTemplate;
+                       if ( ! control.model.get( 'attachment_id' ) && ! control.model.get( 'url' ) ) {
+                               return;
+                       }
+
+                       previewContainer = control.$el.find( '.media-widget-preview' );
+                       previewTemplate = wp.template( 'wp-media-widget-image-preview' );
+                       previewContainer.html( previewTemplate( _.extend( control.previewTemplateProps.toJSON() ) ) );
+               },
+
+               /**
+                * Open the media image-edit frame to modify the selected item.
+                *
+                * @returns {void}
+                */
+               editMedia: function editMedia() {
+                       var control = this, mediaFrame, updateCallback, defaultSync, metadata;
+
+                       metadata = control.mapModelToMediaFrameProps( control.model.toJSON() );
+
+                       // Needed or else none will not be selected if linkUrl is not also empty.
+                       if ( 'none' === metadata.link ) {
+                               metadata.linkUrl = '';
+                       }
+
+                       // Set up the media frame.
+                       mediaFrame = wp.media({
+                               frame: 'image',
+                               state: 'image-details',
+                               metadata: metadata
+                       });
+                       mediaFrame.$el.addClass( 'media-widget' );
+
+                       updateCallback = function() {
+                               var mediaProps;
+
+                               // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+                               mediaProps = mediaFrame.state().attributes.image.toJSON();
+                               control.selectedAttachment.set( mediaProps );
+
+                               control.model.set( _.extend(
+                                       control.mapMediaToModelProps( mediaProps ),
+                                       { error: false }
+                               ) );
+                       };
+
+                       mediaFrame.state( 'image-details' ).on( 'update', updateCallback );
+                       mediaFrame.state( 'replace-image' ).on( 'replace', updateCallback );
+
+                       // Disable syncing of attachment changes back to server. See <https://core.trac.wordpress.org/ticket/40403>.
+                       defaultSync = wp.media.model.Attachment.prototype.sync;
+                       wp.media.model.Attachment.prototype.sync = function rejectedSync() {
+                               return $.Deferred().rejectWith( this ).promise();
+                       };
+                       mediaFrame.on( 'close', function onClose() {
+                               mediaFrame.detach();
+                               wp.media.model.Attachment.prototype.sync = defaultSync;
+                       });
+
+                       mediaFrame.open();
+               },
+
+               /**
+                * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
+                *
+                * @returns {Object} Reset/override props.
+                */
+               getEmbedResetProps: function getEmbedResetProps() {
+                       return _.extend(
+                               component.MediaWidgetControl.prototype.getEmbedResetProps.call( this ),
+                               {
+                                       size: 'full',
+                                       width: 0,
+                                       height: 0
+                               }
+                       );
+               },
+
+               /**
+                * Map model props to preview template props.
+                *
+                * @returns {Object} Preview template props.
+                */
+               mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
+                       var control = this, mediaFrameProps, url;
+                       url = control.model.get( 'url' );
+                       mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call( control );
+                       mediaFrameProps.currentFilename = url ? url.replace( /\?.*$/, '' ).replace( /^.+\//, '' ) : '';
+                       return mediaFrameProps;
+               }
+       });
+
+       // Exports.
+       component.controlConstructors.media_image = ImageWidgetControl;
+       component.modelConstructors.media_image = ImageWidgetModel;
+
+})( wp.mediaWidgets, jQuery );
</ins></span></pre></div>
<a id="trunksrcwpadminjswidgetsmediavideowidgetjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/js/widgets/media-video-widget.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/widgets/media-video-widget.js                               (rev 0)
+++ trunk/src/wp-admin/js/widgets/media-video-widget.js 2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,228 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* eslint consistent-this: [ "error", "control" ] */
+(function( component ) {
+       'use strict';
+
+       var VideoWidgetModel, VideoWidgetControl, VideoDetailsMediaFrame;
+
+       /**
+        * Custom video details frame that removes the replace-video state.
+        *
+        * @class VideoDetailsMediaFrame
+        * @constructor
+        */
+       VideoDetailsMediaFrame = wp.media.view.MediaFrame.VideoDetails.extend({
+
+               /**
+                * Create the default states.
+                *
+                * @returns {void}
+                */
+               createStates: function createStates() {
+                       this.states.add([
+                               new wp.media.controller.VideoDetails({
+                                       media: this.media
+                               }),
+
+                               new wp.media.controller.MediaLibrary( {
+                                       type: 'video',
+                                       id: 'add-video-source',
+                                       title: wp.media.view.l10n.videoAddSourceTitle,
+                                       toolbar: 'add-video-source',
+                                       media: this.media,
+                                       menu: false
+                               } ),
+
+                               new wp.media.controller.MediaLibrary( {
+                                       type: 'text',
+                                       id: 'add-track',
+                                       title: wp.media.view.l10n.videoAddTrackTitle,
+                                       toolbar: 'add-track',
+                                       media: this.media,
+                                       menu: 'video-details'
+                               } )
+                       ]);
+               }
+       });
+
+       /**
+        * Video widget model.
+        *
+        * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports.
+        *
+        * @class VideoWidgetModel
+        * @constructor
+        */
+       VideoWidgetModel = component.MediaWidgetModel.extend( {} );
+
+       /**
+        * Video widget control.
+        *
+        * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports.
+        *
+        * @class VideoWidgetControl
+        * @constructor
+        */
+       VideoWidgetControl = component.MediaWidgetControl.extend( {
+
+               /**
+                * Show display settings.
+                *
+                * @type {boolean}
+                */
+               showDisplaySettings: false,
+
+               /**
+                * Cache of oembed responses.
+                *
+                * @type {Object}
+                */
+               oembedResponses: {},
+
+               /**
+                * Map model props to media frame props.
+                *
+                * @param {Object} modelProps - Model props.
+                * @returns {Object} Media frame props.
+                */
+               mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
+                       var control = this, mediaFrameProps;
+                       mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps );
+                       mediaFrameProps.link = 'embed';
+                       return mediaFrameProps;
+               },
+
+               /**
+                * Fetches embed data for external videos.
+                *
+                * @returns {void}
+                */
+               fetchEmbed: function fetchEmbed() {
+                       var control = this, url;
+                       url = control.model.get( 'url' );
+
+                       // If we already have a local cache of the embed response, return.
+                       if ( control.oembedResponses[ url ] ) {
+                               return;
+                       }
+
+                       // If there is an in-flight embed request, abort it.
+                       if ( control.fetchEmbedDfd && 'pending' === control.fetchEmbedDfd.state() ) {
+                               control.fetchEmbedDfd.abort();
+                       }
+
+                       control.fetchEmbedDfd = jQuery.ajax({
+                               url: 'https://noembed.com/embed',
+                               data: {
+                                       url: control.model.get( 'url' ),
+                                       maxwidth: control.model.get( 'width' ),
+                                       maxheight: control.model.get( 'height' )
+                               },
+                               type: 'GET',
+                               crossDomain: true,
+                               dataType: 'json'
+                       });
+
+                       control.fetchEmbedDfd.done( function( response ) {
+                               control.oembedResponses[ url ] = response;
+                               control.renderPreview();
+                       });
+
+                       control.fetchEmbedDfd.fail( function() {
+                               control.oembedResponses[ url ] = null;
+                       });
+               },
+
+               /**
+                * Render preview.
+                *
+                * @returns {void}
+                */
+               renderPreview: function renderPreview() {
+                       var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl, poster, isHostedEmbed = false, parsedUrl, mime, error;
+                       attachmentId = control.model.get( 'attachment_id' );
+                       attachmentUrl = control.model.get( 'url' );
+                       error = control.model.get( 'error' );
+
+                       if ( ! attachmentId && ! attachmentUrl ) {
+                               return;
+                       }
+
+                       if ( ! attachmentId && attachmentUrl ) {
+                               parsedUrl = document.createElement( 'a' );
+                               parsedUrl.href = attachmentUrl;
+                               isHostedEmbed = /vimeo|youtu\.?be/.test( parsedUrl.host );
+                       }
+
+                       if ( isHostedEmbed ) {
+                               control.fetchEmbed();
+                               poster = control.oembedResponses[ attachmentUrl ] ? control.oembedResponses[ attachmentUrl ].thumbnail_url : null;
+                       }
+
+                       // Verify the selected attachment mime is supported.
+                       mime = control.selectedAttachment.get( 'mime' );
+                       if ( mime && attachmentId ) {
+                               if ( ! _.contains( _.values( wp.media.view.settings.embedMimes ), mime ) ) {
+                                       error = 'unsupported_file_type';
+                               }
+                       }
+
+                       previewContainer = control.$el.find( '.media-widget-preview' );
+                       previewTemplate = wp.template( 'wp-media-widget-video-preview' );
+
+                       previewContainer.html( previewTemplate( {
+                               model: {
+                                       attachment_id: control.model.get( 'attachment_id' ),
+                                       src: attachmentUrl,
+                                       poster: poster
+                               },
+                               is_hosted_embed: isHostedEmbed,
+                               error: error
+                       } ) );
+               },
+
+               /**
+                * Open the media image-edit frame to modify the selected item.
+                *
+                * @returns {void}
+                */
+               editMedia: function editMedia() {
+                       var control = this, mediaFrame, metadata, updateCallback;
+
+                       metadata = control.mapModelToMediaFrameProps( control.model.toJSON() );
+
+                       // Set up the media frame.
+                       mediaFrame = new VideoDetailsMediaFrame({
+                               frame: 'video',
+                               state: 'video-details',
+                               metadata: metadata
+                       });
+                       wp.media.frame = mediaFrame;
+                       mediaFrame.$el.addClass( 'media-widget' );
+
+                       updateCallback = function( mediaFrameProps ) {
+
+                               // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+                               control.selectedAttachment.set( mediaFrameProps );
+
+                               control.model.set( _.extend(
+                                       _.omit( control.model.defaults(), 'title' ),
+                                       control.mapMediaToModelProps( mediaFrameProps ),
+                                       { error: false }
+                               ) );
+                       };
+
+                       mediaFrame.state( 'video-details' ).on( 'update', updateCallback );
+                       mediaFrame.state( 'replace-video' ).on( 'replace', updateCallback );
+                       mediaFrame.on( 'close', function() {
+                               mediaFrame.detach();
+                       });
+
+                       mediaFrame.open();
+               }
+       } );
+
+       // Exports.
+       component.controlConstructors.media_video = VideoWidgetControl;
+       component.modelConstructors.media_video = VideoWidgetModel;
+
+})( wp.mediaWidgets );
</ins></span></pre></div>
<a id="trunksrcwpadminjswidgetsmediawidgetsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-admin/js/widgets/media-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/widgets/media-widgets.js                            (rev 0)
+++ trunk/src/wp-admin/js/widgets/media-widgets.js      2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,1133 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* eslint consistent-this: [ "error", "control" ] */
+wp.mediaWidgets = ( function( $ ) {
+       'use strict';
+
+       var component = {};
+
+       /**
+        * Widget control (view) constructors, mapping widget id_base to subclass of MediaWidgetControl.
+        *
+        * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
+        *
+        * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
+        */
+       component.controlConstructors = {};
+
+       /**
+        * Widget model constructors, mapping widget id_base to subclass of MediaWidgetModel.
+        *
+        * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
+        *
+        * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
+        */
+       component.modelConstructors = {};
+
+       /**
+        * Library which persists the customized display settings across selections.
+        *
+        * @class PersistentDisplaySettingsLibrary
+        * @constructor
+        */
+       component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend({
+
+               /**
+                * Initialize.
+                *
+                * @param {Object} options - Options.
+                * @returns {void}
+                */
+               initialize: function initialize( options ) {
+                       _.bindAll( this, 'handleDisplaySettingChange' );
+                       wp.media.controller.Library.prototype.initialize.call( this, options );
+               },
+
+               /**
+                * Sync changes to the current display settings back into the current customized.
+                *
+                * @param {Backbone.Model} displaySettings - Modified display settings.
+                * @returns {void}
+                */
+               handleDisplaySettingChange: function handleDisplaySettingChange( displaySettings ) {
+                       this.get( 'selectedDisplaySettings' ).set( displaySettings.attributes );
+               },
+
+               /**
+                * Get the display settings model.
+                *
+                * Model returned is updated with the current customized display settings,
+                * and an event listener is added so that changes made to the settings
+                * will sync back into the model storing the session's customized display
+                * settings.
+                *
+                * @param {Backbone.Model} model - Display settings model.
+                * @returns {Backbone.Model} Display settings model.
+                */
+               display: function getDisplaySettingsModel( model ) {
+                       var display, selectedDisplaySettings = this.get( 'selectedDisplaySettings' );
+                       display = wp.media.controller.Library.prototype.display.call( this, model );
+
+                       display.off( 'change', this.handleDisplaySettingChange ); // Prevent duplicated event handlers.
+                       display.set( selectedDisplaySettings.attributes );
+                       if ( 'custom' === selectedDisplaySettings.get( 'link_type' ) ) {
+                               display.linkUrl = selectedDisplaySettings.get( 'link_url' );
+                       }
+                       display.on( 'change', this.handleDisplaySettingChange );
+                       return display;
+               }
+       });
+
+       /**
+        * Extended view for managing the embed UI.
+        *
+        * @class MediaEmbedView
+        * @constructor
+        */
+       component.MediaEmbedView = wp.media.view.Embed.extend({
+
+               /**
+                * Refresh embed view.
+                *
+                * Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field.
+                *
+                * @returns {void}
+                */
+               refresh: function refresh() {
+                       var Constructor;
+
+                       if ( 'image' === this.controller.options.mimeType ) {
+                               Constructor = wp.media.view.EmbedImage;
+                       } else {
+
+                               // This should be eliminated once #40450 lands of when this is merged into core.
+                               Constructor = wp.media.view.EmbedLink.extend({
+
+                                       /**
+                                        * Set the disabled state on the Add to Widget button.
+                                        *
+                                        * @param {boolean} disabled - Disabled.
+                                        * @returns {void}
+                                        */
+                                       setAddToWidgetButtonDisabled: function setAddToWidgetButtonDisabled( disabled ) {
+                                               this.views.parent.views.parent.views.get( '.media-frame-toolbar' )[0].$el.find( '.media-button-select' ).prop( 'disabled', disabled );
+                                       },
+
+                                       /**
+                                        * Set or clear an error notice.
+                                        *
+                                        * @param {string} notice - Notice.
+                                        * @returns {void}
+                                        */
+                                       setErrorNotice: function setErrorNotice( notice ) {
+                                               var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this
+
+                                               noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' );
+                                               if ( ! notice ) {
+                                                       if ( noticeContainer.length ) {
+                                                               noticeContainer.slideUp( 'fast' );
+                                                       }
+                                               } else {
+                                                       if ( ! noticeContainer.length ) {
+                                                               noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt"></div>' );
+                                                               noticeContainer.hide();
+                                                               embedLinkView.views.parent.$el.prepend( noticeContainer );
+                                                       }
+                                                       noticeContainer.empty();
+                                                       noticeContainer.append( $( '<p>', {
+                                                               html: notice
+                                                       } ) );
+                                                       noticeContainer.slideDown( 'fast' );
+                                               }
+                                       },
+
+                                       /**
+                                        * Fetch media.
+                                        *
+                                        * This is a TEMPORARY measure until the WP API supports an oEmbed proxy endpoint. See #40450.
+                                        *
+                                        * @see https://core.trac.wordpress.org/ticket/40450
+                                        * @returns {void}
+                                        */
+                                       fetch: function() {
+                                               var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser; // eslint-disable-line consistent-this
+
+                                               if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) {
+                                                       embedLinkView.dfd.abort();
+                                               }
+
+                                               fetchSuccess = function( response ) {
+                                                       embedLinkView.renderoEmbed({
+                                                               data: {
+                                                                       body: response
+                                                               }
+                                                       });
+
+                                                       $( '#embed-url-field' ).removeClass( 'invalid' );
+                                                       embedLinkView.setErrorNotice( '' );
+                                                       embedLinkView.setAddToWidgetButtonDisabled( false );
+                                               };
+
+                                               urlParser = document.createElement( 'a' );
+                                               urlParser.href = embedLinkView.model.get( 'url' );
+                                               matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ );
+                                               if ( matches ) {
+                                                       fileExt = matches[1];
+                                                       if ( ! wp.media.view.settings.embedMimes[ fileExt ] ) {
+                                                               embedLinkView.renderFail();
+                                                       } else if ( 0 !== wp.media.view.settings.embedMimes[ fileExt ].indexOf( embedLinkView.controller.options.mimeType ) ) {
+                                                               embedLinkView.renderFail();
+                                                       } else {
+                                                               fetchSuccess( '<!--success-->' );
+                                                       }
+                                                       return;
+                                               }
+
+                                               embedLinkView.dfd = $.ajax({
+                                                       url: 'https://noembed.com/embed', // @todo Replace with core proxy endpoint once committed.
+                                                       data: {
+                                                               url: embedLinkView.model.get( 'url' ),
+                                                               maxwidth: embedLinkView.model.get( 'width' ),
+                                                               maxheight: embedLinkView.model.get( 'height' )
+                                                       },
+                                                       type: 'GET',
+                                                       crossDomain: true,
+                                                       dataType: 'json'
+                                               });
+
+                                               embedLinkView.dfd.done( function( response ) {
+                                                       if ( embedLinkView.controller.options.mimeType !== response.type ) {
+                                                               embedLinkView.renderFail();
+                                                               return;
+                                                       }
+                                                       fetchSuccess( response.html );
+                                               });
+                                               embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) );
+                                       },
+
+                                       /**
+                                        * Handle render failure.
+                                        *
+                                        * Overrides the {EmbedLink#renderFail()} method to prevent showing the "Link Text" field.
+                                        * The element is getting display:none in the stylesheet, but the underlying method uses
+                                        * uses {jQuery.fn.show()} which adds an inline style. This avoids the need for !important.
+                                        *
+                                        * @returns {void}
+                                        */
+                                       renderFail: function renderFail(  ) {
+                                               var embedLinkView = this; // eslint-disable-line consistent-this
+                                               $( '#embed-url-field' ).addClass( 'invalid' );
+                                               embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' );
+                                               embedLinkView.setAddToWidgetButtonDisabled( true );
+                                       }
+                               });
+                       }
+
+                       this.settings( new Constructor({
+                               controller: this.controller,
+                               model:      this.model.props,
+                               priority:   40
+                       }) );
+               }
+       });
+
+       /**
+        * Custom media frame for selecting uploaded media or providing media by URL.
+        *
+        * @class MediaFrameSelect
+        * @constructor
+        */
+       component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend({
+
+               /**
+                * Create the default states.
+                *
+                * @returns {void}
+                */
+               createStates: function createStates() {
+                       var mime = this.options.mimeType, specificMimes = [];
+                       _.each( wp.media.view.settings.embedMimes, function( embedMime ) {
+                               if ( 0 === embedMime.indexOf( mime ) ) {
+                                       specificMimes.push( embedMime );
+                               }
+                       });
+                       if ( specificMimes.length > 0 ) {
+                               mime = specificMimes.join( ',' );
+                       }
+
+                       this.states.add( [
+
+                               // Main states.
+                               new component.PersistentDisplaySettingsLibrary({
+                                       id:         'insert',
+                                       title:      this.options.title,
+                                       selection:  this.options.selection,
+                                       priority:   20,
+                                       toolbar:    'main-insert',
+                                       filterable: 'dates',
+                                       library:    wp.media.query({
+                                               type: mime
+                                       }),
+                                       multiple:   false,
+                                       editable:   true,
+
+                                       selectedDisplaySettings: this.options.selectedDisplaySettings,
+                                       displaySettings: _.isUndefined( this.options.showDisplaySettings ) ? true : this.options.showDisplaySettings,
+                                       displayUserSettings: false // We use the display settings from the current/default widget instance props.
+                               }),
+
+                               new wp.media.controller.EditImage({ model: this.options.editImage }),
+
+                               // Embed states.
+                               new wp.media.controller.Embed({
+                                       metadata: this.options.metadata,
+                                       type: 'image' === this.options.mimeType ? 'image' : 'link',
+                                       invalidEmbedTypeError: this.options.invalidEmbedTypeError
+                               })
+                       ] );
+               },
+
+               /**
+                * Main insert toolbar.
+                *
+                * Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text.
+                *
+                * @param {wp.Backbone.View} view - Toolbar view.
+                * @this {wp.media.controller.Library}
+                * @returns {void}
+                */
+               mainInsertToolbar: function mainInsertToolbar( view ) {
+                       var controller = this; // eslint-disable-line consistent-this
+                       view.set( 'insert', {
+                               style:    'primary',
+                               priority: 80,
+                               text:     controller.options.text, // The whole reason for the fork.
+                               requires: { selection: true },
+
+                               /**
+                                * Handle click.
+                                *
+                                * @fires wp.media.controller.State#insert()
+                                * @returns {void}
+                                */
+                               click: function onClick() {
+                                       var state = controller.state(),
+                                               selection = state.get( 'selection' );
+
+                                       controller.close();
+                                       state.trigger( 'insert', selection ).reset();
+                               }
+                       });
+               },
+
+               /**
+                * Main embed toolbar.
+                *
+                * Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text.
+                *
+                * @param {wp.Backbone.View} toolbar - Toolbar view.
+                * @this {wp.media.controller.Library}
+                * @returns {void}
+                */
+               mainEmbedToolbar: function mainEmbedToolbar( toolbar ) {
+                       toolbar.view = new wp.media.view.Toolbar.Embed({
+                               controller: this,
+                               text: this.options.text,
+                               event: 'insert'
+                       });
+               },
+
+               /**
+                * Embed content.
+                *
+                * Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field.
+                *
+                * @returns {void}
+                */
+               embedContent: function embedContent() {
+                       var view = new component.MediaEmbedView({
+                               controller: this,
+                               model:      this.state()
+                       }).render();
+
+                       this.content.set( view );
+
+                       if ( ! wp.media.isTouchDevice ) {
+                               view.url.focus();
+                       }
+               }
+       });
+
+       /**
+        * Media widget control.
+        *
+        * @class MediaWidgetControl
+        * @constructor
+        * @abstract
+        */
+       component.MediaWidgetControl = Backbone.View.extend({
+
+               /**
+                * Translation strings.
+                *
+                * The mapping of translation strings is handled by media widget subclasses,
+                * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
+                *
+                * @type {Object}
+                */
+               l10n: {
+                       add_to_widget: '{{add_to_widget}}',
+                       add_media: '{{add_media}}'
+               },
+
+               /**
+                * Widget ID base.
+                *
+                * This may be defined by the subclass. It may be exported from PHP to JS
+                * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not,
+                * it will attempt to be discovered by looking to see if this control
+                * instance extends each member of component.controlConstructors, and if
+                * it does extend one, will use the key as the id_base.
+                *
+                * @type {string}
+                */
+               id_base: '',
+
+               /**
+                * Mime type.
+                *
+                * This must be defined by the subclass. It may be exported from PHP to JS
+                * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
+                *
+                * @type {string}
+                */
+               mime_type: '',
+
+               /**
+                * View events.
+                *
+                * @type {Object}
+                */
+               events: {
+                       'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
+                       'click .select-media': 'selectMedia',
+                       'click .edit-media': 'editMedia'
+               },
+
+               /**
+                * Show display settings.
+                *
+                * @type {boolean}
+                */
+               showDisplaySettings: true,
+
+               /**
+                * Initialize.
+                *
+                * @param {Object}         options - Options.
+                * @param {Backbone.Model} options.model - Model.
+                * @param {jQuery}         options.el - Control container element.
+                * @returns {void}
+                */
+               initialize: function initialize( options ) {
+                       var control = this;
+
+                       Backbone.View.prototype.initialize.call( control, options );
+
+                       if ( ! control.el ) {
+                               throw new Error( 'Missing options.el' );
+                       }
+                       if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
+                               throw new Error( 'Missing options.model' );
+                       }
+
+                       // Allow methods to be passed in with control context preserved.
+                       _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
+
+                       if ( ! control.id_base ) {
+                               _.find( component.controlConstructors, function( Constructor, idBase ) {
+                                       if ( control instanceof Constructor ) {
+                                               control.id_base = idBase;
+                                               return true;
+                                       }
+                                       return false;
+                               });
+                               if ( ! control.id_base ) {
+                                       throw new Error( 'Missing id_base.' );
+                               }
+                       }
+
+                       // Track attributes needed to renderPreview in it's own model.
+                       control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() );
+
+                       // Re-render the preview when the attachment changes.
+                       control.selectedAttachment = new wp.media.model.Attachment();
+                       control.renderPreview = _.debounce( control.renderPreview );
+                       control.listenTo( control.previewTemplateProps, 'change', control.renderPreview );
+
+                       // Make sure a copy of the selected attachment is always fetched.
+                       control.model.on( 'change:attachment_id', control.updateSelectedAttachment );
+                       control.model.on( 'change:url', control.updateSelectedAttachment );
+                       control.updateSelectedAttachment();
+
+                       /*
+                        * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
+                        * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
+                        * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
+                        */
+                       control.listenTo( control.model, 'change', control.syncModelToInputs );
+                       control.listenTo( control.model, 'change', control.syncModelToPreviewProps );
+                       control.listenTo( control.model, 'change', control.render );
+
+                       // Update the title.
+                       control.$el.on( 'input', '.title', function updateTitle() {
+                               control.model.set({
+                                       title: $.trim( $( this ).val() )
+                               });
+                       });
+
+                       /*
+                        * Copy current display settings from the widget model to serve as basis
+                        * of customized display settings for the current media frame session.
+                        * Changes to display settings will be synced into this model, and
+                        * when a new selection is made, the settings from this will be synced
+                        * into that AttachmentDisplay's model to persist the setting changes.
+                        */
+                       control.displaySettings = new Backbone.Model( _.pick(
+                               control.mapModelToMediaFrameProps(
+                                       _.extend( control.model.defaults(), control.model.toJSON() )
+                               ),
+                               _.keys( wp.media.view.settings.defaultProps )
+                       ) );
+               },
+
+               /**
+                * Update the selected attachment if necessary.
+                *
+                * @returns {void}
+                */
+               updateSelectedAttachment: function updateSelectedAttachment() {
+                       var control = this, attachment;
+
+                       if ( 0 === control.model.get( 'attachment_id' ) ) {
+                               control.selectedAttachment.clear();
+                               control.model.set( 'error', false );
+                       } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) {
+                               attachment = new wp.media.model.Attachment({
+                                       id: control.model.get( 'attachment_id' )
+                               });
+                               attachment.fetch()
+                                       .done( function done() {
+                                               control.model.set( 'error', false );
+                                               control.selectedAttachment.set( attachment.toJSON() );
+                                       })
+                                       .fail( function fail() {
+                                               control.model.set( 'error', 'missing_attachment' );
+                                       });
+                       }
+               },
+
+               /**
+                * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
+                *
+                * @returns {void}
+                */
+               syncModelToPreviewProps: function syncModelToPreviewProps() {
+                       var control = this;
+                       control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() );
+               },
+
+               /**
+                * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
+                *
+                * @returns {void}
+                */
+               syncModelToInputs: function syncModelToInputs() {
+                       var control = this;
+                       control.$el.next( '.widget-content' ).find( '.media-widget-instance-property' ).each( function() {
+                               var input = $( this ), value;
+                               value = control.model.get( input.data( 'property' ) );
+                               if ( _.isUndefined( value ) ) {
+                                       return;
+                               }
+                               value = String( value );
+                               if ( input.val() === value ) {
+                                       return;
+                               }
+                               input.val( value );
+                               input.trigger( 'change' );
+                       });
+               },
+
+               /**
+                * Get template.
+                *
+                * @returns {Function} Template.
+                */
+               template: function template() {
+                       var control = this;
+                       if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) {
+                               throw new Error( 'Missing widget control template for ' + control.id_base );
+                       }
+                       return wp.template( 'widget-media-' + control.id_base + '-control' );
+               },
+
+               /**
+                * Render template.
+                *
+                * @returns {void}
+                */
+               render: function render() {
+                       var control = this, titleInput;
+
+                       if ( ! control.templateRendered ) {
+                               control.$el.html( control.template()( control.model.toJSON() ) );
+                               control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes.
+                               control.templateRendered = true;
+                       }
+
+                       titleInput = control.$el.find( '.title' );
+                       if ( ! titleInput.is( document.activeElement ) ) {
+                               titleInput.val( control.model.get( 'title' ) );
+                       }
+
+                       control.$el.toggleClass( 'selected', control.isSelected() );
+               },
+
+               /**
+                * Render media preview.
+                *
+                * @abstract
+                * @returns {void}
+                */
+               renderPreview: function renderPreview() {
+                       throw new Error( 'renderPreview must be implemented' );
+               },
+
+               /**
+                * Whether a media item is selected.
+                *
+                * @returns {boolean} Whether selected and no error.
+                */
+               isSelected: function isSelected() {
+                       var control = this;
+
+                       if ( control.model.get( 'error' ) ) {
+                               return false;
+                       }
+
+                       return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) );
+               },
+
+               /**
+                * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice.
+                *
+                * @param {jQuery.Event} event - Event.
+                * @returns {void}
+                */
+               handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) {
+                       var control = this;
+                       event.preventDefault();
+                       control.selectMedia();
+               },
+
+               /**
+                * Open the media select frame to chose an item.
+                *
+                * @returns {void}
+                */
+               selectMedia: function selectMedia() {
+                       var control = this, selection, mediaFrame, defaultSync, mediaFrameProps;
+
+                       if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) {
+                               selection = new wp.media.model.Selection( [ control.selectedAttachment ] );
+                       } else {
+                               selection = null;
+                       }
+
+                       mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
+                       if ( mediaFrameProps.size ) {
+                               control.displaySettings.set( 'size', mediaFrameProps.size );
+                       }
+
+                       mediaFrame = new component.MediaFrameSelect({
+                               title: control.l10n.add_media,
+                               frame: 'post',
+                               text: control.l10n.add_to_widget,
+                               selection: selection,
+                               mimeType: control.mime_type,
+                               selectedDisplaySettings: control.displaySettings,
+                               showDisplaySettings: control.showDisplaySettings,
+                               metadata: mediaFrameProps,
+                               state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert',
+                               invalidEmbedTypeError: control.l10n.unsupported_file_type
+                       });
+                       wp.media.frame = mediaFrame; // See wp.media().
+
+                       // Handle selection of a media item.
+                       mediaFrame.on( 'insert', function onInsert() {
+                               var attachment = {}, state = mediaFrame.state();
+
+                               // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
+                               if ( 'embed' === state.get( 'id' ) ) {
+                                       _.extend( attachment, { id: 0 }, state.props.toJSON() );
+                               } else {
+                                       _.extend( attachment, state.get( 'selection' ).first().toJSON() );
+                               }
+
+                               control.selectedAttachment.set( attachment );
+                               control.model.set( 'error', false );
+
+                               // Update widget instance.
+                               control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) );
+                       });
+
+                       // Disable syncing of attachment changes back to server. See <https://core.trac.wordpress.org/ticket/40403>.
+                       defaultSync = wp.media.model.Attachment.prototype.sync;
+                       wp.media.model.Attachment.prototype.sync = function rejectedSync() {
+                               return $.Deferred().rejectWith( this ).promise();
+                       };
+                       mediaFrame.on( 'close', function onClose() {
+                               wp.media.model.Attachment.prototype.sync = defaultSync;
+                       });
+
+                       mediaFrame.$el.addClass( 'media-widget' );
+                       mediaFrame.open();
+
+                       // Clear the selected attachment when it is deleted in the media select frame.
+                       if ( selection ) {
+                               selection.on( 'destroy', function onDestroy( attachment ) {
+                                       if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) {
+                                               control.model.set({
+                                                       attachment_id: 0,
+                                                       url: ''
+                                               });
+                                       }
+                               });
+                       }
+
+                       /*
+                        * Make sure focus is set inside of modal so that hitting Esc will close
+                        * the modal and not inadvertently cause the widget to collapse in the customizer.
+                        */
+                       mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus();
+               },
+
+               /**
+                * Get the instance props from the media selection frame.
+                *
+                * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
+                * @returns {Object} Props.
+                */
+               getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
+                       var control = this, state, mediaFrameProps, modelProps;
+
+                       state = mediaFrame.state();
+                       if ( 'insert' === state.get( 'id' ) ) {
+                               mediaFrameProps = state.get( 'selection' ).first().toJSON();
+                               mediaFrameProps.postUrl = mediaFrameProps.link;
+
+                               if ( control.showDisplaySettings ) {
+                                       _.extend(
+                                               mediaFrameProps,
+                                               mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON()
+                                       );
+                               }
+                               if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) {
+                                       mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url;
+                               }
+                       } else if ( 'embed' === state.get( 'id' ) ) {
+                               mediaFrameProps = _.extend(
+                                       state.props.toJSON(),
+                                       { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`.
+                                       control.model.getEmbedResetProps()
+                               );
+                       }  else {
+                               throw new Error( 'Unexpected state: ' + state.get( 'id' ) );
+                       }
+
+                       if ( mediaFrameProps.id ) {
+                               mediaFrameProps.attachment_id = mediaFrameProps.id;
+                       }
+
+                       modelProps = control.mapMediaToModelProps( mediaFrameProps );
+
+                       // Clear the extension prop so sources will be reset for video and audio media.
+                       _.each( wp.media.view.settings.embedExts, function( ext ) {
+                               if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) {
+                                       modelProps[ ext ] = '';
+                               }
+                       } );
+
+                       return modelProps;
+               },
+
+               /**
+                * Map media frame props to model props.
+                *
+                * @param {Object} mediaFrameProps - Media frame props.
+                * @returns {Object} Model props.
+                */
+               mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) {
+                       var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension;
+                       _.each( control.model.schema, function( fieldSchema, modelProp ) {
+
+                               // Ignore widget title attribute.
+                               if ( 'title' === modelProp ) {
+                                       return;
+                               }
+                               mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp;
+                       });
+
+                       _.each( mediaFrameProps, function( value, mediaProp ) {
+                               var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp;
+                               if ( control.model.schema[ propName ] ) {
+                                       modelProps[ propName ] = value;
+                               }
+                       });
+
+                       if ( 'custom' === mediaFrameProps.size ) {
+                               modelProps.width = mediaFrameProps.customWidth;
+                               modelProps.height = mediaFrameProps.customHeight;
+                       }
+
+                       if ( 'post' === mediaFrameProps.link ) {
+                               modelProps.link_url = mediaFrameProps.postUrl;
+                       } else if ( 'file' === mediaFrameProps.link ) {
+                               modelProps.link_url = mediaFrameProps.url;
+                       }
+
+                       // Because some media frames use `id` instead of `attachment_id`.
+                       if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) {
+                               modelProps.attachment_id = mediaFrameProps.id;
+                       }
+
+                       if ( mediaFrameProps.url ) {
+                               extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase();
+                               if ( extension in control.model.schema ) {
+                                       modelProps[ extension ] = mediaFrameProps.url;
+                               }
+                       }
+
+                       // Always omit the titles derived from mediaFrameProps.
+                       return _.omit( modelProps, 'title' );
+               },
+
+               /**
+                * Map model props to media frame props.
+                *
+                * @param {Object} modelProps - Model props.
+                * @returns {Object} Media frame props.
+                */
+               mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
+                       var control = this, mediaFrameProps = {};
+
+                       _.each( modelProps, function( value, modelProp ) {
+                               var fieldSchema = control.model.schema[ modelProp ] || {};
+                               mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value;
+                       });
+
+                       // Some media frames use attachment_id.
+                       mediaFrameProps.attachment_id = mediaFrameProps.id;
+
+                       if ( 'custom' === mediaFrameProps.size ) {
+                               mediaFrameProps.customWidth = control.model.get( 'width' );
+                               mediaFrameProps.customHeight = control.model.get( 'height' );
+                       }
+
+                       return mediaFrameProps;
+               },
+
+               /**
+                * Map model props to previewTemplateProps.
+                *
+                * @returns {Object} Preview Template Props.
+                */
+               mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
+                       var control = this, previewTemplateProps = {};
+                       _.each( control.model.schema, function( value, prop ) {
+                               if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) {
+                                       previewTemplateProps[ prop ] = control.model.get( prop );
+                               }
+                       });
+
+                       // Templates need to be aware of the error.
+                       previewTemplateProps.error = control.model.get( 'error' );
+                       return previewTemplateProps;
+               },
+
+               /**
+                * Open the media frame to modify the selected item.
+                *
+                * @abstract
+                * @returns {void}
+                */
+               editMedia: function editMedia() {
+                       throw new Error( 'editMedia not implemented' );
+               }
+       });
+
+       /**
+        * Media widget model.
+        *
+        * @class MediaWidgetModel
+        * @constructor
+        */
+       component.MediaWidgetModel = Backbone.Model.extend({
+
+               /**
+                * Id attribute.
+                *
+                * @type {string}
+                */
+               idAttribute: 'widget_id',
+
+               /**
+                * Instance schema.
+                *
+                * This adheres to JSON Schema and subclasses should have their schema
+                * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
+                *
+                * @type {Object.<string, Object>}
+                */
+               schema: {
+                       title: {
+                               type: 'string',
+                               'default': ''
+                       },
+                       attachment_id: {
+                               type: 'integer',
+                               'default': 0
+                       },
+                       url: {
+                               type: 'string',
+                               'default': ''
+                       }
+               },
+
+               /**
+                * Get default attribute values.
+                *
+                * @returns {Object} Mapping of property names to their default values.
+                */
+               defaults: function() {
+                       var defaults = {};
+                       _.each( this.schema, function( fieldSchema, field ) {
+                               defaults[ field ] = fieldSchema['default'];
+                       });
+                       return defaults;
+               },
+
+               /**
+                * Set attribute value(s).
+                *
+                * This is a wrapped version of Backbone.Model#set() which allows us to
+                * cast the attribute values from the hidden inputs' string values into
+                * the appropriate data types (integers or booleans).
+                *
+                * @param {string|Object} key - Attribute name or attribute pairs.
+                * @param {mixed|Object}  [val] - Attribute value or options object.
+                * @param {Object}        [options] - Options when attribute name and value are passed separately.
+                * @returns {wp.mediaWidgets.MediaWidgetModel} This model.
+                */
+               set: function set( key, val, options ) {
+                       var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
+                       if ( null === key ) {
+                               return model;
+                       }
+                       if ( 'object' === typeof key ) {
+                               attrs = key;
+                               opts = val;
+                       } else {
+                               attrs = {};
+                               attrs[ key ] = val;
+                               opts = options;
+                       }
+
+                       castedAttrs = {};
+                       _.each( attrs, function( value, name ) {
+                               var type;
+                               if ( ! model.schema[ name ] ) {
+                                       castedAttrs[ name ] = value;
+                                       return;
+                               }
+                               type = model.schema[ name ].type;
+                               if ( 'integer' === type ) {
+                                       castedAttrs[ name ] = parseInt( value, 10 );
+                               } else if ( 'boolean' === type ) {
+                                       castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
+                               } else {
+                                       castedAttrs[ name ] = value;
+                               }
+                       });
+
+                       return Backbone.Model.prototype.set.call( this, castedAttrs, opts );
+               },
+
+               /**
+                * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
+                *
+                * @returns {Object} Reset/override props.
+                */
+               getEmbedResetProps: function getEmbedResetProps() {
+                       return {
+                               id: 0
+                       };
+               }
+       });
+
+       /**
+        * Collection of all widget model instances.
+        *
+        * @type {Backbone.Collection}
+        */
+       component.modelCollection = new ( Backbone.Collection.extend({
+               model: component.MediaWidgetModel
+       }) )();
+
+       /**
+        * Mapping of widget ID to instances of MediaWidgetControl subclasses.
+        *
+        * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
+        */
+       component.widgetControls = {};
+
+       /**
+        * Handle widget being added or initialized for the first time at the widget-added event.
+        *
+        * @param {jQuery.Event} event - Event.
+        * @param {jQuery}       widgetContainer - Widget container element.
+        * @returns {void}
+        */
+       component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
+               var widgetContent, controlContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId;
+               widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
+               widgetContent = widgetForm.find( '> .widget-content' );
+               idBase = widgetForm.find( '> .id_base' ).val();
+               widgetId = widgetForm.find( '> .widget-id' ).val();
+
+               // Prevent initializing already-added widgets.
+               if ( component.widgetControls[ widgetId ] ) {
+                       return;
+               }
+
+               ControlConstructor = component.controlConstructors[ idBase ];
+               if ( ! ControlConstructor ) {
+                       return;
+               }
+
+               ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
+
+               /*
+                * Create a container element for the widget control (Backbone.View).
+                * This is inserted into the DOM immediately before the the .widget-content
+                * element because the contents of this element are essentially "managed"
+                * by PHP, where each widget update cause the entire element to be emptied
+                * and replaced with the rendered output of WP_Widget::form() which is
+                * sent back in Ajax request made to save/update the widget instance.
+                * To prevent a "flash of replaced DOM elements and re-initialized JS
+                * components", the JS template is rendered outside of the normal form
+                * container.
+                */
+               controlContainer = $( '<div class="media-widget-control"></div>' );
+               widgetContent.before( controlContainer );
+
+               /*
+                * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
+                * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
+                * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
+                */
+               modelAttributes = {};
+               widgetContent.find( '.media-widget-instance-property' ).each( function() {
+                       var input = $( this );
+                       modelAttributes[ input.data( 'property' ) ] = input.val();
+               });
+               modelAttributes.widget_id = widgetId;
+
+               widgetModel = new ModelConstructor( modelAttributes );
+
+               widgetControl = new ControlConstructor({
+                       el: controlContainer,
+                       model: widgetModel
+               });
+               widgetControl.render();
+
+               /*
+                * Note that the model and control currently won't ever get garbage-collected
+                * when a widget gets removed/deleted because there is no widget-removed event.
+                */
+               component.modelCollection.add( [ widgetModel ] );
+               component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl;
+       };
+
+       /**
+        * Sync widget instance data sanitized from server back onto widget model.
+        *
+        * This gets called via the 'widget-updated' event when saving a widget from
+        * the widgets admin screen and also via the 'widget-synced' event when making
+        * a change to a widget in the customizer.
+        *
+        * @param {jQuery.Event} event - Event.
+        * @param {jQuery}       widgetContainer - Widget container element.
+        * @returns {void}
+        */
+       component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
+               var widgetForm, widgetContent, widgetId, widgetControl, attributes = {};
+               widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
+               widgetId = widgetForm.find( '> .widget-id' ).val();
+
+               widgetControl = component.widgetControls[ widgetId ];
+               if ( ! widgetControl ) {
+                       return;
+               }
+
+               // Make sure the server-sanitized values get synced back into the model.
+               widgetContent = widgetForm.find( '> .widget-content' );
+               widgetContent.find( '.media-widget-instance-property' ).each( function() {
+                       var property = $( this ).data( 'property' );
+                       attributes[ property ] = $( this ).val();
+               });
+
+               // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop.
+               widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs );
+               widgetControl.model.set( attributes );
+               widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs );
+       };
+
+       /**
+        * Initialize functionality.
+        *
+        * This function exists to prevent the JS file from having to boot itself.
+        * When WordPress enqueues this script, it should have an inline script
+        * attached which calls wp.mediaWidgets.init().
+        *
+        * @returns {void}
+        */
+       component.init = function init() {
+               var $document = $( document );
+               $document.on( 'widget-added', component.handleWidgetAdded );
+               $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
+
+               /*
+                * Manually trigger widget-added events for media widgets on the admin
+                * screen once they are expanded. The widget-added event is not triggered
+                * for each pre-existing widget on the widgets admin screen like it is
+                * on the customizer. Likewise, the customizer only triggers widget-added
+                * when the widget is expanded to just-in-time construct the widget form
+                * when it is actually going to be displayed. So the following implements
+                * the same for the widgets admin screen, to invoke the widget-added
+                * handler when a pre-existing media widget is expanded.
+                */
+               $( function initializeExistingWidgetContainers() {
+                       var widgetContainers;
+                       if ( 'widgets' !== window.pagenow ) {
+                               return;
+                       }
+                       widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
+                       widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
+                               var widgetContainer = $( this );
+                               component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
+                       });
+               });
+       };
+
+       return component;
+})( jQuery );
</ins></span></pre></div>
<a id="trunksrcwpcontentthemestwentytenstylecss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-content/themes/twentyten/style.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-content/themes/twentyten/style.css   2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-content/themes/twentyten/style.css     2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -841,6 +841,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">        padding: 4px;
</span><span class="cx" style="display: block; padding: 0 10px">        text-align: center;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+.widget-container .wp-caption {
+       max-width: 100% !important;
+}
</ins><span class="cx" style="display: block; padding: 0 10px"> .wp-caption img {
</span><span class="cx" style="display: block; padding: 0 10px">        margin: 5px 5px 0;
</span><span class="cx" style="display: block; padding: 0 10px">        max-width: 622px; /* caption width - 10px */
</span></span></pre></div>
<a id="trunksrcwpincludesdefaultwidgetsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/default-widgets.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/default-widgets.php 2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-includes/default-widgets.php   2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -19,9 +19,24 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /** WP_Widget_Archives class */
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-archives.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/** WP_Widget_Media class */
+require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media.php' );
+
+/** WP_Widget_Media_Audio class */
+require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media-audio.php' );
+
+/** WP_Widget_Media_Image class */
+require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media-image.php' );
+
+/** WP_Widget_Media_Video class */
+require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-media-video.php' );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /** WP_Widget_Meta class */
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-meta.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/** WP_Widget_Meta class */
+require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-meta.php' );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /** WP_Widget_Calendar class */
</span><span class="cx" style="display: block; padding: 0 10px"> require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-calendar.php' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizeselectiverefreshjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/js/customize-selective-refresh.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-selective-refresh.js   2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-includes/js/customize-selective-refresh.js     2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -469,6 +469,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        // Prevent placement container from being being re-triggered as being rendered among nested partials.
</span><span class="cx" style="display: block; padding: 0 10px">                        placement.container.data( 'customize-partial-content-rendered', true );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        /*
+                        * Note that the 'wp_audio_shortcode_library' and 'wp_video_shortcode_library' filters
+                        * will determine whether or not wp.mediaelement is loaded and whether it will
+                        * initialize audio and video respectively. See also https://core.trac.wordpress.org/ticket/40144
+                        */
+                       if ( wp.mediaelement ) {
+                               wp.mediaelement.initialize();
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         /**
</span><span class="cx" style="display: block; padding: 0 10px">                         * Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span></span></pre></div>
<a id="trunksrcwpincludesmediatemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/media-template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/media-template.php  2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-includes/media-template.php    2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -608,7 +608,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <h2><?php _e( 'Attachment Display Settings' ); ?></h2>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <# if ( 'image' === data.type ) { #>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        <label class="setting">
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 <label class="setting align">
</ins><span class="cx" style="display: block; padding: 0 10px">                                 <span><?php _e('Alignment'); ?></span>
</span><span class="cx" style="display: block; padding: 0 10px">                                <select class="alignment"
</span><span class="cx" style="display: block; padding: 0 10px">                                        data-setting="align"
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1087,7 +1087,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        </div>
</span><span class="cx" style="display: block; padding: 0 10px">                                </div>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                <label class="setting checkbox-setting">
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         <label class="setting checkbox-setting autoplay">
</ins><span class="cx" style="display: block; padding: 0 10px">                                         <input type="checkbox" data-setting="autoplay" />
</span><span class="cx" style="display: block; padding: 0 10px">                                        <span><?php _e( 'Autoplay' ); ?></span>
</span><span class="cx" style="display: block; padding: 0 10px">                                </label>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1176,7 +1176,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        </div>
</span><span class="cx" style="display: block; padding: 0 10px">                                </div>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                <label class="setting checkbox-setting">
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         <label class="setting checkbox-setting autoplay">
</ins><span class="cx" style="display: block; padding: 0 10px">                                         <input type="checkbox" data-setting="autoplay" />
</span><span class="cx" style="display: block; padding: 0 10px">                                        <span><?php _e( 'Autoplay' ); ?></span>
</span><span class="cx" style="display: block; padding: 0 10px">                                </label>
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-includes/script-loader.php     2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -602,6 +602,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $scripts->add( 'media-widgets', "/wp-admin/js/widgets/media-widgets$suffix.js", array( 'jquery', 'media-models', 'media-views' ) );
+               $scripts->add_inline_script( 'media-widgets', 'wp.mediaWidgets.init();', 'after' );
+
+               $scripts->add( 'media-audio-widget', "/wp-admin/js/widgets/media-audio-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
+               $scripts->add( 'media-image-widget', "/wp-admin/js/widgets/media-image-widget$suffix.js", array( 'media-widgets' ) );
+               $scripts->add( 'media-video-widget', "/wp-admin/js/widgets/media-video-widget$suffix.js", array( 'media-widgets', 'media-audiovideo' ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludeswidgetsclasswpwidgetmediaaudiophp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/widgets/class-wp-widget-media-audio.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets/class-wp-widget-media-audio.php                             (rev 0)
+++ trunk/src/wp-includes/widgets/class-wp-widget-media-audio.php       2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,204 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Widget API: WP_Widget_Media_Audio class
+ *
+ * @package WordPress
+ * @subpackage Widgets
+ * @since 4.8.0
+ */
+
+/**
+ * Core class that implements an audio widget.
+ *
+ * @since 4.8.0
+ *
+ * @see WP_Widget
+ */
+class WP_Widget_Media_Audio extends WP_Widget_Media {
+
+       /**
+        * Constructor.
+        *
+        * @since  4.8.0
+        * @access public
+        */
+       public function __construct() {
+               parent::__construct( 'media_audio', __( 'Audio' ), array(
+                       'description' => __( 'Displays an audio player.' ),
+                       'mime_type'   => 'audio',
+               ) );
+
+               $this->l10n = array_merge( $this->l10n, array(
+                       'no_media_selected' => __( 'No audio selected' ),
+                       'add_media' => _x( 'Add File', 'label for button in the audio widget; should not be longer than ~13 characters long' ),
+                       'replace_media' => _x( 'Replace Audio', 'label for button in the audio widget; should not be longer than ~13 characters long' ),
+                       'edit_media' => _x( 'Edit Audio', 'label for button in the audio widget; should not be longer than ~13 characters long' ),
+                       'missing_attachment' => sprintf(
+                               /* translators: placeholder is URL to media library */
+                               __( 'We can&#8217;t find that audio file. Check your <a href="%s">media library</a> and make sure it wasn&#8217;t deleted.' ),
+                               esc_url( admin_url( 'upload.php' ) )
+                       ),
+                       /* translators: %d is widget count */
+                       'media_library_state_multi' => _n_noop( 'Audio Widget (%d)', 'Audio Widget (%d)' ),
+                       'media_library_state_single' => __( 'Audio Widget' ),
+                       'unsupported_file_type' => __( 'Looks like this isn&#8217;t the correct kind of file. Please link to an audio file instead.' ),
+               ) );
+       }
+
+       /**
+        * Get schema for properties of a widget instance (item).
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @see WP_REST_Controller::get_item_schema()
+        * @see WP_REST_Controller::get_additional_fields()
+        * @link https://core.trac.wordpress.org/ticket/35574
+        * @return array Schema for properties.
+        */
+       public function get_instance_schema() {
+               $schema = array_merge(
+                       parent::get_instance_schema(),
+                       array(
+                               'preload' => array(
+                                       'type' => 'string',
+                                       'enum' => array( 'none', 'auto', 'metadata' ),
+                                       'default' => 'none',
+                               ),
+                               'loop' => array(
+                                       'type' => 'boolean',
+                                       'default' => false,
+                               ),
+                       )
+               );
+
+               foreach ( wp_get_audio_extensions() as $audio_extension ) {
+                       $schema[ $audio_extension ] = array(
+                               'type' => 'string',
+                               'default' => '',
+                               'format' => 'uri',
+                               /* translators: placeholder is audio extension */
+                               'description' => sprintf( __( 'URL to the %s audio source file' ), $audio_extension ),
+                       );
+               }
+
+               return $schema;
+       }
+
+       /**
+        * Render the media on the frontend.
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @param array $instance Widget instance props.
+        * @return void
+        */
+       public function render_media( $instance ) {
+               $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance );
+               $attachment = null;
+
+               if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) {
+                       $attachment = get_post( $instance['attachment_id'] );
+               }
+
+               if ( $attachment ) {
+                       $src = wp_get_attachment_url( $attachment->ID );
+               } else {
+                       $src = $instance['url'];
+               }
+
+               echo wp_audio_shortcode(
+                       array_merge(
+                               $instance,
+                               compact( 'src' )
+                       )
+               );
+       }
+
+       /**
+        * Enqueue preview scripts.
+        *
+        * These scripts normally are enqueued just-in-time when an audio shortcode is used.
+        * In the customizer, however, widgets can be dynamically added and rendered via
+        * selective refresh, and so it is important to unconditionally enqueue them in
+        * case a widget does get added.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_preview_scripts() {
+               /** This filter is documented in wp-includes/media.php */
+               if ( 'mediaelement' === apply_filters( 'wp_audio_shortcode_library', 'mediaelement' ) ) {
+                       wp_enqueue_style( 'wp-mediaelement' );
+                       wp_enqueue_script( 'wp-mediaelement' );
+               }
+       }
+
+       /**
+        * Loads the required media files for the media manager and scripts for media widgets.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_admin_scripts() {
+               parent::enqueue_admin_scripts();
+
+               wp_enqueue_style( 'wp-mediaelement' );
+               wp_enqueue_script( 'wp-mediaelement' );
+
+               $handle = 'media-audio-widget';
+               wp_enqueue_script( $handle );
+
+               $exported_schema = array();
+               foreach ( $this->get_instance_schema() as $field => $field_schema ) {
+                       $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) );
+               }
+               wp_add_inline_script(
+                       $handle,
+                       sprintf(
+                               'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;',
+                               wp_json_encode( $this->id_base ),
+                               wp_json_encode( $exported_schema )
+                       )
+               );
+
+               wp_add_inline_script(
+                       $handle,
+                       sprintf(
+                               '
+                                       wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s;
+                                       wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s );
+                               ',
+                               wp_json_encode( $this->id_base ),
+                               wp_json_encode( $this->widget_options['mime_type'] ),
+                               wp_json_encode( $this->l10n )
+                       )
+               );
+       }
+
+       /**
+        * Render form template scripts.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function render_control_template_scripts() {
+               parent::render_control_template_scripts()
+               ?>
+               <script type="text/html" id="tmpl-wp-media-widget-audio-preview">
+                       <# if ( data.error && 'missing_attachment' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p><?php echo $this->l10n['missing_attachment']; ?></p>
+                               </div>
+                       <# } else if ( data.error ) { #>
+                               <div class="notice notice-error notice-alt">
+                                       <p><?php _e( 'Unable to preview media due to an unknown error.' ); ?></p>
+                               </div>
+                       <# } else if ( data.model && data.model.src ) { #>
+                               <?php wp_underscore_audio_template() ?>
+                       <# } #>
+               </script>
+               <?php
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludeswidgetsclasswpwidgetmediaimagephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/widgets/class-wp-widget-media-image.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets/class-wp-widget-media-image.php                             (rev 0)
+++ trunk/src/wp-includes/widgets/class-wp-widget-media-image.php       2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,326 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Widget API: WP_Widget_Media_Image class
+ *
+ * @package WordPress
+ * @subpackage Widgets
+ * @since 4.8.0
+ */
+
+/**
+ * Core class that implements an image widget.
+ *
+ * @since 4.8.0
+ *
+ * @see WP_Widget
+ */
+class WP_Widget_Media_Image extends WP_Widget_Media {
+
+       /**
+        * Constructor.
+        *
+        * @since  4.8.0
+        * @access public
+        */
+       public function __construct() {
+               parent::__construct( 'media_image', __( 'Image' ), array(
+                       'description' => __( 'Displays an image.' ),
+                       'mime_type'   => 'image',
+               ) );
+
+               $this->l10n = array_merge( $this->l10n, array(
+                       'no_media_selected' => __( 'No image selected' ),
+                       'add_media' => _x( 'Add Image', 'label for button in the image widget; should not be longer than ~13 characters long' ),
+                       'replace_media' => _x( 'Replace Image', 'label for button in the image widget; should not be longer than ~13 characters long' ),
+                       'edit_media' => _x( 'Edit Image', 'label for button in the image widget; should not be longer than ~13 characters long' ),
+                       'missing_attachment' => sprintf(
+                               /* translators: placeholder is URL to media library */
+                               __( 'We can&#8217;t find that image. Check your <a href="%s">media library</a> and make sure it wasn&#8217;t deleted.' ),
+                               esc_url( admin_url( 'upload.php' ) )
+                       ),
+                       /* translators: %d is widget count */
+                       'media_library_state_multi' => _n_noop( 'Image Widget (%d)', 'Image Widget (%d)' ),
+                       'media_library_state_single' => __( 'Image Widget' ),
+               ) );
+       }
+
+       /**
+        * Get schema for properties of a widget instance (item).
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @see WP_REST_Controller::get_item_schema()
+        * @see WP_REST_Controller::get_additional_fields()
+        * @link https://core.trac.wordpress.org/ticket/35574
+        * @return array Schema for properties.
+        */
+       public function get_instance_schema() {
+               return array_merge(
+                       parent::get_instance_schema(),
+                       array(
+                               'size' => array(
+                                       'type' => 'string',
+                                       'enum' => array_merge( get_intermediate_image_sizes(), array( 'full', 'custom' ) ),
+                                       'default' => 'medium',
+                               ),
+                               'width' => array( // Via 'customWidth', only when size=custom; otherwise via 'width'.
+                                       'type' => 'integer',
+                                       'minimum' => 0,
+                                       'default' => 0,
+                               ),
+                               'height' => array( // Via 'customHeight', only when size=custom; otherwise via 'height'.
+                                       'type' => 'integer',
+                                       'minimum' => 0,
+                                       'default' => 0,
+                               ),
+
+                               'caption' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => 'wp_kses_post',
+                                       'should_preview_update' => false,
+                               ),
+                               'alt' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => 'sanitize_text_field',
+                               ),
+                               'link_type' => array(
+                                       'type' => 'string',
+                                       'enum' => array( 'none', 'file', 'post', 'custom' ),
+                                       'default' => 'none',
+                                       'media_prop' => 'link',
+                                       'should_preview_update' => false,
+                               ),
+                               'link_url' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'format' => 'uri',
+                                       'media_prop' => 'linkUrl',
+                                       'should_preview_update' => false,
+                               ),
+                               'image_classes' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => array( $this, 'sanitize_token_list' ),
+                                       'media_prop' => 'extraClasses',
+                                       'should_preview_update' => false,
+                               ),
+                               'link_classes' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => array( $this, 'sanitize_token_list' ),
+                                       'media_prop' => 'linkClassName',
+                                       'should_preview_update' => false,
+                               ),
+                               'link_rel' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => array( $this, 'sanitize_token_list' ),
+                                       'media_prop' => 'linkRel',
+                                       'should_preview_update' => false,
+                               ),
+                               'link_target_blank' => array( // Via 'linkTargetBlank' property.
+                                       'type' => 'boolean',
+                                       'default' => false,
+                                       'media_prop' => 'linkTargetBlank',
+                                       'should_preview_update' => false,
+                               ),
+                               'image_title' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => 'sanitize_text_field',
+                                       'media_prop' => 'title',
+                                       'should_preview_update' => false,
+                               ),
+
+                               /*
+                                * There are two additional properties exposed by the PostImage modal
+                                * that don't seem to be relevant, as they may only be derived read-only
+                                * values:
+                                * - originalUrl
+                                * - aspectRatio
+                                * - height (redundant when size is not custom)
+                                * - width (redundant when size is not custom)
+                                */
+                       )
+               );
+       }
+
+       /**
+        * Render the media on the frontend.
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @param array $instance Widget instance props.
+        * @return void
+        */
+       public function render_media( $instance ) {
+               $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance );
+               $instance = wp_parse_args( $instance, array(
+                       'size' => 'thumbnail',
+               ) );
+
+               $attachment = null;
+               if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) {
+                       $attachment = get_post( $instance['attachment_id'] );
+               }
+               if ( $attachment ) {
+                       $caption = $attachment->post_excerpt;
+                       if ( $instance['caption'] ) {
+                               $caption = $instance['caption'];
+                       }
+
+                       $image_attributes = array(
+                               'class' => sprintf( 'image wp-image-%d %s', $attachment->ID, $instance['image_classes'] ),
+                               'style' => 'max-width: 100%; height: auto;',
+                       );
+                       if ( ! empty( $instance['image_title'] ) ) {
+                               $image_attributes['title'] = $instance['image_title'];
+                       }
+
+                       if ( $instance['alt'] ) {
+                               $image_attributes['alt'] = $instance['alt'];
+                       }
+
+                       $size = $instance['size'];
+                       if ( 'custom' === $size || ! in_array( $size, array_merge( get_intermediate_image_sizes(), array( 'full' ) ), true ) ) {
+                               $size = array( $instance['width'], $instance['height'] );
+                       }
+
+                       $image = wp_get_attachment_image( $attachment->ID, $size, false, $image_attributes );
+
+                       $caption_size = _wp_get_image_size_from_meta( $instance['size'], wp_get_attachment_metadata( $attachment->ID ) );
+                       $width = empty( $caption_size[0] ) ? 0 : $caption_size[0];
+
+               } else {
+                       if ( empty( $instance['url'] ) ) {
+                               return;
+                       }
+
+                       $instance['size'] = 'custom';
+                       $caption = $instance['caption'];
+                       $width   = $instance['width'];
+                       $classes = 'image ' . $instance['image_classes'];
+                       if ( 0 === $instance['width'] ) {
+                               $instance['width'] = '';
+                       }
+                       if ( 0 === $instance['height'] ) {
+                               $instance['height'] = '';
+                       }
+
+                       $image = sprintf( '<img class="%1$s" src="%2$s" alt="%3$s" width="%4$s" height="%5$s" />',
+                               esc_attr( $classes ),
+                               esc_url( $instance['url'] ),
+                               esc_attr( $instance['alt'] ),
+                               esc_attr( $instance['width'] ),
+                               esc_attr( $instance['height'] )
+                       );
+               } // End if().
+
+               $url = '';
+               if ( 'file' === $instance['link_type'] ) {
+                       $url = $attachment ? wp_get_attachment_url( $attachment->ID ) : $instance['url'];
+               } elseif ( $attachment && 'post' === $instance['link_type'] ) {
+                       $url = get_attachment_link( $attachment->ID );
+               } elseif ( 'custom' === $instance['link_type'] && ! empty( $instance['link_url'] ) ) {
+                       $url = $instance['link_url'];
+               }
+
+               if ( $url ) {
+                       $image = sprintf(
+                               '<a href="%1$s" class="%2$s" rel="%3$s" target="%4$s">%5$s</a>',
+                               esc_url( $url ),
+                               esc_attr( $instance['link_classes'] ),
+                               esc_attr( $instance['link_rel'] ),
+                               ! empty( $instance['link_target_blank'] ) ? '_blank' : '',
+                               $image
+                       );
+               }
+
+               if ( $caption ) {
+                       $image = img_caption_shortcode( array(
+                               'width' => $width,
+                               'caption' => $caption,
+                       ), $image );
+               }
+
+               echo $image;
+       }
+
+       /**
+        * Loads the required media files for the media manager and scripts for media widgets.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_admin_scripts() {
+               parent::enqueue_admin_scripts();
+
+               $handle = 'media-image-widget';
+               wp_enqueue_script( $handle );
+
+               $exported_schema = array();
+               foreach ( $this->get_instance_schema() as $field => $field_schema ) {
+                       $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) );
+               }
+               wp_add_inline_script(
+                       $handle,
+                       sprintf(
+                               'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;',
+                               wp_json_encode( $this->id_base ),
+                               wp_json_encode( $exported_schema )
+                       )
+               );
+
+               wp_add_inline_script(
+                       $handle,
+                       sprintf(
+                               '
+                                       wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s;
+                                       wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s );
+                               ',
+                               wp_json_encode( $this->id_base ),
+                               wp_json_encode( $this->widget_options['mime_type'] ),
+                               wp_json_encode( $this->l10n )
+                       )
+               );
+       }
+
+       /**
+        * Render form template scripts.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function render_control_template_scripts() {
+               parent::render_control_template_scripts();
+
+               ?>
+               <script type="text/html" id="tmpl-wp-media-widget-image-preview">
+                       <#
+                       var describedById = 'describedBy-' + String( Math.random() );
+                       #>
+                       <# if ( data.error && 'missing_attachment' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p><?php echo $this->l10n['missing_attachment']; ?></p>
+                               </div>
+                       <# } else if ( data.error ) { #>
+                               <div class="notice notice-error notice-alt">
+                                       <p><?php _e( 'Unable to preview media due to an unknown error.' ); ?></p>
+                               </div>
+                       <# } else if ( data.url ) { #>
+                               <img class="attachment-thumb" src="{{ data.url }}" draggable="false" alt="{{ data.alt }}" <# if ( ! data.alt && data.currentFilename ) { #> aria-describedby="{{ describedById }}" <# } #> />
+                               <# if ( ! data.alt && data.currentFilename ) { #>
+                                       <p class="hidden" id="{{ describedById }}"><?php
+                                               /* translators: placeholder is image filename */
+                                               echo sprintf( __( 'Current image: %s' ), '{{ data.currentFilename }}' );
+                                       ?></p>
+                               <# } #>
+                       <# } #>
+               </script>
+               <?php
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludeswidgetsclasswpwidgetmediavideophp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/widgets/class-wp-widget-media-video.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets/class-wp-widget-media-video.php                             (rev 0)
+++ trunk/src/wp-includes/widgets/class-wp-widget-media-video.php       2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,255 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Widget API: WP_Widget_Media_Video class
+ *
+ * @package WordPress
+ * @subpackage Widgets
+ * @since 4.8.0
+ */
+
+/**
+ * Core class that implements a video widget.
+ *
+ * @since 4.8.0
+ *
+ * @see WP_Widget
+ */
+class WP_Widget_Media_Video extends WP_Widget_Media {
+
+       /**
+        * Constructor.
+        *
+        * @since  4.8.0
+        * @access public
+        */
+       public function __construct() {
+               parent::__construct( 'media_video', __( 'Video' ), array(
+                       'description' => __( 'Displays a video from the media library or from YouTube, Vimeo, or another provider.' ),
+                       'mime_type'   => 'video',
+               ) );
+
+               $this->l10n = array_merge( $this->l10n, array(
+                       'no_media_selected' => __( 'No video selected' ),
+                       'add_media' => _x( 'Add Video', 'label for button in the video widget; should not be longer than ~13 characters long' ),
+                       'replace_media' => _x( 'Replace Video', 'label for button in the video widget; should not be longer than ~13 characters long' ),
+                       'edit_media' => _x( 'Edit Video', 'label for button in the video widget; should not be longer than ~13 characters long' ),
+                       'missing_attachment' => sprintf(
+                               /* translators: placeholder is URL to media library */
+                               __( 'We can&#8217;t find that video. Check your <a href="%s">media library</a> and make sure it wasn&#8217;t deleted.' ),
+                               esc_url( admin_url( 'upload.php' ) )
+                       ),
+                       /* translators: %d is widget count */
+                       'media_library_state_multi' => _n_noop( 'Video Widget (%d)', 'Video Widget (%d)' ),
+                       'media_library_state_single' => __( 'Video Widget' ),
+                       /* translators: placeholder is a list of valid video file extensions */
+                       'unsupported_file_type' => sprintf( __( 'Sorry, we can&#8217;t display the video file type selected. Please select a supported video file (%1$s) or stream (YouTube or Vimeo) instead.' ), '<code>.' . implode( '</code>, <code>.', wp_get_video_extensions() ) . '</code>' ),
+               ) );
+       }
+
+       /**
+        * Get schema for properties of a widget instance (item).
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @see WP_REST_Controller::get_item_schema()
+        * @see WP_REST_Controller::get_additional_fields()
+        * @link https://core.trac.wordpress.org/ticket/35574
+        * @return array Schema for properties.
+        */
+       public function get_instance_schema() {
+               $schema = array_merge(
+                       parent::get_instance_schema(),
+                       array(
+                               'preload' => array(
+                                       'type' => 'string',
+                                       'enum' => array( 'none', 'auto', 'metadata' ),
+                                       'default' => 'metadata',
+                                       'should_preview_update' => false,
+                               ),
+                               'loop' => array(
+                                       'type' => 'boolean',
+                                       'default' => false,
+                                       'should_preview_update' => false,
+                               ),
+                               'content' => array(
+                                       'type' => 'string',
+                                       'default' => '',
+                                       'sanitize_callback' => 'wp_kses_post',
+                                       'description' => __( 'Tracks (subtitles, captions, descriptions, chapters, or metadata)' ),
+                                       'should_preview_update' => false,
+                               ),
+                       )
+               );
+
+               foreach ( wp_get_video_extensions() as $video_extension ) {
+                       $schema[ $video_extension ] = array(
+                               'type' => 'string',
+                               'default' => '',
+                               'format' => 'uri',
+                               /* translators: placeholder is video extension */
+                               'description' => sprintf( __( 'URL to the %s video source file' ), $video_extension ),
+                       );
+               }
+
+               return $schema;
+       }
+
+       /**
+        * Render the media on the frontend.
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @param array $instance Widget instance props.
+        *
+        * @return void
+        */
+       public function render_media( $instance ) {
+               $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance );
+               $attachment = null;
+
+               if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) {
+                       $attachment = get_post( $instance['attachment_id'] );
+               }
+
+               if ( $attachment ) {
+                       $src = wp_get_attachment_url( $attachment->ID );
+               } else {
+
+                       // Manually add the loop query argument.
+                       $loop = $instance['loop'] ? '1' : '0';
+                       $src = empty( $instance['url'] ) ? $instance['url'] : add_query_arg( 'loop', $loop, $instance['url'] );
+               }
+
+               if ( empty( $src ) ) {
+                       return;
+               }
+
+               add_filter( 'wp_video_shortcode', array( $this, 'inject_video_max_width_style' ) );
+
+               echo wp_video_shortcode(
+                       array_merge(
+                               $instance,
+                               compact( 'src' )
+                       ),
+                       $instance['content']
+               );
+
+               remove_filter( 'wp_video_shortcode', array( $this, 'inject_video_max_width_style' ) );
+       }
+
+       /**
+        * Inject max-width and remove height for videos too constrained to fit inside sidebars on frontend.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @param string $html Video shortcode HTML output.
+        * @return string HTML Output.
+        */
+       public function inject_video_max_width_style( $html ) {
+               $html = preg_replace( '/\sheight="\d+"/', '', $html );
+               $html = preg_replace( '/\swidth="\d+"/', '', $html );
+               $html = preg_replace( '/(?<=width:)\s*\d+px(?=;?)/', '100%', $html );
+               return $html;
+       }
+
+       /**
+        * Enqueue preview scripts.
+        *
+        * These scripts normally are enqueued just-in-time when a video shortcode is used.
+        * In the customizer, however, widgets can be dynamically added and rendered via
+        * selective refresh, and so it is important to unconditionally enqueue them in
+        * case a widget does get added.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_preview_scripts() {
+               /** This filter is documented in wp-includes/media.php */
+               if ( 'mediaelement' === apply_filters( 'wp_video_shortcode_library', 'mediaelement' ) ) {
+                       wp_enqueue_style( 'wp-mediaelement' );
+                       wp_enqueue_script( 'wp-mediaelement' );
+               }
+
+               // Enqueue script needed by Vimeo; see wp_video_shortcode().
+               wp_enqueue_script( 'froogaloop' );
+       }
+
+       /**
+        * Loads the required scripts and styles for the widget control.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_admin_scripts() {
+               parent::enqueue_admin_scripts();
+
+               $handle = 'media-video-widget';
+               wp_enqueue_script( $handle );
+
+               $exported_schema = array();
+               foreach ( $this->get_instance_schema() as $field => $field_schema ) {
+                       $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) );
+               }
+               wp_add_inline_script(
+                       $handle,
+                       sprintf(
+                               'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;',
+                               wp_json_encode( $this->id_base ),
+                               wp_json_encode( $exported_schema )
+                       )
+               );
+
+               wp_add_inline_script(
+                       $handle,
+                       sprintf(
+                               '
+                                       wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s;
+                                       wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s );
+                               ',
+                               wp_json_encode( $this->id_base ),
+                               wp_json_encode( $this->widget_options['mime_type'] ),
+                               wp_json_encode( $this->l10n )
+                       )
+               );
+       }
+
+       /**
+        * Render form template scripts.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function render_control_template_scripts() {
+               parent::render_control_template_scripts()
+               ?>
+               <script type="text/html" id="tmpl-wp-media-widget-video-preview">
+                       <# if ( data.error && 'missing_attachment' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p><?php echo $this->l10n['missing_attachment']; ?></p>
+                               </div>
+                       <# } else if ( data.error && 'unsupported_file_type' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p><?php echo $this->l10n['unsupported_file_type']; ?></p>
+                               </div>
+                       <# } else if ( data.error ) { #>
+                               <div class="notice notice-error notice-alt">
+                                       <p><?php _e( 'Unable to preview media due to an unknown error.' ); ?></p>
+                               </div>
+                       <# } else if ( data.is_hosted_embed && data.model.poster ) { #>
+                               <a href="{{ data.model.src }}" target="_blank" class="media-widget-video-link">
+                                       <img src="{{ data.model.poster }}" />
+                               </a>
+                       <# } else if ( data.is_hosted_embed ) { #>
+                               <a href="{{ data.model.src }}" target="_blank" class="media-widget-video-link no-poster">
+                                       <span class="dashicons dashicons-format-video"></span>
+                               </a>
+                       <# } else if ( data.model.src ) { #>
+                               <?php wp_underscore_video_template() ?>
+                       <# } #>
+               </script>
+               <?php
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludeswidgetsclasswpwidgetmediaphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/widgets/class-wp-widget-media.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets/class-wp-widget-media.php                           (rev 0)
+++ trunk/src/wp-includes/widgets/class-wp-widget-media.php     2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,422 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Widget API: WP_Media_Widget class
+ *
+ * @package WordPress
+ * @subpackage Widgets
+ * @since 4.8.0
+ */
+
+/**
+ * Core class that implements a media widget.
+ *
+ * @since 4.8.0
+ *
+ * @see WP_Widget
+ */
+abstract class WP_Widget_Media extends WP_Widget {
+
+       /**
+        * Translation labels.
+        *
+        * @since 4.8.0
+        * @var array
+        */
+       public $l10n = array(
+               'add_to_widget' => '',
+               'replace_media' => '',
+               'edit_media' => '',
+               'media_library_state_multi' => '',
+               'media_library_state_single' => '',
+               'missing_attachment' => '',
+               'no_media_selected' => '',
+               'add_media' => '',
+       );
+
+       /**
+        * Constructor.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @param string $id_base         Base ID for the widget, lowercase and unique.
+        * @param string $name            Name for the widget displayed on the configuration page.
+        * @param array  $widget_options  Optional. Widget options. See wp_register_sidebar_widget() for
+        *                                information on accepted arguments. Default empty array.
+        * @param array  $control_options Optional. Widget control options. See wp_register_widget_control()
+        *                                for information on accepted arguments. Default empty array.
+        */
+       public function __construct( $id_base, $name, $widget_options = array(), $control_options = array() ) {
+               $widget_opts = wp_parse_args( $widget_options, array(
+                       'description' => __( 'A media item.' ),
+                       'customize_selective_refresh' => true,
+                       'mime_type' => '',
+               ) );
+
+               $control_opts = wp_parse_args( $control_options, array() );
+
+               $l10n_defaults = array(
+                       'no_media_selected' => __( 'No media selected' ),
+                       'add_media' => _x( 'Add Media', 'label for button in the media widget; should not be longer than ~13 characters long' ),
+                       'replace_media' => _x( 'Replace Media', 'label for button in the media widget; should not be longer than ~13 characters long' ),
+                       'edit_media' => _x( 'Edit Media', 'label for button in the media widget; should not be longer than ~13 characters long' ),
+                       'add_to_widget' => __( 'Add to Widget' ),
+                       'missing_attachment' => sprintf(
+                               /* translators: placeholder is URL to media library */
+                               __( 'We can&#8217;t find that file. Check your <a href="%s">media library</a> and make sure it wasn&#8217;t deleted.' ),
+                               esc_url( admin_url( 'upload.php' ) )
+                       ),
+                       /* translators: %d is widget count */
+                       'media_library_state_multi' => _n_noop( 'Media Widget (%d)', 'Media Widget (%d)' ),
+                       'media_library_state_single' => __( 'Media Widget' ),
+                       'unsupported_file_type' => __( 'Looks like this isn&#8217;t the correct kind of file. Please link to an appropriate file instead.' ),
+               );
+               $this->l10n = array_merge( $l10n_defaults, array_filter( $this->l10n ) );
+
+               parent::__construct(
+                       $id_base,
+                       $name,
+                       $widget_opts,
+                       $control_opts
+               );
+       }
+
+       /**
+        * Add hooks while registering all widget instances of this widget class.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function _register() {
+
+               // Note that the widgets component in the customizer will also do the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts().
+               add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) );
+
+               if ( $this->is_preview() ) {
+                       add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
+               }
+
+               // Note that the widgets component in the customizer will also do the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts().
+               add_action( 'admin_footer-widgets.php', array( $this, 'render_control_template_scripts' ) );
+
+               add_filter( 'display_media_states', array( $this, 'display_media_state' ), 10, 2 );
+
+               parent::_register();
+       }
+
+       /**
+        * Get schema for properties of a widget instance (item).
+        *
+        * @since  4.8.0
+        * @access public
+        *
+        * @see WP_REST_Controller::get_item_schema()
+        * @see WP_REST_Controller::get_additional_fields()
+        * @link https://core.trac.wordpress.org/ticket/35574
+        * @return array Schema for properties.
+        */
+       public function get_instance_schema() {
+               return array(
+                       'attachment_id' => array(
+                               'type' => 'integer',
+                               'default' => 0,
+                               'minimum' => 0,
+                               'description' => __( 'Attachment post ID' ),
+                               'media_prop' => 'id',
+                       ),
+                       'url' => array(
+                               'type' => 'string',
+                               'default' => '',
+                               'format' => 'uri',
+                               'description' => __( 'URL to the media file' ),
+                       ),
+                       'title' => array(
+                               'type' => 'string',
+                               'default' => '',
+                               'sanitize_callback' => 'sanitize_text_field',
+                               'description' => __( 'Title for the widget' ),
+                               'should_preview_update' => false,
+                       ),
+               );
+       }
+
+       /**
+        * Determine if the supplied attachment is for a valid attachment post with the specified MIME type.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @param int|WP_Post $attachment Attachment post ID or object.
+        * @param string      $mime_type  MIME type.
+        * @return bool Is matching MIME type.
+        */
+       public function is_attachment_with_mime_type( $attachment, $mime_type ) {
+               if ( empty( $attachment ) ) {
+                       return false;
+               }
+               $attachment = get_post( $attachment );
+               if ( ! $attachment ) {
+                       return false;
+               }
+               if ( 'attachment' !== $attachment->post_type ) {
+                       return false;
+               }
+               return wp_attachment_is( $mime_type, $attachment );
+       }
+
+       /**
+        * Sanitize a token list string, such as used in HTML rel and class attributes.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @link http://w3c.github.io/html/infrastructure.html#space-separated-tokens
+        * @link https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList
+        * @param string|array $tokens List of tokens separated by spaces, or an array of tokens.
+        * @return string Sanitized token string list.
+        */
+       public function sanitize_token_list( $tokens ) {
+               if ( is_string( $tokens ) ) {
+                       $tokens = preg_split( '/\s+/', trim( $tokens ) );
+               }
+               $tokens = array_map( 'sanitize_html_class', $tokens );
+               $tokens = array_filter( $tokens );
+               return join( ' ', $tokens );
+       }
+
+       /**
+        * Displays the widget on the front-end.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @see WP_Widget::widget()
+        *
+        * @param array $args     Display arguments including before_title, after_title, before_widget, and after_widget.
+        * @param array $instance Saved setting from the database.
+        */
+       public function widget( $args, $instance ) {
+               $instance = wp_parse_args( $instance, wp_list_pluck( $this->get_instance_schema(), 'default' ) );
+
+               // Short-circuit if no media is selected.
+               if ( ! $this->has_content( $instance ) ) {
+                       return;
+               }
+
+               echo $args['before_widget'];
+
+               if ( $instance['title'] ) {
+
+                       /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
+                       $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
+                       echo $args['before_title'] . $title . $args['after_title'];
+               }
+
+               /**
+                * Filters the media widget instance prior to rendering the media.
+                *
+                * @since 4.8.0
+                *
+                * @param array           $instance Instance data.
+                * @param array           $args     Widget args.
+                * @param WP_Widget_Media $this     Widget object.
+                */
+               $instance = apply_filters( "widget_{$this->id_base}_instance", $instance, $args, $this );
+
+               $this->render_media( $instance );
+
+               echo $args['after_widget'];
+       }
+
+       /**
+        * Sanitizes the widget form values as they are saved.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @see WP_Widget::update()
+        * @see WP_REST_Request::has_valid_params()
+        * @see WP_REST_Request::sanitize_params()
+        *
+        * @param array $new_instance Values just sent to be saved.
+        * @param array $instance     Previously saved values from database.
+        * @return array Updated safe values to be saved.
+        */
+       public function update( $new_instance, $instance ) {
+
+               $schema = $this->get_instance_schema();
+               foreach ( $schema as $field => $field_schema ) {
+                       if ( ! array_key_exists( $field, $new_instance ) ) {
+                               continue;
+                       }
+                       $value = $new_instance[ $field ];
+                       if ( true !== rest_validate_value_from_schema( $value, $field_schema, $field ) ) {
+                               continue;
+                       }
+
+                       $value = rest_sanitize_value_from_schema( $value, $field_schema );
+
+                       // @codeCoverageIgnoreStart
+                       if ( is_wp_error( $value ) ) {
+                               continue; // Handle case when rest_sanitize_value_from_schema() ever returns WP_Error as its phpdoc @return tag indicates.
+                       }
+
+                       // @codeCoverageIgnoreEnd
+                       if ( isset( $field_schema['sanitize_callback'] ) ) {
+                               $value = call_user_func( $field_schema['sanitize_callback'], $value );
+                       }
+                       if ( is_wp_error( $value ) ) {
+                               continue;
+                       }
+                       $instance[ $field ] = $value;
+               }
+
+               return $instance;
+       }
+
+       /**
+        * Render the media on the frontend.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @param array $instance Widget instance props.
+        * @return string
+        */
+       abstract public function render_media( $instance );
+
+       /**
+        * Outputs the settings update form.
+        *
+        * Note that the widget UI itself is rendered with JavaScript via `MediaWidgetControl#render()`.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @see \WP_Widget_Media::render_control_template_scripts() Where the JS template is located.
+        * @param array $instance Current settings.
+        * @return void
+        */
+       final public function form( $instance ) {
+               $instance_schema = $this->get_instance_schema();
+               $instance = wp_array_slice_assoc(
+                       wp_parse_args( (array) $instance, wp_list_pluck( $instance_schema, 'default' ) ),
+                       array_keys( $instance_schema )
+               );
+
+               foreach ( $instance as $name => $value ) : ?>
+                       <input
+                               type="hidden"
+                               data-property="<?php echo esc_attr( $name ); ?>"
+                               class="media-widget-instance-property"
+                               name="<?php echo esc_attr( $this->get_field_name( $name ) ); ?>"
+                               id="<?php echo esc_attr( $this->get_field_id( $name ) ); // Needed specifically by wpWidgets.appendTitle(). ?>"
+                               value="<?php echo esc_attr( strval( $value ) ); ?>"
+                       />
+               <?php
+               endforeach;
+       }
+
+       /**
+        * Filters the default media display states for items in the Media list table.
+        *
+        * @since 4.8.0
+        * @access public
+        *
+        * @param array   $states An array of media states.
+        * @param WP_Post $post   The current attachment object.
+        * @return array
+        */
+       public function display_media_state( $states, $post = null ) {
+               if ( ! $post ) {
+                       $post = get_post();
+               }
+
+               // Count how many times this attachment is used in widgets.
+               $use_count = 0;
+               foreach ( $this->get_settings() as $instance ) {
+                       if ( isset( $instance['attachment_id'] ) && $instance['attachment_id'] === $post->ID ) {
+                               $use_count++;
+                       }
+               }
+
+               if ( 1 === $use_count ) {
+                       $states[] = $this->l10n['media_library_state_single'];
+               } elseif ( $use_count > 0 ) {
+                       $states[] = sprintf( translate_nooped_plural( $this->l10n['media_library_state_multi'], $use_count ), number_format_i18n( $use_count ) );
+               }
+
+               return $states;
+       }
+
+       /**
+        * Enqueue preview scripts.
+        *
+        * These scripts normally are enqueued just-in-time when a widget is rendered.
+        * In the customizer, however, widgets can be dynamically added and rendered via
+        * selective refresh, and so it is important to unconditionally enqueue them in
+        * case a widget does get added.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_preview_scripts() {}
+
+       /**
+        * Loads the required scripts and styles for the widget control.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function enqueue_admin_scripts() {
+               wp_enqueue_media();
+               wp_enqueue_script( 'media-widgets' );
+       }
+
+       /**
+        * Render form template scripts.
+        *
+        * @since 4.8.0
+        * @access public
+        */
+       public function render_control_template_scripts() {
+               ?>
+               <script type="text/html" id="tmpl-widget-media-<?php echo esc_attr( $this->id_base ); ?>-control">
+                       <# var elementIdPrefix = 'el' + String( Math.random() ) + '_' #>
+                       <p>
+                               <label for="{{ elementIdPrefix }}title"><?php esc_html_e( 'Title:' ); ?></label>
+                               <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
+                       </p>
+                       <div class="media-widget-preview">
+                               <div class="attachment-media-view">
+                                       <div class="placeholder"><?php echo esc_html( $this->l10n['no_media_selected'] ); ?></div>
+                               </div>
+                       </div>
+                       <p class="media-widget-buttons">
+                               <button type="button" class="button edit-media selected">
+                                       <?php echo esc_html( $this->l10n['edit_media'] ); ?>
+                               </button>
+                               <button type="button" class="button change-media select-media selected">
+                                       <?php echo esc_html( $this->l10n['replace_media'] ); ?>
+                               </button>
+                               <button type="button" class="button select-media not-selected">
+                                       <?php echo esc_html( $this->l10n['add_media'] ); ?>
+                               </button>
+                       </p>
+               </script>
+               <?php
+       }
+
+       /**
+        * Whether the widget has content to show.
+        *
+        * @since 4.8.0
+        * @access protected
+        *
+        * @param array $instance Widget instance props.
+        * @return bool Whether widget has content.
+        */
+       protected function has_content( $instance ) {
+               return ( $instance['attachment_id'] && 'attachment' === get_post_type( $instance['attachment_id'] ) ) || $instance['url'];
+       }
+}
</ins></span></pre></div>
<a id="trunksrcwpincludeswidgetsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/widgets.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/widgets.php 2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/src/wp-includes/widgets.php   2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1436,36 +1436,44 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.2.0
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_widgets_init() {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( !is_blog_installed() )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( ! is_blog_installed() ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 return;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Pages');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Pages' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Calendar');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Calendar' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Archives');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Archives' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( get_option( 'link_manager_enabled' ) )
-               register_widget('WP_Widget_Links');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( get_option( 'link_manager_enabled' ) ) {
+               register_widget( 'WP_Widget_Links' );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Meta');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Media_Audio' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Search');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Media_Image' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Text');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Media_Video' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Categories');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Meta' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Recent_Posts');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Search' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Recent_Comments');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Text' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_RSS');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Categories' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Widget_Tag_Cloud');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Recent_Posts' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        register_widget('WP_Nav_Menu_Widget');
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ register_widget( 'WP_Widget_Recent_Comments' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        register_widget( 'WP_Widget_RSS' );
+
+       register_widget( 'WP_Widget_Tag_Cloud' );
+
+       register_widget( 'WP_Nav_Menu_Widget' );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Fires after all default WordPress widgets have been registered.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span></span></pre></div>
<a id="trunktestsphpunitdataimagescanola150x150jpg"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/images/canola-150x150.jpg</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/images/canola-150x150.jpg
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/images/canola-150x150.jpg   2017-05-11 19:53:38 UTC (rev 40639)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/images/canola-150x150.jpg    2017-05-11 21:10:54 UTC (rev 40640)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/images/canola-150x150.jpg
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdataimagescanola300x225jpg"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/images/canola-300x225.jpg</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/images/canola-300x225.jpg
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/images/canola-300x225.jpg   2017-05-11 19:53:38 UTC (rev 40639)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/images/canola-300x225.jpg    2017-05-11 21:10:54 UTC (rev 40640)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/images/canola-300x225.jpg
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatauploadssmallaudiomp3"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/uploads/small-audio.mp3</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/uploads/small-audio.mp3
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/uploads/small-audio.mp3     2017-05-11 19:53:38 UTC (rev 40639)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/uploads/small-audio.mp3      2017-05-11 21:10:54 UTC (rev 40640)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/uploads/small-audio.mp3
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunitdatauploadssmallvideomp4"></a>
<div class="binary"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/data/uploads/small-video.mp4</h4>
<pre class="diff"><span>
<span class="cx">(Binary files differ)
</span></span></pre></div>
<span class="cx" style="display: block; padding: 0 10px">Index: trunk/tests/phpunit/data/uploads/small-video.mp4
</span><span class="cx" style="display: block; padding: 0 10px">===================================================================
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">--- trunk/tests/phpunit/data/uploads/small-video.mp4     2017-05-11 19:53:38 UTC (rev 40639)
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+++ trunk/tests/phpunit/data/uploads/small-video.mp4      2017-05-11 21:10:54 UTC (rev 40640)
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/data/uploads/small-video.mp4
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span><a id="svnmimetype"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:mime-type</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+application/octet-stream
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestswidgetsmediaaudiowidgetphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/widgets/media-audio-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/media-audio-widget.php                          (rev 0)
+++ trunk/tests/phpunit/tests/widgets/media-audio-widget.php    2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,266 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_Widget_Media_Audio functionality.
+ *
+ * @package    WordPress
+ * @subpackage widgets
+ */
+
+/**
+ * Test wp-includes/widgets/class-wp-widget-audio.php
+ *
+ * @group widgets
+ */
+class Test_WP_Widget_Media_Audio extends WP_UnitTestCase {
+
+       /**
+        * Clean up global scope.
+        *
+        * @global WP_Scripts $wp_scripts
+        * @global WP_Styles $wp_styles
+        */
+       function clean_up_global_scope() {
+               global $wp_scripts, $wp_styles;
+               parent::clean_up_global_scope();
+               $wp_scripts = null;
+               $wp_styles = null;
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media_Audio::get_instance_schema
+        */
+       function test_get_instance_schema() {
+               $wp_widget_audio = new WP_Widget_Media_Audio();
+               $schema = $wp_widget_audio->get_instance_schema();
+
+               $this->assertEqualSets(
+                       array_merge(
+                               array(
+                                       'attachment_id',
+                                       'preload',
+                                       'loop',
+                                       'title',
+                                       'url',
+                               ),
+                               wp_get_audio_extensions()
+                       ),
+                       array_keys( $schema )
+               );
+       }
+
+       /**
+        * Test constructor.
+        *
+        * @covers WP_Widget_Media_Audio::__construct()
+        */
+       function test_constructor() {
+               $widget = new WP_Widget_Media_Audio();
+
+               $this->assertArrayHasKey( 'mime_type', $widget->widget_options );
+               $this->assertArrayHasKey( 'customize_selective_refresh', $widget->widget_options );
+               $this->assertArrayHasKey( 'description', $widget->widget_options );
+               $this->assertTrue( $widget->widget_options['customize_selective_refresh'] );
+               $this->assertEquals( 'audio', $widget->widget_options['mime_type'] );
+               $this->assertEqualSets( array(
+                       'add_to_widget',
+                       'replace_media',
+                       'edit_media',
+                       'media_library_state_multi',
+                       'media_library_state_single',
+                       'missing_attachment',
+                       'no_media_selected',
+                       'add_media',
+                       'unsupported_file_type',
+               ), array_keys( $widget->l10n ) );
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media_Audio::update
+        */
+       function test_update() {
+               $widget = new WP_Widget_Media_Audio();
+               $instance = array();
+
+               // Should return valid attachment ID.
+               $expected = array(
+                       'attachment_id' => 1,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment ID.
+               $result = $widget->update( array(
+                       'attachment_id' => 'media',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid attachment url.
+               $expected = array(
+                       'url' => 'https://chickenandribs.org',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment url.
+               $result = $widget->update( array(
+                       'url' => 'not_a_url',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+               $this->assertStringStartsWith( 'http://', $result['url'] );
+
+               // Should return loop setting.
+               $expected = array(
+                       'loop' => true,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid loop setting.
+               $result = $widget->update( array(
+                       'loop' => 'not-boolean',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid attachment title.
+               $expected = array(
+                       'title' => 'An audio sample of parrots',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment title.
+               $result = $widget->update( array(
+                       'title' => '<h1>Cute Baby Goats</h1>',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+
+               // Should return valid preload setting.
+               $expected = array(
+                       'preload' => 'none',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid preload setting.
+               $result = $widget->update( array(
+                       'preload' => 'nope',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should filter invalid key.
+               $result = $widget->update( array(
+                       'h4x' => 'value',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+       }
+
+       /**
+        * Test render_media method.
+        *
+        * @covers WP_Widget_Media_Audio::render_media
+        */
+       function test_render_media() {
+               $test_audio_file = __FILE__ . '../../data/uploads/small-audio.mp3';
+               $widget = new WP_Widget_Media_Audio();
+               $attachment_id = self::factory()->attachment->create_object( array(
+                       'file' => $test_audio_file,
+                       'post_parent' => 0,
+                       'post_mime_type' => 'audio/mp3',
+                       'post_title' => 'Test Audio',
+               ) );
+               wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $test_audio_file ) );
+
+               // Should be empty when there is no attachment_id.
+               ob_start();
+               $widget->render_media( array() );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+
+               // Should be empty when there is an invalid attachment_id.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => 777,
+               ) );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+
+               // Tests with audio from library.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+               ) );
+               $output = ob_get_clean();
+
+               // Check default outputs.
+               $this->assertContains( 'preload="none"', $output );
+               $this->assertContains( 'class="wp-audio-shortcode"', $output );
+               $this->assertContains( 'small-audio.mp3', $output );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'title' => 'Funny',
+                       'preload' => 'auto',
+                       'loop' => true,
+               ) );
+               $output = ob_get_clean();
+
+               // Custom attributes.
+               $this->assertContains( 'preload="auto"', $output );
+               $this->assertContains( 'loop="1"', $output );
+       }
+
+       /**
+        * Test enqueue_preview_scripts method.
+        *
+        * @global WP_Scripts $wp_scripts
+        * @global WP_Styles $wp_styles
+        * @covers WP_Widget_Media_Audio::enqueue_preview_scripts
+        */
+       function test_enqueue_preview_scripts() {
+               global $wp_scripts, $wp_styles;
+               $wp_scripts = null;
+               $wp_styles = null;
+               $widget = new WP_Widget_Media_Audio();
+
+               $this->assertFalse( wp_script_is( 'wp-mediaelement' ) );
+               $this->assertFalse( wp_style_is( 'wp-mediaelement' ) );
+
+               $widget->enqueue_preview_scripts();
+
+               $this->assertTrue( wp_script_is( 'wp-mediaelement' ) );
+               $this->assertTrue( wp_style_is( 'wp-mediaelement' ) );
+       }
+
+       /**
+        * Test enqueue_admin_scripts method.
+        *
+        * @covers WP_Widget_Media_Audio::enqueue_admin_scripts
+        */
+       function test_enqueue_admin_scripts() {
+               set_current_screen( 'widgets.php' );
+               $widget = new WP_Widget_Media_Audio();
+               $widget->enqueue_admin_scripts();
+
+               $this->assertTrue( wp_script_is( 'media-audio-widget' ) );
+       }
+
+       /**
+        * Test render_control_template_scripts method.
+        *
+        * @covers WP_Widget_Media_Audio::render_control_template_scripts
+        */
+       function test_render_control_template_scripts() {
+               $widget = new WP_Widget_Media_Audio();
+
+               ob_start();
+               $widget->render_control_template_scripts();
+               $output = ob_get_clean();
+
+               $this->assertContains( '<script type="text/html" id="tmpl-wp-media-widget-audio-preview">', $output );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestswidgetsmediaimagewidgetphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/widgets/media-image-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/media-image-widget.php                          (rev 0)
+++ trunk/tests/phpunit/tests/widgets/media-image-widget.php    2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,480 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_Widget_Media_Image functionality.
+ *
+ * @package    WordPress
+ * @subpackage widgets
+ */
+
+/**
+ * Test wp-includes/widgets/class-wp-widget-image.php
+ *
+ * @group widgets
+ */
+class Test_WP_Widget_Media_Image extends WP_UnitTestCase {
+
+       /**
+        * Clean up global scope.
+        *
+        * @global WP_Scripts $wp_scripts
+        * @global WP_Styles $wp_styles
+        */
+       function clean_up_global_scope() {
+               global $wp_scripts, $wp_styles;
+               parent::clean_up_global_scope();
+               $wp_scripts = null;
+               $wp_styles = null;
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media_Image::get_instance_schema
+        */
+       function test_get_instance_schema() {
+               $widget = new WP_Widget_Media_Image();
+               $schema = $widget->get_instance_schema();
+
+               $this->assertEqualSets( array(
+                       'alt',
+                       'attachment_id',
+                       'caption',
+                       'height',
+                       'image_classes',
+                       'image_title',
+                       'link_classes',
+                       'link_rel',
+                       'link_target_blank',
+                       'link_type',
+                       'link_url',
+                       'size',
+                       'title',
+                       'url',
+                       'width',
+               ), array_keys( $schema ) );
+       }
+
+       /**
+        * Test constructor.
+        *
+        * @covers WP_Widget_Media_Image::__construct()
+        */
+       function test_constructor() {
+               $widget = new WP_Widget_Media_Image();
+
+               $this->assertArrayHasKey( 'mime_type', $widget->widget_options );
+               $this->assertArrayHasKey( 'customize_selective_refresh', $widget->widget_options );
+               $this->assertArrayHasKey( 'description', $widget->widget_options );
+               $this->assertTrue( $widget->widget_options['customize_selective_refresh'] );
+               $this->assertEquals( 'image', $widget->widget_options['mime_type'] );
+               $this->assertEqualSets( array(
+                       'add_to_widget',
+                       'replace_media',
+                       'edit_media',
+                       'media_library_state_multi',
+                       'media_library_state_single',
+                       'missing_attachment',
+                       'no_media_selected',
+                       'add_media',
+                       'unsupported_file_type',
+               ), array_keys( $widget->l10n ) );
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media_Image::update
+        */
+       function test_update() {
+               $widget = new WP_Widget_Media_Image();
+               $instance = array();
+
+               // Should return valid attachment ID.
+               $expected = array(
+                       'attachment_id' => 1,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment ID.
+               $result = $widget->update( array(
+                       'attachment_id' => 'media',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid attachment url.
+               $expected = array(
+                       'url' => 'https://example.org',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment url.
+               $result = $widget->update( array(
+                       'url' => 'not_a_url',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+               $this->assertStringStartsWith( 'http://', $result['url'] );
+
+               // Should return valid attachment title.
+               $expected = array(
+                       'title' => 'What a title',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment title.
+               $result = $widget->update( array(
+                       'title' => '<h1>W00t!</h1>',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+
+               // Should return valid image size.
+               $expected = array(
+                       'size' => 'thumbnail',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid image size.
+               $result = $widget->update( array(
+                       'size' => 'big league',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid image width.
+               $expected = array(
+                       'width' => 300,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid image width.
+               $result = $widget->update( array(
+                       'width' => 'wide',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid image height.
+               $expected = array(
+                       'height' => 200,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid image height.
+               $result = $widget->update( array(
+                       'height' => 'high',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid image caption.
+               $expected = array(
+                       'caption' => 'A caption with <a href="#">link</a>',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid image caption.
+               $result = $widget->update( array(
+                       'caption' => '"><i onload="alert(\'hello\')" />',
+               ), $instance );
+               $this->assertSame( $result, array(
+                       'caption' => '"&gt;<i />',
+               ) );
+
+               // Should return valid alt text.
+               $expected = array(
+                       'alt' => 'A water tower',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid alt text.
+               $result = $widget->update( array(
+                       'alt' => '"><i onload="alert(\'hello\')" />',
+               ), $instance );
+               $this->assertSame( $result, array(
+                       'alt' => '">',
+               ) );
+
+               // Should return valid link type.
+               $expected = array(
+                       'link_type' => 'file',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid link type.
+               $result = $widget->update( array(
+                       'link_type' => 'interesting',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid link url.
+               $expected = array(
+                       'link_url' => 'https://example.org',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid link url.
+               $result = $widget->update( array(
+                       'link_url' => 'not_a_url',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+               $this->assertStringStartsWith( 'http://', $result['link_url'] );
+
+               // Should return valid image classes.
+               $expected = array(
+                       'image_classes' => 'A water tower',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid image classes.
+               $result = $widget->update( array(
+                       'image_classes' => '"><i onload="alert(\'hello\')" />',
+               ), $instance );
+               $this->assertSame( $result, array(
+                       'image_classes' => 'i onloadalerthello',
+               ) );
+
+               // Should return valid link classes.
+               $expected = array(
+                       'link_classes' => 'A water tower',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid link classes.
+               $result = $widget->update( array(
+                       'link_classes' => '"><i onload="alert(\'hello\')" />',
+               ), $instance );
+               $this->assertSame( $result, array(
+                       'link_classes' => 'i onloadalerthello',
+               ) );
+
+               // Should return valid rel text.
+               $expected = array(
+                       'link_rel' => 'previous',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid rel text.
+               $result = $widget->update( array(
+                       'link_rel' => '"><i onload="alert(\'hello\')" />',
+               ), $instance );
+               $this->assertSame( $result, array(
+                       'link_rel' => 'i onloadalerthello',
+               ) );
+
+               // Should return valid link target.
+               $expected = array(
+                       'link_target_blank' => false,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid  link target.
+               $result = $widget->update( array(
+                       'link_target_blank' => 'top',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid image title.
+               $expected = array(
+                       'image_title' => 'What a title',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid image title.
+               $result = $widget->update( array(
+                       'image_title' => '<h1>W00t!</h1>',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+
+               // Should filter invalid key.
+               $result = $widget->update( array(
+                       'imaginary_key' => 'value',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+       }
+
+       /**
+        * Test render_media method.
+        *
+        * @covers WP_Widget_Media_Image::render_media
+        */
+       function test_render_media() {
+               $widget = new WP_Widget_Media_Image();
+               $attachment_id = self::factory()->attachment->create_object( array(
+                       'file' => DIR_TESTDATA . '/images/canola.jpg',
+                       'post_parent' => 0,
+                       'post_mime_type' => 'image/jpeg',
+                       'post_title' => 'Canola',
+               ) );
+               wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, DIR_TESTDATA . '/images/canola.jpg' ) );
+
+               // Should be empty when there is no attachment_id.
+               ob_start();
+               $widget->render_media( array() );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+
+               // Should be empty when there is an invalid attachment_id.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => 666,
+               ) );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+               ) );
+               $output = ob_get_clean();
+
+               // No default title.
+               $this->assertNotContains( 'title="', $output );
+               // Default image classes.
+               $this->assertContains( 'class="image wp-image-' . $attachment_id, $output );
+               $this->assertContains( 'style="max-width: 100%; height: auto;"', $output );
+               $this->assertContains( 'alt=""', $output );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'image_title' => 'Custom Title',
+                       'image_classes' => 'custom-class',
+                       'alt' => 'A flower',
+                       'size' => 'custom',
+                       'width' => 100,
+                       'height' => 100,
+               ) );
+               $output = ob_get_clean();
+
+               // Custom image title.
+               $this->assertContains( 'title="Custom Title"', $output );
+               // Custom image class.
+               $this->assertContains( 'class="image wp-image-' . $attachment_id . ' custom-class', $output );
+               $this->assertContains( 'alt="A flower"', $output );
+               $this->assertContains( 'width="100"', $output );
+               $this->assertContains( 'height="100"', $output );
+
+               // Embeded images.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => null,
+                       'caption' => 'With caption',
+                       'height' => 100,
+                       'link_type' => 'file',
+                       'url' => 'http://example.org/url/to/image.jpg',
+                       'width' => 100,
+               ) );
+               $output = ob_get_clean();
+
+               // Custom image class.
+               $this->assertContains( 'src="http://example.org/url/to/image.jpg"', $output );
+
+               // Link settings.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'link_type' => 'file',
+               ) );
+               $output = ob_get_clean();
+
+               $link = '<a href="' . wp_get_attachment_url( $attachment_id ) . '"';
+               $this->assertContains( $link, $output );
+               $link .= ' class=""';
+               $this->assertContains( $link, $output );
+               $link .= ' rel=""';
+               $this->assertContains( $link, $output );
+               $link .= ' target=""';
+               $this->assertContains( $link, $output );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'link_type' => 'post',
+                       'link_classes' => 'custom-link-class',
+                       'link_rel' => 'attachment',
+                       'link_target_blank' => false,
+               ) );
+               $output = ob_get_clean();
+
+               $this->assertContains( '<a href="' . get_attachment_link( $attachment_id ) . '"', $output );
+               $this->assertContains( 'class="custom-link-class"', $output );
+               $this->assertContains( 'rel="attachment"', $output );
+               $this->assertContains( 'target=""', $output );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'link_type' => 'custom',
+                       'link_url' => 'https://example.org',
+                       'link_target_blank' => true,
+               ) );
+               $output = ob_get_clean();
+
+               $this->assertContains( '<a href="https://example.org"', $output );
+               $this->assertContains( 'target="_blank"', $output );
+
+               // Caption settings.
+               wp_update_post( array(
+                       'ID' => $attachment_id,
+                       'post_excerpt' => 'Default caption',
+               ) );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+               ) );
+               $output = ob_get_clean();
+
+               $this->assertContains( 'class="wp-caption alignnone"', $output );
+               $this->assertContains( '<p class="wp-caption-text">Default caption</p>', $output );
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'caption' => 'Custom caption',
+               ) );
+               $output = ob_get_clean();
+
+               $this->assertContains( 'class="wp-caption alignnone"', $output );
+               $this->assertContains( '<p class="wp-caption-text">Custom caption</p>', $output );
+       }
+
+       /**
+        * Test enqueue_admin_scripts method.
+        *
+        * @covers WP_Widget_Media_Image::enqueue_admin_scripts
+        */
+       function test_enqueue_admin_scripts() {
+               set_current_screen( 'widgets.php' );
+               $widget = new WP_Widget_Media_Image();
+               $widget->enqueue_admin_scripts();
+
+               $this->assertTrue( wp_script_is( 'media-image-widget' ) );
+       }
+
+       /**
+        * Test render_control_template_scripts method.
+        *
+        * @covers WP_Widget_Media_Image::render_control_template_scripts
+        */
+       function test_render_control_template_scripts() {
+               $widget = new WP_Widget_Media_Image();
+
+               ob_start();
+               $widget->render_control_template_scripts();
+               $output = ob_get_clean();
+
+               $this->assertContains( '<script type="text/html" id="tmpl-wp-media-widget-image-preview">', $output );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestswidgetsmediavideowidgetphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/widgets/media-video-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/media-video-widget.php                          (rev 0)
+++ trunk/tests/phpunit/tests/widgets/media-video-widget.php    2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,292 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_Widget_Media_Video functionality.
+ *
+ * @package    WordPress
+ * @subpackage widgets
+ */
+
+/**
+ * Test wp-includes/widgets/class-wp-widget-video.php
+ *
+ * @group widgets
+ */
+class Test_WP_Widget_Media_Video extends WP_UnitTestCase {
+
+       /**
+        * Clean up global scope.
+        *
+        * @global WP_Scripts $wp_scripts
+        * @global WP_Styles $wp_styles
+        */
+       function clean_up_global_scope() {
+               global $wp_scripts, $wp_styles;
+               parent::clean_up_global_scope();
+               $wp_scripts = null;
+               $wp_styles = null;
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media_Video::get_instance_schema()
+        */
+       function test_get_instance_schema() {
+               $widget = new WP_Widget_Media_Video();
+               $schema = $widget->get_instance_schema();
+
+               $this->assertEqualSets(
+                       array_merge(
+                               array(
+                                       'attachment_id',
+                                       'preload',
+                                       'loop',
+                                       'title',
+                                       'url',
+                                       'content',
+                               ),
+                               wp_get_video_extensions()
+                       ),
+                       array_keys( $schema )
+               );
+       }
+
+       /**
+        * Test constructor.
+        *
+        * @covers WP_Widget_Media_Video::__construct()
+        */
+       function test_constructor() {
+               $widget = new WP_Widget_Media_Video();
+
+               $this->assertArrayHasKey( 'mime_type', $widget->widget_options );
+               $this->assertArrayHasKey( 'customize_selective_refresh', $widget->widget_options );
+               $this->assertArrayHasKey( 'description', $widget->widget_options );
+               $this->assertTrue( $widget->widget_options['customize_selective_refresh'] );
+               $this->assertEquals( 'video', $widget->widget_options['mime_type'] );
+               $this->assertEqualSets( array(
+                       'add_to_widget',
+                       'replace_media',
+                       'unsupported_file_type',
+                       'edit_media',
+                       'media_library_state_multi',
+                       'media_library_state_single',
+                       'missing_attachment',
+                       'no_media_selected',
+                       'add_media',
+               ), array_keys( $widget->l10n ) );
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media_Video::update()
+        */
+       function test_update() {
+               $widget = new WP_Widget_Media_Video();
+               $instance = array();
+
+               // Should return valid attachment ID.
+               $expected = array(
+                       'attachment_id' => 1,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment ID.
+               $result = $widget->update( array(
+                       'attachment_id' => 'media',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid attachment url.
+               $expected = array(
+                       'url' => 'https://chickenandribs.org',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment url.
+               $result = $widget->update( array(
+                       'url' => 'not_a_url',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+               $this->assertStringStartsWith( 'http://', $result['url'] );
+
+               // Should return loop setting.
+               $expected = array(
+                       'loop' => true,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid loop setting.
+               $result = $widget->update( array(
+                       'loop' => 'not-boolean',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid attachment title.
+               $expected = array(
+                       'title' => 'A video of goats',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment title.
+               $result = $widget->update( array(
+                       'title' => '<h1>Cute Baby Goats</h1>',
+               ), $instance );
+               $this->assertNotSame( $result, $instance );
+
+               // Should return valid preload setting.
+               $expected = array(
+                       'preload' => 'none',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid preload setting.
+               $result = $widget->update( array(
+                       'preload' => 'nope',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+
+               // Should filter invalid key.
+               $result = $widget->update( array(
+                       'h4x' => 'value',
+               ), $instance );
+               $this->assertSame( $result, $instance );
+       }
+
+       /**
+        * Test render_media method.
+        *
+        * @covers WP_Widget_Media_Video::render_media()
+        * @covers WP_Widget_Media_Video::inject_video_max_width_style()
+        */
+       function test_render_media() {
+               $test_movie_file = __FILE__ . '../../data/uploads/small-video.m4v';
+               $widget = new WP_Widget_Media_Video();
+               $attachment_id = self::factory()->attachment->create_object( array(
+                       'file' => $test_movie_file,
+                       'post_parent' => 0,
+                       'post_mime_type' => 'video/mp4',
+                       'post_title' => 'Test Video',
+               ) );
+               wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $test_movie_file ) );
+
+               // Should be empty when there is no attachment_id.
+               ob_start();
+               $widget->render_media( array() );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+
+               // Should be empty when there is an invalid attachment_id.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => 777,
+               ) );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+
+               // Tests with video from library.
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+               ) );
+               $output = ob_get_clean();
+
+               // Check default outputs.
+               $this->assertContains( 'preload="metadata"', $output );
+               $this->assertContains( 'class="wp-video"', $output );
+               $this->assertContains( 'width:100%', $output );
+               $this->assertNotContains( 'height=', $output );
+               $this->assertNotContains( 'width="', $output );
+               $this->assertContains( 'small-video.m4v', $output );// Auto parses dimensions.
+
+               ob_start();
+               $widget->render_media( array(
+                       'attachment_id' => $attachment_id,
+                       'title' => 'Open Source Cartoon',
+                       'preload' => 'metadata',
+                       'loop' => true,
+               ) );
+               $output = ob_get_clean();
+
+               // Custom attributes.
+               $this->assertContains( 'preload="metadata"', $output );
+               $this->assertContains( 'loop="1"', $output );
+
+               // Externally hosted video.
+               ob_start();
+               $content = '<track srclang="en" label="English" kind="subtitles" src="http://example.com/wp-content/uploads/2017/04/subtitles-en.vtt">';
+               $widget->render_media( array(
+                       'attachment_id' => null,
+                       'loop' => false,
+                       'url' => 'https://www.youtube.com/watch?v=OQSNhk5ICTI',
+                       'content' => $content,
+               ) );
+               $output = ob_get_clean();
+
+               // Custom attributes.
+               $this->assertContains( 'preload="metadata"', $output );
+               $this->assertContains( 'src="https://www.youtube.com/watch?v=OQSNhk5ICTI', $output );
+               $this->assertContains( $content, $output );
+       }
+
+       /**
+        * Test enqueue_preview_scripts method.
+        *
+        * @global WP_Scripts $wp_scripts
+        * @global WP_Styles $wp_styles
+        * @covers WP_Widget_Media_Video::enqueue_preview_scripts()
+        */
+       function test_enqueue_preview_scripts() {
+               global $wp_scripts, $wp_styles;
+               $widget = new WP_Widget_Media_Video();
+
+               $wp_scripts = null;
+               $wp_styles = null;
+               $widget->enqueue_preview_scripts();
+               $this->assertTrue( wp_script_is( 'wp-mediaelement' ) );
+               $this->assertTrue( wp_style_is( 'wp-mediaelement' ) );
+               $this->assertTrue( wp_script_is( 'froogaloop' ) );
+
+               $wp_scripts = null;
+               $wp_styles = null;
+               add_filter( 'wp_video_shortcode_library', '__return_empty_string' );
+               $widget->enqueue_preview_scripts();
+               $this->assertFalse( wp_script_is( 'wp-mediaelement' ) );
+               $this->assertFalse( wp_style_is( 'wp-mediaelement' ) );
+               $this->assertTrue( wp_script_is( 'froogaloop' ) );
+       }
+
+       /**
+        * Test enqueue_admin_scripts method.
+        *
+        * @covers WP_Widget_Media_Video::enqueue_admin_scripts()
+        */
+       function test_enqueue_admin_scripts() {
+               set_current_screen( 'widgets.php' );
+               $widget = new WP_Widget_Media_Video();
+               $widget->enqueue_admin_scripts();
+
+               $this->assertTrue( wp_script_is( 'media-video-widget' ) );
+       }
+
+       /**
+        * Test render_control_template_scripts method.
+        *
+        * @covers WP_Widget_Media_Video::render_control_template_scripts()
+        */
+       function test_render_control_template_scripts() {
+               $widget = new WP_Widget_Media_Video();
+
+               ob_start();
+               $widget->render_control_template_scripts();
+               $output = ob_get_clean();
+
+               $this->assertContains( '<script type="text/html" id="tmpl-wp-media-widget-video-preview">', $output );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestswidgetsmediawidgetphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/widgets/media-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/media-widget.php                                (rev 0)
+++ trunk/tests/phpunit/tests/widgets/media-widget.php  2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,467 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_Widget_Media functionality.
+ *
+ * @package    WordPress
+ * @subpackage widgets
+ */
+
+/**
+ * Class Test_WP_Widget_Media
+ *
+ * @group widgets
+ */
+class Test_WP_Widget_Media extends WP_UnitTestCase {
+
+       /**
+        * Clean up global scope.
+        *
+        * @global WP_Scripts $wp_scripts
+        * @global WP_Styles $wp_styles
+        */
+       function clean_up_global_scope() {
+               global $wp_scripts, $wp_styles;
+               parent::clean_up_global_scope();
+               $wp_scripts = null;
+               $wp_styles = null;
+       }
+
+       /**
+        * Get instance for mocked media widget class.
+        *
+        * @param string $id_base         Base ID for the widget, lowercase and unique.
+        * @param string $name            Name for the widget displayed on the configuration page.
+        * @param array  $widget_options  Optional. Widget options.
+        * @param array  $control_options Optional. Widget control options.
+        * @return PHPUnit_Framework_MockObject_MockObject|WP_Widget_Media Mocked instance.
+        */
+       function get_mocked_class_instance( $id_base = 'mocked', $name = 'Mocked', $widget_options = array(), $control_options = array() ) {
+               $original_class_name = 'WP_Widget_Media';
+               $arguments = array( $id_base, $name, $widget_options, $control_options );
+               $mock_class_name = '';
+               $call_original_constructor = true;
+               $call_original_clone = true;
+               $call_autoload = true;
+               $mocked_methods = array( 'render_media' );
+
+               return $this->getMockForAbstractClass( $original_class_name, $arguments, $mock_class_name, $call_original_constructor, $call_original_clone, $call_autoload, $mocked_methods );
+       }
+
+       /**
+        * Test constructor.
+        *
+        * @covers WP_Widget_Media::__construct()
+        * @covers WP_Widget_Media::_register()
+        */
+       function test_constructor() {
+               $widget = $this->get_mocked_class_instance();
+               $widget->_register();
+
+               $this->assertArrayHasKey( 'mime_type', $widget->widget_options );
+               $this->assertArrayHasKey( 'customize_selective_refresh', $widget->widget_options );
+               $this->assertArrayHasKey( 'description', $widget->widget_options );
+               $this->assertTrue( $widget->widget_options['customize_selective_refresh'] );
+               $this->assertEmpty( $widget->widget_options['mime_type'] );
+               $this->assertEqualSets( array(
+                       'add_to_widget',
+                       'replace_media',
+                       'edit_media',
+                       'media_library_state_multi',
+                       'media_library_state_single',
+                       'missing_attachment',
+                       'no_media_selected',
+                       'add_media',
+                       'unsupported_file_type',
+               ), array_keys( $widget->l10n ) );
+               $this->assertEquals( count( $widget->l10n ), count( array_filter( $widget->l10n ) ), 'Expected all translation strings to be defined.' );
+               $this->assertEquals( 10, has_action( 'admin_print_scripts-widgets.php', array( $widget, 'enqueue_admin_scripts' ) ) );
+               $this->assertFalse( has_action( 'wp_enqueue_scripts', array( $widget, 'enqueue_preview_scripts' ) ), 'Did not expect preview scripts to be enqueued when not in customize preview context.' );
+               $this->assertEquals( 10, has_action( 'admin_footer-widgets.php', array( $widget, 'render_control_template_scripts' ) ) );
+
+               // With non-default args.
+               $id_base = 'media_pdf';
+               $name = 'PDF';
+               $widget_options = array(
+                       'mime_type' => 'application/pdf',
+               );
+               $control_options = array(
+                       'width' => 850,
+                       'height' => 1100,
+               );
+               $widget = $this->get_mocked_class_instance( $id_base, $name, $widget_options, $control_options );
+               $this->assertEquals( $id_base, $widget->id_base );
+               $this->assertEquals( $name, $widget->name );
+
+               // Method assertArraySubset doesn't exist in phpunit versions compatible with PHP 5.2.
+               if ( method_exists( $this, 'assertArraySubset' ) ) {
+                       $this->assertArraySubset( $widget_options, $widget->widget_options );
+                       $this->assertArraySubset( $control_options, $widget->control_options );
+               }
+       }
+
+       /**
+        * Test constructor in customize preview.
+        *
+        * @global WP_Customize_Manager $wp_customize
+        * @covers WP_Widget_Media::__construct()
+        * @covers WP_Widget_Media::_register()
+        */
+       function test_constructor_in_customize_preview() {
+               global $wp_customize;
+               wp_set_current_user( $this->factory()->user->create( array(
+                       'role' => 'administrator',
+               ) ) );
+               require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
+               $wp_customize = new WP_Customize_Manager( array(
+                       'changeset_uuid' => wp_generate_uuid4(),
+               ) );
+               $wp_customize->start_previewing_theme();
+
+               $widget = $this->get_mocked_class_instance();
+               $widget->_register();
+               $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array( $widget, 'enqueue_preview_scripts' ) ) );
+       }
+
+       /**
+        * Test is_attachment_with_mime_type method.
+        *
+        * @covers WP_Widget_Media::is_attachment_with_mime_type
+        */
+       function test_is_attachment_with_mime_type() {
+               $attachment_id = self::factory()->attachment->create_object( array(
+                       'file' => DIR_TESTDATA . '/images/canola.jpg',
+                       'post_parent' => 0,
+                       'post_mime_type' => 'image/jpeg',
+                       'post_title' => 'Canola',
+               ) );
+               wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, DIR_TESTDATA . '/images/canola.jpg' ) );
+               $widget = $this->get_mocked_class_instance();
+
+               $this->assertFalse( $widget->is_attachment_with_mime_type( 0, 'image' ) );
+               $this->assertFalse( $widget->is_attachment_with_mime_type( -123, 'image' ) );
+
+               $post_id = $this->factory()->post->create();
+               $this->assertFalse( $widget->is_attachment_with_mime_type( $post_id, 'image' ) );
+               $this->assertFalse( $widget->is_attachment_with_mime_type( $attachment_id, 'video' ) );
+               $this->assertTrue( $widget->is_attachment_with_mime_type( $attachment_id, 'image' ) );
+       }
+
+       /**
+        * Test sanitize_token_list method.
+        *
+        * @covers WP_Widget_Media::sanitize_token_list
+        */
+       function test_sanitize_token_list_string() {
+               $widget = $this->get_mocked_class_instance();
+
+               $result = $widget->sanitize_token_list( 'What A false class with-token <a href="#">and link</a>' );
+               $this->assertEquals( 'What A false class with-token a hrefand linka', $result );
+
+               $result = $widget->sanitize_token_list( array( 'foo', '<i>bar', '">NO' ) );
+               $this->assertEquals( $result, 'foo ibar NO' );
+       }
+
+       /**
+        * Test get_instance_schema method.
+        *
+        * @covers WP_Widget_Media::get_instance_schema
+        */
+       function test_get_instance_schema() {
+               $widget = $this->get_mocked_class_instance();
+               $schema = $widget->get_instance_schema();
+
+               $this->assertEqualSets( array(
+                       'attachment_id',
+                       'title',
+                       'url',
+               ), array_keys( $schema ) );
+       }
+
+       /**
+        * Test update method.
+        *
+        * @covers WP_Widget_Media::update()
+        */
+       function test_update() {
+               $widget = $this->get_mocked_class_instance();
+               $instance = array();
+
+               // Should return valid attachment ID.
+               $expected = array(
+                       'attachment_id' => 1,
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment ID.
+               $result = $widget->update(
+                       array(
+                               'attachment_id' => 'media',
+                       ),
+                       $instance
+               );
+               $this->assertSame( $result, $instance );
+
+               // Should return valid attachment url.
+               $expected = array(
+                       'url' => 'https://example.org',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment url.
+               $result = $widget->update(
+                       array(
+                               'url' => 'not_a_url',
+                       ),
+                       $instance
+               );
+               $this->assertNotSame( $result, $instance );
+
+               // Should return valid attachment title.
+               $expected = array(
+                       'title' => 'What a title',
+               );
+               $result = $widget->update( $expected, $instance );
+               $this->assertSame( $result, $expected );
+
+               // Should filter invalid attachment title.
+               $result = $widget->update(
+                       array(
+                               'title' => '<h1>W00t!</h1>',
+                       ),
+                       $instance
+               );
+               $this->assertNotSame( $result, $instance );
+
+               // Should filter invalid key.
+               $result = $widget->update(
+                       array(
+                               'imaginary_key' => 'value',
+                       ),
+                       $instance
+               );
+               $this->assertSame( $result, $instance );
+
+               add_filter( 'sanitize_text_field', array( $this, '_return_wp_error' ) );
+               $result = $widget->update(
+                       array(
+                               'title' => 'Title',
+                       ),
+                       $instance
+               );
+               remove_filter( 'sanitize_text_field', array( $this, '_return_wp_error' ) );
+               $this->assertSame( $result, $instance );
+       }
+
+       /**
+        * Helper function for Test_WP_Widget_Media::test_update().
+        *
+        * @return \WP_Error
+        */
+       function _return_wp_error() {
+               return new WP_Error( 'some-error', 'This is not valid!' );
+       }
+
+       /**
+        * Test widget method.
+        *
+        * @covers WP_Widget_Media::widget()
+        * @covers WP_Widget_Media::render_media()
+        */
+       function test_widget() {
+               $args = array(
+                       'before_title' => '<h2>',
+                       'after_title' => "</h2>\n",
+                       'before_widget' => '<section>',
+                       'after_widget' => "</section>\n",
+               );
+               $instance = array(
+                       'title' => 'Foo',
+                       'url' => 'http://example.com/image.jpg',
+                       'attachment_id' => 0,
+               );
+
+               add_filter( 'widget_mocked_instance', array( $this, 'filter_widget_mocked_instance' ), 10, 3 );
+
+               ob_start();
+               $widget = $this->get_mocked_class_instance();
+               $widget->expects( $this->atLeastOnce() )->method( 'render_media' )->with( $instance );
+               $this->widget_instance_filter_args = array();
+               $widget->widget( $args, $instance );
+               $this->assertCount( 3, $this->widget_instance_filter_args );
+               $this->assertEquals( $instance, $this->widget_instance_filter_args[0] );
+               $this->assertEquals( $args, $this->widget_instance_filter_args[1] );
+               $this->assertEquals( $widget, $this->widget_instance_filter_args[2] );
+               $output = ob_get_clean();
+
+               $this->assertContains( '<h2>Foo</h2>', $output );
+               $this->assertContains( '<section>', $output );
+               $this->assertContains( '</section>', $output );
+
+               // No title.
+               ob_start();
+               $widget = $this->get_mocked_class_instance();
+               $instance['title'] = '';
+               $widget->expects( $this->atLeastOnce() )->method( 'render_media' )->with( $instance );
+               $widget->widget( $args, $instance );
+               $output = ob_get_clean();
+               $this->assertNotContains( '<h2>Foo</h2>', $output );
+
+               // No attachment_id nor url.
+               $instance['url'] = '';
+               $instance['attachment_id'] = 0;
+               ob_start();
+               $widget = $this->get_mocked_class_instance();
+               $widget->widget( $args, $instance );
+               $output = ob_get_clean();
+               $this->assertEmpty( $output );
+       }
+
+       /**
+        * Args passed to the widget_{$id_base}_instance filter.
+        *
+        * @var array
+        */
+       protected $widget_instance_filter_args = array();
+
+       /**
+        * Filters the media widget instance prior to rendering the media.
+        *
+        * @param array           $instance Instance data.
+        * @param array           $args     Widget args.
+        * @param WP_Widget_Media $object   Widget object.
+        * @return array Instance.
+        */
+       function filter_widget_mocked_instance( $instance, $args, $object ) {
+               $this->widget_instance_filter_args = func_get_args();
+               return $instance;
+       }
+
+       /**
+        * Test form method.
+        *
+        * @covers WP_Widget_Media::form()
+        */
+       function test_form() {
+               $widget = $this->get_mocked_class_instance();
+
+               ob_start();
+               $widget->form( array() );
+               $output = ob_get_clean();
+
+               $this->assertContains( 'name="widget-mocked[][attachment_id]"', $output );
+               $this->assertContains( 'name="widget-mocked[][title]"', $output );
+               $this->assertContains( 'name="widget-mocked[][url]"', $output );
+       }
+
+       /**
+        * Test display_media_state method.
+        *
+        * @covers WP_Widget_Media::display_media_state()
+        */
+       function test_display_media_state() {
+               $widget = $this->get_mocked_class_instance();
+               $attachment_id = self::factory()->attachment->create_object( array(
+                       'file' => DIR_TESTDATA . '/images/canola.jpg',
+                       'post_parent' => 0,
+                       'post_mime_type' => 'image/jpeg',
+               ) );
+
+               $result = $widget->display_media_state( array(), get_post( $attachment_id ) );
+               $this->assertEqualSets( array(), $result );
+
+               $widget->save_settings( array(
+                       array(
+                               'attachment_id' => $attachment_id,
+                       ),
+               ) );
+               $result = $widget->display_media_state( array(), get_post( $attachment_id ) );
+               $this->assertEqualSets( array( $widget->l10n['media_library_state_single'] ), $result );
+
+               $widget->save_settings( array(
+                       array(
+                               'attachment_id' => $attachment_id,
+                       ),
+                       array(
+                               'attachment_id' => $attachment_id,
+                       ),
+               ) );
+               $result = $widget->display_media_state( array(), get_post( $attachment_id ) );
+               $this->assertEqualSets( array( sprintf( $widget->l10n['media_library_state_multi']['singular'], 2 ) ), $result );
+       }
+
+       /**
+        * Test enqueue_admin_scripts method.
+        *
+        * @covers WP_Widget_Media::enqueue_admin_scripts()
+        */
+       function test_enqueue_admin_scripts() {
+               set_current_screen( 'widgets.php' );
+               $widget = $this->get_mocked_class_instance();
+               $widget->enqueue_admin_scripts();
+
+               $this->assertTrue( wp_script_is( 'media-widgets' ) );
+       }
+
+       /**
+        * Test render_control_template_scripts method.
+        *
+        * @covers WP_Widget_Media::render_control_template_scripts
+        */
+       function test_render_control_template_scripts() {
+               $widget = $this->get_mocked_class_instance();
+
+               ob_start();
+               $widget->render_control_template_scripts();
+               $output = ob_get_clean();
+
+               $this->assertContains( '<script type="text/html" id="tmpl-widget-media-mocked-control">', $output );
+       }
+
+       /**
+        * Test has_content method.
+        *
+        * @covers WP_Widget_Media::has_content()
+        */
+       function test_has_content() {
+               if ( version_compare( PHP_VERSION, '5.3', '<' ) ) {
+                       $this->markTestSkipped( 'ReflectionMethod::setAccessible is only available for PHP 5.3+' );
+                       return;
+               }
+
+               $attachment_id = self::factory()->attachment->create_object( array(
+                       'file' => DIR_TESTDATA . '/images/canola.jpg',
+                       'post_parent' => 0,
+                       'post_mime_type' => 'image/jpeg',
+               ) );
+
+               $wp_widget_media = new ReflectionClass( 'WP_Widget_Media' );
+               $has_content = $wp_widget_media->getMethod( 'has_content' );
+               $has_content->setAccessible( true );
+
+               $result = $has_content->invokeArgs( $this->get_mocked_class_instance(), array(
+                       array(
+                               'attachment_id' => 0,
+                               'url' => '',
+                       ),
+               ) );
+               $this->assertFalse( $result );
+
+               $result = $has_content->invokeArgs( $this->get_mocked_class_instance(), array(
+                       array(
+                               'attachment_id' => $attachment_id,
+                               'url' => '',
+                       ),
+               ) );
+               $this->assertTrue( $result );
+
+               $result = $has_content->invokeArgs( $this->get_mocked_class_instance(), array(
+                       array(
+                               'attachment_id' => 0,
+                               'url' => 'http://example.com/image.jpg',
+                       ),
+               ) );
+               $this->assertTrue( $result );
+       }
+}
</ins></span></pre></div>
<a id="trunktestsphpunittestswidgetstextwidgetphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/widgets/text-widget.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/widgets/text-widget.php 2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/tests/phpunit/tests/widgets/text-widget.php   2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -178,15 +178,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $expected['text'] = $instance['text'];
</span><span class="cx" style="display: block; padding: 0 10px">                $result = $widget->update( $instance, array() );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $result, $expected );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_filter( 'map_meta_cap', array( $this, 'grant_unfiltered_html_cap' ) );
</del><span class="cx" style="display: block; padding: 0 10px">                 add_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertFalse( current_user_can( 'unfiltered_html' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                $instance['text'] = '<script>alert( "Howdy!" );</script>';
</span><span class="cx" style="display: block; padding: 0 10px">                $expected['text'] = wp_kses_post( $instance['text'] );
</span><span class="cx" style="display: block; padding: 0 10px">                $result = $widget->update( $instance, array() );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( $result, $expected );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10, 2 );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         remove_filter( 'map_meta_cap', array( $this, 'revoke_unfiltered_html_cap' ), 10 );
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre></div>
<a id="trunktestsqunitindexhtml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/index.html</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/index.html      2017-05-11 19:53:38 UTC (rev 40639)
+++ trunk/tests/qunit/index.html        2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -24,6 +24,33 @@
</span><span class="cx" style="display: block; padding: 0 10px">                </script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../src/wp-includes/js/wp-util.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../src/wp-includes/js/wp-a11y.js"></script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script>
+                       window._wpMediaModelsL10n = {"settings":{"ajaxurl":"\/wp-admin\/admin-ajax.php","post":{"id":0}}};
+               </script>
+               <script src="../../src/wp-includes/js/media-models.js"></script>
+               <script>
+                       window.userSettings = {"url":"\/","uid":"0","time":"1493325477","secure":""};
+               </script>
+               <script src="../../src/wp-includes/js/utils.js"></script>
+               <script src="../../src/wp-includes/js/plupload/plupload.full.min.js"></script>
+               <script>
+                       window.pluploadL10n = {"queue_limit_exceeded":"You have attempted to queue too many files.","file_exceeds_size_limit":"%s exceeds the maximum upload size for this site.","zero_byte_file":"This file is empty. Please try another.","invalid_filetype":"Sorry, this file type is not permitted for security reasons.","not_an_image":"This file is not an image. Please try another.","image_memory_exceeded":"Memory exceeded. Please try another smaller file.","image_dimensions_exceeded":"This is larger than the maximum size. Please try another.","default_error":"An error occurred in the upload. Please try again later.","missing_upload_url":"There was a configuration error. Please contact the server administrator.","upload_limit_exceeded":"You may only upload 1 file.","http_error":"
 HTTP error.","upload_failed":"Upload failed.","big_upload_failed":"Please try uploading this file with the %1$sbrowser uploader%2$s.","big_upload_queued":"%s exceeds the maximum upload size for the multi-file uploader when used in your browser.","io_error":"IO error.","security_error":"Security error.","file_cancelled":"File canceled.","upload_stopped":"Upload stopped.","dismiss":"Dismiss","crunching":"Crunching\u2026","deleted":"moved to the trash.","error_uploading":"\u201c%s\u201d has failed to upload."};
+                       window._wpPluploadSettings = {"defaults":{"runtimes":"html5,flash,silverlight,html4","file_data_name":"async-upload","url":"\/wp-admin\/async-upload.php","flash_swf_url":"http:\/\/src.wordpress-develop.dev\/wp-includes\/js\/plupload\/plupload.flash.swf","silverlight_xap_url":"http:\/\/src.wordpress-develop.dev\/wp-includes\/js\/plupload\/plupload.silverlight.xap","filters":{"max_file_size":"2097152b","mime_types":[{"extensions":"jpg,jpeg,jpe,gif,png,bmp,tiff,tif,ico,asf,asx,wmv,wmx,wm,avi,divx,flv,mov,qt,mpeg,mpg,mpe,mp4,m4v,ogv,webm,mkv,3gp,3gpp,3g2,3gp2,txt,asc,c,cc,h,srt,csv,tsv,ics,rtx,css,vtt,dfxp,mp3,m4a,m4b,ra,ram,wav,ogg,oga,mid,midi,wma,wax,mka,rtf,js,pdf,class,tar,zip,gz,gzip,rar,7z,psd,xcf,doc,pot,pps,ppt,wri,xla,xls,xlt,xlw,mdb,mpp,docx,docm,dotx,dotm,xlsx,xlsm,xlsb,xltx,xltm,xlam,pptx,pptm,ppsx,ppsm,potx,
 potm,ppam,sldx,sldm,onetoc,onetoc2,onetmp,onepkg,oxps,xps,odt,odp,ods,odg,odc,odb,odf,wp,wpd,key,numbers,pages"}]},"multipart_params":{"action":"upload-attachment","_wpnonce":"87fa5740b8"}},"browser":{"mobile":false,"supported":true},"limitExceeded":false};
+               </script>
+               <script src="../../src/wp-includes/js/plupload/wp-plupload.js"></script>
+               <script>
+                       window.mejsL10n = {"language":"en-US","strings":{"Close":"Close","Fullscreen":"Fullscreen","Turn off Fullscreen":"Turn off Fullscreen","Go Fullscreen":"Go Fullscreen","Download File":"Download File","Download Video":"Download Video","Play":"Play","Pause":"Pause","Captions\/Subtitles":"Captions\/Subtitles","None":"None","Time Slider":"Time Slider","Skip back %1 seconds":"Skip back %1 seconds","Video Player":"Video Player","Audio Player":"Audio Player","Volume Slider":"Volume Slider","Mute Toggle":"Mute Toggle","Unmute":"Unmute","Mute":"Mute","Use Up\/Down Arrow keys to increase or decrease vol
 ume.":"Use Up\/Down Arrow keys to increase or decrease volume.","Use Left\/Right Arrow keys to advance one second, Up\/Down arrows to advance ten seconds.":"Use Left\/Right Arrow keys to advance one second, Up\/Down arrows to advance ten seconds."}};
+                       window._wpmejsSettings = {"pluginPath":"\/wp-includes\/js\/mediaelement\/"};
+               </script>
+               <script src="../../src/wp-includes/js/mediaelement/mediaelement-and-player.min.js"></script>
+               <script src="../../src/wp-includes/js/mediaelement/wp-mediaelement.js"></script>
+               <script>
+                       window._wpMediaViewsL10n = {"url":"URL","addMedia":"Add Media","search":"Search","select":"Select","cancel":"Cancel","update":"Update","replace":"Replace","remove":"Remove","back":"Back","selected":"%d selected","dragInfo":"Drag and drop to reorder media files.","uploadFilesTitle":"Upload Files","uploadImagesTitle":"Upload Images","mediaLibraryTitle":"Media Library","insertMediaTitle":"Insert Media","createNewGallery":"Create a new gallery","createNewPlaylist":"Create a new playlist","createNewVideoPlaylist":"Create a new video playlist","returnToLibrary":"\u2190 Return to library","allMediaItems"
 :"All media items","allDates":"All dates","noItemsFound":"No items found.","insertIntoPost":"Insert into post","unattached":"Unattached","trash":"Trash","uploadedToThisPost":"Uploaded to this post","warnDelete":"You are about to permanently delete this item.\nThis will remove it from your site.\n 'Cancel' to stop, 'OK' to delete.","warnBulkDelete":"You are about to permanently delete these items.\nThis will remove them from your site.\n 'Cancel' to stop, 'OK' to delete.","warnBulkTrash":"You are about to trash these items.\n  'Cancel' to stop, 'OK' to delete.","bulkSelect":"Bulk Select","cancelSelection":"Cancel Selection","trashSelected":"Trash Selected","untrashSelected":"Untrash Selected","deleteSelected"
 :"Delete Selected","deletePermanently":"Delete Permanently","apply":"Apply","filterByDate":"Filter by date","filterByType":"Filter by type","searchMediaLabel":"Search Media","searchMediaPlaceholder":"Search media items...","noMedia":"No media files found.","attachmentDetails":"Attachment Details","insertFromUrlTitle":"Insert from URL","setFeaturedImageTitle":"Featured Image","setFeaturedImage":"Set featured image","createGalleryTitle":"Create Gallery","editGalleryTitle":"Edit Gallery","cancelGalleryTitle":"\u2190 Cancel Gallery","insertGallery":"Insert gallery","updateGallery":"Update gallery","addToGallery":"Add to gallery","addToGal
 leryTitle":"Add to Gallery","reverseOrder":"Reverse order","imageDetailsTitle":"Image Details","imageReplaceTitle":"Replace Image","imageDetailsCancel":"Cancel Edit","editImage":"Edit Image","chooseImage":"Choose Image","selectAndCrop":"Select and Crop","skipCropping":"Skip Cropping","cropImage":"Crop Image","cropYourImage":"Crop your image","cropping":"Cropping\u2026","suggestedDimensions":"Suggested image dimensions:","cropError":"There has been an error cropping your image.","audioDetailsTitle":"Audio Details","audioReplaceTitle":"Replace Audio","audioAddSourceTitle":"Add Audio Source","audioDetailsCancel":"Cancel Edit","video
 DetailsTitle":"Video Details","videoReplaceTitle":"Replace Video","videoAddSourceTitle":"Add Video Source","videoDetailsCancel":"Cancel Edit","videoSelectPosterImageTitle":"Select Poster Image","videoAddTrackTitle":"Add Subtitles","playlistDragInfo":"Drag and drop to reorder tracks.","createPlaylistTitle":"Create Audio Playlist","editPlaylistTitle":"Edit Audio Playlist","cancelPlaylistTitle":"\u2190 Cancel Audio Playlist","insertPlaylist":"Insert audio playlist","updatePlaylist":"Update audio playlist","addToPlaylist":"Add to audio playlist","addToPlaylistTitle":"Add to Audio Playlist","videoPlaylistDragInfo":"Drag and drop to reorder videos.","createVideoPlaylistTitle":"Create 
 Video Playlist","editVideoPlaylistTitle":"Edit Video Playlist","cancelVideoPlaylistTitle":"\u2190 Cancel Video Playlist","insertVideoPlaylist":"Insert video playlist","updateVideoPlaylist":"Update video playlist","addToVideoPlaylist":"Add to video playlist","addToVideoPlaylistTitle":"Add to Video Playlist","settings":{"tabs":[],"tabUrl":"http:\/\/src.wordpress-develop.dev\/wp-admin\/media-upload.php?chromeless=1","mimeTypes":{"image":"Images","audio":"Audio","video":"Video"},"captions":true,"nonce":{"sendToEditor":"9585d11de6"},"post":{"id":0},"defaultProps":{"link":"none","align":"","size":""},"attachmentCounts":{
 "audio":1,"video":1},"embedExts":["mp3","ogg","wma","m4a","wav","mp4","m4v","webm","ogv","wmv","flv"],"embedMimes":{"mp3":"audio\/mpeg","ogg":"audio\/ogg","wma":"audio\/x-ms-wma","m4a":"audio\/mpeg","wav":"audio\/wav","mp4":"video\/mp4","m4v":"video\/mp4","webm":"video\/webm","ogv":"video\/ogg","wmv":"video\/x-ms-wmv","flv":"video\/x-flv"},"contentWidth":525,"months":[{"year":"2017","month":"4","text":"April 2017"}],"mediaTrash":0}};
+               </script>
+               <script src="../../src/wp-includes/js/media-views.js"></script>
+               <script src="../../src/wp-includes/js/media-editor.js"></script>
+               <script src="../../src/wp-includes/js/media-audiovideo.js"></script>
+               <script src="../../src/wp-includes/js/mce-view.js"></script>
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <!-- QUnit -->
</span><span class="cx" style="display: block; padding: 0 10px">                <link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -64,6 +91,32 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../src/wp-admin/js/customize-widgets.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../src/wp-admin/js/word-count.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script src="../../src/wp-admin/js/widgets/media-widgets.js"></script>
+               <script>
+                       wp.mediaWidgets.init();
+               </script>
+               <script src="../../src/wp-admin/js/widgets/media-image-widget.js"></script>
+               <script>
+                       wp.mediaWidgets.modelConstructors[ "media_image" ].prototype.schema = {"attachment_id":{"type":"integer","default":0,"minimum":0,"media_prop":"id"},"url":{"type":"string","default":"","format":"uri"},"title":{"type":"string","default":"","should_preview_update":false},"size":{"type":"string","default":"medium","enum":["thumbnail","medium","medium_large","large","twentyseventeen-featured-image","twentyseventeen-thumbnail-avatar","full","custom"]},"width":{"type":"integer","default":0,"minimum":0},"height":{"type":"integer","default":0,"minimum&
 quot;:0},"caption":{"type":"string","default":"","should_preview_update":false},"alt":{"type":"string","default":""},"link_type":{"type":"string","default":"none","enum":["none","file","post","custom"],"media_prop":"link","should_preview_update":false},"link_url":{"type":"string","default":"","format":"uri","media_prop":"linkUrl","should_preview_update":false},"image_classes":{"type":"string","default":"","media_prop":"extraClasses","should_preview_update":false},"link_classes":{"type":"string","default":"","media_pr
 op":"linkClassName","should_preview_update":false},"link_rel":{"type":"string","default":"","media_prop":"linkRel","should_preview_update":false},"link_target_blank":{"type":"boolean","default":false,"media_prop":"linkTargetBlank","should_preview_update":false},"image_title":{"type":"string","default":"","media_prop":"title","should_preview_update":false}};
+
+                       wp.mediaWidgets.controlConstructors[ "media_image" ].prototype.mime_type = "image";
+                       _.extend( wp.mediaWidgets.controlConstructors[ "media_image" ].prototype.l10n, {"no_media_selected":"No image selected","select_media":"Select Image","change_media":"Change Image","edit_media":"Edit Image","add_to_widget":"Add to Widget","missing_attachment":"We can&#8217;t find that image. Check your <a href=\"http:\/\/src.wordpress-develop.dev\/wp-admin\/upload.php\">media library<\/a> and make sure it wasn&#8217;t deleted.","media_library_state_multi":{"0":"Image Widget (%d)","1":"Image Widget (%d)","singular":"Image Widget (%d)","plural":"Image Widget (%d)","context":null,"domain":null},"media_library_state_single":"Image Widget"} );
+               </script>
+               <script src="../../src/wp-admin/js/widgets/media-video-widget.js"></script>
+               <script>
+                       wp.mediaWidgets.modelConstructors[ "media_video" ].prototype.schema = {"attachment_id":{"type":"integer","default":0,"minimum":0,"media_prop":"id"},"url":{"type":"string","default":"","format":"uri"},"title":{"type":"string","default":""},"preload":{"type":"string","default":"metadata","enum":["none","auto","metadata"]},"loop":{"type":"boolean","default":false},"content":{"type":"string","default":""},"mp4":{"type":"string","default":"","format":"uri"},"m4v":{"type":"string","default":"",&
 quot;format":"uri"},"webm":{"type":"string","default":"","format":"uri"},"ogv":{"type":"string","default":"","format":"uri"},"wmv":{"type":"string","default":"","format":"uri"},"flv":{"type":"string","default":"","format":"uri"}};
+
+                       wp.mediaWidgets.controlConstructors[ "media_video" ].prototype.mime_type = "video";
+                       _.extend( wp.mediaWidgets.controlConstructors[ "media_video" ].prototype.l10n, {"no_media_selected":"No video selected","select_media":"Select Video","change_media":"Change Video","edit_media":"Edit Video","add_to_widget":"Add to Widget","missing_attachment":"We can&#8217;t find that video. Check your <a href=\"http:\/\/src.wordpress-develop.dev\/wp-admin\/upload.php\">media library<\/a> and make sure it wasn&#8217;t deleted.","media_library_state_multi":{"0":"Video Widget (%d)","1":"Video Widget (%d)","singular":"Video Widget (%d)","plural":"Video Widget (%d)","context":null,"domain":null},"media_library_state_single":"Video Widget"} );
+               </script>
+               <script src="../../src/wp-admin/js/widgets/media-audio-widget.js"></script>
+               <script>
+                       wp.mediaWidgets.modelConstructors[ "media_audio" ].prototype.schema = {"attachment_id":{"type":"integer","default":0,"minimum":0,"media_prop":"id"},"url":{"type":"string","default":"","format":"uri"},"title":{"type":"string","default":""},"preload":{"type":"string","default":"none","enum":["none","auto","metadata"]},"loop":{"type":"boolean","default":false},"mp3":{"type":"string","default":"","format":"uri"},"ogg":{"type":"string","default":"","format":"uri"},"wma":{"type":"string","d
 efault":"","format":"uri"},"m4a":{"type":"string","default":"","format":"uri"},"wav":{"type":"string","default":"","format":"uri"}};
+
+                       wp.mediaWidgets.controlConstructors[ "media_audio" ].prototype.mime_type = "audio";
+                       _.extend( wp.mediaWidgets.controlConstructors[ "media_audio" ].prototype.l10n, {"no_media_selected":"No audio selected","select_media":"Select File","change_media":"Change Audio","edit_media":"Edit Audio","add_to_widget":"Add to Widget","missing_attachment":"We can&#8217;t find that audio file. Check your <a href=\"http:\/\/src.wordpress-develop.dev\/wp-admin\/upload.php\">media library<\/a> and make sure it wasn&#8217;t deleted.","media_library_state_multi":{"0":"Audio Widget (%d)","1":"Audio Widget (%d)","singular":"Audio Widget (%d)","plural":"Audio Widget (%d)","context":null,"domain":null},"media_library_state_single":"Audio Widget"} );
+               </script>
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 <!-- Unit tests -->
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/password-strength-meter.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/customize-base.js"></script>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -76,6 +129,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/customize-widgets.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/word-count.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="wp-admin/js/nav-menu.js"></script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script src="wp-admin/js/widgets/test-media-widgets.js"></script>
+               <script src="wp-admin/js/widgets/test-media-image-widget.js"></script>
+               <script src="wp-admin/js/widgets/test-media-video-widget.js"></script>
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <!-- Customizer templates for sections -->
</span><span class="cx" style="display: block; padding: 0 10px">                <script type="text/html" id="tmpl-customize-section-default">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -499,6 +555,1495 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        </div><!-- #available-widgets-list -->
</span><span class="cx" style="display: block; padding: 0 10px">                                </div><!-- #available-widgets -->
</span><span class="cx" style="display: block; padding: 0 10px">                        </div><!-- #widgets-left -->
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        
+                       <script type="text/html" id="tmpl-widget-media-media_image-control">
+                       <# var elementIdPrefix = 'el' + String( Math.random() ) + '_' #>
+                       <p>
+                               <label for="{{ elementIdPrefix }}title">Title:</label>
+                               <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
+                       </p>
+                       <div class="media-widget-preview">
+                               <div class="attachment-media-view">
+                                       <div class="placeholder">No image selected</div>
+                               </div>
+                       </div>
+                       <p class="media-widget-buttons">
+                               <button type="button" class="button edit-media selected">
+                                       Edit Image                              </button>
+                               <button type="button" class="button change-media select-media selected">
+                                       Replace Image                           </button>
+                               <button type="button" class="button select-media not-selected">
+                                       Add Image                               </button>
+                       </p>
+               </script>
+                               <script type="text/html" id="tmpl-wp-media-widget-image-preview">
+                       <#
+                       var describedById = 'describedBy-' + String( Math.random() );
+                       #>
+                       <# if ( data.error && 'missing_attachment' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p>We can&#8217;t find that image. Check your <a href="http://src.wordpress-develop.dev/wp-admin/upload.php">media library</a> and make sure it wasn&#8217;t deleted.</p>
+                               </div>
+                       <# } else if ( data.error ) { #>
+                               <div class="notice notice-error notice-alt">
+                                       <p>Unable to preview media due to an unknown error.</p>
+                               </div>
+                       <# } else if ( data.url ) { #>
+                               <img class="attachment-thumb" src="{{ data.url }}" draggable="false" alt="{{ data.alt }}" <# if ( ! data.alt && data.currentFilename ) { #> aria-describedby="{{ describedById }}" <# } #> />
+                               <# if ( ! data.alt && data.currentFilename ) { #>
+                                       <p class="hidden" id="{{ describedById }}">Current image: {{ data.currentFilename }}</p>
+                               <# } #>
+                       <# } #>
+               </script>
+                               <script type="text/html" id="tmpl-widget-media-media_video-control">
+                       <# var elementIdPrefix = 'el' + String( Math.random() ) + '_' #>
+                       <p>
+                               <label for="{{ elementIdPrefix }}title">Title:</label>
+                               <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
+                       </p>
+                       <div class="media-widget-preview">
+                               <div class="attachment-media-view">
+                                       <div class="placeholder">No video selected</div>
+                               </div>
+                       </div>
+                       <p class="media-widget-buttons">
+                               <button type="button" class="button edit-media selected">
+                                       Edit Video                              </button>
+                               <button type="button" class="button change-media select-media selected">
+                                       Replace Video                           </button>
+                               <button type="button" class="button select-media not-selected">
+                                       Add Video                               </button>
+                       </p>
+               </script>
+                               <script type="text/html" id="tmpl-wp-media-widget-video-preview">
+                       <# if ( data.error && 'missing_attachment' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p>We can&#8217;t find that video. Check your <a href="http://src.wordpress-develop.dev/wp-admin/upload.php">media library</a> and make sure it wasn&#8217;t deleted.</p>
+                               </div>
+                       <# } else if ( data.error && 'unsupported_file_type' === data.error ) { #>
+                               <div class="notice notice-error notice-alt notice-missing-attachment">
+                                       <p>Sorry, we can&#8217;t display the video file type selected. Please select a supported video file (<code>.mp4</code>, <code>.m4v</code>, <code>.webm</code>, <code>.ogv</code>, <code>.wmv</code>, <code>.flv</code>) or stream (YouTube or Vimeo) instead.</p>
+                               </div>
+                       <# } else if ( data.error ) { #>
+                               <div class="notice notice-error notice-alt">
+                                       <p>Unable to preview media due to an unknown error.</p>
+                               </div>
+                       <# } else if ( data.is_hosted_embed && data.model.poster ) { #>
+                               <a href="{{ data.model.src }}" target="_blank" class="media-widget-video-link">
+                                       <img src="{{ data.model.poster }}" />
+                               </a>
+                       <# } else if ( data.is_hosted_embed ) { #>
+                               <a href="{{ data.model.src }}" target="_blank" class="media-widget-video-link no-poster">
+                                       <span class="dashicons dashicons-format-video"></span>
+                               </a>
+                       <# } else if ( data.model.src ) { #>
+                               <#  var w_rule = '', classes = [],
+               w, h, settings = wp.media.view.settings,
+               isYouTube = isVimeo = false;
+
+       if ( ! _.isEmpty( data.model.src ) ) {
+               isYouTube = data.model.src.match(/youtube|youtu\.be/);
+               isVimeo = -1 !== data.model.src.indexOf('vimeo');
+       }
+
+       if ( settings.contentWidth && data.model.width >= settings.contentWidth ) {
+               w = settings.contentWidth;
+       } else {
+               w = data.model.width;
+       }
+
+       if ( w !== data.model.width ) {
+               h = Math.ceil( ( data.model.height * w ) / data.model.width );
+       } else {
+               h = data.model.height;
+       }
+
+       if ( w ) {
+               w_rule = 'width: ' + w + 'px; ';
+       }
+
+       if ( isYouTube ) {
+               classes.push( 'youtube-video' );
+       }
+
+       if ( isVimeo ) {
+               classes.push( 'vimeo-video' );
+       }
+
+#>
+<div style="{{ w_rule }}" class="wp-video">
+<video controls
+       class="wp-video-shortcode {{ classes.join( ' ' ) }}"
+       <# if ( w ) { #>width="{{ w }}"<# } #>
+       <# if ( h ) { #>height="{{ h }}"<# } #>
+       <#
+               if ( ! _.isUndefined( data.model.poster ) && data.model.poster ) {
+                       #> poster="{{ data.model.poster }}"<#
+               } #>
+               preload="{{ _.isUndefined( data.model.preload ) ? 'metadata' : data.model.preload }}"<#
+        if ( ! _.isUndefined( data.model.autoplay ) && data.model.autoplay ) {
+               #> autoplay<#
+       }
+        if ( ! _.isUndefined( data.model.loop ) && data.model.loop ) {
+               #> loop<#
+       }
+       #>
+>
+       <# if ( ! _.isEmpty( data.model.src ) ) {
+               if ( isYouTube ) { #>
+               <source src="{{ data.model.src }}" type="video/youtube" />
+               <# } else if ( isVimeo ) { #>
+               <source src="{{ data.model.src }}" type="video/vimeo" />
+               <# } else { #>
+               <source src="{{ data.model.src }}" type="{{ settings.embedMimes[ data.model.src.split('.').pop() ] }}" />
+               <# }
+       } #>
+
+       <# if ( data.model.mp4 ) { #>
+       <source src="{{ data.model.mp4 }}" type="{{ settings.embedMimes[ 'mp4' ] }}" />
+       <# } #>
+       <# if ( data.model.m4v ) { #>
+       <source src="{{ data.model.m4v }}" type="{{ settings.embedMimes[ 'm4v' ] }}" />
+       <# } #>
+       <# if ( data.model.webm ) { #>
+       <source src="{{ data.model.webm }}" type="{{ settings.embedMimes[ 'webm' ] }}" />
+       <# } #>
+       <# if ( data.model.ogv ) { #>
+       <source src="{{ data.model.ogv }}" type="{{ settings.embedMimes[ 'ogv' ] }}" />
+       <# } #>
+       <# if ( data.model.wmv ) { #>
+       <source src="{{ data.model.wmv }}" type="{{ settings.embedMimes[ 'wmv' ] }}" />
+       <# } #>
+       <# if ( data.model.flv ) { #>
+       <source src="{{ data.model.flv }}" type="{{ settings.embedMimes[ 'flv' ] }}" />
+       <# } #>
+               {{{ data.model.content }}}
+</video>
+</div>
+                       <# } #>
+               </script>
+                       <script type="text/html" id="tmpl-widget-media-media_audio-control">
+                               <# var elementIdPrefix = 'el' + String( Math.random() ) + '_' #>
+                               <p>
+                                       <label for="{{ elementIdPrefix }}title">Title:</label>
+                                       <input id="{{ elementIdPrefix }}title" type="text" class="widefat title">
+                               </p>
+                               <div class="media-widget-preview">
+                                       <div class="attachment-media-view">
+                                               <div class="placeholder">No audio selected</div>
+                                       </div>
+                               </div>
+                               <p class="media-widget-buttons">
+                                       <button type="button" class="button edit-media selected">
+                                               Edit Audio                              </button>
+                                       <button type="button" class="button change-media select-media selected">
+                                               Replace Audio                           </button>
+                                       <button type="button" class="button select-media not-selected">
+                                               Add File                                </button>
+                               </p>
+                       </script>
+                       <script type="text/html" id="tmpl-wp-media-widget-audio-preview">
+                               <# if ( data.error && 'missing_attachment' === data.error ) { #>
+                                       <div class="notice notice-error notice-alt notice-missing-attachment">
+                                               <p>We can&#8217;t find that audio file. Check your <a href="http://src.wordpress-develop.dev/wp-admin/upload.php">media library</a> and make sure it wasn&#8217;t deleted.</p>
+                                       </div>
+                               <# } else if ( data.error ) { #>
+                                       <div class="notice notice-error notice-alt">
+                                               <p>Unable to preview media due to an unknown error.</p>
+                                       </div>
+                               <# } else if ( data.model && data.model.src ) { #>
+                                       <audio style="visibility: hidden"
+                                               controls
+                                               class="wp-audio-shortcode"
+                                               width="{{ _.isUndefined( data.model.width ) ? 400 : data.model.width }}"
+                                               preload="{{ _.isUndefined( data.model.preload ) ? 'none' : data.model.preload }}"
+                                               <#
+                                               if ( ! _.isUndefined( data.model.autoplay ) && data.model.autoplay ) {
+                                                       #> autoplay<#
+                                               }
+                                               if ( ! _.isUndefined( data.model.loop ) && data.model.loop ) {
+                                                       #> loop<#
+                                               }
+                                               #>
+                                       >
+                                       <# if ( ! _.isEmpty( data.model.src ) ) { #>
+                                               <source src="{{ data.model.src }}" type="{{ wp.media.view.settings.embedMimes[ data.model.src.split('.').pop() ] }}" />
+                                       <# } #>
+
+                                       <# if ( ! _.isEmpty( data.model.mp3 ) ) { #>
+                                               <source src="{{ data.model.mp3 }}" type="{{ wp.media.view.settings.embedMimes[ 'mp3' ] }}" />
+                                       <# } #>
+                                       <# if ( ! _.isEmpty( data.model.ogg ) ) { #>
+                                               <source src="{{ data.model.ogg }}" type="{{ wp.media.view.settings.embedMimes[ 'ogg' ] }}" />
+                                       <# } #>
+                                       <# if ( ! _.isEmpty( data.model.wma ) ) { #>
+                                               <source src="{{ data.model.wma }}" type="{{ wp.media.view.settings.embedMimes[ 'wma' ] }}" />
+                                       <# } #>
+                                       <# if ( ! _.isEmpty( data.model.m4a ) ) { #>
+                                               <source src="{{ data.model.m4a }}" type="{{ wp.media.view.settings.embedMimes[ 'm4a' ] }}" />
+                                       <# } #>
+                                       <# if ( ! _.isEmpty( data.model.wav ) ) { #>
+                                               <source src="{{ data.model.wav }}" type="{{ wp.media.view.settings.embedMimes[ 'wav' ] }}" />
+                                       <# } #>
+                                       </audio>
+                               <# } #>
+                       </script>
+                       <script type="text/html" id="tmpl-media-frame">
+               <div class="media-frame-menu"></div>
+               <div class="media-frame-title"></div>
+               <div class="media-frame-router"></div>
+               <div class="media-frame-content"></div>
+               <div class="media-frame-toolbar"></div>
+               <div class="media-frame-uploader"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-media-modal">
+               <div class="media-modal wp-core-ui">
+                       <button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text">Close media panel</span></span></button>
+                       <div class="media-modal-content"></div>
+               </div>
+               <div class="media-modal-backdrop"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-uploader-window">
+               <div class="uploader-window-content">
+                       <h1>Drop files to upload</h1>
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-uploader-editor">
+               <div class="uploader-editor-content">
+                       <div class="uploader-editor-title">Drop files to upload</div>
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-uploader-inline">
+               <# var messageClass = data.message ? 'has-upload-message' : 'no-upload-message'; #>
+               <# if ( data.canClose ) { #>
+               <button class="close dashicons dashicons-no"><span class="screen-reader-text">Close uploader</span></button>
+               <# } #>
+               <div class="uploader-inline-content {{ messageClass }}">
+               <# if ( data.message ) { #>
+                       <h2 class="upload-message">{{ data.message }}</h2>
+               <# } #>
+                                       <div class="upload-ui">
+                               <h2 class="upload-instructions drop-instructions">Drop files anywhere to upload</h2>
+                               <p class="upload-instructions drop-instructions">or</p>
+                               <button type="button" class="browser button button-hero">Select Files</button>
+                       </div>
+
+                       <div class="upload-inline-status"></div>
+
+                       <div class="post-upload-ui">
+
+                               <p class="max-upload-size">Maximum upload file size: 2 MB.</p>
+
+                               <# if ( data.suggestedWidth && data.suggestedHeight ) { #>
+                                       <p class="suggested-dimensions">
+                                               Suggested image dimensions: {{data.suggestedWidth}} &times; {{data.suggestedHeight}}
+                                       </p>
+                               <# } #>
+
+                                                       </div>
+                               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-media-library-view-switcher">
+               <a href="?mode=list" class="view-list">
+                       <span class="screen-reader-text">List View</span>
+               </a>
+               <a href="?mode=grid" class="view-grid current">
+                       <span class="screen-reader-text">Grid View</span>
+               </a>
+       </script>
+
+       <script type="text/html" id="tmpl-uploader-status">
+               <h2>Uploading</h2>
+               <button type="button" class="button-link upload-dismiss-errors"><span class="screen-reader-text">Dismiss Errors</span></button>
+
+               <div class="media-progress-bar"><div></div></div>
+               <div class="upload-details">
+                       <span class="upload-count">
+                               <span class="upload-index"></span> / <span class="upload-total"></span>
+                       </span>
+                       <span class="upload-detail-separator">&ndash;</span>
+                       <span class="upload-filename"></span>
+               </div>
+               <div class="upload-errors"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-uploader-status-error">
+               <span class="upload-error-filename">{{{ data.filename }}}</span>
+               <span class="upload-error-message">{{ data.message }}</span>
+       </script>
+
+       <script type="text/html" id="tmpl-edit-attachment-frame">
+               <div class="edit-media-header">
+                       <button class="left dashicons <# if ( ! data.hasPrevious ) { #> disabled <# } #>"><span class="screen-reader-text">Edit previous media item</span></button>
+                       <button class="right dashicons <# if ( ! data.hasNext ) { #> disabled <# } #>"><span class="screen-reader-text">Edit next media item</span></button>
+               </div>
+               <div class="media-frame-title"></div>
+               <div class="media-frame-content"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-attachment-details-two-column">
+               <div class="attachment-media-view {{ data.orientation }}">
+                       <div class="thumbnail thumbnail-{{ data.type }}">
+                               <# if ( data.uploading ) { #>
+                                       <div class="media-progress-bar"><div></div></div>
+                               <# } else if ( data.sizes && data.sizes.large ) { #>
+                                       <img class="details-image" src="{{ data.sizes.large.url }}" draggable="false" alt="" />
+                               <# } else if ( data.sizes && data.sizes.full ) { #>
+                                       <img class="details-image" src="{{ data.sizes.full.url }}" draggable="false" alt="" />
+                               <# } else if ( -1 === jQuery.inArray( data.type, [ 'audio', 'video' ] ) ) { #>
+                                       <img class="details-image icon" src="{{ data.icon }}" draggable="false" alt="" />
+                               <# } #>
+
+                               <# if ( 'audio' === data.type ) { #>
+                               <div class="wp-media-wrapper">
+                                       <audio style="visibility: hidden" controls class="wp-audio-shortcode" width="100%" preload="none">
+                                               <source type="{{ data.mime }}" src="{{ data.url }}"/>
+                                       </audio>
+                               </div>
+                               <# } else if ( 'video' === data.type ) {
+                                       var w_rule = '';
+                                       if ( data.width ) {
+                                               w_rule = 'width: ' + data.width + 'px;';
+                                       } else if ( wp.media.view.settings.contentWidth ) {
+                                               w_rule = 'width: ' + wp.media.view.settings.contentWidth + 'px;';
+                                       }
+                               #>
+                               <div style="{{ w_rule }}" class="wp-media-wrapper wp-video">
+                                       <video controls="controls" class="wp-video-shortcode" preload="metadata"
+                                               <# if ( data.width ) { #>width="{{ data.width }}"<# } #>
+                                               <# if ( data.height ) { #>height="{{ data.height }}"<# } #>
+                                               <# if ( data.image && data.image.src !== data.icon ) { #>poster="{{ data.image.src }}"<# } #>>
+                                               <source type="{{ data.mime }}" src="{{ data.url }}"/>
+                                       </video>
+                               </div>
+                               <# } #>
+
+                               <div class="attachment-actions">
+                                       <# if ( 'image' === data.type && ! data.uploading && data.sizes && data.can.save ) { #>
+                                       <button type="button" class="button edit-attachment">Edit Image</button>
+                                       <# } else if ( 'pdf' === data.subtype && data.sizes ) { #>
+                                       Document Preview                                        <# } #>
+                               </div>
+                       </div>
+               </div>
+               <div class="attachment-info">
+                       <span class="settings-save-status">
+                               <span class="spinner"></span>
+                               <span class="saved">Saved.</span>
+                       </span>
+                       <div class="details">
+                               <div class="filename"><strong>File name:</strong> {{ data.filename }}</div>
+                               <div class="filename"><strong>File type:</strong> {{ data.mime }}</div>
+                               <div class="uploaded"><strong>Uploaded on:</strong> {{ data.dateFormatted }}</div>
+
+                               <div class="file-size"><strong>File size:</strong> {{ data.filesizeHumanReadable }}</div>
+                               <# if ( 'image' === data.type && ! data.uploading ) { #>
+                                       <# if ( data.width && data.height ) { #>
+                                               <div class="dimensions"><strong>Dimensions:</strong> {{ data.width }} &times; {{ data.height }}</div>
+                                       <# } #>
+                               <# } #>
+
+                               <# if ( data.fileLength ) { #>
+                                       <div class="file-length"><strong>Length:</strong> {{ data.fileLength }}</div>
+                               <# } #>
+
+                               <# if ( 'audio' === data.type && data.meta.bitrate ) { #>
+                                       <div class="bitrate">
+                                               <strong>Bitrate:</strong> {{ Math.round( data.meta.bitrate / 1000 ) }}kb/s
+                                               <# if ( data.meta.bitrate_mode ) { #>
+                                               {{ ' ' + data.meta.bitrate_mode.toUpperCase() }}
+                                               <# } #>
+                                       </div>
+                               <# } #>
+
+                               <div class="compat-meta">
+                                       <# if ( data.compat && data.compat.meta ) { #>
+                                               {{{ data.compat.meta }}}
+                                       <# } #>
+                               </div>
+                       </div>
+
+                       <div class="settings">
+                               <label class="setting" data-setting="url">
+                                       <span class="name">URL</span>
+                                       <input type="text" value="{{ data.url }}" readonly />
+                               </label>
+                               <# var maybeReadOnly = data.can.save || data.allowLocalEdits ? '' : 'readonly'; #>
+                                                               <label class="setting" data-setting="title">
+                                       <span class="name">Title</span>
+                                       <input type="text" value="{{ data.title }}" {{ maybeReadOnly }} />
+                               </label>
+                                                               <# if ( 'audio' === data.type ) { #>
+                                                               <label class="setting" data-setting="artist">
+                                       <span class="name">Artist</span>
+                                       <input type="text" value="{{ data.artist || data.meta.artist || '' }}" />
+                               </label>
+                                                               <label class="setting" data-setting="album">
+                                       <span class="name">Album</span>
+                                       <input type="text" value="{{ data.album || data.meta.album || '' }}" />
+                               </label>
+                                                               <# } #>
+                               <label class="setting" data-setting="caption">
+                                       <span class="name">Caption</span>
+                                       <textarea {{ maybeReadOnly }}>{{ data.caption }}</textarea>
+                               </label>
+                               <# if ( 'image' === data.type ) { #>
+                                       <label class="setting" data-setting="alt">
+                                               <span class="name">Alt Text</span>
+                                               <input type="text" value="{{ data.alt }}" {{ maybeReadOnly }} />
+                                       </label>
+                               <# } #>
+                               <label class="setting" data-setting="description">
+                                       <span class="name">Description</span>
+                                       <textarea {{ maybeReadOnly }}>{{ data.description }}</textarea>
+                               </label>
+                               <label class="setting">
+                                       <span class="name">Uploaded By</span>
+                                       <span class="value">{{ data.authorName }}</span>
+                               </label>
+                               <# if ( data.uploadedToTitle ) { #>
+                                       <label class="setting">
+                                               <span class="name">Uploaded To</span>
+                                               <# if ( data.uploadedToLink ) { #>
+                                                       <span class="value"><a href="{{ data.uploadedToLink }}">{{ data.uploadedToTitle }}</a></span>
+                                               <# } else { #>
+                                                       <span class="value">{{ data.uploadedToTitle }}</span>
+                                               <# } #>
+                                       </label>
+                               <# } #>
+                               <div class="attachment-compat"></div>
+                       </div>
+
+                       <div class="actions">
+                               <a class="view-attachment" href="{{ data.link }}">View attachment page</a>
+                               <# if ( data.can.save ) { #> |
+                                       <a href="post.php?post={{ data.id }}&action=edit">Edit more details</a>
+                               <# } #>
+                               <# if ( ! data.uploading && data.can.remove ) { #> |
+                                                                                       <button type="button" class="button-link delete-attachment">Delete Permanently</button>
+                                                                       <# } #>
+                       </div>
+
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-attachment">
+               <div class="attachment-preview js--select-attachment type-{{ data.type }} subtype-{{ data.subtype }} {{ data.orientation }}">
+                       <div class="thumbnail">
+                               <# if ( data.uploading ) { #>
+                                       <div class="media-progress-bar"><div style="width: {{ data.percent }}%"></div></div>
+                               <# } else if ( 'image' === data.type && data.sizes ) { #>
+                                       <div class="centered">
+                                               <img src="{{ data.size.url }}" draggable="false" alt="" />
+                                       </div>
+                               <# } else { #>
+                                       <div class="centered">
+                                               <# if ( data.image && data.image.src && data.image.src !== data.icon ) { #>
+                                                       <img src="{{ data.image.src }}" class="thumbnail" draggable="false" alt="" />
+                                               <# } else if ( data.sizes && data.sizes.medium ) { #>
+                                                       <img src="{{ data.sizes.medium.url }}" class="thumbnail" draggable="false" alt="" />
+                                               <# } else { #>
+                                                       <img src="{{ data.icon }}" class="icon" draggable="false" alt="" />
+                                               <# } #>
+                                       </div>
+                                       <div class="filename">
+                                               <div>{{ data.filename }}</div>
+                                       </div>
+                               <# } #>
+                       </div>
+                       <# if ( data.buttons.close ) { #>
+                               <button type="button" class="button-link attachment-close media-modal-icon"><span class="screen-reader-text">Remove</span></button>
+                       <# } #>
+               </div>
+               <# if ( data.buttons.check ) { #>
+                       <button type="button" class="check" tabindex="-1"><span class="media-modal-icon"></span><span class="screen-reader-text">Deselect</span></button>
+               <# } #>
+               <#
+               var maybeReadOnly = data.can.save || data.allowLocalEdits ? '' : 'readonly';
+               if ( data.describe ) {
+                       if ( 'image' === data.type ) { #>
+                               <input type="text" value="{{ data.caption }}" class="describe" data-setting="caption"
+                                       placeholder="Caption this image&hellip;" {{ maybeReadOnly }} />
+                       <# } else { #>
+                               <input type="text" value="{{ data.title }}" class="describe" data-setting="title"
+                                       <# if ( 'video' === data.type ) { #>
+                                               placeholder="Describe this video&hellip;"
+                                       <# } else if ( 'audio' === data.type ) { #>
+                                               placeholder="Describe this audio file&hellip;"
+                                       <# } else { #>
+                                               placeholder="Describe this media file&hellip;"
+                                       <# } #> {{ maybeReadOnly }} />
+                       <# }
+               } #>
+       </script>
+
+       <script type="text/html" id="tmpl-attachment-details">
+               <h2>
+                       Attachment Details                      <span class="settings-save-status">
+                               <span class="spinner"></span>
+                               <span class="saved">Saved.</span>
+                       </span>
+               </h2>
+               <div class="attachment-info">
+                       <div class="thumbnail thumbnail-{{ data.type }}">
+                               <# if ( data.uploading ) { #>
+                                       <div class="media-progress-bar"><div></div></div>
+                               <# } else if ( 'image' === data.type && data.sizes ) { #>
+                                       <img src="{{ data.size.url }}" draggable="false" alt="" />
+                               <# } else { #>
+                                       <img src="{{ data.icon }}" class="icon" draggable="false" alt="" />
+                               <# } #>
+                       </div>
+                       <div class="details">
+                               <div class="filename">{{ data.filename }}</div>
+                               <div class="uploaded">{{ data.dateFormatted }}</div>
+
+                               <div class="file-size">{{ data.filesizeHumanReadable }}</div>
+                               <# if ( 'image' === data.type && ! data.uploading ) { #>
+                                       <# if ( data.width && data.height ) { #>
+                                               <div class="dimensions">{{ data.width }} &times; {{ data.height }}</div>
+                                       <# } #>
+
+                                       <# if ( data.can.save && data.sizes ) { #>
+                                               <a class="edit-attachment" href="{{ data.editLink }}&amp;image-editor" target="_blank">Edit Image</a>
+                                       <# } #>
+                               <# } #>
+
+                               <# if ( data.fileLength ) { #>
+                                       <div class="file-length">Length: {{ data.fileLength }}</div>
+                               <# } #>
+
+                               <# if ( ! data.uploading && data.can.remove ) { #>
+                                                                                       <button type="button" class="button-link delete-attachment">Delete Permanently</button>
+                                                                       <# } #>
+
+                               <div class="compat-meta">
+                                       <# if ( data.compat && data.compat.meta ) { #>
+                                               {{{ data.compat.meta }}}
+                                       <# } #>
+                               </div>
+                       </div>
+               </div>
+
+               <label class="setting" data-setting="url">
+                       <span class="name">URL</span>
+                       <input type="text" value="{{ data.url }}" readonly />
+               </label>
+               <# var maybeReadOnly = data.can.save || data.allowLocalEdits ? '' : 'readonly'; #>
+                               <label class="setting" data-setting="title">
+                       <span class="name">Title</span>
+                       <input type="text" value="{{ data.title }}" {{ maybeReadOnly }} />
+               </label>
+                               <# if ( 'audio' === data.type ) { #>
+                               <label class="setting" data-setting="artist">
+                       <span class="name">Artist</span>
+                       <input type="text" value="{{ data.artist || data.meta.artist || '' }}" />
+               </label>
+                               <label class="setting" data-setting="album">
+                       <span class="name">Album</span>
+                       <input type="text" value="{{ data.album || data.meta.album || '' }}" />
+               </label>
+                               <# } #>
+               <label class="setting" data-setting="caption">
+                       <span class="name">Caption</span>
+                       <textarea {{ maybeReadOnly }}>{{ data.caption }}</textarea>
+               </label>
+               <# if ( 'image' === data.type ) { #>
+                       <label class="setting" data-setting="alt">
+                               <span class="name">Alt Text</span>
+                               <input type="text" value="{{ data.alt }}" {{ maybeReadOnly }} />
+                       </label>
+               <# } #>
+               <label class="setting" data-setting="description">
+                       <span class="name">Description</span>
+                       <textarea {{ maybeReadOnly }}>{{ data.description }}</textarea>
+               </label>
+       </script>
+
+       <script type="text/html" id="tmpl-media-selection">
+               <div class="selection-info">
+                       <span class="count"></span>
+                       <# if ( data.editable ) { #>
+                               <button type="button" class="button-link edit-selection">Edit Selection</button>
+                       <# } #>
+                       <# if ( data.clearable ) { #>
+                               <button type="button" class="button-link clear-selection">Clear</button>
+                       <# } #>
+               </div>
+               <div class="selection-view"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-attachment-display-settings">
+               <h2>Attachment Display Settings</h2>
+
+               <# if ( 'image' === data.type ) { #>
+                       <label class="setting">
+                               <span>Alignment</span>
+                               <select class="alignment"
+                                       data-setting="align"
+                                       <# if ( data.userSettings ) { #>
+                                               data-user-setting="align"
+                                       <# } #>>
+
+                                       <option value="left">
+                                               Left                                    </option>
+                                       <option value="center">
+                                               Center                                  </option>
+                                       <option value="right">
+                                               Right                                   </option>
+                                       <option value="none" selected>
+                                               None                                    </option>
+                               </select>
+                       </label>
+               <# } #>
+
+               <div class="setting">
+                       <label>
+                               <# if ( data.model.canEmbed ) { #>
+                                       <span>Embed or Link</span>
+                               <# } else { #>
+                                       <span>Link To</span>
+                               <# } #>
+
+                               <select class="link-to"
+                                       data-setting="link"
+                                       <# if ( data.userSettings && ! data.model.canEmbed ) { #>
+                                               data-user-setting="urlbutton"
+                                       <# } #>>
+
+                               <# if ( data.model.canEmbed ) { #>
+                                       <option value="embed" selected>
+                                               Embed Media Player                                      </option>
+                                       <option value="file">
+                               <# } else { #>
+                                       <option value="none" selected>
+                                               None                                    </option>
+                                       <option value="file">
+                               <# } #>
+                                       <# if ( data.model.canEmbed ) { #>
+                                               Link to Media File                                      <# } else { #>
+                                               Media File                                      <# } #>
+                                       </option>
+                                       <option value="post">
+                                       <# if ( data.model.canEmbed ) { #>
+                                               Link to Attachment Page                                 <# } else { #>
+                                               Attachment Page                                 <# } #>
+                                       </option>
+                               <# if ( 'image' === data.type ) { #>
+                                       <option value="custom">
+                                               Custom URL                                      </option>
+                               <# } #>
+                               </select>
+                       </label>
+                       <input type="text" class="link-to-custom" data-setting="linkUrl" />
+               </div>
+
+               <# if ( 'undefined' !== typeof data.sizes ) { #>
+                       <label class="setting">
+                               <span>Size</span>
+                               <select class="size" name="size"
+                                       data-setting="size"
+                                       <# if ( data.userSettings ) { #>
+                                               data-user-setting="imgsize"
+                                       <# } #>>
+                                                                                       <#
+                                               var size = data.sizes['thumbnail'];
+                                               if ( size ) { #>
+                                                       <option value="thumbnail" >
+                                                               Thumbnail &ndash; {{ size.width }} &times; {{ size.height }}
+                                                       </option>
+                                               <# } #>
+                                                                                       <#
+                                               var size = data.sizes['medium'];
+                                               if ( size ) { #>
+                                                       <option value="medium" >
+                                                               Medium &ndash; {{ size.width }} &times; {{ size.height }}
+                                                       </option>
+                                               <# } #>
+                                                                                       <#
+                                               var size = data.sizes['large'];
+                                               if ( size ) { #>
+                                                       <option value="large" >
+                                                               Large &ndash; {{ size.width }} &times; {{ size.height }}
+                                                       </option>
+                                               <# } #>
+                                                                                       <#
+                                               var size = data.sizes['full'];
+                                               if ( size ) { #>
+                                                       <option value="full"  selected='selected'>
+                                                               Full Size &ndash; {{ size.width }} &times; {{ size.height }}
+                                                       </option>
+                                               <# } #>
+                                                                       </select>
+                       </label>
+               <# } #>
+       </script>
+
+       <script type="text/html" id="tmpl-gallery-settings">
+               <h2>Gallery Settings</h2>
+
+               <label class="setting">
+                       <span>Link To</span>
+                       <select class="link-to"
+                               data-setting="link"
+                               <# if ( data.userSettings ) { #>
+                                       data-user-setting="urlbutton"
+                               <# } #>>
+
+                               <option value="post" <# if ( ! wp.media.galleryDefaults.link || 'post' == wp.media.galleryDefaults.link ) {
+                                       #>selected="selected"<# }
+                               #>>
+                                       Attachment Page                         </option>
+                               <option value="file" <# if ( 'file' == wp.media.galleryDefaults.link ) { #>selected="selected"<# } #>>
+                                       Media File                              </option>
+                               <option value="none" <# if ( 'none' == wp.media.galleryDefaults.link ) { #>selected="selected"<# } #>>
+                                       None                            </option>
+                       </select>
+               </label>
+
+               <label class="setting">
+                       <span>Columns</span>
+                       <select class="columns" name="columns"
+                               data-setting="columns">
+                                                                       <option value="1" <#
+                                               if ( 1 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               1                                       </option>
+                                                                       <option value="2" <#
+                                               if ( 2 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               2                                       </option>
+                                                                       <option value="3" <#
+                                               if ( 3 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               3                                       </option>
+                                                                       <option value="4" <#
+                                               if ( 4 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               4                                       </option>
+                                                                       <option value="5" <#
+                                               if ( 5 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               5                                       </option>
+                                                                       <option value="6" <#
+                                               if ( 6 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               6                                       </option>
+                                                                       <option value="7" <#
+                                               if ( 7 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               7                                       </option>
+                                                                       <option value="8" <#
+                                               if ( 8 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               8                                       </option>
+                                                                       <option value="9" <#
+                                               if ( 9 == wp.media.galleryDefaults.columns ) { #>selected="selected"<# }
+                                       #>>
+                                               9                                       </option>
+                                                       </select>
+               </label>
+
+               <label class="setting">
+                       <span>Random Order</span>
+                       <input type="checkbox" data-setting="_orderbyRandom" />
+               </label>
+
+               <label class="setting size">
+                       <span>Size</span>
+                       <select class="size" name="size"
+                               data-setting="size"
+                               <# if ( data.userSettings ) { #>
+                                       data-user-setting="imgsize"
+                               <# } #>
+                               >
+                                                                       <option value="thumbnail">
+                                               Thumbnail                                       </option>
+                                                                       <option value="medium">
+                                               Medium                                  </option>
+                                                                       <option value="large">
+                                               Large                                   </option>
+                                                                       <option value="full">
+                                               Full Size                                       </option>
+                                                       </select>
+               </label>
+       </script>
+
+       <script type="text/html" id="tmpl-playlist-settings">
+               <h2>Playlist Settings</h2>
+
+               <# var emptyModel = _.isEmpty( data.model ),
+                       isVideo = 'video' === data.controller.get('library').props.get('type'); #>
+
+               <label class="setting">
+                       <input type="checkbox" data-setting="tracklist" <# if ( emptyModel ) { #>
+                               checked="checked"
+                       <# } #> />
+                       <# if ( isVideo ) { #>
+                       <span>Show Video List</span>
+                       <# } else { #>
+                       <span>Show Tracklist</span>
+                       <# } #>
+               </label>
+
+               <# if ( ! isVideo ) { #>
+               <label class="setting">
+                       <input type="checkbox" data-setting="artists" <# if ( emptyModel ) { #>
+                               checked="checked"
+                       <# } #> />
+                       <span>Show Artist Name in Tracklist</span>
+               </label>
+               <# } #>
+
+               <label class="setting">
+                       <input type="checkbox" data-setting="images" <# if ( emptyModel ) { #>
+                               checked="checked"
+                       <# } #> />
+                       <span>Show Images</span>
+               </label>
+       </script>
+
+       <script type="text/html" id="tmpl-embed-link-settings">
+               <label class="setting link-text">
+                       <span>Link Text</span>
+                       <input type="text" class="alignment" data-setting="linkText" />
+               </label>
+               <div class="embed-container" style="display: none;">
+                       <div class="embed-preview"></div>
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-embed-image-settings">
+               <div class="thumbnail">
+                       <img src="{{ data.model.url }}" draggable="false" alt="" />
+               </div>
+
+                                       <label class="setting caption">
+                               <span>Caption</span>
+                               <textarea data-setting="caption" />
+                       </label>
+
+               <label class="setting alt-text">
+                       <span>Alt Text</span>
+                       <input type="text" data-setting="alt" />
+               </label>
+
+               <div class="setting align">
+                       <span>Align</span>
+                       <div class="button-group button-large" data-setting="align">
+                               <button class="button" value="left">
+                                       Left                            </button>
+                               <button class="button" value="center">
+                                       Center                          </button>
+                               <button class="button" value="right">
+                                       Right                           </button>
+                               <button class="button active" value="none">
+                                       None                            </button>
+                       </div>
+               </div>
+
+               <div class="setting link-to">
+                       <span>Link To</span>
+                       <div class="button-group button-large" data-setting="link">
+                               <button class="button" value="file">
+                                       Image URL                               </button>
+                               <button class="button" value="custom">
+                                       Custom URL                              </button>
+                               <button class="button active" value="none">
+                                       None                            </button>
+                       </div>
+                       <input type="text" class="link-to-custom" data-setting="linkUrl" />
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-image-details">
+               <div class="media-embed">
+                       <div class="embed-media-settings">
+                               <div class="column-image">
+                                       <div class="image">
+                                               <img src="{{ data.model.url }}" draggable="false" alt="" />
+
+                                               <# if ( data.attachment && window.imageEdit ) { #>
+                                                       <div class="actions">
+                                                               <input type="button" class="edit-attachment button" value="Edit Original" />
+                                                               <input type="button" class="replace-attachment button" value="Replace" />
+                                                       </div>
+                                               <# } #>
+                                       </div>
+                               </div>
+                               <div class="column-settings">
+                                                                                       <label class="setting caption">
+                                                       <span>Caption</span>
+                                                       <textarea data-setting="caption">{{ data.model.caption }}</textarea>
+                                               </label>
+
+                                       <label class="setting alt-text">
+                                               <span>Alternative Text</span>
+                                               <input type="text" data-setting="alt" value="{{ data.model.alt }}" />
+                                       </label>
+
+                                       <h2>Display Settings</h2>
+                                       <div class="setting align">
+                                               <span>Align</span>
+                                               <div class="button-group button-large" data-setting="align">
+                                                       <button class="button" value="left">
+                                                               Left                                                    </button>
+                                                       <button class="button" value="center">
+                                                               Center                                                  </button>
+                                                       <button class="button" value="right">
+                                                               Right                                                   </button>
+                                                       <button class="button active" value="none">
+                                                               None                                                    </button>
+                                               </div>
+                                       </div>
+
+                                       <# if ( data.attachment ) { #>
+                                               <# if ( 'undefined' !== typeof data.attachment.sizes ) { #>
+                                                       <label class="setting size">
+                                                               <span>Size</span>
+                                                               <select class="size" name="size"
+                                                                       data-setting="size"
+                                                                       <# if ( data.userSettings ) { #>
+                                                                               data-user-setting="imgsize"
+                                                                       <# } #>>
+                                                                                                                                                       <#
+                                                                               var size = data.sizes['thumbnail'];
+                                                                               if ( size ) { #>
+                                                                                       <option value="thumbnail">
+                                                                                               Thumbnail &ndash; {{ size.width }} &times; {{ size.height }}
+                                                                                       </option>
+                                                                               <# } #>
+                                                                                                                                                       <#
+                                                                               var size = data.sizes['medium'];
+                                                                               if ( size ) { #>
+                                                                                       <option value="medium">
+                                                                                               Medium &ndash; {{ size.width }} &times; {{ size.height }}
+                                                                                       </option>
+                                                                               <# } #>
+                                                                                                                                                       <#
+                                                                               var size = data.sizes['large'];
+                                                                               if ( size ) { #>
+                                                                                       <option value="large">
+                                                                                               Large &ndash; {{ size.width }} &times; {{ size.height }}
+                                                                                       </option>
+                                                                               <# } #>
+                                                                                                                                                       <#
+                                                                               var size = data.sizes['full'];
+                                                                               if ( size ) { #>
+                                                                                       <option value="full">
+                                                                                               Full Size &ndash; {{ size.width }} &times; {{ size.height }}
+                                                                                       </option>
+                                                                               <# } #>
+                                                                                                                                               <option value="custom">
+                                                                               Custom Size                                                                     </option>
+                                                               </select>
+                                                       </label>
+                                               <# } #>
+                                                       <div class="custom-size<# if ( data.model.size !== 'custom' ) { #> hidden<# } #>">
+                                                               <label><span>Width <small>(px)</small></span> <input data-setting="customWidth" type="number" step="1" value="{{ data.model.customWidth }}" /></label><span class="sep">&times;</span><label><span>Height <small>(px)</small></span><input data-setting="customHeight" type="number" step="1" value="{{ data.model.customHeight }}" /></label>
+                                                       </div>
+                                       <# } #>
+
+                                       <div class="setting link-to">
+                                               <span>Link To</span>
+                                               <select data-setting="link">
+                                               <# if ( data.attachment ) { #>
+                                                       <option value="file">
+                                                               Media File                                                      </option>
+                                                       <option value="post">
+                                                               Attachment Page                                                 </option>
+                                               <# } else { #>
+                                                       <option value="file">
+                                                               Image URL                                                       </option>
+                                               <# } #>
+                                                       <option value="custom">
+                                                               Custom URL                                                      </option>
+                                                       <option value="none">
+                                                               None                                                    </option>
+                                               </select>
+                                               <input type="text" class="link-to-custom" data-setting="linkUrl" />
+                                       </div>
+                                       <div class="advanced-section">
+                                               <h2><button type="button" class="button-link advanced-toggle">Advanced Options</button></h2>
+                                               <div class="advanced-settings hidden">
+                                                       <div class="advanced-image">
+                                                               <label class="setting title-text">
+                                                                       <span>Image Title Attribute</span>
+                                                                       <input type="text" data-setting="title" value="{{ data.model.title }}" />
+                                                               </label>
+                                                               <label class="setting extra-classes">
+                                                                       <span>Image CSS Class</span>
+                                                                       <input type="text" data-setting="extraClasses" value="{{ data.model.extraClasses }}" />
+                                                               </label>
+                                                       </div>
+                                                       <div class="advanced-link">
+                                                               <div class="setting link-target">
+                                                                       <label><input type="checkbox" data-setting="linkTargetBlank" value="_blank" <# if ( data.model.linkTargetBlank ) { #>checked="checked"<# } #>>Open link in a new tab</label>
+                                                               </div>
+                                                               <label class="setting link-rel">
+                                                                       <span>Link Rel</span>
+                                                                       <input type="text" data-setting="linkRel" value="{{ data.model.linkClassName }}" />
+                                                               </label>
+                                                               <label class="setting link-class-name">
+                                                                       <span>Link CSS Class</span>
+                                                                       <input type="text" data-setting="linkClassName" value="{{ data.model.linkClassName }}" />
+                                                               </label>
+                                                       </div>
+                                               </div>
+                                       </div>
+                               </div>
+                       </div>
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-image-editor">
+               <div id="media-head-{{ data.id }}"></div>
+               <div id="image-editor-{{ data.id }}"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-audio-details">
+               <# var ext, html5types = {
+                       mp3: wp.media.view.settings.embedMimes.mp3,
+                       ogg: wp.media.view.settings.embedMimes.ogg
+               }; #>
+
+                               <div class="media-embed media-embed-details">
+                       <div class="embed-media-settings embed-audio-settings">
+                               <audio style="visibility: hidden"
+       controls
+       class="wp-audio-shortcode"
+       width="{{ _.isUndefined( data.model.width ) ? 400 : data.model.width }}"
+       preload="{{ _.isUndefined( data.model.preload ) ? 'none' : data.model.preload }}"
+       <#
+       if ( ! _.isUndefined( data.model.autoplay ) && data.model.autoplay ) {
+               #> autoplay<#
+       }
+       if ( ! _.isUndefined( data.model.loop ) && data.model.loop ) {
+               #> loop<#
+       }
+       #>
+>
+       <# if ( ! _.isEmpty( data.model.src ) ) { #>
+       <source src="{{ data.model.src }}" type="{{ wp.media.view.settings.embedMimes[ data.model.src.split('.').pop() ] }}" />
+       <# } #>
+
+       <# if ( ! _.isEmpty( data.model.mp3 ) ) { #>
+       <source src="{{ data.model.mp3 }}" type="{{ wp.media.view.settings.embedMimes[ 'mp3' ] }}" />
+       <# } #>
+       <# if ( ! _.isEmpty( data.model.ogg ) ) { #>
+       <source src="{{ data.model.ogg }}" type="{{ wp.media.view.settings.embedMimes[ 'ogg' ] }}" />
+       <# } #>
+       <# if ( ! _.isEmpty( data.model.wma ) ) { #>
+       <source src="{{ data.model.wma }}" type="{{ wp.media.view.settings.embedMimes[ 'wma' ] }}" />
+       <# } #>
+       <# if ( ! _.isEmpty( data.model.m4a ) ) { #>
+       <source src="{{ data.model.m4a }}" type="{{ wp.media.view.settings.embedMimes[ 'm4a' ] }}" />
+       <# } #>
+       <# if ( ! _.isEmpty( data.model.wav ) ) { #>
+       <source src="{{ data.model.wav }}" type="{{ wp.media.view.settings.embedMimes[ 'wav' ] }}" />
+       <# } #>
+       </audio>
+
+                               <# if ( ! _.isEmpty( data.model.src ) ) {
+                                       ext = data.model.src.split('.').pop();
+                                       if ( html5types[ ext ] ) {
+                                               delete html5types[ ext ];
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>SRC</span>
+                                       <input type="text" disabled="disabled" data-setting="src" value="{{ data.model.src }}" />
+                                       <button type="button" class="button-link remove-setting">Remove audio source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.mp3 ) ) {
+                                       if ( ! _.isUndefined( html5types.mp3 ) ) {
+                                               delete html5types.mp3;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>MP3</span>
+                                       <input type="text" disabled="disabled" data-setting="mp3" value="{{ data.model.mp3 }}" />
+                                       <button type="button" class="button-link remove-setting">Remove audio source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.ogg ) ) {
+                                       if ( ! _.isUndefined( html5types.ogg ) ) {
+                                               delete html5types.ogg;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>OGG</span>
+                                       <input type="text" disabled="disabled" data-setting="ogg" value="{{ data.model.ogg }}" />
+                                       <button type="button" class="button-link remove-setting">Remove audio source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.wma ) ) {
+                                       if ( ! _.isUndefined( html5types.wma ) ) {
+                                               delete html5types.wma;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>WMA</span>
+                                       <input type="text" disabled="disabled" data-setting="wma" value="{{ data.model.wma }}" />
+                                       <button type="button" class="button-link remove-setting">Remove audio source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.m4a ) ) {
+                                       if ( ! _.isUndefined( html5types.m4a ) ) {
+                                               delete html5types.m4a;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>M4A</span>
+                                       <input type="text" disabled="disabled" data-setting="m4a" value="{{ data.model.m4a }}" />
+                                       <button type="button" class="button-link remove-setting">Remove audio source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.wav ) ) {
+                                       if ( ! _.isUndefined( html5types.wav ) ) {
+                                               delete html5types.wav;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>WAV</span>
+                                       <input type="text" disabled="disabled" data-setting="wav" value="{{ data.model.wav }}" />
+                                       <button type="button" class="button-link remove-setting">Remove audio source</button>
+                               </label>
+                               <# } #>
+
+                               <# if ( ! _.isEmpty( html5types ) ) { #>
+                               <div class="setting">
+                                       <span>Add alternate sources for maximum HTML5 playback:</span>
+                                       <div class="button-large">
+                                       <# _.each( html5types, function (mime, type) { #>
+                                       <button class="button add-media-source" data-mime="{{ mime }}">{{ type }}</button>
+                                       <# } ) #>
+                                       </div>
+                               </div>
+                               <# } #>
+
+                               <div class="setting preload">
+                                       <span>Preload</span>
+                                       <div class="button-group button-large" data-setting="preload">
+                                               <button class="button" value="auto">Auto</button>
+                                               <button class="button" value="metadata">Metadata</button>
+                                               <button class="button active" value="none">None</button>
+                                       </div>
+                               </div>
+
+                               <label class="setting checkbox-setting">
+                                       <input type="checkbox" data-setting="autoplay" />
+                                       <span>Autoplay</span>
+                               </label>
+
+                               <label class="setting checkbox-setting">
+                                       <input type="checkbox" data-setting="loop" />
+                                       <span>Loop</span>
+                               </label>
+                       </div>
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-video-details">
+               <# var ext, html5types = {
+                       mp4: wp.media.view.settings.embedMimes.mp4,
+                       ogv: wp.media.view.settings.embedMimes.ogv,
+                       webm: wp.media.view.settings.embedMimes.webm
+               }; #>
+
+                               <div class="media-embed media-embed-details">
+                       <div class="embed-media-settings embed-video-settings">
+                               <div class="wp-video-holder">
+                               <#
+                               var w = ! data.model.width || data.model.width > 640 ? 640 : data.model.width,
+                                       h = ! data.model.height ? 360 : data.model.height;
+
+                               if ( data.model.width && w !== data.model.width ) {
+                                       h = Math.ceil( ( h * w ) / data.model.width );
+                               }
+                               #>
+
+                               <#  var w_rule = '', classes = [],
+               w, h, settings = wp.media.view.settings,
+               isYouTube = isVimeo = false;
+
+       if ( ! _.isEmpty( data.model.src ) ) {
+               isYouTube = data.model.src.match(/youtube|youtu\.be/);
+               isVimeo = -1 !== data.model.src.indexOf('vimeo');
+       }
+
+       if ( settings.contentWidth && data.model.width >= settings.contentWidth ) {
+               w = settings.contentWidth;
+       } else {
+               w = data.model.width;
+       }
+
+       if ( w !== data.model.width ) {
+               h = Math.ceil( ( data.model.height * w ) / data.model.width );
+       } else {
+               h = data.model.height;
+       }
+
+       if ( w ) {
+               w_rule = 'width: ' + w + 'px; ';
+       }
+
+       if ( isYouTube ) {
+               classes.push( 'youtube-video' );
+       }
+
+       if ( isVimeo ) {
+               classes.push( 'vimeo-video' );
+       }
+
+#>
+<div style="{{ w_rule }}" class="wp-video">
+<video controls
+       class="wp-video-shortcode {{ classes.join( ' ' ) }}"
+       <# if ( w ) { #>width="{{ w }}"<# } #>
+       <# if ( h ) { #>height="{{ h }}"<# } #>
+       <#
+               if ( ! _.isUndefined( data.model.poster ) && data.model.poster ) {
+                       #> poster="{{ data.model.poster }}"<#
+               } #>
+               preload="{{ _.isUndefined( data.model.preload ) ? 'metadata' : data.model.preload }}"<#
+        if ( ! _.isUndefined( data.model.autoplay ) && data.model.autoplay ) {
+               #> autoplay<#
+       }
+        if ( ! _.isUndefined( data.model.loop ) && data.model.loop ) {
+               #> loop<#
+       }
+       #>
+>
+       <# if ( ! _.isEmpty( data.model.src ) ) {
+               if ( isYouTube ) { #>
+               <source src="{{ data.model.src }}" type="video/youtube" />
+               <# } else if ( isVimeo ) { #>
+               <source src="{{ data.model.src }}" type="video/vimeo" />
+               <# } else { #>
+               <source src="{{ data.model.src }}" type="{{ settings.embedMimes[ data.model.src.split('.').pop() ] }}" />
+               <# }
+       } #>
+
+       <# if ( data.model.mp4 ) { #>
+       <source src="{{ data.model.mp4 }}" type="{{ settings.embedMimes[ 'mp4' ] }}" />
+       <# } #>
+       <# if ( data.model.m4v ) { #>
+       <source src="{{ data.model.m4v }}" type="{{ settings.embedMimes[ 'm4v' ] }}" />
+       <# } #>
+       <# if ( data.model.webm ) { #>
+       <source src="{{ data.model.webm }}" type="{{ settings.embedMimes[ 'webm' ] }}" />
+       <# } #>
+       <# if ( data.model.ogv ) { #>
+       <source src="{{ data.model.ogv }}" type="{{ settings.embedMimes[ 'ogv' ] }}" />
+       <# } #>
+       <# if ( data.model.wmv ) { #>
+       <source src="{{ data.model.wmv }}" type="{{ settings.embedMimes[ 'wmv' ] }}" />
+       <# } #>
+       <# if ( data.model.flv ) { #>
+       <source src="{{ data.model.flv }}" type="{{ settings.embedMimes[ 'flv' ] }}" />
+       <# } #>
+               {{{ data.model.content }}}
+</video>
+</div>
+
+                               <# if ( ! _.isEmpty( data.model.src ) ) {
+                                       ext = data.model.src.split('.').pop();
+                                       if ( html5types[ ext ] ) {
+                                               delete html5types[ ext ];
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>SRC</span>
+                                       <input type="text" disabled="disabled" data-setting="src" value="{{ data.model.src }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.mp4 ) ) {
+                                       if ( ! _.isUndefined( html5types.mp4 ) ) {
+                                               delete html5types.mp4;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>MP4</span>
+                                       <input type="text" disabled="disabled" data-setting="mp4" value="{{ data.model.mp4 }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.m4v ) ) {
+                                       if ( ! _.isUndefined( html5types.m4v ) ) {
+                                               delete html5types.m4v;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>M4V</span>
+                                       <input type="text" disabled="disabled" data-setting="m4v" value="{{ data.model.m4v }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.webm ) ) {
+                                       if ( ! _.isUndefined( html5types.webm ) ) {
+                                               delete html5types.webm;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>WEBM</span>
+                                       <input type="text" disabled="disabled" data-setting="webm" value="{{ data.model.webm }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.ogv ) ) {
+                                       if ( ! _.isUndefined( html5types.ogv ) ) {
+                                               delete html5types.ogv;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>OGV</span>
+                                       <input type="text" disabled="disabled" data-setting="ogv" value="{{ data.model.ogv }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.wmv ) ) {
+                                       if ( ! _.isUndefined( html5types.wmv ) ) {
+                                               delete html5types.wmv;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>WMV</span>
+                                       <input type="text" disabled="disabled" data-setting="wmv" value="{{ data.model.wmv }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                               <# if ( ! _.isEmpty( data.model.flv ) ) {
+                                       if ( ! _.isUndefined( html5types.flv ) ) {
+                                               delete html5types.flv;
+                                       }
+                               #>
+                               <label class="setting">
+                                       <span>FLV</span>
+                                       <input type="text" disabled="disabled" data-setting="flv" value="{{ data.model.flv }}" />
+                                       <button type="button" class="button-link remove-setting">Remove video source</button>
+                               </label>
+                               <# } #>
+                                                               </div>
+
+                               <# if ( ! _.isEmpty( html5types ) ) { #>
+                               <div class="setting">
+                                       <span>Add alternate sources for maximum HTML5 playback:</span>
+                                       <div class="button-large">
+                                       <# _.each( html5types, function (mime, type) { #>
+                                       <button class="button add-media-source" data-mime="{{ mime }}">{{ type }}</button>
+                                       <# } ) #>
+                                       </div>
+                               </div>
+                               <# } #>
+
+                               <# if ( ! _.isEmpty( data.model.poster ) ) { #>
+                               <label class="setting">
+                                       <span>Poster Image</span>
+                                       <input type="text" disabled="disabled" data-setting="poster" value="{{ data.model.poster }}" />
+                                       <button type="button" class="button-link remove-setting">Remove poster image</button>
+                               </label>
+                               <# } #>
+                               <div class="setting preload">
+                                       <span>Preload</span>
+                                       <div class="button-group button-large" data-setting="preload">
+                                               <button class="button" value="auto">Auto</button>
+                                               <button class="button" value="metadata">Metadata</button>
+                                               <button class="button active" value="none">None</button>
+                                       </div>
+                               </div>
+
+                               <label class="setting checkbox-setting">
+                                       <input type="checkbox" data-setting="autoplay" />
+                                       <span>Autoplay</span>
+                               </label>
+
+                               <label class="setting checkbox-setting">
+                                       <input type="checkbox" data-setting="loop" />
+                                       <span>Loop</span>
+                               </label>
+
+                               <label class="setting" data-setting="content">
+                                       <span>Tracks (subtitles, captions, descriptions, chapters, or metadata)</span>
+                                       <#
+                                       var content = '';
+                                       if ( ! _.isEmpty( data.model.content ) ) {
+                                               var tracks = jQuery( data.model.content ).filter( 'track' );
+                                               _.each( tracks.toArray(), function (track) {
+                                                       content += track.outerHTML; #>
+                                               <p>
+                                                       <input class="content-track" type="text" value="{{ track.outerHTML }}" />
+                                                       <button type="button" class="button-link remove-setting remove-track">Remove video track</button>
+                                               </p>
+                                               <# } ); #>
+                                       <# } else { #>
+                                       <em>There are no associated subtitles.</em>
+                                       <# } #>
+                                       <textarea class="hidden content-setting">{{ content }}</textarea>
+                               </label>
+                       </div>
+               </div>
+       </script>
+
+       <script type="text/html" id="tmpl-editor-gallery">
+               <# if ( data.attachments.length ) { #>
+                       <div class="gallery gallery-columns-{{ data.columns }}">
+                               <# _.each( data.attachments, function( attachment, index ) { #>
+                                       <dl class="gallery-item">
+                                               <dt class="gallery-icon">
+                                                       <# if ( attachment.thumbnail ) { #>
+                                                               <img src="{{ attachment.thumbnail.url }}" width="{{ attachment.thumbnail.width }}" height="{{ attachment.thumbnail.height }}" alt="" />
+                                                       <# } else { #>
+                                                               <img src="{{ attachment.url }}" alt="" />
+                                                       <# } #>
+                                               </dt>
+                                               <# if ( attachment.caption ) { #>
+                                                       <dd class="wp-caption-text gallery-caption">
+                                                               {{{ data.verifyHTML( attachment.caption ) }}}
+                                                       </dd>
+                                               <# } #>
+                                       </dl>
+                                       <# if ( index % data.columns === data.columns - 1 ) { #>
+                                               <br style="clear: both;">
+                                       <# } #>
+                               <# } ); #>
+                       </div>
+               <# } else { #>
+                       <div class="wpview-error">
+                               <div class="dashicons dashicons-format-gallery"></div><p>No items found.</p>
+                       </div>
+               <# } #>
+       </script>
+
+       <script type="text/html" id="tmpl-crop-content">
+               <img class="crop-image" src="{{ data.url }}" alt="Image crop area preview. Requires mouse interaction.">
+               <div class="upload-errors"></div>
+       </script>
+
+       <script type="text/html" id="tmpl-site-icon-preview">
+               <h2>Preview</h2>
+               <strong aria-hidden="true">As a browser icon</strong>
+               <div class="favicon-preview">
+                       <img src="http://src.wordpress-develop.dev/wp-admin/images/browser.png" class="browser-preview" width="182" height="" alt="" />
+
+                       <div class="favicon">
+                               <img id="preview-favicon" src="{{ data.url }}" alt="Preview as a browser icon"/>
+                       </div>
+                       <span class="browser-title" aria-hidden="true">WordPress Develop</span>
+               </div>
+
+               <strong aria-hidden="true">As an app icon</strong>
+               <div class="app-icon-preview">
+                       <img id="preview-app-icon" src="{{ data.url }}" alt="Preview as an app icon"/>
+               </div>
+       </script>
</ins><span class="cx" style="display: block; padding: 0 10px">                 </div><!-- end widget templates -->
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../src/wp-includes/js/tinymce/tinymce.js"></script>
</span><span class="cx" style="display: block; padding: 0 10px">                <script src="../../src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js"></script>
</span></span></pre></div>
<a id="trunktestsqunitwpadminjswidgetstestmediaimagewidgetjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js                          (rev 0)
+++ trunk/tests/qunit/wp-admin/js/widgets/test-media-image-widget.js    2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,113 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* globals wp */
+/* jshint qunit: true */
+/* eslint-env qunit */
+/* eslint-disable no-magic-numbers */
+
+( function() {
+       'use strict';
+
+       module( 'Image Media Widget' );
+
+       test( 'image widget control', function() {
+               var ImageWidgetControl, imageWidgetControlInstance, imageWidgetModelInstance, mappedProps, testImageUrl, templateProps;
+               testImageUrl = 'http://s.w.org/style/images/wp-header-logo.png';
+               equal( typeof wp.mediaWidgets.controlConstructors.media_image, 'function', 'wp.mediaWidgets.controlConstructors.media_image is a function' );
+               ImageWidgetControl = wp.mediaWidgets.controlConstructors.media_image;
+               ok( ImageWidgetControl.prototype instanceof wp.mediaWidgets.MediaWidgetControl, 'wp.mediaWidgets.controlConstructors.media_image subclasses wp.mediaWidgets.MediaWidgetControl' );
+
+               imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image();
+               imageWidgetControlInstance = new ImageWidgetControl({
+                       model: imageWidgetModelInstance
+               });
+
+               // Test mapModelToPreviewTemplateProps() when no data is set.
+               templateProps = imageWidgetControlInstance.mapModelToPreviewTemplateProps();
+               equal( templateProps.caption, undefined, 'mapModelToPreviewTemplateProps should not return attributes that are should_preview_update false' );
+               equal( templateProps.attachment_id, 0, 'mapModelToPreviewTemplateProps should return default values' );
+               equal( templateProps.currentFilename, '', 'mapModelToPreviewTemplateProps should return a currentFilename' );
+
+               // Test mapModelToPreviewTemplateProps() when data is set on model.
+               imageWidgetControlInstance.model.set( { url: testImageUrl, alt: 'some alt text', link_type: 'none' } );
+               templateProps = imageWidgetControlInstance.mapModelToPreviewTemplateProps();
+               equal( templateProps.currentFilename, 'wp-header-logo.png', 'mapModelToPreviewTemplateProps should set currentFilename based off of url' );
+               equal( templateProps.url, testImageUrl, 'mapModelToPreviewTemplateProps should return the proper url' );
+               equal( templateProps.alt, 'some alt text', 'mapModelToPreviewTemplateProps should return the proper alt text' );
+               equal( templateProps.link_type, undefined, 'mapModelToPreviewTemplateProps should ignore attributes that are not needed in the preview' );
+               equal( templateProps.error, false, 'mapModelToPreviewTemplateProps should return error state' );
+
+               // Test mapModelToPreviewTemplateProps() when error is set on model.
+               imageWidgetControlInstance.model.set( 'error', 'missing_attachment' );
+               templateProps = imageWidgetControlInstance.mapModelToPreviewTemplateProps();
+               equal( templateProps.error, 'missing_attachment', 'mapModelToPreviewTemplateProps should return error string' );
+
+               // Reset model.
+               imageWidgetControlInstance.model.set({ error: false, attachment_id: 0, url: null });
+
+               // Test isSelected().
+               equal( imageWidgetControlInstance.isSelected(), false, 'media_image.isSelected() should return false when no media is selected' );
+               imageWidgetControlInstance.model.set({ error: 'missing_attachment', attachment_id: 777 });
+               equal( imageWidgetControlInstance.isSelected(), false, 'media_image.isSelected() should return false when media is selected and error is set' );
+               imageWidgetControlInstance.model.set({ error: false, attachment_id: 777 });
+               equal( imageWidgetControlInstance.isSelected(), true, 'media_image.isSelected() should return true when media is selected and no error exists' );
+               imageWidgetControlInstance.model.set({ error: false, attachment_id: 0, url: testImageUrl });
+               equal( imageWidgetControlInstance.isSelected(), true, 'media_image.isSelected() should return true when url is set and no error exists' );
+
+               // Reset model.
+               imageWidgetControlInstance.model.set({ error: false, attachment_id: 0, url: null });
+
+               // Test editing of widget title.
+               imageWidgetControlInstance.render();
+               imageWidgetControlInstance.$el.find( '.title' ).val( 'Chicken and Ribs' ).trigger( 'input' );
+               equal( imageWidgetModelInstance.get( 'title' ), 'Chicken and Ribs', 'Changing title should update model title attribute' );
+
+               // Test mapMediaToModelProps
+               mappedProps = imageWidgetControlInstance.mapMediaToModelProps( { link: 'file', url: testImageUrl } );
+               equal( mappedProps.link_url, testImageUrl, 'mapMediaToModelProps should set file link_url according to mediaFrameProps.link' );
+               mappedProps = imageWidgetControlInstance.mapMediaToModelProps( { link: 'post', postUrl: 'https://wordpress.org/image-2/' } );
+               equal( mappedProps.link_url, 'https://wordpress.org/image-2/', 'mapMediaToModelProps should set file link_url according to mediaFrameProps.link' );
+               mappedProps = imageWidgetControlInstance.mapMediaToModelProps( { link: 'custom', linkUrl: 'https://wordpress.org' } );
+               equal( mappedProps.link_url, 'https://wordpress.org', 'mapMediaToModelProps should set custom link_url according to mediaFrameProps.linkUrl' );
+
+               // Test mapModelToMediaFrameProps().
+               imageWidgetControlInstance.model.set({ error: false, url: testImageUrl, 'link_type': 'custom', 'link_url': 'https://wordpress.org', 'size': 'custom', 'width': 100, 'height': 150, 'title': 'widget title', 'image_title': 'title of image' });
+               mappedProps = imageWidgetControlInstance.mapModelToMediaFrameProps( imageWidgetControlInstance.model.toJSON() );
+               equal( mappedProps.linkUrl, 'https://wordpress.org', 'mapModelToMediaFrameProps should set linkUrl from model.link_url' );
+               equal( mappedProps.link, 'custom', 'mapModelToMediaFrameProps should set link from model.link_type' );
+               equal( mappedProps.width, 100, 'mapModelToMediaFrameProps should set width when model.size is custom' );
+               equal( mappedProps.height, 150, 'mapModelToMediaFrameProps should set height when model.size is custom' );
+               equal( mappedProps.title, 'title of image', 'mapModelToMediaFrameProps should set title from model.image_title' );
+       });
+
+       test( 'image widget control renderPreview', function( assert ) {
+               var imageWidgetControlInstance, imageWidgetModelInstance, done;
+               done = assert.async();
+
+               imageWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_image();
+               imageWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_image({
+                       model: imageWidgetModelInstance
+               });
+               equal( imageWidgetControlInstance.$el.find( 'img' ).length, 0, 'No images should be rendered' );
+               imageWidgetControlInstance.model.set({ error: false, url: 'http://s.w.org/style/images/wp-header-logo.png' });
+
+               // Due to renderPreview being deferred.
+               setTimeout( function() {
+                       equal( imageWidgetControlInstance.$el.find( 'img[src="http://s.w.org/style/images/wp-header-logo.png"]' ).length, 1, 'One image should be rendered' );
+                       done();
+               }, 50 );
+
+               start();
+       });
+
+       test( 'image media model', function() {
+               var ImageWidgetModel, imageWidgetModelInstance;
+               equal( typeof wp.mediaWidgets.modelConstructors.media_image, 'function', 'wp.mediaWidgets.modelConstructors.media_image is a function' );
+               ImageWidgetModel = wp.mediaWidgets.modelConstructors.media_image;
+               ok( ImageWidgetModel.prototype instanceof wp.mediaWidgets.MediaWidgetModel, 'wp.mediaWidgets.modelConstructors.media_image subclasses wp.mediaWidgets.MediaWidgetModel' );
+
+               imageWidgetModelInstance = new ImageWidgetModel();
+               _.each( imageWidgetModelInstance.attributes, function( value, key ) {
+                       equal( value, ImageWidgetModel.prototype.schema[ key ][ 'default' ], 'Should properly set default for ' + key );
+               });
+       });
+
+})();
</ins></span></pre></div>
<a id="trunktestsqunitwpadminjswidgetstestmediavideowidgetjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js                          (rev 0)
+++ trunk/tests/qunit/wp-admin/js/widgets/test-media-video-widget.js    2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,68 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* globals wp */
+/* jshint qunit: true */
+/* eslint-env qunit */
+/* eslint-disable no-magic-numbers */
+
+( function() {
+       'use strict';
+
+       module( 'Video Media Widget' );
+
+       test( 'video widget control', function() {
+               var VideoWidgetControl, videoWidgetControlInstance, videoWidgetModelInstance, mappedProps, testVideoUrl;
+               testVideoUrl = 'https://videos.files.wordpress.com/AHz0Ca46/wp4-7-vaughan-r8-mastered_hd.mp4';
+               equal( typeof wp.mediaWidgets.controlConstructors.media_video, 'function', 'wp.mediaWidgets.controlConstructors.media_video is a function' );
+               VideoWidgetControl = wp.mediaWidgets.controlConstructors.media_video;
+               ok( VideoWidgetControl.prototype instanceof wp.mediaWidgets.MediaWidgetControl, 'wp.mediaWidgets.controlConstructors.media_video subclasses wp.mediaWidgets.MediaWidgetControl' );
+
+               videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video();
+               videoWidgetControlInstance = new VideoWidgetControl({
+                       model: videoWidgetModelInstance
+               });
+
+               // Test mapModelToMediaFrameProps().
+               videoWidgetControlInstance.model.set({ error: false, url: testVideoUrl, loop: false, preload: 'meta' });
+               mappedProps = videoWidgetControlInstance.mapModelToMediaFrameProps( videoWidgetControlInstance.model.toJSON() );
+               equal( mappedProps.url, testVideoUrl, 'mapModelToMediaFrameProps should set url' );
+               equal( mappedProps.loop, false, 'mapModelToMediaFrameProps should set loop' );
+               equal( mappedProps.preload, 'meta', 'mapModelToMediaFrameProps should set preload' );
+
+               // Test mapMediaToModelProps().
+               mappedProps = videoWidgetControlInstance.mapMediaToModelProps( { loop: false, preload: 'meta', url: testVideoUrl, title: 'random movie file title' } );
+               equal( mappedProps.title, undefined, 'mapMediaToModelProps should ignore title inputs' );
+               equal( mappedProps.loop, false, 'mapMediaToModelProps should set loop' );
+               equal( mappedProps.preload, 'meta', 'mapMediaToModelProps should set preload' );
+       });
+
+       test( 'video widget control renderPreview', function( assert ) {
+               var videoWidgetControlInstance, videoWidgetModelInstance, done;
+               done = assert.async();
+
+               videoWidgetModelInstance = new wp.mediaWidgets.modelConstructors.media_video();
+               videoWidgetControlInstance = new wp.mediaWidgets.controlConstructors.media_video({
+                       model: videoWidgetModelInstance
+               });
+               equal( videoWidgetControlInstance.$el.find( 'a' ).length, 0, 'No video links should be rendered' );
+               videoWidgetControlInstance.model.set({ error: false, url: 'https://videos.files.wordpress.com/AHz0Ca46/wp4-7-vaughan-r8-mastered_hd.mp4' });
+
+               // Due to renderPreview being deferred.
+               setTimeout( function() {
+                       equal( videoWidgetControlInstance.$el.find( 'a[href="https://videos.files.wordpress.com/AHz0Ca46/wp4-7-vaughan-r8-mastered_hd.mp4"]' ).length, 1, 'One video link should be rendered' );
+                       done();
+               }, 50 );
+               start();
+       });
+
+       test( 'video media model', function() {
+               var VideoWidgetModel, videoWidgetModelInstance;
+               equal( typeof wp.mediaWidgets.modelConstructors.media_video, 'function', 'wp.mediaWidgets.modelConstructors.media_video is a function' );
+               VideoWidgetModel = wp.mediaWidgets.modelConstructors.media_video;
+               ok( VideoWidgetModel.prototype instanceof wp.mediaWidgets.MediaWidgetModel, 'wp.mediaWidgets.modelConstructors.media_video subclasses wp.mediaWidgets.MediaWidgetModel' );
+
+               videoWidgetModelInstance = new VideoWidgetModel();
+               _.each( videoWidgetModelInstance.attributes, function( value, key ) {
+                       equal( value, VideoWidgetModel.prototype.schema[ key ][ 'default' ], 'Should properly set default for ' + key );
+               });
+       });
+
+})();
</ins></span></pre></div>
<a id="trunktestsqunitwpadminjswidgetstestmediawidgetsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/qunit/wp-admin/js/widgets/test-media-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/widgets/test-media-widgets.js                               (rev 0)
+++ trunk/tests/qunit/wp-admin/js/widgets/test-media-widgets.js 2017-05-11 21:10:54 UTC (rev 40640)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,45 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* globals wp, Backbone */
+/* jshint qunit: true */
+/* eslint-env qunit */
+
+(function() {
+       'use strict';
+
+       module( 'Media Widgets' );
+
+       test( 'namespace', function() {
+               equal( typeof wp.mediaWidgets, 'object', 'wp.mediaWidgets is an object' );
+               equal( typeof wp.mediaWidgets.controlConstructors, 'object', 'wp.mediaWidgets.controlConstructors is an object' );
+               equal( typeof wp.mediaWidgets.modelConstructors, 'object', 'wp.mediaWidgets.modelConstructors is an object' );
+               equal( typeof wp.mediaWidgets.widgetControls, 'object', 'wp.mediaWidgets.widgetControls is an object' );
+               equal( typeof wp.mediaWidgets.handleWidgetAdded, 'function', 'wp.mediaWidgets.handleWidgetAdded is an function' );
+               equal( typeof wp.mediaWidgets.handleWidgetUpdated, 'function', 'wp.mediaWidgets.handleWidgetUpdated is an function' );
+               equal( typeof wp.mediaWidgets.init, 'function', 'wp.mediaWidgets.init is an function' );
+       });
+
+       test( 'media widget control', function() {
+               equal( typeof wp.mediaWidgets.MediaWidgetControl, 'function', 'wp.mediaWidgets.MediaWidgetControl' );
+               ok( wp.mediaWidgets.MediaWidgetControl.prototype instanceof Backbone.View, 'wp.mediaWidgets.MediaWidgetControl subclasses Backbone.View' );
+       });
+
+       test( 'media widget model', function() {
+               var widgetModelInstance;
+               equal( typeof wp.mediaWidgets.MediaWidgetModel, 'function', 'wp.mediaWidgets.MediaWidgetModel is a function' );
+               ok( wp.mediaWidgets.MediaWidgetModel.prototype instanceof Backbone.Model, 'wp.mediaWidgets.MediaWidgetModel subclasses Backbone.Model' );
+
+               widgetModelInstance = new wp.mediaWidgets.MediaWidgetModel();
+               equal( widgetModelInstance.get( 'title' ), '', 'wp.mediaWidgets.MediaWidgetModel defaults title to empty string' );
+               equal( widgetModelInstance.get( 'attachment_id' ), 0, 'wp.mediaWidgets.MediaWidgetModel defaults attachment_id to 0' );
+               equal( widgetModelInstance.get( 'url' ), 0, 'wp.mediaWidgets.MediaWidgetModel defaults url to empty string' );
+
+               widgetModelInstance.set({
+                       title: 'chicken and ribs',
+                       attachment_id: '1',
+                       url: 'https://wordpress.org'
+               });
+               equal( widgetModelInstance.get( 'title' ), 'chicken and ribs', 'wp.mediaWidgets.MediaWidgetModel properly sets the title attribute' );
+               equal( widgetModelInstance.get( 'url' ), 'https://wordpress.org', 'wp.mediaWidgets.MediaWidgetModel properly sets the url attribute' );
+               equal( widgetModelInstance.get( 'attachment_id' ), 1, 'wp.mediaWidgets.MediaWidgetModel properly sets and casts the attachment_id attribute' );
+       });
+
+})();
</ins></span></pre>
</div>
</div>

</body>
</html>