Make WordPress Core

source: trunk/src/js/_enqueues/wp/widgets/media.js @ 58455

Last change on this file since 58455 was 58455, checked in by joedolson, 5 weeks ago

Administration: A11y: Add role="alert" on JS injected admin notices.

Add the attribute role="alert" on 12 instances of admin notices that are injected into the DOM using JavaScript. The role="alert" attribute allows screen readers to recognize the addition to the DOM and announce the errors to users.

Props afercia, cyrus11, rcreators, joedolson.
Fixes #47111.

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