: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Get default attribute values.
* @return {Object} Mapping of property names to their default values.
_.each( this.schema, function( fieldSchema, field ) {
defaults[ field ] = fieldSchema['default'];
* 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.
* @return {wp.mediaWidgets.MediaWidgetModel} This model.
set: function set( key, val, options ) {
var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
if ( 'object' === typeof key ) {
_.each( attrs, function( value, name ) {
if ( ! model.schema[ name ] ) {
castedAttrs[ name ] = value;
type = model.schema[ name ].type;
if ( 'array' === type ) {
castedAttrs[ name ] = value;
if ( ! _.isArray( castedAttrs[ name ] ) ) {
castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
castedAttrs[ name ] = _.filter(
_.map( castedAttrs[ name ], function( id ) {
return parseInt( id, 10 );
return 'number' === typeof id;
} else if ( 'integer' === type ) {
castedAttrs[ name ] = parseInt( value, 10 );
} else if ( 'boolean' === type ) {
castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
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).
* @return {Object} Reset/override props.
getEmbedResetProps: function getEmbedResetProps() {
* Collection of all widget model instances.
* @memberOf wp.mediaWidgets
* @type {Backbone.Collection}
component.modelCollection = new ( Backbone.Collection.extend( {
model: component.MediaWidgetModel
* Mapping of widget ID to instances of MediaWidgetControl subclasses.
* @memberOf wp.mediaWidgets
* @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
component.widgetControls = {};
* Handle widget being added or initialized for the first time at the widget-added event.
* @memberOf wp.mediaWidgets
* @param {jQuery.Event} event - Event.
* @param {jQuery} widgetContainer - Widget container element.
component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone;
widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
idBase = widgetForm.find( '> .id_base' ).val();
widgetId = widgetForm.find( '> .widget-id' ).val();
// Prevent initializing already-added widgets.
if ( component.widgetControls[ widgetId ] ) {
ControlConstructor = component.controlConstructors[ idBase ];
if ( ! ControlConstructor ) {
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 .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
fieldContainer = $( '<div></div>' );
syncContainer = widgetContainer.find( '.widget-content:first' );
syncContainer.before( fieldContainer );
* 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>.
syncContainer.find( '.media-widget-instance-property' ).each( function() {
modelAttributes[ input.data( 'property' ) ] = input.val();
modelAttributes.widget_id = widgetId;
widgetModel = new ModelConstructor( modelAttributes );
widgetControl = new ControlConstructor({
syncContainer: syncContainer,
* Render the widget once the widget parent's container finishes animating,
* as the widget-added event fires with a slideDown of the container.
* This ensures that the container's dimensions are fixed so that ME.js
* can initialize with the proper dimensions.
renderWhenAnimationDone = function() {
if ( ! widgetContainer.hasClass( 'open' ) ) {
setTimeout( renderWhenAnimationDone, animatedCheckDelay );
renderWhenAnimationDone();
* 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;
* Setup widget in accessibility mode.
* @memberOf wp.mediaWidgets
component.setupAccessibleMode = function setupAccessibleMode() {
var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer;
widgetForm = $( '.editwidget > form' );
if ( 0 === widgetForm.length ) {
idBase = widgetForm.find( '.id_base' ).val();
ControlConstructor = component.controlConstructors[ idBase ];
if ( ! ControlConstructor ) {
widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val();
ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
fieldContainer = $( '<div></div>' );
syncContainer = widgetForm.find( '> .widget-inside' );
syncContainer.before( fieldContainer );
syncContainer.find( '.media-widget-instance-property' ).each( function() {
modelAttributes[ input.data( 'property' ) ] = input.val();
modelAttributes.widget_id = widgetId;
widgetControl = new ControlConstructor({
syncContainer: syncContainer,
model: new ModelConstructor( modelAttributes )
component.modelCollection.add( [ widgetControl.model ] );
component.widgetControls[ widgetControl.model.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.
* @memberOf wp.mediaWidgets
* @param {jQuery.Event} event - Event.
* @param {jQuery} widgetContainer - Widget container element.
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 ];
// 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().
* @memberOf wp.mediaWidgets
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() {
if ( 'widgets' !== window.pagenow ) {
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 );
if ( document.readyState === 'complete' ) {
component.setupAccessibleMode();
// Page is still loading.
$( window ).on( 'load', function() {
component.setupAccessibleMode();