* @output wp-admin/js/widgets/media-widgets.js
/* eslint consistent-this: [ "error", "control" ] */
* @namespace wp.mediaWidgets
wp.mediaWidgets = ( function( $ ) {
* 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.
* @memberOf wp.mediaWidgets
* @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.
* @memberOf wp.mediaWidgets
* @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
component.modelConstructors = {};
component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend(/** @lends wp.mediaWidgets.PersistentDisplaySettingsLibrary.prototype */{
* Library which persists the customized display settings across selections.
* @constructs wp.mediaWidgets.PersistentDisplaySettingsLibrary
* @augments wp.media.controller.Library
* @param {Object} options - Options.
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.
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
* @param {Backbone.Model} model - Display settings model.
* @return {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 );
* Extended view for managing the embed UI.
* @class wp.mediaWidgets.MediaEmbedView
* @augments wp.media.view.Embed
component.MediaEmbedView = wp.media.view.Embed.extend(/** @lends wp.mediaWidgets.MediaEmbedView.prototype */{
* @param {Object} options - Options.
initialize: function( options ) {
var view = this, embedController; // eslint-disable-line consistent-this
wp.media.view.Embed.prototype.initialize.call( view, options );
if ( 'image' !== view.controller.options.mimeType ) {
embedController = view.controller.states.get( 'embed' );
embedController.off( 'scan', embedController.scanImage, embedController );
* Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field.
refresh: function refresh() {
* @class wp.mediaWidgets~Constructor
if ( 'image' === this.controller.options.mimeType ) {
Constructor = wp.media.view.EmbedImage;
// This should be eliminated once #40450 lands of when this is merged into core.
Constructor = wp.media.view.EmbedLink.extend(/** @lends wp.mediaWidgets~Constructor.prototype */{
* Set the disabled state on the Add to Widget button.
* @param {boolean} disabled - Disabled.
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.
setErrorNotice: function setErrorNotice( notice ) {
var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this
noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' );
if ( noticeContainer.length ) {
noticeContainer.slideUp( 'fast' );
if ( ! noticeContainer.length ) {
noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt"></div>' );
embedLinkView.views.parent.$el.prepend( noticeContainer );
noticeContainer.append( $( '<p>', {
noticeContainer.slideDown( 'fast' );
updateoEmbed: function() {
var embedLinkView = this, url; // eslint-disable-line consistent-this
url = embedLinkView.model.get( 'url' );
// Abort if the URL field was emptied out.
embedLinkView.setErrorNotice( '' );
embedLinkView.setAddToWidgetButtonDisabled( true );
if ( ! url.match( /^(http|https):\/\/.+\// ) ) {
embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
embedLinkView.setAddToWidgetButtonDisabled( true );
wp.media.view.EmbedLink.prototype.updateoEmbed.call( embedLinkView );
var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser, url, re, youTubeEmbedMatch; // eslint-disable-line consistent-this
url = embedLinkView.model.get( 'url' );
if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) {
embedLinkView.dfd.abort();
fetchSuccess = function( response ) {
embedLinkView.renderoEmbed({
embedLinkView.controller.$el.find( '#embed-url-field' ).removeClass( 'invalid' );
embedLinkView.setErrorNotice( '' );
embedLinkView.setAddToWidgetButtonDisabled( false );
urlParser = document.createElement( 'a' );
matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ );
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();
fetchSuccess( '<!--success-->' );
// Support YouTube embed links.
re = /https?:\/\/www\.youtube\.com\/embed\/([^/]+)/;
youTubeEmbedMatch = re.exec( url );
if ( youTubeEmbedMatch ) {
url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ];
// silently change url to proper oembed-able version.
embedLinkView.model.attributes.url = url;
embedLinkView.dfd = wp.apiRequest({
url: wp.media.view.settings.oEmbedProxyUrl,
maxwidth: embedLinkView.model.get( 'width' ),
maxheight: embedLinkView.model.get( 'height' ),
embedLinkView.dfd.done( function( response ) {
if ( embedLinkView.controller.options.mimeType !== response.type ) {
embedLinkView.renderFail();
fetchSuccess( response.html );
embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) );
* 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.
renderFail: function renderFail() {
var embedLinkView = this; // eslint-disable-line consistent-this
embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' );
embedLinkView.setAddToWidgetButtonDisabled( true );
this.settings( new Constructor({
controller: this.controller,
* Custom media frame for selecting uploaded media or providing media by URL.
* @class wp.mediaWidgets.MediaFrameSelect
* @augments wp.media.view.MediaFrame.Post
component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend(/** @lends wp.mediaWidgets.MediaFrameSelect.prototype */{
* Create the default states.
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 ) {
new component.PersistentDisplaySettingsLibrary({
title: this.options.title,
selection: this.options.selection,
library: wp.media.query({
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 }),
new wp.media.controller.Embed({
metadata: this.options.metadata,
type: 'image' === this.options.mimeType ? 'image' : 'link',
invalidEmbedTypeError: this.options.invalidEmbedTypeError
* Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text.
* @param {wp.Backbone.View} view - Toolbar view.
* @this {wp.media.controller.Library}
mainInsertToolbar: function mainInsertToolbar( view ) {
var controller = this; // eslint-disable-line consistent-this
text: controller.options.text, // The whole reason for the fork.
requires: { selection: true },
* @fires wp.media.controller.State#insert()
click: function onClick() {
var state = controller.state(),
selection = state.get( 'selection' );
state.trigger( 'insert', selection ).reset();
* Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text.
* @param {wp.Backbone.View} toolbar - Toolbar view.
* @this {wp.media.controller.Library}
mainEmbedToolbar: function mainEmbedToolbar( toolbar ) {
toolbar.view = new wp.media.view.Toolbar.Embed({
* Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field.
embedContent: function embedContent() {
var view = new component.MediaEmbedView({
this.content.set( view );
component.MediaWidgetControl = Backbone.View.extend(/** @lends wp.mediaWidgets.MediaWidgetControl.prototype */{
* 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().
add_to_widget: '{{add_to_widget}}',
add_media: '{{add_media}}'
* 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.
* 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().
'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
'click .select-media': 'selectMedia',
'click .placeholder': 'selectMedia',
'click .edit-media': 'editMedia'
showDisplaySettings: true,
* @constructs wp.mediaWidgets.MediaWidgetControl
* @augments Backbone.View
* @param {Object} options - Options.
* @param {Backbone.Model} options.model - Model.
* @param {jQuery} options.el - Control field container element.
* @param {jQuery} options.syncContainer - Container element where fields are synced for the server.
initialize: function initialize( options ) {
Backbone.View.prototype.initialize.call( control, options );
if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
throw new Error( 'Missing options.model' );