home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-admin / js / widgets / media-widgets.js < prev    next >
Encoding:
JavaScript  |  2017-10-19  |  40.9 KB  |  1,307 lines

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