[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

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


Generated: Sat Nov 23 20:47:33 2019 Cross-referenced by PHPXref 0.7