]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/media-views.js
WordPress 4.0.1
[autoinstalls/wordpress.git] / wp-includes / js / media-views.js
1 /* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */
2 ( function( $, _ ) {
3         var l10n,
4                 media = wp.media,
5                 isTouchDevice = ( 'ontouchend' in document );
6
7         // Link any localized strings.
8         l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
9
10         // Link any settings.
11         media.view.settings = l10n.settings || {};
12         delete l10n.settings;
13
14         // Copy the `post` setting over to the model settings.
15         media.model.settings.post = media.view.settings.post;
16
17         // Check if the browser supports CSS 3.0 transitions
18         $.support.transition = (function(){
19                 var style = document.documentElement.style,
20                         transitions = {
21                                 WebkitTransition: 'webkitTransitionEnd',
22                                 MozTransition:    'transitionend',
23                                 OTransition:      'oTransitionEnd otransitionend',
24                                 transition:       'transitionend'
25                         }, transition;
26
27                 transition = _.find( _.keys( transitions ), function( transition ) {
28                         return ! _.isUndefined( style[ transition ] );
29                 });
30
31                 return transition && {
32                         end: transitions[ transition ]
33                 };
34         }());
35
36         /**
37          * A shared event bus used to provide events into
38          * the media workflows that 3rd-party devs can use to hook
39          * in.
40          */
41         media.events = _.extend( {}, Backbone.Events );
42
43         /**
44          * Makes it easier to bind events using transitions.
45          *
46          * @param {string} selector
47          * @param {Number} sensitivity
48          * @returns {Promise}
49          */
50         media.transition = function( selector, sensitivity ) {
51                 var deferred = $.Deferred();
52
53                 sensitivity = sensitivity || 2000;
54
55                 if ( $.support.transition ) {
56                         if ( ! (selector instanceof $) ) {
57                                 selector = $( selector );
58                         }
59
60                         // Resolve the deferred when the first element finishes animating.
61                         selector.first().one( $.support.transition.end, deferred.resolve );
62
63                         // Just in case the event doesn't trigger, fire a callback.
64                         _.delay( deferred.resolve, sensitivity );
65
66                 // Otherwise, execute on the spot.
67                 } else {
68                         deferred.resolve();
69                 }
70
71                 return deferred.promise();
72         };
73
74         /**
75          * ========================================================================
76          * CONTROLLERS
77          * ========================================================================
78          */
79
80         /**
81          * wp.media.controller.Region
82          *
83          * @constructor
84          * @augments Backbone.Model
85          *
86          * @param {Object} [options={}]
87          */
88         media.controller.Region = function( options ) {
89                 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
90         };
91
92         // Use Backbone's self-propagating `extend` inheritance method.
93         media.controller.Region.extend = Backbone.Model.extend;
94
95         _.extend( media.controller.Region.prototype, {
96                 /**
97                  * Activate a mode.
98                  *
99                  * @param {string} mode
100                  *
101                  * @fires this.view#{this.id}:activate:{this._mode}
102                  * @fires this.view#{this.id}:activate
103                  * @fires this.view#{this.id}:deactivate:{this._mode}
104                  * @fires this.view#{this.id}:deactivate
105                  *
106                  * @returns {wp.media.controller.Region} Returns itself to allow chaining.
107                  */
108                 mode: function( mode ) {
109                         if ( ! mode ) {
110                                 return this._mode;
111                         }
112                         // Bail if we're trying to change to the current mode.
113                         if ( mode === this._mode ) {
114                                 return this;
115                         }
116
117                         /**
118                          * Region mode deactivation event.
119                          *
120                          * @event this.view#{this.id}:deactivate:{this._mode}
121                          * @event this.view#{this.id}:deactivate
122                          */
123                         this.trigger('deactivate');
124
125                         this._mode = mode;
126                         this.render( mode );
127
128                         /**
129                          * Region mode activation event.
130                          *
131                          * @event this.view#{this.id}:activate:{this._mode}
132                          * @event this.view#{this.id}:activate
133                          */
134                         this.trigger('activate');
135                         return this;
136                 },
137                 /**
138                  * Render a mode.
139                  *
140                  * @param {string} mode
141                  *
142                  * @fires this.view#{this.id}:create:{this._mode}
143                  * @fires this.view#{this.id}:create
144                  * @fires this.view#{this.id}:render:{this._mode}
145                  * @fires this.view#{this.id}:render
146                  *
147                  * @returns {wp.media.controller.Region} Returns itself to allow chaining
148                  */
149                 render: function( mode ) {
150                         // If the mode isn't active, activate it.
151                         if ( mode && mode !== this._mode ) {
152                                 return this.mode( mode );
153                         }
154
155                         var set = { view: null },
156                                 view;
157
158                         /**
159                          * Create region view event.
160                          *
161                          * Region view creation takes place in an event callback on the frame.
162                          *
163                          * @event this.view#{this.id}:create:{this._mode}
164                          * @event this.view#{this.id}:create
165                          */
166                         this.trigger( 'create', set );
167                         view = set.view;
168
169                         /**
170                          * Render region view event.
171                          *
172                          * Region view creation takes place in an event callback on the frame.
173                          *
174                          * @event this.view#{this.id}:create:{this._mode}
175                          * @event this.view#{this.id}:create
176                          */
177                         this.trigger( 'render', view );
178                         if ( view ) {
179                                 this.set( view );
180                         }
181                         return this;
182                 },
183
184                 /**
185                  * Get the region's view.
186                  *
187                  * @returns {wp.media.View}
188                  */
189                 get: function() {
190                         return this.view.views.first( this.selector );
191                 },
192
193                 /**
194                  * Set the region's view as a subview of the frame.
195                  *
196                  * @param {Array|Object} views
197                  * @param {Object} [options={}]
198                  * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining
199                  */
200                 set: function( views, options ) {
201                         if ( options ) {
202                                 options.add = false;
203                         }
204                         return this.view.views.set( this.selector, views, options );
205                 },
206
207                 /**
208                  * Trigger regional view events on the frame.
209                  *
210                  * @param {string} event
211                  * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining.
212                  */
213                 trigger: function( event ) {
214                         var base, args;
215
216                         if ( ! this._mode ) {
217                                 return;
218                         }
219
220                         args = _.toArray( arguments );
221                         base = this.id + ':' + event;
222
223                         // Trigger `{this.id}:{event}:{this._mode}` event on the frame.
224                         args[0] = base + ':' + this._mode;
225                         this.view.trigger.apply( this.view, args );
226
227                         // Trigger `{this.id}:{event}` event on the frame.
228                         args[0] = base;
229                         this.view.trigger.apply( this.view, args );
230                         return this;
231                 }
232         });
233
234         /**
235          * wp.media.controller.StateMachine
236          *
237          * @constructor
238          * @augments Backbone.Model
239          * @mixin
240          * @mixes Backbone.Events
241          *
242          * @param {Array} states
243          */
244         media.controller.StateMachine = function( states ) {
245                 this.states = new Backbone.Collection( states );
246         };
247
248         // Use Backbone's self-propagating `extend` inheritance method.
249         media.controller.StateMachine.extend = Backbone.Model.extend;
250
251         _.extend( media.controller.StateMachine.prototype, Backbone.Events, {
252                 /**
253                  * Fetch a state.
254                  *
255                  * If no `id` is provided, returns the active state.
256                  *
257                  * Implicitly creates states.
258                  *
259                  * Ensure that the `states` collection exists so the `StateMachine`
260                  *   can be used as a mixin.
261                  *
262                  * @param {string} id
263                  * @returns {wp.media.controller.State} Returns a State model
264                  *   from the StateMachine collection
265                  */
266                 state: function( id ) {
267                         this.states = this.states || new Backbone.Collection();
268
269                         // Default to the active state.
270                         id = id || this._state;
271
272                         if ( id && ! this.states.get( id ) ) {
273                                 this.states.add({ id: id });
274                         }
275                         return this.states.get( id );
276                 },
277
278                 /**
279                  * Sets the active state.
280                  *
281                  * Bail if we're trying to select the current state, if we haven't
282                  * created the `states` collection, or are trying to select a state
283                  * that does not exist.
284                  *
285                  * @param {string} id
286                  *
287                  * @fires wp.media.controller.State#deactivate
288                  * @fires wp.media.controller.State#activate
289                  *
290                  * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining
291                  */
292                 setState: function( id ) {
293                         var previous = this.state();
294
295                         if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) {
296                                 return this;
297                         }
298
299                         if ( previous ) {
300                                 previous.trigger('deactivate');
301                                 this._lastState = previous.id;
302                         }
303
304                         this._state = id;
305                         this.state().trigger('activate');
306
307                         return this;
308                 },
309
310                 /**
311                  * Returns the previous active state.
312                  *
313                  * Call the `state()` method with no parameters to retrieve the current
314                  * active state.
315                  *
316                  * @returns {wp.media.controller.State} Returns a State model
317                  *    from the StateMachine collection
318                  */
319                 lastState: function() {
320                         if ( this._lastState ) {
321                                 return this.state( this._lastState );
322                         }
323                 }
324         });
325
326         // Map all event binding and triggering on a StateMachine to its `states` collection.
327         _.each([ 'on', 'off', 'trigger' ], function( method ) {
328                 /**
329                  * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining.
330                  */
331                 media.controller.StateMachine.prototype[ method ] = function() {
332                         // Ensure that the `states` collection exists so the `StateMachine`
333                         // can be used as a mixin.
334                         this.states = this.states || new Backbone.Collection();
335                         // Forward the method to the `states` collection.
336                         this.states[ method ].apply( this.states, arguments );
337                         return this;
338                 };
339         });
340
341         /**
342          * wp.media.controller.State
343          *
344          * A state is a step in a workflow that when set will trigger the controllers
345          * for the regions to be updated as specified in the frame. This is the base
346          * class that the various states used in wp.media extend.
347          *
348          * @constructor
349          * @augments Backbone.Model
350          */
351         media.controller.State = Backbone.Model.extend({
352                 constructor: function() {
353                         this.on( 'activate', this._preActivate, this );
354                         this.on( 'activate', this.activate, this );
355                         this.on( 'activate', this._postActivate, this );
356                         this.on( 'deactivate', this._deactivate, this );
357                         this.on( 'deactivate', this.deactivate, this );
358                         this.on( 'reset', this.reset, this );
359                         this.on( 'ready', this._ready, this );
360                         this.on( 'ready', this.ready, this );
361                         /**
362                          * Call parent constructor with passed arguments
363                          */
364                         Backbone.Model.apply( this, arguments );
365                         this.on( 'change:menu', this._updateMenu, this );
366                 },
367                 /**
368                  * @abstract
369                  */
370                 ready: function() {},
371                 /**
372                  * @abstract
373                  */
374                 activate: function() {},
375                 /**
376                  * @abstract
377                  */
378                 deactivate: function() {},
379                 /**
380                  * @abstract
381                  */
382                 reset: function() {},
383                 /**
384                  * @access private
385                  */
386                 _ready: function() {
387                         this._updateMenu();
388                 },
389                 /**
390                  * @access private
391                 */
392                 _preActivate: function() {
393                         this.active = true;
394                 },
395                 /**
396                  * @access private
397                  */
398                 _postActivate: function() {
399                         this.on( 'change:menu', this._menu, this );
400                         this.on( 'change:titleMode', this._title, this );
401                         this.on( 'change:content', this._content, this );
402                         this.on( 'change:toolbar', this._toolbar, this );
403
404                         this.frame.on( 'title:render:default', this._renderTitle, this );
405
406                         this._title();
407                         this._menu();
408                         this._toolbar();
409                         this._content();
410                         this._router();
411                 },
412                 /**
413                  * @access private
414                  */
415                 _deactivate: function() {
416                         this.active = false;
417
418                         this.frame.off( 'title:render:default', this._renderTitle, this );
419
420                         this.off( 'change:menu', this._menu, this );
421                         this.off( 'change:titleMode', this._title, this );
422                         this.off( 'change:content', this._content, this );
423                         this.off( 'change:toolbar', this._toolbar, this );
424                 },
425                 /**
426                  * @access private
427                  */
428                 _title: function() {
429                         this.frame.title.render( this.get('titleMode') || 'default' );
430                 },
431                 /**
432                  * @access private
433                  */
434                 _renderTitle: function( view ) {
435                         view.$el.text( this.get('title') || '' );
436                 },
437                 /**
438                  * @access private
439                  */
440                 _router: function() {
441                         var router = this.frame.router,
442                                 mode = this.get('router'),
443                                 view;
444
445                         this.frame.$el.toggleClass( 'hide-router', ! mode );
446                         if ( ! mode ) {
447                                 return;
448                         }
449
450                         this.frame.router.render( mode );
451
452                         view = router.get();
453                         if ( view && view.select ) {
454                                 view.select( this.frame.content.mode() );
455                         }
456                 },
457                 /**
458                  * @access private
459                  */
460                 _menu: function() {
461                         var menu = this.frame.menu,
462                                 mode = this.get('menu'),
463                                 view;
464
465                         this.frame.$el.toggleClass( 'hide-menu', ! mode );
466                         if ( ! mode ) {
467                                 return;
468                         }
469
470                         menu.mode( mode );
471
472                         view = menu.get();
473                         if ( view && view.select ) {
474                                 view.select( this.id );
475                         }
476                 },
477                 /**
478                  * @access private
479                  */
480                 _updateMenu: function() {
481                         var previous = this.previous('menu'),
482                                 menu = this.get('menu');
483
484                         if ( previous ) {
485                                 this.frame.off( 'menu:render:' + previous, this._renderMenu, this );
486                         }
487
488                         if ( menu ) {
489                                 this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
490                         }
491                 },
492                 /**
493                  * @access private
494                  */
495                 _renderMenu: function( view ) {
496                         var menuItem = this.get('menuItem'),
497                                 title = this.get('title'),
498                                 priority = this.get('priority');
499
500                         if ( ! menuItem && title ) {
501                                 menuItem = { text: title };
502
503                                 if ( priority ) {
504                                         menuItem.priority = priority;
505                                 }
506                         }
507
508                         if ( ! menuItem ) {
509                                 return;
510                         }
511
512                         view.set( this.id, menuItem );
513                 }
514         });
515
516         _.each(['toolbar','content'], function( region ) {
517                 /**
518                  * @access private
519                  */
520                 media.controller.State.prototype[ '_' + region ] = function() {
521                         var mode = this.get( region );
522                         if ( mode ) {
523                                 this.frame[ region ].render( mode );
524                         }
525                 };
526         });
527
528         media.selectionSync = {
529                 syncSelection: function() {
530                         var selection = this.get('selection'),
531                                 manager = this.frame._selection;
532
533                         if ( ! this.get('syncSelection') || ! manager || ! selection ) {
534                                 return;
535                         }
536
537                         // If the selection supports multiple items, validate the stored
538                         // attachments based on the new selection's conditions. Record
539                         // the attachments that are not included; we'll maintain a
540                         // reference to those. Other attachments are considered in flux.
541                         if ( selection.multiple ) {
542                                 selection.reset( [], { silent: true });
543                                 selection.validateAll( manager.attachments );
544                                 manager.difference = _.difference( manager.attachments.models, selection.models );
545                         }
546
547                         // Sync the selection's single item with the master.
548                         selection.single( manager.single );
549                 },
550
551                 /**
552                  * Record the currently active attachments, which is a combination
553                  * of the selection's attachments and the set of selected
554                  * attachments that this specific selection considered invalid.
555                  * Reset the difference and record the single attachment.
556                  */
557                 recordSelection: function() {
558                         var selection = this.get('selection'),
559                                 manager = this.frame._selection;
560
561                         if ( ! this.get('syncSelection') || ! manager || ! selection ) {
562                                 return;
563                         }
564
565                         if ( selection.multiple ) {
566                                 manager.attachments.reset( selection.toArray().concat( manager.difference ) );
567                                 manager.difference = [];
568                         } else {
569                                 manager.attachments.add( selection.toArray() );
570                         }
571
572                         manager.single = selection._single;
573                 }
574         };
575
576         /**
577          * A state for choosing an attachment from the media library.
578          *
579          * @constructor
580          * @augments wp.media.controller.State
581          * @augments Backbone.Model
582          */
583         media.controller.Library = media.controller.State.extend({
584                 defaults: {
585                         id:                 'library',
586                         title:              l10n.mediaLibraryTitle,
587                         // Selection defaults. @see media.model.Selection
588                         multiple:           false,
589                         // Initial region modes.
590                         content:            'upload',
591                         menu:               'default',
592                         router:             'browse',
593                         toolbar:            'select',
594                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
595                         searchable:         true,
596                         filterable:         false,
597                         sortable:           true,
598
599                         autoSelect:         true,
600                         describe:           false,
601                         // Uses a user setting to override the content mode.
602                         contentUserSetting: true,
603                         // Sync the selection from the last state when 'multiple' matches.
604                         syncSelection:      true
605                 },
606
607                 /**
608                  * If a library isn't provided, query all media items.
609                  * If a selection instance isn't provided, create one.
610                  */
611                 initialize: function() {
612                         var selection = this.get('selection'),
613                                 props;
614
615                         if ( ! this.get('library') ) {
616                                 this.set( 'library', media.query() );
617                         }
618
619                         if ( ! (selection instanceof media.model.Selection) ) {
620                                 props = selection;
621
622                                 if ( ! props ) {
623                                         props = this.get('library').props.toJSON();
624                                         props = _.omit( props, 'orderby', 'query' );
625                                 }
626
627                                 // If the `selection` attribute is set to an object,
628                                 // it will use those values as the selection instance's
629                                 // `props` model. Otherwise, it will copy the library's
630                                 // `props` model.
631                                 this.set( 'selection', new media.model.Selection( null, {
632                                         multiple: this.get('multiple'),
633                                         props: props
634                                 }) );
635                         }
636
637                         this.resetDisplays();
638                 },
639
640                 activate: function() {
641                         this.syncSelection();
642
643                         wp.Uploader.queue.on( 'add', this.uploading, this );
644
645                         this.get('selection').on( 'add remove reset', this.refreshContent, this );
646
647                         if ( this.get( 'router' ) && this.get('contentUserSetting') ) {
648                                 this.frame.on( 'content:activate', this.saveContentMode, this );
649                                 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
650                         }
651                 },
652
653                 deactivate: function() {
654                         this.recordSelection();
655
656                         this.frame.off( 'content:activate', this.saveContentMode, this );
657
658                         // Unbind all event handlers that use this state as the context
659                         // from the selection.
660                         this.get('selection').off( null, null, this );
661
662                         wp.Uploader.queue.off( null, null, this );
663                 },
664
665                 reset: function() {
666                         this.get('selection').reset();
667                         this.resetDisplays();
668                         this.refreshContent();
669                 },
670
671                 resetDisplays: function() {
672                         var defaultProps = media.view.settings.defaultProps;
673                         this._displays = [];
674                         this._defaultDisplaySettings = {
675                                 align: defaultProps.align || getUserSetting( 'align', 'none' ),
676                                 size:  defaultProps.size  || getUserSetting( 'imgsize', 'medium' ),
677                                 link:  defaultProps.link  || getUserSetting( 'urlbutton', 'file' )
678                         };
679                 },
680
681                 /**
682                  * @param {wp.media.model.Attachment} attachment
683                  * @returns {Backbone.Model}
684                  */
685                 display: function( attachment ) {
686                         var displays = this._displays;
687
688                         if ( ! displays[ attachment.cid ] ) {
689                                 displays[ attachment.cid ] = new Backbone.Model( this.defaultDisplaySettings( attachment ) );
690                         }
691                         return displays[ attachment.cid ];
692                 },
693
694                 /**
695                  * @param {wp.media.model.Attachment} attachment
696                  * @returns {Object}
697                  */
698                 defaultDisplaySettings: function( attachment ) {
699                         var settings = this._defaultDisplaySettings;
700                         if ( settings.canEmbed = this.canEmbed( attachment ) ) {
701                                 settings.link = 'embed';
702                         }
703                         return settings;
704                 },
705
706                 /**
707                  * @param {wp.media.model.Attachment} attachment
708                  * @returns {Boolean}
709                  */
710                 canEmbed: function( attachment ) {
711                         // If uploading, we know the filename but not the mime type.
712                         if ( ! attachment.get('uploading') ) {
713                                 var type = attachment.get('type');
714                                 if ( type !== 'audio' && type !== 'video' ) {
715                                         return false;
716                                 }
717                         }
718
719                         return _.contains( media.view.settings.embedExts, attachment.get('filename').split('.').pop() );
720                 },
721
722
723                 /**
724                  * If the state is active, no items are selected, and the current
725                  * content mode is not an option in the state's router (provided
726                  * the state has a router), reset the content mode to the default.
727                  */
728                 refreshContent: function() {
729                         var selection = this.get('selection'),
730                                 frame = this.frame,
731                                 router = frame.router.get(),
732                                 mode = frame.content.mode();
733
734                         if ( this.active && ! selection.length && router && ! router.get( mode ) ) {
735                                 this.frame.content.render( this.get('content') );
736                         }
737                 },
738
739                 /**
740                  * If the uploader was selected, navigate to the browser.
741                  *
742                  * Automatically select any uploading attachments.
743                  *
744                  * Selections that don't support multiple attachments automatically
745                  * limit themselves to one attachment (in this case, the last
746                  * attachment in the upload queue).
747                  *
748                  * @param {wp.media.model.Attachment} attachment
749                  */
750                 uploading: function( attachment ) {
751                         var content = this.frame.content;
752
753                         if ( 'upload' === content.mode() ) {
754                                 this.frame.content.mode('browse');
755                         }
756
757                         if ( this.get( 'autoSelect' ) ) {
758                                 this.get('selection').add( attachment );
759                                 this.frame.trigger( 'library:selection:add' );
760                         }
761                 },
762
763                 /**
764                  * Only track the browse router on library states.
765                  */
766                 saveContentMode: function() {
767                         if ( 'browse' !== this.get('router') ) {
768                                 return;
769                         }
770
771                         var mode = this.frame.content.mode(),
772                                 view = this.frame.router.get();
773
774                         if ( view && view.get( mode ) ) {
775                                 setUserSetting( 'libraryContent', mode );
776                         }
777                 }
778         });
779
780         _.extend( media.controller.Library.prototype, media.selectionSync );
781
782         /**
783          * A state for editing the settings of an image within a content editor.
784          *
785          * @constructor
786          * @augments wp.media.controller.State
787          * @augments Backbone.Model
788          */
789         media.controller.ImageDetails = media.controller.State.extend({
790                 defaults: _.defaults({
791                         id:       'image-details',
792                         title:    l10n.imageDetailsTitle,
793                         // Initial region modes.
794                         content:  'image-details',
795                         menu:     false,
796                         router:   false,
797                         toolbar:  'image-details',
798
799                         editing:  false,
800                         priority: 60
801                 }, media.controller.Library.prototype.defaults ),
802
803                 initialize: function( options ) {
804                         this.image = options.image;
805                         media.controller.State.prototype.initialize.apply( this, arguments );
806                 },
807
808                 activate: function() {
809                         this.frame.modal.$el.addClass('image-details');
810                 }
811         });
812
813         /**
814          * A state for editing a gallery's images and settings.
815          *
816          * @constructor
817          * @augments wp.media.controller.Library
818          * @augments wp.media.controller.State
819          * @augments Backbone.Model
820          */
821         media.controller.GalleryEdit = media.controller.Library.extend({
822                 defaults: {
823                         id:              'gallery-edit',
824                         title:           l10n.editGalleryTitle,
825                         // Selection defaults. @see media.model.Selection
826                         multiple:        false,
827                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
828                         searchable:      false,
829                         sortable:        true,
830                         display:         false,
831                         // Initial region modes.
832                         content:         'browse',
833                         toolbar:         'gallery-edit',
834
835                         describe:         true,
836                         displaySettings:  true,
837                         dragInfo:         true,
838                         idealColumnWidth: 170,
839                         editing:          false,
840                         priority:         60,
841
842                         // Don't sync the selection, as the Edit Gallery library
843                         // *is* the selection.
844                         syncSelection: false
845                 },
846
847                 initialize: function() {
848                         // If we haven't been provided a `library`, create a `Selection`.
849                         if ( ! this.get('library') )
850                                 this.set( 'library', new media.model.Selection() );
851
852                         // The single `Attachment` view to be used in the `Attachments` view.
853                         if ( ! this.get('AttachmentView') )
854                                 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
855                         media.controller.Library.prototype.initialize.apply( this, arguments );
856                 },
857
858                 activate: function() {
859                         var library = this.get('library');
860
861                         // Limit the library to images only.
862                         library.props.set( 'type', 'image' );
863
864                         // Watch for uploaded attachments.
865                         this.get('library').observe( wp.Uploader.queue );
866
867                         this.frame.on( 'content:render:browse', this.gallerySettings, this );
868
869                         media.controller.Library.prototype.activate.apply( this, arguments );
870                 },
871
872                 deactivate: function() {
873                         // Stop watching for uploaded attachments.
874                         this.get('library').unobserve( wp.Uploader.queue );
875
876                         this.frame.off( 'content:render:browse', this.gallerySettings, this );
877
878                         media.controller.Library.prototype.deactivate.apply( this, arguments );
879                 },
880
881                 gallerySettings: function( browser ) {
882                         if ( ! this.get('displaySettings') ) {
883                                 return;
884                         }
885
886                         var library = this.get('library');
887
888                         if ( ! library || ! browser ) {
889                                 return;
890                         }
891
892                         library.gallery = library.gallery || new Backbone.Model();
893
894                         browser.sidebar.set({
895                                 gallery: new media.view.Settings.Gallery({
896                                         controller: this,
897                                         model:      library.gallery,
898                                         priority:   40
899                                 })
900                         });
901
902                         browser.toolbar.set( 'reverse', {
903                                 text:     l10n.reverseOrder,
904                                 priority: 80,
905
906                                 click: function() {
907                                         library.reset( library.toArray().reverse() );
908                                 }
909                         });
910                 }
911         });
912
913         /**
914          * A state for adding an image to a gallery.
915          *
916          * @constructor
917          * @augments wp.media.controller.Library
918          * @augments wp.media.controller.State
919          * @augments Backbone.Model
920          */
921         media.controller.GalleryAdd = media.controller.Library.extend({
922                 defaults: _.defaults({
923                         id:            'gallery-library',
924                         title:         l10n.addToGalleryTitle,
925                         // Selection defaults. @see media.model.Selection
926                         multiple:      'add',
927                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
928                         filterable:    'uploaded',
929                         // Initial region modes.
930                         menu:          'gallery',
931                         toolbar:       'gallery-add',
932
933                         priority:      100,
934                         // Don't sync the selection, as the Edit Gallery library
935                         // *is* the selection.
936                         syncSelection: false
937                 }, media.controller.Library.prototype.defaults ),
938
939                 initialize: function() {
940                         // If we haven't been provided a `library`, create a `Selection`.
941                         if ( ! this.get('library') )
942                                 this.set( 'library', media.query({ type: 'image' }) );
943
944                         media.controller.Library.prototype.initialize.apply( this, arguments );
945                 },
946
947                 activate: function() {
948                         var library = this.get('library'),
949                                 edit    = this.frame.state('gallery-edit').get('library');
950
951                         if ( this.editLibrary && this.editLibrary !== edit )
952                                 library.unobserve( this.editLibrary );
953
954                         // Accepts attachments that exist in the original library and
955                         // that do not exist in gallery's library.
956                         library.validator = function( attachment ) {
957                                 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
958                         };
959
960                         // Reset the library to ensure that all attachments are re-added
961                         // to the collection. Do so silently, as calling `observe` will
962                         // trigger the `reset` event.
963                         library.reset( library.mirroring.models, { silent: true });
964                         library.observe( edit );
965                         this.editLibrary = edit;
966
967                         media.controller.Library.prototype.activate.apply( this, arguments );
968                 }
969         });
970
971         /**
972          * wp.media.controller.CollectionEdit
973          *
974          * @constructor
975          * @augments wp.media.controller.Library
976          * @augments wp.media.controller.State
977          * @augments Backbone.Model
978          */
979         media.controller.CollectionEdit = media.controller.Library.extend({
980                 defaults: {
981                         // Selection defaults. @see media.model.Selection
982                         multiple:     false,
983                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
984                         sortable:     true,
985                         searchable:   false,
986                         // Region mode defaults.
987                         content:      'browse',
988
989                         describe:         true,
990                         dragInfo:         true,
991                         idealColumnWidth: 170,
992                         editing:          false,
993                         priority:         60,
994                         SettingsView:     false,
995
996                         // Don't sync the selection, as the Edit {Collection} library
997                         // *is* the selection.
998                         syncSelection: false
999                 },
1000
1001                 initialize: function() {
1002                         var collectionType = this.get('collectionType');
1003
1004                         if ( 'video' === this.get( 'type' ) ) {
1005                                 collectionType = 'video-' + collectionType;
1006                         }
1007
1008                         this.set( 'id', collectionType + '-edit' );
1009                         this.set( 'toolbar', collectionType + '-edit' );
1010
1011                         // If we haven't been provided a `library`, create a `Selection`.
1012                         if ( ! this.get('library') ) {
1013                                 this.set( 'library', new media.model.Selection() );
1014                         }
1015                         // The single `Attachment` view to be used in the `Attachments` view.
1016                         if ( ! this.get('AttachmentView') ) {
1017                                 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
1018                         }
1019                         media.controller.Library.prototype.initialize.apply( this, arguments );
1020                 },
1021
1022                 activate: function() {
1023                         var library = this.get('library');
1024
1025                         // Limit the library to images only.
1026                         library.props.set( 'type', this.get( 'type' ) );
1027
1028                         // Watch for uploaded attachments.
1029                         this.get('library').observe( wp.Uploader.queue );
1030
1031                         this.frame.on( 'content:render:browse', this.renderSettings, this );
1032
1033                         media.controller.Library.prototype.activate.apply( this, arguments );
1034                 },
1035
1036                 deactivate: function() {
1037                         // Stop watching for uploaded attachments.
1038                         this.get('library').unobserve( wp.Uploader.queue );
1039
1040                         this.frame.off( 'content:render:browse', this.renderSettings, this );
1041
1042                         media.controller.Library.prototype.deactivate.apply( this, arguments );
1043                 },
1044
1045                 renderSettings: function( browser ) {
1046                         var library = this.get('library'),
1047                                 collectionType = this.get('collectionType'),
1048                                 dragInfoText = this.get('dragInfoText'),
1049                                 SettingsView = this.get('SettingsView'),
1050                                 obj = {};
1051
1052                         if ( ! library || ! browser ) {
1053                                 return;
1054                         }
1055
1056                         library[ collectionType ] = library[ collectionType ] || new Backbone.Model();
1057
1058                         obj[ collectionType ] = new SettingsView({
1059                                 controller: this,
1060                                 model:      library[ collectionType ],
1061                                 priority:   40
1062                         });
1063
1064                         browser.sidebar.set( obj );
1065
1066                         if ( dragInfoText ) {
1067                                 browser.toolbar.set( 'dragInfo', new media.View({
1068                                         el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0],
1069                                         priority: -40
1070                                 }) );
1071                         }
1072
1073                         browser.toolbar.set( 'reverse', {
1074                                 text:     l10n.reverseOrder,
1075                                 priority: 80,
1076
1077                                 click: function() {
1078                                         library.reset( library.toArray().reverse() );
1079                                 }
1080                         });
1081                 }
1082         });
1083
1084         /**
1085          * wp.media.controller.CollectionAdd
1086          *
1087          * @constructor
1088          * @augments wp.media.controller.Library
1089          * @augments wp.media.controller.State
1090          * @augments Backbone.Model
1091          */
1092         media.controller.CollectionAdd = media.controller.Library.extend({
1093                 defaults: _.defaults( {
1094                         // Selection defaults. @see media.model.Selection
1095                         multiple:      'add',
1096                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
1097                         filterable:    'uploaded',
1098
1099                         priority:      100,
1100                         syncSelection: false
1101                 }, media.controller.Library.prototype.defaults ),
1102
1103                 initialize: function() {
1104                         var collectionType = this.get('collectionType');
1105
1106                         if ( 'video' === this.get( 'type' ) ) {
1107                                 collectionType = 'video-' + collectionType;
1108                         }
1109
1110                         this.set( 'id', collectionType + '-library' );
1111                         this.set( 'toolbar', collectionType + '-add' );
1112                         this.set( 'menu', collectionType );
1113
1114                         // If we haven't been provided a `library`, create a `Selection`.
1115                         if ( ! this.get('library') ) {
1116                                 this.set( 'library', media.query({ type: this.get('type') }) );
1117                         }
1118                         media.controller.Library.prototype.initialize.apply( this, arguments );
1119                 },
1120
1121                 activate: function() {
1122                         var library = this.get('library'),
1123                                 editLibrary = this.get('editLibrary'),
1124                                 edit = this.frame.state( this.get('collectionType') + '-edit' ).get('library');
1125
1126                         if ( editLibrary && editLibrary !== edit ) {
1127                                 library.unobserve( editLibrary );
1128                         }
1129
1130                         // Accepts attachments that exist in the original library and
1131                         // that do not exist in gallery's library.
1132                         library.validator = function( attachment ) {
1133                                 return !! this.mirroring.get( attachment.cid ) && ! edit.get( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
1134                         };
1135
1136                         // Reset the library to ensure that all attachments are re-added
1137                         // to the collection. Do so silently, as calling `observe` will
1138                         // trigger the `reset` event.
1139                         library.reset( library.mirroring.models, { silent: true });
1140                         library.observe( edit );
1141                         this.set('editLibrary', edit);
1142
1143                         media.controller.Library.prototype.activate.apply( this, arguments );
1144                 }
1145         });
1146
1147         /**
1148          * A state for selecting a featured image for a post.
1149          *
1150          * @constructor
1151          * @augments wp.media.controller.Library
1152          * @augments wp.media.controller.State
1153          * @augments Backbone.Model
1154          */
1155         media.controller.FeaturedImage = media.controller.Library.extend({
1156                 defaults: _.defaults({
1157                         id:            'featured-image',
1158                         title:         l10n.setFeaturedImageTitle,
1159                         // Selection defaults. @see media.model.Selection
1160                         multiple:      false,
1161                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
1162                         filterable:    'uploaded',
1163                         // Region mode defaults.
1164                         toolbar:       'featured-image',
1165
1166                         priority:      60,
1167                         syncSelection: true
1168                 }, media.controller.Library.prototype.defaults ),
1169
1170                 initialize: function() {
1171                         var library, comparator;
1172
1173                         // If we haven't been provided a `library`, create a `Selection`.
1174                         if ( ! this.get('library') ) {
1175                                 this.set( 'library', media.query({ type: 'image' }) );
1176                         }
1177
1178                         media.controller.Library.prototype.initialize.apply( this, arguments );
1179
1180                         library    = this.get('library');
1181                         comparator = library.comparator;
1182
1183                         // Overload the library's comparator to push items that are not in
1184                         // the mirrored query to the front of the aggregate collection.
1185                         library.comparator = function( a, b ) {
1186                                 var aInQuery = !! this.mirroring.get( a.cid ),
1187                                         bInQuery = !! this.mirroring.get( b.cid );
1188
1189                                 if ( ! aInQuery && bInQuery ) {
1190                                         return -1;
1191                                 } else if ( aInQuery && ! bInQuery ) {
1192                                         return 1;
1193                                 } else {
1194                                         return comparator.apply( this, arguments );
1195                                 }
1196                         };
1197
1198                         // Add all items in the selection to the library, so any featured
1199                         // images that are not initially loaded still appear.
1200                         library.observe( this.get('selection') );
1201                 },
1202
1203                 activate: function() {
1204                         this.updateSelection();
1205                         this.frame.on( 'open', this.updateSelection, this );
1206
1207                         media.controller.Library.prototype.activate.apply( this, arguments );
1208                 },
1209
1210                 deactivate: function() {
1211                         this.frame.off( 'open', this.updateSelection, this );
1212
1213                         media.controller.Library.prototype.deactivate.apply( this, arguments );
1214                 },
1215
1216                 updateSelection: function() {
1217                         var selection = this.get('selection'),
1218                                 id = media.view.settings.post.featuredImageId,
1219                                 attachment;
1220
1221                         if ( '' !== id && -1 !== id ) {
1222                                 attachment = media.model.Attachment.get( id );
1223                                 attachment.fetch();
1224                         }
1225
1226                         selection.reset( attachment ? [ attachment ] : [] );
1227                 }
1228         });
1229
1230         /**
1231          * A state for replacing an image.
1232          *
1233          * @constructor
1234          * @augments wp.media.controller.Library
1235          * @augments wp.media.controller.State
1236          * @augments Backbone.Model
1237          */
1238         media.controller.ReplaceImage = media.controller.Library.extend({
1239                 defaults: _.defaults({
1240                         id:            'replace-image',
1241                         title:         l10n.replaceImageTitle,
1242                         // Selection defaults. @see media.model.Selection
1243                         multiple:      false,
1244                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
1245                         filterable:    'uploaded',
1246                         // Region mode defaults.
1247                         toolbar:       'replace',
1248                         menu:          false,
1249
1250                         priority:      60,
1251                         syncSelection: true
1252                 }, media.controller.Library.prototype.defaults ),
1253
1254                 initialize: function( options ) {
1255                         var library, comparator;
1256
1257                         this.image = options.image;
1258                         // If we haven't been provided a `library`, create a `Selection`.
1259                         if ( ! this.get('library') ) {
1260                                 this.set( 'library', media.query({ type: 'image' }) );
1261                         }
1262
1263                         media.controller.Library.prototype.initialize.apply( this, arguments );
1264
1265                         library    = this.get('library');
1266                         comparator = library.comparator;
1267
1268                         // Overload the library's comparator to push items that are not in
1269                         // the mirrored query to the front of the aggregate collection.
1270                         library.comparator = function( a, b ) {
1271                                 var aInQuery = !! this.mirroring.get( a.cid ),
1272                                         bInQuery = !! this.mirroring.get( b.cid );
1273
1274                                 if ( ! aInQuery && bInQuery ) {
1275                                         return -1;
1276                                 } else if ( aInQuery && ! bInQuery ) {
1277                                         return 1;
1278                                 } else {
1279                                         return comparator.apply( this, arguments );
1280                                 }
1281                         };
1282
1283                         // Add all items in the selection to the library, so any featured
1284                         // images that are not initially loaded still appear.
1285                         library.observe( this.get('selection') );
1286                 },
1287
1288                 activate: function() {
1289                         this.updateSelection();
1290                         media.controller.Library.prototype.activate.apply( this, arguments );
1291                 },
1292
1293                 updateSelection: function() {
1294                         var selection = this.get('selection'),
1295                                 attachment = this.image.attachment;
1296
1297                         selection.reset( attachment ? [ attachment ] : [] );
1298                 }
1299         });
1300
1301         /**
1302          * A state for editing (cropping, etc.) an image.
1303          *
1304          * @constructor
1305          * @augments wp.media.controller.State
1306          * @augments Backbone.Model
1307          */
1308         media.controller.EditImage = media.controller.State.extend({
1309                 defaults: {
1310                         id:      'edit-image',
1311                         title:   l10n.editImage,
1312                         // Region mode defaults.
1313                         menu:    false,
1314                         toolbar: 'edit-image',
1315                         content: 'edit-image',
1316
1317                         url:     ''
1318                 },
1319
1320                 activate: function() {
1321                         this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar );
1322                 },
1323
1324                 deactivate: function() {
1325                         this.stopListening( this.frame );
1326                 },
1327
1328                 toolbar: function() {
1329                         var frame = this.frame,
1330                                 lastState = frame.lastState(),
1331                                 previous = lastState && lastState.id;
1332
1333                         frame.toolbar.set( new media.view.Toolbar({
1334                                 controller: frame,
1335                                 items: {
1336                                         back: {
1337                                                 style: 'primary',
1338                                                 text:     l10n.back,
1339                                                 priority: 20,
1340                                                 click:    function() {
1341                                                         if ( previous ) {
1342                                                                 frame.setState( previous );
1343                                                         } else {
1344                                                                 frame.close();
1345                                                         }
1346                                                 }
1347                                         }
1348                                 }
1349                         }) );
1350                 }
1351         });
1352
1353         /**
1354          * wp.media.controller.MediaLibrary
1355          *
1356          * @constructor
1357          * @augments wp.media.controller.Library
1358          * @augments wp.media.controller.State
1359          * @augments Backbone.Model
1360          */
1361         media.controller.MediaLibrary = media.controller.Library.extend({
1362                 defaults: _.defaults({
1363                         // Attachments browser defaults. @see media.view.AttachmentsBrowser
1364                         filterable:      'uploaded',
1365
1366                         displaySettings: false,
1367                         priority:        80,
1368                         syncSelection:   false
1369                 }, media.controller.Library.prototype.defaults ),
1370
1371                 initialize: function( options ) {
1372                         this.media = options.media;
1373                         this.type = options.type;
1374                         this.set( 'library', media.query({ type: this.type }) );
1375
1376                         media.controller.Library.prototype.initialize.apply( this, arguments );
1377                 },
1378
1379                 activate: function() {
1380                         if ( media.frame.lastMime ) {
1381                                 this.set( 'library', media.query({ type: media.frame.lastMime }) );
1382                                 delete media.frame.lastMime;
1383                         }
1384                         media.controller.Library.prototype.activate.apply( this, arguments );
1385                 }
1386         });
1387
1388         /**
1389          * wp.media.controller.Embed
1390          *
1391          * @constructor
1392          * @augments wp.media.controller.State
1393          * @augments Backbone.Model
1394          */
1395         media.controller.Embed = media.controller.State.extend({
1396                 defaults: {
1397                         id:       'embed',
1398                         title:    l10n.insertFromUrlTitle,
1399                         // Region mode defaults.
1400                         content:  'embed',
1401                         menu:     'default',
1402                         toolbar:  'main-embed',
1403
1404                         priority: 120,
1405                         type:     'link',
1406                         url:      '',
1407                         metadata: {}
1408                 },
1409
1410                 // The amount of time used when debouncing the scan.
1411                 sensitivity: 200,
1412
1413                 initialize: function(options) {
1414                         this.metadata = options.metadata;
1415                         this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
1416                         this.props = new Backbone.Model( this.metadata || { url: '' });
1417                         this.props.on( 'change:url', this.debouncedScan, this );
1418                         this.props.on( 'change:url', this.refresh, this );
1419                         this.on( 'scan', this.scanImage, this );
1420                 },
1421
1422                 /**
1423                  * @fires wp.media.controller.Embed#scan
1424                  */
1425                 scan: function() {
1426                         var scanners,
1427                                 embed = this,
1428                                 attributes = {
1429                                         type: 'link',
1430                                         scanners: []
1431                                 };
1432
1433                         // Scan is triggered with the list of `attributes` to set on the
1434                         // state, useful for the 'type' attribute and 'scanners' attribute,
1435                         // an array of promise objects for asynchronous scan operations.
1436                         if ( this.props.get('url') ) {
1437                                 this.trigger( 'scan', attributes );
1438                         }
1439
1440                         if ( attributes.scanners.length ) {
1441                                 scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
1442                                 scanners.always( function() {
1443                                         if ( embed.get('scanners') === scanners ) {
1444                                                 embed.set( 'loading', false );
1445                                         }
1446                                 });
1447                         } else {
1448                                 attributes.scanners = null;
1449                         }
1450
1451                         attributes.loading = !! attributes.scanners;
1452                         this.set( attributes );
1453                 },
1454                 /**
1455                  * @param {Object} attributes
1456                  */
1457                 scanImage: function( attributes ) {
1458                         var frame = this.frame,
1459                                 state = this,
1460                                 url = this.props.get('url'),
1461                                 image = new Image(),
1462                                 deferred = $.Deferred();
1463
1464                         attributes.scanners.push( deferred.promise() );
1465
1466                         // Try to load the image and find its width/height.
1467                         image.onload = function() {
1468                                 deferred.resolve();
1469
1470                                 if ( state !== frame.state() || url !== state.props.get('url') ) {
1471                                         return;
1472                                 }
1473
1474                                 state.set({
1475                                         type: 'image'
1476                                 });
1477
1478                                 state.props.set({
1479                                         width:  image.width,
1480                                         height: image.height
1481                                 });
1482                         };
1483
1484                         image.onerror = deferred.reject;
1485                         image.src = url;
1486                 },
1487
1488                 refresh: function() {
1489                         this.frame.toolbar.get().refresh();
1490                 },
1491
1492                 reset: function() {
1493                         this.props.clear().set({ url: '' });
1494
1495                         if ( this.active ) {
1496                                 this.refresh();
1497                         }
1498                 }
1499         });
1500
1501         /**
1502          * wp.media.controller.Cropper
1503          *
1504          * Allows for a cropping step.
1505          *
1506          * @constructor
1507          * @augments wp.media.controller.State
1508          * @augments Backbone.Model
1509          */
1510         media.controller.Cropper = media.controller.State.extend({
1511                 defaults: {
1512                         id:          'cropper',
1513                         title:       l10n.cropImage,
1514                         // Region mode defaults.
1515                         toolbar:     'crop',
1516                         content:     'crop',
1517                         router:      false,
1518
1519                         canSkipCrop: false
1520                 },
1521
1522                 activate: function() {
1523                         this.frame.on( 'content:create:crop', this.createCropContent, this );
1524                         this.frame.on( 'close', this.removeCropper, this );
1525                         this.set('selection', new Backbone.Collection(this.frame._selection.single));
1526                 },
1527
1528                 deactivate: function() {
1529                         this.frame.toolbar.mode('browse');
1530                 },
1531
1532                 createCropContent: function() {
1533                         this.cropperView = new wp.media.view.Cropper({controller: this,
1534                                         attachment: this.get('selection').first() });
1535                         this.cropperView.on('image-loaded', this.createCropToolbar, this);
1536                         this.frame.content.set(this.cropperView);
1537
1538                 },
1539                 removeCropper: function() {
1540                         this.imgSelect.cancelSelection();
1541                         this.imgSelect.setOptions({remove: true});
1542                         this.imgSelect.update();
1543                         this.cropperView.remove();
1544                 },
1545                 createCropToolbar: function() {
1546                         var canSkipCrop, toolbarOptions;
1547
1548                         canSkipCrop = this.get('canSkipCrop') || false;
1549
1550                         toolbarOptions = {
1551                                 controller: this.frame,
1552                                 items: {
1553                                         insert: {
1554                                                 style:    'primary',
1555                                                 text:     l10n.cropImage,
1556                                                 priority: 80,
1557                                                 requires: { library: false, selection: false },
1558
1559                                                 click: function() {
1560                                                         var self = this,
1561                                                                 selection = this.controller.state().get('selection').first();
1562
1563                                                         selection.set({cropDetails: this.controller.state().imgSelect.getSelection()});
1564
1565                                                         this.$el.text(l10n.cropping);
1566                                                         this.$el.attr('disabled', true);
1567                                                         this.controller.state().doCrop( selection ).done( function( croppedImage ) {
1568                                                                 self.controller.trigger('cropped', croppedImage );
1569                                                                 self.controller.close();
1570                                                         }).fail( function() {
1571                                                                 self.controller.trigger('content:error:crop');
1572                                                         });
1573                                                 }
1574                                         }
1575                                 }
1576                         };
1577
1578                         if ( canSkipCrop ) {
1579                                 _.extend( toolbarOptions.items, {
1580                                         skip: {
1581                                                 style:      'secondary',
1582                                                 text:       l10n.skipCropping,
1583                                                 priority:   70,
1584                                                 requires:   { library: false, selection: false },
1585                                                 click:      function() {
1586                                                         var selection = this.controller.state().get('selection').first();
1587                                                         this.controller.state().cropperView.remove();
1588                                                         this.controller.trigger('skippedcrop', selection);
1589                                                         this.controller.close();
1590                                                 }
1591                                         }
1592                                 });
1593                         }
1594
1595                         this.frame.toolbar.set( new wp.media.view.Toolbar(toolbarOptions) );
1596                 },
1597
1598                 doCrop: function( attachment ) {
1599                         return wp.ajax.post( 'custom-header-crop', {
1600                                 nonce: attachment.get('nonces').edit,
1601                                 id: attachment.get('id'),
1602                                 cropDetails: attachment.get('cropDetails')
1603                         } );
1604                 }
1605         });
1606
1607         /**
1608          * ========================================================================
1609          * VIEWS
1610          * ========================================================================
1611          */
1612
1613         /**
1614          * wp.media.View
1615          * -------------
1616          *
1617          * The base view class.
1618          *
1619          * Undelegating events, removing events from the model, and
1620          * removing events from the controller mirror the code for
1621          * `Backbone.View.dispose` in Backbone 0.9.8 development.
1622          *
1623          * This behavior has since been removed, and should not be used
1624          * outside of the media manager.
1625          *
1626          * @constructor
1627          * @augments wp.Backbone.View
1628          * @augments Backbone.View
1629          */
1630         media.View = wp.Backbone.View.extend({
1631                 constructor: function( options ) {
1632                         if ( options && options.controller ) {
1633                                 this.controller = options.controller;
1634                         }
1635                         wp.Backbone.View.apply( this, arguments );
1636                 },
1637                 /**
1638                  * @returns {wp.media.View} Returns itself to allow chaining
1639                  */
1640                 dispose: function() {
1641                         // Undelegating events, removing events from the model, and
1642                         // removing events from the controller mirror the code for
1643                         // `Backbone.View.dispose` in Backbone 0.9.8 development.
1644                         this.undelegateEvents();
1645
1646                         if ( this.model && this.model.off ) {
1647                                 this.model.off( null, null, this );
1648                         }
1649
1650                         if ( this.collection && this.collection.off ) {
1651                                 this.collection.off( null, null, this );
1652                         }
1653
1654                         // Unbind controller events.
1655                         if ( this.controller && this.controller.off ) {
1656                                 this.controller.off( null, null, this );
1657                         }
1658
1659                         return this;
1660                 },
1661                 /**
1662                  * @returns {wp.media.View} Returns itself to allow chaining
1663                  */
1664                 remove: function() {
1665                         this.dispose();
1666                         /**
1667                          * call 'remove' directly on the parent class
1668                          */
1669                         return wp.Backbone.View.prototype.remove.apply( this, arguments );
1670                 }
1671         });
1672
1673         /**
1674          * wp.media.view.Frame
1675          *
1676          * A frame is a composite view consisting of one or more regions and one or more
1677          * states. Only one state can be active at any given moment.
1678          *
1679          * @constructor
1680          * @augments wp.media.View
1681          * @augments wp.Backbone.View
1682          * @augments Backbone.View
1683          * @mixes wp.media.controller.StateMachine
1684          */
1685         media.view.Frame = media.View.extend({
1686                 initialize: function() {
1687                         _.defaults( this.options, {
1688                                 mode: [ 'select' ]
1689                         });
1690                         this._createRegions();
1691                         this._createStates();
1692                         this._createModes();
1693                 },
1694
1695                 _createRegions: function() {
1696                         // Clone the regions array.
1697                         this.regions = this.regions ? this.regions.slice() : [];
1698
1699                         // Initialize regions.
1700                         _.each( this.regions, function( region ) {
1701                                 this[ region ] = new media.controller.Region({
1702                                         view:     this,
1703                                         id:       region,
1704                                         selector: '.media-frame-' + region
1705                                 });
1706                         }, this );
1707                 },
1708                 /**
1709                  * @fires wp.media.controller.State#ready
1710                  */
1711                 _createStates: function() {
1712                         // Create the default `states` collection.
1713                         this.states = new Backbone.Collection( null, {
1714                                 model: media.controller.State
1715                         });
1716
1717                         // Ensure states have a reference to the frame.
1718                         this.states.on( 'add', function( model ) {
1719                                 model.frame = this;
1720                                 model.trigger('ready');
1721                         }, this );
1722
1723                         if ( this.options.states ) {
1724                                 this.states.add( this.options.states );
1725                         }
1726                 },
1727                 _createModes: function() {
1728                         // Store active "modes" that the frame is in. Unrelated to region modes.
1729                         this.activeModes = new Backbone.Collection();
1730                         this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) );
1731
1732                         _.each( this.options.mode, function( mode ) {
1733                                 this.activateMode( mode );
1734                         }, this );
1735                 },
1736                 /**
1737                  * @returns {wp.media.view.Frame} Returns itself to allow chaining
1738                  */
1739                 reset: function() {
1740                         this.states.invoke( 'trigger', 'reset' );
1741                         return this;
1742                 },
1743                 /**
1744                  * Map activeMode collection events to the frame.
1745                  */
1746                 triggerModeEvents: function( model, collection, options ) {
1747                         var collectionEvent,
1748                                 modeEventMap = {
1749                                         add: 'activate',
1750                                         remove: 'deactivate'
1751                                 },
1752                                 eventToTrigger;
1753                         // Probably a better way to do this.
1754                         _.each( options, function( value, key ) {
1755                                 if ( value ) {
1756                                         collectionEvent = key;
1757                                 }
1758                         } );
1759
1760                         if ( ! _.has( modeEventMap, collectionEvent ) ) {
1761                                 return;
1762                         }
1763
1764                         eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent];
1765                         this.trigger( eventToTrigger );
1766                 },
1767                 /**
1768                  * Activate a mode on the frame.
1769                  *
1770                  * @param string mode Mode ID.
1771                  * @returns {this} Returns itself to allow chaining.
1772                  */
1773                 activateMode: function( mode ) {
1774                         // Bail if the mode is already active.
1775                         if ( this.isModeActive( mode ) ) {
1776                                 return;
1777                         }
1778                         this.activeModes.add( [ { id: mode } ] );
1779                         // Add a CSS class to the frame so elements can be styled for the mode.
1780                         this.$el.addClass( 'mode-' + mode );
1781
1782                         return this;
1783                 },
1784                 /**
1785                  * Deactivate a mode on the frame.
1786                  *
1787                  * @param string mode Mode ID.
1788                  * @returns {this} Returns itself to allow chaining.
1789                  */
1790                 deactivateMode: function( mode ) {
1791                         // Bail if the mode isn't active.
1792                         if ( ! this.isModeActive( mode ) ) {
1793                                 return this;
1794                         }
1795                         this.activeModes.remove( this.activeModes.where( { id: mode } ) );
1796                         this.$el.removeClass( 'mode-' + mode );
1797                         /**
1798                          * Frame mode deactivation event.
1799                          *
1800                          * @event this#{mode}:deactivate
1801                          */
1802                         this.trigger( mode + ':deactivate' );
1803
1804                         return this;
1805                 },
1806                 /**
1807                  * Check if a mode is enabled on the frame.
1808                  *
1809                  * @param  string mode Mode ID.
1810                  * @return bool
1811                  */
1812                 isModeActive: function( mode ) {
1813                         return Boolean( this.activeModes.where( { id: mode } ).length );
1814                 }
1815         });
1816
1817         // Make the `Frame` a `StateMachine`.
1818         _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
1819
1820         /**
1821          * wp.media.view.MediaFrame
1822          *
1823          * Type of frame used to create the media modal.
1824          *
1825          * @constructor
1826          * @augments wp.media.view.Frame
1827          * @augments wp.media.View
1828          * @augments wp.Backbone.View
1829          * @augments Backbone.View
1830          * @mixes wp.media.controller.StateMachine
1831          */
1832         media.view.MediaFrame = media.view.Frame.extend({
1833                 className: 'media-frame',
1834                 template:  media.template('media-frame'),
1835                 regions:   ['menu','title','content','toolbar','router'],
1836
1837                 events: {
1838                         'click div.media-frame-title h1': 'toggleMenu'
1839                 },
1840
1841                 /**
1842                  * @global wp.Uploader
1843                  */
1844                 initialize: function() {
1845                         media.view.Frame.prototype.initialize.apply( this, arguments );
1846
1847                         _.defaults( this.options, {
1848                                 title:    '',
1849                                 modal:    true,
1850                                 uploader: true
1851                         });
1852
1853                         // Ensure core UI is enabled.
1854                         this.$el.addClass('wp-core-ui');
1855
1856                         // Initialize modal container view.
1857                         if ( this.options.modal ) {
1858                                 this.modal = new media.view.Modal({
1859                                         controller: this,
1860                                         title:      this.options.title
1861                                 });
1862
1863                                 this.modal.content( this );
1864                         }
1865
1866                         // Force the uploader off if the upload limit has been exceeded or
1867                         // if the browser isn't supported.
1868                         if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported ) {
1869                                 this.options.uploader = false;
1870                         }
1871
1872                         // Initialize window-wide uploader.
1873                         if ( this.options.uploader ) {
1874                                 this.uploader = new media.view.UploaderWindow({
1875                                         controller: this,
1876                                         uploader: {
1877                                                 dropzone:  this.modal ? this.modal.$el : this.$el,
1878                                                 container: this.$el
1879                                         }
1880                                 });
1881                                 this.views.set( '.media-frame-uploader', this.uploader );
1882                         }
1883
1884                         this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1885
1886                         // Bind default title creation.
1887                         this.on( 'title:create:default', this.createTitle, this );
1888                         this.title.mode('default');
1889
1890                         this.on( 'title:render', function( view ) {
1891                                 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );
1892                         });
1893
1894                         // Bind default menu.
1895                         this.on( 'menu:create:default', this.createMenu, this );
1896                 },
1897                 /**
1898                  * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
1899                  */
1900                 render: function() {
1901                         // Activate the default state if no active state exists.
1902                         if ( ! this.state() && this.options.state ) {
1903                                 this.setState( this.options.state );
1904                         }
1905                         /**
1906                          * call 'render' directly on the parent class
1907                          */
1908                         return media.view.Frame.prototype.render.apply( this, arguments );
1909                 },
1910                 /**
1911                  * @param {Object} title
1912                  * @this wp.media.controller.Region
1913                  */
1914                 createTitle: function( title ) {
1915                         title.view = new media.View({
1916                                 controller: this,
1917                                 tagName: 'h1'
1918                         });
1919                 },
1920                 /**
1921                  * @param {Object} menu
1922                  * @this wp.media.controller.Region
1923                  */
1924                 createMenu: function( menu ) {
1925                         menu.view = new media.view.Menu({
1926                                 controller: this
1927                         });
1928                 },
1929
1930                 toggleMenu: function() {
1931                         this.$el.find( '.media-menu' ).toggleClass( 'visible' );
1932                 },
1933
1934                 /**
1935                  * @param {Object} toolbar
1936                  * @this wp.media.controller.Region
1937                  */
1938                 createToolbar: function( toolbar ) {
1939                         toolbar.view = new media.view.Toolbar({
1940                                 controller: this
1941                         });
1942                 },
1943                 /**
1944                  * @param {Object} router
1945                  * @this wp.media.controller.Region
1946                  */
1947                 createRouter: function( router ) {
1948                         router.view = new media.view.Router({
1949                                 controller: this
1950                         });
1951                 },
1952                 /**
1953                  * @param {Object} options
1954                  */
1955                 createIframeStates: function( options ) {
1956                         var settings = media.view.settings,
1957                                 tabs = settings.tabs,
1958                                 tabUrl = settings.tabUrl,
1959                                 $postId;
1960
1961                         if ( ! tabs || ! tabUrl ) {
1962                                 return;
1963                         }
1964
1965                         // Add the post ID to the tab URL if it exists.
1966                         $postId = $('#post_ID');
1967                         if ( $postId.length ) {
1968                                 tabUrl += '&post_id=' + $postId.val();
1969                         }
1970
1971                         // Generate the tab states.
1972                         _.each( tabs, function( title, id ) {
1973                                 this.state( 'iframe:' + id ).set( _.defaults({
1974                                         tab:     id,
1975                                         src:     tabUrl + '&tab=' + id,
1976                                         title:   title,
1977                                         content: 'iframe',
1978                                         menu:    'default'
1979                                 }, options ) );
1980                         }, this );
1981
1982                         this.on( 'content:create:iframe', this.iframeContent, this );
1983                         this.on( 'menu:render:default', this.iframeMenu, this );
1984                         this.on( 'open', this.hijackThickbox, this );
1985                         this.on( 'close', this.restoreThickbox, this );
1986                 },
1987
1988                 /**
1989                  * @param {Object} content
1990                  * @this wp.media.controller.Region
1991                  */
1992                 iframeContent: function( content ) {
1993                         this.$el.addClass('hide-toolbar');
1994                         content.view = new media.view.Iframe({
1995                                 controller: this
1996                         });
1997                 },
1998
1999                 iframeMenu: function( view ) {
2000                         var views = {};
2001
2002                         if ( ! view ) {
2003                                 return;
2004                         }
2005
2006                         _.each( media.view.settings.tabs, function( title, id ) {
2007                                 views[ 'iframe:' + id ] = {
2008                                         text: this.state( 'iframe:' + id ).get('title'),
2009                                         priority: 200
2010                                 };
2011                         }, this );
2012
2013                         view.set( views );
2014                 },
2015
2016                 hijackThickbox: function() {
2017                         var frame = this;
2018
2019                         if ( ! window.tb_remove || this._tb_remove ) {
2020                                 return;
2021                         }
2022
2023                         this._tb_remove = window.tb_remove;
2024                         window.tb_remove = function() {
2025                                 frame.close();
2026                                 frame.reset();
2027                                 frame.setState( frame.options.state );
2028                                 frame._tb_remove.call( window );
2029                         };
2030                 },
2031
2032                 restoreThickbox: function() {
2033                         if ( ! this._tb_remove ) {
2034                                 return;
2035                         }
2036
2037                         window.tb_remove = this._tb_remove;
2038                         delete this._tb_remove;
2039                 }
2040         });
2041
2042         // Map some of the modal's methods to the frame.
2043         _.each(['open','close','attach','detach','escape'], function( method ) {
2044                 /**
2045                  * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
2046                  */
2047                 media.view.MediaFrame.prototype[ method ] = function() {
2048                         if ( this.modal ) {
2049                                 this.modal[ method ].apply( this.modal, arguments );
2050                         }
2051                         return this;
2052                 };
2053         });
2054
2055         /**
2056          * wp.media.view.MediaFrame.Select
2057          *
2058          * Type of media frame that is used to select an item or items from the media library
2059          *
2060          * @constructor
2061          * @augments wp.media.view.MediaFrame
2062          * @augments wp.media.view.Frame
2063          * @augments wp.media.View
2064          * @augments wp.Backbone.View
2065          * @augments Backbone.View
2066          * @mixes wp.media.controller.StateMachine
2067          */
2068         media.view.MediaFrame.Select = media.view.MediaFrame.extend({
2069                 initialize: function() {
2070                         /**
2071                          * call 'initialize' directly on the parent class
2072                          */
2073                         media.view.MediaFrame.prototype.initialize.apply( this, arguments );
2074
2075                         _.defaults( this.options, {
2076                                 selection: [],
2077                                 library:   {},
2078                                 multiple:  false,
2079                                 state:    'library'
2080                         });
2081
2082                         this.createSelection();
2083                         this.createStates();
2084                         this.bindHandlers();
2085                 },
2086
2087                 /**
2088                  * Attach a selection collection to the frame.
2089                  *
2090                  * A selection is a collection of attachments used for a specific purpose
2091                  * by a media frame. e.g. Selecting an attachment (or many) to insert into
2092                  * post content.
2093                  *
2094                  * @see media.model.Selection
2095                  */
2096                 createSelection: function() {
2097                         var selection = this.options.selection;
2098
2099                         if ( ! (selection instanceof media.model.Selection) ) {
2100                                 this.options.selection = new media.model.Selection( selection, {
2101                                         multiple: this.options.multiple
2102                                 });
2103                         }
2104
2105                         this._selection = {
2106                                 attachments: new media.model.Attachments(),
2107                                 difference: []
2108                         };
2109                 },
2110
2111                 /**
2112                  * Create the default states on the frame.
2113                  */
2114                 createStates: function() {
2115                         var options = this.options;
2116
2117                         if ( this.options.states ) {
2118                                 return;
2119                         }
2120
2121                         // Add the default states.
2122                         this.states.add([
2123                                 // Main states.
2124                                 new media.controller.Library({
2125                                         library:   media.query( options.library ),
2126                                         multiple:  options.multiple,
2127                                         title:     options.title,
2128                                         priority:  20
2129                                 })
2130                         ]);
2131                 },
2132
2133                 /**
2134                  * Bind region mode event callbacks.
2135                  *
2136                  * @see media.controller.Region.render
2137                  */
2138                 bindHandlers: function() {
2139                         this.on( 'router:create:browse', this.createRouter, this );
2140                         this.on( 'router:render:browse', this.browseRouter, this );
2141                         this.on( 'content:create:browse', this.browseContent, this );
2142                         this.on( 'content:render:upload', this.uploadContent, this );
2143                         this.on( 'toolbar:create:select', this.createSelectToolbar, this );
2144                 },
2145
2146                 /**
2147                  * Render callback for the router region in the `browse` mode.
2148                  *
2149                  * @param {wp.media.view.Router} routerView
2150                  */
2151                 browseRouter: function( routerView ) {
2152                         routerView.set({
2153                                 upload: {
2154                                         text:     l10n.uploadFilesTitle,
2155                                         priority: 20
2156                                 },
2157                                 browse: {
2158                                         text:     l10n.mediaLibraryTitle,
2159                                         priority: 40
2160                                 }
2161                         });
2162                 },
2163
2164                 /**
2165                  * Render callback for the content region in the `browse` mode.
2166                  *
2167                  * @param {wp.media.controller.Region} contentRegion
2168                  */
2169                 browseContent: function( contentRegion ) {
2170                         var state = this.state();
2171
2172                         this.$el.removeClass('hide-toolbar');
2173
2174                         // Browse our library of attachments.
2175                         contentRegion.view = new media.view.AttachmentsBrowser({
2176                                 controller: this,
2177                                 collection: state.get('library'),
2178                                 selection:  state.get('selection'),
2179                                 model:      state,
2180                                 sortable:   state.get('sortable'),
2181                                 search:     state.get('searchable'),
2182                                 filters:    state.get('filterable'),
2183                                 display:    state.has('display') ? state.get('display') : state.get('displaySettings'),
2184                                 dragInfo:   state.get('dragInfo'),
2185
2186                                 idealColumnWidth: state.get('idealColumnWidth'),
2187                                 suggestedWidth:   state.get('suggestedWidth'),
2188                                 suggestedHeight:  state.get('suggestedHeight'),
2189
2190                                 AttachmentView: state.get('AttachmentView')
2191                         });
2192                 },
2193
2194                 /**
2195                  * Render callback for the content region in the `upload` mode.
2196                  */
2197                 uploadContent: function() {
2198                         this.$el.removeClass( 'hide-toolbar' );
2199                         this.content.set( new media.view.UploaderInline({
2200                                 controller: this
2201                         }) );
2202                 },
2203
2204                 /**
2205                  * Toolbars
2206                  *
2207                  * @param {Object} toolbar
2208                  * @param {Object} [options={}]
2209                  * @this wp.media.controller.Region
2210                  */
2211                 createSelectToolbar: function( toolbar, options ) {
2212                         options = options || this.options.button || {};
2213                         options.controller = this;
2214
2215                         toolbar.view = new media.view.Toolbar.Select( options );
2216                 }
2217         });
2218
2219         /**
2220          * wp.media.view.MediaFrame.Post
2221          *
2222          * @constructor
2223          * @augments wp.media.view.MediaFrame.Select
2224          * @augments wp.media.view.MediaFrame
2225          * @augments wp.media.view.Frame
2226          * @augments wp.media.View
2227          * @augments wp.Backbone.View
2228          * @augments Backbone.View
2229          * @mixes wp.media.controller.StateMachine
2230          */
2231         media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
2232                 initialize: function() {
2233                         this.counts = {
2234                                 audio: {
2235                                         count: media.view.settings.attachmentCounts.audio,
2236                                         state: 'playlist'
2237                                 },
2238                                 video: {
2239                                         count: media.view.settings.attachmentCounts.video,
2240                                         state: 'video-playlist'
2241                                 }
2242                         };
2243
2244                         _.defaults( this.options, {
2245                                 multiple:  true,
2246                                 editing:   false,
2247                                 state:    'insert',
2248                                 metadata:  {}
2249                         });
2250                         /**
2251                          * call 'initialize' directly on the parent class
2252                          */
2253                         media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
2254                         this.createIframeStates();
2255
2256                 },
2257
2258                 createStates: function() {
2259                         var options = this.options;
2260
2261                         // Add the default states.
2262                         this.states.add([
2263                                 // Main states.
2264                                 new media.controller.Library({
2265                                         id:         'insert',
2266                                         title:      l10n.insertMediaTitle,
2267                                         priority:   20,
2268                                         toolbar:    'main-insert',
2269                                         filterable: 'all',
2270                                         library:    media.query( options.library ),
2271                                         multiple:   options.multiple ? 'reset' : false,
2272                                         editable:   true,
2273
2274                                         // If the user isn't allowed to edit fields,
2275                                         // can they still edit it locally?
2276                                         allowLocalEdits: true,
2277
2278                                         // Show the attachment display settings.
2279                                         displaySettings: true,
2280                                         // Update user settings when users adjust the
2281                                         // attachment display settings.
2282                                         displayUserSettings: true
2283                                 }),
2284
2285                                 new media.controller.Library({
2286                                         id:         'gallery',
2287                                         title:      l10n.createGalleryTitle,
2288                                         priority:   40,
2289                                         toolbar:    'main-gallery',
2290                                         filterable: 'uploaded',
2291                                         multiple:   'add',
2292                                         editable:   false,
2293
2294                                         library:  media.query( _.defaults({
2295                                                 type: 'image'
2296                                         }, options.library ) )
2297                                 }),
2298
2299                                 // Embed states.
2300                                 new media.controller.Embed( { metadata: options.metadata } ),
2301
2302                                 new media.controller.EditImage( { model: options.editImage } ),
2303
2304                                 // Gallery states.
2305                                 new media.controller.GalleryEdit({
2306                                         library: options.selection,
2307                                         editing: options.editing,
2308                                         menu:    'gallery'
2309                                 }),
2310
2311                                 new media.controller.GalleryAdd(),
2312
2313                                 new media.controller.Library({
2314                                         id:         'playlist',
2315                                         title:      l10n.createPlaylistTitle,
2316                                         priority:   60,
2317                                         toolbar:    'main-playlist',
2318                                         filterable: 'uploaded',
2319                                         multiple:   'add',
2320                                         editable:   false,
2321
2322                                         library:  media.query( _.defaults({
2323                                                 type: 'audio'
2324                                         }, options.library ) )
2325                                 }),
2326
2327                                 // Playlist states.
2328                                 new media.controller.CollectionEdit({
2329                                         type: 'audio',
2330                                         collectionType: 'playlist',
2331                                         title:          l10n.editPlaylistTitle,
2332                                         SettingsView:   media.view.Settings.Playlist,
2333                                         library:        options.selection,
2334                                         editing:        options.editing,
2335                                         menu:           'playlist',
2336                                         dragInfoText:   l10n.playlistDragInfo,
2337                                         dragInfo:       false
2338                                 }),
2339
2340                                 new media.controller.CollectionAdd({
2341                                         type: 'audio',
2342                                         collectionType: 'playlist',
2343                                         title: l10n.addToPlaylistTitle
2344                                 }),
2345
2346                                 new media.controller.Library({
2347                                         id:         'video-playlist',
2348                                         title:      l10n.createVideoPlaylistTitle,
2349                                         priority:   60,
2350                                         toolbar:    'main-video-playlist',
2351                                         filterable: 'uploaded',
2352                                         multiple:   'add',
2353                                         editable:   false,
2354
2355                                         library:  media.query( _.defaults({
2356                                                 type: 'video'
2357                                         }, options.library ) )
2358                                 }),
2359
2360                                 new media.controller.CollectionEdit({
2361                                         type: 'video',
2362                                         collectionType: 'playlist',
2363                                         title:          l10n.editVideoPlaylistTitle,
2364                                         SettingsView:   media.view.Settings.Playlist,
2365                                         library:        options.selection,
2366                                         editing:        options.editing,
2367                                         menu:           'video-playlist',
2368                                         dragInfoText:   l10n.videoPlaylistDragInfo,
2369                                         dragInfo:       false
2370                                 }),
2371
2372                                 new media.controller.CollectionAdd({
2373                                         type: 'video',
2374                                         collectionType: 'playlist',
2375                                         title: l10n.addToVideoPlaylistTitle
2376                                 })
2377                         ]);
2378
2379                         if ( media.view.settings.post.featuredImageId ) {
2380                                 this.states.add( new media.controller.FeaturedImage() );
2381                         }
2382                 },
2383
2384                 bindHandlers: function() {
2385                         var handlers, checkCounts;
2386
2387                         media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
2388
2389                         this.on( 'activate', this.activate, this );
2390
2391                         // Only bother checking media type counts if one of the counts is zero
2392                         checkCounts = _.find( this.counts, function( type ) {
2393                                 return type.count === 0;
2394                         } );
2395
2396                         if ( typeof checkCounts !== 'undefined' ) {
2397                                 this.listenTo( media.model.Attachments.all, 'change:type', this.mediaTypeCounts );
2398                         }
2399
2400                         this.on( 'menu:create:gallery', this.createMenu, this );
2401                         this.on( 'menu:create:playlist', this.createMenu, this );
2402                         this.on( 'menu:create:video-playlist', this.createMenu, this );
2403                         this.on( 'toolbar:create:main-insert', this.createToolbar, this );
2404                         this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
2405                         this.on( 'toolbar:create:main-playlist', this.createToolbar, this );
2406                         this.on( 'toolbar:create:main-video-playlist', this.createToolbar, this );
2407                         this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
2408                         this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
2409
2410                         handlers = {
2411                                 menu: {
2412                                         'default': 'mainMenu',
2413                                         'gallery': 'galleryMenu',
2414                                         'playlist': 'playlistMenu',
2415                                         'video-playlist': 'videoPlaylistMenu'
2416                                 },
2417
2418                                 content: {
2419                                         'embed':          'embedContent',
2420                                         'edit-image':     'editImageContent',
2421                                         'edit-selection': 'editSelectionContent'
2422                                 },
2423
2424                                 toolbar: {
2425                                         'main-insert':      'mainInsertToolbar',
2426                                         'main-gallery':     'mainGalleryToolbar',
2427                                         'gallery-edit':     'galleryEditToolbar',
2428                                         'gallery-add':      'galleryAddToolbar',
2429                                         'main-playlist':        'mainPlaylistToolbar',
2430                                         'playlist-edit':        'playlistEditToolbar',
2431                                         'playlist-add':         'playlistAddToolbar',
2432                                         'main-video-playlist': 'mainVideoPlaylistToolbar',
2433                                         'video-playlist-edit': 'videoPlaylistEditToolbar',
2434                                         'video-playlist-add': 'videoPlaylistAddToolbar'
2435                                 }
2436                         };
2437
2438                         _.each( handlers, function( regionHandlers, region ) {
2439                                 _.each( regionHandlers, function( callback, handler ) {
2440                                         this.on( region + ':render:' + handler, this[ callback ], this );
2441                                 }, this );
2442                         }, this );
2443                 },
2444
2445                 activate: function() {
2446                         // Hide menu items for states tied to particular media types if there are no items
2447                         _.each( this.counts, function( type ) {
2448                                 if ( type.count < 1 ) {
2449                                         this.menuItemVisibility( type.state, 'hide' );
2450                                 }
2451                         }, this );
2452                 },
2453
2454                 mediaTypeCounts: function( model, attr ) {
2455                         if ( typeof this.counts[ attr ] !== 'undefined' && this.counts[ attr ].count < 1 ) {
2456                                 this.counts[ attr ].count++;
2457                                 this.menuItemVisibility( this.counts[ attr ].state, 'show' );
2458                         }
2459                 },
2460
2461                 // Menus
2462                 /**
2463                  * @param {wp.Backbone.View} view
2464                  */
2465                 mainMenu: function( view ) {
2466                         view.set({
2467                                 'library-separator': new media.View({
2468                                         className: 'separator',
2469                                         priority: 100
2470                                 })
2471                         });
2472                 },
2473
2474                 menuItemVisibility: function( state, visibility ) {
2475                         var menu = this.menu.get();
2476                         if ( visibility === 'hide' ) {
2477                                 menu.hide( state );
2478                         } else if ( visibility === 'show' ) {
2479                                 menu.show( state );
2480                         }
2481                 },
2482                 /**
2483                  * @param {wp.Backbone.View} view
2484                  */
2485                 galleryMenu: function( view ) {
2486                         var lastState = this.lastState(),
2487                                 previous = lastState && lastState.id,
2488                                 frame = this;
2489
2490                         view.set({
2491                                 cancel: {
2492                                         text:     l10n.cancelGalleryTitle,
2493                                         priority: 20,
2494                                         click:    function() {
2495                                                 if ( previous ) {
2496                                                         frame.setState( previous );
2497                                                 } else {
2498                                                         frame.close();
2499                                                 }
2500
2501                                                 // Keep focus inside media modal
2502                                                 // after canceling a gallery
2503                                                 this.controller.modal.focusManager.focus();
2504                                         }
2505                                 },
2506                                 separateCancel: new media.View({
2507                                         className: 'separator',
2508                                         priority: 40
2509                                 })
2510                         });
2511                 },
2512
2513                 playlistMenu: function( view ) {
2514                         var lastState = this.lastState(),
2515                                 previous = lastState && lastState.id,
2516                                 frame = this;
2517
2518                         view.set({
2519                                 cancel: {
2520                                         text:     l10n.cancelPlaylistTitle,
2521                                         priority: 20,
2522                                         click:    function() {
2523                                                 if ( previous ) {
2524                                                         frame.setState( previous );
2525                                                 } else {
2526                                                         frame.close();
2527                                                 }
2528                                         }
2529                                 },
2530                                 separateCancel: new media.View({
2531                                         className: 'separator',
2532                                         priority: 40
2533                                 })
2534                         });
2535                 },
2536
2537                 videoPlaylistMenu: function( view ) {
2538                         var lastState = this.lastState(),
2539                                 previous = lastState && lastState.id,
2540                                 frame = this;
2541
2542                         view.set({
2543                                 cancel: {
2544                                         text:     l10n.cancelVideoPlaylistTitle,
2545                                         priority: 20,
2546                                         click:    function() {
2547                                                 if ( previous ) {
2548                                                         frame.setState( previous );
2549                                                 } else {
2550                                                         frame.close();
2551                                                 }
2552                                         }
2553                                 },
2554                                 separateCancel: new media.View({
2555                                         className: 'separator',
2556                                         priority: 40
2557                                 })
2558                         });
2559                 },
2560
2561                 // Content
2562                 embedContent: function() {
2563                         var view = new media.view.Embed({
2564                                 controller: this,
2565                                 model:      this.state()
2566                         }).render();
2567
2568                         this.content.set( view );
2569
2570                         if ( ! isTouchDevice ) {
2571                                 view.url.focus();
2572                         }
2573                 },
2574
2575                 editSelectionContent: function() {
2576                         var state = this.state(),
2577                                 selection = state.get('selection'),
2578                                 view;
2579
2580                         view = new media.view.AttachmentsBrowser({
2581                                 controller: this,
2582                                 collection: selection,
2583                                 selection:  selection,
2584                                 model:      state,
2585                                 sortable:   true,
2586                                 search:     false,
2587                                 dragInfo:   true,
2588
2589                                 AttachmentView: media.view.Attachment.EditSelection
2590                         }).render();
2591
2592                         view.toolbar.set( 'backToLibrary', {
2593                                 text:     l10n.returnToLibrary,
2594                                 priority: -100,
2595
2596                                 click: function() {
2597                                         this.controller.content.mode('browse');
2598                                 }
2599                         });
2600
2601                         // Browse our library of attachments.
2602                         this.content.set( view );
2603                 },
2604
2605                 editImageContent: function() {
2606                         var image = this.state().get('image'),
2607                                 view = new media.view.EditImage( { model: image, controller: this } ).render();
2608
2609                         this.content.set( view );
2610
2611                         // after creating the wrapper view, load the actual editor via an ajax call
2612                         view.loadEditor();
2613
2614                 },
2615
2616                 // Toolbars
2617
2618                 /**
2619                  * @param {wp.Backbone.View} view
2620                  */
2621                 selectionStatusToolbar: function( view ) {
2622                         var editable = this.state().get('editable');
2623
2624                         view.set( 'selection', new media.view.Selection({
2625                                 controller: this,
2626                                 collection: this.state().get('selection'),
2627                                 priority:   -40,
2628
2629                                 // If the selection is editable, pass the callback to
2630                                 // switch the content mode.
2631                                 editable: editable && function() {
2632                                         this.controller.content.mode('edit-selection');
2633                                 }
2634                         }).render() );
2635                 },
2636
2637                 /**
2638                  * @param {wp.Backbone.View} view
2639                  */
2640                 mainInsertToolbar: function( view ) {
2641                         var controller = this;
2642
2643                         this.selectionStatusToolbar( view );
2644
2645                         view.set( 'insert', {
2646                                 style:    'primary',
2647                                 priority: 80,
2648                                 text:     l10n.insertIntoPost,
2649                                 requires: { selection: true },
2650
2651                                 /**
2652                                  * @fires wp.media.controller.State#insert
2653                                  */
2654                                 click: function() {
2655                                         var state = controller.state(),
2656                                                 selection = state.get('selection');
2657
2658                                         controller.close();
2659                                         state.trigger( 'insert', selection ).reset();
2660                                 }
2661                         });
2662                 },
2663
2664                 /**
2665                  * @param {wp.Backbone.View} view
2666                  */
2667                 mainGalleryToolbar: function( view ) {
2668                         var controller = this;
2669
2670                         this.selectionStatusToolbar( view );
2671
2672                         view.set( 'gallery', {
2673                                 style:    'primary',
2674                                 text:     l10n.createNewGallery,
2675                                 priority: 60,
2676                                 requires: { selection: true },
2677
2678                                 click: function() {
2679                                         var selection = controller.state().get('selection'),
2680                                                 edit = controller.state('gallery-edit'),
2681                                                 models = selection.where({ type: 'image' });
2682
2683                                         edit.set( 'library', new media.model.Selection( models, {
2684                                                 props:    selection.props.toJSON(),
2685                                                 multiple: true
2686                                         }) );
2687
2688                                         this.controller.setState('gallery-edit');
2689
2690                                         // Keep focus inside media modal
2691                                         // after jumping to gallery view
2692                                         this.controller.modal.focusManager.focus();
2693                                 }
2694                         });
2695                 },
2696
2697                 mainPlaylistToolbar: function( view ) {
2698                         var controller = this;
2699
2700                         this.selectionStatusToolbar( view );
2701
2702                         view.set( 'playlist', {
2703                                 style:    'primary',
2704                                 text:     l10n.createNewPlaylist,
2705                                 priority: 100,
2706                                 requires: { selection: true },
2707
2708                                 click: function() {
2709                                         var selection = controller.state().get('selection'),
2710                                                 edit = controller.state('playlist-edit'),
2711                                                 models = selection.where({ type: 'audio' });
2712
2713                                         edit.set( 'library', new media.model.Selection( models, {
2714                                                 props:    selection.props.toJSON(),
2715                                                 multiple: true
2716                                         }) );
2717
2718                                         this.controller.setState('playlist-edit');
2719
2720                                         // Keep focus inside media modal
2721                                         // after jumping to playlist view
2722                                         this.controller.modal.focusManager.focus();
2723                                 }
2724                         });
2725                 },
2726
2727                 mainVideoPlaylistToolbar: function( view ) {
2728                         var controller = this;
2729
2730                         this.selectionStatusToolbar( view );
2731
2732                         view.set( 'video-playlist', {
2733                                 style:    'primary',
2734                                 text:     l10n.createNewVideoPlaylist,
2735                                 priority: 100,
2736                                 requires: { selection: true },
2737
2738                                 click: function() {
2739                                         var selection = controller.state().get('selection'),
2740                                                 edit = controller.state('video-playlist-edit'),
2741                                                 models = selection.where({ type: 'video' });
2742
2743                                         edit.set( 'library', new media.model.Selection( models, {
2744                                                 props:    selection.props.toJSON(),
2745                                                 multiple: true
2746                                         }) );
2747
2748                                         this.controller.setState('video-playlist-edit');
2749
2750                                         // Keep focus inside media modal
2751                                         // after jumping to video playlist view
2752                                         this.controller.modal.focusManager.focus();
2753                                 }
2754                         });
2755                 },
2756
2757                 featuredImageToolbar: function( toolbar ) {
2758                         this.createSelectToolbar( toolbar, {
2759                                 text:  l10n.setFeaturedImage,
2760                                 state: this.options.state
2761                         });
2762                 },
2763
2764                 mainEmbedToolbar: function( toolbar ) {
2765                         toolbar.view = new media.view.Toolbar.Embed({
2766                                 controller: this
2767                         });
2768                 },
2769
2770                 galleryEditToolbar: function() {
2771                         var editing = this.state().get('editing');
2772                         this.toolbar.set( new media.view.Toolbar({
2773                                 controller: this,
2774                                 items: {
2775                                         insert: {
2776                                                 style:    'primary',
2777                                                 text:     editing ? l10n.updateGallery : l10n.insertGallery,
2778                                                 priority: 80,
2779                                                 requires: { library: true },
2780
2781                                                 /**
2782                                                  * @fires wp.media.controller.State#update
2783                                                  */
2784                                                 click: function() {
2785                                                         var controller = this.controller,
2786                                                                 state = controller.state();
2787
2788                                                         controller.close();
2789                                                         state.trigger( 'update', state.get('library') );
2790
2791                                                         // Restore and reset the default state.
2792                                                         controller.setState( controller.options.state );
2793                                                         controller.reset();
2794                                                 }
2795                                         }
2796                                 }
2797                         }) );
2798                 },
2799
2800                 galleryAddToolbar: function() {
2801                         this.toolbar.set( new media.view.Toolbar({
2802                                 controller: this,
2803                                 items: {
2804                                         insert: {
2805                                                 style:    'primary',
2806                                                 text:     l10n.addToGallery,
2807                                                 priority: 80,
2808                                                 requires: { selection: true },
2809
2810                                                 /**
2811                                                  * @fires wp.media.controller.State#reset
2812                                                  */
2813                                                 click: function() {
2814                                                         var controller = this.controller,
2815                                                                 state = controller.state(),
2816                                                                 edit = controller.state('gallery-edit');
2817
2818                                                         edit.get('library').add( state.get('selection').models );
2819                                                         state.trigger('reset');
2820                                                         controller.setState('gallery-edit');
2821                                                 }
2822                                         }
2823                                 }
2824                         }) );
2825                 },
2826
2827                 playlistEditToolbar: function() {
2828                         var editing = this.state().get('editing');
2829                         this.toolbar.set( new media.view.Toolbar({
2830                                 controller: this,
2831                                 items: {
2832                                         insert: {
2833                                                 style:    'primary',
2834                                                 text:     editing ? l10n.updatePlaylist : l10n.insertPlaylist,
2835                                                 priority: 80,
2836                                                 requires: { library: true },
2837
2838                                                 /**
2839                                                  * @fires wp.media.controller.State#update
2840                                                  */
2841                                                 click: function() {
2842                                                         var controller = this.controller,
2843                                                                 state = controller.state();
2844
2845                                                         controller.close();
2846                                                         state.trigger( 'update', state.get('library') );
2847
2848                                                         // Restore and reset the default state.
2849                                                         controller.setState( controller.options.state );
2850                                                         controller.reset();
2851                                                 }
2852                                         }
2853                                 }
2854                         }) );
2855                 },
2856
2857                 playlistAddToolbar: function() {
2858                         this.toolbar.set( new media.view.Toolbar({
2859                                 controller: this,
2860                                 items: {
2861                                         insert: {
2862                                                 style:    'primary',
2863                                                 text:     l10n.addToPlaylist,
2864                                                 priority: 80,
2865                                                 requires: { selection: true },
2866
2867                                                 /**
2868                                                  * @fires wp.media.controller.State#reset
2869                                                  */
2870                                                 click: function() {
2871                                                         var controller = this.controller,
2872                                                                 state = controller.state(),
2873                                                                 edit = controller.state('playlist-edit');
2874
2875                                                         edit.get('library').add( state.get('selection').models );
2876                                                         state.trigger('reset');
2877                                                         controller.setState('playlist-edit');
2878                                                 }
2879                                         }
2880                                 }
2881                         }) );
2882                 },
2883
2884                 videoPlaylistEditToolbar: function() {
2885                         var editing = this.state().get('editing');
2886                         this.toolbar.set( new media.view.Toolbar({
2887                                 controller: this,
2888                                 items: {
2889                                         insert: {
2890                                                 style:    'primary',
2891                                                 text:     editing ? l10n.updateVideoPlaylist : l10n.insertVideoPlaylist,
2892                                                 priority: 140,
2893                                                 requires: { library: true },
2894
2895                                                 click: function() {
2896                                                         var controller = this.controller,
2897                                                                 state = controller.state(),
2898                                                                 library = state.get('library');
2899
2900                                                         library.type = 'video';
2901
2902                                                         controller.close();
2903                                                         state.trigger( 'update', library );
2904
2905                                                         // Restore and reset the default state.
2906                                                         controller.setState( controller.options.state );
2907                                                         controller.reset();
2908                                                 }
2909                                         }
2910                                 }
2911                         }) );
2912                 },
2913
2914                 videoPlaylistAddToolbar: function() {
2915                         this.toolbar.set( new media.view.Toolbar({
2916                                 controller: this,
2917                                 items: {
2918                                         insert: {
2919                                                 style:    'primary',
2920                                                 text:     l10n.addToVideoPlaylist,
2921                                                 priority: 140,
2922                                                 requires: { selection: true },
2923
2924                                                 click: function() {
2925                                                         var controller = this.controller,
2926                                                                 state = controller.state(),
2927                                                                 edit = controller.state('video-playlist-edit');
2928
2929                                                         edit.get('library').add( state.get('selection').models );
2930                                                         state.trigger('reset');
2931                                                         controller.setState('video-playlist-edit');
2932                                                 }
2933                                         }
2934                                 }
2935                         }) );
2936                 }
2937         });
2938
2939         /**
2940          * wp.media.view.MediaFrame.ImageDetails
2941          *
2942          * @constructor
2943          * @augments wp.media.view.MediaFrame.Select
2944          * @augments wp.media.view.MediaFrame
2945          * @augments wp.media.view.Frame
2946          * @augments wp.media.View
2947          * @augments wp.Backbone.View
2948          * @augments Backbone.View
2949          * @mixes wp.media.controller.StateMachine
2950          */
2951         media.view.MediaFrame.ImageDetails = media.view.MediaFrame.Select.extend({
2952                 defaults: {
2953                         id:      'image',
2954                         url:     '',
2955                         menu:    'image-details',
2956                         content: 'image-details',
2957                         toolbar: 'image-details',
2958                         type:    'link',
2959                         title:    l10n.imageDetailsTitle,
2960                         priority: 120
2961                 },
2962
2963                 initialize: function( options ) {
2964                         this.image = new media.model.PostImage( options.metadata );
2965                         this.options.selection = new media.model.Selection( this.image.attachment, { multiple: false } );
2966                         media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
2967                 },
2968
2969                 bindHandlers: function() {
2970                         media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
2971                         this.on( 'menu:create:image-details', this.createMenu, this );
2972                         this.on( 'content:create:image-details', this.imageDetailsContent, this );
2973                         this.on( 'content:render:edit-image', this.editImageContent, this );
2974                         this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this );
2975                         // override the select toolbar
2976                         this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this );
2977                 },
2978
2979                 createStates: function() {
2980                         this.states.add([
2981                                 new media.controller.ImageDetails({
2982                                         image: this.image,
2983                                         editable: false
2984                                 }),
2985                                 new media.controller.ReplaceImage({
2986                                         id: 'replace-image',
2987                                         library:   media.query( { type: 'image' } ),
2988                                         image: this.image,
2989                                         multiple:  false,
2990                                         title:     l10n.imageReplaceTitle,
2991                                         toolbar: 'replace',
2992                                         priority:  80,
2993                                         displaySettings: true
2994                                 }),
2995                                 new media.controller.EditImage( {
2996                                         image: this.image,
2997                                         selection: this.options.selection
2998                                 } )
2999                         ]);
3000                 },
3001
3002                 imageDetailsContent: function( options ) {
3003                         options.view = new media.view.ImageDetails({
3004                                 controller: this,
3005                                 model: this.state().image,
3006                                 attachment: this.state().image.attachment
3007                         });
3008                 },
3009
3010                 editImageContent: function() {
3011                         var state = this.state(),
3012                                 model = state.get('image'),
3013                                 view;
3014
3015                         if ( ! model ) {
3016                                 return;
3017                         }
3018
3019                         view = new media.view.EditImage( { model: model, controller: this } ).render();
3020
3021                         this.content.set( view );
3022
3023                         // after bringing in the frame, load the actual editor via an ajax call
3024                         view.loadEditor();
3025
3026                 },
3027
3028                 renderImageDetailsToolbar: function() {
3029                         this.toolbar.set( new media.view.Toolbar({
3030                                 controller: this,
3031                                 items: {
3032                                         select: {
3033                                                 style:    'primary',
3034                                                 text:     l10n.update,
3035                                                 priority: 80,
3036
3037                                                 click: function() {
3038                                                         var controller = this.controller,
3039                                                                 state = controller.state();
3040
3041                                                         controller.close();
3042
3043                                                         // not sure if we want to use wp.media.string.image which will create a shortcode or
3044                                                         // perhaps wp.html.string to at least to build the <img />
3045                                                         state.trigger( 'update', controller.image.toJSON() );
3046
3047                                                         // Restore and reset the default state.
3048                                                         controller.setState( controller.options.state );
3049                                                         controller.reset();
3050                                                 }
3051                                         }
3052                                 }
3053                         }) );
3054                 },
3055
3056                 renderReplaceImageToolbar: function() {
3057                         var frame = this,
3058                                 lastState = frame.lastState(),
3059                                 previous = lastState && lastState.id;
3060
3061                         this.toolbar.set( new media.view.Toolbar({
3062                                 controller: this,
3063                                 items: {
3064                                         back: {
3065                                                 text:     l10n.back,
3066                                                 priority: 20,
3067                                                 click:    function() {
3068                                                         if ( previous ) {
3069                                                                 frame.setState( previous );
3070                                                         } else {
3071                                                                 frame.close();
3072                                                         }
3073                                                 }
3074                                         },
3075
3076                                         replace: {
3077                                                 style:    'primary',
3078                                                 text:     l10n.replace,
3079                                                 priority: 80,
3080
3081                                                 click: function() {
3082                                                         var controller = this.controller,
3083                                                                 state = controller.state(),
3084                                                                 selection = state.get( 'selection' ),
3085                                                                 attachment = selection.single();
3086
3087                                                         controller.close();
3088
3089                                                         controller.image.changeAttachment( attachment, state.display( attachment ) );
3090
3091                                                         // not sure if we want to use wp.media.string.image which will create a shortcode or
3092                                                         // perhaps wp.html.string to at least to build the <img />
3093                                                         state.trigger( 'replace', controller.image.toJSON() );
3094
3095                                                         // Restore and reset the default state.
3096                                                         controller.setState( controller.options.state );
3097                                                         controller.reset();
3098                                                 }
3099                                         }
3100                                 }
3101                         }) );
3102                 }
3103
3104         });
3105
3106         /**
3107          * wp.media.view.Modal
3108          *
3109          * @constructor
3110          * @augments wp.media.View
3111          * @augments wp.Backbone.View
3112          * @augments Backbone.View
3113          */
3114         media.view.Modal = media.View.extend({
3115                 tagName:  'div',
3116                 template: media.template('media-modal'),
3117
3118                 attributes: {
3119                         tabindex: 0
3120                 },
3121
3122                 events: {
3123                         'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
3124                         'keydown': 'keydown'
3125                 },
3126
3127                 initialize: function() {
3128                         _.defaults( this.options, {
3129                                 container: document.body,
3130                                 title:     '',
3131                                 propagate: true,
3132                                 freeze:    true
3133                         });
3134
3135                         this.focusManager = new media.view.FocusManager({
3136                                 el: this.el
3137                         });
3138                 },
3139                 /**
3140                  * @returns {Object}
3141                  */
3142                 prepare: function() {
3143                         return {
3144                                 title: this.options.title
3145                         };
3146                 },
3147
3148                 /**
3149                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3150                  */
3151                 attach: function() {
3152                         if ( this.views.attached ) {
3153                                 return this;
3154                         }
3155
3156                         if ( ! this.views.rendered ) {
3157                                 this.render();
3158                         }
3159
3160                         this.$el.appendTo( this.options.container );
3161
3162                         // Manually mark the view as attached and trigger ready.
3163                         this.views.attached = true;
3164                         this.views.ready();
3165
3166                         return this.propagate('attach');
3167                 },
3168
3169                 /**
3170                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3171                  */
3172                 detach: function() {
3173                         if ( this.$el.is(':visible') ) {
3174                                 this.close();
3175                         }
3176
3177                         this.$el.detach();
3178                         this.views.attached = false;
3179                         return this.propagate('detach');
3180                 },
3181
3182                 /**
3183                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3184                  */
3185                 open: function() {
3186                         var $el = this.$el,
3187                                 options = this.options,
3188                                 mceEditor;
3189
3190                         if ( $el.is(':visible') ) {
3191                                 return this;
3192                         }
3193
3194                         if ( ! this.views.attached ) {
3195                                 this.attach();
3196                         }
3197
3198                         // If the `freeze` option is set, record the window's scroll position.
3199                         if ( options.freeze ) {
3200                                 this._freeze = {
3201                                         scrollTop: $( window ).scrollTop()
3202                                 };
3203                         }
3204
3205                         // Disable page scrolling.
3206                         $( 'body' ).addClass( 'modal-open' );
3207
3208                         $el.show();
3209
3210                         // Try to close the onscreen keyboard
3211                         if ( 'ontouchend' in document ) {
3212                                 if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor )  && ! mceEditor.isHidden() && mceEditor.iframeElement ) {
3213                                         mceEditor.iframeElement.focus();
3214                                         mceEditor.iframeElement.blur();
3215
3216                                         setTimeout( function() {
3217                                                 mceEditor.iframeElement.blur();
3218                                         }, 100 );
3219                                 }
3220                         }
3221
3222                         this.$el.focus();
3223
3224                         return this.propagate('open');
3225                 },
3226
3227                 /**
3228                  * @param {Object} options
3229                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3230                  */
3231                 close: function( options ) {
3232                         var freeze = this._freeze;
3233
3234                         if ( ! this.views.attached || ! this.$el.is(':visible') ) {
3235                                 return this;
3236                         }
3237
3238                         // Enable page scrolling.
3239                         $( 'body' ).removeClass( 'modal-open' );
3240
3241                         // Hide modal and remove restricted media modal tab focus once it's closed
3242                         this.$el.hide().undelegate( 'keydown' );
3243
3244                         // Put focus back in useful location once modal is closed
3245                         $('#wpbody-content').focus();
3246
3247                         this.propagate('close');
3248
3249                         // If the `freeze` option is set, restore the container's scroll position.
3250                         if ( freeze ) {
3251                                 $( window ).scrollTop( freeze.scrollTop );
3252                         }
3253
3254                         if ( options && options.escape ) {
3255                                 this.propagate('escape');
3256                         }
3257
3258                         return this;
3259                 },
3260                 /**
3261                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3262                  */
3263                 escape: function() {
3264                         return this.close({ escape: true });
3265                 },
3266                 /**
3267                  * @param {Object} event
3268                  */
3269                 escapeHandler: function( event ) {
3270                         event.preventDefault();
3271                         this.escape();
3272                 },
3273
3274                 /**
3275                  * @param {Array|Object} content Views to register to '.media-modal-content'
3276                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3277                  */
3278                 content: function( content ) {
3279                         this.views.set( '.media-modal-content', content );
3280                         return this;
3281                 },
3282
3283                 /**
3284                  * Triggers a modal event and if the `propagate` option is set,
3285                  * forwards events to the modal's controller.
3286                  *
3287                  * @param {string} id
3288                  * @returns {wp.media.view.Modal} Returns itself to allow chaining
3289                  */
3290                 propagate: function( id ) {
3291                         this.trigger( id );
3292
3293                         if ( this.options.propagate ) {
3294                                 this.controller.trigger( id );
3295                         }
3296
3297                         return this;
3298                 },
3299                 /**
3300                  * @param {Object} event
3301                  */
3302                 keydown: function( event ) {
3303                         // Close the modal when escape is pressed.
3304                         if ( 27 === event.which && this.$el.is(':visible') ) {
3305                                 this.escape();
3306                                 event.stopImmediatePropagation();
3307                         }
3308                 }
3309         });
3310
3311         /**
3312          * wp.media.view.FocusManager
3313          *
3314          * @constructor
3315          * @augments wp.media.View
3316          * @augments wp.Backbone.View
3317          * @augments Backbone.View
3318          */
3319         media.view.FocusManager = media.View.extend({
3320
3321                 events: {
3322                         'keydown': 'constrainTabbing'
3323                 },
3324
3325                 focus: function() { // Reset focus on first left menu item
3326                         this.$('.media-menu-item').first().focus();
3327                 },
3328                 /**
3329                  * @param {Object} event
3330                  */
3331                 constrainTabbing: function( event ) {
3332                         var tabbables;
3333
3334                         // Look for the tab key.
3335                         if ( 9 !== event.keyCode ) {
3336                                 return;
3337                         }
3338
3339                         tabbables = this.$( ':tabbable' );
3340
3341                         // Keep tab focus within media modal while it's open
3342                         if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
3343                                 tabbables.first().focus();
3344                                 return false;
3345                         } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
3346                                 tabbables.last().focus();
3347                                 return false;
3348                         }
3349                 }
3350
3351         });
3352
3353         /**
3354          * wp.media.view.UploaderWindow
3355          *
3356          * @constructor
3357          * @augments wp.media.View
3358          * @augments wp.Backbone.View
3359          * @augments Backbone.View
3360          */
3361         media.view.UploaderWindow = media.View.extend({
3362                 tagName:   'div',
3363                 className: 'uploader-window',
3364                 template:  media.template('uploader-window'),
3365
3366                 initialize: function() {
3367                         var uploader;
3368
3369                         this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
3370
3371                         uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
3372                                 dropzone:  this.$el,
3373                                 browser:   this.$browser,
3374                                 params:    {}
3375                         });
3376
3377                         // Ensure the dropzone is a jQuery collection.
3378                         if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) {
3379                                 uploader.dropzone = $( uploader.dropzone );
3380                         }
3381
3382                         this.controller.on( 'activate', this.refresh, this );
3383
3384                         this.controller.on( 'detach', function() {
3385                                 this.$browser.remove();
3386                         }, this );
3387                 },
3388
3389                 refresh: function() {
3390                         if ( this.uploader ) {
3391                                 this.uploader.refresh();
3392                         }
3393                 },
3394
3395                 ready: function() {
3396                         var postId = media.view.settings.post.id,
3397                                 dropzone;
3398
3399                         // If the uploader already exists, bail.
3400                         if ( this.uploader ) {
3401                                 return;
3402                         }
3403
3404                         if ( postId ) {
3405                                 this.options.uploader.params.post_id = postId;
3406                         }
3407                         this.uploader = new wp.Uploader( this.options.uploader );
3408
3409                         dropzone = this.uploader.dropzone;
3410                         dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
3411                         dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
3412
3413                         $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) );
3414                 },
3415
3416                 _ready: function() {
3417                         this.controller.trigger( 'uploader:ready' );
3418                 },
3419
3420                 show: function() {
3421                         var $el = this.$el.show();
3422
3423                         // Ensure that the animation is triggered by waiting until
3424                         // the transparent element is painted into the DOM.
3425                         _.defer( function() {
3426                                 $el.css({ opacity: 1 });
3427                         });
3428                 },
3429
3430                 hide: function() {
3431                         var $el = this.$el.css({ opacity: 0 });
3432
3433                         media.transition( $el ).done( function() {
3434                                 // Transition end events are subject to race conditions.
3435                                 // Make sure that the value is set as intended.
3436                                 if ( '0' === $el.css('opacity') ) {
3437                                         $el.hide();
3438                                 }
3439                         });
3440
3441                         // https://core.trac.wordpress.org/ticket/27341
3442                         _.delay( function() {
3443                                 if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
3444                                         $el.hide();
3445                                 }
3446                         }, 500 );
3447                 }
3448         });
3449
3450         /**
3451          * wp.media.view.EditorUploader
3452          *
3453          * @constructor
3454          * @augments wp.media.View
3455          * @augments wp.Backbone.View
3456          * @augments Backbone.View
3457          */
3458         media.view.EditorUploader = media.View.extend({
3459                 tagName:   'div',
3460                 className: 'uploader-editor',
3461                 template:  media.template( 'uploader-editor' ),
3462
3463                 localDrag: false,
3464                 overContainer: false,
3465                 overDropzone: false,
3466                 draggingFile: null,
3467
3468                 initialize: function() {
3469                         var self = this;
3470
3471                         this.initialized = false;
3472
3473                         // Bail if not enabled or UA does not support drag'n'drop or File API.
3474                         if ( ! window.tinyMCEPreInit || ! window.tinyMCEPreInit.dragDropUpload || ! this.browserSupport() ) {
3475                                 return this;
3476                         }
3477
3478                         this.$document = $(document);
3479                         this.dropzones = [];
3480                         this.files = [];
3481
3482                         this.$document.on( 'drop', '.uploader-editor', _.bind( this.drop, this ) );
3483                         this.$document.on( 'dragover', '.uploader-editor', _.bind( this.dropzoneDragover, this ) );
3484                         this.$document.on( 'dragleave', '.uploader-editor', _.bind( this.dropzoneDragleave, this ) );
3485                         this.$document.on( 'click', '.uploader-editor', _.bind( this.click, this ) );
3486
3487                         this.$document.on( 'dragover', _.bind( this.containerDragover, this ) );
3488                         this.$document.on( 'dragleave', _.bind( this.containerDragleave, this ) );
3489
3490                         this.$document.on( 'dragstart dragend drop', function( event ) {
3491                                 self.localDrag = event.type === 'dragstart';
3492                         });
3493
3494                         this.initialized = true;
3495                         return this;
3496                 },
3497
3498                 browserSupport: function() {
3499                         var supports = false, div = document.createElement('div');
3500
3501                         supports = ( 'draggable' in div ) || ( 'ondragstart' in div && 'ondrop' in div );
3502                         supports = supports && !! ( window.File && window.FileList && window.FileReader );
3503                         return supports;
3504                 },
3505
3506                 isDraggingFile: function( event ) {
3507                         if ( this.draggingFile !== null ) {
3508                                 return this.draggingFile;
3509                         }
3510
3511                         if ( _.isUndefined( event.originalEvent ) || _.isUndefined( event.originalEvent.dataTransfer ) ) {
3512                                 return false;
3513                         }
3514
3515                         this.draggingFile = _.indexOf( event.originalEvent.dataTransfer.types, 'Files' ) > -1 &&
3516                                 _.indexOf( event.originalEvent.dataTransfer.types, 'text/plain' ) === -1;
3517
3518                         return this.draggingFile;
3519                 },
3520
3521                 refresh: function( e ) {
3522                         var dropzone_id;
3523                         for ( dropzone_id in this.dropzones ) {
3524                                 // Hide the dropzones only if dragging has left the screen.
3525                                 this.dropzones[ dropzone_id ].toggle( this.overContainer || this.overDropzone );
3526                         }
3527
3528                         if ( ! _.isUndefined( e ) ) {
3529                                 $( e.target ).closest( '.uploader-editor' ).toggleClass( 'droppable', this.overDropzone );
3530                         }
3531
3532                         if ( ! this.overContainer && ! this.overDropzone ) {
3533                                 this.draggingFile = null;
3534                         }
3535
3536                         return this;
3537                 },
3538
3539                 render: function() {
3540                         if ( ! this.initialized ) {
3541                                 return this;
3542                         }
3543
3544                         media.View.prototype.render.apply( this, arguments );
3545                         $( '.wp-editor-wrap, #wp-fullscreen-body' ).each( _.bind( this.attach, this ) );
3546                         return this;
3547                 },
3548
3549                 attach: function( index, editor ) {
3550                         // Attach a dropzone to an editor.
3551                         var dropzone = this.$el.clone();
3552                         this.dropzones.push( dropzone );
3553                         $( editor ).append( dropzone );
3554                         return this;
3555                 },
3556
3557                 drop: function( event ) {
3558                         var $wrap = null;
3559
3560                         this.containerDragleave( event );
3561                         this.dropzoneDragleave( event );
3562
3563                         this.files = event.originalEvent.dataTransfer.files;
3564                         if ( this.files.length < 1 ) {
3565                                 return;
3566                         }
3567
3568                         // Set the active editor to the drop target.
3569                         $wrap = $( event.target ).parents( '.wp-editor-wrap' );
3570                         if ( $wrap.length > 0 && $wrap[0].id ) {
3571                                 window.wpActiveEditor = $wrap[0].id.slice( 3, -5 );
3572                         }
3573
3574                         if ( ! this.workflow ) {
3575                                 this.workflow = wp.media.editor.open( 'content', {
3576                                         frame:    'post',
3577                                         state:    'insert',
3578                                         title:    wp.media.view.l10n.addMedia,
3579                                         multiple: true
3580                                 });
3581                                 this.workflow.on( 'uploader:ready', this.addFiles, this );
3582                         } else {
3583                                 this.workflow.state().reset();
3584                                 this.addFiles.apply( this );
3585                                 this.workflow.open();
3586                         }
3587
3588                         return false;
3589                 },
3590
3591                 addFiles: function() {
3592                         if ( this.files.length ) {
3593                                 this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) );
3594                                 this.files = [];
3595                         }
3596                         return this;
3597                 },
3598
3599                 containerDragover: function( event ) {
3600                         if ( this.localDrag || ! this.isDraggingFile( event ) ) {
3601                                 return;
3602                         }
3603
3604                         this.overContainer = true;
3605                         this.refresh();
3606                 },
3607
3608                 containerDragleave: function() {
3609                         this.overContainer = false;
3610
3611                         // Throttle dragleave because it's called when bouncing from some elements to others.
3612                         _.delay( _.bind( this.refresh, this ), 50 );
3613                 },
3614
3615                 dropzoneDragover: function( event ) {
3616                         if ( this.localDrag || ! this.isDraggingFile( event ) ) {
3617                                 return;
3618                         }
3619
3620                         this.overDropzone = true;
3621                         this.refresh( event );
3622                         return false;
3623                 },
3624
3625                 dropzoneDragleave: function( e ) {
3626                         this.overDropzone = false;
3627                         _.delay( _.bind( this.refresh, this, e ), 50 );
3628                 },
3629
3630                 click: function( e ) {
3631                         // In the rare case where the dropzone gets stuck, hide it on click.
3632                         this.containerDragleave( e );
3633                         this.dropzoneDragleave( e );
3634                         this.localDrag = false;
3635                 }
3636         });
3637
3638         /**
3639          * wp.media.view.UploaderInline
3640          *
3641          * @constructor
3642          * @augments wp.media.View
3643          * @augments wp.Backbone.View
3644          * @augments Backbone.View
3645          */
3646         media.view.UploaderInline = media.View.extend({
3647                 tagName:   'div',
3648                 className: 'uploader-inline',
3649                 template:  media.template('uploader-inline'),
3650
3651                 events: {
3652                         'click .close': 'hide'
3653                 },
3654
3655                 initialize: function() {
3656                         _.defaults( this.options, {
3657                                 message: '',
3658                                 status:  true,
3659                                 canClose: false
3660                         });
3661
3662                         if ( ! this.options.$browser && this.controller.uploader ) {
3663                                 this.options.$browser = this.controller.uploader.$browser;
3664                         }
3665
3666                         if ( _.isUndefined( this.options.postId ) ) {
3667                                 this.options.postId = media.view.settings.post.id;
3668                         }
3669
3670                         if ( this.options.status ) {
3671                                 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
3672                                         controller: this.controller
3673                                 }) );
3674                         }
3675                 },
3676
3677                 prepare: function() {
3678                         var suggestedWidth = this.controller.state().get('suggestedWidth'),
3679                                 suggestedHeight = this.controller.state().get('suggestedHeight'),
3680                                 data = {};
3681
3682                         data.message = this.options.message;
3683                         data.canClose = this.options.canClose;
3684
3685                         if ( suggestedWidth && suggestedHeight ) {
3686                                 data.suggestedWidth = suggestedWidth;
3687                                 data.suggestedHeight = suggestedHeight;
3688                         }
3689
3690                         return data;
3691                 },
3692                 /**
3693                  * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
3694                  */
3695                 dispose: function() {
3696                         if ( this.disposing ) {
3697                                 /**
3698                                  * call 'dispose' directly on the parent class
3699                                  */
3700                                 return media.View.prototype.dispose.apply( this, arguments );
3701                         }
3702
3703                         // Run remove on `dispose`, so we can be sure to refresh the
3704                         // uploader with a view-less DOM. Track whether we're disposing
3705                         // so we don't trigger an infinite loop.
3706                         this.disposing = true;
3707                         return this.remove();
3708                 },
3709                 /**
3710                  * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
3711                  */
3712                 remove: function() {
3713                         /**
3714                          * call 'remove' directly on the parent class
3715                          */
3716                         var result = media.View.prototype.remove.apply( this, arguments );
3717
3718                         _.defer( _.bind( this.refresh, this ) );
3719                         return result;
3720                 },
3721
3722                 refresh: function() {
3723                         var uploader = this.controller.uploader;
3724
3725                         if ( uploader ) {
3726                                 uploader.refresh();
3727                         }
3728                 },
3729                 /**
3730                  * @returns {wp.media.view.UploaderInline}
3731                  */
3732                 ready: function() {
3733                         var $browser = this.options.$browser,
3734                                 $placeholder;
3735
3736                         if ( this.controller.uploader ) {
3737                                 $placeholder = this.$('.browser');
3738
3739                                 // Check if we've already replaced the placeholder.
3740                                 if ( $placeholder[0] === $browser[0] ) {
3741                                         return;
3742                                 }
3743
3744                                 $browser.detach().text( $placeholder.text() );
3745                                 $browser[0].className = $placeholder[0].className;
3746                                 $placeholder.replaceWith( $browser.show() );
3747                         }
3748
3749                         this.refresh();
3750                         return this;
3751                 },
3752                 show: function() {
3753                         this.$el.removeClass( 'hidden' );
3754                 },
3755                 hide: function() {
3756                         this.$el.addClass( 'hidden' );
3757                 }
3758
3759         });
3760
3761         /**
3762          * wp.media.view.UploaderStatus
3763          *
3764          * @constructor
3765          * @augments wp.media.View
3766          * @augments wp.Backbone.View
3767          * @augments Backbone.View
3768          */
3769         media.view.UploaderStatus = media.View.extend({
3770                 className: 'media-uploader-status',
3771                 template:  media.template('uploader-status'),
3772
3773                 events: {
3774                         'click .upload-dismiss-errors': 'dismiss'
3775                 },
3776
3777                 initialize: function() {
3778                         this.queue = wp.Uploader.queue;
3779                         this.queue.on( 'add remove reset', this.visibility, this );
3780                         this.queue.on( 'add remove reset change:percent', this.progress, this );
3781                         this.queue.on( 'add remove reset change:uploading', this.info, this );
3782
3783                         this.errors = wp.Uploader.errors;
3784                         this.errors.reset();
3785                         this.errors.on( 'add remove reset', this.visibility, this );
3786                         this.errors.on( 'add', this.error, this );
3787                 },
3788                 /**
3789                  * @global wp.Uploader
3790                  * @returns {wp.media.view.UploaderStatus}
3791                  */
3792                 dispose: function() {
3793                         wp.Uploader.queue.off( null, null, this );
3794                         /**
3795                          * call 'dispose' directly on the parent class
3796                          */
3797                         media.View.prototype.dispose.apply( this, arguments );
3798                         return this;
3799                 },
3800
3801                 visibility: function() {
3802                         this.$el.toggleClass( 'uploading', !! this.queue.length );
3803                         this.$el.toggleClass( 'errors', !! this.errors.length );
3804                         this.$el.toggle( !! this.queue.length || !! this.errors.length );
3805                 },
3806
3807                 ready: function() {
3808                         _.each({
3809                                 '$bar':      '.media-progress-bar div',
3810                                 '$index':    '.upload-index',
3811                                 '$total':    '.upload-total',
3812                                 '$filename': '.upload-filename'
3813                         }, function( selector, key ) {
3814                                 this[ key ] = this.$( selector );
3815                         }, this );
3816
3817                         this.visibility();
3818                         this.progress();
3819                         this.info();
3820                 },
3821
3822                 progress: function() {
3823                         var queue = this.queue,
3824                                 $bar = this.$bar;
3825
3826                         if ( ! $bar || ! queue.length ) {
3827                                 return;
3828                         }
3829
3830                         $bar.width( ( queue.reduce( function( memo, attachment ) {
3831                                 if ( ! attachment.get('uploading') ) {
3832                                         return memo + 100;
3833                                 }
3834
3835                                 var percent = attachment.get('percent');
3836                                 return memo + ( _.isNumber( percent ) ? percent : 100 );
3837                         }, 0 ) / queue.length ) + '%' );
3838                 },
3839
3840                 info: function() {
3841                         var queue = this.queue,
3842                                 index = 0, active;
3843
3844                         if ( ! queue.length ) {
3845                                 return;
3846                         }
3847
3848                         active = this.queue.find( function( attachment, i ) {
3849                                 index = i;
3850                                 return attachment.get('uploading');
3851                         });
3852
3853                         this.$index.text( index + 1 );
3854                         this.$total.text( queue.length );
3855                         this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
3856                 },
3857                 /**
3858                  * @param {string} filename
3859                  * @returns {string}
3860                  */
3861                 filename: function( filename ) {
3862                         return media.truncate( _.escape( filename ), 24 );
3863                 },
3864                 /**
3865                  * @param {Backbone.Model} error
3866                  */
3867                 error: function( error ) {
3868                         this.views.add( '.upload-errors', new media.view.UploaderStatusError({
3869                                 filename: this.filename( error.get('file').name ),
3870                                 message:  error.get('message')
3871                         }), { at: 0 });
3872                 },
3873
3874                 /**
3875                  * @global wp.Uploader
3876                  *
3877                  * @param {Object} event
3878                  */
3879                 dismiss: function( event ) {
3880                         var errors = this.views.get('.upload-errors');
3881
3882                         event.preventDefault();
3883
3884                         if ( errors ) {
3885                                 _.invoke( errors, 'remove' );
3886                         }
3887                         wp.Uploader.errors.reset();
3888                 }
3889         });
3890
3891         /**
3892          * wp.media.view.UploaderStatusError
3893          *
3894          * @constructor
3895          * @augments wp.media.View
3896          * @augments wp.Backbone.View
3897          * @augments Backbone.View
3898          */
3899         media.view.UploaderStatusError = media.View.extend({
3900                 className: 'upload-error',
3901                 template:  media.template('uploader-status-error')
3902         });
3903
3904         /**
3905          * wp.media.view.Toolbar
3906          *
3907          * @constructor
3908          * @augments wp.media.View
3909          * @augments wp.Backbone.View
3910          * @augments Backbone.View
3911          */
3912         media.view.Toolbar = media.View.extend({
3913                 tagName:   'div',
3914                 className: 'media-toolbar',
3915
3916                 initialize: function() {
3917                         var state = this.controller.state(),
3918                                 selection = this.selection = state.get('selection'),
3919                                 library = this.library = state.get('library');
3920
3921                         this._views = {};
3922
3923                         // The toolbar is composed of two `PriorityList` views.
3924                         this.primary   = new media.view.PriorityList();
3925                         this.secondary = new media.view.PriorityList();
3926                         this.primary.$el.addClass('media-toolbar-primary search-form');
3927                         this.secondary.$el.addClass('media-toolbar-secondary');
3928
3929                         this.views.set([ this.secondary, this.primary ]);
3930
3931                         if ( this.options.items ) {
3932                                 this.set( this.options.items, { silent: true });
3933                         }
3934
3935                         if ( ! this.options.silent ) {
3936                                 this.render();
3937                         }
3938
3939                         if ( selection ) {
3940                                 selection.on( 'add remove reset', this.refresh, this );
3941                         }
3942
3943                         if ( library ) {
3944                                 library.on( 'add remove reset', this.refresh, this );
3945                         }
3946                 },
3947                 /**
3948                  * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining
3949                  */
3950                 dispose: function() {
3951                         if ( this.selection ) {
3952                                 this.selection.off( null, null, this );
3953                         }
3954
3955                         if ( this.library ) {
3956                                 this.library.off( null, null, this );
3957                         }
3958                         /**
3959                          * call 'dispose' directly on the parent class
3960                          */
3961                         return media.View.prototype.dispose.apply( this, arguments );
3962                 },
3963
3964                 ready: function() {
3965                         this.refresh();
3966                 },
3967
3968                 /**
3969                  * @param {string} id
3970                  * @param {Backbone.View|Object} view
3971                  * @param {Object} [options={}]
3972                  * @returns {wp.media.view.Toolbar} Returns itself to allow chaining
3973                  */
3974                 set: function( id, view, options ) {
3975                         var list;
3976                         options = options || {};
3977
3978                         // Accept an object with an `id` : `view` mapping.
3979                         if ( _.isObject( id ) ) {
3980                                 _.each( id, function( view, id ) {
3981                                         this.set( id, view, { silent: true });
3982                                 }, this );
3983
3984                         } else {
3985                                 if ( ! ( view instanceof Backbone.View ) ) {
3986                                         view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
3987                                         view = new media.view.Button( view ).render();
3988                                 }
3989
3990                                 view.controller = view.controller || this.controller;
3991
3992                                 this._views[ id ] = view;
3993
3994                                 list = view.options.priority < 0 ? 'secondary' : 'primary';
3995                                 this[ list ].set( id, view, options );
3996                         }
3997
3998                         if ( ! options.silent ) {
3999                                 this.refresh();
4000                         }
4001
4002                         return this;
4003                 },
4004                 /**
4005                  * @param {string} id
4006                  * @returns {wp.media.view.Button}
4007                  */
4008                 get: function( id ) {
4009                         return this._views[ id ];
4010                 },
4011                 /**
4012                  * @param {string} id
4013                  * @param {Object} options
4014                  * @returns {wp.media.view.Toolbar} Returns itself to allow chaining
4015                  */
4016                 unset: function( id, options ) {
4017                         delete this._views[ id ];
4018                         this.primary.unset( id, options );
4019                         this.secondary.unset( id, options );
4020
4021                         if ( ! options || ! options.silent ) {
4022                                 this.refresh();
4023                         }
4024                         return this;
4025                 },
4026
4027                 refresh: function() {
4028                         var state = this.controller.state(),
4029                                 library = state.get('library'),
4030                                 selection = state.get('selection');
4031
4032                         _.each( this._views, function( button ) {
4033                                 if ( ! button.model || ! button.options || ! button.options.requires ) {
4034                                         return;
4035                                 }
4036
4037                                 var requires = button.options.requires,
4038                                         disabled = false;
4039
4040                                 // Prevent insertion of attachments if any of them are still uploading
4041                                 disabled = _.some( selection.models, function( attachment ) {
4042                                         return attachment.get('uploading') === true;
4043                                 });
4044
4045                                 if ( requires.selection && selection && ! selection.length ) {
4046                                         disabled = true;
4047                                 } else if ( requires.library && library && ! library.length ) {
4048                                         disabled = true;
4049                                 }
4050                                 button.model.set( 'disabled', disabled );
4051                         });
4052                 }
4053         });
4054
4055         /**
4056          * wp.media.view.Toolbar.Select
4057          *
4058          * @constructor
4059          * @augments wp.media.view.Toolbar
4060          * @augments wp.media.View
4061          * @augments wp.Backbone.View
4062          * @augments Backbone.View
4063          */
4064         media.view.Toolbar.Select = media.view.Toolbar.extend({
4065                 initialize: function() {
4066                         var options = this.options;
4067
4068                         _.bindAll( this, 'clickSelect' );
4069
4070                         _.defaults( options, {
4071                                 event: 'select',
4072                                 state: false,
4073                                 reset: true,
4074                                 close: true,
4075                                 text:  l10n.select,
4076
4077                                 // Does the button rely on the selection?
4078                                 requires: {
4079                                         selection: true
4080                                 }
4081                         });
4082
4083                         options.items = _.defaults( options.items || {}, {
4084                                 select: {
4085                                         style:    'primary',
4086                                         text:     options.text,
4087                                         priority: 80,
4088                                         click:    this.clickSelect,
4089                                         requires: options.requires
4090                                 }
4091                         });
4092                         /**
4093                          * call 'initialize' directly on the parent class
4094                          */
4095                         media.view.Toolbar.prototype.initialize.apply( this, arguments );
4096                 },
4097
4098                 clickSelect: function() {
4099                         var options = this.options,
4100                                 controller = this.controller;
4101
4102                         if ( options.close ) {
4103                                 controller.close();
4104                         }
4105
4106                         if ( options.event ) {
4107                                 controller.state().trigger( options.event );
4108                         }
4109
4110                         if ( options.state ) {
4111                                 controller.setState( options.state );
4112                         }
4113
4114                         if ( options.reset ) {
4115                                 controller.reset();
4116                         }
4117                 }
4118         });
4119
4120         /**
4121          * wp.media.view.Toolbar.Embed
4122          *
4123          * @constructor
4124          * @augments wp.media.view.Toolbar.Select
4125          * @augments wp.media.view.Toolbar
4126          * @augments wp.media.View
4127          * @augments wp.Backbone.View
4128          * @augments Backbone.View
4129          */
4130         media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
4131                 initialize: function() {
4132                         _.defaults( this.options, {
4133                                 text: l10n.insertIntoPost,
4134                                 requires: false
4135                         });
4136                         /**
4137                          * call 'initialize' directly on the parent class
4138                          */
4139                         media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
4140                 },
4141
4142                 refresh: function() {
4143                         var url = this.controller.state().props.get('url');
4144                         this.get('select').model.set( 'disabled', ! url || url === 'http://' );
4145                         /**
4146                          * call 'refresh' directly on the parent class
4147                          */
4148                         media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
4149                 }
4150         });
4151
4152         /**
4153          * wp.media.view.Button
4154          *
4155          * @constructor
4156          * @augments wp.media.View
4157          * @augments wp.Backbone.View
4158          * @augments Backbone.View
4159          */
4160         media.view.Button = media.View.extend({
4161                 tagName:    'a',
4162                 className:  'media-button',
4163                 attributes: { href: '#' },
4164
4165                 events: {
4166                         'click': 'click'
4167                 },
4168
4169                 defaults: {
4170                         text:     '',
4171                         style:    '',
4172                         size:     'large',
4173                         disabled: false
4174                 },
4175
4176                 initialize: function() {
4177                         /**
4178                          * Create a model with the provided `defaults`.
4179                          *
4180                          * @member {Backbone.Model}
4181                          */
4182                         this.model = new Backbone.Model( this.defaults );
4183
4184                         // If any of the `options` have a key from `defaults`, apply its
4185                         // value to the `model` and remove it from the `options object.
4186                         _.each( this.defaults, function( def, key ) {
4187                                 var value = this.options[ key ];
4188                                 if ( _.isUndefined( value ) ) {
4189                                         return;
4190                                 }
4191
4192                                 this.model.set( key, value );
4193                                 delete this.options[ key ];
4194                         }, this );
4195
4196                         this.model.on( 'change', this.render, this );
4197                 },
4198                 /**
4199                  * @returns {wp.media.view.Button} Returns itself to allow chaining
4200                  */
4201                 render: function() {
4202                         var classes = [ 'button', this.className ],
4203                                 model = this.model.toJSON();
4204
4205                         if ( model.style ) {
4206                                 classes.push( 'button-' + model.style );
4207                         }
4208
4209                         if ( model.size ) {
4210                                 classes.push( 'button-' + model.size );
4211                         }
4212
4213                         classes = _.uniq( classes.concat( this.options.classes ) );
4214                         this.el.className = classes.join(' ');
4215
4216                         this.$el.attr( 'disabled', model.disabled );
4217                         this.$el.text( this.model.get('text') );
4218
4219                         return this;
4220                 },
4221                 /**
4222                  * @param {Object} event
4223                  */
4224                 click: function( event ) {
4225                         if ( '#' === this.attributes.href ) {
4226                                 event.preventDefault();
4227                         }
4228
4229                         if ( this.options.click && ! this.model.get('disabled') ) {
4230                                 this.options.click.apply( this, arguments );
4231                         }
4232                 }
4233         });
4234
4235         /**
4236          * wp.media.view.ButtonGroup
4237          *
4238          * @constructor
4239          * @augments wp.media.View
4240          * @augments wp.Backbone.View
4241          * @augments Backbone.View
4242          */
4243         media.view.ButtonGroup = media.View.extend({
4244                 tagName:   'div',
4245                 className: 'button-group button-large media-button-group',
4246
4247                 initialize: function() {
4248                         /**
4249                          * @member {wp.media.view.Button[]}
4250                          */
4251                         this.buttons = _.map( this.options.buttons || [], function( button ) {
4252                                 if ( button instanceof Backbone.View ) {
4253                                         return button;
4254                                 } else {
4255                                         return new media.view.Button( button ).render();
4256                                 }
4257                         });
4258
4259                         delete this.options.buttons;
4260
4261                         if ( this.options.classes ) {
4262                                 this.$el.addClass( this.options.classes );
4263                         }
4264                 },
4265
4266                 /**
4267                  * @returns {wp.media.view.ButtonGroup}
4268                  */
4269                 render: function() {
4270                         this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
4271                         return this;
4272                 }
4273         });
4274
4275         /**
4276          * wp.media.view.PriorityList
4277          *
4278          * @constructor
4279          * @augments wp.media.View
4280          * @augments wp.Backbone.View
4281          * @augments Backbone.View
4282          */
4283         media.view.PriorityList = media.View.extend({
4284                 tagName:   'div',
4285
4286                 initialize: function() {
4287                         this._views = {};
4288
4289                         this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
4290                         delete this.options.views;
4291
4292                         if ( ! this.options.silent ) {
4293                                 this.render();
4294                         }
4295                 },
4296                 /**
4297                  * @param {string} id
4298                  * @param {wp.media.View|Object} view
4299                  * @param {Object} options
4300                  * @returns {wp.media.view.PriorityList} Returns itself to allow chaining
4301                  */
4302                 set: function( id, view, options ) {
4303                         var priority, views, index;
4304
4305                         options = options || {};
4306
4307                         // Accept an object with an `id` : `view` mapping.
4308                         if ( _.isObject( id ) ) {
4309                                 _.each( id, function( view, id ) {
4310                                         this.set( id, view );
4311                                 }, this );
4312                                 return this;
4313                         }
4314
4315                         if ( ! (view instanceof Backbone.View) ) {
4316                                 view = this.toView( view, id, options );
4317                         }
4318                         view.controller = view.controller || this.controller;
4319
4320                         this.unset( id );
4321
4322                         priority = view.options.priority || 10;
4323                         views = this.views.get() || [];
4324
4325                         _.find( views, function( existing, i ) {
4326                                 if ( existing.options.priority > priority ) {
4327                                         index = i;
4328                                         return true;
4329                                 }
4330                         });
4331
4332                         this._views[ id ] = view;
4333                         this.views.add( view, {
4334                                 at: _.isNumber( index ) ? index : views.length || 0
4335                         });
4336
4337                         return this;
4338                 },
4339                 /**
4340                  * @param {string} id
4341                  * @returns {wp.media.View}
4342                  */
4343                 get: function( id ) {
4344                         return this._views[ id ];
4345                 },
4346                 /**
4347                  * @param {string} id
4348                  * @returns {wp.media.view.PriorityList}
4349                  */
4350                 unset: function( id ) {
4351                         var view = this.get( id );
4352
4353                         if ( view ) {
4354                                 view.remove();
4355                         }
4356
4357                         delete this._views[ id ];
4358                         return this;
4359                 },
4360                 /**
4361                  * @param {Object} options
4362                  * @returns {wp.media.View}
4363                  */
4364                 toView: function( options ) {
4365                         return new media.View( options );
4366                 }
4367         });
4368
4369         /**
4370          * wp.media.view.MenuItem
4371          *
4372          * @constructor
4373          * @augments wp.media.View
4374          * @augments wp.Backbone.View
4375          * @augments Backbone.View
4376          */
4377         media.view.MenuItem = media.View.extend({
4378                 tagName:   'a',
4379                 className: 'media-menu-item',
4380
4381                 attributes: {
4382                         href: '#'
4383                 },
4384
4385                 events: {
4386                         'click': '_click'
4387                 },
4388                 /**
4389                  * @param {Object} event
4390                  */
4391                 _click: function( event ) {
4392                         var clickOverride = this.options.click;
4393
4394                         if ( event ) {
4395                                 event.preventDefault();
4396                         }
4397
4398                         if ( clickOverride ) {
4399                                 clickOverride.call( this );
4400                         } else {
4401                                 this.click();
4402                         }
4403
4404                         // When selecting a tab along the left side,
4405                         // focus should be transferred into the main panel
4406                         if ( ! isTouchDevice ) {
4407                                 $('.media-frame-content input').first().focus();
4408                         }
4409                 },
4410
4411                 click: function() {
4412                         var state = this.options.state;
4413
4414                         if ( state ) {
4415                                 this.controller.setState( state );
4416                                 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
4417                         }
4418                 },
4419                 /**
4420                  * @returns {wp.media.view.MenuItem} returns itself to allow chaining
4421                  */
4422                 render: function() {
4423                         var options = this.options;
4424
4425                         if ( options.text ) {
4426                                 this.$el.text( options.text );
4427                         } else if ( options.html ) {
4428                                 this.$el.html( options.html );
4429                         }
4430
4431                         return this;
4432                 }
4433         });
4434
4435         /**
4436          * wp.media.view.Menu
4437          *
4438          * @constructor
4439          * @augments wp.media.view.PriorityList
4440          * @augments wp.media.View
4441          * @augments wp.Backbone.View
4442          * @augments Backbone.View
4443          */
4444         media.view.Menu = media.view.PriorityList.extend({
4445                 tagName:   'div',
4446                 className: 'media-menu',
4447                 property:  'state',
4448                 ItemView:  media.view.MenuItem,
4449                 region:    'menu',
4450
4451                 /* TODO: alternatively hide on any click anywhere
4452                 events: {
4453                         'click': 'click'
4454                 },
4455
4456                 click: function() {
4457                         this.$el.removeClass( 'visible' );
4458                 },
4459                 */
4460
4461                 /**
4462                  * @param {Object} options
4463                  * @param {string} id
4464                  * @returns {wp.media.View}
4465                  */
4466                 toView: function( options, id ) {
4467                         options = options || {};
4468                         options[ this.property ] = options[ this.property ] || id;
4469                         return new this.ItemView( options ).render();
4470                 },
4471
4472                 ready: function() {
4473                         /**
4474                          * call 'ready' directly on the parent class
4475                          */
4476                         media.view.PriorityList.prototype.ready.apply( this, arguments );
4477                         this.visibility();
4478                 },
4479
4480                 set: function() {
4481                         /**
4482                          * call 'set' directly on the parent class
4483                          */
4484                         media.view.PriorityList.prototype.set.apply( this, arguments );
4485                         this.visibility();
4486                 },
4487
4488                 unset: function() {
4489                         /**
4490                          * call 'unset' directly on the parent class
4491                          */
4492                         media.view.PriorityList.prototype.unset.apply( this, arguments );
4493                         this.visibility();
4494                 },
4495
4496                 visibility: function() {
4497                         var region = this.region,
4498                                 view = this.controller[ region ].get(),
4499                                 views = this.views.get(),
4500                                 hide = ! views || views.length < 2;
4501
4502                         if ( this === view ) {
4503                                 this.controller.$el.toggleClass( 'hide-' + region, hide );
4504                         }
4505                 },
4506                 /**
4507                  * @param {string} id
4508                  */
4509                 select: function( id ) {
4510                         var view = this.get( id );
4511
4512                         if ( ! view ) {
4513                                 return;
4514                         }
4515
4516                         this.deselect();
4517                         view.$el.addClass('active');
4518                 },
4519
4520                 deselect: function() {
4521                         this.$el.children().removeClass('active');
4522                 },
4523
4524                 hide: function( id ) {
4525                         var view = this.get( id );
4526
4527                         if ( ! view ) {
4528                                 return;
4529                         }
4530
4531                         view.$el.addClass('hidden');
4532                 },
4533
4534                 show: function( id ) {
4535                         var view = this.get( id );
4536
4537                         if ( ! view ) {
4538                                 return;
4539                         }
4540
4541                         view.$el.removeClass('hidden');
4542                 }
4543         });
4544
4545         /**
4546          * wp.media.view.RouterItem
4547          *
4548          * @constructor
4549          * @augments wp.media.view.MenuItem
4550          * @augments wp.media.View
4551          * @augments wp.Backbone.View
4552          * @augments Backbone.View
4553          */
4554         media.view.RouterItem = media.view.MenuItem.extend({
4555                 /**
4556                  * On click handler to activate the content region's corresponding mode.
4557                  */
4558                 click: function() {
4559                         var contentMode = this.options.contentMode;
4560                         if ( contentMode ) {
4561                                 this.controller.content.mode( contentMode );
4562                         }
4563                 }
4564         });
4565
4566         /**
4567          * wp.media.view.Router
4568          *
4569          * @constructor
4570          * @augments wp.media.view.Menu
4571          * @augments wp.media.view.PriorityList
4572          * @augments wp.media.View
4573          * @augments wp.Backbone.View
4574          * @augments Backbone.View
4575          */
4576         media.view.Router = media.view.Menu.extend({
4577                 tagName:   'div',
4578                 className: 'media-router',
4579                 property:  'contentMode',
4580                 ItemView:  media.view.RouterItem,
4581                 region:    'router',
4582
4583                 initialize: function() {
4584                         this.controller.on( 'content:render', this.update, this );
4585                         /**
4586                          * call 'initialize' directly on the parent class
4587                          */
4588                         media.view.Menu.prototype.initialize.apply( this, arguments );
4589                 },
4590
4591                 update: function() {
4592                         var mode = this.controller.content.mode();
4593                         if ( mode ) {
4594                                 this.select( mode );
4595                         }
4596                 }
4597         });
4598
4599         /**
4600          * wp.media.view.Sidebar
4601          *
4602          * @constructor
4603          * @augments wp.media.view.PriorityList
4604          * @augments wp.media.View
4605          * @augments wp.Backbone.View
4606          * @augments Backbone.View
4607          */
4608         media.view.Sidebar = media.view.PriorityList.extend({
4609                 className: 'media-sidebar'
4610         });
4611
4612         /**
4613          * wp.media.view.Attachment
4614          *
4615          * @constructor
4616          * @augments wp.media.View
4617          * @augments wp.Backbone.View
4618          * @augments Backbone.View
4619          */
4620         media.view.Attachment = media.View.extend({
4621                 tagName:   'li',
4622                 className: 'attachment',
4623                 template:  media.template('attachment'),
4624
4625                 attributes: function() {
4626                         return {
4627                                 'tabIndex':     0,
4628                                 'role':         'checkbox',
4629                                 'aria-label':   this.model.get( 'title' ),
4630                                 'aria-checked': false,
4631                                 'data-id':      this.model.get( 'id' )
4632                         };
4633                 },
4634
4635                 events: {
4636                         'click .js--select-attachment':   'toggleSelectionHandler',
4637                         'change [data-setting]':          'updateSetting',
4638                         'change [data-setting] input':    'updateSetting',
4639                         'change [data-setting] select':   'updateSetting',
4640                         'change [data-setting] textarea': 'updateSetting',
4641                         'click .close':                   'removeFromLibrary',
4642                         'click .check':                   'checkClickHandler',
4643                         'click a':                        'preventDefault',
4644                         'keydown':                        'toggleSelectionHandler'
4645                 },
4646
4647                 buttons: {},
4648
4649                 initialize: function() {
4650                         var selection = this.options.selection,
4651                                 options = _.defaults( this.options, {
4652                                         rerenderOnModelChange: true
4653                                 } );
4654
4655                         if ( options.rerenderOnModelChange ) {
4656                                 this.model.on( 'change', this.render, this );
4657                         } else {
4658                                 this.model.on( 'change:percent', this.progress, this );
4659                         }
4660                         this.model.on( 'change:title', this._syncTitle, this );
4661                         this.model.on( 'change:caption', this._syncCaption, this );
4662                         this.model.on( 'change:artist', this._syncArtist, this );
4663                         this.model.on( 'change:album', this._syncAlbum, this );
4664
4665                         // Update the selection.
4666                         this.model.on( 'add', this.select, this );
4667                         this.model.on( 'remove', this.deselect, this );
4668                         if ( selection ) {
4669                                 selection.on( 'reset', this.updateSelect, this );
4670                                 // Update the model's details view.
4671                                 this.model.on( 'selection:single selection:unsingle', this.details, this );
4672                                 this.details( this.model, this.controller.state().get('selection') );
4673                         }
4674                 },
4675                 /**
4676                  * @returns {wp.media.view.Attachment} Returns itself to allow chaining
4677                  */
4678                 dispose: function() {
4679                         var selection = this.options.selection;
4680
4681                         // Make sure all settings are saved before removing the view.
4682                         this.updateAll();
4683
4684                         if ( selection ) {
4685                                 selection.off( null, null, this );
4686                         }
4687                         /**
4688                          * call 'dispose' directly on the parent class
4689                          */
4690                         media.View.prototype.dispose.apply( this, arguments );
4691                         return this;
4692                 },
4693                 /**
4694                  * @returns {wp.media.view.Attachment} Returns itself to allow chaining
4695                  */
4696                 render: function() {
4697                         var options = _.defaults( this.model.toJSON(), {
4698                                         orientation:   'landscape',
4699                                         uploading:     false,
4700                                         type:          '',
4701                                         subtype:       '',
4702                                         icon:          '',
4703                                         filename:      '',
4704                                         caption:       '',
4705                                         title:         '',
4706                                         dateFormatted: '',
4707                                         width:         '',
4708                                         height:        '',
4709                                         compat:        false,
4710                                         alt:           '',
4711                                         description:   ''
4712                                 }, this.options );
4713
4714                         options.buttons  = this.buttons;
4715                         options.describe = this.controller.state().get('describe');
4716
4717                         if ( 'image' === options.type ) {
4718                                 options.size = this.imageSize();
4719                         }
4720
4721                         options.can = {};
4722                         if ( options.nonces ) {
4723                                 options.can.remove = !! options.nonces['delete'];
4724                                 options.can.save = !! options.nonces.update;
4725                         }
4726
4727                         if ( this.controller.state().get('allowLocalEdits') ) {
4728                                 options.allowLocalEdits = true;
4729                         }
4730
4731                         if ( options.uploading && ! options.percent ) {
4732                                 options.percent = 0;
4733                         }
4734
4735                         this.views.detach();
4736                         this.$el.html( this.template( options ) );
4737
4738                         this.$el.toggleClass( 'uploading', options.uploading );
4739
4740                         if ( options.uploading ) {
4741                                 this.$bar = this.$('.media-progress-bar div');
4742                         } else {
4743                                 delete this.$bar;
4744                         }
4745
4746                         // Check if the model is selected.
4747                         this.updateSelect();
4748
4749                         // Update the save status.
4750                         this.updateSave();
4751
4752                         this.views.render();
4753
4754                         return this;
4755                 },
4756
4757                 progress: function() {
4758                         if ( this.$bar && this.$bar.length ) {
4759                                 this.$bar.width( this.model.get('percent') + '%' );
4760                         }
4761                 },
4762
4763                 /**
4764                  * @param {Object} event
4765                  */
4766                 toggleSelectionHandler: function( event ) {
4767                         var method;
4768
4769                         // Don't do anything inside inputs.
4770                         if ( 'INPUT' === event.target.nodeName ) {
4771                                 return;
4772                         }
4773
4774                         // Catch arrow events
4775                         if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
4776                                 this.controller.trigger( 'attachment:keydown:arrow', event );
4777                                 return;
4778                         }
4779
4780                         // Catch enter and space events
4781                         if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
4782                                 return;
4783                         }
4784
4785                         // In the grid view, bubble up an edit:attachment event to the controller.
4786                         if ( this.controller.isModeActive( 'grid' ) ) {
4787                                 if ( this.controller.isModeActive( 'edit' ) ) {
4788                                         // Pass the current target to restore focus when closing
4789                                         this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
4790
4791                                         // Don't scroll the view and don't attempt to submit anything.
4792                                         event.stopPropagation();
4793                                         return;
4794                                 }
4795
4796                                 if ( this.controller.isModeActive( 'select' ) ) {
4797                                         method = 'toggle';
4798                                 }
4799                         }
4800
4801                         if ( event.shiftKey ) {
4802                                 method = 'between';
4803                         } else if ( event.ctrlKey || event.metaKey ) {
4804                                 method = 'toggle';
4805                         }
4806
4807                         this.toggleSelection({
4808                                 method: method
4809                         });
4810
4811                         this.controller.trigger( 'selection:toggle' );
4812
4813                         // Don't scroll the view and don't attempt to submit anything.
4814                         event.stopPropagation();
4815                 },
4816                 /**
4817                  * @param {Object} options
4818                  */
4819                 toggleSelection: function( options ) {
4820                         var collection = this.collection,
4821                                 selection = this.options.selection,
4822                                 model = this.model,
4823                                 method = options && options.method,
4824                                 single, models, singleIndex, modelIndex;
4825
4826                         if ( ! selection ) {
4827                                 return;
4828                         }
4829
4830                         single = selection.single();
4831                         method = _.isUndefined( method ) ? selection.multiple : method;
4832
4833                         // If the `method` is set to `between`, select all models that
4834                         // exist between the current and the selected model.
4835                         if ( 'between' === method && single && selection.multiple ) {
4836                                 // If the models are the same, short-circuit.
4837                                 if ( single === model ) {
4838                                         return;
4839                                 }
4840
4841                                 singleIndex = collection.indexOf( single );
4842                                 modelIndex  = collection.indexOf( this.model );
4843
4844                                 if ( singleIndex < modelIndex ) {
4845                                         models = collection.models.slice( singleIndex, modelIndex + 1 );
4846                                 } else {
4847                                         models = collection.models.slice( modelIndex, singleIndex + 1 );
4848                                 }
4849
4850                                 selection.add( models );
4851                                 selection.single( model );
4852                                 return;
4853
4854                         // If the `method` is set to `toggle`, just flip the selection
4855                         // status, regardless of whether the model is the single model.
4856                         } else if ( 'toggle' === method ) {
4857                                 selection[ this.selected() ? 'remove' : 'add' ]( model );
4858                                 selection.single( model );
4859                                 return;
4860                         } else if ( 'add' === method ) {
4861                                 selection.add( model );
4862                                 selection.single( model );
4863                                 return;
4864                         }
4865
4866                         // Fixes bug that loses focus when selecting a featured image
4867                         if ( ! method ) {
4868                                 method = 'add';
4869                         }
4870
4871                         if ( method !== 'add' ) {
4872                                 method = 'reset';
4873                         }
4874
4875                         if ( this.selected() ) {
4876                                 // If the model is the single model, remove it.
4877                                 // If it is not the same as the single model,
4878                                 // it now becomes the single model.
4879                                 selection[ single === model ? 'remove' : 'single' ]( model );
4880                         } else {
4881                                 // If the model is not selected, run the `method` on the
4882                                 // selection. By default, we `reset` the selection, but the
4883                                 // `method` can be set to `add` the model to the selection.
4884                                 selection[ method ]( model );
4885                                 selection.single( model );
4886                         }
4887                 },
4888
4889                 updateSelect: function() {
4890                         this[ this.selected() ? 'select' : 'deselect' ]();
4891                 },
4892                 /**
4893                  * @returns {unresolved|Boolean}
4894                  */
4895                 selected: function() {
4896                         var selection = this.options.selection;
4897                         if ( selection ) {
4898                                 return !! selection.get( this.model.cid );
4899                         }
4900                 },
4901                 /**
4902                  * @param {Backbone.Model} model
4903                  * @param {Backbone.Collection} collection
4904                  */
4905                 select: function( model, collection ) {
4906                         var selection = this.options.selection,
4907                                 controller = this.controller;
4908
4909                         // Check if a selection exists and if it's the collection provided.
4910                         // If they're not the same collection, bail; we're in another
4911                         // selection's event loop.
4912                         if ( ! selection || ( collection && collection !== selection ) ) {
4913                                 return;
4914                         }
4915
4916                         // Bail if the model is already selected.
4917                         if ( this.$el.hasClass( 'selected' ) ) {
4918                                 return;
4919                         }
4920
4921                         // Add 'selected' class to model, set aria-checked to true.
4922                         this.$el.addClass( 'selected' ).attr( 'aria-checked', true );
4923                         //  Make the checkbox tabable, except in media grid (bulk select mode).
4924                         if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) {
4925                                 this.$( '.check' ).attr( 'tabindex', '0' );
4926                         }
4927                 },
4928                 /**
4929                  * @param {Backbone.Model} model
4930                  * @param {Backbone.Collection} collection
4931                  */
4932                 deselect: function( model, collection ) {
4933                         var selection = this.options.selection;
4934
4935                         // Check if a selection exists and if it's the collection provided.
4936                         // If they're not the same collection, bail; we're in another
4937                         // selection's event loop.
4938                         if ( ! selection || ( collection && collection !== selection ) ) {
4939                                 return;
4940                         }
4941                         this.$el.removeClass( 'selected' ).attr( 'aria-checked', false )
4942                                 .find( '.check' ).attr( 'tabindex', '-1' );
4943                 },
4944                 /**
4945                  * @param {Backbone.Model} model
4946                  * @param {Backbone.Collection} collection
4947                  */
4948                 details: function( model, collection ) {
4949                         var selection = this.options.selection,
4950                                 details;
4951
4952                         if ( selection !== collection ) {
4953                                 return;
4954                         }
4955
4956                         details = selection.single();
4957                         this.$el.toggleClass( 'details', details === this.model );
4958                 },
4959                 /**
4960                  * @param {Object} event
4961                  */
4962                 preventDefault: function( event ) {
4963                         event.preventDefault();
4964                 },
4965                 /**
4966                  * @param {string} size
4967                  * @returns {Object}
4968                  */
4969                 imageSize: function( size ) {
4970                         var sizes = this.model.get('sizes');
4971
4972                         size = size || 'medium';
4973
4974                         // Use the provided image size if possible.
4975                         if ( sizes && sizes[ size ] ) {
4976                                 return _.clone( sizes[ size ] );
4977                         } else {
4978                                 return {
4979                                         url:         this.model.get('url'),
4980                                         width:       this.model.get('width'),
4981                                         height:      this.model.get('height'),
4982                                         orientation: this.model.get('orientation')
4983                                 };
4984                         }
4985                 },
4986                 /**
4987                  * @param {Object} event
4988                  */
4989                 updateSetting: function( event ) {
4990                         var $setting = $( event.target ).closest('[data-setting]'),
4991                                 setting, value;
4992
4993                         if ( ! $setting.length ) {
4994                                 return;
4995                         }
4996
4997                         setting = $setting.data('setting');
4998                         value   = event.target.value;
4999
5000                         if ( this.model.get( setting ) !== value ) {
5001                                 this.save( setting, value );
5002                         }
5003                 },
5004
5005                 /**
5006                  * Pass all the arguments to the model's save method.
5007                  *
5008                  * Records the aggregate status of all save requests and updates the
5009                  * view's classes accordingly.
5010                  */
5011                 save: function() {
5012                         var view = this,
5013                                 save = this._save = this._save || { status: 'ready' },
5014                                 request = this.model.save.apply( this.model, arguments ),
5015                                 requests = save.requests ? $.when( request, save.requests ) : request;
5016
5017                         // If we're waiting to remove 'Saved.', stop.
5018                         if ( save.savedTimer ) {
5019                                 clearTimeout( save.savedTimer );
5020                         }
5021
5022                         this.updateSave('waiting');
5023                         save.requests = requests;
5024                         requests.always( function() {
5025                                 // If we've performed another request since this one, bail.
5026                                 if ( save.requests !== requests ) {
5027                                         return;
5028                                 }
5029
5030                                 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
5031                                 save.savedTimer = setTimeout( function() {
5032                                         view.updateSave('ready');
5033                                         delete save.savedTimer;
5034                                 }, 2000 );
5035                         });
5036                 },
5037                 /**
5038                  * @param {string} status
5039                  * @returns {wp.media.view.Attachment} Returns itself to allow chaining
5040                  */
5041                 updateSave: function( status ) {
5042                         var save = this._save = this._save || { status: 'ready' };
5043
5044                         if ( status && status !== save.status ) {
5045                                 this.$el.removeClass( 'save-' + save.status );
5046                                 save.status = status;
5047                         }
5048
5049                         this.$el.addClass( 'save-' + save.status );
5050                         return this;
5051                 },
5052
5053                 updateAll: function() {
5054                         var $settings = this.$('[data-setting]'),
5055                                 model = this.model,
5056                                 changed;
5057
5058                         changed = _.chain( $settings ).map( function( el ) {
5059                                 var $input = $('input, textarea, select, [value]', el ),
5060                                         setting, value;
5061
5062                                 if ( ! $input.length ) {
5063                                         return;
5064                                 }
5065
5066                                 setting = $(el).data('setting');
5067                                 value = $input.val();
5068
5069                                 // Record the value if it changed.
5070                                 if ( model.get( setting ) !== value ) {
5071                                         return [ setting, value ];
5072                                 }
5073                         }).compact().object().value();
5074
5075                         if ( ! _.isEmpty( changed ) ) {
5076                                 model.save( changed );
5077                         }
5078                 },
5079                 /**
5080                  * @param {Object} event
5081                  */
5082                 removeFromLibrary: function( event ) {
5083                         // Stop propagation so the model isn't selected.
5084                         event.stopPropagation();
5085
5086                         this.collection.remove( this.model );
5087                 },
5088
5089                 /**
5090                  * Add the model if it isn't in the selection, if it is in the selection,
5091                  * remove it.
5092                  *
5093                  * @param  {[type]} event [description]
5094                  * @return {[type]}       [description]
5095                  */
5096                 checkClickHandler: function ( event ) {
5097                         var selection = this.options.selection;
5098                         if ( ! selection ) {
5099                                 return;
5100                         }
5101                         event.stopPropagation();
5102                         if ( selection.where( { id: this.model.get( 'id' ) } ).length ) {
5103                                 selection.remove( this.model );
5104                                 // Move focus back to the attachment tile (from the check).
5105                                 this.$el.focus();
5106                         } else {
5107                                 selection.add( this.model );
5108                         }
5109                 }
5110         });
5111
5112         // Ensure settings remain in sync between attachment views.
5113         _.each({
5114                 caption: '_syncCaption',
5115                 title:   '_syncTitle',
5116                 artist:  '_syncArtist',
5117                 album:   '_syncAlbum'
5118         }, function( method, setting ) {
5119                 /**
5120                  * @param {Backbone.Model} model
5121                  * @param {string} value
5122                  * @returns {wp.media.view.Attachment} Returns itself to allow chaining
5123                  */
5124                 media.view.Attachment.prototype[ method ] = function( model, value ) {
5125                         var $setting = this.$('[data-setting="' + setting + '"]');
5126
5127                         if ( ! $setting.length ) {
5128                                 return this;
5129                         }
5130
5131                         // If the updated value is in sync with the value in the DOM, there
5132                         // is no need to re-render. If we're currently editing the value,
5133                         // it will automatically be in sync, suppressing the re-render for
5134                         // the view we're editing, while updating any others.
5135                         if ( value === $setting.find('input, textarea, select, [value]').val() ) {
5136                                 return this;
5137                         }
5138
5139                         return this.render();
5140                 };
5141         });
5142
5143         /**
5144          * wp.media.view.Attachment.Library
5145          *
5146          * @constructor
5147          * @augments wp.media.view.Attachment
5148          * @augments wp.media.View
5149          * @augments wp.Backbone.View
5150          * @augments Backbone.View
5151          */
5152         media.view.Attachment.Library = media.view.Attachment.extend({
5153                 buttons: {
5154                         check: true
5155                 }
5156         });
5157
5158         /**
5159          * wp.media.view.Attachment.EditLibrary
5160          *
5161          * @constructor
5162          * @augments wp.media.view.Attachment
5163          * @augments wp.media.View
5164          * @augments wp.Backbone.View
5165          * @augments Backbone.View
5166          */
5167         media.view.Attachment.EditLibrary = media.view.Attachment.extend({
5168                 buttons: {
5169                         close: true
5170                 }
5171         });
5172
5173         /**
5174          * wp.media.view.Attachments
5175          *
5176          * @constructor
5177          * @augments wp.media.View
5178          * @augments wp.Backbone.View
5179          * @augments Backbone.View
5180          */
5181         media.view.Attachments = media.View.extend({
5182                 tagName:   'ul',
5183                 className: 'attachments',
5184
5185                 attributes: {
5186                         tabIndex: -1
5187                 },
5188
5189                 initialize: function() {
5190                         this.el.id = _.uniqueId('__attachments-view-');
5191
5192                         _.defaults( this.options, {
5193                                 refreshSensitivity: isTouchDevice ? 300 : 200,
5194                                 refreshThreshold:   3,
5195                                 AttachmentView:     media.view.Attachment,
5196                                 sortable:           false,
5197                                 resize:             true,
5198                                 idealColumnWidth:   $( window ).width() < 640 ? 135 : 150
5199                         });
5200
5201                         this._viewsByCid = {};
5202                         this.$window = $( window );
5203                         this.resizeEvent = 'resize.media-modal-columns';
5204
5205                         this.collection.on( 'add', function( attachment ) {
5206                                 this.views.add( this.createAttachmentView( attachment ), {
5207                                         at: this.collection.indexOf( attachment )
5208                                 });
5209                         }, this );
5210
5211                         this.collection.on( 'remove', function( attachment ) {
5212                                 var view = this._viewsByCid[ attachment.cid ];
5213                                 delete this._viewsByCid[ attachment.cid ];
5214
5215                                 if ( view ) {
5216                                         view.remove();
5217                                 }
5218                         }, this );
5219
5220                         this.collection.on( 'reset', this.render, this );
5221
5222                         this.listenTo( this.controller, 'library:selection:add',    this.attachmentFocus );
5223
5224                         // Throttle the scroll handler and bind this.
5225                         this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
5226
5227                         this.options.scrollElement = this.options.scrollElement || this.el;
5228                         $( this.options.scrollElement ).on( 'scroll', this.scroll );
5229
5230                         this.initSortable();
5231
5232                         _.bindAll( this, 'setColumns' );
5233
5234                         if ( this.options.resize ) {
5235                                 this.on( 'ready', this.bindEvents );
5236                                 this.controller.on( 'open', this.setColumns );
5237
5238                                 // Call this.setColumns() after this view has been rendered in the DOM so
5239                                 // attachments get proper width applied.
5240                                 _.defer( this.setColumns, this );
5241                         }
5242                 },
5243
5244                 bindEvents: function() {
5245                         this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) );
5246                 },
5247
5248                 attachmentFocus: function() {
5249                         this.$( 'li:first' ).focus();
5250                 },
5251
5252                 restoreFocus: function() {
5253                         this.$( 'li.selected:first' ).focus();
5254                 },
5255
5256                 arrowEvent: function( event ) {
5257                         var attachments = this.$el.children( 'li' ),
5258                                 perRow = this.columns,
5259                                 index = attachments.filter( ':focus' ).index(),
5260                                 row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow );
5261
5262                         if ( index === -1 ) {
5263                                 return;
5264                         }
5265
5266                         // Left arrow
5267                         if ( 37 === event.keyCode ) {
5268                                 if ( 0 === index ) {
5269                                         return;
5270                                 }
5271                                 attachments.eq( index - 1 ).focus();
5272                         }
5273
5274                         // Up arrow
5275                         if ( 38 === event.keyCode ) {
5276                                 if ( 1 === row ) {
5277                                         return;
5278                                 }
5279                                 attachments.eq( index - perRow ).focus();
5280                         }
5281
5282                         // Right arrow
5283                         if ( 39 === event.keyCode ) {
5284                                 if ( attachments.length === index ) {
5285                                         return;
5286                                 }
5287                                 attachments.eq( index + 1 ).focus();
5288                         }
5289
5290                         // Down arrow
5291                         if ( 40 === event.keyCode ) {
5292                                 if ( Math.ceil( attachments.length / perRow ) === row ) {
5293                                         return;
5294                                 }
5295                                 attachments.eq( index + perRow ).focus();
5296                         }
5297                 },
5298
5299                 dispose: function() {
5300                         this.collection.props.off( null, null, this );
5301                         if ( this.options.resize ) {
5302                                 this.$window.off( this.resizeEvent );
5303                         }
5304
5305                         /**
5306                          * call 'dispose' directly on the parent class
5307                          */
5308                         media.View.prototype.dispose.apply( this, arguments );
5309                 },
5310
5311                 setColumns: function() {
5312                         var prev = this.columns,
5313                                 width = this.$el.width();
5314
5315                         if ( width ) {
5316                                 this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1;
5317
5318                                 if ( ! prev || prev !== this.columns ) {
5319                                         this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns );
5320                                 }
5321                         }
5322                 },
5323
5324                 initSortable: function() {
5325                         var collection = this.collection;
5326
5327                         if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
5328                                 return;
5329                         }
5330
5331                         this.$el.sortable( _.extend({
5332                                 // If the `collection` has a `comparator`, disable sorting.
5333                                 disabled: !! collection.comparator,
5334
5335                                 // Prevent attachments from being dragged outside the bounding
5336                                 // box of the list.
5337                                 containment: this.$el,
5338
5339                                 // Change the position of the attachment as soon as the
5340                                 // mouse pointer overlaps a thumbnail.
5341                                 tolerance: 'pointer',
5342
5343                                 // Record the initial `index` of the dragged model.
5344                                 start: function( event, ui ) {
5345                                         ui.item.data('sortableIndexStart', ui.item.index());
5346                                 },
5347
5348                                 // Update the model's index in the collection.
5349                                 // Do so silently, as the view is already accurate.
5350                                 update: function( event, ui ) {
5351                                         var model = collection.at( ui.item.data('sortableIndexStart') ),
5352                                                 comparator = collection.comparator;
5353
5354                                         // Temporarily disable the comparator to prevent `add`
5355                                         // from re-sorting.
5356                                         delete collection.comparator;
5357
5358                                         // Silently shift the model to its new index.
5359                                         collection.remove( model, {
5360                                                 silent: true
5361                                         });
5362                                         collection.add( model, {
5363                                                 silent: true,
5364                                                 at:     ui.item.index()
5365                                         });
5366
5367                                         // Restore the comparator.
5368                                         collection.comparator = comparator;
5369
5370                                         // Fire the `reset` event to ensure other collections sync.
5371                                         collection.trigger( 'reset', collection );
5372
5373                                         // If the collection is sorted by menu order,
5374                                         // update the menu order.
5375                                         collection.saveMenuOrder();
5376                                 }
5377                         }, this.options.sortable ) );
5378
5379                         // If the `orderby` property is changed on the `collection`,
5380                         // check to see if we have a `comparator`. If so, disable sorting.
5381                         collection.props.on( 'change:orderby', function() {
5382                                 this.$el.sortable( 'option', 'disabled', !! collection.comparator );
5383                         }, this );
5384
5385                         this.collection.props.on( 'change:orderby', this.refreshSortable, this );
5386                         this.refreshSortable();
5387                 },
5388
5389                 refreshSortable: function() {
5390                         if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
5391                                 return;
5392                         }
5393
5394                         // If the `collection` has a `comparator`, disable sorting.
5395                         var collection = this.collection,
5396                                 orderby = collection.props.get('orderby'),
5397                                 enabled = 'menuOrder' === orderby || ! collection.comparator;
5398
5399                         this.$el.sortable( 'option', 'disabled', ! enabled );
5400                 },
5401
5402                 /**
5403                  * @param {wp.media.model.Attachment} attachment
5404                  * @returns {wp.media.View}
5405                  */
5406                 createAttachmentView: function( attachment ) {
5407                         var view = new this.options.AttachmentView({
5408                                 controller:           this.controller,
5409                                 model:                attachment,
5410                                 collection:           this.collection,
5411                                 selection:            this.options.selection
5412                         });
5413
5414                         return this._viewsByCid[ attachment.cid ] = view;
5415                 },
5416
5417                 prepare: function() {
5418                         // Create all of the Attachment views, and replace
5419                         // the list in a single DOM operation.
5420                         if ( this.collection.length ) {
5421                                 this.views.set( this.collection.map( this.createAttachmentView, this ) );
5422
5423                         // If there are no elements, clear the views and load some.
5424                         } else {
5425                                 this.views.unset();
5426                                 this.collection.more().done( this.scroll );
5427                         }
5428                 },
5429
5430                 ready: function() {
5431                         // Trigger the scroll event to check if we're within the
5432                         // threshold to query for additional attachments.
5433                         this.scroll();
5434                 },
5435
5436                 scroll: function() {
5437                         var view = this,
5438                                 el = this.options.scrollElement,
5439                                 scrollTop = el.scrollTop,
5440                                 toolbar;
5441
5442                         // The scroll event occurs on the document, but the element
5443                         // that should be checked is the document body.
5444                         if ( el == document ) {
5445                                 el = document.body;
5446                                 scrollTop = $(document).scrollTop();
5447                         }
5448
5449                         if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) {
5450                                 return;
5451                         }
5452
5453                         toolbar = this.views.parent.toolbar;
5454
5455                         // Show the spinner only if we are close to the bottom.
5456                         if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) {
5457                                 toolbar.get('spinner').show();
5458                         }
5459
5460                         if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) {
5461                                 this.collection.more().done(function() {
5462                                         view.scroll();
5463                                         toolbar.get('spinner').hide();
5464                                 });
5465                         }
5466                 }
5467         });
5468
5469         /**
5470          * wp.media.view.Search
5471          *
5472          * @constructor
5473          * @augments wp.media.View
5474          * @augments wp.Backbone.View
5475          * @augments Backbone.View
5476          */
5477         media.view.Search = media.View.extend({
5478                 tagName:   'input',
5479                 className: 'search',
5480                 id:        'media-search-input',
5481
5482                 attributes: {
5483                         type:        'search',
5484                         placeholder: l10n.search
5485                 },
5486
5487                 events: {
5488                         'input':  'search',
5489                         'keyup':  'search',
5490                         'change': 'search',
5491                         'search': 'search'
5492                 },
5493
5494                 /**
5495                  * @returns {wp.media.view.Search} Returns itself to allow chaining
5496                  */
5497                 render: function() {
5498                         this.el.value = this.model.escape('search');
5499                         return this;
5500                 },
5501
5502                 search: function( event ) {
5503                         if ( event.target.value ) {
5504                                 this.model.set( 'search', event.target.value );
5505                         } else {
5506                                 this.model.unset('search');
5507                         }
5508                 }
5509         });
5510
5511         /**
5512          * wp.media.view.AttachmentFilters
5513          *
5514          * @constructor
5515          * @augments wp.media.View
5516          * @augments wp.Backbone.View
5517          * @augments Backbone.View
5518          */
5519         media.view.AttachmentFilters = media.View.extend({
5520                 tagName:   'select',
5521                 className: 'attachment-filters',
5522                 id:        'media-attachment-filters',
5523
5524                 events: {
5525                         change: 'change'
5526                 },
5527
5528                 keys: [],
5529
5530                 initialize: function() {
5531                         this.createFilters();
5532                         _.extend( this.filters, this.options.filters );
5533
5534                         // Build `<option>` elements.
5535                         this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
5536                                 return {
5537                                         el: $( '<option></option>' ).val( value ).html( filter.text )[0],
5538                                         priority: filter.priority || 50
5539                                 };
5540                         }, this ).sortBy('priority').pluck('el').value() );
5541
5542                         this.model.on( 'change', this.select, this );
5543                         this.select();
5544                 },
5545
5546                 /**
5547                  * @abstract
5548                  */
5549                 createFilters: function() {
5550                         this.filters = {};
5551                 },
5552
5553                 /**
5554                  * When the selection changes, set the Query properties
5555                  * accordingly for the selected filter.
5556                  */
5557                 change: function() {
5558                         var filter = this.filters[ this.el.value ];
5559                         if ( filter ) {
5560                                 this.model.set( filter.props );
5561                         }
5562                 },
5563
5564                 select: function() {
5565                         var model = this.model,
5566                                 value = 'all',
5567                                 props = model.toJSON();
5568
5569                         _.find( this.filters, function( filter, id ) {
5570                                 var equal = _.all( filter.props, function( prop, key ) {
5571                                         return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
5572                                 });
5573
5574                                 if ( equal ) {
5575                                         return value = id;
5576                                 }
5577                         });
5578
5579                         this.$el.val( value );
5580                 }
5581         });
5582
5583         /**
5584          * wp.media.view.AttachmentFilters.Uploaded
5585          *
5586          * @constructor
5587          * @augments wp.media.view.AttachmentFilters
5588          * @augments wp.media.View
5589          * @augments wp.Backbone.View
5590          * @augments Backbone.View
5591          */
5592         media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
5593                 createFilters: function() {
5594                         var type = this.model.get('type'),
5595                                 types = media.view.settings.mimeTypes,
5596                                 text;
5597
5598                         if ( types && type ) {
5599                                 text = types[ type ];
5600                         }
5601
5602                         this.filters = {
5603                                 all: {
5604                                         text:  text || l10n.allMediaItems,
5605                                         props: {
5606                                                 uploadedTo: null,
5607                                                 orderby: 'date',
5608                                                 order:   'DESC'
5609                                         },
5610                                         priority: 10
5611                                 },
5612
5613                                 uploaded: {
5614                                         text:  l10n.uploadedToThisPost,
5615                                         props: {
5616                                                 uploadedTo: media.view.settings.post.id,
5617                                                 orderby: 'menuOrder',
5618                                                 order:   'ASC'
5619                                         },
5620                                         priority: 20
5621                                 }
5622                         };
5623                 }
5624         });
5625
5626         /**
5627          * wp.media.view.AttachmentFilters.All
5628          *
5629          * @constructor
5630          * @augments wp.media.view.AttachmentFilters
5631          * @augments wp.media.View
5632          * @augments wp.Backbone.View
5633          * @augments Backbone.View
5634          */
5635         media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
5636                 createFilters: function() {
5637                         var filters = {};
5638
5639                         _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
5640                                 filters[ key ] = {
5641                                         text: text,
5642                                         props: {
5643                                                 status:  null,
5644                                                 type:    key,
5645                                                 uploadedTo: null,
5646                                                 orderby: 'date',
5647                                                 order:   'DESC'
5648                                         }
5649                                 };
5650                         });
5651
5652                         filters.all = {
5653                                 text:  l10n.allMediaItems,
5654                                 props: {
5655                                         status:  null,
5656                                         type:    null,
5657                                         uploadedTo: null,
5658                                         orderby: 'date',
5659                                         order:   'DESC'
5660                                 },
5661                                 priority: 10
5662                         };
5663
5664                         if ( media.view.settings.post.id ) {
5665                                 filters.uploaded = {
5666                                         text:  l10n.uploadedToThisPost,
5667                                         props: {
5668                                                 status:  null,
5669                                                 type:    null,
5670                                                 uploadedTo: media.view.settings.post.id,
5671                                                 orderby: 'menuOrder',
5672                                                 order:   'ASC'
5673                                         },
5674                                         priority: 20
5675                                 };
5676                         }
5677
5678                         filters.unattached = {
5679                                 text:  l10n.unattached,
5680                                 props: {
5681                                         status:     null,
5682                                         uploadedTo: 0,
5683                                         type:       null,
5684                                         orderby:    'menuOrder',
5685                                         order:      'ASC'
5686                                 },
5687                                 priority: 50
5688                         };
5689
5690                         if ( media.view.settings.mediaTrash &&
5691                                 this.controller.isModeActive( 'grid' ) ) {
5692
5693                                 filters.trash = {
5694                                         text:  l10n.trash,
5695                                         props: {
5696                                                 uploadedTo: null,
5697                                                 status:     'trash',
5698                                                 type:       null,
5699                                                 orderby:    'date',
5700                                                 order:      'DESC'
5701                                         },
5702                                         priority: 50
5703                                 };
5704                         }
5705
5706                         this.filters = filters;
5707                 }
5708         });
5709
5710         /**
5711          * wp.media.view.AttachmentsBrowser
5712          *
5713          * @constructor
5714          * @augments wp.media.View
5715          * @augments wp.Backbone.View
5716          * @augments Backbone.View
5717          */
5718         media.view.AttachmentsBrowser = media.View.extend({
5719                 tagName:   'div',
5720                 className: 'attachments-browser',
5721
5722                 initialize: function() {
5723                         _.defaults( this.options, {
5724                                 filters: false,
5725                                 search:  true,
5726                                 display: false,
5727                                 sidebar: true,
5728                                 AttachmentView: media.view.Attachment.Library
5729                         });
5730
5731                         this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
5732
5733                         this.createToolbar();
5734                         if ( this.options.sidebar ) {
5735                                 this.createSidebar();
5736                         }
5737                         this.createUploader();
5738                         this.createAttachments();
5739                         this.updateContent();
5740
5741                         if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) {
5742                                 this.$el.addClass( 'hide-sidebar' );
5743
5744                                 if ( 'errors' === this.options.sidebar ) {
5745                                         this.$el.addClass( 'sidebar-for-errors' );
5746                                 }
5747                         }
5748
5749                         this.collection.on( 'add remove reset', this.updateContent, this );
5750                 },
5751                 /**
5752                  * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining
5753                  */
5754                 dispose: function() {
5755                         this.options.selection.off( null, null, this );
5756                         media.View.prototype.dispose.apply( this, arguments );
5757                         return this;
5758                 },
5759
5760                 createToolbar: function() {
5761                         var LibraryViewSwitcher, Filters, toolbarOptions;
5762
5763                         toolbarOptions = {
5764                                 controller: this.controller
5765                         };
5766
5767                         if ( this.controller.isModeActive( 'grid' ) ) {
5768                                 toolbarOptions.className = 'media-toolbar wp-filter';
5769                         }
5770
5771                         /**
5772                         * @member {wp.media.view.Toolbar}
5773                         */
5774                         this.toolbar = new media.view.Toolbar( toolbarOptions );
5775
5776                         this.views.add( this.toolbar );
5777
5778                         this.toolbar.set( 'spinner', new media.view.Spinner({
5779                                 priority: -60
5780                         }) );
5781
5782                         if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) {
5783                                 // "Filters" will return a <select>, need to render
5784                                 // screen reader text before
5785                                 this.toolbar.set( 'filtersLabel', new media.view.Label({
5786                                         value: l10n.filterByType,
5787                                         attributes: {
5788                                                 'for':  'media-attachment-filters'
5789                                         },
5790                                         priority:   -80
5791                                 }).render() );
5792
5793                                 if ( 'uploaded' === this.options.filters ) {
5794                                         this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({
5795                                                 controller: this.controller,
5796                                                 model:      this.collection.props,
5797                                                 priority:   -80
5798                                         }).render() );
5799                                 } else {
5800                                         Filters = new media.view.AttachmentFilters.All({
5801                                                 controller: this.controller,
5802                                                 model:      this.collection.props,
5803                                                 priority:   -80
5804                                         });
5805
5806                                         this.toolbar.set( 'filters', Filters.render() );
5807                                 }
5808                         }
5809
5810                         // Feels odd to bring the global media library switcher into the Attachment
5811                         // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar );
5812                         // which the controller can tap into and add this view?
5813                         if ( this.controller.isModeActive( 'grid' ) ) {
5814                                 LibraryViewSwitcher = media.View.extend({
5815                                         className: 'view-switch media-grid-view-switch',
5816                                         template: media.template( 'media-library-view-switcher')
5817                                 });
5818
5819                                 this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({
5820                                         controller: this.controller,
5821                                         priority: -90
5822                                 }).render() );
5823
5824                                 // DateFilter is a <select>, screen reader text needs to be rendered before
5825                                 this.toolbar.set( 'dateFilterLabel', new media.view.Label({
5826                                         value: l10n.filterByDate,
5827                                         attributes: {
5828                                                 'for': 'media-attachment-date-filters'
5829                                         },
5830                                         priority: -75
5831                                 }).render() );
5832                                 this.toolbar.set( 'dateFilter', new media.view.DateFilter({
5833                                         controller: this.controller,
5834                                         model:      this.collection.props,
5835                                         priority: -75
5836                                 }).render() );
5837
5838                                 // BulkSelection is a <div> with subviews, including screen reader text
5839                                 this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({
5840                                         text: l10n.bulkSelect,
5841                                         controller: this.controller,
5842                                         priority: -70
5843                                 }).render() );
5844
5845                                 this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
5846                                         filters: Filters,
5847                                         style: 'primary',
5848                                         disabled: true,
5849                                         text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
5850                                         controller: this.controller,
5851                                         priority: -60,
5852                                         click: function() {
5853                                                 var changed = [], removed = [], self = this,
5854                                                         selection = this.controller.state().get( 'selection' ),
5855                                                         library = this.controller.state().get( 'library' );
5856
5857                                                 if ( ! selection.length ) {
5858                                                         return;
5859                                                 }
5860
5861                                                 if ( ! media.view.settings.mediaTrash && ! confirm( l10n.warnBulkDelete ) ) {
5862                                                         return;
5863                                                 }
5864
5865                                                 if ( media.view.settings.mediaTrash &&
5866                                                         'trash' !== selection.at( 0 ).get( 'status' ) &&
5867                                                         ! confirm( l10n.warnBulkTrash ) ) {
5868
5869                                                         return;
5870                                                 }
5871
5872                                                 selection.each( function( model ) {
5873                                                         if ( ! model.get( 'nonces' )['delete'] ) {
5874                                                                 removed.push( model );
5875                                                                 return;
5876                                                         }
5877
5878                                                         if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
5879                                                                 model.set( 'status', 'inherit' );
5880                                                                 changed.push( model.save() );
5881                                                                 removed.push( model );
5882                                                         } else if ( media.view.settings.mediaTrash ) {
5883                                                                 model.set( 'status', 'trash' );
5884                                                                 changed.push( model.save() );
5885                                                                 removed.push( model );
5886                                                         } else {
5887                                                                 model.destroy();
5888                                                         }
5889                                                 } );
5890
5891                                                 if ( changed.length ) {
5892                                                         selection.remove( removed );
5893
5894                                                         $.when.apply( null, changed ).then( function() {
5895                                                                 library._requery( true );
5896                                                                 self.controller.trigger( 'selection:action:done' );
5897                                                         } );
5898                                                 } else {
5899                                                         this.controller.trigger( 'selection:action:done' );
5900                                                 }
5901                                         }
5902                                 }).render() );
5903                         }
5904
5905                         if ( this.options.search ) {
5906                                 // Search is an input, screen reader text needs to be rendered before
5907                                 this.toolbar.set( 'searchLabel', new media.view.Label({
5908                                         value: l10n.searchMediaLabel,
5909                                         attributes: {
5910                                                 'for': 'media-search-input'
5911                                         },
5912                                         priority:   60
5913                                 }).render() );
5914                                 this.toolbar.set( 'search', new media.view.Search({
5915                                         controller: this.controller,
5916                                         model:      this.collection.props,
5917                                         priority:   60
5918                                 }).render() );
5919                         }
5920
5921                         if ( this.options.dragInfo ) {
5922                                 this.toolbar.set( 'dragInfo', new media.View({
5923                                         el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
5924                                         priority: -40
5925                                 }) );
5926                         }
5927
5928                         if ( this.options.suggestedWidth && this.options.suggestedHeight ) {
5929                                 this.toolbar.set( 'suggestedDimensions', new media.View({
5930                                         el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' &times; ' + this.options.suggestedHeight + '</div>' )[0],
5931                                         priority: -40
5932                                 }) );
5933                         }
5934                 },
5935
5936                 updateContent: function() {
5937                         var view = this,
5938                                 noItemsView;
5939
5940                         if ( this.controller.isModeActive( 'grid' ) ) {
5941                                 noItemsView = view.attachmentsNoResults;
5942                         } else {
5943                                 noItemsView = view.uploader;
5944                         }
5945
5946                         if ( ! this.collection.length ) {
5947                                 this.toolbar.get( 'spinner' ).show();
5948                                 this.dfd = this.collection.more().done( function() {
5949                                         if ( ! view.collection.length ) {
5950                                                 noItemsView.$el.removeClass( 'hidden' );
5951                                         } else {
5952                                                 noItemsView.$el.addClass( 'hidden' );
5953                                         }
5954                                         view.toolbar.get( 'spinner' ).hide();
5955                                 } );
5956                         } else {
5957                                 noItemsView.$el.addClass( 'hidden' );
5958                                 view.toolbar.get( 'spinner' ).hide();
5959                         }
5960                 },
5961
5962                 createUploader: function() {
5963                         this.uploader = new media.view.UploaderInline({
5964                                 controller: this.controller,
5965                                 status:     false,
5966                                 message:    this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound,
5967                                 canClose:   this.controller.isModeActive( 'grid' )
5968                         });
5969
5970                         this.uploader.hide();
5971                         this.views.add( this.uploader );
5972                 },
5973
5974                 toggleUploader: function() {
5975                         if ( this.uploader.$el.hasClass( 'hidden' ) ) {
5976                                 this.uploader.show();
5977                         } else {
5978                                 this.uploader.hide();
5979                         }
5980                 },
5981
5982                 createAttachments: function() {
5983                         this.attachments = new media.view.Attachments({
5984                                 controller:           this.controller,
5985                                 collection:           this.collection,
5986                                 selection:            this.options.selection,
5987                                 model:                this.model,
5988                                 sortable:             this.options.sortable,
5989                                 scrollElement:        this.options.scrollElement,
5990                                 idealColumnWidth:     this.options.idealColumnWidth,
5991
5992                                 // The single `Attachment` view to be used in the `Attachments` view.
5993                                 AttachmentView: this.options.AttachmentView
5994                         });
5995
5996                         // Add keydown listener to the instance of the Attachments view
5997                         this.attachments.listenTo( this.controller, 'attachment:keydown:arrow',     this.attachments.arrowEvent );
5998                         this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus );
5999
6000                         this.views.add( this.attachments );
6001
6002
6003                         if ( this.controller.isModeActive( 'grid' ) ) {
6004                                 this.attachmentsNoResults = new media.View({
6005                                         controller: this.controller,
6006                                         tagName: 'p'
6007                                 });
6008
6009                                 this.attachmentsNoResults.$el.addClass( 'hidden no-media' );
6010                                 this.attachmentsNoResults.$el.html( l10n.noMedia );
6011
6012                                 this.views.add( this.attachmentsNoResults );
6013                         }
6014                 },
6015
6016                 createSidebar: function() {
6017                         var options = this.options,
6018                                 selection = options.selection,
6019                                 sidebar = this.sidebar = new media.view.Sidebar({
6020                                         controller: this.controller
6021                                 });
6022
6023                         this.views.add( sidebar );
6024
6025                         if ( this.controller.uploader ) {
6026                                 sidebar.set( 'uploads', new media.view.UploaderStatus({
6027                                         controller: this.controller,
6028                                         priority:   40
6029                                 }) );
6030                         }
6031
6032                         selection.on( 'selection:single', this.createSingle, this );
6033                         selection.on( 'selection:unsingle', this.disposeSingle, this );
6034
6035                         if ( selection.single() ) {
6036                                 this.createSingle();
6037                         }
6038                 },
6039
6040                 createSingle: function() {
6041                         var sidebar = this.sidebar,
6042                                 single = this.options.selection.single();
6043
6044                         sidebar.set( 'details', new media.view.Attachment.Details({
6045                                 controller: this.controller,
6046                                 model:      single,
6047                                 priority:   80
6048                         }) );
6049
6050                         sidebar.set( 'compat', new media.view.AttachmentCompat({
6051                                 controller: this.controller,
6052                                 model:      single,
6053                                 priority:   120
6054                         }) );
6055
6056                         if ( this.options.display ) {
6057                                 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
6058                                         controller:   this.controller,
6059                                         model:        this.model.display( single ),
6060                                         attachment:   single,
6061                                         priority:     160,
6062                                         userSettings: this.model.get('displayUserSettings')
6063                                 }) );
6064                         }
6065
6066                         // Show the sidebar on mobile
6067                         if ( this.model.id === 'insert' ) {
6068                                 sidebar.$el.addClass( 'visible' );
6069                         }
6070                 },
6071
6072                 disposeSingle: function() {
6073                         var sidebar = this.sidebar;
6074                         sidebar.unset('details');
6075                         sidebar.unset('compat');
6076                         sidebar.unset('display');
6077                         // Hide the sidebar on mobile
6078                         sidebar.$el.removeClass( 'visible' );
6079                 }
6080         });
6081
6082         /**
6083          * wp.media.view.Selection
6084          *
6085          * @constructor
6086          * @augments wp.media.View
6087          * @augments wp.Backbone.View
6088          * @augments Backbone.View
6089          */
6090         media.view.Selection = media.View.extend({
6091                 tagName:   'div',
6092                 className: 'media-selection',
6093                 template:  media.template('media-selection'),
6094
6095                 events: {
6096                         'click .edit-selection':  'edit',
6097                         'click .clear-selection': 'clear'
6098                 },
6099
6100                 initialize: function() {
6101                         _.defaults( this.options, {
6102                                 editable:  false,
6103                                 clearable: true
6104                         });
6105
6106                         /**
6107                          * @member {wp.media.view.Attachments.Selection}
6108                          */
6109                         this.attachments = new media.view.Attachments.Selection({
6110                                 controller: this.controller,
6111                                 collection: this.collection,
6112                                 selection:  this.collection,
6113                                 model:      new Backbone.Model()
6114                         });
6115
6116                         this.views.set( '.selection-view', this.attachments );
6117                         this.collection.on( 'add remove reset', this.refresh, this );
6118                         this.controller.on( 'content:activate', this.refresh, this );
6119                 },
6120
6121                 ready: function() {
6122                         this.refresh();
6123                 },
6124
6125                 refresh: function() {
6126                         // If the selection hasn't been rendered, bail.
6127                         if ( ! this.$el.children().length ) {
6128                                 return;
6129                         }
6130
6131                         var collection = this.collection,
6132                                 editing = 'edit-selection' === this.controller.content.mode();
6133
6134                         // If nothing is selected, display nothing.
6135                         this.$el.toggleClass( 'empty', ! collection.length );
6136                         this.$el.toggleClass( 'one', 1 === collection.length );
6137                         this.$el.toggleClass( 'editing', editing );
6138
6139                         this.$('.count').text( l10n.selected.replace('%d', collection.length) );
6140                 },
6141
6142                 edit: function( event ) {
6143                         event.preventDefault();
6144                         if ( this.options.editable ) {
6145                                 this.options.editable.call( this, this.collection );
6146                         }
6147                 },
6148
6149                 clear: function( event ) {
6150                         event.preventDefault();
6151                         this.collection.reset();
6152
6153                         // Keep focus inside media modal
6154                         // after clear link is selected
6155                         this.controller.modal.focusManager.focus();
6156                 }
6157         });
6158
6159
6160         /**
6161          * wp.media.view.Attachment.Selection
6162          *
6163          * @constructor
6164          * @augments wp.media.view.Attachment
6165          * @augments wp.media.View
6166          * @augments wp.Backbone.View
6167          * @augments Backbone.View
6168          */
6169         media.view.Attachment.Selection = media.view.Attachment.extend({
6170                 className: 'attachment selection',
6171
6172                 // On click, just select the model, instead of removing the model from
6173                 // the selection.
6174                 toggleSelection: function() {
6175                         this.options.selection.single( this.model );
6176                 }
6177         });
6178
6179         /**
6180          * wp.media.view.Attachments.Selection
6181          *
6182          * @constructor
6183          * @augments wp.media.view.Attachments
6184          * @augments wp.media.View
6185          * @augments wp.Backbone.View
6186          * @augments Backbone.View
6187          */
6188         media.view.Attachments.Selection = media.view.Attachments.extend({
6189                 events: {},
6190                 initialize: function() {
6191                         _.defaults( this.options, {
6192                                 sortable:   true,
6193                                 resize:     false,
6194
6195                                 // The single `Attachment` view to be used in the `Attachments` view.
6196                                 AttachmentView: media.view.Attachment.Selection
6197                         });
6198                         /**
6199                          * call 'initialize' directly on the parent class
6200                          */
6201                         return media.view.Attachments.prototype.initialize.apply( this, arguments );
6202                 }
6203         });
6204
6205         /**
6206          * wp.media.view.Attachments.EditSelection
6207          *
6208          * @constructor
6209          * @augments wp.media.view.Attachment.Selection
6210          * @augments wp.media.view.Attachment
6211          * @augments wp.media.View
6212          * @augments wp.Backbone.View
6213          * @augments Backbone.View
6214          */
6215         media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
6216                 buttons: {
6217                         close: true
6218                 }
6219         });
6220
6221
6222         /**
6223          * wp.media.view.Settings
6224          *
6225          * @constructor
6226          * @augments wp.media.View
6227          * @augments wp.Backbone.View
6228          * @augments Backbone.View
6229          */
6230         media.view.Settings = media.View.extend({
6231                 events: {
6232                         'click button':    'updateHandler',
6233                         'change input':    'updateHandler',
6234                         'change select':   'updateHandler',
6235                         'change textarea': 'updateHandler'
6236                 },
6237
6238                 initialize: function() {
6239                         this.model = this.model || new Backbone.Model();
6240                         this.model.on( 'change', this.updateChanges, this );
6241                 },
6242
6243                 prepare: function() {
6244                         return _.defaults({
6245                                 model: this.model.toJSON()
6246                         }, this.options );
6247                 },
6248                 /**
6249                  * @returns {wp.media.view.Settings} Returns itself to allow chaining
6250                  */
6251                 render: function() {
6252                         media.View.prototype.render.apply( this, arguments );
6253                         // Select the correct values.
6254                         _( this.model.attributes ).chain().keys().each( this.update, this );
6255                         return this;
6256                 },
6257                 /**
6258                  * @param {string} key
6259                  */
6260                 update: function( key ) {
6261                         var value = this.model.get( key ),
6262                                 $setting = this.$('[data-setting="' + key + '"]'),
6263                                 $buttons, $value;
6264
6265                         // Bail if we didn't find a matching setting.
6266                         if ( ! $setting.length ) {
6267                                 return;
6268                         }
6269
6270                         // Attempt to determine how the setting is rendered and update
6271                         // the selected value.
6272
6273                         // Handle dropdowns.
6274                         if ( $setting.is('select') ) {
6275                                 $value = $setting.find('[value="' + value + '"]');
6276
6277                                 if ( $value.length ) {
6278                                         $setting.find('option').prop( 'selected', false );
6279                                         $value.prop( 'selected', true );
6280                                 } else {
6281                                         // If we can't find the desired value, record what *is* selected.
6282                                         this.model.set( key, $setting.find(':selected').val() );
6283                                 }
6284
6285                         // Handle button groups.
6286                         } else if ( $setting.hasClass('button-group') ) {
6287                                 $buttons = $setting.find('button').removeClass('active');
6288                                 $buttons.filter( '[value="' + value + '"]' ).addClass('active');
6289
6290                         // Handle text inputs and textareas.
6291                         } else if ( $setting.is('input[type="text"], textarea') ) {
6292                                 if ( ! $setting.is(':focus') ) {
6293                                         $setting.val( value );
6294                                 }
6295                         // Handle checkboxes.
6296                         } else if ( $setting.is('input[type="checkbox"]') ) {
6297                                 $setting.prop( 'checked', !! value && 'false' !== value );
6298                         }
6299                 },
6300                 /**
6301                  * @param {Object} event
6302                  */
6303                 updateHandler: function( event ) {
6304                         var $setting = $( event.target ).closest('[data-setting]'),
6305                                 value = event.target.value,
6306                                 userSetting;
6307
6308                         event.preventDefault();
6309
6310                         if ( ! $setting.length ) {
6311                                 return;
6312                         }
6313
6314                         // Use the correct value for checkboxes.
6315                         if ( $setting.is('input[type="checkbox"]') ) {
6316                                 value = $setting[0].checked;
6317                         }
6318
6319                         // Update the corresponding setting.
6320                         this.model.set( $setting.data('setting'), value );
6321
6322                         // If the setting has a corresponding user setting,
6323                         // update that as well.
6324                         if ( userSetting = $setting.data('userSetting') ) {
6325                                 setUserSetting( userSetting, value );
6326                         }
6327                 },
6328
6329                 updateChanges: function( model ) {
6330                         if ( model.hasChanged() ) {
6331                                 _( model.changed ).chain().keys().each( this.update, this );
6332                         }
6333                 }
6334         });
6335
6336         /**
6337          * wp.media.view.Settings.AttachmentDisplay
6338          *
6339          * @constructor
6340          * @augments wp.media.view.Settings
6341          * @augments wp.media.View
6342          * @augments wp.Backbone.View
6343          * @augments Backbone.View
6344          */
6345         media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
6346                 className: 'attachment-display-settings',
6347                 template:  media.template('attachment-display-settings'),
6348
6349                 initialize: function() {
6350                         var attachment = this.options.attachment;
6351
6352                         _.defaults( this.options, {
6353                                 userSettings: false
6354                         });
6355                         /**
6356                          * call 'initialize' directly on the parent class
6357                          */
6358                         media.view.Settings.prototype.initialize.apply( this, arguments );
6359                         this.model.on( 'change:link', this.updateLinkTo, this );
6360
6361                         if ( attachment ) {
6362                                 attachment.on( 'change:uploading', this.render, this );
6363                         }
6364                 },
6365
6366                 dispose: function() {
6367                         var attachment = this.options.attachment;
6368                         if ( attachment ) {
6369                                 attachment.off( null, null, this );
6370                         }
6371                         /**
6372                          * call 'dispose' directly on the parent class
6373                          */
6374                         media.view.Settings.prototype.dispose.apply( this, arguments );
6375                 },
6376                 /**
6377                  * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining
6378                  */
6379                 render: function() {
6380                         var attachment = this.options.attachment;
6381                         if ( attachment ) {
6382                                 _.extend( this.options, {
6383                                         sizes: attachment.get('sizes'),
6384                                         type:  attachment.get('type')
6385                                 });
6386                         }
6387                         /**
6388                          * call 'render' directly on the parent class
6389                          */
6390                         media.view.Settings.prototype.render.call( this );
6391                         this.updateLinkTo();
6392                         return this;
6393                 },
6394
6395                 updateLinkTo: function() {
6396                         var linkTo = this.model.get('link'),
6397                                 $input = this.$('.link-to-custom'),
6398                                 attachment = this.options.attachment;
6399
6400                         if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
6401                                 $input.addClass( 'hidden' );
6402                                 return;
6403                         }
6404
6405                         if ( attachment ) {
6406                                 if ( 'post' === linkTo ) {
6407                                         $input.val( attachment.get('link') );
6408                                 } else if ( 'file' === linkTo ) {
6409                                         $input.val( attachment.get('url') );
6410                                 } else if ( ! this.model.get('linkUrl') ) {
6411                                         $input.val('http://');
6412                                 }
6413
6414                                 $input.prop( 'readonly', 'custom' !== linkTo );
6415                         }
6416
6417                         $input.removeClass( 'hidden' );
6418
6419                         // If the input is visible, focus and select its contents.
6420                         if ( ! isTouchDevice && $input.is(':visible') ) {
6421                                 $input.focus()[0].select();
6422                         }
6423                 }
6424         });
6425
6426         /**
6427          * wp.media.view.Settings.Gallery
6428          *
6429          * @constructor
6430          * @augments wp.media.view.Settings
6431          * @augments wp.media.View
6432          * @augments wp.Backbone.View
6433          * @augments Backbone.View
6434          */
6435         media.view.Settings.Gallery = media.view.Settings.extend({
6436                 className: 'collection-settings gallery-settings',
6437                 template:  media.template('gallery-settings')
6438         });
6439
6440         /**
6441          * wp.media.view.Settings.Playlist
6442          *
6443          * @constructor
6444          * @augments wp.media.view.Settings
6445          * @augments wp.media.View
6446          * @augments wp.Backbone.View
6447          * @augments Backbone.View
6448          */
6449         media.view.Settings.Playlist = media.view.Settings.extend({
6450                 className: 'collection-settings playlist-settings',
6451                 template:  media.template('playlist-settings')
6452         });
6453
6454         /**
6455          * wp.media.view.Attachment.Details
6456          *
6457          * @constructor
6458          * @augments wp.media.view.Attachment
6459          * @augments wp.media.View
6460          * @augments wp.Backbone.View
6461          * @augments Backbone.View
6462          */
6463         media.view.Attachment.Details = media.view.Attachment.extend({
6464                 tagName:   'div',
6465                 className: 'attachment-details',
6466                 template:  media.template('attachment-details'),
6467
6468                 events: {
6469                         'change [data-setting]':          'updateSetting',
6470                         'change [data-setting] input':    'updateSetting',
6471                         'change [data-setting] select':   'updateSetting',
6472                         'change [data-setting] textarea': 'updateSetting',
6473                         'click .delete-attachment':       'deleteAttachment',
6474                         'click .trash-attachment':        'trashAttachment',
6475                         'click .untrash-attachment':      'untrashAttachment',
6476                         'click .edit-attachment':         'editAttachment',
6477                         'click .refresh-attachment':      'refreshAttachment',
6478                         'keydown':                        'toggleSelectionHandler'
6479                 },
6480
6481                 initialize: function() {
6482                         this.options = _.defaults( this.options, {
6483                                 rerenderOnModelChange: false
6484                         });
6485
6486                         this.on( 'ready', this.initialFocus );
6487                         /**
6488                          * call 'initialize' directly on the parent class
6489                          */
6490                         media.view.Attachment.prototype.initialize.apply( this, arguments );
6491                 },
6492
6493                 initialFocus: function() {
6494                         if ( ! isTouchDevice ) {
6495                                 this.$( ':input' ).eq( 0 ).focus();
6496                         }
6497                 },
6498                 /**
6499                  * @param {Object} event
6500                  */
6501                 deleteAttachment: function( event ) {
6502                         event.preventDefault();
6503
6504                         if ( confirm( l10n.warnDelete ) ) {
6505                                 this.model.destroy();
6506                                 // Keep focus inside media modal
6507                                 // after image is deleted
6508                                 this.controller.modal.focusManager.focus();
6509                         }
6510                 },
6511                 /**
6512                  * @param {Object} event
6513                  */
6514                 trashAttachment: function( event ) {
6515                         var library = this.controller.library;
6516                         event.preventDefault();
6517
6518                         if ( media.view.settings.mediaTrash &&
6519                                 'edit-metadata' === this.controller.content.mode() ) {
6520
6521                                 this.model.set( 'status', 'trash' );
6522                                 this.model.save().done( function() {
6523                                         library._requery( true );
6524                                 } );
6525                         }  else {
6526                                 this.model.destroy();
6527                         }
6528                 },
6529                 /**
6530                  * @param {Object} event
6531                  */
6532                 untrashAttachment: function( event ) {
6533                         var library = this.controller.library;
6534                         event.preventDefault();
6535
6536                         this.model.set( 'status', 'inherit' );
6537                         this.model.save().done( function() {
6538                                 library._requery( true );
6539                         } );
6540                 },
6541                 /**
6542                  * @param {Object} event
6543                  */
6544                 editAttachment: function( event ) {
6545                         var editState = this.controller.states.get( 'edit-image' );
6546                         if ( window.imageEdit && editState ) {
6547                                 event.preventDefault();
6548
6549                                 editState.set( 'image', this.model );
6550                                 this.controller.setState( 'edit-image' );
6551                         } else {
6552                                 this.$el.addClass('needs-refresh');
6553                         }
6554                 },
6555                 /**
6556                  * @param {Object} event
6557                  */
6558                 refreshAttachment: function( event ) {
6559                         this.$el.removeClass('needs-refresh');
6560                         event.preventDefault();
6561                         this.model.fetch();
6562                 },
6563                 /**
6564                  * When reverse tabbing(shift+tab) out of the right details panel, deliver
6565                  * the focus to the item in the list that was being edited.
6566                  *
6567                  * @param {Object} event
6568                  */
6569                 toggleSelectionHandler: function( event ) {
6570                         if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) {
6571                                 this.controller.trigger( 'attachment:details:shift-tab', event );
6572                                 return false;
6573                         }
6574
6575                         if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
6576                                 this.controller.trigger( 'attachment:keydown:arrow', event );
6577                                 return;
6578                         }
6579                 }
6580         });
6581
6582         /**
6583          * wp.media.view.AttachmentCompat
6584          *
6585          * A view to display fields added via the `attachment_fields_to_edit` filter.
6586          *
6587          * @constructor
6588          * @augments wp.media.View
6589          * @augments wp.Backbone.View
6590          * @augments Backbone.View
6591          */
6592         media.view.AttachmentCompat = media.View.extend({
6593                 tagName:   'form',
6594                 className: 'compat-item',
6595
6596                 events: {
6597                         'submit':          'preventDefault',
6598                         'change input':    'save',
6599                         'change select':   'save',
6600                         'change textarea': 'save'
6601                 },
6602
6603                 initialize: function() {
6604                         this.model.on( 'change:compat', this.render, this );
6605                 },
6606                 /**
6607                  * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining
6608                  */
6609                 dispose: function() {
6610                         if ( this.$(':focus').length ) {
6611                                 this.save();
6612                         }
6613                         /**
6614                          * call 'dispose' directly on the parent class
6615                          */
6616                         return media.View.prototype.dispose.apply( this, arguments );
6617                 },
6618                 /**
6619                  * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining
6620                  */
6621                 render: function() {
6622                         var compat = this.model.get('compat');
6623                         if ( ! compat || ! compat.item ) {
6624                                 return;
6625                         }
6626
6627                         this.views.detach();
6628                         this.$el.html( compat.item );
6629                         this.views.render();
6630                         return this;
6631                 },
6632                 /**
6633                  * @param {Object} event
6634                  */
6635                 preventDefault: function( event ) {
6636                         event.preventDefault();
6637                 },
6638                 /**
6639                  * @param {Object} event
6640                  */
6641                 save: function( event ) {
6642                         var data = {};
6643
6644                         if ( event ) {
6645                                 event.preventDefault();
6646                         }
6647
6648                         _.each( this.$el.serializeArray(), function( pair ) {
6649                                 data[ pair.name ] = pair.value;
6650                         });
6651
6652                         this.model.saveCompat( data );
6653                 }
6654         });
6655
6656         /**
6657          * wp.media.view.Iframe
6658          *
6659          * @constructor
6660          * @augments wp.media.View
6661          * @augments wp.Backbone.View
6662          * @augments Backbone.View
6663          */
6664         media.view.Iframe = media.View.extend({
6665                 className: 'media-iframe',
6666                 /**
6667                  * @returns {wp.media.view.Iframe} Returns itself to allow chaining
6668                  */
6669                 render: function() {
6670                         this.views.detach();
6671                         this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
6672                         this.views.render();
6673                         return this;
6674                 }
6675         });
6676
6677         /**
6678          * wp.media.view.Embed
6679          *
6680          * @constructor
6681          * @augments wp.media.View
6682          * @augments wp.Backbone.View
6683          * @augments Backbone.View
6684          */
6685         media.view.Embed = media.View.extend({
6686                 className: 'media-embed',
6687
6688                 initialize: function() {
6689                         /**
6690                          * @member {wp.media.view.EmbedUrl}
6691                          */
6692                         this.url = new media.view.EmbedUrl({
6693                                 controller: this.controller,
6694                                 model:      this.model.props
6695                         }).render();
6696
6697                         this.views.set([ this.url ]);
6698                         this.refresh();
6699                         this.model.on( 'change:type', this.refresh, this );
6700                         this.model.on( 'change:loading', this.loading, this );
6701                 },
6702
6703                 /**
6704                  * @param {Object} view
6705                  */
6706                 settings: function( view ) {
6707                         if ( this._settings ) {
6708                                 this._settings.remove();
6709                         }
6710                         this._settings = view;
6711                         this.views.add( view );
6712                 },
6713
6714                 refresh: function() {
6715                         var type = this.model.get('type'),
6716                                 constructor;
6717
6718                         if ( 'image' === type ) {
6719                                 constructor = media.view.EmbedImage;
6720                         } else if ( 'link' === type ) {
6721                                 constructor = media.view.EmbedLink;
6722                         } else {
6723                                 return;
6724                         }
6725
6726                         this.settings( new constructor({
6727                                 controller: this.controller,
6728                                 model:      this.model.props,
6729                                 priority:   40
6730                         }) );
6731                 },
6732
6733                 loading: function() {
6734                         this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
6735                 }
6736         });
6737
6738         /**
6739          * @constructor
6740          * @augments wp.media.View
6741          * @augments wp.Backbone.View
6742          * @augments Backbone.View
6743          */
6744         media.view.Label = media.View.extend({
6745                 tagName: 'label',
6746                 className: 'screen-reader-text',
6747
6748                 initialize: function() {
6749                         this.value = this.options.value;
6750                 },
6751
6752                 render: function() {
6753                         this.$el.html( this.value );
6754
6755                         return this;
6756                 }
6757         });
6758
6759         /**
6760          * wp.media.view.EmbedUrl
6761          *
6762          * @constructor
6763          * @augments wp.media.View
6764          * @augments wp.Backbone.View
6765          * @augments Backbone.View
6766          */
6767         media.view.EmbedUrl = media.View.extend({
6768                 tagName:   'label',
6769                 className: 'embed-url',
6770
6771                 events: {
6772                         'input':  'url',
6773                         'keyup':  'url',
6774                         'change': 'url'
6775                 },
6776
6777                 initialize: function() {
6778                         var self = this;
6779
6780                         this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') );
6781                         this.input = this.$input[0];
6782
6783                         this.spinner = $('<span class="spinner" />')[0];
6784                         this.$el.append([ this.input, this.spinner ]);
6785
6786                         this.model.on( 'change:url', this.render, this );
6787
6788                         if ( this.model.get( 'url' ) ) {
6789                                 _.delay( function () {
6790                                         self.model.trigger( 'change:url' );
6791                                 }, 500 );
6792                         }
6793                 },
6794                 /**
6795                  * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining
6796                  */
6797                 render: function() {
6798                         var $input = this.$input;
6799
6800                         if ( $input.is(':focus') ) {
6801                                 return;
6802                         }
6803
6804                         this.input.value = this.model.get('url') || 'http://';
6805                         /**
6806                          * Call `render` directly on parent class with passed arguments
6807                          */
6808                         media.View.prototype.render.apply( this, arguments );
6809                         return this;
6810                 },
6811
6812                 ready: function() {
6813                         if ( ! isTouchDevice ) {
6814                                 this.focus();
6815                         }
6816                 },
6817
6818                 url: function( event ) {
6819                         this.model.set( 'url', event.target.value );
6820                 },
6821
6822                 /**
6823                  * If the input is visible, focus and select its contents.
6824                  */
6825                 focus: function() {
6826                         var $input = this.$input;
6827                         if ( $input.is(':visible') ) {
6828                                 $input.focus()[0].select();
6829                         }
6830                 }
6831         });
6832
6833         /**
6834          * wp.media.view.EmbedLink
6835          *
6836          * @constructor
6837          * @augments wp.media.view.Settings
6838          * @augments wp.media.View
6839          * @augments wp.Backbone.View
6840          * @augments Backbone.View
6841          */
6842         media.view.EmbedLink = media.view.Settings.extend({
6843                 className: 'embed-link-settings',
6844                 template:  media.template('embed-link-settings'),
6845
6846                 initialize: function() {
6847                         this.spinner = $('<span class="spinner" />');
6848                         this.$el.append( this.spinner[0] );
6849                         this.listenTo( this.model, 'change:url', this.updateoEmbed );
6850                 },
6851
6852                 updateoEmbed: function() {
6853                         var url = this.model.get( 'url' );
6854
6855                         this.$('.setting.title').show();
6856                         // clear out previous results
6857                         this.$('.embed-container').hide().find('.embed-preview').html('');
6858
6859                         // only proceed with embed if the field contains more than 6 characters
6860                         if ( url && url.length < 6 ) {
6861                                 return;
6862                         }
6863
6864                         this.spinner.show();
6865
6866                         setTimeout( _.bind( this.fetch, this ), 500 );
6867                 },
6868
6869                 fetch: function() {
6870                         // check if they haven't typed in 500 ms
6871                         if ( $('#embed-url-field').val() !== this.model.get('url') ) {
6872                                 return;
6873                         }
6874
6875                         wp.ajax.send( 'parse-embed', {
6876                                 data : {
6877                                         post_ID: media.view.settings.post.id,
6878                                         shortcode: '[embed]' + this.model.get('url') + '[/embed]'
6879                                 }
6880                         } ).done( _.bind( this.renderoEmbed, this ) );
6881                 },
6882
6883                 renderoEmbed: function( response ) {
6884                         var html = ( response && response.body ) || '';
6885
6886                         this.spinner.hide();
6887
6888                         this.$('.setting.title').hide();
6889                         this.$('.embed-container').show().find('.embed-preview').html( html );
6890                 }
6891         });
6892
6893         /**
6894          * wp.media.view.EmbedImage
6895          *
6896          * @constructor
6897          * @augments wp.media.view.Settings.AttachmentDisplay
6898          * @augments wp.media.view.Settings
6899          * @augments wp.media.View
6900          * @augments wp.Backbone.View
6901          * @augments Backbone.View
6902          */
6903         media.view.EmbedImage =  media.view.Settings.AttachmentDisplay.extend({
6904                 className: 'embed-media-settings',
6905                 template:  media.template('embed-image-settings'),
6906
6907                 initialize: function() {
6908                         /**
6909                          * Call `initialize` directly on parent class with passed arguments
6910                          */
6911                         media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
6912                         this.model.on( 'change:url', this.updateImage, this );
6913                 },
6914
6915                 updateImage: function() {
6916                         this.$('img').attr( 'src', this.model.get('url') );
6917                 }
6918         });
6919
6920         /**
6921          * wp.media.view.ImageDetails
6922          *
6923          * @constructor
6924          * @augments wp.media.view.Settings.AttachmentDisplay
6925          * @augments wp.media.view.Settings
6926          * @augments wp.media.View
6927          * @augments wp.Backbone.View
6928          * @augments Backbone.View
6929          */
6930         media.view.ImageDetails = media.view.Settings.AttachmentDisplay.extend({
6931                 className: 'image-details',
6932                 template:  media.template('image-details'),
6933                 events: _.defaults( media.view.Settings.AttachmentDisplay.prototype.events, {
6934                         'click .edit-attachment': 'editAttachment',
6935                         'click .replace-attachment': 'replaceAttachment',
6936                         'click .advanced-toggle': 'onToggleAdvanced',
6937                         'change [data-setting="customWidth"]': 'onCustomSize',
6938                         'change [data-setting="customHeight"]': 'onCustomSize',
6939                         'keyup [data-setting="customWidth"]': 'onCustomSize',
6940                         'keyup [data-setting="customHeight"]': 'onCustomSize'
6941                 } ),
6942                 initialize: function() {
6943                         // used in AttachmentDisplay.prototype.updateLinkTo
6944                         this.options.attachment = this.model.attachment;
6945                         this.listenTo( this.model, 'change:url', this.updateUrl );
6946                         this.listenTo( this.model, 'change:link', this.toggleLinkSettings );
6947                         this.listenTo( this.model, 'change:size', this.toggleCustomSize );
6948
6949                         media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
6950                 },
6951
6952                 prepare: function() {
6953                         var attachment = false;
6954
6955                         if ( this.model.attachment ) {
6956                                 attachment = this.model.attachment.toJSON();
6957                         }
6958                         return _.defaults({
6959                                 model: this.model.toJSON(),
6960                                 attachment: attachment
6961                         }, this.options );
6962                 },
6963
6964                 render: function() {
6965                         var self = this,
6966                                 args = arguments;
6967
6968                         if ( this.model.attachment && 'pending' === this.model.dfd.state() ) {
6969                                 this.model.dfd.done( function() {
6970                                         media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
6971                                         self.postRender();
6972                                 } ).fail( function() {
6973                                         self.model.attachment = false;
6974                                         media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
6975                                         self.postRender();
6976                                 } );
6977                         } else {
6978                                 media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments );
6979                                 this.postRender();
6980                         }
6981
6982                         return this;
6983                 },
6984
6985                 postRender: function() {
6986                         setTimeout( _.bind( this.resetFocus, this ), 10 );
6987                         this.toggleLinkSettings();
6988                         if ( getUserSetting( 'advImgDetails' ) === 'show' ) {
6989                                 this.toggleAdvanced( true );
6990                         }
6991                         this.trigger( 'post-render' );
6992                 },
6993
6994                 resetFocus: function() {
6995                         this.$( '.link-to-custom' ).blur();
6996                         this.$( '.embed-media-settings' ).scrollTop( 0 );
6997                 },
6998
6999                 updateUrl: function() {
7000                         this.$( '.image img' ).attr( 'src', this.model.get( 'url' ) );
7001                         this.$( '.url' ).val( this.model.get( 'url' ) );
7002                 },
7003
7004                 toggleLinkSettings: function() {
7005                         if ( this.model.get( 'link' ) === 'none' ) {
7006                                 this.$( '.link-settings' ).addClass('hidden');
7007                         } else {
7008                                 this.$( '.link-settings' ).removeClass('hidden');
7009                         }
7010                 },
7011
7012                 toggleCustomSize: function() {
7013                         if ( this.model.get( 'size' ) !== 'custom' ) {
7014                                 this.$( '.custom-size' ).addClass('hidden');
7015                         } else {
7016                                 this.$( '.custom-size' ).removeClass('hidden');
7017                         }
7018                 },
7019
7020                 onCustomSize: function( event ) {
7021                         var dimension = $( event.target ).data('setting'),
7022                                 num = $( event.target ).val(),
7023                                 value;
7024
7025                         // Ignore bogus input
7026                         if ( ! /^\d+/.test( num ) || parseInt( num, 10 ) < 1 ) {
7027                                 event.preventDefault();
7028                                 return;
7029                         }
7030
7031                         if ( dimension === 'customWidth' ) {
7032                                 value = Math.round( 1 / this.model.get( 'aspectRatio' ) * num );
7033                                 this.model.set( 'customHeight', value, { silent: true } );
7034                                 this.$( '[data-setting="customHeight"]' ).val( value );
7035                         } else {
7036                                 value = Math.round( this.model.get( 'aspectRatio' ) * num );
7037                                 this.model.set( 'customWidth', value, { silent: true  } );
7038                                 this.$( '[data-setting="customWidth"]' ).val( value );
7039                         }
7040                 },
7041
7042                 onToggleAdvanced: function( event ) {
7043                         event.preventDefault();
7044                         this.toggleAdvanced();
7045                 },
7046
7047                 toggleAdvanced: function( show ) {
7048                         var $advanced = this.$el.find( '.advanced-section' ),
7049                                 mode;
7050
7051                         if ( $advanced.hasClass('advanced-visible') || show === false ) {
7052                                 $advanced.removeClass('advanced-visible');
7053                                 $advanced.find('.advanced-settings').addClass('hidden');
7054                                 mode = 'hide';
7055                         } else {
7056                                 $advanced.addClass('advanced-visible');
7057                                 $advanced.find('.advanced-settings').removeClass('hidden');
7058                                 mode = 'show';
7059                         }
7060
7061                         setUserSetting( 'advImgDetails', mode );
7062                 },
7063
7064                 editAttachment: function( event ) {
7065                         var editState = this.controller.states.get( 'edit-image' );
7066
7067                         if ( window.imageEdit && editState ) {
7068                                 event.preventDefault();
7069                                 editState.set( 'image', this.model.attachment );
7070                                 this.controller.setState( 'edit-image' );
7071                         }
7072                 },
7073
7074                 replaceAttachment: function( event ) {
7075                         event.preventDefault();
7076                         this.controller.setState( 'replace-image' );
7077                 }
7078         });
7079
7080         /**
7081          * wp.media.view.Cropper
7082          *
7083          * Uses the imgAreaSelect plugin to allow a user to crop an image.
7084          *
7085          * Takes imgAreaSelect options from
7086          * wp.customize.HeaderControl.calculateImageSelectOptions via
7087          * wp.customize.HeaderControl.openMM.
7088          *
7089          * @constructor
7090          * @augments wp.media.View
7091          * @augments wp.Backbone.View
7092          * @augments Backbone.View
7093          */
7094         media.view.Cropper = media.View.extend({
7095                 className: 'crop-content',
7096                 template: media.template('crop-content'),
7097                 initialize: function() {
7098                         _.bindAll(this, 'onImageLoad');
7099                 },
7100                 ready: function() {
7101                         this.controller.frame.on('content:error:crop', this.onError, this);
7102                         this.$image = this.$el.find('.crop-image');
7103                         this.$image.on('load', this.onImageLoad);
7104                         $(window).on('resize.cropper', _.debounce(this.onImageLoad, 250));
7105                 },
7106                 remove: function() {
7107                         $(window).off('resize.cropper');
7108                         this.$el.remove();
7109                         this.$el.off();
7110                         wp.media.View.prototype.remove.apply(this, arguments);
7111                 },
7112                 prepare: function() {
7113                         return {
7114                                 title: l10n.cropYourImage,
7115                                 url: this.options.attachment.get('url')
7116                         };
7117                 },
7118                 onImageLoad: function() {
7119                         var imgOptions = this.controller.get('imgSelectOptions');
7120                         if (typeof imgOptions === 'function') {
7121                                 imgOptions = imgOptions(this.options.attachment, this.controller);
7122                         }
7123
7124                         imgOptions = _.extend(imgOptions, {parent: this.$el});
7125                         this.trigger('image-loaded');
7126                         this.controller.imgSelect = this.$image.imgAreaSelect(imgOptions);
7127                 },
7128                 onError: function() {
7129                         var filename = this.options.attachment.get('filename');
7130
7131                         this.views.add( '.upload-errors', new media.view.UploaderStatusError({
7132                                 filename: media.view.UploaderStatus.prototype.filename(filename),
7133                                 message: _wpMediaViewsL10n.cropError
7134                         }), { at: 0 });
7135                 }
7136         });
7137
7138         media.view.EditImage = media.View.extend({
7139
7140                 className: 'image-editor',
7141                 template: media.template('image-editor'),
7142
7143                 initialize: function( options ) {
7144                         this.editor = window.imageEdit;
7145                         this.controller = options.controller;
7146                         media.View.prototype.initialize.apply( this, arguments );
7147                 },
7148
7149                 prepare: function() {
7150                         return this.model.toJSON();
7151                 },
7152
7153                 render: function() {
7154                         media.View.prototype.render.apply( this, arguments );
7155                         return this;
7156                 },
7157
7158                 loadEditor: function() {
7159                         var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
7160                         dfd.done( _.bind( this.focus, this ) );
7161                 },
7162
7163                 focus: function() {
7164                         this.$( '.imgedit-submit .button' ).eq( 0 ).focus();
7165                 },
7166
7167                 back: function() {
7168                         var lastState = this.controller.lastState();
7169                         this.controller.setState( lastState );
7170                 },
7171
7172                 refresh: function() {
7173                         this.model.fetch();
7174                 },
7175
7176                 save: function() {
7177                         var self = this,
7178                                 lastState = this.controller.lastState();
7179
7180                         this.model.fetch().done( function() {
7181                                 self.controller.setState( lastState );
7182                         });
7183                 }
7184
7185         });
7186
7187         /**
7188          * wp.media.view.Spinner
7189          *
7190          * @constructor
7191          * @augments wp.media.View
7192          * @augments wp.Backbone.View
7193          * @augments Backbone.View
7194          */
7195         media.view.Spinner = media.View.extend({
7196                 tagName:   'span',
7197                 className: 'spinner',
7198                 spinnerTimeout: false,
7199                 delay: 400,
7200
7201                 show: function() {
7202                         if ( ! this.spinnerTimeout ) {
7203                                 this.spinnerTimeout = _.delay(function( $el ) {
7204                                         $el.show();
7205                                 }, this.delay, this.$el );
7206                         }
7207
7208                         return this;
7209                 },
7210
7211                 hide: function() {
7212                         this.$el.hide();
7213                         this.spinnerTimeout = clearTimeout( this.spinnerTimeout );
7214
7215                         return this;
7216                 }
7217         });
7218 }(jQuery, _));