[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/widgets/ -> media-widgets.js (source)

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


Generated : Sun Sep 6 08:20:27 2026 Cross-referenced by PHPXref