[ Index ]

PHP Cross Reference of WordPress

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              if ( ! wp.media.isTouchDevice ) {
 422                  view.url.focus();
 423              }
 424          }
 425      });
 426  
 427      component.MediaWidgetControl = Backbone.View.extend(/** @lends wp.mediaWidgets.MediaWidgetControl.prototype */{
 428  
 429          /**
 430           * Translation strings.
 431           *
 432           * The mapping of translation strings is handled by media widget subclasses,
 433           * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
 434           *
 435           * @type {Object}
 436           */
 437          l10n: {
 438              add_to_widget: '{{add_to_widget}}',
 439              add_media: '{{add_media}}'
 440          },
 441  
 442          /**
 443           * Widget ID base.
 444           *
 445           * This may be defined by the subclass. It may be exported from PHP to JS
 446           * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts(). If not,
 447           * it will attempt to be discovered by looking to see if this control
 448           * instance extends each member of component.controlConstructors, and if
 449           * it does extend one, will use the key as the id_base.
 450           *
 451           * @type {string}
 452           */
 453          id_base: '',
 454  
 455          /**
 456           * Mime type.
 457           *
 458           * This must be defined by the subclass. It may be exported from PHP to JS
 459           * such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
 460           *
 461           * @type {string}
 462           */
 463          mime_type: '',
 464  
 465          /**
 466           * View events.
 467           *
 468           * @type {Object}
 469           */
 470          events: {
 471              'click .notice-missing-attachment a': 'handleMediaLibraryLinkClick',
 472              'click .select-media': 'selectMedia',
 473              'click .placeholder': 'selectMedia',
 474              'click .edit-media': 'editMedia'
 475          },
 476  
 477          /**
 478           * Show display settings.
 479           *
 480           * @type {boolean}
 481           */
 482          showDisplaySettings: true,
 483  
 484          /**
 485           * Media Widget Control.
 486           *
 487           * @constructs wp.mediaWidgets.MediaWidgetControl
 488           * @augments   Backbone.View
 489           * @abstract
 490           *
 491           * @param {Object}         options - Options.
 492           * @param {Backbone.Model} options.model - Model.
 493           * @param {jQuery}         options.el - Control field container element.
 494           * @param {jQuery}         options.syncContainer - Container element where fields are synced for the server.
 495           *
 496           * @returns {void}
 497           */
 498          initialize: function initialize( options ) {
 499              var control = this;
 500  
 501              Backbone.View.prototype.initialize.call( control, options );
 502  
 503              if ( ! ( control.model instanceof component.MediaWidgetModel ) ) {
 504                  throw new Error( 'Missing options.model' );
 505              }
 506              if ( ! options.el ) {
 507                  throw new Error( 'Missing options.el' );
 508              }
 509              if ( ! options.syncContainer ) {
 510                  throw new Error( 'Missing options.syncContainer' );
 511              }
 512  
 513              control.syncContainer = options.syncContainer;
 514  
 515              control.$el.addClass( 'media-widget-control' );
 516  
 517              // Allow methods to be passed in with control context preserved.
 518              _.bindAll( control, 'syncModelToInputs', 'render', 'updateSelectedAttachment', 'renderPreview' );
 519  
 520              if ( ! control.id_base ) {
 521                  _.find( component.controlConstructors, function( Constructor, idBase ) {
 522                      if ( control instanceof Constructor ) {
 523                          control.id_base = idBase;
 524                          return true;
 525                      }
 526                      return false;
 527                  });
 528                  if ( ! control.id_base ) {
 529                      throw new Error( 'Missing id_base.' );
 530                  }
 531              }
 532  
 533              // Track attributes needed to renderPreview in it's own model.
 534              control.previewTemplateProps = new Backbone.Model( control.mapModelToPreviewTemplateProps() );
 535  
 536              // Re-render the preview when the attachment changes.
 537              control.selectedAttachment = new wp.media.model.Attachment();
 538              control.renderPreview = _.debounce( control.renderPreview );
 539              control.listenTo( control.previewTemplateProps, 'change', control.renderPreview );
 540  
 541              // Make sure a copy of the selected attachment is always fetched.
 542              control.model.on( 'change:attachment_id', control.updateSelectedAttachment );
 543              control.model.on( 'change:url', control.updateSelectedAttachment );
 544              control.updateSelectedAttachment();
 545  
 546              /*
 547               * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
 548               * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
 549               * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
 550               */
 551              control.listenTo( control.model, 'change', control.syncModelToInputs );
 552              control.listenTo( control.model, 'change', control.syncModelToPreviewProps );
 553              control.listenTo( control.model, 'change', control.render );
 554  
 555              // Update the title.
 556              control.$el.on( 'input change', '.title', function updateTitle() {
 557                  control.model.set({
 558                      title: $.trim( $( this ).val() )
 559                  });
 560              });
 561  
 562              // Update link_url attribute.
 563              control.$el.on( 'input change', '.link', function updateLinkUrl() {
 564                  var linkUrl = $.trim( $( this ).val() ), linkType = 'custom';
 565                  if ( control.selectedAttachment.get( 'linkUrl' ) === linkUrl || control.selectedAttachment.get( 'link' ) === linkUrl ) {
 566                      linkType = 'post';
 567                  } else if ( control.selectedAttachment.get( 'url' ) === linkUrl ) {
 568                      linkType = 'file';
 569                  }
 570                  control.model.set( {
 571                      link_url: linkUrl,
 572                      link_type: linkType
 573                  });
 574  
 575                  // Update display settings for the next time the user opens to select from the media library.
 576                  control.displaySettings.set( {
 577                      link: linkType,
 578                      linkUrl: linkUrl
 579                  });
 580              });
 581  
 582              /*
 583               * Copy current display settings from the widget model to serve as basis
 584               * of customized display settings for the current media frame session.
 585               * Changes to display settings will be synced into this model, and
 586               * when a new selection is made, the settings from this will be synced
 587               * into that AttachmentDisplay's model to persist the setting changes.
 588               */
 589              control.displaySettings = new Backbone.Model( _.pick(
 590                  control.mapModelToMediaFrameProps(
 591                      _.extend( control.model.defaults(), control.model.toJSON() )
 592                  ),
 593                  _.keys( wp.media.view.settings.defaultProps )
 594              ) );
 595          },
 596  
 597          /**
 598           * Update the selected attachment if necessary.
 599           *
 600           * @returns {void}
 601           */
 602          updateSelectedAttachment: function updateSelectedAttachment() {
 603              var control = this, attachment;
 604  
 605              if ( 0 === control.model.get( 'attachment_id' ) ) {
 606                  control.selectedAttachment.clear();
 607                  control.model.set( 'error', false );
 608              } else if ( control.model.get( 'attachment_id' ) !== control.selectedAttachment.get( 'id' ) ) {
 609                  attachment = new wp.media.model.Attachment({
 610                      id: control.model.get( 'attachment_id' )
 611                  });
 612                  attachment.fetch()
 613                      .done( function done() {
 614                          control.model.set( 'error', false );
 615                          control.selectedAttachment.set( attachment.toJSON() );
 616                      })
 617                      .fail( function fail() {
 618                          control.model.set( 'error', 'missing_attachment' );
 619                      });
 620              }
 621          },
 622  
 623          /**
 624           * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
 625           *
 626           * @returns {void}
 627           */
 628          syncModelToPreviewProps: function syncModelToPreviewProps() {
 629              var control = this;
 630              control.previewTemplateProps.set( control.mapModelToPreviewTemplateProps() );
 631          },
 632  
 633          /**
 634           * Sync the model attributes to the hidden inputs, and update previewTemplateProps.
 635           *
 636           * @returns {void}
 637           */
 638          syncModelToInputs: function syncModelToInputs() {
 639              var control = this;
 640              control.syncContainer.find( '.media-widget-instance-property' ).each( function() {
 641                  var input = $( this ), value, propertyName;
 642                  propertyName = input.data( 'property' );
 643                  value = control.model.get( propertyName );
 644                  if ( _.isUndefined( value ) ) {
 645                      return;
 646                  }
 647  
 648                  if ( 'array' === control.model.schema[ propertyName ].type && _.isArray( value ) ) {
 649                      value = value.join( ',' );
 650                  } else if ( 'boolean' === control.model.schema[ propertyName ].type ) {
 651                      value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''.
 652                  } else {
 653                      value = String( value );
 654                  }
 655  
 656                  if ( input.val() !== value ) {
 657                      input.val( value );
 658                      input.trigger( 'change' );
 659                  }
 660              });
 661          },
 662  
 663          /**
 664           * Get template.
 665           *
 666           * @returns {Function} Template.
 667           */
 668          template: function template() {
 669              var control = this;
 670              if ( ! $( '#tmpl-widget-media-' + control.id_base + '-control' ).length ) {
 671                  throw new Error( 'Missing widget control template for ' + control.id_base );
 672              }
 673              return wp.template( 'widget-media-' + control.id_base + '-control' );
 674          },
 675  
 676          /**
 677           * Render template.
 678           *
 679           * @returns {void}
 680           */
 681          render: function render() {
 682              var control = this, titleInput;
 683  
 684              if ( ! control.templateRendered ) {
 685                  control.$el.html( control.template()( control.model.toJSON() ) );
 686                  control.renderPreview(); // Hereafter it will re-render when control.selectedAttachment changes.
 687                  control.templateRendered = true;
 688              }
 689  
 690              titleInput = control.$el.find( '.title' );
 691              if ( ! titleInput.is( document.activeElement ) ) {
 692                  titleInput.val( control.model.get( 'title' ) );
 693              }
 694  
 695              control.$el.toggleClass( 'selected', control.isSelected() );
 696          },
 697  
 698          /**
 699           * Render media preview.
 700           *
 701           * @abstract
 702           * @returns {void}
 703           */
 704          renderPreview: function renderPreview() {
 705              throw new Error( 'renderPreview must be implemented' );
 706          },
 707  
 708          /**
 709           * Whether a media item is selected.
 710           *
 711           * @returns {boolean} Whether selected and no error.
 712           */
 713          isSelected: function isSelected() {
 714              var control = this;
 715  
 716              if ( control.model.get( 'error' ) ) {
 717                  return false;
 718              }
 719  
 720              return Boolean( control.model.get( 'attachment_id' ) || control.model.get( 'url' ) );
 721          },
 722  
 723          /**
 724           * Handle click on link to Media Library to open modal, such as the link that appears when in the missing attachment error notice.
 725           *
 726           * @param {jQuery.Event} event - Event.
 727           * @returns {void}
 728           */
 729          handleMediaLibraryLinkClick: function handleMediaLibraryLinkClick( event ) {
 730              var control = this;
 731              event.preventDefault();
 732              control.selectMedia();
 733          },
 734  
 735          /**
 736           * Open the media select frame to chose an item.
 737           *
 738           * @returns {void}
 739           */
 740          selectMedia: function selectMedia() {
 741              var control = this, selection, mediaFrame, defaultSync, mediaFrameProps, selectionModels = [];
 742  
 743              if ( control.isSelected() && 0 !== control.model.get( 'attachment_id' ) ) {
 744                  selectionModels.push( control.selectedAttachment );
 745              }
 746  
 747              selection = new wp.media.model.Selection( selectionModels, { multiple: false } );
 748  
 749              mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() );
 750              if ( mediaFrameProps.size ) {
 751                  control.displaySettings.set( 'size', mediaFrameProps.size );
 752              }
 753  
 754              mediaFrame = new component.MediaFrameSelect({
 755                  title: control.l10n.add_media,
 756                  frame: 'post',
 757                  text: control.l10n.add_to_widget,
 758                  selection: selection,
 759                  mimeType: control.mime_type,
 760                  selectedDisplaySettings: control.displaySettings,
 761                  showDisplaySettings: control.showDisplaySettings,
 762                  metadata: mediaFrameProps,
 763                  state: control.isSelected() && 0 === control.model.get( 'attachment_id' ) ? 'embed' : 'insert',
 764                  invalidEmbedTypeError: control.l10n.unsupported_file_type
 765              });
 766              wp.media.frame = mediaFrame; // See wp.media().
 767  
 768              // Handle selection of a media item.
 769              mediaFrame.on( 'insert', function onInsert() {
 770                  var attachment = {}, state = mediaFrame.state();
 771  
 772                  // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview.
 773                  if ( 'embed' === state.get( 'id' ) ) {
 774                      _.extend( attachment, { id: 0 }, state.props.toJSON() );
 775                  } else {
 776                      _.extend( attachment, state.get( 'selection' ).first().toJSON() );
 777                  }
 778  
 779                  control.selectedAttachment.set( attachment );
 780                  control.model.set( 'error', false );
 781  
 782                  // Update widget instance.
 783                  control.model.set( control.getModelPropsFromMediaFrame( mediaFrame ) );
 784              });
 785  
 786              // Disable syncing of attachment changes back to server (except for deletions). See <https://core.trac.wordpress.org/ticket/40403>.
 787              defaultSync = wp.media.model.Attachment.prototype.sync;
 788              wp.media.model.Attachment.prototype.sync = function( method ) {
 789                  if ( 'delete' === method ) {
 790                      return defaultSync.apply( this, arguments );
 791                  } else {
 792                      return $.Deferred().rejectWith( this ).promise();
 793                  }
 794              };
 795              mediaFrame.on( 'close', function onClose() {
 796                  wp.media.model.Attachment.prototype.sync = defaultSync;
 797              });
 798  
 799              mediaFrame.$el.addClass( 'media-widget' );
 800              mediaFrame.open();
 801  
 802              // Clear the selected attachment when it is deleted in the media select frame.
 803              if ( selection ) {
 804                  selection.on( 'destroy', function onDestroy( attachment ) {
 805                      if ( control.model.get( 'attachment_id' ) === attachment.get( 'id' ) ) {
 806                          control.model.set({
 807                              attachment_id: 0,
 808                              url: ''
 809                          });
 810                      }
 811                  });
 812              }
 813  
 814              /*
 815               * Make sure focus is set inside of modal so that hitting Esc will close
 816               * the modal and not inadvertently cause the widget to collapse in the customizer.
 817               */
 818              mediaFrame.$el.find( '.media-frame-menu .media-menu-item.active' ).focus();
 819          },
 820  
 821          /**
 822           * Get the instance props from the media selection frame.
 823           *
 824           * @param {wp.media.view.MediaFrame.Select} mediaFrame - Select frame.
 825           * @returns {Object} Props.
 826           */
 827          getModelPropsFromMediaFrame: function getModelPropsFromMediaFrame( mediaFrame ) {
 828              var control = this, state, mediaFrameProps, modelProps;
 829  
 830              state = mediaFrame.state();
 831              if ( 'insert' === state.get( 'id' ) ) {
 832                  mediaFrameProps = state.get( 'selection' ).first().toJSON();
 833                  mediaFrameProps.postUrl = mediaFrameProps.link;
 834  
 835                  if ( control.showDisplaySettings ) {
 836                      _.extend(
 837                          mediaFrameProps,
 838                          mediaFrame.content.get( '.attachments-browser' ).sidebar.get( 'display' ).model.toJSON()
 839                      );
 840                  }
 841                  if ( mediaFrameProps.sizes && mediaFrameProps.size && mediaFrameProps.sizes[ mediaFrameProps.size ] ) {
 842                      mediaFrameProps.url = mediaFrameProps.sizes[ mediaFrameProps.size ].url;
 843                  }
 844              } else if ( 'embed' === state.get( 'id' ) ) {
 845                  mediaFrameProps = _.extend(
 846                      state.props.toJSON(),
 847                      { attachment_id: 0 }, // Because some media frames use `attachment_id` not `id`.
 848                      control.model.getEmbedResetProps()
 849                  );
 850              } else {
 851                  throw new Error( 'Unexpected state: ' + state.get( 'id' ) );
 852              }
 853  
 854              if ( mediaFrameProps.id ) {
 855                  mediaFrameProps.attachment_id = mediaFrameProps.id;
 856              }
 857  
 858              modelProps = control.mapMediaToModelProps( mediaFrameProps );
 859  
 860              // Clear the extension prop so sources will be reset for video and audio media.
 861              _.each( wp.media.view.settings.embedExts, function( ext ) {
 862                  if ( ext in control.model.schema && modelProps.url !== modelProps[ ext ] ) {
 863                      modelProps[ ext ] = '';
 864                  }
 865              });
 866  
 867              return modelProps;
 868          },
 869  
 870          /**
 871           * Map media frame props to model props.
 872           *
 873           * @param {Object} mediaFrameProps - Media frame props.
 874           * @returns {Object} Model props.
 875           */
 876          mapMediaToModelProps: function mapMediaToModelProps( mediaFrameProps ) {
 877              var control = this, mediaFramePropToModelPropMap = {}, modelProps = {}, extension;
 878              _.each( control.model.schema, function( fieldSchema, modelProp ) {
 879  
 880                  // Ignore widget title attribute.
 881                  if ( 'title' === modelProp ) {
 882                      return;
 883                  }
 884                  mediaFramePropToModelPropMap[ fieldSchema.media_prop || modelProp ] = modelProp;
 885              });
 886  
 887              _.each( mediaFrameProps, function( value, mediaProp ) {
 888                  var propName = mediaFramePropToModelPropMap[ mediaProp ] || mediaProp;
 889                  if ( control.model.schema[ propName ] ) {
 890                      modelProps[ propName ] = value;
 891                  }
 892              });
 893  
 894              if ( 'custom' === mediaFrameProps.size ) {
 895                  modelProps.width = mediaFrameProps.customWidth;
 896                  modelProps.height = mediaFrameProps.customHeight;
 897              }
 898  
 899              if ( 'post' === mediaFrameProps.link ) {
 900                  modelProps.link_url = mediaFrameProps.postUrl || mediaFrameProps.linkUrl;
 901              } else if ( 'file' === mediaFrameProps.link ) {
 902                  modelProps.link_url = mediaFrameProps.url;
 903              }
 904  
 905              // Because some media frames use `id` instead of `attachment_id`.
 906              if ( ! mediaFrameProps.attachment_id && mediaFrameProps.id ) {
 907                  modelProps.attachment_id = mediaFrameProps.id;
 908              }
 909  
 910              if ( mediaFrameProps.url ) {
 911                  extension = mediaFrameProps.url.replace( /#.*$/, '' ).replace( /\?.*$/, '' ).split( '.' ).pop().toLowerCase();
 912                  if ( extension in control.model.schema ) {
 913                      modelProps[ extension ] = mediaFrameProps.url;
 914                  }
 915              }
 916  
 917              // Always omit the titles derived from mediaFrameProps.
 918              return _.omit( modelProps, 'title' );
 919          },
 920  
 921          /**
 922           * Map model props to media frame props.
 923           *
 924           * @param {Object} modelProps - Model props.
 925           * @returns {Object} Media frame props.
 926           */
 927          mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) {
 928              var control = this, mediaFrameProps = {};
 929  
 930              _.each( modelProps, function( value, modelProp ) {
 931                  var fieldSchema = control.model.schema[ modelProp ] || {};
 932                  mediaFrameProps[ fieldSchema.media_prop || modelProp ] = value;
 933              });
 934  
 935              // Some media frames use attachment_id.
 936              mediaFrameProps.attachment_id = mediaFrameProps.id;
 937  
 938              if ( 'custom' === mediaFrameProps.size ) {
 939                  mediaFrameProps.customWidth = control.model.get( 'width' );
 940                  mediaFrameProps.customHeight = control.model.get( 'height' );
 941              }
 942  
 943              return mediaFrameProps;
 944          },
 945  
 946          /**
 947           * Map model props to previewTemplateProps.
 948           *
 949           * @returns {Object} Preview Template Props.
 950           */
 951          mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() {
 952              var control = this, previewTemplateProps = {};
 953              _.each( control.model.schema, function( value, prop ) {
 954                  if ( ! value.hasOwnProperty( 'should_preview_update' ) || value.should_preview_update ) {
 955                      previewTemplateProps[ prop ] = control.model.get( prop );
 956                  }
 957              });
 958  
 959              // Templates need to be aware of the error.
 960              previewTemplateProps.error = control.model.get( 'error' );
 961              return previewTemplateProps;
 962          },
 963  
 964          /**
 965           * Open the media frame to modify the selected item.
 966           *
 967           * @abstract
 968           * @returns {void}
 969           */
 970          editMedia: function editMedia() {
 971              throw new Error( 'editMedia not implemented' );
 972          }
 973      });
 974  
 975      /**
 976       * Media widget model.
 977       *
 978       * @class    wp.mediaWidgets.MediaWidgetModel
 979       * @augments Backbone.Model
 980       */
 981      component.MediaWidgetModel = Backbone.Model.extend(/** @lends wp.mediaWidgets.MediaWidgetModel.prototype */{
 982  
 983          /**
 984           * Id attribute.
 985           *
 986           * @type {string}
 987           */
 988          idAttribute: 'widget_id',
 989  
 990          /**
 991           * Instance schema.
 992           *
 993           * This adheres to JSON Schema and subclasses should have their schema
 994           * exported from PHP to JS such as is done in WP_Widget_Media_Image::enqueue_admin_scripts().
 995           *
 996           * @type {Object.<string, Object>}
 997           */
 998          schema: {
 999              title: {
1000                  type: 'string',
1001                  'default': ''
1002              },
1003              attachment_id: {
1004                  type: 'integer',
1005                  'default': 0
1006              },
1007              url: {
1008                  type: 'string',
1009                  'default': ''
1010              }
1011          },
1012  
1013          /**
1014           * Get default attribute values.
1015           *
1016           * @returns {Object} Mapping of property names to their default values.
1017           */
1018          defaults: function() {
1019              var defaults = {};
1020              _.each( this.schema, function( fieldSchema, field ) {
1021                  defaults[ field ] = fieldSchema['default'];
1022              });
1023              return defaults;
1024          },
1025  
1026          /**
1027           * Set attribute value(s).
1028           *
1029           * This is a wrapped version of Backbone.Model#set() which allows us to
1030           * cast the attribute values from the hidden inputs' string values into
1031           * the appropriate data types (integers or booleans).
1032           *
1033           * @param {string|Object} key - Attribute name or attribute pairs.
1034           * @param {mixed|Object}  [val] - Attribute value or options object.
1035           * @param {Object}        [options] - Options when attribute name and value are passed separately.
1036           * @returns {wp.mediaWidgets.MediaWidgetModel} This model.
1037           */
1038          set: function set( key, val, options ) {
1039              var model = this, attrs, opts, castedAttrs; // eslint-disable-line consistent-this
1040              if ( null === key ) {
1041                  return model;
1042              }
1043              if ( 'object' === typeof key ) {
1044                  attrs = key;
1045                  opts = val;
1046              } else {
1047                  attrs = {};
1048                  attrs[ key ] = val;
1049                  opts = options;
1050              }
1051  
1052              castedAttrs = {};
1053              _.each( attrs, function( value, name ) {
1054                  var type;
1055                  if ( ! model.schema[ name ] ) {
1056                      castedAttrs[ name ] = value;
1057                      return;
1058                  }
1059                  type = model.schema[ name ].type;
1060                  if ( 'array' === type ) {
1061                      castedAttrs[ name ] = value;
1062                      if ( ! _.isArray( castedAttrs[ name ] ) ) {
1063                          castedAttrs[ name ] = castedAttrs[ name ].split( /,/ ); // Good enough for parsing an ID list.
1064                      }
1065                      if ( model.schema[ name ].items && 'integer' === model.schema[ name ].items.type ) {
1066                          castedAttrs[ name ] = _.filter(
1067                              _.map( castedAttrs[ name ], function( id ) {
1068                                  return parseInt( id, 10 );
1069                              },
1070                              function( id ) {
1071                                  return 'number' === typeof id;
1072                              }
1073                          ) );
1074                      }
1075                  } else if ( 'integer' === type ) {
1076                      castedAttrs[ name ] = parseInt( value, 10 );
1077                  } else if ( 'boolean' === type ) {
1078                      castedAttrs[ name ] = ! ( ! value || '0' === value || 'false' === value );
1079                  } else {
1080                      castedAttrs[ name ] = value;
1081                  }
1082              });
1083  
1084              return Backbone.Model.prototype.set.call( this, castedAttrs, opts );
1085          },
1086  
1087          /**
1088           * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment).
1089           *
1090           * @returns {Object} Reset/override props.
1091           */
1092          getEmbedResetProps: function getEmbedResetProps() {
1093              return {
1094                  id: 0
1095              };
1096          }
1097      });
1098  
1099      /**
1100       * Collection of all widget model instances.
1101       *
1102       * @memberOf wp.mediaWidgets
1103       *
1104       * @type {Backbone.Collection}
1105       */
1106      component.modelCollection = new ( Backbone.Collection.extend( {
1107          model: component.MediaWidgetModel
1108      }) )();
1109  
1110      /**
1111       * Mapping of widget ID to instances of MediaWidgetControl subclasses.
1112       *
1113       * @memberOf wp.mediaWidgets
1114       *
1115       * @type {Object.<string, wp.mediaWidgets.MediaWidgetControl>}
1116       */
1117      component.widgetControls = {};
1118  
1119      /**
1120       * Handle widget being added or initialized for the first time at the widget-added event.
1121       *
1122       * @memberOf wp.mediaWidgets
1123       *
1124       * @param {jQuery.Event} event - Event.
1125       * @param {jQuery}       widgetContainer - Widget container element.
1126       *
1127       * @returns {void}
1128       */
1129      component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) {
1130          var fieldContainer, syncContainer, widgetForm, idBase, ControlConstructor, ModelConstructor, modelAttributes, widgetControl, widgetModel, widgetId, animatedCheckDelay = 50, renderWhenAnimationDone;
1131          widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen.
1132          idBase = widgetForm.find( '> .id_base' ).val();
1133          widgetId = widgetForm.find( '> .widget-id' ).val();
1134  
1135          // Prevent initializing already-added widgets.
1136          if ( component.widgetControls[ widgetId ] ) {
1137              return;
1138          }
1139  
1140          ControlConstructor = component.controlConstructors[ idBase ];
1141          if ( ! ControlConstructor ) {
1142              return;
1143          }
1144  
1145          ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
1146  
1147          /*
1148           * Create a container element for the widget control (Backbone.View).
1149           * This is inserted into the DOM immediately before the .widget-content
1150           * element because the contents of this element are essentially "managed"
1151           * by PHP, where each widget update cause the entire element to be emptied
1152           * and replaced with the rendered output of WP_Widget::form() which is
1153           * sent back in Ajax request made to save/update the widget instance.
1154           * To prevent a "flash of replaced DOM elements and re-initialized JS
1155           * components", the JS template is rendered outside of the normal form
1156           * container.
1157           */
1158          fieldContainer = $( '<div></div>' );
1159          syncContainer = widgetContainer.find( '.widget-content:first' );
1160          syncContainer.before( fieldContainer );
1161  
1162          /*
1163           * Sync the widget instance model attributes onto the hidden inputs that widgets currently use to store the state.
1164           * In the future, when widgets are JS-driven, the underlying widget instance data should be exposed as a model
1165           * from the start, without having to sync with hidden fields. See <https://core.trac.wordpress.org/ticket/33507>.
1166           */
1167          modelAttributes = {};
1168          syncContainer.find( '.media-widget-instance-property' ).each( function() {
1169              var input = $( this );
1170              modelAttributes[ input.data( 'property' ) ] = input.val();
1171          });
1172          modelAttributes.widget_id = widgetId;
1173  
1174          widgetModel = new ModelConstructor( modelAttributes );
1175  
1176          widgetControl = new ControlConstructor({
1177              el: fieldContainer,
1178              syncContainer: syncContainer,
1179              model: widgetModel
1180          });
1181  
1182          /*
1183           * Render the widget once the widget parent's container finishes animating,
1184           * as the widget-added event fires with a slideDown of the container.
1185           * This ensures that the container's dimensions are fixed so that ME.js
1186           * can initialize with the proper dimensions.
1187           */
1188          renderWhenAnimationDone = function() {
1189              if ( ! widgetContainer.hasClass( 'open' ) ) {
1190                  setTimeout( renderWhenAnimationDone, animatedCheckDelay );
1191              } else {
1192                  widgetControl.render();
1193              }
1194          };
1195          renderWhenAnimationDone();
1196  
1197          /*
1198           * Note that the model and control currently won't ever get garbage-collected
1199           * when a widget gets removed/deleted because there is no widget-removed event.
1200           */
1201          component.modelCollection.add( [ widgetModel ] );
1202          component.widgetControls[ widgetModel.get( 'widget_id' ) ] = widgetControl;
1203      };
1204  
1205      /**
1206       * Setup widget in accessibility mode.
1207       *
1208       * @memberOf wp.mediaWidgets
1209       *
1210       * @returns {void}
1211       */
1212      component.setupAccessibleMode = function setupAccessibleMode() {
1213          var widgetForm, widgetId, idBase, widgetControl, ControlConstructor, ModelConstructor, modelAttributes, fieldContainer, syncContainer;
1214          widgetForm = $( '.editwidget > form' );
1215          if ( 0 === widgetForm.length ) {
1216              return;
1217          }
1218  
1219          idBase = widgetForm.find( '> .widget-control-actions > .id_base' ).val();
1220  
1221          ControlConstructor = component.controlConstructors[ idBase ];
1222          if ( ! ControlConstructor ) {
1223              return;
1224          }
1225  
1226          widgetId = widgetForm.find( '> .widget-control-actions > .widget-id' ).val();
1227  
1228          ModelConstructor = component.modelConstructors[ idBase ] || component.MediaWidgetModel;
1229          fieldContainer = $( '<div></div>' );
1230          syncContainer = widgetForm.find( '> .widget-inside' );
1231          syncContainer.before( fieldContainer );
1232  
1233          modelAttributes = {};
1234          syncContainer.find( '.media-widget-instance-property' ).each( function() {
1235              var input = $( this );
1236              modelAttributes[ input.data( 'property' ) ] = input.val();
1237          });
1238          modelAttributes.widget_id = widgetId;
1239  
1240          widgetControl = new ControlConstructor({
1241              el: fieldContainer,
1242              syncContainer: syncContainer,
1243              model: new ModelConstructor( modelAttributes )
1244          });
1245  
1246          component.modelCollection.add( [ widgetControl.model ] );
1247          component.widgetControls[ widgetControl.model.get( 'widget_id' ) ] = widgetControl;
1248  
1249          widgetControl.render();
1250      };
1251  
1252      /**
1253       * Sync widget instance data sanitized from server back onto widget model.
1254       *
1255       * This gets called via the 'widget-updated' event when saving a widget from
1256       * the widgets admin screen and also via the 'widget-synced' event when making
1257       * a change to a widget in the customizer.
1258       *
1259       * @memberOf wp.mediaWidgets
1260       *
1261       * @param {jQuery.Event} event - Event.
1262       * @param {jQuery}       widgetContainer - Widget container element.
1263       *
1264       * @returns {void}
1265       */
1266      component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) {
1267          var widgetForm, widgetContent, widgetId, widgetControl, attributes = {};
1268          widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' );
1269          widgetId = widgetForm.find( '> .widget-id' ).val();
1270  
1271          widgetControl = component.widgetControls[ widgetId ];
1272          if ( ! widgetControl ) {
1273              return;
1274          }
1275  
1276          // Make sure the server-sanitized values get synced back into the model.
1277          widgetContent = widgetForm.find( '> .widget-content' );
1278          widgetContent.find( '.media-widget-instance-property' ).each( function() {
1279              var property = $( this ).data( 'property' );
1280              attributes[ property ] = $( this ).val();
1281          });
1282  
1283          // Suspend syncing model back to inputs when syncing from inputs to model, preventing infinite loop.
1284          widgetControl.stopListening( widgetControl.model, 'change', widgetControl.syncModelToInputs );
1285          widgetControl.model.set( attributes );
1286          widgetControl.listenTo( widgetControl.model, 'change', widgetControl.syncModelToInputs );
1287      };
1288  
1289      /**
1290       * Initialize functionality.
1291       *
1292       * This function exists to prevent the JS file from having to boot itself.
1293       * When WordPress enqueues this script, it should have an inline script
1294       * attached which calls wp.mediaWidgets.init().
1295       *
1296       * @memberOf wp.mediaWidgets
1297       *
1298       * @returns {void}
1299       */
1300      component.init = function init() {
1301          var $document = $( document );
1302          $document.on( 'widget-added', component.handleWidgetAdded );
1303          $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated );
1304  
1305          /*
1306           * Manually trigger widget-added events for media widgets on the admin
1307           * screen once they are expanded. The widget-added event is not triggered
1308           * for each pre-existing widget on the widgets admin screen like it is
1309           * on the customizer. Likewise, the customizer only triggers widget-added
1310           * when the widget is expanded to just-in-time construct the widget form
1311           * when it is actually going to be displayed. So the following implements
1312           * the same for the widgets admin screen, to invoke the widget-added
1313           * handler when a pre-existing media widget is expanded.
1314           */
1315          $( function initializeExistingWidgetContainers() {
1316              var widgetContainers;
1317              if ( 'widgets' !== window.pagenow ) {
1318                  return;
1319              }
1320              widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' );
1321              widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() {
1322                  var widgetContainer = $( this );
1323                  component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer );
1324              });
1325  
1326              // Accessibility mode.
1327              $( window ).on( 'load', function() {
1328                  component.setupAccessibleMode();
1329              });
1330          });
1331      };
1332  
1333      return component;
1334  })( jQuery );


Generated: Tue Sep 17 01:00:03 2019 Cross-referenced by PHPXref 0.7.1