wp/wp-admin/js/widgets/media-widgets.js
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     1 /* eslint consistent-this: [ "error", "control" ] */
       
     2 wp.mediaWidgets = ( function( $ ) {
       
     3 	'use strict';
       
     4 
       
     5 	var component = {};
       
     6 
       
     7 	/**
       
     8 	 * Widget control (view) constructors, mapping widget id_base to subclass of MediaWidgetControl.
       
     9 	 *
       
    10 	 * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
       
    11 	 *
       
    12 	 * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
       
    13 	 */
       
    14 	component.controlConstructors = {};
       
    15 
       
    16 	/**
       
    17 	 * Widget model constructors, mapping widget id_base to subclass of MediaWidgetModel.
       
    18 	 *
       
    19 	 * Media widgets register themselves by assigning subclasses of MediaWidgetControl onto this object by widget ID base.
       
    20 	 *
       
    21 	 * @type {Object.<string, wp.mediaWidgets.MediaWidgetModel>}
       
    22 	 */
       
    23 	component.modelConstructors = {};
       
    24 
       
    25 	/**
       
    26 	 * Library which persists the customized display settings across selections.
       
    27 	 *
       
    28 	 * @class PersistentDisplaySettingsLibrary
       
    29 	 * @constructor
       
    30 	 */
       
    31 	component.PersistentDisplaySettingsLibrary = wp.media.controller.Library.extend({
       
    32 
       
    33 		/**
       
    34 		 * Initialize.
       
    35 		 *
       
    36 		 * @param {Object} options - Options.
       
    37 		 * @returns {void}
       
    38 		 */
       
    39 		initialize: function initialize( options ) {
       
    40 			_.bindAll( this, 'handleDisplaySettingChange' );
       
    41 			wp.media.controller.Library.prototype.initialize.call( this, options );
       
    42 		},
       
    43 
       
    44 		/**
       
    45 		 * Sync changes to the current display settings back into the current customized.
       
    46 		 *
       
    47 		 * @param {Backbone.Model} displaySettings - Modified display settings.
       
    48 		 * @returns {void}
       
    49 		 */
       
    50 		handleDisplaySettingChange: function handleDisplaySettingChange( displaySettings ) {
       
    51 			this.get( 'selectedDisplaySettings' ).set( displaySettings.attributes );
       
    52 		},
       
    53 
       
    54 		/**
       
    55 		 * Get the display settings model.
       
    56 		 *
       
    57 		 * Model returned is updated with the current customized display settings,
       
    58 		 * and an event listener is added so that changes made to the settings
       
    59 		 * will sync back into the model storing the session's customized display
       
    60 		 * settings.
       
    61 		 *
       
    62 		 * @param {Backbone.Model} model - Display settings model.
       
    63 		 * @returns {Backbone.Model} Display settings model.
       
    64 		 */
       
    65 		display: function getDisplaySettingsModel( model ) {
       
    66 			var display, selectedDisplaySettings = this.get( 'selectedDisplaySettings' );
       
    67 			display = wp.media.controller.Library.prototype.display.call( this, model );
       
    68 
       
    69 			display.off( 'change', this.handleDisplaySettingChange ); // Prevent duplicated event handlers.
       
    70 			display.set( selectedDisplaySettings.attributes );
       
    71 			if ( 'custom' === selectedDisplaySettings.get( 'link_type' ) ) {
       
    72 				display.linkUrl = selectedDisplaySettings.get( 'link_url' );
       
    73 			}
       
    74 			display.on( 'change', this.handleDisplaySettingChange );
       
    75 			return display;
       
    76 		}
       
    77 	});
       
    78 
       
    79 	/**
       
    80 	 * Extended view for managing the embed UI.
       
    81 	 *
       
    82 	 * @class MediaEmbedView
       
    83 	 * @constructor
       
    84 	 */
       
    85 	component.MediaEmbedView = wp.media.view.Embed.extend({
       
    86 
       
    87 		/**
       
    88 		 * Initialize.
       
    89 		 *
       
    90 		 * @since 4.9.0
       
    91 		 *
       
    92 		 * @param {object} options - Options.
       
    93 		 * @returns {void}
       
    94 		 */
       
    95 		initialize: function( options ) {
       
    96 			var view = this, embedController; // eslint-disable-line consistent-this
       
    97 			wp.media.view.Embed.prototype.initialize.call( view, options );
       
    98 			if ( 'image' !== view.controller.options.mimeType ) {
       
    99 				embedController = view.controller.states.get( 'embed' );
       
   100 				embedController.off( 'scan', embedController.scanImage, embedController );
       
   101 			}
       
   102 		},
       
   103 
       
   104 		/**
       
   105 		 * Refresh embed view.
       
   106 		 *
       
   107 		 * Forked override of {wp.media.view.Embed#refresh()} to suppress irrelevant "link text" field.
       
   108 		 *
       
   109 		 * @returns {void}
       
   110 		 */
       
   111 		refresh: function refresh() {
       
   112 			var Constructor;
       
   113 
       
   114 			if ( 'image' === this.controller.options.mimeType ) {
       
   115 				Constructor = wp.media.view.EmbedImage;
       
   116 			} else {
       
   117 
       
   118 				// This should be eliminated once #40450 lands of when this is merged into core.
       
   119 				Constructor = wp.media.view.EmbedLink.extend({
       
   120 
       
   121 					/**
       
   122 					 * Set the disabled state on the Add to Widget button.
       
   123 					 *
       
   124 					 * @param {boolean} disabled - Disabled.
       
   125 					 * @returns {void}
       
   126 					 */
       
   127 					setAddToWidgetButtonDisabled: function setAddToWidgetButtonDisabled( disabled ) {
       
   128 						this.views.parent.views.parent.views.get( '.media-frame-toolbar' )[0].$el.find( '.media-button-select' ).prop( 'disabled', disabled );
       
   129 					},
       
   130 
       
   131 					/**
       
   132 					 * Set or clear an error notice.
       
   133 					 *
       
   134 					 * @param {string} notice - Notice.
       
   135 					 * @returns {void}
       
   136 					 */
       
   137 					setErrorNotice: function setErrorNotice( notice ) {
       
   138 						var embedLinkView = this, noticeContainer; // eslint-disable-line consistent-this
       
   139 
       
   140 						noticeContainer = embedLinkView.views.parent.$el.find( '> .notice:first-child' );
       
   141 						if ( ! notice ) {
       
   142 							if ( noticeContainer.length ) {
       
   143 								noticeContainer.slideUp( 'fast' );
       
   144 							}
       
   145 						} else {
       
   146 							if ( ! noticeContainer.length ) {
       
   147 								noticeContainer = $( '<div class="media-widget-embed-notice notice notice-error notice-alt"></div>' );
       
   148 								noticeContainer.hide();
       
   149 								embedLinkView.views.parent.$el.prepend( noticeContainer );
       
   150 							}
       
   151 							noticeContainer.empty();
       
   152 							noticeContainer.append( $( '<p>', {
       
   153 								html: notice
       
   154 							}));
       
   155 							noticeContainer.slideDown( 'fast' );
       
   156 						}
       
   157 					},
       
   158 
       
   159 					/**
       
   160 					 * Update oEmbed.
       
   161 					 *
       
   162 					 * @since 4.9.0
       
   163 					 *
       
   164 					 * @returns {void}
       
   165 					 */
       
   166 					updateoEmbed: function() {
       
   167 						var embedLinkView = this, url; // eslint-disable-line consistent-this
       
   168 
       
   169 						url = embedLinkView.model.get( 'url' );
       
   170 
       
   171 						// Abort if the URL field was emptied out.
       
   172 						if ( ! url ) {
       
   173 							embedLinkView.setErrorNotice( '' );
       
   174 							embedLinkView.setAddToWidgetButtonDisabled( true );
       
   175 							return;
       
   176 						}
       
   177 
       
   178 						if ( ! url.match( /^(http|https):\/\/.+\// ) ) {
       
   179 							embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
       
   180 							embedLinkView.setAddToWidgetButtonDisabled( true );
       
   181 						}
       
   182 
       
   183 						wp.media.view.EmbedLink.prototype.updateoEmbed.call( embedLinkView );
       
   184 					},
       
   185 
       
   186 					/**
       
   187 					 * Fetch media.
       
   188 					 *
       
   189 					 * @returns {void}
       
   190 					 */
       
   191 					fetch: function() {
       
   192 						var embedLinkView = this, fetchSuccess, matches, fileExt, urlParser, url, re, youTubeEmbedMatch; // eslint-disable-line consistent-this
       
   193 						url = embedLinkView.model.get( 'url' );
       
   194 
       
   195 						if ( embedLinkView.dfd && 'pending' === embedLinkView.dfd.state() ) {
       
   196 							embedLinkView.dfd.abort();
       
   197 						}
       
   198 
       
   199 						fetchSuccess = function( response ) {
       
   200 							embedLinkView.renderoEmbed({
       
   201 								data: {
       
   202 									body: response
       
   203 								}
       
   204 							});
       
   205 
       
   206 							embedLinkView.controller.$el.find( '#embed-url-field' ).removeClass( 'invalid' );
       
   207 							embedLinkView.setErrorNotice( '' );
       
   208 							embedLinkView.setAddToWidgetButtonDisabled( false );
       
   209 						};
       
   210 
       
   211 						urlParser = document.createElement( 'a' );
       
   212 						urlParser.href = url;
       
   213 						matches = urlParser.pathname.toLowerCase().match( /\.(\w+)$/ );
       
   214 						if ( matches ) {
       
   215 							fileExt = matches[1];
       
   216 							if ( ! wp.media.view.settings.embedMimes[ fileExt ] ) {
       
   217 								embedLinkView.renderFail();
       
   218 							} else if ( 0 !== wp.media.view.settings.embedMimes[ fileExt ].indexOf( embedLinkView.controller.options.mimeType ) ) {
       
   219 								embedLinkView.renderFail();
       
   220 							} else {
       
   221 								fetchSuccess( '<!--success-->' );
       
   222 							}
       
   223 							return;
       
   224 						}
       
   225 
       
   226 						// Support YouTube embed links.
       
   227 						re = /https?:\/\/www\.youtube\.com\/embed\/([^/]+)/;
       
   228 						youTubeEmbedMatch = re.exec( url );
       
   229 						if ( youTubeEmbedMatch ) {
       
   230 							url = 'https://www.youtube.com/watch?v=' + youTubeEmbedMatch[ 1 ];
       
   231 							// silently change url to proper oembed-able version.
       
   232 							embedLinkView.model.attributes.url = url;
       
   233 						}
       
   234 
       
   235 						embedLinkView.dfd = wp.apiRequest({
       
   236 							url: wp.media.view.settings.oEmbedProxyUrl,
       
   237 							data: {
       
   238 								url: url,
       
   239 								maxwidth: embedLinkView.model.get( 'width' ),
       
   240 								maxheight: embedLinkView.model.get( 'height' ),
       
   241 								discover: false
       
   242 							},
       
   243 							type: 'GET',
       
   244 							dataType: 'json',
       
   245 							context: embedLinkView
       
   246 						});
       
   247 
       
   248 						embedLinkView.dfd.done( function( response ) {
       
   249 							if ( embedLinkView.controller.options.mimeType !== response.type ) {
       
   250 								embedLinkView.renderFail();
       
   251 								return;
       
   252 							}
       
   253 							fetchSuccess( response.html );
       
   254 						});
       
   255 						embedLinkView.dfd.fail( _.bind( embedLinkView.renderFail, embedLinkView ) );
       
   256 					},
       
   257 
       
   258 					/**
       
   259 					 * Handle render failure.
       
   260 					 *
       
   261 					 * Overrides the {EmbedLink#renderFail()} method to prevent showing the "Link Text" field.
       
   262 					 * The element is getting display:none in the stylesheet, but the underlying method uses
       
   263 					 * uses {jQuery.fn.show()} which adds an inline style. This avoids the need for !important.
       
   264 					 *
       
   265 					 * @returns {void}
       
   266 					 */
       
   267 					renderFail: function renderFail() {
       
   268 						var embedLinkView = this; // eslint-disable-line consistent-this
       
   269 						embedLinkView.controller.$el.find( '#embed-url-field' ).addClass( 'invalid' );
       
   270 						embedLinkView.setErrorNotice( embedLinkView.controller.options.invalidEmbedTypeError || 'ERROR' );
       
   271 						embedLinkView.setAddToWidgetButtonDisabled( true );
       
   272 					}
       
   273 				});
       
   274 			}
       
   275 
       
   276 			this.settings( new Constructor({
       
   277 				controller: this.controller,
       
   278 				model:      this.model.props,
       
   279 				priority:   40
       
   280 			}));
       
   281 		}
       
   282 	});
       
   283 
       
   284 	/**
       
   285 	 * Custom media frame for selecting uploaded media or providing media by URL.
       
   286 	 *
       
   287 	 * @class MediaFrameSelect
       
   288 	 * @constructor
       
   289 	 */
       
   290 	component.MediaFrameSelect = wp.media.view.MediaFrame.Post.extend({
       
   291 
       
   292 		/**
       
   293 		 * Create the default states.
       
   294 		 *
       
   295 		 * @returns {void}
       
   296 		 */
       
   297 		createStates: function createStates() {
       
   298 			var mime = this.options.mimeType, specificMimes = [];
       
   299 			_.each( wp.media.view.settings.embedMimes, function( embedMime ) {
       
   300 				if ( 0 === embedMime.indexOf( mime ) ) {
       
   301 					specificMimes.push( embedMime );
       
   302 				}
       
   303 			});
       
   304 			if ( specificMimes.length > 0 ) {
       
   305 				mime = specificMimes;
       
   306 			}
       
   307 
       
   308 			this.states.add([
       
   309 
       
   310 				// Main states.
       
   311 				new component.PersistentDisplaySettingsLibrary({
       
   312 					id:         'insert',
       
   313 					title:      this.options.title,
       
   314 					selection:  this.options.selection,
       
   315 					priority:   20,
       
   316 					toolbar:    'main-insert',
       
   317 					filterable: 'dates',
       
   318 					library:    wp.media.query({
       
   319 						type: mime
       
   320 					}),
       
   321 					multiple:   false,
       
   322 					editable:   true,
       
   323 
       
   324 					selectedDisplaySettings: this.options.selectedDisplaySettings,
       
   325 					displaySettings: _.isUndefined( this.options.showDisplaySettings ) ? true : this.options.showDisplaySettings,
       
   326 					displayUserSettings: false // We use the display settings from the current/default widget instance props.
       
   327 				}),
       
   328 
       
   329 				new wp.media.controller.EditImage({ model: this.options.editImage }),
       
   330 
       
   331 				// Embed states.
       
   332 				new wp.media.controller.Embed({
       
   333 					metadata: this.options.metadata,
       
   334 					type: 'image' === this.options.mimeType ? 'image' : 'link',
       
   335 					invalidEmbedTypeError: this.options.invalidEmbedTypeError
       
   336 				})
       
   337 			]);
       
   338 		},
       
   339 
       
   340 		/**
       
   341 		 * Main insert toolbar.
       
   342 		 *
       
   343 		 * Forked override of {wp.media.view.MediaFrame.Post#mainInsertToolbar()} to override text.
       
   344 		 *
       
   345 		 * @param {wp.Backbone.View} view - Toolbar view.
       
   346 		 * @this {wp.media.controller.Library}
       
   347 		 * @returns {void}
       
   348 		 */
       
   349 		mainInsertToolbar: function mainInsertToolbar( view ) {
       
   350 			var controller = this; // eslint-disable-line consistent-this
       
   351 			view.set( 'insert', {
       
   352 				style:    'primary',
       
   353 				priority: 80,
       
   354 				text:     controller.options.text, // The whole reason for the fork.
       
   355 				requires: { selection: true },
       
   356 
       
   357 				/**
       
   358 				 * Handle click.
       
   359 				 *
       
   360 				 * @fires wp.media.controller.State#insert()
       
   361 				 * @returns {void}
       
   362 				 */
       
   363 				click: function onClick() {
       
   364 					var state = controller.state(),
       
   365 						selection = state.get( 'selection' );
       
   366 
       
   367 					controller.close();
       
   368 					state.trigger( 'insert', selection ).reset();
       
   369 				}
       
   370 			});
       
   371 		},
       
   372 
       
   373 		/**
       
   374 		 * Main embed toolbar.
       
   375 		 *
       
   376 		 * Forked override of {wp.media.view.MediaFrame.Post#mainEmbedToolbar()} to override text.
       
   377 		 *
       
   378 		 * @param {wp.Backbone.View} toolbar - Toolbar view.
       
   379 		 * @this {wp.media.controller.Library}
       
   380 		 * @returns {void}
       
   381 		 */
       
   382 		mainEmbedToolbar: function mainEmbedToolbar( toolbar ) {
       
   383 			toolbar.view = new wp.media.view.Toolbar.Embed({
       
   384 				controller: this,
       
   385 				text: this.options.text,
       
   386 				event: 'insert'
       
   387 			});
       
   388 		},
       
   389 
       
   390 		/**
       
   391 		 * Embed content.
       
   392 		 *
       
   393 		 * Forked override of {wp.media.view.MediaFrame.Post#embedContent()} to suppress irrelevant "link text" field.
       
   394 		 *
       
   395 		 * @returns {void}
       
   396 		 */
       
   397 		embedContent: function embedContent() {
       
   398 			var view = new component.MediaEmbedView({
       
   399 				controller: this,
       
   400 				model:      this.state()
       
   401 			}).render();
       
   402 
       
   403 			this.content.set( view );
       
   404 
       
   405 			if ( ! wp.media.isTouchDevice ) {
       
   406 				view.url.focus();
       
   407 			}
       
   408 		}
       
   409 	});
       
   410 
       
   411 	/**
       
   412 	 * Media widget control.
       
   413 	 *
       
   414 	 * @class MediaWidgetControl
       
   415 	 * @constructor
       
   416 	 * @abstract
       
   417 	 */
       
   418 	component.MediaWidgetControl = Backbone.View.extend({
       
   419 
       
   420 		/**
       
   421 		 * Translation strings.
       
   422 		 *
       
   423 		 * The mapping of translation strings is handled by media widget subclasses,
       
   424 		 * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
       
   425 		 *
       
   426 		 * @type {Object}
       
   427 		 */
       
   428 		l10n: {
       
   429 			add_to_widget: '{{add_to_widget}}',
       
   430 			add_media: '{{add_media}}'
       
   431 		},
       
   432 
       
   433 		/**
       
   434 		 * Widget ID base.
       
   435 		 *
       
   436 		 * This may be defined by the subclass. It may be exported from PHP to JS
       
   437 		 * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not,
       
   438 		 * it will attempt to be discovered by looking to see if this control
       
   439 		 * instance extends each member of component.controlConstructors, and if
       
   440 		 * it does extend one, will use the key as the id_base.
       
   441 		 *
       
   442 		 * @type {string}
       
   443 		 */
       
   444 		id_base: '',
       
   445 
       
   446 		/**
       
   447 		 * Mime type.
       
   448 		 *
       
   449 		 * This must be defined by the subclass. It may be exported from PHP to JS
       
   450 		 * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
       
   451 		 *
       
   452 		 * @type {string}
       
   453 		 */
       
   454 		mime_type: '',
       
   455 
       
   456 		/**
       
   457 		 * View events.
       
   458 		 *
       
   459 		 * @type {Object}
       
   460 		 */
       
   461 		events: {
       
   462 			'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
       
   463 			'click .select-media': 'selectMedia',
       
   464 			'click .placeholder': 'selectMedia',
       
   465 			'click .edit-media': 'editMedia'
       
   466 		},
       
   467 
       
   468 		/**
       
   469 		 * Show display settings.
       
   470 		 *
       
   471 		 * @type {boolean}
       
   472 		 */
       
   473 		showDisplaySettings: true,
       
   474 
       
   475 		/**
       
   476 		 * Initialize.
       
   477 		 *
       
   478 		 * @param {Object}         options - Options.
       
   479 		 * @param {Backbone.Model} options.model - Model.
       
   480 		 * @param {jQuery}         options.el - Control field container element.
       
   481 		 * @param {jQuery}         options.syncContainer - Container element where fields are synced for the server.
       
   482 		 * @returns {void}
       
   483 		 */
       
   484 		initialize: function initialize( options ) {
       
   485 			var control = this;
       
   486 
       
   487 			Backbone.View.prototype.initialize.call( control, options );
       
   488 
       
   489 			if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
       
   490 				throw new Error( 'Missing options.model' );
       
   491 			}
       
   492 			if ( ! options.el ) {
       
   493 				throw new Error( 'Missing options.el' );
       
   494 			}
       
   495 			if ( ! options.syncContainer ) {
       
   496 				throw new Error( 'Missing options.syncContainer' );
       
   497 			}
       
   498 
       
   499 			control.syncContainer = options.syncContainer;
       
   500 
       
   501 			control.$el.addClass( 'media-widget-control' );
       
   502 
       
   503 			// Allow methods to be passed in with control context preserved.
       
   504 			_.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
       
   505 
       
   506 			if ( ! control.id_base ) {
       
   507 				_.find( component.controlConstructors, function( Constructor, idBase ) {
       
   508 					if ( control instanceof Constructor ) {
       
   509 						control.id_base = idBase;
       
   510 						return true;
       
   511 					}
       
   512 					return false;
       
   513 				});
       
   514 				if ( ! control.id_base ) {
       
   515 					throw new Error( 'Missing id_base.' );
       
   516 				}
       
   517 			}
       
   518 
       
   519 			// Track attributes needed to renderPreview in it's own model.
       
   520 			control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() );
       
   521 
       
   522 			// Re-render the preview when the attachment changes.
       
   523 			control.selectedAttachment = new wp.media.model.Attachment();
       
   524 			control.renderPreview = _.debounce( control.renderPreview );
       
   525 			control.listenTo( control.previewTemplateProps, 'change', control.renderPreview );
       
   526 
       
   527 			// Make sure a copy of the selected attachment is always fetched.
       
   528 			control.model.on( 'change:attachment_id', control.updateSelectedAttachment );
       
   529 			control.model.on( 'change:url', control.updateSelectedAttachment );
       
   530 			control.updateSelectedAttachment();
       
   531 
       
   532 			/*
       
   533 			 * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
       
   534 			 * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
       
   535 			 * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
       
   536 			 */
       
   537 			control.listenTo( control.model, 'change', control.syncModelToInputs );
       
   538 			control.listenTo( control.model, 'change', control.syncModelToPreviewProps );
       
   539 			control.listenTo( control.model, 'change', control.render );
       
   540 
       
   541 			// Update the title.
       
   542 			control.$el.on( 'input change', '.title', function updateTitle() {
       
   543 				control.model.set({
       
   544 					title: $.trim( $( this ).val() )
       
   545 				});
       
   546 			});
       
   547 
       
   548 			// Update link_url attribute.
       
   549 			control.$el.on( 'input change', '.link', function updateLinkUrl() {
       
   550 				var linkUrl = $.trim( $( this ).val() ), linkType = 'custom';
       
   551 				if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) {
       
   552 					linkType = 'post';
       
   553 				} else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) {
       
   554 					linkType = 'file';
       
   555 				}
       
   556 				control.model.set( {
       
   557 					link_url: linkUrl,
       
   558 					link_type: linkType
       
   559 				});
       
   560 
       
   561 				// Update display settings for the next time the user opens to select from the media library.
       
   562 				control.displaySettings.set( {
       
   563 					link: linkType,
       
   564 					linkUrl: linkUrl
       
   565 				});
       
   566 			});
       
   567 
       
   568 			/*
       
   569 			 * Copy current display settings from the widget model to serve as basis
       
   570 			 * of customized display settings for the current media frame session.
       
   571 			 * Changes to display settings will be synced into this model, and
       
   572 			 * when a new selection is made, the settings from this will be synced
       
   573 			 * into that AttachmentDisplay's model to persist the setting changes.
       
   574 			 */
       
   575 			control.displaySettings = new Backbone.Model( _.pick(
       
   576 				control.mapModelToMediaFrameProps(
       
   577 					_.extend( control.model.defaults(), control.model.toJSON() )
       
   578 				),
       
   579 				_.keys( wp.media.view.settings.defaultProps )
       
   580 			) );
       
   581 		},
       
   582 
       
   583 		/**
       
   584 		 * Update the selected attachment if necessary.
       
   585 		 *
       
   586 		 * @returns {void}
       
   587 		 */
       
   588 		updateSelectedAttachment: function updateSelectedAttachment() {
       
   589 			var control = this, attachment;
       
   590 
       
   591 			if ( 0 === control.model.get( 'attachment_id' ) ) {
       
   592 				control.selectedAttachment.clear();
       
   593 				control.model.set( 'error', false );
       
   594 			} else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) {
       
   595 				attachment = new wp.media.model.Attachment({
       
   596 					id: control.model.get( 'attachment_id' )
       
   597 				});
       
   598 				attachment.fetch()
       
   599 					.done( function done() {
       
   600 						control.model.set( 'error', false );
       
   601 						control.selectedAttachment.set( attachment.toJSON() );
       
   602 					})
       
   603 					.fail( function fail() {
       
   604 						control.model.set( 'error', 'missing_attachment' );
       
   605 					});
       
   606 			}
       
   607 		},
       
   608 
       
   609 		/**
       
   610 		 * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
       
   611 		 *
       
   612 		 * @returns {void}
       
   613 		 */
       
   614 		syncModelToPreviewProps: function syncModelToPreviewProps() {
       
   615 			var control = this;
       
   616 			control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() );
       
   617 		},
       
   618 
       
   619 		/**
       
   620 		 * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
       
   621 		 *
       
   622 		 * @returns {void}
       
   623 		 */
       
   624 		syncModelToInputs: function syncModelToInputs() {
       
   625 			var control = this;
       
   626 			control.syncContainer.find( '.media-widget-instance-property' ).each( function() {
       
   627 				var input = $( this ), value, propertyName;
       
   628 				propertyName = input.data( 'property' );
       
   629 				value = control.model.get( propertyName );
       
   630 				if ( _.isUndefined( value ) ) {
       
   631 					return;
       
   632 				}
       
   633 
       
   634 				if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) {
       
   635 					value = value.join( ',' );
       
   636 				} else if ( 'boolean' === control.model.schema[ propertyName ].type ) {
       
   637 					value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''.
       
   638 				} else {
       
   639 					value = String( value );
       
   640 				}
       
   641 
       
   642 				if ( input.val() !== value ) {
       
   643 					input.val( value );
       
   644 					input.trigger( 'change' );
       
   645 				}
       
   646 			});
       
   647 		},
       
   648 
       
   649 		/**
       
   650 		 * Get template.
       
   651 		 *
       
   652 		 * @returns {Function} Template.
       
   653 		 */
       
   654 		template: function template() {
       
   655 			var control = this;
       
   656 			if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) {
       
   657 				throw new Error( 'Missing widget control template for ' + control.id_base );
       
   658 			}
       
   659 			return wp.template( 'widget-media-' + control.id_base + '-control' );
       
   660 		},
       
   661 
       
   662 		/**
       
   663 		 * Render template.
       
   664 		 *
       
   665 		 * @returns {void}
       
   666 		 */
       
   667 		render: function render() {
       
   668 			var control = this, titleInput;
       
   669 
       
   670 			if ( ! control.templateRendered ) {
       
   671 				control.$el.html( control.template()( control.model.toJSON() ) );
       
   672 				control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes.
       
   673 				control.templateRendered = true;
       
   674 			}
       
   675 
       
   676 			titleInput = control.$el.find( '.title' );
       
   677 			if ( ! titleInput.is( document.activeElement ) ) {
       
   678 				titleInput.val( control.model.get( 'title' ) );
       
   679 			}
       
   680 
       
   681 			control.$el.toggleClass( 'selected', control.isSelected() );
       
   682 		},
       
   683 
       
   684 		/**
       
   685 		 * Render media preview.
       
   686 		 *
       
   687 		 * @abstract
       
   688 		 * @returns {void}
       
   689 		 */
       
   690 		renderPreview: function renderPreview() {
       
   691 			throw new Error( 'renderPreview must be implemented' );
       
   692 		},
       
   693 
       
   694 		/**
       
   695 		 * Whether a media item is selected.
       
   696 		 *
       
   697 		 * @returns {boolean} Whether selected and no error.
       
   698 		 */
       
   699 		isSelected: function isSelected() {
       
   700 			var control = this;
       
   701 
       
   702 			if ( control.model.get( 'error' ) ) {
       
   703 				return false;
       
   704 			}
       
   705 
       
   706 			return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) );
       
   707 		},
       
   708 
       
   709 		/**
       
   710 		 * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice.
       
   711 		 *
       
   712 		 * @param {jQuery.Event} event - Event.
       
   713 		 * @returns {void}
       
   714 		 */
       
   715 		handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) {
       
   716 			var control = this;
       
   717 			event.preventDefault();
       
   718 			control.selectMedia();
       
   719 		},
       
   720 
       
   721 		/**
       
   722 		 * Open the media select frame to chose an item.
       
   723 		 *
       
   724 		 * @returns {void}
       
   725 		 */
       
   726 		selectMedia: function selectMedia() {
       
   727 			var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = [];
       
   728 
       
   729 			if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) {
       
   730 				selectionModels.push( control.selectedAttachment );
       
   731 			}
       
   732 
       
   733 			selection = new wp.media.model.Selection( selectionModels, { multiple: false } );
       
   734 
       
   735 			mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
       
   736 			if ( mediaFrameProps.size ) {
       
   737 				control.displaySettings.set( 'size', mediaFrameProps.size );
       
   738 			}
       
   739 
       
   740 			mediaFrame = new component.MediaFrameSelect({
       
   741 				title: control.l10n.add_media,
       
   742 				frame: 'post',
       
   743 				text: control.l10n.add_to_widget,
       
   744 				selection: selection,
       
   745 				mimeType: control.mime_type,
       
   746 				selectedDisplaySettings: control.displaySettings,
       
   747 				showDisplaySettings: control.showDisplaySettings,
       
   748 				metadata: mediaFrameProps,
       
   749 				state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert',
       
   750 				invalidEmbedTypeError: control.l10n.unsupported_file_type
       
   751 			});
       
   752 			wp.media.frame = mediaFrame; // See wp.media().
       
   753 
       
   754 			// Handle selection of a media item.
       
   755 			mediaFrame.on( 'insert', function onInsert() {
       
   756 				var attachment = {}, state = mediaFrame.state();
       
   757 
       
   758 				// Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
       
   759 				if ( 'embed' === state.get( 'id' ) ) {
       
   760 					_.extend( attachment, { id: 0 }, state.props.toJSON() );
       
   761 				} else {
       
   762 					_.extend( attachment, state.get( 'selection' ).first().toJSON() );
       
   763 				}
       
   764 
       
   765 				control.selectedAttachment.set( attachment );
       
   766 				control.model.set( 'error', false );
       
   767 
       
   768 				// Update widget instance.
       
   769 				control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) );
       
   770 			});
       
   771 
       
   772 			// Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>.
       
   773 			defaultSync = wp.media.model.Attachment.prototype.sync;
       
   774 			wp.media.model.Attachment.prototype.sync = function( method ) {
       
   775 				if ( 'delete' === method ) {
       
   776 					return defaultSync.apply( this, arguments );
       
   777 				} else {
       
   778 					return $.Deferred().rejectWith( this ).promise();
       
   779 				}
       
   780 			};
       
   781 			mediaFrame.on( 'close', function onClose() {
       
   782 				wp.media.model.Attachment.prototype.sync = defaultSync;
       
   783 			});
       
   784 
       
   785 			mediaFrame.$el.addClass( 'media-widget' );
       
   786 			mediaFrame.open();
       
   787 
       
   788 			// Clear the selected attachment when it is deleted in the media select frame.
       
   789 			if ( selection ) {
       
   790 				selection.on( 'destroy', function onDestroy( attachment ) {
       
   791 					if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) {
       
   792 						control.model.set({
       
   793 							attachment_id: 0,
       
   794 							url: ''
       
   795 						});
       
   796 					}
       
   797 				});
       
   798 			}
       
   799 
       
   800 			/*
       
   801 			 * Make sure focus is set inside of modal so that hitting Esc will close
       
   802 			 * the modal and not inadvertently cause the widget to collapse in the customizer.
       
   803 			 */
       
   804 			mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus();
       
   805 		},
       
   806 
       
   807 		/**
       
   808 		 * Get the instance props from the media selection frame.
       
   809 		 *
       
   810 		 * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
       
   811 		 * @returns {Object} Props.
       
   812 		 */
       
   813 		getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
       
   814 			var control = this, state, mediaFrameProps, modelProps;
       
   815 
       
   816 			state = mediaFrame.state();
       
   817 			if ( 'insert' === state.get( 'id' ) ) {
       
   818 				mediaFrameProps = state.get( 'selection' ).first().toJSON();
       
   819 				mediaFrameProps.postUrl = mediaFrameProps.link;
       
   820 
       
   821 				if ( control.showDisplaySettings ) {
       
   822 					_.extend(
       
   823 						mediaFrameProps,
       
   824 						mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON()
       
   825 					);
       
   826 				}
       
   827 				if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) {
       
   828 					mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url;
       
   829 				}
       
   830 			} else if ( 'embed' === state.get( 'id' ) ) {
       
   831 				mediaFrameProps = _.extend(
       
   832 					state.props.toJSON(),
       
   833 					{ attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`.
       
   834 					control.model.getEmbedResetProps()
       
   835 				);
       
   836 			} else {
       
   837 				throw new Error( 'Unexpected state: ' + state.get( 'id' ) );
       
   838 			}
       
   839 
       
   840 			if ( mediaFrameProps.id ) {
       
   841 				mediaFrameProps.attachment_id = mediaFrameProps.id;
       
   842 			}
       
   843 
       
   844 			modelProps = control.mapMediaToModelProps( mediaFrameProps );
       
   845 
       
   846 			// Clear the extension prop so sources will be reset for video and audio media.
       
   847 			_.each( wp.media.view.settings.embedExts, function( ext ) {
       
   848 				if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) {
       
   849 					modelProps[ ext ] = '';
       
   850 				}
       
   851 			});
       
   852 
       
   853 			return modelProps;
       
   854 		},
       
   855 
       
   856 		/**
       
   857 		 * Map media frame props to model props.
       
   858 		 *
       
   859 		 * @param {Object} mediaFrameProps - Media frame props.
       
   860 		 * @returns {Object} Model props.
       
   861 		 */
       
   862 		mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) {
       
   863 			var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension;
       
   864 			_.each( control.model.schema, function( fieldSchema, modelProp ) {
       
   865 
       
   866 				// Ignore widget title attribute.
       
   867 				if ( 'title' === modelProp ) {
       
   868 					return;
       
   869 				}
       
   870 				mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp;
       
   871 			});
       
   872 
       
   873 			_.each( mediaFrameProps, function( value, mediaProp ) {
       
   874 				var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp;
       
   875 				if ( control.model.schema[ propName ] ) {
       
   876 					modelProps[ propName ] = value;
       
   877 				}
       
   878 			});
       
   879 
       
   880 			if ( 'custom' === mediaFrameProps.size ) {
       
   881 				modelProps.width = mediaFrameProps.customWidth;
       
   882 				modelProps.height = mediaFrameProps.customHeight;
       
   883 			}
       
   884 
       
   885 			if ( 'post' === mediaFrameProps.link ) {
       
   886 				modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl;
       
   887 			} else if ( 'file' === mediaFrameProps.link ) {
       
   888 				modelProps.link_url = mediaFrameProps.url;
       
   889 			}
       
   890 
       
   891 			// Because some media frames use `id` instead of `attachment_id`.
       
   892 			if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) {
       
   893 				modelProps.attachment_id = mediaFrameProps.id;
       
   894 			}
       
   895 
       
   896 			if ( mediaFrameProps.url ) {
       
   897 				extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase();
       
   898 				if ( extension in control.model.schema ) {
       
   899 					modelProps[ extension ] = mediaFrameProps.url;
       
   900 				}
       
   901 			}
       
   902 
       
   903 			// Always omit the titles derived from mediaFrameProps.
       
   904 			return _.omit( modelProps, 'title' );
       
   905 		},
       
   906 
       
   907 		/**
       
   908 		 * Map model props to media frame props.
       
   909 		 *
       
   910 		 * @param {Object} modelProps - Model props.
       
   911 		 * @returns {Object} Media frame props.
       
   912 		 */
       
   913 		mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
       
   914 			var control = this, mediaFrameProps = {};
       
   915 
       
   916 			_.each( modelProps, function( value, modelProp ) {
       
   917 				var fieldSchema = control.model.schema[ modelProp ] || {};
       
   918 				mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value;
       
   919 			});
       
   920 
       
   921 			// Some media frames use attachment_id.
       
   922 			mediaFrameProps.attachment_id = mediaFrameProps.id;
       
   923 
       
   924 			if ( 'custom' === mediaFrameProps.size ) {
       
   925 				mediaFrameProps.customWidth = control.model.get( 'width' );
       
   926 				mediaFrameProps.customHeight = control.model.get( 'height' );
       
   927 			}
       
   928 
       
   929 			return mediaFrameProps;
       
   930 		},
       
   931 
       
   932 		/**
       
   933 		 * Map model props to previewTemplateProps.
       
   934 		 *
       
   935 		 * @returns {Object} Preview Template Props.
       
   936 		 */
       
   937 		mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
       
   938 			var control = this, previewTemplateProps = {};
       
   939 			_.each( control.model.schema, function( value, prop ) {
       
   940 				if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) {
       
   941 					previewTemplateProps[ prop ] = control.model.get( prop );
       
   942 				}
       
   943 			});
       
   944 
       
   945 			// Templates need to be aware of the error.
       
   946 			previewTemplateProps.error = control.model.get( 'error' );
       
   947 			return previewTemplateProps;
       
   948 		},
       
   949 
       
   950 		/**
       
   951 		 * Open the media frame to modify the selected item.
       
   952 		 *
       
   953 		 * @abstract
       
   954 		 * @returns {void}
       
   955 		 */
       
   956 		editMedia: function editMedia() {
       
   957 			throw new Error( 'editMedia not implemented' );
       
   958 		}
       
   959 	});
       
   960 
       
   961 	/**
       
   962 	 * Media widget model.
       
   963 	 *
       
   964 	 * @class MediaWidgetModel
       
   965 	 * @constructor
       
   966 	 */
       
   967 	component.MediaWidgetModel = Backbone.Model.extend({
       
   968 
       
   969 		/**
       
   970 		 * Id attribute.
       
   971 		 *
       
   972 		 * @type {string}
       
   973 		 */
       
   974 		idAttribute: 'widget_id',
       
   975 
       
   976 		/**
       
   977 		 * Instance schema.
       
   978 		 *
       
   979 		 * This adheres to JSON Schema and subclasses should have their schema
       
   980 		 * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
       
   981 		 *
       
   982 		 * @type {Object.<string, Object>}
       
   983 		 */
       
   984 		schema: {
       
   985 			title: {
       
   986 				type: 'string',
       
   987 				'default': ''
       
   988 			},
       
   989 			attachment_id: {
       
   990 				type: 'integer',
       
   991 				'default': 0
       
   992 			},
       
   993 			url: {
       
   994 				type: 'string',
       
   995 				'default': ''
       
   996 			}
       
   997 		},
       
   998 
       
   999 		/**
       
  1000 		 * Get default attribute values.
       
  1001 		 *
       
  1002 		 * @returns {Object} Mapping of property names to their default values.
       
  1003 		 */
       
  1004 		defaults: function() {
       
  1005 			var defaults = {};
       
  1006 			_.each( this.schema, function( fieldSchema, field ) {
       
  1007 				defaults[ field ] = fieldSchema['default'];
       
  1008 			});
       
  1009 			return defaults;
       
  1010 		},
       
  1011 
       
  1012 		/**
       
  1013 		 * Set attribute value(s).
       
  1014 		 *
       
  1015 		 * This is a wrapped version of Backbone.Model#set() which allows us to
       
  1016 		 * cast the attribute values from the hidden inputs' string values into
       
  1017 		 * the appropriate data types (integers or booleans).
       
  1018 		 *
       
  1019 		 * @param {string|Object} key - Attribute name or attribute pairs.
       
  1020 		 * @param {mixed|Object}  [val] - Attribute value or options object.
       
  1021 		 * @param {Object}        [options] - Options when attribute name and value are passed separately.
       
  1022 		 * @returns {wp.mediaWidgets.MediaWidgetModel} This model.
       
  1023 		 */
       
  1024 		set: function set( key, val, options ) {
       
  1025 			var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
       
  1026 			if ( null === key ) {
       
  1027 				return model;
       
  1028 			}
       
  1029 			if ( 'object' === typeof key ) {
       
  1030 				attrs = key;
       
  1031 				opts = val;
       
  1032 			} else {
       
  1033 				attrs = {};
       
  1034 				attrs[ key ] = val;
       
  1035 				opts = options;
       
  1036 			}
       
  1037 
       
  1038 			castedAttrs = {};
       
  1039 			_.each( attrs, function( value, name ) {
       
  1040 				var type;
       
  1041 				if ( ! model.schema[ name ] ) {
       
  1042 					castedAttrs[ name ] = value;
       
  1043 					return;
       
  1044 				}
       
  1045 				type = model.schema[ name ].type;
       
  1046 				if ( 'array' === type ) {
       
  1047 					castedAttrs[ name ] = value;
       
  1048 					if ( ! _.isArray( castedAttrs[ name ] ) ) {
       
  1049 						castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
       
  1050 					}
       
  1051 					if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
       
  1052 						castedAttrs[ name ] = _.filter(
       
  1053 							_.map( castedAttrs[ name ], function( id ) {
       
  1054 								return parseInt( id, 10 );
       
  1055 							},
       
  1056 							function( id ) {
       
  1057 								return 'number' === typeof id;
       
  1058 							}
       
  1059 						) );
       
  1060 					}
       
  1061 				} else if ( 'integer' === type ) {
       
  1062 					castedAttrs[ name ] = parseInt( value, 10 );
       
  1063 				} else if ( 'boolean' === type ) {
       
  1064 					castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
       
  1065 				} else {
       
  1066 					castedAttrs[ name ] = value;
       
  1067 				}
       
  1068 			});
       
  1069 
       
  1070 			return Backbone.Model.prototype.set.call( this, castedAttrs, opts );
       
  1071 		},
       
  1072 
       
  1073 		/**
       
  1074 		 * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
       
  1075 		 *
       
  1076 		 * @returns {Object} Reset/override props.
       
  1077 		 */
       
  1078 		getEmbedResetProps: function getEmbedResetProps() {
       
  1079 			return {
       
  1080 				id: 0
       
  1081 			};
       
  1082 		}
       
  1083 	});
       
  1084 
       
  1085 	/**
       
  1086 	 * Collection of all widget model instances.
       
  1087 	 *
       
  1088 	 * @type {Backbone.Collection}
       
  1089 	 */
       
  1090 	component.modelCollection = new ( Backbone.Collection.extend({
       
  1091 		model: component.MediaWidgetModel
       
  1092 	}) )();
       
  1093 
       
  1094 	/**
       
  1095 	 * Mapping of widget ID to instances of MediaWidgetControl subclasses.
       
  1096 	 *
       
  1097 	 * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
       
  1098 	 */
       
  1099 	component.widgetControls = {};
       
  1100 
       
  1101 	/**
       
  1102 	 * Handle widget being added or initialized for the first time at the widget-added event.
       
  1103 	 *
       
  1104 	 * @param {jQuery.Event} event - Event.
       
  1105 	 * @param {jQuery}       widgetContainer - Widget container element.
       
  1106 	 * @returns {void}
       
  1107 	 */
       
  1108 	component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
       
  1109 		var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone;
       
  1110 		widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
       
  1111 		idBase = widgetForm.find( '> .id_base' ).val();
       
  1112 		widgetId = widgetForm.find( '> .widget-id' ).val();
       
  1113 
       
  1114 		// Prevent initializing already-added widgets.
       
  1115 		if ( component.widgetControls[ widgetId ] ) {
       
  1116 			return;
       
  1117 		}
       
  1118 
       
  1119 		ControlConstructor = component.controlConstructors[ idBase ];
       
  1120 		if ( ! ControlConstructor ) {
       
  1121 			return;
       
  1122 		}
       
  1123 
       
  1124 		ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
       
  1125 
       
  1126 		/*
       
  1127 		 * Create a container element for the widget control (Backbone.View).
       
  1128 		 * This is inserted into the DOM immediately before the .widget-content
       
  1129 		 * element because the contents of this element are essentially "managed"
       
  1130 		 * by PHP, where each widget update cause the entire element to be emptied
       
  1131 		 * and replaced with the rendered output of WP_Widget::form() which is
       
  1132 		 * sent back in Ajax request made to save/update the widget instance.
       
  1133 		 * To prevent a "flash of replaced DOM elements and re-initialized JS
       
  1134 		 * components", the JS template is rendered outside of the normal form
       
  1135 		 * container.
       
  1136 		 */
       
  1137 		fieldContainer = $( '<div></div>' );
       
  1138 		syncContainer = widgetContainer.find( '.widget-content:first' );
       
  1139 		syncContainer.before( fieldContainer );
       
  1140 
       
  1141 		/*
       
  1142 		 * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
       
  1143 		 * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
       
  1144 		 * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
       
  1145 		 */
       
  1146 		modelAttributes = {};
       
  1147 		syncContainer.find( '.media-widget-instance-property' ).each( function() {
       
  1148 			var input = $( this );
       
  1149 			modelAttributes[ input.data( 'property' ) ] = input.val();
       
  1150 		});
       
  1151 		modelAttributes.widget_id = widgetId;
       
  1152 
       
  1153 		widgetModel = new ModelConstructor( modelAttributes );
       
  1154 
       
  1155 		widgetControl = new ControlConstructor({
       
  1156 			el: fieldContainer,
       
  1157 			syncContainer: syncContainer,
       
  1158 			model: widgetModel
       
  1159 		});
       
  1160 
       
  1161 		/*
       
  1162 		 * Render the widget once the widget parent's container finishes animating,
       
  1163 		 * as the widget-added event fires with a slideDown of the container.
       
  1164 		 * This ensures that the container's dimensions are fixed so that ME.js
       
  1165 		 * can initialize with the proper dimensions.
       
  1166 		 */
       
  1167 		renderWhenAnimationDone = function() {
       
  1168 			if ( ! widgetContainer.hasClass( 'open' ) ) {
       
  1169 				setTimeout( renderWhenAnimationDone, animatedCheckDelay );
       
  1170 			} else {
       
  1171 				widgetControl.render();
       
  1172 			}
       
  1173 		};
       
  1174 		renderWhenAnimationDone();
       
  1175 
       
  1176 		/*
       
  1177 		 * Note that the model and control currently won't ever get garbage-collected
       
  1178 		 * when a widget gets removed/deleted because there is no widget-removed event.
       
  1179 		 */
       
  1180 		component.modelCollection.add( [ widgetModel ] );
       
  1181 		component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl;
       
  1182 	};
       
  1183 
       
  1184 	/**
       
  1185 	 * Setup widget in accessibility mode.
       
  1186 	 *
       
  1187 	 * @returns {void}
       
  1188 	 */
       
  1189 	component.setupAccessibleMode = function setupAccessibleMode() {
       
  1190 		var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer;
       
  1191 		widgetForm = $( '.editwidget > form' );
       
  1192 		if ( 0 === widgetForm.length ) {
       
  1193 			return;
       
  1194 		}
       
  1195 
       
  1196 		idBase = widgetForm.find( '> .widget-control-actions > .id_base' ).val();
       
  1197 
       
  1198 		ControlConstructor = component.controlConstructors[ idBase ];
       
  1199 		if ( ! ControlConstructor ) {
       
  1200 			return;
       
  1201 		}
       
  1202 
       
  1203 		widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val();
       
  1204 
       
  1205 		ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
       
  1206 		fieldContainer = $( '<div></div>' );
       
  1207 		syncContainer = widgetForm.find( '> .widget-inside' );
       
  1208 		syncContainer.before( fieldContainer );
       
  1209 
       
  1210 		modelAttributes = {};
       
  1211 		syncContainer.find( '.media-widget-instance-property' ).each( function() {
       
  1212 			var input = $( this );
       
  1213 			modelAttributes[ input.data( 'property' ) ] = input.val();
       
  1214 		});
       
  1215 		modelAttributes.widget_id = widgetId;
       
  1216 
       
  1217 		widgetControl = new ControlConstructor({
       
  1218 			el: fieldContainer,
       
  1219 			syncContainer: syncContainer,
       
  1220 			model: new ModelConstructor( modelAttributes )
       
  1221 		});
       
  1222 
       
  1223 		component.modelCollection.add( [ widgetControl.model ] );
       
  1224 		component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl;
       
  1225 
       
  1226 		widgetControl.render();
       
  1227 	};
       
  1228 
       
  1229 	/**
       
  1230 	 * Sync widget instance data sanitized from server back onto widget model.
       
  1231 	 *
       
  1232 	 * This gets called via the 'widget-updated' event when saving a widget from
       
  1233 	 * the widgets admin screen and also via the 'widget-synced' event when making
       
  1234 	 * a change to a widget in the customizer.
       
  1235 	 *
       
  1236 	 * @param {jQuery.Event} event - Event.
       
  1237 	 * @param {jQuery}       widgetContainer - Widget container element.
       
  1238 	 * @returns {void}
       
  1239 	 */
       
  1240 	component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
       
  1241 		var widgetForm, widgetContent, widgetId, widgetControl, attributes = {};
       
  1242 		widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
       
  1243 		widgetId = widgetForm.find( '> .widget-id' ).val();
       
  1244 
       
  1245 		widgetControl = component.widgetControls[ widgetId ];
       
  1246 		if ( ! widgetControl ) {
       
  1247 			return;
       
  1248 		}
       
  1249 
       
  1250 		// Make sure the server-sanitized values get synced back into the model.
       
  1251 		widgetContent = widgetForm.find( '> .widget-content' );
       
  1252 		widgetContent.find( '.media-widget-instance-property' ).each( function() {
       
  1253 			var property = $( this ).data( 'property' );
       
  1254 			attributes[ property ] = $( this ).val();
       
  1255 		});
       
  1256 
       
  1257 		// Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop.
       
  1258 		widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs );
       
  1259 		widgetControl.model.set( attributes );
       
  1260 		widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs );
       
  1261 	};
       
  1262 
       
  1263 	/**
       
  1264 	 * Initialize functionality.
       
  1265 	 *
       
  1266 	 * This function exists to prevent the JS file from having to boot itself.
       
  1267 	 * When WordPress enqueues this script, it should have an inline script
       
  1268 	 * attached which calls wp.mediaWidgets.init().
       
  1269 	 *
       
  1270 	 * @returns {void}
       
  1271 	 */
       
  1272 	component.init = function init() {
       
  1273 		var $document = $( document );
       
  1274 		$document.on( 'widget-added', component.handleWidgetAdded );
       
  1275 		$document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
       
  1276 
       
  1277 		/*
       
  1278 		 * Manually trigger widget-added events for media widgets on the admin
       
  1279 		 * screen once they are expanded. The widget-added event is not triggered
       
  1280 		 * for each pre-existing widget on the widgets admin screen like it is
       
  1281 		 * on the customizer. Likewise, the customizer only triggers widget-added
       
  1282 		 * when the widget is expanded to just-in-time construct the widget form
       
  1283 		 * when it is actually going to be displayed. So the following implements
       
  1284 		 * the same for the widgets admin screen, to invoke the widget-added
       
  1285 		 * handler when a pre-existing media widget is expanded.
       
  1286 		 */
       
  1287 		$( function initializeExistingWidgetContainers() {
       
  1288 			var widgetContainers;
       
  1289 			if ( 'widgets' !== window.pagenow ) {
       
  1290 				return;
       
  1291 			}
       
  1292 			widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
       
  1293 			widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
       
  1294 				var widgetContainer = $( this );
       
  1295 				component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
       
  1296 			});
       
  1297 
       
  1298 			// Accessibility mode.
       
  1299 			$( window ).on( 'load', function() {
       
  1300 				component.setupAccessibleMode();
       
  1301 			});
       
  1302 		});
       
  1303 	};
       
  1304 
       
  1305 	return component;
       
  1306 })( jQuery );