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