6ce8470a409e3858012353b4356eb719f3d3ad25
[autoinstalls/wordpress.git] / wp-includes / js / media-views.js
1 (function($){
2         var media       = wp.media,
3                 Attachment  = media.model.Attachment,
4                 Attachments = media.model.Attachments,
5                 Query       = media.model.Query,
6                 l10n;
7
8         // Link any localized strings.
9         l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
10
11         // Link any settings.
12         media.view.settings = l10n.settings || {};
13         delete l10n.settings;
14
15         // Copy the `post` setting over to the model settings.
16         media.model.settings.post = media.view.settings.post;
17
18         // Check if the browser supports CSS 3.0 transitions
19         $.support.transition = (function(){
20                 var style = document.documentElement.style,
21                         transitions = {
22                                 WebkitTransition: 'webkitTransitionEnd',
23                                 MozTransition:    'transitionend',
24                                 OTransition:      'oTransitionEnd otransitionend',
25                                 transition:       'transitionend'
26                         }, transition;
27
28                 transition = _.find( _.keys( transitions ), function( transition ) {
29                         return ! _.isUndefined( style[ transition ] );
30                 });
31
32                 return transition && {
33                         end: transitions[ transition ]
34                 };
35         }());
36
37         // Makes it easier to bind events using transitions.
38         media.transition = function( selector, sensitivity ) {
39                 var deferred = $.Deferred();
40
41                 sensitivity = sensitivity || 2000;
42
43                 if ( $.support.transition ) {
44                         if ( ! (selector instanceof $) )
45                                 selector = $( selector );
46
47                         // Resolve the deferred when the first element finishes animating.
48                         selector.first().one( $.support.transition.end, deferred.resolve );
49
50                         // Just in case the event doesn't trigger, fire a callback.
51                         _.delay( deferred.resolve, sensitivity );
52
53                 // Otherwise, execute on the spot.
54                 } else {
55                         deferred.resolve();
56                 }
57
58                 return deferred.promise();
59         };
60
61         /**
62          * ========================================================================
63          * CONTROLLERS
64          * ========================================================================
65          */
66
67         /**
68          * wp.media.controller.Region
69          */
70         media.controller.Region = function( options ) {
71                 _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
72         };
73
74         // Use Backbone's self-propagating `extend` inheritance method.
75         media.controller.Region.extend = Backbone.Model.extend;
76
77         _.extend( media.controller.Region.prototype, {
78                 mode: function( mode ) {
79                         if ( ! mode )
80                                 return this._mode;
81
82                         // Bail if we're trying to change to the current mode.
83                         if ( mode === this._mode )
84                                 return this;
85
86                         this.trigger('deactivate');
87                         this._mode = mode;
88                         this.render( mode );
89                         this.trigger('activate');
90                         return this;
91                 },
92
93                 render: function( mode ) {
94                         // If no mode is provided, just re-render the current mode.
95                         // If the provided mode isn't active, perform a full switch.
96                         if ( mode && mode !== this._mode )
97                                 return this.mode( mode );
98
99                         var set = { view: null },
100                                 view;
101
102                         this.trigger( 'create', set );
103                         view = set.view;
104                         this.trigger( 'render', view );
105                         if ( view )
106                                 this.set( view );
107                         return this;
108                 },
109
110                 get: function() {
111                         return this.view.views.first( this.selector );
112                 },
113
114                 set: function( views, options ) {
115                         if ( options )
116                                 options.add = false;
117                         return this.view.views.set( this.selector, views, options );
118                 },
119
120                 trigger: function( event ) {
121                         var base;
122                         if ( ! this._mode )
123                                 return;
124
125                         var args = _.toArray( arguments );
126                         base = this.id + ':' + event;
127
128                         // Trigger `region:action:mode` event.
129                         args[0] = base + ':' + this._mode;
130                         this.view.trigger.apply( this.view, args );
131
132                         // Trigger `region:action` event.
133                         args[0] = base;
134                         this.view.trigger.apply( this.view, args );
135                         return this;
136                 }
137         });
138
139         /**
140          * wp.media.controller.StateMachine
141          */
142         media.controller.StateMachine = function( states ) {
143                 this.states = new Backbone.Collection( states );
144         };
145
146         // Use Backbone's self-propagating `extend` inheritance method.
147         media.controller.StateMachine.extend = Backbone.Model.extend;
148
149         // Add events to the `StateMachine`.
150         _.extend( media.controller.StateMachine.prototype, Backbone.Events, {
151
152                 // Fetch a state.
153                 //
154                 // If no `id` is provided, returns the active state.
155                 //
156                 // Implicitly creates states.
157                 state: function( id ) {
158                         // Ensure that the `states` collection exists so the `StateMachine`
159                         // can be used as a mixin.
160                         this.states = this.states || new Backbone.Collection();
161
162                         // Default to the active state.
163                         id = id || this._state;
164
165                         if ( id && ! this.states.get( id ) )
166                                 this.states.add({ id: id });
167                         return this.states.get( id );
168                 },
169
170                 // Sets the active state.
171                 setState: function( id ) {
172                         var previous = this.state();
173
174                         // Bail if we're trying to select the current state, if we haven't
175                         // created the `states` collection, or are trying to select a state
176                         // that does not exist.
177                         if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) )
178                                 return this;
179
180                         if ( previous ) {
181                                 previous.trigger('deactivate');
182                                 this._lastState = previous.id;
183                         }
184
185                         this._state = id;
186                         this.state().trigger('activate');
187
188                         return this;
189                 },
190
191                 // Returns the previous active state.
192                 //
193                 // Call the `state()` method with no parameters to retrieve the current
194                 // active state.
195                 lastState: function() {
196                         if ( this._lastState )
197                                 return this.state( this._lastState );
198                 }
199         });
200
201         // Map methods from the `states` collection to the `StateMachine` itself.
202         _.each([ 'on', 'off', 'trigger' ], function( method ) {
203                 media.controller.StateMachine.prototype[ method ] = function() {
204                         // Ensure that the `states` collection exists so the `StateMachine`
205                         // can be used as a mixin.
206                         this.states = this.states || new Backbone.Collection();
207                         // Forward the method to the `states` collection.
208                         this.states[ method ].apply( this.states, arguments );
209                         return this;
210                 };
211         });
212
213
214         // wp.media.controller.State
215         // ---------------------------
216         media.controller.State = Backbone.Model.extend({
217                 constructor: function() {
218                         this.on( 'activate', this._preActivate, this );
219                         this.on( 'activate', this.activate, this );
220                         this.on( 'activate', this._postActivate, this );
221                         this.on( 'deactivate', this._deactivate, this );
222                         this.on( 'deactivate', this.deactivate, this );
223                         this.on( 'reset', this.reset, this );
224                         this.on( 'ready', this._ready, this );
225                         this.on( 'ready', this.ready, this );
226
227                         this.on( 'change:menu', this._updateMenu, this );
228
229                         Backbone.Model.apply( this, arguments );
230                 },
231
232                 ready: function() {},
233                 activate: function() {},
234                 deactivate: function() {},
235                 reset: function() {},
236
237                 _ready: function() {
238                         this._updateMenu();
239                 },
240
241                 _preActivate: function() {
242                         this.active = true;
243                 },
244
245                 _postActivate: function() {
246                         this.on( 'change:menu', this._menu, this );
247                         this.on( 'change:titleMode', this._title, this );
248                         this.on( 'change:content', this._content, this );
249                         this.on( 'change:toolbar', this._toolbar, this );
250
251                         this.frame.on( 'title:render:default', this._renderTitle, this );
252
253                         this._title();
254                         this._menu();
255                         this._toolbar();
256                         this._content();
257                         this._router();
258                 },
259
260
261                 _deactivate: function() {
262                         this.active = false;
263
264                         this.frame.off( 'title:render:default', this._renderTitle, this );
265
266                         this.off( 'change:menu', this._menu, this );
267                         this.off( 'change:titleMode', this._title, this );
268                         this.off( 'change:content', this._content, this );
269                         this.off( 'change:toolbar', this._toolbar, this );
270                 },
271
272                 _title: function() {
273                         this.frame.title.render( this.get('titleMode') || 'default' );
274                 },
275
276                 _renderTitle: function( view ) {
277                         view.$el.text( this.get('title') || '' );
278                 },
279
280                 _router: function() {
281                         var router = this.frame.router,
282                                 mode = this.get('router'),
283                                 view;
284
285                         this.frame.$el.toggleClass( 'hide-router', ! mode );
286                         if ( ! mode )
287                                 return;
288
289                         this.frame.router.render( mode );
290
291                         view = router.get();
292                         if ( view && view.select )
293                                 view.select( this.frame.content.mode() );
294                 },
295
296                 _menu: function() {
297                         var menu = this.frame.menu,
298                                 mode = this.get('menu'),
299                                 view;
300
301                         if ( ! mode )
302                                 return;
303
304                         menu.mode( mode );
305
306                         view = menu.get();
307                         if ( view && view.select )
308                                 view.select( this.id );
309                 },
310
311                 _updateMenu: function() {
312                         var previous = this.previous('menu'),
313                                 menu = this.get('menu');
314
315                         if ( previous )
316                                 this.frame.off( 'menu:render:' + previous, this._renderMenu, this );
317
318                         if ( menu )
319                                 this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
320                 },
321
322                 _renderMenu: function( view ) {
323                         var menuItem = this.get('menuItem'),
324                                 title = this.get('title'),
325                                 priority = this.get('priority');
326
327                         if ( ! menuItem && title ) {
328                                 menuItem = { text: title };
329
330                                 if ( priority )
331                                         menuItem.priority = priority;
332                         }
333
334                         if ( ! menuItem )
335                                 return;
336
337                         view.set( this.id, menuItem );
338                 }
339         });
340
341         _.each(['toolbar','content'], function( region ) {
342                 media.controller.State.prototype[ '_' + region ] = function() {
343                         var mode = this.get( region );
344                         if ( mode )
345                                 this.frame[ region ].render( mode );
346                 };
347         });
348
349         // wp.media.controller.Library
350         // ---------------------------
351         media.controller.Library = media.controller.State.extend({
352                 defaults: {
353                         id:         'library',
354                         multiple:   false, // false, 'add', 'reset'
355                         describe:   false,
356                         toolbar:    'select',
357                         sidebar:    'settings',
358                         content:    'upload',
359                         router:     'browse',
360                         menu:       'default',
361                         searchable: true,
362                         filterable: false,
363                         sortable:   true,
364                         title:      l10n.mediaLibraryTitle,
365
366                         // Uses a user setting to override the content mode.
367                         contentUserSetting: true,
368
369                         // Sync the selection from the last state when 'multiple' matches.
370                         syncSelection: true
371                 },
372
373                 initialize: function() {
374                         var selection = this.get('selection'),
375                                 props;
376
377                         // If a library isn't provided, query all media items.
378                         if ( ! this.get('library') )
379                                 this.set( 'library', media.query() );
380
381                         // If a selection instance isn't provided, create one.
382                         if ( ! (selection instanceof media.model.Selection) ) {
383                                 props = selection;
384
385                                 if ( ! props ) {
386                                         props = this.get('library').props.toJSON();
387                                         props = _.omit( props, 'orderby', 'query' );
388                                 }
389
390                                 // If the `selection` attribute is set to an object,
391                                 // it will use those values as the selection instance's
392                                 // `props` model. Otherwise, it will copy the library's
393                                 // `props` model.
394                                 this.set( 'selection', new media.model.Selection( null, {
395                                         multiple: this.get('multiple'),
396                                         props: props
397                                 }) );
398                         }
399
400                         if ( ! this.get('edge') )
401                                 this.set( 'edge', 120 );
402
403                         if ( ! this.get('gutter') )
404                                 this.set( 'gutter', 8 );
405
406                         this.resetDisplays();
407                 },
408
409                 activate: function() {
410                         this.syncSelection();
411
412                         wp.Uploader.queue.on( 'add', this.uploading, this );
413
414                         this.get('selection').on( 'add remove reset', this.refreshContent, this );
415
416                         this.on( 'insert', this._insertDisplaySettings, this );
417
418                         if ( this.get('contentUserSetting') ) {
419                                 this.frame.on( 'content:activate', this.saveContentMode, this );
420                                 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
421                         }
422                 },
423
424                 deactivate: function() {
425                         this.recordSelection();
426
427                         this.frame.off( 'content:activate', this.saveContentMode, this );
428
429                         // Unbind all event handlers that use this state as the context
430                         // from the selection.
431                         this.get('selection').off( null, null, this );
432
433                         wp.Uploader.queue.off( null, null, this );
434                 },
435
436                 reset: function() {
437                         this.get('selection').reset();
438                         this.resetDisplays();
439                         this.refreshContent();
440                 },
441
442                 resetDisplays: function() {
443                         this._displays = [];
444                         this._defaultDisplaySettings = {
445                                 align: getUserSetting( 'align', 'none' ),
446                                 size:  getUserSetting( 'imgsize', 'medium' ),
447                                 link:  getUserSetting( 'urlbutton', 'post' )
448                         };
449                 },
450
451                 display: function( attachment ) {
452                         var displays = this._displays;
453
454                         if ( ! displays[ attachment.cid ] )
455                                 displays[ attachment.cid ] = new Backbone.Model( this._defaultDisplaySettings );
456
457                         return displays[ attachment.cid ];
458                 },
459
460                 _insertDisplaySettings: function() {
461                         var selection = this.get('selection'),
462                                 display;
463
464                         // If inserting one image, set those display properties as the
465                         // default user setting.
466                         if ( selection.length !== 1 )
467                                 return;
468
469                         display = this.display( selection.first() ).toJSON();
470
471                         setUserSetting( 'align', display.align );
472                         setUserSetting( 'imgsize', display.size );
473                         setUserSetting( 'urlbutton', display.link );
474                 },
475
476                 syncSelection: function() {
477                         var selection = this.get('selection'),
478                                 manager = this.frame._selection;
479
480                         if ( ! this.get('syncSelection') || ! manager || ! selection )
481                                 return;
482
483                         // If the selection supports multiple items, validate the stored
484                         // attachments based on the new selection's conditions. Record
485                         // the attachments that are not included; we'll maintain a
486                         // reference to those. Other attachments are considered in flux.
487                         if ( selection.multiple ) {
488                                 selection.reset( [], { silent: true });
489                                 selection.validateAll( manager.attachments );
490                                 manager.difference = _.difference( manager.attachments.models, selection.models );
491                         }
492
493                         // Sync the selection's single item with the master.
494                         selection.single( manager.single );
495                 },
496
497                 recordSelection: function() {
498                         var selection = this.get('selection'),
499                                 manager = this.frame._selection,
500                                 filtered;
501
502                         if ( ! this.get('syncSelection') || ! manager || ! selection )
503                                 return;
504
505                         // Record the currently active attachments, which is a combination
506                         // of the selection's attachments and the set of selected
507                         // attachments that this specific selection considered invalid.
508                         // Reset the difference and record the single attachment.
509                         if ( selection.multiple ) {
510                                 manager.attachments.reset( selection.toArray().concat( manager.difference ) );
511                                 manager.difference = [];
512                         } else {
513                                 manager.attachments.add( selection.toArray() );
514                         }
515
516                         manager.single = selection._single;
517                 },
518
519                 refreshContent: function() {
520                         var selection = this.get('selection'),
521                                 frame = this.frame,
522                                 router = frame.router.get(),
523                                 mode = frame.content.mode();
524
525                         if ( this.active && ! selection.length && ! router.get( mode ) )
526                                 this.frame.content.render( this.get('content') );
527                 },
528
529                 uploading: function( attachment ) {
530                         var content = this.frame.content;
531
532                         // If the uploader was selected, navigate to the browser.
533                         if ( 'upload' === content.mode() )
534                                 this.frame.content.mode('browse');
535
536                         // If we're in a workflow that supports multiple attachments,
537                         // automatically select any uploading attachments.
538                         if ( this.get('multiple') )
539                                 this.get('selection').add( attachment );
540                 },
541
542                 saveContentMode: function() {
543                         // Only track the browse router on library states.
544                         if ( 'browse' !== this.get('router') )
545                                 return;
546
547                         var mode = this.frame.content.mode(),
548                                 view = this.frame.router.get();
549
550                         if ( view && view.get( mode ) )
551                                 setUserSetting( 'libraryContent', mode );
552                 }
553         });
554
555         // wp.media.controller.GalleryEdit
556         // -------------------------------
557         media.controller.GalleryEdit = media.controller.Library.extend({
558                 defaults: {
559                         id:         'gallery-edit',
560                         multiple:   false,
561                         describe:   true,
562                         edge:       199,
563                         editing:    false,
564                         sortable:   true,
565                         searchable: false,
566                         toolbar:    'gallery-edit',
567                         content:    'browse',
568                         title:      l10n.editGalleryTitle,
569                         priority:   60,
570                         dragInfo:   true,
571
572                         // Don't sync the selection, as the Edit Gallery library
573                         // *is* the selection.
574                         syncSelection: false
575                 },
576
577                 initialize: function() {
578                         // If we haven't been provided a `library`, create a `Selection`.
579                         if ( ! this.get('library') )
580                                 this.set( 'library', new media.model.Selection() );
581
582                         // The single `Attachment` view to be used in the `Attachments` view.
583                         if ( ! this.get('AttachmentView') )
584                                 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
585                         media.controller.Library.prototype.initialize.apply( this, arguments );
586                 },
587
588                 activate: function() {
589                         var library = this.get('library');
590
591                         // Limit the library to images only.
592                         library.props.set( 'type', 'image' );
593
594                         // Watch for uploaded attachments.
595                         this.get('library').observe( wp.Uploader.queue );
596
597                         this.frame.on( 'content:render:browse', this.gallerySettings, this );
598
599                         media.controller.Library.prototype.activate.apply( this, arguments );
600                 },
601
602                 deactivate: function() {
603                         // Stop watching for uploaded attachments.
604                         this.get('library').unobserve( wp.Uploader.queue );
605
606                         this.frame.off( 'content:render:browse', this.gallerySettings, this );
607
608                         media.controller.Library.prototype.deactivate.apply( this, arguments );
609                 },
610
611                 gallerySettings: function( browser ) {
612                         var library = this.get('library');
613
614                         if ( ! library || ! browser )
615                                 return;
616
617                         library.gallery = library.gallery || new Backbone.Model();
618
619                         browser.sidebar.set({
620                                 gallery: new media.view.Settings.Gallery({
621                                         controller: this,
622                                         model:      library.gallery,
623                                         priority:   40
624                                 })
625                         });
626
627                         browser.toolbar.set( 'reverse', {
628                                 text:     l10n.reverseOrder,
629                                 priority: 80,
630
631                                 click: function() {
632                                         library.reset( library.toArray().reverse() );
633                                 }
634                         });
635                 }
636         });
637
638         // wp.media.controller.GalleryAdd
639         // ---------------------------------
640         media.controller.GalleryAdd = media.controller.Library.extend({
641                 defaults: _.defaults({
642                         id:           'gallery-library',
643                         filterable:   'uploaded',
644                         multiple:     'add',
645                         menu:         'gallery',
646                         toolbar:      'gallery-add',
647                         title:        l10n.addToGalleryTitle,
648                         priority:     100,
649
650                         // Don't sync the selection, as the Edit Gallery library
651                         // *is* the selection.
652                         syncSelection: false
653                 }, media.controller.Library.prototype.defaults ),
654
655                 initialize: function() {
656                         // If we haven't been provided a `library`, create a `Selection`.
657                         if ( ! this.get('library') )
658                                 this.set( 'library', media.query({ type: 'image' }) );
659
660                         media.controller.Library.prototype.initialize.apply( this, arguments );
661                 },
662
663                 activate: function() {
664                         var library = this.get('library'),
665                                 edit    = this.frame.state('gallery-edit').get('library');
666
667                         if ( this.editLibrary && this.editLibrary !== edit )
668                                 library.unobserve( this.editLibrary );
669
670                         // Accepts attachments that exist in the original library and
671                         // that do not exist in gallery's library.
672                         library.validator = function( attachment ) {
673                                 return !! this.mirroring.getByCid( attachment.cid ) && ! edit.getByCid( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
674                         };
675
676                         library.observe( edit );
677                         this.editLibrary = edit;
678
679                         media.controller.Library.prototype.activate.apply( this, arguments );
680                 }
681         });
682
683         // wp.media.controller.FeaturedImage
684         // ---------------------------------
685         media.controller.FeaturedImage = media.controller.Library.extend({
686                 defaults: _.defaults({
687                         id:         'featured-image',
688                         filterable: 'uploaded',
689                         multiple:   false,
690                         toolbar:    'featured-image',
691                         title:      l10n.setFeaturedImageTitle,
692                         priority:   60,
693
694                         syncSelection: false
695                 }, media.controller.Library.prototype.defaults ),
696
697                 initialize: function() {
698                         var library, comparator;
699
700                         // If we haven't been provided a `library`, create a `Selection`.
701                         if ( ! this.get('library') )
702                                 this.set( 'library', media.query({ type: 'image' }) );
703
704                         media.controller.Library.prototype.initialize.apply( this, arguments );
705
706                         library    = this.get('library');
707                         comparator = library.comparator;
708
709                         // Overload the library's comparator to push items that are not in
710                         // the mirrored query to the front of the aggregate collection.
711                         library.comparator = function( a, b ) {
712                                 var aInQuery = !! this.mirroring.getByCid( a.cid ),
713                                         bInQuery = !! this.mirroring.getByCid( b.cid );
714
715                                 if ( ! aInQuery && bInQuery )
716                                         return -1;
717                                 else if ( aInQuery && ! bInQuery )
718                                         return 1;
719                                 else
720                                         return comparator.apply( this, arguments );
721                         };
722
723                         // Add all items in the selection to the library, so any featured
724                         // images that are not initially loaded still appear.
725                         library.observe( this.get('selection') );
726                 },
727
728                 activate: function() {
729                         this.updateSelection();
730                         this.frame.on( 'open', this.updateSelection, this );
731                         media.controller.Library.prototype.activate.apply( this, arguments );
732                 },
733
734                 deactivate: function() {
735                         this.frame.off( 'open', this.updateSelection, this );
736                         media.controller.Library.prototype.deactivate.apply( this, arguments );
737                 },
738
739                 updateSelection: function() {
740                         var selection = this.get('selection'),
741                                 id = media.view.settings.post.featuredImageId,
742                                 attachment;
743
744                         if ( '' !== id && -1 !== id ) {
745                                 attachment = Attachment.get( id );
746                                 attachment.fetch();
747                         }
748
749                         selection.reset( attachment ? [ attachment ] : [] );
750                 }
751         });
752
753
754         // wp.media.controller.Embed
755         // -------------------------
756         media.controller.Embed = media.controller.State.extend({
757                 defaults: {
758                         id:      'embed',
759                         url:     '',
760                         menu:    'default',
761                         content: 'embed',
762                         toolbar: 'main-embed',
763                         type:    'link',
764
765                         title:    l10n.insertFromUrlTitle,
766                         priority: 120
767                 },
768
769                 // The amount of time used when debouncing the scan.
770                 sensitivity: 200,
771
772                 initialize: function() {
773                         this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
774                         this.props = new Backbone.Model({ url: '' });
775                         this.props.on( 'change:url', this.debouncedScan, this );
776                         this.props.on( 'change:url', this.refresh, this );
777                         this.on( 'scan', this.scanImage, this );
778                 },
779
780                 scan: function() {
781                         var scanners,
782                                 embed = this,
783                                 attributes = {
784                                         type: 'link',
785                                         scanners: []
786                                 };
787
788                         // Scan is triggered with the list of `attributes` to set on the
789                         // state, useful for the 'type' attribute and 'scanners' attribute,
790                         // an array of promise objects for asynchronous scan operations.
791                         if ( this.props.get('url') )
792                                 this.trigger( 'scan', attributes );
793
794                         if ( attributes.scanners.length ) {
795                                 scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
796                                 scanners.always( function() {
797                                         if ( embed.get('scanners') === scanners )
798                                                 embed.set( 'loading', false );
799                                 });
800                         } else {
801                                 attributes.scanners = null;
802                         }
803
804                         attributes.loading = !! attributes.scanners;
805                         this.set( attributes );
806                 },
807
808                 scanImage: function( attributes ) {
809                         var frame = this.frame,
810                                 state = this,
811                                 url = this.props.get('url'),
812                                 image = new Image(),
813                                 deferred = $.Deferred();
814
815                         attributes.scanners.push( deferred.promise() );
816
817                         // Try to load the image and find its width/height.
818                         image.onload = function() {
819                                 deferred.resolve();
820
821                                 if ( state !== frame.state() || url !== state.props.get('url') )
822                                         return;
823
824                                 state.set({
825                                         type: 'image'
826                                 });
827
828                                 state.props.set({
829                                         width:  image.width,
830                                         height: image.height
831                                 });
832                         };
833
834                         image.onerror = deferred.reject;
835                         image.src = url;
836                 },
837
838                 refresh: function() {
839                         this.frame.toolbar.get().refresh();
840                 },
841
842                 reset: function() {
843                         this.props.clear().set({ url: '' });
844
845                         if ( this.active )
846                                 this.refresh();
847                 }
848         });
849
850         /**
851          * ========================================================================
852          * VIEWS
853          * ========================================================================
854          */
855
856         // wp.media.Views
857         // -------------
858         //
859         // A subview manager.
860
861         media.Views = function( view, views ) {
862                 this.view = view;
863                 this._views = _.isArray( views ) ? { '': views } : views || {};
864         };
865
866         media.Views.extend = Backbone.Model.extend;
867
868         _.extend( media.Views.prototype, {
869                 // ### Fetch all of the subviews
870                 //
871                 // Returns an array of all subviews.
872                 all: function() {
873                         return _.flatten( this._views );
874                 },
875
876                 // ### Get a selector's subviews
877                 //
878                 // Fetches all subviews that match a given `selector`.
879                 //
880                 // If no `selector` is provided, it will grab all subviews attached
881                 // to the view's root.
882                 get: function( selector ) {
883                         selector = selector || '';
884                         return this._views[ selector ];
885                 },
886
887                 // ### Get a selector's first subview
888                 //
889                 // Fetches the first subview that matches a given `selector`.
890                 //
891                 // If no `selector` is provided, it will grab the first subview
892                 // attached to the view's root.
893                 //
894                 // Useful when a selector only has one subview at a time.
895                 first: function( selector ) {
896                         var views = this.get( selector );
897                         return views && views.length ? views[0] : null;
898                 },
899
900                 // ### Register subview(s)
901                 //
902                 // Registers any number of `views` to a `selector`.
903                 //
904                 // When no `selector` is provided, the root selector (the empty string)
905                 // is used. `views` accepts a `Backbone.View` instance or an array of
906                 // `Backbone.View` instances.
907                 //
908                 // ---
909                 //
910                 // Accepts an `options` object, which has a significant effect on the
911                 // resulting behavior.
912                 //
913                 // `options.silent` – *boolean, `false`*
914                 // > If `options.silent` is true, no DOM modifications will be made.
915                 //
916                 // `options.add` – *boolean, `false`*
917                 // > Use `Views.add()` as a shortcut for setting `options.add` to true.
918                 //
919                 // > By default, the provided `views` will replace
920                 // any existing views associated with the selector. If `options.add`
921                 // is true, the provided `views` will be added to the existing views.
922                 //
923                 // `options.at` – *integer, `undefined`*
924                 // > When adding, to insert `views` at a specific index, use
925                 // `options.at`. By default, `views` are added to the end of the array.
926                 set: function( selector, views, options ) {
927                         var existing, next;
928
929                         if ( ! _.isString( selector ) ) {
930                                 options  = views;
931                                 views    = selector;
932                                 selector = '';
933                         }
934
935                         options  = options || {};
936                         views    = _.isArray( views ) ? views : [ views ];
937                         existing = this.get( selector );
938                         next     = views;
939
940                         if ( existing ) {
941                                 if ( options.add ) {
942                                         if ( _.isUndefined( options.at ) ) {
943                                                 next = existing.concat( views );
944                                         } else {
945                                                 next = existing;
946                                                 next.splice.apply( next, [ options.at, 0 ].concat( views ) );
947                                         }
948                                 } else {
949                                         _.each( next, function( view ) {
950                                                 view.__detach = true;
951                                         });
952
953                                         _.each( existing, function( view ) {
954                                                 if ( view.__detach )
955                                                         view.$el.detach();
956                                                 else
957                                                         view.dispose();
958                                         });
959
960                                         _.each( next, function( view ) {
961                                                 delete view.__detach;
962                                         });
963                                 }
964                         }
965
966                         this._views[ selector ] = next;
967
968                         _.each( views, function( subview ) {
969                                 var constructor = subview.Views || media.Views,
970                                         subviews = subview.views = subview.views || new constructor( subview );
971                                 subviews.parent   = this.view;
972                                 subviews.selector = selector;
973                         }, this );
974
975                         if ( ! options.silent )
976                                 this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) );
977
978                         return this;
979                 },
980
981                 // ### Add subview(s) to existing subviews
982                 //
983                 // An alias to `Views.set()`, which defaults `options.add` to true.
984                 //
985                 // Adds any number of `views` to a `selector`.
986                 //
987                 // When no `selector` is provided, the root selector (the empty string)
988                 // is used. `views` accepts a `Backbone.View` instance or an array of
989                 // `Backbone.View` instances.
990                 //
991                 // Use `Views.set()` when setting `options.add` to `false`.
992                 //
993                 // Accepts an `options` object. By default, provided `views` will be
994                 // inserted at the end of the array of existing views. To insert
995                 // `views` at a specific index, use `options.at`. If `options.silent`
996                 // is true, no DOM modifications will be made.
997                 //
998                 // For more information on the `options` object, see `Views.set()`.
999                 add: function( selector, views, options ) {
1000                         if ( ! _.isString( selector ) ) {
1001                                 options  = views;
1002                                 views    = selector;
1003                                 selector = '';
1004                         }
1005
1006                         return this.set( selector, views, _.extend({ add: true }, options ) );
1007                 },
1008
1009                 // ### Stop tracking subviews
1010                 //
1011                 // Stops tracking `views` registered to a `selector`. If no `views` are
1012                 // set, then all of the `selector`'s subviews will be unregistered and
1013                 // disposed.
1014                 //
1015                 // Accepts an `options` object. If `options.silent` is set, `dispose`
1016                 // will *not* be triggered on the unregistered views.
1017                 unset: function( selector, views, options ) {
1018                         var existing;
1019
1020                         if ( ! _.isString( selector ) ) {
1021                                 options = views;
1022                                 views = selector;
1023                                 selector = '';
1024                         }
1025
1026                         views = views || [];
1027
1028                         if ( existing = this.get( selector ) ) {
1029                                 views = _.isArray( views ) ? views : [ views ];
1030                                 this._views[ selector ] = views.length ? _.difference( existing, views ) : [];
1031                         }
1032
1033                         if ( ! options || ! options.silent )
1034                                 _.invoke( views, 'dispose' );
1035
1036                         return this;
1037                 },
1038
1039                 // ### Detach all subviews
1040                 //
1041                 // Detaches all subviews from the DOM.
1042                 //
1043                 // Helps to preserve all subview events when re-rendering the master
1044                 // view. Used in conjunction with `Views.render()`.
1045                 detach: function() {
1046                         $( _.pluck( this.all(), 'el' ) ).detach();
1047                         return this;
1048                 },
1049
1050                 // ### Render all subviews
1051                 //
1052                 // Renders all subviews. Used in conjunction with `Views.detach()`.
1053                 render: function() {
1054                         var options = {
1055                                         ready: this._isReady()
1056                                 };
1057
1058                         _.each( this._views, function( views, selector ) {
1059                                 this._attach( selector, views, options );
1060                         }, this );
1061
1062                         this.rendered = true;
1063                         return this;
1064                 },
1065
1066                 // ### Dispose all subviews
1067                 //
1068                 // Triggers the `dispose()` method on all subviews. Detaches the master
1069                 // view from its parent. Resets the internals of the views manager.
1070                 //
1071                 // Accepts an `options` object. If `options.silent` is set, `unset`
1072                 // will *not* be triggered on the master view's parent.
1073                 dispose: function( options ) {
1074                         if ( ! options || ! options.silent ) {
1075                                 if ( this.parent && this.parent.views )
1076                                         this.parent.views.unset( this.selector, this.view, { silent: true });
1077                                 delete this.parent;
1078                                 delete this.selector;
1079                         }
1080
1081                         _.invoke( this.all(), 'dispose' );
1082                         this._views = [];
1083                         return this;
1084                 },
1085
1086                 // ### Replace a selector's subviews
1087                 //
1088                 // By default, sets the `$target` selector's html to the subview `els`.
1089                 //
1090                 // Can be overridden in subclasses.
1091                 replace: function( $target, els ) {
1092                         $target.html( els );
1093                         return this;
1094                 },
1095
1096                 // ### Insert subviews into a selector
1097                 //
1098                 // By default, appends the subview `els` to the end of the `$target`
1099                 // selector. If `options.at` is set, inserts the subview `els` at the
1100                 // provided index.
1101                 //
1102                 // Can be overridden in subclasses.
1103                 insert: function( $target, els, options ) {
1104                         var at = options && options.at,
1105                                 $children;
1106
1107                         if ( _.isNumber( at ) && ($children = $target.children()).length > at )
1108                                 $children.eq( at ).before( els );
1109                         else
1110                                 $target.append( els );
1111
1112                         return this;
1113                 },
1114
1115                 // ### Trigger the ready event
1116                 //
1117                 // **Only use this method if you know what you're doing.**
1118                 // For performance reasons, this method does not check if the view is
1119                 // actually attached to the DOM. It's taking your word for it.
1120                 //
1121                 // Fires the ready event on the current view and all attached subviews.
1122                 ready: function() {
1123                         this.view.trigger('ready');
1124
1125                         // Find all attached subviews, and call ready on them.
1126                         _.chain( this.all() ).map( function( view ) {
1127                                 return view.views;
1128                         }).flatten().where({ attached: true }).invoke('ready');
1129                 },
1130
1131                 // #### Internal. Attaches a series of views to a selector.
1132                 //
1133                 // Checks to see if a matching selector exists, renders the views,
1134                 // performs the proper DOM operation, and then checks if the view is
1135                 // attached to the document.
1136                 _attach: function( selector, views, options ) {
1137                         var $selector = selector ? this.view.$( selector ) : this.view.$el,
1138                                 managers;
1139
1140                         // Check if we found a location to attach the views.
1141                         if ( ! $selector.length )
1142                                 return this;
1143
1144                         managers = _.chain( views ).pluck('views').flatten().value();
1145
1146                         // Render the views if necessary.
1147                         _.each( managers, function( manager ) {
1148                                 if ( manager.rendered )
1149                                         return;
1150
1151                                 manager.view.render();
1152                                 manager.rendered = true;
1153                         }, this );
1154
1155                         // Insert or replace the views.
1156                         this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options );
1157
1158                         // Set attached and trigger ready if the current view is already
1159                         // attached to the DOM.
1160                         _.each( managers, function( manager ) {
1161                                 manager.attached = true;
1162
1163                                 if ( options.ready )
1164                                         manager.ready();
1165                         }, this );
1166
1167                         return this;
1168                 },
1169
1170                 // #### Internal. Checks if the current view is in the DOM.
1171                 _isReady: function() {
1172                         var node = this.view.el;
1173                         while ( node ) {
1174                                 if ( node === document.body )
1175                                         return true;
1176                                 node = node.parentNode;
1177                         }
1178
1179                         return false;
1180                 }
1181         });
1182
1183         // wp.media.View
1184         // -------------
1185         //
1186         // The base view class.
1187         media.View = Backbone.View.extend({
1188                 // The constructor for the `Views` manager.
1189                 Views: media.Views,
1190
1191                 constructor: function( options ) {
1192                         this.views = new this.Views( this, this.views );
1193                         this.on( 'ready', this.ready, this );
1194
1195                         if ( options && options.controller )
1196                                 this.controller = options.controller;
1197
1198                         Backbone.View.apply( this, arguments );
1199                 },
1200
1201                 dispose: function() {
1202                         // Undelegating events, removing events from the model, and
1203                         // removing events from the controller mirror the code for
1204                         // `Backbone.View.dispose` in Backbone master.
1205                         this.undelegateEvents();
1206
1207                         if ( this.model && this.model.off )
1208                                 this.model.off( null, null, this );
1209
1210                         if ( this.collection && this.collection.off )
1211                                 this.collection.off( null, null, this );
1212
1213                         // Unbind controller events.
1214                         if ( this.controller && this.controller.off )
1215                                 this.controller.off( null, null, this );
1216
1217                         // Recursively dispose child views.
1218                         if ( this.views )
1219                                 this.views.dispose();
1220
1221                         return this;
1222                 },
1223
1224                 remove: function() {
1225                         this.dispose();
1226                         return Backbone.View.prototype.remove.apply( this, arguments );
1227                 },
1228
1229                 render: function() {
1230                         var options;
1231
1232                         if ( this.prepare )
1233                                 options = this.prepare();
1234
1235                         this.views.detach();
1236
1237                         if ( this.template ) {
1238                                 options = options || {};
1239                                 this.trigger( 'prepare', options );
1240                                 this.$el.html( this.template( options ) );
1241                         }
1242
1243                         this.views.render();
1244                         return this;
1245                 },
1246
1247                 prepare: function() {
1248                         return this.options;
1249                 },
1250
1251                 ready: function() {}
1252         });
1253
1254         /**
1255          * wp.media.view.Frame
1256          */
1257         media.view.Frame = media.View.extend({
1258                 initialize: function() {
1259                         this._createRegions();
1260                         this._createStates();
1261                 },
1262
1263                 _createRegions: function() {
1264                         // Clone the regions array.
1265                         this.regions = this.regions ? this.regions.slice() : [];
1266
1267                         // Initialize regions.
1268                         _.each( this.regions, function( region ) {
1269                                 this[ region ] = new media.controller.Region({
1270                                         view:     this,
1271                                         id:       region,
1272                                         selector: '.media-frame-' + region
1273                                 });
1274                         }, this );
1275                 },
1276
1277                 _createStates: function() {
1278                         // Create the default `states` collection.
1279                         this.states = new Backbone.Collection( null, {
1280                                 model: media.controller.State
1281                         });
1282
1283                         // Ensure states have a reference to the frame.
1284                         this.states.on( 'add', function( model ) {
1285                                 model.frame = this;
1286                                 model.trigger('ready');
1287                         }, this );
1288
1289                         if ( this.options.states )
1290                                 this.states.add( this.options.states );
1291                 },
1292
1293                 reset: function() {
1294                         this.states.invoke( 'trigger', 'reset' );
1295                         return this;
1296                 }
1297         });
1298
1299         // Make the `Frame` a `StateMachine`.
1300         _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
1301
1302         /**
1303          * wp.media.view.MediaFrame
1304          */
1305         media.view.MediaFrame = media.view.Frame.extend({
1306                 className: 'media-frame',
1307                 template:  media.template('media-frame'),
1308                 regions:   ['menu','title','content','toolbar','router'],
1309
1310                 initialize: function() {
1311                         media.view.Frame.prototype.initialize.apply( this, arguments );
1312
1313                         _.defaults( this.options, {
1314                                 title:    '',
1315                                 modal:    true,
1316                                 uploader: true
1317                         });
1318
1319                         // Ensure core UI is enabled.
1320                         this.$el.addClass('wp-core-ui');
1321
1322                         // Initialize modal container view.
1323                         if ( this.options.modal ) {
1324                                 this.modal = new media.view.Modal({
1325                                         controller: this,
1326                                         title:      this.options.title
1327                                 });
1328
1329                                 this.modal.content( this );
1330                         }
1331
1332                         // Force the uploader off if the upload limit has been exceeded or
1333                         // if the browser isn't supported.
1334                         if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported )
1335                                 this.options.uploader = false;
1336
1337                         // Initialize window-wide uploader.
1338                         if ( this.options.uploader ) {
1339                                 this.uploader = new media.view.UploaderWindow({
1340                                         controller: this,
1341                                         uploader: {
1342                                                 dropzone:  this.modal ? this.modal.$el : this.$el,
1343                                                 container: this.$el
1344                                         }
1345                                 });
1346                                 this.views.set( '.media-frame-uploader', this.uploader );
1347                         }
1348
1349                         this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1350
1351                         // Bind default title creation.
1352                         this.on( 'title:create:default', this.createTitle, this );
1353                         this.title.mode('default');
1354
1355                         // Bind default menu.
1356                         this.on( 'menu:create:default', this.createMenu, this );
1357                 },
1358
1359                 render: function() {
1360                         // Activate the default state if no active state exists.
1361                         if ( ! this.state() && this.options.state )
1362                                 this.setState( this.options.state );
1363
1364                         return media.view.Frame.prototype.render.apply( this, arguments );
1365                 },
1366
1367                 createTitle: function( title ) {
1368                         title.view = new media.View({
1369                                 controller: this,
1370                                 tagName: 'h1'
1371                         });
1372                 },
1373
1374                 createMenu: function( menu ) {
1375                         menu.view = new media.view.Menu({
1376                                 controller: this
1377                         });
1378                 },
1379
1380                 createToolbar: function( toolbar ) {
1381                         toolbar.view = new media.view.Toolbar({
1382                                 controller: this
1383                         });
1384                 },
1385
1386                 createRouter: function( router ) {
1387                         router.view = new media.view.Router({
1388                                 controller: this
1389                         });
1390                 },
1391
1392                 createIframeStates: function( options ) {
1393                         var settings = media.view.settings,
1394                                 tabs = settings.tabs,
1395                                 tabUrl = settings.tabUrl,
1396                                 $postId;
1397
1398                         if ( ! tabs || ! tabUrl )
1399                                 return;
1400
1401                         // Add the post ID to the tab URL if it exists.
1402                         $postId = $('#post_ID');
1403                         if ( $postId.length )
1404                                 tabUrl += '&post_id=' + $postId.val();
1405
1406                         // Generate the tab states.
1407                         _.each( tabs, function( title, id ) {
1408                                 var frame = this.state( 'iframe:' + id ).set( _.defaults({
1409                                         tab:     id,
1410                                         src:     tabUrl + '&tab=' + id,
1411                                         title:   title,
1412                                         content: 'iframe',
1413                                         menu:    'default'
1414                                 }, options ) );
1415                         }, this );
1416
1417                         this.on( 'content:create:iframe', this.iframeContent, this );
1418                         this.on( 'menu:render:default', this.iframeMenu, this );
1419                         this.on( 'open', this.hijackThickbox, this );
1420                         this.on( 'close', this.restoreThickbox, this );
1421                 },
1422
1423                 iframeContent: function( content ) {
1424                         this.$el.addClass('hide-toolbar');
1425                         content.view = new media.view.Iframe({
1426                                 controller: this
1427                         });
1428                 },
1429
1430                 iframeMenu: function( view ) {
1431                         var views = {};
1432
1433                         if ( ! view )
1434                                 return;
1435
1436                         _.each( media.view.settings.tabs, function( title, id ) {
1437                                 views[ 'iframe:' + id ] = {
1438                                         text: this.state( 'iframe:' + id ).get('title'),
1439                                         priority: 200
1440                                 };
1441                         }, this );
1442
1443                         view.set( views );
1444                 },
1445
1446                 hijackThickbox: function() {
1447                         var frame = this;
1448
1449                         if ( ! window.tb_remove || this._tb_remove )
1450                                 return;
1451
1452                         this._tb_remove = window.tb_remove;
1453                         window.tb_remove = function() {
1454                                 frame.close();
1455                                 frame.reset();
1456                                 frame.setState( frame.options.state );
1457                                 frame._tb_remove.call( window );
1458                         };
1459                 },
1460
1461                 restoreThickbox: function() {
1462                         if ( ! this._tb_remove )
1463                                 return;
1464
1465                         window.tb_remove = this._tb_remove;
1466                         delete this._tb_remove;
1467                 }
1468         });
1469
1470         // Map some of the modal's methods to the frame.
1471         _.each(['open','close','attach','detach','escape'], function( method ) {
1472                 media.view.MediaFrame.prototype[ method ] = function( view ) {
1473                         if ( this.modal )
1474                                 this.modal[ method ].apply( this.modal, arguments );
1475                         return this;
1476                 };
1477         });
1478
1479         /**
1480          * wp.media.view.MediaFrame.Select
1481          */
1482         media.view.MediaFrame.Select = media.view.MediaFrame.extend({
1483                 initialize: function() {
1484                         media.view.MediaFrame.prototype.initialize.apply( this, arguments );
1485
1486                         _.defaults( this.options, {
1487                                 selection: [],
1488                                 library:   {},
1489                                 multiple:  false,
1490                                 state:    'library'
1491                         });
1492
1493                         this.createSelection();
1494                         this.createStates();
1495                         this.bindHandlers();
1496                 },
1497
1498                 createSelection: function() {
1499                         var controller = this,
1500                                 selection = this.options.selection;
1501
1502                         if ( ! (selection instanceof media.model.Selection) ) {
1503                                 this.options.selection = new media.model.Selection( selection, {
1504                                         multiple: this.options.multiple
1505                                 });
1506                         }
1507
1508                         this._selection = {
1509                                 attachments: new Attachments(),
1510                                 difference: []
1511                         };
1512                 },
1513
1514                 createStates: function() {
1515                         var options = this.options;
1516
1517                         if ( this.options.states )
1518                                 return;
1519
1520                         // Add the default states.
1521                         this.states.add([
1522                                 // Main states.
1523                                 new media.controller.Library({
1524                                         library:   media.query( options.library ),
1525                                         multiple:  options.multiple,
1526                                         title:     options.title,
1527                                         priority:  20
1528                                 })
1529                         ]);
1530                 },
1531
1532                 bindHandlers: function() {
1533                         this.on( 'router:create:browse', this.createRouter, this );
1534                         this.on( 'router:render:browse', this.browseRouter, this );
1535                         this.on( 'content:create:browse', this.browseContent, this );
1536                         this.on( 'content:render:upload', this.uploadContent, this );
1537                         this.on( 'toolbar:create:select', this.createSelectToolbar, this );
1538                 },
1539
1540                 // Routers
1541                 browseRouter: function( view ) {
1542                         view.set({
1543                                 upload: {
1544                                         text:     l10n.uploadFilesTitle,
1545                                         priority: 20
1546                                 },
1547                                 browse: {
1548                                         text:     l10n.mediaLibraryTitle,
1549                                         priority: 40
1550                                 }
1551                         });
1552                 },
1553
1554                 // Content
1555                 browseContent: function( content ) {
1556                         var state = this.state();
1557
1558                         this.$el.removeClass('hide-toolbar');
1559
1560                         // Browse our library of attachments.
1561                         content.view = new media.view.AttachmentsBrowser({
1562                                 controller: this,
1563                                 collection: state.get('library'),
1564                                 selection:  state.get('selection'),
1565                                 model:      state,
1566                                 sortable:   state.get('sortable'),
1567                                 search:     state.get('searchable'),
1568                                 filters:    state.get('filterable'),
1569                                 display:    state.get('displaySettings'),
1570                                 dragInfo:   state.get('dragInfo'),
1571
1572                                 AttachmentView: state.get('AttachmentView')
1573                         });
1574                 },
1575
1576                 uploadContent: function() {
1577                         this.$el.removeClass('hide-toolbar');
1578                         this.content.set( new media.view.UploaderInline({
1579                                 controller: this
1580                         }) );
1581                 },
1582
1583                 // Toolbars
1584                 createSelectToolbar: function( toolbar, options ) {
1585                         options = options || this.options.button || {};
1586                         options.controller = this;
1587
1588                         toolbar.view = new media.view.Toolbar.Select( options );
1589                 }
1590         });
1591
1592         /**
1593          * wp.media.view.MediaFrame.Post
1594          */
1595         media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
1596                 initialize: function() {
1597                         _.defaults( this.options, {
1598                                 multiple:  true,
1599                                 editing:   false,
1600                                 state:    'insert'
1601                         });
1602
1603                         media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
1604                         this.createIframeStates();
1605                 },
1606
1607                 createStates: function() {
1608                         var options = this.options;
1609
1610                         // Add the default states.
1611                         this.states.add([
1612                                 // Main states.
1613                                 new media.controller.Library({
1614                                         id:         'insert',
1615                                         title:      l10n.insertMediaTitle,
1616                                         priority:   20,
1617                                         toolbar:    'main-insert',
1618                                         filterable: 'all',
1619                                         library:    media.query( options.library ),
1620                                         multiple:   options.multiple ? 'reset' : false,
1621                                         editable:   true,
1622
1623                                         // If the user isn't allowed to edit fields,
1624                                         // can they still edit it locally?
1625                                         allowLocalEdits: true,
1626
1627                                         // Show the attachment display settings.
1628                                         displaySettings: true,
1629                                         // Update user settings when users adjust the
1630                                         // attachment display settings.
1631                                         displayUserSettings: true
1632                                 }),
1633
1634                                 new media.controller.Library({
1635                                         id:         'gallery',
1636                                         title:      l10n.createGalleryTitle,
1637                                         priority:   40,
1638                                         toolbar:    'main-gallery',
1639                                         filterable: 'uploaded',
1640                                         multiple:   'add',
1641                                         editable:   false,
1642
1643                                         library:  media.query( _.defaults({
1644                                                 type: 'image'
1645                                         }, options.library ) )
1646                                 }),
1647
1648                                 // Embed states.
1649                                 new media.controller.Embed(),
1650
1651                                 // Gallery states.
1652                                 new media.controller.GalleryEdit({
1653                                         library: options.selection,
1654                                         editing: options.editing,
1655                                         menu:    'gallery'
1656                                 }),
1657
1658                                 new media.controller.GalleryAdd()
1659                         ]);
1660
1661
1662                         if ( media.view.settings.post.featuredImageId ) {
1663                                 this.states.add( new media.controller.FeaturedImage() );
1664                         }
1665                 },
1666
1667                 bindHandlers: function() {
1668                         media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
1669                         this.on( 'menu:create:gallery', this.createMenu, this );
1670                         this.on( 'toolbar:create:main-insert', this.createToolbar, this );
1671                         this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
1672                         this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
1673                         this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
1674
1675                         var handlers = {
1676                                         menu: {
1677                                                 'default': 'mainMenu',
1678                                                 'gallery': 'galleryMenu'
1679                                         },
1680
1681                                         content: {
1682                                                 'embed':          'embedContent',
1683                                                 'edit-selection': 'editSelectionContent'
1684                                         },
1685
1686                                         toolbar: {
1687                                                 'main-insert':      'mainInsertToolbar',
1688                                                 'main-gallery':     'mainGalleryToolbar',
1689                                                 'gallery-edit':     'galleryEditToolbar',
1690                                                 'gallery-add':      'galleryAddToolbar'
1691                                         }
1692                                 };
1693
1694                         _.each( handlers, function( regionHandlers, region ) {
1695                                 _.each( regionHandlers, function( callback, handler ) {
1696                                         this.on( region + ':render:' + handler, this[ callback ], this );
1697                                 }, this );
1698                         }, this );
1699                 },
1700
1701                 // Menus
1702                 mainMenu: function( view ) {
1703                         view.set({
1704                                 'library-separator': new media.View({
1705                                         className: 'separator',
1706                                         priority: 100
1707                                 })
1708                         });
1709                 },
1710
1711                 galleryMenu: function( view ) {
1712                         var lastState = this.lastState(),
1713                                 previous = lastState && lastState.id,
1714                                 frame = this;
1715
1716                         view.set({
1717                                 cancel: {
1718                                         text:     l10n.cancelGalleryTitle,
1719                                         priority: 20,
1720                                         click:    function() {
1721                                                 if ( previous )
1722                                                         frame.setState( previous );
1723                                                 else
1724                                                         frame.close();
1725                                         }
1726                                 },
1727                                 separateCancel: new media.View({
1728                                         className: 'separator',
1729                                         priority: 40
1730                                 })
1731                         });
1732                 },
1733
1734                 // Content
1735                 embedContent: function() {
1736                         var view = new media.view.Embed({
1737                                 controller: this,
1738                                 model:      this.state()
1739                         }).render();
1740
1741                         this.content.set( view );
1742                         view.url.focus();
1743                 },
1744
1745                 editSelectionContent: function() {
1746                         var state = this.state(),
1747                                 selection = state.get('selection'),
1748                                 view;
1749
1750                         view = new media.view.AttachmentsBrowser({
1751                                 controller: this,
1752                                 collection: selection,
1753                                 selection:  selection,
1754                                 model:      state,
1755                                 sortable:   true,
1756                                 search:     false,
1757                                 dragInfo:   true,
1758
1759                                 AttachmentView: media.view.Attachment.EditSelection
1760                         }).render();
1761
1762                         view.toolbar.set( 'backToLibrary', {
1763                                 text:     l10n.returnToLibrary,
1764                                 priority: -100,
1765
1766                                 click: function() {
1767                                         this.controller.content.mode('browse');
1768                                 }
1769                         });
1770
1771                         // Browse our library of attachments.
1772                         this.content.set( view );
1773                 },
1774
1775                 // Toolbars
1776                 selectionStatusToolbar: function( view ) {
1777                         var editable = this.state().get('editable');
1778
1779                         view.set( 'selection', new media.view.Selection({
1780                                 controller: this,
1781                                 collection: this.state().get('selection'),
1782                                 priority:   -40,
1783
1784                                 // If the selection is editable, pass the callback to
1785                                 // switch the content mode.
1786                                 editable: editable && function() {
1787                                         this.controller.content.mode('edit-selection');
1788                                 }
1789                         }).render() );
1790                 },
1791
1792                 mainInsertToolbar: function( view ) {
1793                         var controller = this;
1794
1795                         this.selectionStatusToolbar( view );
1796
1797                         view.set( 'insert', {
1798                                 style:    'primary',
1799                                 priority: 80,
1800                                 text:     l10n.insertIntoPost,
1801                                 requires: { selection: true },
1802
1803                                 click: function() {
1804                                         var state = controller.state(),
1805                                                 selection = state.get('selection');
1806
1807                                         controller.close();
1808                                         state.trigger( 'insert', selection ).reset();
1809                                 }
1810                         });
1811                 },
1812
1813                 mainGalleryToolbar: function( view ) {
1814                         var controller = this;
1815
1816                         this.selectionStatusToolbar( view );
1817
1818                         view.set( 'gallery', {
1819                                 style:    'primary',
1820                                 text:     l10n.createNewGallery,
1821                                 priority: 60,
1822                                 requires: { selection: true },
1823
1824                                 click: function() {
1825                                         var selection = controller.state().get('selection'),
1826                                                 edit = controller.state('gallery-edit'),
1827                                                 models = selection.where({ type: 'image' });
1828
1829                                         edit.set( 'library', new media.model.Selection( models, {
1830                                                 props:    selection.props.toJSON(),
1831                                                 multiple: true
1832                                         }) );
1833
1834                                         this.controller.setState('gallery-edit');
1835                                 }
1836                         });
1837                 },
1838
1839                 featuredImageToolbar: function( toolbar ) {
1840                         this.createSelectToolbar( toolbar, {
1841                                 text:  l10n.setFeaturedImage,
1842                                 state: this.options.state || 'upload'
1843                         });
1844                 },
1845
1846                 mainEmbedToolbar: function( toolbar ) {
1847                         toolbar.view = new media.view.Toolbar.Embed({
1848                                 controller: this
1849                         });
1850                 },
1851
1852                 galleryEditToolbar: function() {
1853                         var editing = this.state().get('editing');
1854                         this.toolbar.set( new media.view.Toolbar({
1855                                 controller: this,
1856                                 items: {
1857                                         insert: {
1858                                                 style:    'primary',
1859                                                 text:     editing ? l10n.updateGallery : l10n.insertGallery,
1860                                                 priority: 80,
1861                                                 requires: { library: true },
1862
1863                                                 click: function() {
1864                                                         var controller = this.controller,
1865                                                                 state = controller.state();
1866
1867                                                         controller.close();
1868                                                         state.trigger( 'update', state.get('library') );
1869
1870                                                         controller.reset();
1871                                                         // @todo: Make the state activated dynamic (instead of hardcoded).
1872                                                         controller.setState('upload');
1873                                                 }
1874                                         }
1875                                 }
1876                         }) );
1877                 },
1878
1879                 galleryAddToolbar: function() {
1880                         this.toolbar.set( new media.view.Toolbar({
1881                                 controller: this,
1882                                 items: {
1883                                         insert: {
1884                                                 style:    'primary',
1885                                                 text:     l10n.addToGallery,
1886                                                 priority: 80,
1887                                                 requires: { selection: true },
1888
1889                                                 click: function() {
1890                                                         var controller = this.controller,
1891                                                                 state = controller.state(),
1892                                                                 edit = controller.state('gallery-edit');
1893
1894                                                         edit.get('library').add( state.get('selection').models );
1895                                                         state.trigger('reset');
1896                                                         controller.setState('gallery-edit');
1897                                                 }
1898                                         }
1899                                 }
1900                         }) );
1901                 }
1902         });
1903
1904         /**
1905          * wp.media.view.Modal
1906          */
1907         media.view.Modal = media.View.extend({
1908                 tagName:  'div',
1909                 template: media.template('media-modal'),
1910
1911                 attributes: {
1912                         tabindex: 0
1913                 },
1914
1915                 events: {
1916                         'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
1917                         'keydown': 'keydown'
1918                 },
1919
1920                 initialize: function() {
1921                         _.defaults( this.options, {
1922                                 container: document.body,
1923                                 title:     '',
1924                                 propagate: true,
1925                                 freeze:    true
1926                         });
1927                 },
1928
1929                 prepare: function() {
1930                         return {
1931                                 title: this.options.title
1932                         };
1933                 },
1934
1935                 attach: function() {
1936                         if ( this.views.attached )
1937                                 return this;
1938
1939                         if ( ! this.views.rendered )
1940                                 this.render();
1941
1942                         this.$el.appendTo( this.options.container );
1943
1944                         // Manually mark the view as attached and trigger ready.
1945                         this.views.attached = true;
1946                         this.views.ready();
1947
1948                         return this.propagate('attach');
1949                 },
1950
1951                 detach: function() {
1952                         if ( this.$el.is(':visible') )
1953                                 this.close();
1954
1955                         this.$el.detach();
1956                         this.views.attached = false;
1957                         return this.propagate('detach');
1958                 },
1959
1960                 open: function() {
1961                         var $el = this.$el,
1962                                 options = this.options;
1963
1964                         if ( $el.is(':visible') )
1965                                 return this;
1966
1967                         if ( ! this.views.attached )
1968                                 this.attach();
1969
1970                         // If the `freeze` option is set, record the window's scroll position.
1971                         if ( options.freeze ) {
1972                                 this._freeze = {
1973                                         scrollTop: $( window ).scrollTop()
1974                                 };
1975                         }
1976
1977                         $el.show().focus();
1978                         return this.propagate('open');
1979                 },
1980
1981                 close: function( options ) {
1982                         var freeze = this._freeze;
1983
1984                         if ( ! this.views.attached || ! this.$el.is(':visible') )
1985                                 return this;
1986
1987                         this.$el.hide();
1988                         this.propagate('close');
1989
1990                         // If the `freeze` option is set, restore the container's scroll position.
1991                         if ( freeze ) {
1992                                 $( window ).scrollTop( freeze.scrollTop );
1993                         }
1994
1995                         if ( options && options.escape )
1996                                 this.propagate('escape');
1997
1998                         return this;
1999                 },
2000
2001                 escape: function() {
2002                         return this.close({ escape: true });
2003                 },
2004
2005                 escapeHandler: function( event ) {
2006                         event.preventDefault();
2007                         this.escape();
2008                 },
2009
2010                 content: function( content ) {
2011                         this.views.set( '.media-modal-content', content );
2012                         return this;
2013                 },
2014
2015                 // Triggers a modal event and if the `propagate` option is set,
2016                 // forwards events to the modal's controller.
2017                 propagate: function( id ) {
2018                         this.trigger( id );
2019
2020                         if ( this.options.propagate )
2021                                 this.controller.trigger( id );
2022
2023                         return this;
2024                 },
2025
2026                 keydown: function( event ) {
2027                         // Close the modal when escape is pressed.
2028                         if ( 27 === event.which ) {
2029                                 event.preventDefault();
2030                                 this.escape();
2031                                 return;
2032                         }
2033                 }
2034         });
2035
2036         // wp.media.view.FocusManager
2037         // ----------------------------
2038         media.view.FocusManager = media.View.extend({
2039                 events: {
2040                         keydown: 'recordTab',
2041                         focusin: 'updateIndex'
2042                 },
2043
2044                 focus: function() {
2045                         if ( _.isUndefined( this.index ) )
2046                                 return;
2047
2048                         // Update our collection of `$tabbables`.
2049                         this.$tabbables = this.$(':tabbable');
2050
2051                         // If tab is saved, focus it.
2052                         this.$tabbables.eq( this.index ).focus();
2053                 },
2054
2055                 recordTab: function( event ) {
2056                         // Look for the tab key.
2057                         if ( 9 !== event.keyCode )
2058                                 return;
2059
2060                         // First try to update the index.
2061                         if ( _.isUndefined( this.index ) )
2062                                 this.updateIndex( event );
2063
2064                         // If we still don't have an index, bail.
2065                         if ( _.isUndefined( this.index ) )
2066                                 return;
2067
2068                         var index = this.index + ( event.shiftKey ? -1 : 1 );
2069
2070                         if ( index >= 0 && index < this.$tabbables.length )
2071                                 this.index = index;
2072                         else
2073                                 delete this.index;
2074                 },
2075
2076                 updateIndex: function( event ) {
2077                         this.$tabbables = this.$(':tabbable');
2078
2079                         var index = this.$tabbables.index( event.target );
2080
2081                         if ( -1 === index )
2082                                 delete this.index;
2083                         else
2084                                 this.index = index;
2085                 }
2086         });
2087
2088         // wp.media.view.UploaderWindow
2089         // ----------------------------
2090         media.view.UploaderWindow = media.View.extend({
2091                 tagName:   'div',
2092                 className: 'uploader-window',
2093                 template:  media.template('uploader-window'),
2094
2095                 initialize: function() {
2096                         var uploader;
2097
2098                         this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
2099
2100                         uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
2101                                 dropzone:  this.$el,
2102                                 browser:   this.$browser,
2103                                 params:    {}
2104                         });
2105
2106                         // Ensure the dropzone is a jQuery collection.
2107                         if ( uploader.dropzone && ! (uploader.dropzone instanceof $) )
2108                                 uploader.dropzone = $( uploader.dropzone );
2109
2110                         this.controller.on( 'activate', this.refresh, this );
2111                 },
2112
2113                 refresh: function() {
2114                         if ( this.uploader )
2115                                 this.uploader.refresh();
2116                 },
2117
2118                 ready: function() {
2119                         var postId = media.view.settings.post.id,
2120                                 dropzone;
2121
2122                         // If the uploader already exists, bail.
2123                         if ( this.uploader )
2124                                 return;
2125
2126                         if ( postId )
2127                                 this.options.uploader.params.post_id = postId;
2128
2129                         this.uploader = new wp.Uploader( this.options.uploader );
2130
2131                         dropzone = this.uploader.dropzone;
2132                         dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
2133                         dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
2134                 },
2135
2136                 show: function() {
2137                         var $el = this.$el.show();
2138
2139                         // Ensure that the animation is triggered by waiting until
2140                         // the transparent element is painted into the DOM.
2141                         _.defer( function() {
2142                                 $el.css({ opacity: 1 });
2143                         });
2144                 },
2145
2146                 hide: function() {
2147                         var $el = this.$el.css({ opacity: 0 });
2148
2149                         media.transition( $el ).done( function() {
2150                                 // Transition end events are subject to race conditions.
2151                                 // Make sure that the value is set as intended.
2152                                 if ( '0' === $el.css('opacity') )
2153                                         $el.hide();
2154                         });
2155                 }
2156         });
2157
2158         media.view.UploaderInline = media.View.extend({
2159                 tagName:   'div',
2160                 className: 'uploader-inline',
2161                 template:  media.template('uploader-inline'),
2162
2163                 initialize: function() {
2164                         _.defaults( this.options, {
2165                                 message: '',
2166                                 status:  true
2167                         });
2168
2169                         if ( ! this.options.$browser && this.controller.uploader )
2170                                 this.options.$browser = this.controller.uploader.$browser;
2171
2172                         if ( _.isUndefined( this.options.postId ) )
2173                                 this.options.postId = media.view.settings.post.id;
2174
2175                         if ( this.options.status ) {
2176                                 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
2177                                         controller: this.controller
2178                                 }) );
2179                         }
2180                 },
2181
2182                 dispose: function() {
2183                         if ( this.disposing )
2184                                 return media.View.prototype.dispose.apply( this, arguments );
2185
2186                         // Run remove on `dispose`, so we can be sure to refresh the
2187                         // uploader with a view-less DOM. Track whether we're disposing
2188                         // so we don't trigger an infinite loop.
2189                         this.disposing = true;
2190                         return this.remove();
2191                 },
2192
2193                 remove: function() {
2194                         var result = media.View.prototype.remove.apply( this, arguments );
2195
2196                         _.defer( _.bind( this.refresh, this ) );
2197                         return result;
2198                 },
2199
2200                 refresh: function() {
2201                         var uploader = this.controller.uploader;
2202
2203                         if ( uploader )
2204                                 uploader.refresh();
2205                 },
2206
2207                 ready: function() {
2208                         var $browser = this.options.$browser,
2209                                 $placeholder;
2210
2211                         if ( this.controller.uploader ) {
2212                                 $placeholder = this.$('.browser');
2213
2214                                 // Check if we've already replaced the placeholder.
2215                                 if ( $placeholder[0] === $browser[0] )
2216                                         return;
2217
2218                                 $browser.detach().text( $placeholder.text() );
2219                                 $browser[0].className = $placeholder[0].className;
2220                                 $placeholder.replaceWith( $browser.show() );
2221                         }
2222
2223                         this.refresh();
2224                         return this;
2225                 }
2226         });
2227
2228         /**
2229          * wp.media.view.UploaderStatus
2230          */
2231         media.view.UploaderStatus = media.View.extend({
2232                 className: 'media-uploader-status',
2233                 template:  media.template('uploader-status'),
2234
2235                 events: {
2236                         'click .upload-dismiss-errors': 'dismiss'
2237                 },
2238
2239                 initialize: function() {
2240                         this.queue = wp.Uploader.queue;
2241                         this.queue.on( 'add remove reset', this.visibility, this );
2242                         this.queue.on( 'add remove reset change:percent', this.progress, this );
2243                         this.queue.on( 'add remove reset change:uploading', this.info, this );
2244
2245                         this.errors = wp.Uploader.errors;
2246                         this.errors.reset();
2247                         this.errors.on( 'add remove reset', this.visibility, this );
2248                         this.errors.on( 'add', this.error, this );
2249                 },
2250
2251                 dispose: function() {
2252                         wp.Uploader.queue.off( null, null, this );
2253                         media.View.prototype.dispose.apply( this, arguments );
2254                         return this;
2255                 },
2256
2257                 visibility: function() {
2258                         this.$el.toggleClass( 'uploading', !! this.queue.length );
2259                         this.$el.toggleClass( 'errors', !! this.errors.length );
2260                         this.$el.toggle( !! this.queue.length || !! this.errors.length );
2261                 },
2262
2263                 ready: function() {
2264                         _.each({
2265                                 '$bar':      '.media-progress-bar div',
2266                                 '$index':    '.upload-index',
2267                                 '$total':    '.upload-total',
2268                                 '$filename': '.upload-filename'
2269                         }, function( selector, key ) {
2270                                 this[ key ] = this.$( selector );
2271                         }, this );
2272
2273                         this.visibility();
2274                         this.progress();
2275                         this.info();
2276                 },
2277
2278                 progress: function() {
2279                         var queue = this.queue,
2280                                 $bar = this.$bar,
2281                                 memo = 0;
2282
2283                         if ( ! $bar || ! queue.length )
2284                                 return;
2285
2286                         $bar.width( ( queue.reduce( function( memo, attachment ) {
2287                                 if ( ! attachment.get('uploading') )
2288                                         return memo + 100;
2289
2290                                 var percent = attachment.get('percent');
2291                                 return memo + ( _.isNumber( percent ) ? percent : 100 );
2292                         }, 0 ) / queue.length ) + '%' );
2293                 },
2294
2295                 info: function() {
2296                         var queue = this.queue,
2297                                 index = 0, active;
2298
2299                         if ( ! queue.length )
2300                                 return;
2301
2302                         active = this.queue.find( function( attachment, i ) {
2303                                 index = i;
2304                                 return attachment.get('uploading');
2305                         });
2306
2307                         this.$index.text( index + 1 );
2308                         this.$total.text( queue.length );
2309                         this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
2310                 },
2311
2312                 filename: function( filename ) {
2313                         return media.truncate( _.escape( filename ), 24 );
2314                 },
2315
2316                 error: function( error ) {
2317                         this.views.add( '.upload-errors', new media.view.UploaderStatusError({
2318                                 filename: this.filename( error.get('file').name ),
2319                                 message:  error.get('message')
2320                         }), { at: 0 });
2321                 },
2322
2323                 dismiss: function( event ) {
2324                         var errors = this.views.get('.upload-errors');
2325
2326                         event.preventDefault();
2327
2328                         if ( errors )
2329                                 _.invoke( errors, 'remove' );
2330                         wp.Uploader.errors.reset();
2331                 }
2332         });
2333
2334         media.view.UploaderStatusError = media.View.extend({
2335                 className: 'upload-error',
2336                 template:  media.template('uploader-status-error')
2337         });
2338
2339         /**
2340          * wp.media.view.Toolbar
2341          */
2342         media.view.Toolbar = media.View.extend({
2343                 tagName:   'div',
2344                 className: 'media-toolbar',
2345
2346                 initialize: function() {
2347                         var state = this.controller.state(),
2348                                 selection = this.selection = state.get('selection'),
2349                                 library = this.library = state.get('library');
2350
2351                         this._views = {};
2352
2353                         // The toolbar is composed of two `PriorityList` views.
2354                         this.primary   = new media.view.PriorityList();
2355                         this.secondary = new media.view.PriorityList();
2356                         this.primary.$el.addClass('media-toolbar-primary');
2357                         this.secondary.$el.addClass('media-toolbar-secondary');
2358
2359                         this.views.set([ this.secondary, this.primary ]);
2360
2361                         if ( this.options.items )
2362                                 this.set( this.options.items, { silent: true });
2363
2364                         if ( ! this.options.silent )
2365                                 this.render();
2366
2367                         if ( selection )
2368                                 selection.on( 'add remove reset', this.refresh, this );
2369                         if ( library )
2370                                 library.on( 'add remove reset', this.refresh, this );
2371                 },
2372
2373                 dispose: function() {
2374                         if ( this.selection )
2375                                 this.selection.off( null, null, this );
2376                         if ( this.library )
2377                                 this.library.off( null, null, this );
2378                         return media.View.prototype.dispose.apply( this, arguments );
2379                 },
2380
2381                 ready: function() {
2382                         this.refresh();
2383                 },
2384
2385                 set: function( id, view, options ) {
2386                         var list;
2387                         options = options || {};
2388
2389                         // Accept an object with an `id` : `view` mapping.
2390                         if ( _.isObject( id ) ) {
2391                                 _.each( id, function( view, id ) {
2392                                         this.set( id, view, { silent: true });
2393                                 }, this );
2394
2395                         } else {
2396                                 if ( ! ( view instanceof Backbone.View ) ) {
2397                                         view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
2398                                         view = new media.view.Button( view ).render();
2399                                 }
2400
2401                                 view.controller = view.controller || this.controller;
2402
2403                                 this._views[ id ] = view;
2404
2405                                 list = view.options.priority < 0 ? 'secondary' : 'primary';
2406                                 this[ list ].set( id, view, options );
2407                         }
2408
2409                         if ( ! options.silent )
2410                                 this.refresh();
2411
2412                         return this;
2413                 },
2414
2415                 get: function( id ) {
2416                         return this._views[ id ];
2417                 },
2418
2419                 unset: function( id, options ) {
2420                         delete this._views[ id ];
2421                         this.primary.unset( id, options );
2422                         this.secondary.unset( id, options );
2423
2424                         if ( ! options || ! options.silent )
2425                                 this.refresh();
2426                         return this;
2427                 },
2428
2429                 refresh: function() {
2430                         var state = this.controller.state(),
2431                                 library = state.get('library'),
2432                                 selection = state.get('selection');
2433
2434                         _.each( this._views, function( button ) {
2435                                 if ( ! button.model || ! button.options || ! button.options.requires )
2436                                         return;
2437
2438                                 var requires = button.options.requires,
2439                                         disabled = false;
2440
2441                                 if ( requires.selection && selection && ! selection.length )
2442                                         disabled = true;
2443                                 else if ( requires.library && library && ! library.length )
2444                                         disabled = true;
2445
2446                                 button.model.set( 'disabled', disabled );
2447                         });
2448                 }
2449         });
2450
2451         // wp.media.view.Toolbar.Select
2452         // ----------------------------
2453         media.view.Toolbar.Select = media.view.Toolbar.extend({
2454                 initialize: function() {
2455                         var options = this.options,
2456                                 controller = options.controller,
2457                                 selection = controller.state().get('selection');
2458
2459                         _.bindAll( this, 'clickSelect' );
2460
2461                         _.defaults( options, {
2462                                 event: 'select',
2463                                 state: false,
2464                                 reset: true,
2465                                 close: true,
2466                                 text:  l10n.select,
2467
2468                                 // Does the button rely on the selection?
2469                                 requires: {
2470                                         selection: true
2471                                 }
2472                         });
2473
2474                         options.items = _.defaults( options.items || {}, {
2475                                 select: {
2476                                         style:    'primary',
2477                                         text:     options.text,
2478                                         priority: 80,
2479                                         click:    this.clickSelect,
2480                                         requires: options.requires
2481                                 }
2482                         });
2483
2484                         media.view.Toolbar.prototype.initialize.apply( this, arguments );
2485                 },
2486
2487                 clickSelect: function() {
2488                         var options = this.options,
2489                                 controller = this.controller;
2490
2491                         if ( options.close )
2492                                 controller.close();
2493
2494                         if ( options.event )
2495                                 controller.state().trigger( options.event );
2496
2497                         if ( options.reset )
2498                                 controller.reset();
2499
2500                         if ( options.state )
2501                                 controller.setState( options.state );
2502                 }
2503         });
2504
2505         // wp.media.view.Toolbar.Embed
2506         // ---------------------------
2507         media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
2508                 initialize: function() {
2509                         _.defaults( this.options, {
2510                                 text: l10n.insertIntoPost,
2511                                 requires: false
2512                         });
2513
2514                         media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
2515                 },
2516
2517                 refresh: function() {
2518                         var url = this.controller.state().props.get('url');
2519                         this.get('select').model.set( 'disabled', ! url || url === 'http://' );
2520
2521                         media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
2522                 }
2523         });
2524
2525         /**
2526          * wp.media.view.Button
2527          */
2528         media.view.Button = media.View.extend({
2529                 tagName:    'a',
2530                 className:  'media-button',
2531                 attributes: { href: '#' },
2532
2533                 events: {
2534                         'click': 'click'
2535                 },
2536
2537                 defaults: {
2538                         text:     '',
2539                         style:    '',
2540                         size:     'large',
2541                         disabled: false
2542                 },
2543
2544                 initialize: function() {
2545                         // Create a model with the provided `defaults`.
2546                         this.model = new Backbone.Model( this.defaults );
2547
2548                         // If any of the `options` have a key from `defaults`, apply its
2549                         // value to the `model` and remove it from the `options object.
2550                         _.each( this.defaults, function( def, key ) {
2551                                 var value = this.options[ key ];
2552                                 if ( _.isUndefined( value ) )
2553                                         return;
2554
2555                                 this.model.set( key, value );
2556                                 delete this.options[ key ];
2557                         }, this );
2558
2559                         this.model.on( 'change', this.render, this );
2560                 },
2561
2562                 render: function() {
2563                         var classes = [ 'button', this.className ],
2564                                 model = this.model.toJSON();
2565
2566                         if ( model.style )
2567                                 classes.push( 'button-' + model.style );
2568
2569                         if ( model.size )
2570                                 classes.push( 'button-' + model.size );
2571
2572                         classes = _.uniq( classes.concat( this.options.classes ) );
2573                         this.el.className = classes.join(' ');
2574
2575                         this.$el.attr( 'disabled', model.disabled );
2576                         this.$el.text( this.model.get('text') );
2577
2578                         return this;
2579                 },
2580
2581                 click: function( event ) {
2582                         if ( '#' === this.attributes.href )
2583                                 event.preventDefault();
2584
2585                         if ( this.options.click && ! this.model.get('disabled') )
2586                                 this.options.click.apply( this, arguments );
2587                 }
2588         });
2589
2590         /**
2591          * wp.media.view.ButtonGroup
2592          */
2593         media.view.ButtonGroup = media.View.extend({
2594                 tagName:   'div',
2595                 className: 'button-group button-large media-button-group',
2596
2597                 initialize: function() {
2598                         this.buttons = _.map( this.options.buttons || [], function( button ) {
2599                                 if ( button instanceof Backbone.View )
2600                                         return button;
2601                                 else
2602                                         return new media.view.Button( button ).render();
2603                         });
2604
2605                         delete this.options.buttons;
2606
2607                         if ( this.options.classes )
2608                                 this.$el.addClass( this.options.classes );
2609                 },
2610
2611                 render: function() {
2612                         this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
2613                         return this;
2614                 }
2615         });
2616
2617         /**
2618          * wp.media.view.PriorityList
2619          */
2620
2621         media.view.PriorityList = media.View.extend({
2622                 tagName:   'div',
2623
2624                 initialize: function() {
2625                         this._views = {};
2626
2627                         this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
2628                         delete this.options.views;
2629
2630                         if ( ! this.options.silent )
2631                                 this.render();
2632                 },
2633
2634                 set: function( id, view, options ) {
2635                         var priority, views, index;
2636
2637                         options = options || {};
2638
2639                         // Accept an object with an `id` : `view` mapping.
2640                         if ( _.isObject( id ) ) {
2641                                 _.each( id, function( view, id ) {
2642                                         this.set( id, view );
2643                                 }, this );
2644                                 return this;
2645                         }
2646
2647                         if ( ! (view instanceof Backbone.View) )
2648                                 view = this.toView( view, id, options );
2649
2650                         view.controller = view.controller || this.controller;
2651
2652                         this.unset( id );
2653
2654                         priority = view.options.priority || 10;
2655                         views = this.views.get() || [];
2656
2657                         _.find( views, function( existing, i ) {
2658                                 if ( existing.options.priority > priority ) {
2659                                         index = i;
2660                                         return true;
2661                                 }
2662                         });
2663
2664                         this._views[ id ] = view;
2665                         this.views.add( view, {
2666                                 at: _.isNumber( index ) ? index : views.length || 0
2667                         });
2668
2669                         return this;
2670                 },
2671
2672                 get: function( id ) {
2673                         return this._views[ id ];
2674                 },
2675
2676                 unset: function( id ) {
2677                         var view = this.get( id );
2678
2679                         if ( view )
2680                                 view.remove();
2681
2682                         delete this._views[ id ];
2683                         return this;
2684                 },
2685
2686                 toView: function( options ) {
2687                         return new media.View( options );
2688                 }
2689         });
2690
2691         /**
2692          * wp.media.view.MenuItem
2693          */
2694         media.view.MenuItem = media.View.extend({
2695                 tagName:   'a',
2696                 className: 'media-menu-item',
2697
2698                 attributes: {
2699                         href: '#'
2700                 },
2701
2702                 events: {
2703                         'click': '_click'
2704                 },
2705
2706                 _click: function( event ) {
2707                         var clickOverride = this.options.click;
2708
2709                         if ( event )
2710                                 event.preventDefault();
2711
2712                         if ( clickOverride )
2713                                 clickOverride.call( this );
2714                         else
2715                                 this.click();
2716                 },
2717
2718                 click: function() {
2719                         var state = this.options.state;
2720                         if ( state )
2721                                 this.controller.setState( state );
2722                 },
2723
2724                 render: function() {
2725                         var options = this.options;
2726
2727                         if ( options.text )
2728                                 this.$el.text( options.text );
2729                         else if ( options.html )
2730                                 this.$el.html( options.html );
2731
2732                         return this;
2733                 }
2734         });
2735
2736         /**
2737          * wp.media.view.Menu
2738          */
2739         media.view.Menu = media.view.PriorityList.extend({
2740                 tagName:   'div',
2741                 className: 'media-menu',
2742                 property:  'state',
2743                 ItemView:  media.view.MenuItem,
2744                 region:    'menu',
2745
2746                 toView: function( options, id ) {
2747                         options = options || {};
2748                         options[ this.property ] = options[ this.property ] || id;
2749                         return new this.ItemView( options ).render();
2750                 },
2751
2752                 ready: function() {
2753                         media.view.PriorityList.prototype.ready.apply( this, arguments );
2754                         this.visibility();
2755                 },
2756
2757                 set: function() {
2758                         media.view.PriorityList.prototype.set.apply( this, arguments );
2759                         this.visibility();
2760                 },
2761
2762                 unset: function() {
2763                         media.view.PriorityList.prototype.unset.apply( this, arguments );
2764                         this.visibility();
2765                 },
2766
2767                 visibility: function() {
2768                         var region = this.region,
2769                                 view = this.controller[ region ].get(),
2770                                 views = this.views.get(),
2771                                 hide = ! views || views.length < 2;
2772
2773                         if ( this === view )
2774                                 this.controller.$el.toggleClass( 'hide-' + region, hide );
2775                 },
2776
2777                 select: function( id ) {
2778                         var view = this.get( id );
2779
2780                         if ( ! view )
2781                                 return;
2782
2783                         this.deselect();
2784                         view.$el.addClass('active');
2785                 },
2786
2787                 deselect: function() {
2788                         this.$el.children().removeClass('active');
2789                 }
2790         });
2791
2792         /**
2793          * wp.media.view.RouterItem
2794          */
2795         media.view.RouterItem = media.view.MenuItem.extend({
2796                 click: function() {
2797                         var contentMode = this.options.contentMode;
2798                         if ( contentMode )
2799                                 this.controller.content.mode( contentMode );
2800                 }
2801         });
2802
2803         /**
2804          * wp.media.view.Router
2805          */
2806         media.view.Router = media.view.Menu.extend({
2807                 tagName:   'div',
2808                 className: 'media-router',
2809                 property:  'contentMode',
2810                 ItemView:  media.view.RouterItem,
2811                 region:    'router',
2812
2813                 initialize: function() {
2814                         this.controller.on( 'content:render', this.update, this );
2815                         media.view.Menu.prototype.initialize.apply( this, arguments );
2816                 },
2817
2818                 update: function() {
2819                         var mode = this.controller.content.mode();
2820                         if ( mode )
2821                                 this.select( mode );
2822                 }
2823         });
2824
2825
2826         /**
2827          * wp.media.view.Sidebar
2828          */
2829         media.view.Sidebar = media.view.PriorityList.extend({
2830                 className: 'media-sidebar'
2831         });
2832
2833         /**
2834          * wp.media.view.Attachment
2835          */
2836         media.view.Attachment = media.View.extend({
2837                 tagName:   'li',
2838                 className: 'attachment',
2839                 template:  media.template('attachment'),
2840
2841                 events: {
2842                         'click .attachment-preview':      'toggleSelectionHandler',
2843                         'change [data-setting]':          'updateSetting',
2844                         'change [data-setting] input':    'updateSetting',
2845                         'change [data-setting] select':   'updateSetting',
2846                         'change [data-setting] textarea': 'updateSetting',
2847                         'click .close':                   'removeFromLibrary',
2848                         'click .check':                   'removeFromSelection',
2849                         'click a':                        'preventDefault'
2850                 },
2851
2852                 buttons: {},
2853
2854                 initialize: function() {
2855                         var selection = this.options.selection;
2856
2857                         this.model.on( 'change:sizes change:uploading change:caption change:title', this.render, this );
2858                         this.model.on( 'change:percent', this.progress, this );
2859
2860                         // Update the selection.
2861                         this.model.on( 'add', this.select, this );
2862                         this.model.on( 'remove', this.deselect, this );
2863                         if ( selection )
2864                                 selection.on( 'reset', this.updateSelect, this );
2865
2866                         // Update the model's details view.
2867                         this.model.on( 'selection:single selection:unsingle', this.details, this );
2868                         this.details( this.model, this.controller.state().get('selection') );
2869                 },
2870
2871                 dispose: function() {
2872                         var selection = this.options.selection;
2873
2874                         // Make sure all settings are saved before removing the view.
2875                         this.updateAll();
2876
2877                         if ( selection )
2878                                 selection.off( null, null, this );
2879
2880                         media.View.prototype.dispose.apply( this, arguments );
2881                         return this;
2882                 },
2883
2884                 render: function() {
2885                         var options = _.defaults( this.model.toJSON(), {
2886                                         orientation:   'landscape',
2887                                         uploading:     false,
2888                                         type:          '',
2889                                         subtype:       '',
2890                                         icon:          '',
2891                                         filename:      '',
2892                                         caption:       '',
2893                                         title:         '',
2894                                         dateFormatted: '',
2895                                         width:         '',
2896                                         height:        '',
2897                                         compat:        false,
2898                                         alt:           '',
2899                                         description:   ''
2900                                 });
2901
2902                         options.buttons  = this.buttons;
2903                         options.describe = this.controller.state().get('describe');
2904
2905                         if ( 'image' === options.type )
2906                                 options.size = this.imageSize();
2907
2908                         options.can = {};
2909                         if ( options.nonces ) {
2910                                 options.can.remove = !! options.nonces['delete'];
2911                                 options.can.save = !! options.nonces.update;
2912                         }
2913
2914                         if ( this.controller.state().get('allowLocalEdits') )
2915                                 options.allowLocalEdits = true;
2916
2917                         this.views.detach();
2918                         this.$el.html( this.template( options ) );
2919
2920                         this.$el.toggleClass( 'uploading', options.uploading );
2921                         if ( options.uploading )
2922                                 this.$bar = this.$('.media-progress-bar div');
2923                         else
2924                                 delete this.$bar;
2925
2926                         // Check if the model is selected.
2927                         this.updateSelect();
2928
2929                         // Update the save status.
2930                         this.updateSave();
2931
2932                         this.views.render();
2933
2934                         return this;
2935                 },
2936
2937                 progress: function() {
2938                         if ( this.$bar && this.$bar.length )
2939                                 this.$bar.width( this.model.get('percent') + '%' );
2940                 },
2941
2942                 toggleSelectionHandler: function( event ) {
2943                         var method;
2944
2945                         if ( event.shiftKey )
2946                                 method = 'between';
2947                         else if ( event.ctrlKey || event.metaKey )
2948                                 method = 'toggle';
2949
2950                         this.toggleSelection({
2951                                 method: method
2952                         });
2953                 },
2954
2955                 toggleSelection: function( options ) {
2956                         var collection = this.collection,
2957                                 selection = this.options.selection,
2958                                 model = this.model,
2959                                 method = options && options.method,
2960                                 single, between, models, singleIndex, modelIndex;
2961
2962                         if ( ! selection )
2963                                 return;
2964
2965                         single = selection.single();
2966                         method = _.isUndefined( method ) ? selection.multiple : method;
2967
2968                         // If the `method` is set to `between`, select all models that
2969                         // exist between the current and the selected model.
2970                         if ( 'between' === method && single && selection.multiple ) {
2971                                 // If the models are the same, short-circuit.
2972                                 if ( single === model )
2973                                         return;
2974
2975                                 singleIndex = collection.indexOf( single );
2976                                 modelIndex  = collection.indexOf( this.model );
2977
2978                                 if ( singleIndex < modelIndex )
2979                                         models = collection.models.slice( singleIndex, modelIndex + 1 );
2980                                 else
2981                                         models = collection.models.slice( modelIndex, singleIndex + 1 );
2982
2983                                 selection.add( models ).single( model );
2984                                 return;
2985
2986                         // If the `method` is set to `toggle`, just flip the selection
2987                         // status, regardless of whether the model is the single model.
2988                         } else if ( 'toggle' === method ) {
2989                                 selection[ this.selected() ? 'remove' : 'add' ]( model ).single( model );
2990                                 return;
2991                         }
2992
2993                         if ( method !== 'add' )
2994                                 method = 'reset';
2995
2996                         if ( this.selected() ) {
2997                                 // If the model is the single model, remove it.
2998                                 // If it is not the same as the single model,
2999                                 // it now becomes the single model.
3000                                 selection[ single === model ? 'remove' : 'single' ]( model );
3001                         } else {
3002                                 // If the model is not selected, run the `method` on the
3003                                 // selection. By default, we `reset` the selection, but the
3004                                 // `method` can be set to `add` the model to the selection.
3005                                 selection[ method ]( model ).single( model );
3006                         }
3007                 },
3008
3009                 updateSelect: function() {
3010                         this[ this.selected() ? 'select' : 'deselect' ]();
3011                 },
3012
3013                 selected: function() {
3014                         var selection = this.options.selection;
3015                         if ( selection )
3016                                 return !! selection.getByCid( this.model.cid );
3017                 },
3018
3019                 select: function( model, collection ) {
3020                         var selection = this.options.selection;
3021
3022                         // Check if a selection exists and if it's the collection provided.
3023                         // If they're not the same collection, bail; we're in another
3024                         // selection's event loop.
3025                         if ( ! selection || ( collection && collection !== selection ) )
3026                                 return;
3027
3028                         this.$el.addClass('selected');
3029                 },
3030
3031                 deselect: function( model, collection ) {
3032                         var selection = this.options.selection;
3033
3034                         // Check if a selection exists and if it's the collection provided.
3035                         // If they're not the same collection, bail; we're in another
3036                         // selection's event loop.
3037                         if ( ! selection || ( collection && collection !== selection ) )
3038                                 return;
3039
3040                         this.$el.removeClass('selected');
3041                 },
3042
3043                 details: function( model, collection ) {
3044                         var selection = this.options.selection,
3045                                 details;
3046
3047                         if ( selection !== collection )
3048                                 return;
3049
3050                         details = selection.single();
3051                         this.$el.toggleClass( 'details', details === this.model );
3052                 },
3053
3054                 preventDefault: function( event ) {
3055                         event.preventDefault();
3056                 },
3057
3058                 imageSize: function( size ) {
3059                         var sizes = this.model.get('sizes');
3060
3061                         size = size || 'medium';
3062
3063                         // Use the provided image size if possible.
3064                         if ( sizes && sizes[ size ] ) {
3065                                 return _.clone( sizes[ size ] );
3066                         } else {
3067                                 return {
3068                                         url:         this.model.get('url'),
3069                                         width:       this.model.get('width'),
3070                                         height:      this.model.get('height'),
3071                                         orientation: this.model.get('orientation')
3072                                 };
3073                         }
3074                 },
3075
3076                 updateSetting: function( event ) {
3077                         var $setting = $( event.target ).closest('[data-setting]'),
3078                                 setting, value;
3079
3080                         if ( ! $setting.length )
3081                                 return;
3082
3083                         setting = $setting.data('setting');
3084                         value   = event.target.value;
3085
3086                         if ( this.model.get( setting ) !== value )
3087                                 this.save( setting, value );
3088                 },
3089
3090                 // Pass all the arguments to the model's save method.
3091                 //
3092                 // Records the aggregate status of all save requests and updates the
3093                 // view's classes accordingly.
3094                 save: function() {
3095                         var view = this,
3096                                 save = this._save = this._save || { status: 'ready' },
3097                                 request = this.model.save.apply( this.model, arguments ),
3098                                 requests = save.requests ? $.when( request, save.requests ) : request;
3099
3100                         // If we're waiting to remove 'Saved.', stop.
3101                         if ( save.savedTimer )
3102                                 clearTimeout( save.savedTimer );
3103