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 #32417, #39993, #39994, #39995.

Built from https://develop.svn.wordpress.org/trunk@40640


git-svn-id: http://core.svn.wordpress.org/trunk@40501 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter 2017-05-11 21:11:44 +00:00
parent 763f09fb7e
commit 31d4d81039
24 changed files with 3122 additions and 22 deletions

View File

@ -55,6 +55,110 @@
color: #a0a5aa;
}
/* 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: right;
margin-bottom: 10px;
}
.media-widget-control .media-widget-buttons .button {
margin-right: 8px;
width: auto;
}
.media-widget-control:not(.selected) .media-widget-buttons .button,
.media-widget-buttons .button:first-child {
margin-right: 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 0 0 3px;
}
.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;
}
/* Widget Dragging Helpers */
.widget.ui-draggable-dragging {
min-width: 100%;

File diff suppressed because one or more lines are too long

View File

@ -55,6 +55,110 @@
color: #a0a5aa;
}
/* 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;
}
/* Widget Dragging Helpers */
.widget.ui-draggable-dragging {
min-width: 100%;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,150 @@
/* 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 );

View File

@ -0,0 +1 @@
!function(a){"use strict";var b,c,d;d=wp.media.view.MediaFrame.AudioDetails.extend({createStates:function(){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:!1})])}}),b=a.MediaWidgetModel.extend({}),c=a.MediaWidgetControl.extend({showDisplaySettings:!1,mapModelToMediaFrameProps:function(b){var c,d=this;return c=a.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call(d,b),c.link="embed",c},renderPreview:function(){var a,b,c,d,e=this;c=e.model.get("attachment_id"),d=e.model.get("url"),(c||d)&&(a=e.$el.find(".media-widget-preview"),b=wp.template("wp-media-widget-audio-preview"),a.html(b({model:{attachment_id:e.model.get("attachment_id"),src:d},error:e.model.get("error")})),wp.mediaelement.initialize())},editMedia:function(){var a,b,c,e=this;b=e.mapModelToMediaFrameProps(e.model.toJSON()),a=new d({frame:"audio",state:"audio-details",metadata:b}),wp.media.frame=a,a.$el.addClass("media-widget"),c=function(a){e.selectedAttachment.set(a),e.model.set(_.extend(e.model.defaults(),e.mapMediaToModelProps(a),{error:!1}))},a.state("audio-details").on("update",c),a.state("replace-audio").on("replace",c),a.on("close",function(){a.detach()}),a.open()}}),a.controlConstructors.media_audio=c,a.modelConstructors.media_audio=b}(wp.mediaWidgets);

View File

@ -0,0 +1,129 @@
/* 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 );

View File

@ -0,0 +1 @@
!function(a,b){"use strict";var c,d;c=a.MediaWidgetModel.extend({}),d=a.MediaWidgetControl.extend({renderPreview:function(){var a,b,c=this;(c.model.get("attachment_id")||c.model.get("url"))&&(a=c.$el.find(".media-widget-preview"),b=wp.template("wp-media-widget-image-preview"),a.html(b(_.extend(c.previewTemplateProps.toJSON()))))},editMedia:function(){var a,c,d,e,f=this;e=f.mapModelToMediaFrameProps(f.model.toJSON()),"none"===e.link&&(e.linkUrl=""),a=wp.media({frame:"image",state:"image-details",metadata:e}),a.$el.addClass("media-widget"),c=function(){var b;b=a.state().attributes.image.toJSON(),f.selectedAttachment.set(b),f.model.set(_.extend(f.mapMediaToModelProps(b),{error:!1}))},a.state("image-details").on("update",c),a.state("replace-image").on("replace",c),d=wp.media.model.Attachment.prototype.sync,wp.media.model.Attachment.prototype.sync=function(){return b.Deferred().rejectWith(this).promise()},a.on("close",function(){a.detach(),wp.media.model.Attachment.prototype.sync=d}),a.open()},getEmbedResetProps:function(){return _.extend(a.MediaWidgetControl.prototype.getEmbedResetProps.call(this),{size:"full",width:0,height:0})},mapModelToPreviewTemplateProps:function(){var b,c,d=this;return c=d.model.get("url"),b=a.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call(d),b.currentFilename=c?c.replace(/\?.*$/,"").replace(/^.+\//,""):"",b}}),a.controlConstructors.media_image=d,a.modelConstructors.media_image=c}(wp.mediaWidgets,jQuery);

View File

@ -0,0 +1,228 @@
/* 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 );

View File

@ -0,0 +1 @@
!function(a){"use strict";var b,c,d;d=wp.media.view.MediaFrame.VideoDetails.extend({createStates:function(){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:!1}),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"})])}}),b=a.MediaWidgetModel.extend({}),c=a.MediaWidgetControl.extend({showDisplaySettings:!1,oembedResponses:{},mapModelToMediaFrameProps:function(b){var c,d=this;return c=a.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call(d,b),c.link="embed",c},fetchEmbed:function(){var a,b=this;a=b.model.get("url"),b.oembedResponses[a]||(b.fetchEmbedDfd&&"pending"===b.fetchEmbedDfd.state()&&b.fetchEmbedDfd.abort(),b.fetchEmbedDfd=jQuery.ajax({url:"https://noembed.com/embed",data:{url:b.model.get("url"),maxwidth:b.model.get("width"),maxheight:b.model.get("height")},type:"GET",crossDomain:!0,dataType:"json"}),b.fetchEmbedDfd.done(function(c){b.oembedResponses[a]=c,b.renderPreview()}),b.fetchEmbedDfd.fail(function(){b.oembedResponses[a]=null}))},renderPreview:function(){var a,b,c,d,e,f,g,h,i=this,j=!1;c=i.model.get("attachment_id"),d=i.model.get("url"),h=i.model.get("error"),(c||d)&&(!c&&d&&(f=document.createElement("a"),f.href=d,j=/vimeo|youtu\.?be/.test(f.host)),j&&(i.fetchEmbed(),e=i.oembedResponses[d]?i.oembedResponses[d].thumbnail_url:null),g=i.selectedAttachment.get("mime"),g&&c&&(_.contains(_.values(wp.media.view.settings.embedMimes),g)||(h="unsupported_file_type")),a=i.$el.find(".media-widget-preview"),b=wp.template("wp-media-widget-video-preview"),a.html(b({model:{attachment_id:i.model.get("attachment_id"),src:d,poster:e},is_hosted_embed:j,error:h})))},editMedia:function(){var a,b,c,e=this;b=e.mapModelToMediaFrameProps(e.model.toJSON()),a=new d({frame:"video",state:"video-details",metadata:b}),wp.media.frame=a,a.$el.addClass("media-widget"),c=function(a){e.selectedAttachment.set(a),e.model.set(_.extend(_.omit(e.model.defaults(),"title"),e.mapMediaToModelProps(a),{error:!1}))},a.state("video-details").on("update",c),a.state("replace-video").on("replace",c),a.on("close",function(){a.detach()}),a.open()}}),a.controlConstructors.media_video=c,a.modelConstructors.media_video=b}(wp.mediaWidgets);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -841,6 +841,9 @@ img.aligncenter {
padding: 4px;
text-align: center;
}
.widget-container .wp-caption {
max-width: 100% !important;
}
.wp-caption img {
margin: 5px 5px 0;
max-width: 622px; /* caption width - 10px */

View File

@ -19,6 +19,21 @@ require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-search.php' );
/** WP_Widget_Archives class */
require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-archives.php' );
/** 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' );
/** WP_Widget_Meta class */
require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-meta.php' );
/** WP_Widget_Meta class */
require_once( ABSPATH . WPINC . '/widgets/class-wp-widget-meta.php' );

View File

@ -469,6 +469,15 @@ wp.customize.selectiveRefresh = ( function( $, api ) {
// Prevent placement container from being being re-triggered as being rendered among nested partials.
placement.container.data( 'customize-partial-content-rendered', true );
/*
* 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();
}
/**
* Announce when a partial's placement has been rendered so that dynamic elements can be re-built.
*/

File diff suppressed because one or more lines are too long

View File

@ -608,7 +608,7 @@ function wp_print_media_templates() {
<h2><?php _e( 'Attachment Display Settings' ); ?></h2>
<# if ( 'image' === data.type ) { #>
<label class="setting">
<label class="setting align">
<span><?php _e('Alignment'); ?></span>
<select class="alignment"
data-setting="align"
@ -1087,7 +1087,7 @@ function wp_print_media_templates() {
</div>
</div>
<label class="setting checkbox-setting">
<label class="setting checkbox-setting autoplay">
<input type="checkbox" data-setting="autoplay" />
<span><?php _e( 'Autoplay' ); ?></span>
</label>
@ -1176,7 +1176,7 @@ function wp_print_media_templates() {
</div>
</div>
<label class="setting checkbox-setting">
<label class="setting checkbox-setting autoplay">
<input type="checkbox" data-setting="autoplay" />
<span><?php _e( 'Autoplay' ); ?></span>
</label>

View File

@ -602,6 +602,12 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'admin-gallery', "/wp-admin/js/gallery$suffix.js", array( 'jquery-ui-sortable' ) );
$scripts->add( 'admin-widgets', "/wp-admin/js/widgets$suffix.js", array( 'jquery-ui-sortable', 'jquery-ui-draggable', 'jquery-ui-droppable' ), false, 1 );
$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' ) );
$scripts->add( 'text-widgets', "/wp-admin/js/widgets/text-widgets$suffix.js", array( 'jquery', 'backbone', 'editor', 'wp-util' ) );
$scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' );

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.8-alpha-40639';
$wp_version = '4.8-alpha-40640';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

View File

@ -1436,35 +1436,43 @@ function wp_widget_rss_process( $widget_rss, $check_feed = true ) {
* @since 2.2.0
*/
function wp_widgets_init() {
if ( !is_blog_installed() )
if ( ! is_blog_installed() ) {
return;
}
register_widget('WP_Widget_Pages');
register_widget( 'WP_Widget_Pages' );
register_widget('WP_Widget_Calendar');
register_widget( 'WP_Widget_Calendar' );
register_widget('WP_Widget_Archives');
register_widget( 'WP_Widget_Archives' );
if ( get_option( 'link_manager_enabled' ) )
register_widget('WP_Widget_Links');
if ( get_option( 'link_manager_enabled' ) ) {
register_widget( 'WP_Widget_Links' );
}
register_widget('WP_Widget_Meta');
register_widget( 'WP_Widget_Media_Audio' );
register_widget('WP_Widget_Search');
register_widget( 'WP_Widget_Media_Image' );
register_widget('WP_Widget_Text');
register_widget( 'WP_Widget_Media_Video' );
register_widget('WP_Widget_Categories');
register_widget( 'WP_Widget_Meta' );
register_widget('WP_Widget_Recent_Posts');
register_widget( 'WP_Widget_Search' );
register_widget('WP_Widget_Recent_Comments');
register_widget( 'WP_Widget_Text' );
register_widget('WP_Widget_RSS');
register_widget( 'WP_Widget_Categories' );
register_widget('WP_Widget_Tag_Cloud');
register_widget( 'WP_Widget_Recent_Posts' );
register_widget('WP_Nav_Menu_Widget');
register_widget( 'WP_Widget_Recent_Comments' );
register_widget( 'WP_Widget_RSS' );
register_widget( 'WP_Widget_Tag_Cloud' );
register_widget( 'WP_Nav_Menu_Widget' );
/**
* Fires after all default WordPress widgets have been registered.

View File

@ -0,0 +1,204 @@
<?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
}
}

View File

@ -0,0 +1,326 @@
<?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
}
}

View File

@ -0,0 +1,255 @@
<?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
}
}

View File

@ -0,0 +1,422 @@
<?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'];
}
}