]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/media-views.js
WordPress 3.5.1
[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                         if ( this.get('contentUserSetting') ) {
417                                 this.frame.on( 'content:activate', this.saveContentMode, this );
418                                 this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
419                         }
420                 },
421
422                 deactivate: function() {
423                         this.recordSelection();
424
425                         this.frame.off( 'content:activate', this.saveContentMode, this );
426
427                         // Unbind all event handlers that use this state as the context
428                         // from the selection.
429                         this.get('selection').off( null, null, this );
430
431                         wp.Uploader.queue.off( null, null, this );
432                 },
433
434                 reset: function() {
435                         this.get('selection').reset();
436                         this.resetDisplays();
437                         this.refreshContent();
438                 },
439
440                 resetDisplays: function() {
441                         var defaultProps = media.view.settings.defaultProps;
442                         this._displays = [];
443                         this._defaultDisplaySettings = {
444                                 align: defaultProps.align || getUserSetting( 'align', 'none' ),
445                                 size:  defaultProps.size  || getUserSetting( 'imgsize', 'medium' ),
446                                 link:  defaultProps.link  || getUserSetting( 'urlbutton', 'file' )
447                         };
448                 },
449
450                 display: function( attachment ) {
451                         var displays = this._displays;
452
453                         if ( ! displays[ attachment.cid ] )
454                                 displays[ attachment.cid ] = new Backbone.Model( this._defaultDisplaySettings );
455
456                         return displays[ attachment.cid ];
457                 },
458
459                 syncSelection: function() {
460                         var selection = this.get('selection'),
461                                 manager = this.frame._selection;
462
463                         if ( ! this.get('syncSelection') || ! manager || ! selection )
464                                 return;
465
466                         // If the selection supports multiple items, validate the stored
467                         // attachments based on the new selection's conditions. Record
468                         // the attachments that are not included; we'll maintain a
469                         // reference to those. Other attachments are considered in flux.
470                         if ( selection.multiple ) {
471                                 selection.reset( [], { silent: true });
472                                 selection.validateAll( manager.attachments );
473                                 manager.difference = _.difference( manager.attachments.models, selection.models );
474                         }
475
476                         // Sync the selection's single item with the master.
477                         selection.single( manager.single );
478                 },
479
480                 recordSelection: function() {
481                         var selection = this.get('selection'),
482                                 manager = this.frame._selection,
483                                 filtered;
484
485                         if ( ! this.get('syncSelection') || ! manager || ! selection )
486                                 return;
487
488                         // Record the currently active attachments, which is a combination
489                         // of the selection's attachments and the set of selected
490                         // attachments that this specific selection considered invalid.
491                         // Reset the difference and record the single attachment.
492                         if ( selection.multiple ) {
493                                 manager.attachments.reset( selection.toArray().concat( manager.difference ) );
494                                 manager.difference = [];
495                         } else {
496                                 manager.attachments.add( selection.toArray() );
497                         }
498
499                         manager.single = selection._single;
500                 },
501
502                 refreshContent: function() {
503                         var selection = this.get('selection'),
504                                 frame = this.frame,
505                                 router = frame.router.get(),
506                                 mode = frame.content.mode();
507
508                         // If the state is active, no items are selected, and the current
509                         // content mode is not an option in the state's router (provided
510                         // the state has a router), reset the content mode to the default.
511                         if ( this.active && ! selection.length && router && ! router.get( mode ) )
512                                 this.frame.content.render( this.get('content') );
513                 },
514
515                 uploading: function( attachment ) {
516                         var content = this.frame.content;
517
518                         // If the uploader was selected, navigate to the browser.
519                         if ( 'upload' === content.mode() )
520                                 this.frame.content.mode('browse');
521
522                         // Automatically select any uploading attachments.
523                         //
524                         // Selections that don't support multiple attachments automatically
525                         // limit themselves to one attachment (in this case, the last
526                         // attachment in the upload queue).
527                         this.get('selection').add( attachment );
528                 },
529
530                 saveContentMode: function() {
531                         // Only track the browse router on library states.
532                         if ( 'browse' !== this.get('router') )
533                                 return;
534
535                         var mode = this.frame.content.mode(),
536                                 view = this.frame.router.get();
537
538                         if ( view && view.get( mode ) )
539                                 setUserSetting( 'libraryContent', mode );
540                 }
541         });
542
543         // wp.media.controller.GalleryEdit
544         // -------------------------------
545         media.controller.GalleryEdit = media.controller.Library.extend({
546                 defaults: {
547                         id:         'gallery-edit',
548                         multiple:   false,
549                         describe:   true,
550                         edge:       199,
551                         editing:    false,
552                         sortable:   true,
553                         searchable: false,
554                         toolbar:    'gallery-edit',
555                         content:    'browse',
556                         title:      l10n.editGalleryTitle,
557                         priority:   60,
558                         dragInfo:   true,
559
560                         // Don't sync the selection, as the Edit Gallery library
561                         // *is* the selection.
562                         syncSelection: false
563                 },
564
565                 initialize: function() {
566                         // If we haven't been provided a `library`, create a `Selection`.
567                         if ( ! this.get('library') )
568                                 this.set( 'library', new media.model.Selection() );
569
570                         // The single `Attachment` view to be used in the `Attachments` view.
571                         if ( ! this.get('AttachmentView') )
572                                 this.set( 'AttachmentView', media.view.Attachment.EditLibrary );
573                         media.controller.Library.prototype.initialize.apply( this, arguments );
574                 },
575
576                 activate: function() {
577                         var library = this.get('library');
578
579                         // Limit the library to images only.
580                         library.props.set( 'type', 'image' );
581
582                         // Watch for uploaded attachments.
583                         this.get('library').observe( wp.Uploader.queue );
584
585                         this.frame.on( 'content:render:browse', this.gallerySettings, this );
586
587                         media.controller.Library.prototype.activate.apply( this, arguments );
588                 },
589
590                 deactivate: function() {
591                         // Stop watching for uploaded attachments.
592                         this.get('library').unobserve( wp.Uploader.queue );
593
594                         this.frame.off( 'content:render:browse', this.gallerySettings, this );
595
596                         media.controller.Library.prototype.deactivate.apply( this, arguments );
597                 },
598
599                 gallerySettings: function( browser ) {
600                         var library = this.get('library');
601
602                         if ( ! library || ! browser )
603                                 return;
604
605                         library.gallery = library.gallery || new Backbone.Model();
606
607                         browser.sidebar.set({
608                                 gallery: new media.view.Settings.Gallery({
609                                         controller: this,
610                                         model:      library.gallery,
611                                         priority:   40
612                                 })
613                         });
614
615                         browser.toolbar.set( 'reverse', {
616                                 text:     l10n.reverseOrder,
617                                 priority: 80,
618
619                                 click: function() {
620                                         library.reset( library.toArray().reverse() );
621                                 }
622                         });
623                 }
624         });
625
626         // wp.media.controller.GalleryAdd
627         // ---------------------------------
628         media.controller.GalleryAdd = media.controller.Library.extend({
629                 defaults: _.defaults({
630                         id:           'gallery-library',
631                         filterable:   'uploaded',
632                         multiple:     'add',
633                         menu:         'gallery',
634                         toolbar:      'gallery-add',
635                         title:        l10n.addToGalleryTitle,
636                         priority:     100,
637
638                         // Don't sync the selection, as the Edit Gallery library
639                         // *is* the selection.
640                         syncSelection: false
641                 }, media.controller.Library.prototype.defaults ),
642
643                 initialize: function() {
644                         // If we haven't been provided a `library`, create a `Selection`.
645                         if ( ! this.get('library') )
646                                 this.set( 'library', media.query({ type: 'image' }) );
647
648                         media.controller.Library.prototype.initialize.apply( this, arguments );
649                 },
650
651                 activate: function() {
652                         var library = this.get('library'),
653                                 edit    = this.frame.state('gallery-edit').get('library');
654
655                         if ( this.editLibrary && this.editLibrary !== edit )
656                                 library.unobserve( this.editLibrary );
657
658                         // Accepts attachments that exist in the original library and
659                         // that do not exist in gallery's library.
660                         library.validator = function( attachment ) {
661                                 return !! this.mirroring.getByCid( attachment.cid ) && ! edit.getByCid( attachment.cid ) && media.model.Selection.prototype.validator.apply( this, arguments );
662                         };
663
664                         // Reset the library to ensure that all attachments are re-added
665                         // to the collection. Do so silently, as calling `observe` will
666                         // trigger the `reset` event.
667                         library.reset( library.mirroring.models, { silent: true });
668                         library.observe( edit );
669                         this.editLibrary = edit;
670
671                         media.controller.Library.prototype.activate.apply( this, arguments );
672                 }
673         });
674
675         // wp.media.controller.FeaturedImage
676         // ---------------------------------
677         media.controller.FeaturedImage = media.controller.Library.extend({
678                 defaults: _.defaults({
679                         id:         'featured-image',
680                         filterable: 'uploaded',
681                         multiple:   false,
682                         toolbar:    'featured-image',
683                         title:      l10n.setFeaturedImageTitle,
684                         priority:   60,
685
686                         syncSelection: false
687                 }, media.controller.Library.prototype.defaults ),
688
689                 initialize: function() {
690                         var library, comparator;
691
692                         // If we haven't been provided a `library`, create a `Selection`.
693                         if ( ! this.get('library') )
694                                 this.set( 'library', media.query({ type: 'image' }) );
695
696                         media.controller.Library.prototype.initialize.apply( this, arguments );
697
698                         library    = this.get('library');
699                         comparator = library.comparator;
700
701                         // Overload the library's comparator to push items that are not in
702                         // the mirrored query to the front of the aggregate collection.
703                         library.comparator = function( a, b ) {
704                                 var aInQuery = !! this.mirroring.getByCid( a.cid ),
705                                         bInQuery = !! this.mirroring.getByCid( b.cid );
706
707                                 if ( ! aInQuery && bInQuery )
708                                         return -1;
709                                 else if ( aInQuery && ! bInQuery )
710                                         return 1;
711                                 else
712                                         return comparator.apply( this, arguments );
713                         };
714
715                         // Add all items in the selection to the library, so any featured
716                         // images that are not initially loaded still appear.
717                         library.observe( this.get('selection') );
718                 },
719
720                 activate: function() {
721                         this.updateSelection();
722                         this.frame.on( 'open', this.updateSelection, this );
723                         media.controller.Library.prototype.activate.apply( this, arguments );
724                 },
725
726                 deactivate: function() {
727                         this.frame.off( 'open', this.updateSelection, this );
728                         media.controller.Library.prototype.deactivate.apply( this, arguments );
729                 },
730
731                 updateSelection: function() {
732                         var selection = this.get('selection'),
733                                 id = media.view.settings.post.featuredImageId,
734                                 attachment;
735
736                         if ( '' !== id && -1 !== id ) {
737                                 attachment = Attachment.get( id );
738                                 attachment.fetch();
739                         }
740
741                         selection.reset( attachment ? [ attachment ] : [] );
742                 }
743         });
744
745
746         // wp.media.controller.Embed
747         // -------------------------
748         media.controller.Embed = media.controller.State.extend({
749                 defaults: {
750                         id:      'embed',
751                         url:     '',
752                         menu:    'default',
753                         content: 'embed',
754                         toolbar: 'main-embed',
755                         type:    'link',
756
757                         title:    l10n.insertFromUrlTitle,
758                         priority: 120
759                 },
760
761                 // The amount of time used when debouncing the scan.
762                 sensitivity: 200,
763
764                 initialize: function() {
765                         this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
766                         this.props = new Backbone.Model({ url: '' });
767                         this.props.on( 'change:url', this.debouncedScan, this );
768                         this.props.on( 'change:url', this.refresh, this );
769                         this.on( 'scan', this.scanImage, this );
770                 },
771
772                 scan: function() {
773                         var scanners,
774                                 embed = this,
775                                 attributes = {
776                                         type: 'link',
777                                         scanners: []
778                                 };
779
780                         // Scan is triggered with the list of `attributes` to set on the
781                         // state, useful for the 'type' attribute and 'scanners' attribute,
782                         // an array of promise objects for asynchronous scan operations.
783                         if ( this.props.get('url') )
784                                 this.trigger( 'scan', attributes );
785
786                         if ( attributes.scanners.length ) {
787                                 scanners = attributes.scanners = $.when.apply( $, attributes.scanners );
788                                 scanners.always( function() {
789                                         if ( embed.get('scanners') === scanners )
790                                                 embed.set( 'loading', false );
791                                 });
792                         } else {
793                                 attributes.scanners = null;
794                         }
795
796                         attributes.loading = !! attributes.scanners;
797                         this.set( attributes );
798                 },
799
800                 scanImage: function( attributes ) {
801                         var frame = this.frame,
802                                 state = this,
803                                 url = this.props.get('url'),
804                                 image = new Image(),
805                                 deferred = $.Deferred();
806
807                         attributes.scanners.push( deferred.promise() );
808
809                         // Try to load the image and find its width/height.
810                         image.onload = function() {
811                                 deferred.resolve();
812
813                                 if ( state !== frame.state() || url !== state.props.get('url') )
814                                         return;
815
816                                 state.set({
817                                         type: 'image'
818                                 });
819
820                                 state.props.set({
821                                         width:  image.width,
822                                         height: image.height
823                                 });
824                         };
825
826                         image.onerror = deferred.reject;
827                         image.src = url;
828                 },
829
830                 refresh: function() {
831                         this.frame.toolbar.get().refresh();
832                 },
833
834                 reset: function() {
835                         this.props.clear().set({ url: '' });
836
837                         if ( this.active )
838                                 this.refresh();
839                 }
840         });
841
842         /**
843          * ========================================================================
844          * VIEWS
845          * ========================================================================
846          */
847
848         // wp.media.Views
849         // -------------
850         //
851         // A subview manager.
852
853         media.Views = function( view, views ) {
854                 this.view = view;
855                 this._views = _.isArray( views ) ? { '': views } : views || {};
856         };
857
858         media.Views.extend = Backbone.Model.extend;
859
860         _.extend( media.Views.prototype, {
861                 // ### Fetch all of the subviews
862                 //
863                 // Returns an array of all subviews.
864                 all: function() {
865                         return _.flatten( this._views );
866                 },
867
868                 // ### Get a selector's subviews
869                 //
870                 // Fetches all subviews that match a given `selector`.
871                 //
872                 // If no `selector` is provided, it will grab all subviews attached
873                 // to the view's root.
874                 get: function( selector ) {
875                         selector = selector || '';
876                         return this._views[ selector ];
877                 },
878
879                 // ### Get a selector's first subview
880                 //
881                 // Fetches the first subview that matches a given `selector`.
882                 //
883                 // If no `selector` is provided, it will grab the first subview
884                 // attached to the view's root.
885                 //
886                 // Useful when a selector only has one subview at a time.
887                 first: function( selector ) {
888                         var views = this.get( selector );
889                         return views && views.length ? views[0] : null;
890                 },
891
892                 // ### Register subview(s)
893                 //
894                 // Registers any number of `views` to a `selector`.
895                 //
896                 // When no `selector` is provided, the root selector (the empty string)
897                 // is used. `views` accepts a `Backbone.View` instance or an array of
898                 // `Backbone.View` instances.
899                 //
900                 // ---
901                 //
902                 // Accepts an `options` object, which has a significant effect on the
903                 // resulting behavior.
904                 //
905                 // `options.silent` – *boolean, `false`*
906                 // > If `options.silent` is true, no DOM modifications will be made.
907                 //
908                 // `options.add` – *boolean, `false`*
909                 // > Use `Views.add()` as a shortcut for setting `options.add` to true.
910                 //
911                 // > By default, the provided `views` will replace
912                 // any existing views associated with the selector. If `options.add`
913                 // is true, the provided `views` will be added to the existing views.
914                 //
915                 // `options.at` – *integer, `undefined`*
916                 // > When adding, to insert `views` at a specific index, use
917                 // `options.at`. By default, `views` are added to the end of the array.
918                 set: function( selector, views, options ) {
919                         var existing, next;
920
921                         if ( ! _.isString( selector ) ) {
922                                 options  = views;
923                                 views    = selector;
924                                 selector = '';
925                         }
926
927                         options  = options || {};
928                         views    = _.isArray( views ) ? views : [ views ];
929                         existing = this.get( selector );
930                         next     = views;
931
932                         if ( existing ) {
933                                 if ( options.add ) {
934                                         if ( _.isUndefined( options.at ) ) {
935                                                 next = existing.concat( views );
936                                         } else {
937                                                 next = existing;
938                                                 next.splice.apply( next, [ options.at, 0 ].concat( views ) );
939                                         }
940                                 } else {
941                                         _.each( next, function( view ) {
942                                                 view.__detach = true;
943                                         });
944
945                                         _.each( existing, function( view ) {
946                                                 if ( view.__detach )
947                                                         view.$el.detach();
948                                                 else
949                                                         view.dispose();
950                                         });
951
952                                         _.each( next, function( view ) {
953                                                 delete view.__detach;
954                                         });
955                                 }
956                         }
957
958                         this._views[ selector ] = next;
959
960                         _.each( views, function( subview ) {
961                                 var constructor = subview.Views || media.Views,
962                                         subviews = subview.views = subview.views || new constructor( subview );
963                                 subviews.parent   = this.view;
964                                 subviews.selector = selector;
965                         }, this );
966
967                         if ( ! options.silent )
968                                 this._attach( selector, views, _.extend({ ready: this._isReady() }, options ) );
969
970                         return this;
971                 },
972
973                 // ### Add subview(s) to existing subviews
974                 //
975                 // An alias to `Views.set()`, which defaults `options.add` to true.
976                 //
977                 // Adds any number of `views` to a `selector`.
978                 //
979                 // When no `selector` is provided, the root selector (the empty string)
980                 // is used. `views` accepts a `Backbone.View` instance or an array of
981                 // `Backbone.View` instances.
982                 //
983                 // Use `Views.set()` when setting `options.add` to `false`.
984                 //
985                 // Accepts an `options` object. By default, provided `views` will be
986                 // inserted at the end of the array of existing views. To insert
987                 // `views` at a specific index, use `options.at`. If `options.silent`
988                 // is true, no DOM modifications will be made.
989                 //
990                 // For more information on the `options` object, see `Views.set()`.
991                 add: function( selector, views, options ) {
992                         if ( ! _.isString( selector ) ) {
993                                 options  = views;
994                                 views    = selector;
995                                 selector = '';
996                         }
997
998                         return this.set( selector, views, _.extend({ add: true }, options ) );
999                 },
1000
1001                 // ### Stop tracking subviews
1002                 //
1003                 // Stops tracking `views` registered to a `selector`. If no `views` are
1004                 // set, then all of the `selector`'s subviews will be unregistered and
1005                 // disposed.
1006                 //
1007                 // Accepts an `options` object. If `options.silent` is set, `dispose`
1008                 // will *not* be triggered on the unregistered views.
1009                 unset: function( selector, views, options ) {
1010                         var existing;
1011
1012                         if ( ! _.isString( selector ) ) {
1013                                 options = views;
1014                                 views = selector;
1015                                 selector = '';
1016                         }
1017
1018                         views = views || [];
1019
1020                         if ( existing = this.get( selector ) ) {
1021                                 views = _.isArray( views ) ? views : [ views ];
1022                                 this._views[ selector ] = views.length ? _.difference( existing, views ) : [];
1023                         }
1024
1025                         if ( ! options || ! options.silent )
1026                                 _.invoke( views, 'dispose' );
1027
1028                         return this;
1029                 },
1030
1031                 // ### Detach all subviews
1032                 //
1033                 // Detaches all subviews from the DOM.
1034                 //
1035                 // Helps to preserve all subview events when re-rendering the master
1036                 // view. Used in conjunction with `Views.render()`.
1037                 detach: function() {
1038                         $( _.pluck( this.all(), 'el' ) ).detach();
1039                         return this;
1040                 },
1041
1042                 // ### Render all subviews
1043                 //
1044                 // Renders all subviews. Used in conjunction with `Views.detach()`.
1045                 render: function() {
1046                         var options = {
1047                                         ready: this._isReady()
1048                                 };
1049
1050                         _.each( this._views, function( views, selector ) {
1051                                 this._attach( selector, views, options );
1052                         }, this );
1053
1054                         this.rendered = true;
1055                         return this;
1056                 },
1057
1058                 // ### Dispose all subviews
1059                 //
1060                 // Triggers the `dispose()` method on all subviews. Detaches the master
1061                 // view from its parent. Resets the internals of the views manager.
1062                 //
1063                 // Accepts an `options` object. If `options.silent` is set, `unset`
1064                 // will *not* be triggered on the master view's parent.
1065                 dispose: function( options ) {
1066                         if ( ! options || ! options.silent ) {
1067                                 if ( this.parent && this.parent.views )
1068                                         this.parent.views.unset( this.selector, this.view, { silent: true });
1069                                 delete this.parent;
1070                                 delete this.selector;
1071                         }
1072
1073                         _.invoke( this.all(), 'dispose' );
1074                         this._views = [];
1075                         return this;
1076                 },
1077
1078                 // ### Replace a selector's subviews
1079                 //
1080                 // By default, sets the `$target` selector's html to the subview `els`.
1081                 //
1082                 // Can be overridden in subclasses.
1083                 replace: function( $target, els ) {
1084                         $target.html( els );
1085                         return this;
1086                 },
1087
1088                 // ### Insert subviews into a selector
1089                 //
1090                 // By default, appends the subview `els` to the end of the `$target`
1091                 // selector. If `options.at` is set, inserts the subview `els` at the
1092                 // provided index.
1093                 //
1094                 // Can be overridden in subclasses.
1095                 insert: function( $target, els, options ) {
1096                         var at = options && options.at,
1097                                 $children;
1098
1099                         if ( _.isNumber( at ) && ($children = $target.children()).length > at )
1100                                 $children.eq( at ).before( els );
1101                         else
1102                                 $target.append( els );
1103
1104                         return this;
1105                 },
1106
1107                 // ### Trigger the ready event
1108                 //
1109                 // **Only use this method if you know what you're doing.**
1110                 // For performance reasons, this method does not check if the view is
1111                 // actually attached to the DOM. It's taking your word for it.
1112                 //
1113                 // Fires the ready event on the current view and all attached subviews.
1114                 ready: function() {
1115                         this.view.trigger('ready');
1116
1117                         // Find all attached subviews, and call ready on them.
1118                         _.chain( this.all() ).map( function( view ) {
1119                                 return view.views;
1120                         }).flatten().where({ attached: true }).invoke('ready');
1121                 },
1122
1123                 // #### Internal. Attaches a series of views to a selector.
1124                 //
1125                 // Checks to see if a matching selector exists, renders the views,
1126                 // performs the proper DOM operation, and then checks if the view is
1127                 // attached to the document.
1128                 _attach: function( selector, views, options ) {
1129                         var $selector = selector ? this.view.$( selector ) : this.view.$el,
1130                                 managers;
1131
1132                         // Check if we found a location to attach the views.
1133                         if ( ! $selector.length )
1134                                 return this;
1135
1136                         managers = _.chain( views ).pluck('views').flatten().value();
1137
1138                         // Render the views if necessary.
1139                         _.each( managers, function( manager ) {
1140                                 if ( manager.rendered )
1141                                         return;
1142
1143                                 manager.view.render();
1144                                 manager.rendered = true;
1145                         }, this );
1146
1147                         // Insert or replace the views.
1148                         this[ options.add ? 'insert' : 'replace' ]( $selector, _.pluck( views, 'el' ), options );
1149
1150                         // Set attached and trigger ready if the current view is already
1151                         // attached to the DOM.
1152                         _.each( managers, function( manager ) {
1153                                 manager.attached = true;
1154
1155                                 if ( options.ready )
1156                                         manager.ready();
1157                         }, this );
1158
1159                         return this;
1160                 },
1161
1162                 // #### Internal. Checks if the current view is in the DOM.
1163                 _isReady: function() {
1164                         var node = this.view.el;
1165                         while ( node ) {
1166                                 if ( node === document.body )
1167                                         return true;
1168                                 node = node.parentNode;
1169                         }
1170
1171                         return false;
1172                 }
1173         });
1174
1175         // wp.media.View
1176         // -------------
1177         //
1178         // The base view class.
1179         media.View = Backbone.View.extend({
1180                 // The constructor for the `Views` manager.
1181                 Views: media.Views,
1182
1183                 constructor: function( options ) {
1184                         this.views = new this.Views( this, this.views );
1185                         this.on( 'ready', this.ready, this );
1186
1187                         if ( options && options.controller )
1188                                 this.controller = options.controller;
1189
1190                         Backbone.View.apply( this, arguments );
1191                 },
1192
1193                 dispose: function() {
1194                         // Undelegating events, removing events from the model, and
1195                         // removing events from the controller mirror the code for
1196                         // `Backbone.View.dispose` in Backbone master.
1197                         this.undelegateEvents();
1198
1199                         if ( this.model && this.model.off )
1200                                 this.model.off( null, null, this );
1201
1202                         if ( this.collection && this.collection.off )
1203                                 this.collection.off( null, null, this );
1204
1205                         // Unbind controller events.
1206                         if ( this.controller && this.controller.off )
1207                                 this.controller.off( null, null, this );
1208
1209                         // Recursively dispose child views.
1210                         if ( this.views )
1211                                 this.views.dispose();
1212
1213                         return this;
1214                 },
1215
1216                 remove: function() {
1217                         this.dispose();
1218                         return Backbone.View.prototype.remove.apply( this, arguments );
1219                 },
1220
1221                 render: function() {
1222                         var options;
1223
1224                         if ( this.prepare )
1225                                 options = this.prepare();
1226
1227                         this.views.detach();
1228
1229                         if ( this.template ) {
1230                                 options = options || {};
1231                                 this.trigger( 'prepare', options );
1232                                 this.$el.html( this.template( options ) );
1233                         }
1234
1235                         this.views.render();
1236                         return this;
1237                 },
1238
1239                 prepare: function() {
1240                         return this.options;
1241                 },
1242
1243                 ready: function() {}
1244         });
1245
1246         /**
1247          * wp.media.view.Frame
1248          */
1249         media.view.Frame = media.View.extend({
1250                 initialize: function() {
1251                         this._createRegions();
1252                         this._createStates();
1253                 },
1254
1255                 _createRegions: function() {
1256                         // Clone the regions array.
1257                         this.regions = this.regions ? this.regions.slice() : [];
1258
1259                         // Initialize regions.
1260                         _.each( this.regions, function( region ) {
1261                                 this[ region ] = new media.controller.Region({
1262                                         view:     this,
1263                                         id:       region,
1264                                         selector: '.media-frame-' + region
1265                                 });
1266                         }, this );
1267                 },
1268
1269                 _createStates: function() {
1270                         // Create the default `states` collection.
1271                         this.states = new Backbone.Collection( null, {
1272                                 model: media.controller.State
1273                         });
1274
1275                         // Ensure states have a reference to the frame.
1276                         this.states.on( 'add', function( model ) {
1277                                 model.frame = this;
1278                                 model.trigger('ready');
1279                         }, this );
1280
1281                         if ( this.options.states )
1282                                 this.states.add( this.options.states );
1283                 },
1284
1285                 reset: function() {
1286                         this.states.invoke( 'trigger', 'reset' );
1287                         return this;
1288                 }
1289         });
1290
1291         // Make the `Frame` a `StateMachine`.
1292         _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
1293
1294         /**
1295          * wp.media.view.MediaFrame
1296          */
1297         media.view.MediaFrame = media.view.Frame.extend({
1298                 className: 'media-frame',
1299                 template:  media.template('media-frame'),
1300                 regions:   ['menu','title','content','toolbar','router'],
1301
1302                 initialize: function() {
1303                         media.view.Frame.prototype.initialize.apply( this, arguments );
1304
1305                         _.defaults( this.options, {
1306                                 title:    '',
1307                                 modal:    true,
1308                                 uploader: true
1309                         });
1310
1311                         // Ensure core UI is enabled.
1312                         this.$el.addClass('wp-core-ui');
1313
1314                         // Initialize modal container view.
1315                         if ( this.options.modal ) {
1316                                 this.modal = new media.view.Modal({
1317                                         controller: this,
1318                                         title:      this.options.title
1319                                 });
1320
1321                                 this.modal.content( this );
1322                         }
1323
1324                         // Force the uploader off if the upload limit has been exceeded or
1325                         // if the browser isn't supported.
1326                         if ( wp.Uploader.limitExceeded || ! wp.Uploader.browser.supported )
1327                                 this.options.uploader = false;
1328
1329                         // Initialize window-wide uploader.
1330                         if ( this.options.uploader ) {
1331                                 this.uploader = new media.view.UploaderWindow({
1332                                         controller: this,
1333                                         uploader: {
1334                                                 dropzone:  this.modal ? this.modal.$el : this.$el,
1335                                                 container: this.$el
1336                                         }
1337                                 });
1338                                 this.views.set( '.media-frame-uploader', this.uploader );
1339                         }
1340
1341                         this.on( 'attach', _.bind( this.views.ready, this.views ), this );
1342
1343                         // Bind default title creation.
1344                         this.on( 'title:create:default', this.createTitle, this );
1345                         this.title.mode('default');
1346
1347                         // Bind default menu.
1348                         this.on( 'menu:create:default', this.createMenu, this );
1349                 },
1350
1351                 render: function() {
1352                         // Activate the default state if no active state exists.
1353                         if ( ! this.state() && this.options.state )
1354                                 this.setState( this.options.state );
1355
1356                         return media.view.Frame.prototype.render.apply( this, arguments );
1357                 },
1358
1359                 createTitle: function( title ) {
1360                         title.view = new media.View({
1361                                 controller: this,
1362                                 tagName: 'h1'
1363                         });
1364                 },
1365
1366                 createMenu: function( menu ) {
1367                         menu.view = new media.view.Menu({
1368                                 controller: this
1369                         });
1370                 },
1371
1372                 createToolbar: function( toolbar ) {
1373                         toolbar.view = new media.view.Toolbar({
1374                                 controller: this
1375                         });
1376                 },
1377
1378                 createRouter: function( router ) {
1379                         router.view = new media.view.Router({
1380                                 controller: this
1381                         });
1382                 },
1383
1384                 createIframeStates: function( options ) {
1385                         var settings = media.view.settings,
1386                                 tabs = settings.tabs,
1387                                 tabUrl = settings.tabUrl,
1388                                 $postId;
1389
1390                         if ( ! tabs || ! tabUrl )
1391                                 return;
1392
1393                         // Add the post ID to the tab URL if it exists.
1394                         $postId = $('#post_ID');
1395                         if ( $postId.length )
1396                                 tabUrl += '&post_id=' + $postId.val();
1397
1398                         // Generate the tab states.
1399                         _.each( tabs, function( title, id ) {
1400                                 var frame = this.state( 'iframe:' + id ).set( _.defaults({
1401                                         tab:     id,
1402                                         src:     tabUrl + '&tab=' + id,
1403                                         title:   title,
1404                                         content: 'iframe',
1405                                         menu:    'default'
1406                                 }, options ) );
1407                         }, this );
1408
1409                         this.on( 'content:create:iframe', this.iframeContent, this );
1410                         this.on( 'menu:render:default', this.iframeMenu, this );
1411                         this.on( 'open', this.hijackThickbox, this );
1412                         this.on( 'close', this.restoreThickbox, this );
1413                 },
1414
1415                 iframeContent: function( content ) {
1416                         this.$el.addClass('hide-toolbar');
1417                         content.view = new media.view.Iframe({
1418                                 controller: this
1419                         });
1420                 },
1421
1422                 iframeMenu: function( view ) {
1423                         var views = {};
1424
1425                         if ( ! view )
1426                                 return;
1427
1428                         _.each( media.view.settings.tabs, function( title, id ) {
1429                                 views[ 'iframe:' + id ] = {
1430                                         text: this.state( 'iframe:' + id ).get('title'),
1431                                         priority: 200
1432                                 };
1433                         }, this );
1434
1435                         view.set( views );
1436                 },
1437
1438                 hijackThickbox: function() {
1439                         var frame = this;
1440
1441                         if ( ! window.tb_remove || this._tb_remove )
1442                                 return;
1443
1444                         this._tb_remove = window.tb_remove;
1445                         window.tb_remove = function() {
1446                                 frame.close();
1447                                 frame.reset();
1448                                 frame.setState( frame.options.state );
1449                                 frame._tb_remove.call( window );
1450                         };
1451                 },
1452
1453                 restoreThickbox: function() {
1454                         if ( ! this._tb_remove )
1455                                 return;
1456
1457                         window.tb_remove = this._tb_remove;
1458                         delete this._tb_remove;
1459                 }
1460         });
1461
1462         // Map some of the modal's methods to the frame.
1463         _.each(['open','close','attach','detach','escape'], function( method ) {
1464                 media.view.MediaFrame.prototype[ method ] = function( view ) {
1465                         if ( this.modal )
1466                                 this.modal[ method ].apply( this.modal, arguments );
1467                         return this;
1468                 };
1469         });
1470
1471         /**
1472          * wp.media.view.MediaFrame.Select
1473          */
1474         media.view.MediaFrame.Select = media.view.MediaFrame.extend({
1475                 initialize: function() {
1476                         media.view.MediaFrame.prototype.initialize.apply( this, arguments );
1477
1478                         _.defaults( this.options, {
1479                                 selection: [],
1480                                 library:   {},
1481                                 multiple:  false,
1482                                 state:    'library'
1483                         });
1484
1485                         this.createSelection();
1486                         this.createStates();
1487                         this.bindHandlers();
1488                 },
1489
1490                 createSelection: function() {
1491                         var controller = this,
1492                                 selection = this.options.selection;
1493
1494                         if ( ! (selection instanceof media.model.Selection) ) {
1495                                 this.options.selection = new media.model.Selection( selection, {
1496                                         multiple: this.options.multiple
1497                                 });
1498                         }
1499
1500                         this._selection = {
1501                                 attachments: new Attachments(),
1502                                 difference: []
1503                         };
1504                 },
1505
1506                 createStates: function() {
1507                         var options = this.options;
1508
1509                         if ( this.options.states )
1510                                 return;
1511
1512                         // Add the default states.
1513                         this.states.add([
1514                                 // Main states.
1515                                 new media.controller.Library({
1516                                         library:   media.query( options.library ),
1517                                         multiple:  options.multiple,
1518                                         title:     options.title,
1519                                         priority:  20
1520                                 })
1521                         ]);
1522                 },
1523
1524                 bindHandlers: function() {
1525                         this.on( 'router:create:browse', this.createRouter, this );
1526                         this.on( 'router:render:browse', this.browseRouter, this );
1527                         this.on( 'content:create:browse', this.browseContent, this );
1528                         this.on( 'content:render:upload', this.uploadContent, this );
1529                         this.on( 'toolbar:create:select', this.createSelectToolbar, this );
1530                 },
1531
1532                 // Routers
1533                 browseRouter: function( view ) {
1534                         view.set({
1535                                 upload: {
1536                                         text:     l10n.uploadFilesTitle,
1537                                         priority: 20
1538                                 },
1539                                 browse: {
1540                                         text:     l10n.mediaLibraryTitle,
1541                                         priority: 40
1542                                 }
1543                         });
1544                 },
1545
1546                 // Content
1547                 browseContent: function( content ) {
1548                         var state = this.state();
1549
1550                         this.$el.removeClass('hide-toolbar');
1551
1552                         // Browse our library of attachments.
1553                         content.view = new media.view.AttachmentsBrowser({
1554                                 controller: this,
1555                                 collection: state.get('library'),
1556                                 selection:  state.get('selection'),
1557                                 model:      state,
1558                                 sortable:   state.get('sortable'),
1559                                 search:     state.get('searchable'),
1560                                 filters:    state.get('filterable'),
1561                                 display:    state.get('displaySettings'),
1562                                 dragInfo:   state.get('dragInfo'),
1563
1564                                 AttachmentView: state.get('AttachmentView')
1565                         });
1566                 },
1567
1568                 uploadContent: function() {
1569                         this.$el.removeClass('hide-toolbar');
1570                         this.content.set( new media.view.UploaderInline({
1571                                 controller: this
1572                         }) );
1573                 },
1574
1575                 // Toolbars
1576                 createSelectToolbar: function( toolbar, options ) {
1577                         options = options || this.options.button || {};
1578                         options.controller = this;
1579
1580                         toolbar.view = new media.view.Toolbar.Select( options );
1581                 }
1582         });
1583
1584         /**
1585          * wp.media.view.MediaFrame.Post
1586          */
1587         media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
1588                 initialize: function() {
1589                         _.defaults( this.options, {
1590                                 multiple:  true,
1591                                 editing:   false,
1592                                 state:    'insert'
1593                         });
1594
1595                         media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
1596                         this.createIframeStates();
1597                 },
1598
1599                 createStates: function() {
1600                         var options = this.options;
1601
1602                         // Add the default states.
1603                         this.states.add([
1604                                 // Main states.
1605                                 new media.controller.Library({
1606                                         id:         'insert',
1607                                         title:      l10n.insertMediaTitle,
1608                                         priority:   20,
1609                                         toolbar:    'main-insert',
1610                                         filterable: 'all',
1611                                         library:    media.query( options.library ),
1612                                         multiple:   options.multiple ? 'reset' : false,
1613                                         editable:   true,
1614
1615                                         // If the user isn't allowed to edit fields,
1616                                         // can they still edit it locally?
1617                                         allowLocalEdits: true,
1618
1619                                         // Show the attachment display settings.
1620                                         displaySettings: true,
1621                                         // Update user settings when users adjust the
1622                                         // attachment display settings.
1623                                         displayUserSettings: true
1624                                 }),
1625
1626                                 new media.controller.Library({
1627                                         id:         'gallery',
1628                                         title:      l10n.createGalleryTitle,
1629                                         priority:   40,
1630                                         toolbar:    'main-gallery',
1631                                         filterable: 'uploaded',
1632                                         multiple:   'add',
1633                                         editable:   false,
1634
1635                                         library:  media.query( _.defaults({
1636                                                 type: 'image'
1637                                         }, options.library ) )
1638                                 }),
1639
1640                                 // Embed states.
1641                                 new media.controller.Embed(),
1642
1643                                 // Gallery states.
1644                                 new media.controller.GalleryEdit({
1645                                         library: options.selection,
1646                                         editing: options.editing,
1647                                         menu:    'gallery'
1648                                 }),
1649
1650                                 new media.controller.GalleryAdd()
1651                         ]);
1652
1653
1654                         if ( media.view.settings.post.featuredImageId ) {
1655                                 this.states.add( new media.controller.FeaturedImage() );
1656                         }
1657                 },
1658
1659                 bindHandlers: function() {
1660                         media.view.MediaFrame.Select.prototype.bindHandlers.apply( this, arguments );
1661                         this.on( 'menu:create:gallery', this.createMenu, this );
1662                         this.on( 'toolbar:create:main-insert', this.createToolbar, this );
1663                         this.on( 'toolbar:create:main-gallery', this.createToolbar, this );
1664                         this.on( 'toolbar:create:featured-image', this.featuredImageToolbar, this );
1665                         this.on( 'toolbar:create:main-embed', this.mainEmbedToolbar, this );
1666
1667                         var handlers = {
1668                                         menu: {
1669                                                 'default': 'mainMenu',
1670                                                 'gallery': 'galleryMenu'
1671                                         },
1672
1673                                         content: {
1674                                                 'embed':          'embedContent',
1675                                                 'edit-selection': 'editSelectionContent'
1676                                         },
1677
1678                                         toolbar: {
1679                                                 'main-insert':      'mainInsertToolbar',
1680                                                 'main-gallery':     'mainGalleryToolbar',
1681                                                 'gallery-edit':     'galleryEditToolbar',
1682                                                 'gallery-add':      'galleryAddToolbar'
1683                                         }
1684                                 };
1685
1686                         _.each( handlers, function( regionHandlers, region ) {
1687                                 _.each( regionHandlers, function( callback, handler ) {
1688                                         this.on( region + ':render:' + handler, this[ callback ], this );
1689                                 }, this );
1690                         }, this );
1691                 },
1692
1693                 // Menus
1694                 mainMenu: function( view ) {
1695                         view.set({
1696                                 'library-separator': new media.View({
1697                                         className: 'separator',
1698                                         priority: 100
1699                                 })
1700                         });
1701                 },
1702
1703                 galleryMenu: function( view ) {
1704                         var lastState = this.lastState(),
1705                                 previous = lastState && lastState.id,
1706                                 frame = this;
1707
1708                         view.set({
1709                                 cancel: {
1710                                         text:     l10n.cancelGalleryTitle,
1711                                         priority: 20,
1712                                         click:    function() {
1713                                                 if ( previous )
1714                                                         frame.setState( previous );
1715                                                 else
1716                                                         frame.close();
1717                                         }
1718                                 },
1719                                 separateCancel: new media.View({
1720                                         className: 'separator',
1721                                         priority: 40
1722                                 })
1723                         });
1724                 },
1725
1726                 // Content
1727                 embedContent: function() {
1728                         var view = new media.view.Embed({
1729                                 controller: this,
1730                                 model:      this.state()
1731                         }).render();
1732
1733                         this.content.set( view );
1734                         view.url.focus();
1735                 },
1736
1737                 editSelectionContent: function() {
1738                         var state = this.state(),
1739                                 selection = state.get('selection'),
1740                                 view;
1741
1742                         view = new media.view.AttachmentsBrowser({
1743                                 controller: this,
1744                                 collection: selection,
1745                                 selection:  selection,
1746                                 model:      state,
1747                                 sortable:   true,
1748                                 search:     false,
1749                                 dragInfo:   true,
1750
1751                                 AttachmentView: media.view.Attachment.EditSelection
1752                         }).render();
1753
1754                         view.toolbar.set( 'backToLibrary', {
1755                                 text:     l10n.returnToLibrary,
1756                                 priority: -100,
1757
1758                                 click: function() {
1759                                         this.controller.content.mode('browse');
1760                                 }
1761                         });
1762
1763                         // Browse our library of attachments.
1764                         this.content.set( view );
1765                 },
1766
1767                 // Toolbars
1768                 selectionStatusToolbar: function( view ) {
1769                         var editable = this.state().get('editable');
1770
1771                         view.set( 'selection', new media.view.Selection({
1772                                 controller: this,
1773                                 collection: this.state().get('selection'),
1774                                 priority:   -40,
1775
1776                                 // If the selection is editable, pass the callback to
1777                                 // switch the content mode.
1778                                 editable: editable && function() {
1779                                         this.controller.content.mode('edit-selection');
1780                                 }
1781                         }).render() );
1782                 },
1783
1784                 mainInsertToolbar: function( view ) {
1785                         var controller = this;
1786
1787                         this.selectionStatusToolbar( view );
1788
1789                         view.set( 'insert', {
1790                                 style:    'primary',
1791                                 priority: 80,
1792                                 text:     l10n.insertIntoPost,
1793                                 requires: { selection: true },
1794
1795                                 click: function() {
1796                                         var state = controller.state(),
1797                                                 selection = state.get('selection');
1798
1799                                         controller.close();
1800                                         state.trigger( 'insert', selection ).reset();
1801                                 }
1802                         });
1803                 },
1804
1805                 mainGalleryToolbar: function( view ) {
1806                         var controller = this;
1807
1808                         this.selectionStatusToolbar( view );
1809
1810                         view.set( 'gallery', {
1811                                 style:    'primary',
1812                                 text:     l10n.createNewGallery,
1813                                 priority: 60,
1814                                 requires: { selection: true },
1815
1816                                 click: function() {
1817                                         var selection = controller.state().get('selection'),
1818                                                 edit = controller.state('gallery-edit'),
1819                                                 models = selection.where({ type: 'image' });
1820
1821                                         edit.set( 'library', new media.model.Selection( models, {
1822                                                 props:    selection.props.toJSON(),
1823                                                 multiple: true
1824                                         }) );
1825
1826                                         this.controller.setState('gallery-edit');
1827                                 }
1828                         });
1829                 },
1830
1831                 featuredImageToolbar: function( toolbar ) {
1832                         this.createSelectToolbar( toolbar, {
1833                                 text:  l10n.setFeaturedImage,
1834                                 state: this.options.state || 'upload'
1835                         });
1836                 },
1837
1838                 mainEmbedToolbar: function( toolbar ) {
1839                         toolbar.view = new media.view.Toolbar.Embed({
1840                                 controller: this
1841                         });
1842                 },
1843
1844                 galleryEditToolbar: function() {
1845                         var editing = this.state().get('editing');
1846                         this.toolbar.set( new media.view.Toolbar({
1847                                 controller: this,
1848                                 items: {
1849                                         insert: {
1850                                                 style:    'primary',
1851                                                 text:     editing ? l10n.updateGallery : l10n.insertGallery,
1852                                                 priority: 80,
1853                                                 requires: { library: true },
1854
1855                                                 click: function() {
1856                                                         var controller = this.controller,
1857                                                                 state = controller.state();
1858
1859                                                         controller.close();
1860                                                         state.trigger( 'update', state.get('library') );
1861
1862                                                         controller.reset();
1863                                                         // @todo: Make the state activated dynamic (instead of hardcoded).
1864                                                         controller.setState('upload');
1865                                                 }
1866                                         }
1867                                 }
1868                         }) );
1869                 },
1870
1871                 galleryAddToolbar: function() {
1872                         this.toolbar.set( new media.view.Toolbar({
1873                                 controller: this,
1874                                 items: {
1875                                         insert: {
1876                                                 style:    'primary',
1877                                                 text:     l10n.addToGallery,
1878                                                 priority: 80,
1879                                                 requires: { selection: true },
1880
1881                                                 click: function() {
1882                                                         var controller = this.controller,
1883                                                                 state = controller.state(),
1884                                                                 edit = controller.state('gallery-edit');
1885
1886                                                         edit.get('library').add( state.get('selection').models );
1887                                                         state.trigger('reset');
1888                                                         controller.setState('gallery-edit');
1889                                                 }
1890                                         }
1891                                 }
1892                         }) );
1893                 }
1894         });
1895
1896         /**
1897          * wp.media.view.Modal
1898          */
1899         media.view.Modal = media.View.extend({
1900                 tagName:  'div',
1901                 template: media.template('media-modal'),
1902
1903                 attributes: {
1904                         tabindex: 0
1905                 },
1906
1907                 events: {
1908                         'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
1909                         'keydown': 'keydown'
1910                 },
1911
1912                 initialize: function() {
1913                         _.defaults( this.options, {
1914                                 container: document.body,
1915                                 title:     '',
1916                                 propagate: true,
1917                                 freeze:    true
1918                         });
1919                 },
1920
1921                 prepare: function() {
1922                         return {
1923                                 title: this.options.title
1924                         };
1925                 },
1926
1927                 attach: function() {
1928                         if ( this.views.attached )
1929                                 return this;
1930
1931                         if ( ! this.views.rendered )
1932                                 this.render();
1933
1934                         this.$el.appendTo( this.options.container );
1935
1936                         // Manually mark the view as attached and trigger ready.
1937                         this.views.attached = true;
1938                         this.views.ready();
1939
1940                         return this.propagate('attach');
1941                 },
1942
1943                 detach: function() {
1944                         if ( this.$el.is(':visible') )
1945                                 this.close();
1946
1947                         this.$el.detach();
1948                         this.views.attached = false;
1949                         return this.propagate('detach');
1950                 },
1951
1952                 open: function() {
1953                         var $el = this.$el,
1954                                 options = this.options;
1955
1956                         if ( $el.is(':visible') )
1957                                 return this;
1958
1959                         if ( ! this.views.attached )
1960                                 this.attach();
1961
1962                         // If the `freeze` option is set, record the window's scroll position.
1963                         if ( options.freeze ) {
1964                                 this._freeze = {
1965                                         scrollTop: $( window ).scrollTop()
1966                                 };
1967                         }
1968
1969                         $el.show().focus();
1970                         return this.propagate('open');
1971                 },
1972
1973                 close: function( options ) {
1974                         var freeze = this._freeze;
1975
1976                         if ( ! this.views.attached || ! this.$el.is(':visible') )
1977                                 return this;
1978
1979                         this.$el.hide();
1980                         this.propagate('close');
1981
1982                         // If the `freeze` option is set, restore the container's scroll position.
1983                         if ( freeze ) {
1984                                 $( window ).scrollTop( freeze.scrollTop );
1985                         }
1986
1987                         if ( options && options.escape )
1988                                 this.propagate('escape');
1989
1990                         return this;
1991                 },
1992
1993                 escape: function() {
1994                         return this.close({ escape: true });
1995                 },
1996
1997                 escapeHandler: function( event ) {
1998                         event.preventDefault();
1999                         this.escape();
2000                 },
2001
2002                 content: function( content ) {
2003                         this.views.set( '.media-modal-content', content );
2004                         return this;
2005                 },
2006
2007                 // Triggers a modal event and if the `propagate` option is set,
2008                 // forwards events to the modal's controller.
2009                 propagate: function( id ) {
2010                         this.trigger( id );
2011
2012                         if ( this.options.propagate )
2013                                 this.controller.trigger( id );
2014
2015                         return this;
2016                 },
2017
2018                 keydown: function( event ) {
2019                         // Close the modal when escape is pressed.
2020                         if ( 27 === event.which ) {
2021                                 event.preventDefault();
2022                                 this.escape();
2023                                 return;
2024                         }
2025                 }
2026         });
2027
2028         // wp.media.view.FocusManager
2029         // ----------------------------
2030         media.view.FocusManager = media.View.extend({
2031                 events: {
2032                         keydown: 'recordTab',
2033                         focusin: 'updateIndex'
2034                 },
2035
2036                 focus: function() {
2037                         if ( _.isUndefined( this.index ) )
2038                                 return;
2039
2040                         // Update our collection of `$tabbables`.
2041                         this.$tabbables = this.$(':tabbable');
2042
2043                         // If tab is saved, focus it.
2044                         this.$tabbables.eq( this.index ).focus();
2045                 },
2046
2047                 recordTab: function( event ) {
2048                         // Look for the tab key.
2049                         if ( 9 !== event.keyCode )
2050                                 return;
2051
2052                         // First try to update the index.
2053                         if ( _.isUndefined( this.index ) )
2054                                 this.updateIndex( event );
2055
2056                         // If we still don't have an index, bail.
2057                         if ( _.isUndefined( this.index ) )
2058                                 return;
2059
2060                         var index = this.index + ( event.shiftKey ? -1 : 1 );
2061
2062                         if ( index >= 0 && index < this.$tabbables.length )
2063                                 this.index = index;
2064                         else
2065                                 delete this.index;
2066                 },
2067
2068                 updateIndex: function( event ) {
2069                         this.$tabbables = this.$(':tabbable');
2070
2071                         var index = this.$tabbables.index( event.target );
2072
2073                         if ( -1 === index )
2074                                 delete this.index;
2075                         else
2076                                 this.index = index;
2077                 }
2078         });
2079
2080         // wp.media.view.UploaderWindow
2081         // ----------------------------
2082         media.view.UploaderWindow = media.View.extend({
2083                 tagName:   'div',
2084                 className: 'uploader-window',
2085                 template:  media.template('uploader-window'),
2086
2087                 initialize: function() {
2088                         var uploader;
2089
2090                         this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
2091
2092                         uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
2093                                 dropzone:  this.$el,
2094                                 browser:   this.$browser,
2095                                 params:    {}
2096                         });
2097
2098                         // Ensure the dropzone is a jQuery collection.
2099                         if ( uploader.dropzone && ! (uploader.dropzone instanceof $) )
2100                                 uploader.dropzone = $( uploader.dropzone );
2101
2102                         this.controller.on( 'activate', this.refresh, this );
2103                 },
2104
2105                 refresh: function() {
2106                         if ( this.uploader )
2107                                 this.uploader.refresh();
2108                 },
2109
2110                 ready: function() {
2111                         var postId = media.view.settings.post.id,
2112                                 dropzone;
2113
2114                         // If the uploader already exists, bail.
2115                         if ( this.uploader )
2116                                 return;
2117
2118                         if ( postId )
2119                                 this.options.uploader.params.post_id = postId;
2120
2121                         this.uploader = new wp.Uploader( this.options.uploader );
2122
2123                         dropzone = this.uploader.dropzone;
2124                         dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
2125                         dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
2126                 },
2127
2128                 show: function() {
2129                         var $el = this.$el.show();
2130
2131                         // Ensure that the animation is triggered by waiting until
2132                         // the transparent element is painted into the DOM.
2133                         _.defer( function() {
2134                                 $el.css({ opacity: 1 });
2135                         });
2136                 },
2137
2138                 hide: function() {
2139                         var $el = this.$el.css({ opacity: 0 });
2140
2141                         media.transition( $el ).done( function() {
2142                                 // Transition end events are subject to race conditions.
2143                                 // Make sure that the value is set as intended.
2144                                 if ( '0' === $el.css('opacity') )
2145                                         $el.hide();
2146                         });
2147                 }
2148         });
2149
2150         media.view.UploaderInline = media.View.extend({
2151                 tagName:   'div',
2152                 className: 'uploader-inline',
2153                 template:  media.template('uploader-inline'),
2154
2155                 initialize: function() {
2156                         _.defaults( this.options, {
2157                                 message: '',
2158                                 status:  true
2159                         });
2160
2161                         if ( ! this.options.$browser && this.controller.uploader )
2162                                 this.options.$browser = this.controller.uploader.$browser;
2163
2164                         if ( _.isUndefined( this.options.postId ) )
2165                                 this.options.postId = media.view.settings.post.id;
2166
2167                         if ( this.options.status ) {
2168                                 this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
2169                                         controller: this.controller
2170                                 }) );
2171                         }
2172                 },
2173
2174                 dispose: function() {
2175                         if ( this.disposing )
2176                                 return media.View.prototype.dispose.apply( this, arguments );
2177
2178                         // Run remove on `dispose`, so we can be sure to refresh the
2179                         // uploader with a view-less DOM. Track whether we're disposing
2180                         // so we don't trigger an infinite loop.
2181                         this.disposing = true;
2182                         return this.remove();
2183                 },
2184
2185                 remove: function() {
2186                         var result = media.View.prototype.remove.apply( this, arguments );
2187
2188                         _.defer( _.bind( this.refresh, this ) );
2189                         return result;
2190                 },
2191
2192                 refresh: function() {
2193                         var uploader = this.controller.uploader;
2194
2195                         if ( uploader )
2196                                 uploader.refresh();
2197                 },
2198
2199                 ready: function() {
2200                         var $browser = this.options.$browser,
2201                                 $placeholder;
2202
2203                         if ( this.controller.uploader ) {
2204                                 $placeholder = this.$('.browser');
2205
2206                                 // Check if we've already replaced the placeholder.
2207                                 if ( $placeholder[0] === $browser[0] )
2208                                         return;
2209
2210                                 $browser.detach().text( $placeholder.text() );
2211                                 $browser[0].className = $placeholder[0].className;
2212                                 $placeholder.replaceWith( $browser.show() );
2213                         }
2214
2215                         this.refresh();
2216                         return this;
2217                 }
2218         });
2219
2220         /**
2221          * wp.media.view.UploaderStatus
2222          */
2223         media.view.UploaderStatus = media.View.extend({
2224                 className: 'media-uploader-status',
2225                 template:  media.template('uploader-status'),
2226
2227                 events: {
2228                         'click .upload-dismiss-errors': 'dismiss'
2229                 },
2230
2231                 initialize: function() {
2232                         this.queue = wp.Uploader.queue;
2233                         this.queue.on( 'add remove reset', this.visibility, this );
2234                         this.queue.on( 'add remove reset change:percent', this.progress, this );
2235                         this.queue.on( 'add remove reset change:uploading', this.info, this );
2236
2237                         this.errors = wp.Uploader.errors;
2238                         this.errors.reset();
2239                         this.errors.on( 'add remove reset', this.visibility, this );
2240                         this.errors.on( 'add', this.error, this );
2241                 },
2242
2243                 dispose: function() {
2244                         wp.Uploader.queue.off( null, null, this );
2245                         media.View.prototype.dispose.apply( this, arguments );
2246                         return this;
2247                 },
2248
2249                 visibility: function() {
2250                         this.$el.toggleClass( 'uploading', !! this.queue.length );
2251                         this.$el.toggleClass( 'errors', !! this.errors.length );
2252                         this.$el.toggle( !! this.queue.length || !! this.errors.length );
2253                 },
2254
2255                 ready: function() {
2256                         _.each({
2257                                 '$bar':      '.media-progress-bar div',
2258                                 '$index':    '.upload-index',
2259                                 '$total':    '.upload-total',
2260                                 '$filename': '.upload-filename'
2261                         }, function( selector, key ) {
2262                                 this[ key ] = this.$( selector );
2263                         }, this );
2264
2265                         this.visibility();
2266                         this.progress();
2267                         this.info();
2268                 },
2269
2270                 progress: function() {
2271                         var queue = this.queue,
2272                                 $bar = this.$bar,
2273                                 memo = 0;
2274
2275                         if ( ! $bar || ! queue.length )
2276                                 return;
2277
2278                         $bar.width( ( queue.reduce( function( memo, attachment ) {
2279                                 if ( ! attachment.get('uploading') )
2280                                         return memo + 100;
2281
2282                                 var percent = attachment.get('percent');
2283                                 return memo + ( _.isNumber( percent ) ? percent : 100 );
2284                         }, 0 ) / queue.length ) + '%' );
2285                 },
2286
2287                 info: function() {
2288                         var queue = this.queue,
2289                                 index = 0, active;
2290
2291                         if ( ! queue.length )
2292                                 return;
2293
2294                         active = this.queue.find( function( attachment, i ) {
2295                                 index = i;
2296                                 return attachment.get('uploading');
2297                         });
2298
2299                         this.$index.text( index + 1 );
2300                         this.$total.text( queue.length );
2301                         this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
2302                 },
2303
2304                 filename: function( filename ) {
2305                         return media.truncate( _.escape( filename ), 24 );
2306                 },
2307
2308                 error: function( error ) {
2309                         this.views.add( '.upload-errors', new media.view.UploaderStatusError({
2310                                 filename: this.filename( error.get('file').name ),
2311                                 message:  error.get('message')
2312                         }), { at: 0 });
2313                 },
2314
2315                 dismiss: function( event ) {
2316                         var errors = this.views.get('.upload-errors');
2317
2318                         event.preventDefault();
2319
2320                         if ( errors )
2321                                 _.invoke( errors, 'remove' );
2322                         wp.Uploader.errors.reset();
2323                 }
2324         });
2325
2326         media.view.UploaderStatusError = media.View.extend({
2327                 className: 'upload-error',
2328                 template:  media.template('uploader-status-error')
2329         });
2330
2331         /**
2332          * wp.media.view.Toolbar
2333          */
2334         media.view.Toolbar = media.View.extend({
2335                 tagName:   'div',
2336                 className: 'media-toolbar',
2337
2338                 initialize: function() {
2339                         var state = this.controller.state(),
2340                                 selection = this.selection = state.get('selection'),
2341                                 library = this.library = state.get('library');
2342
2343                         this._views = {};
2344
2345                         // The toolbar is composed of two `PriorityList` views.
2346                         this.primary   = new media.view.PriorityList();
2347                         this.secondary = new media.view.PriorityList();
2348                         this.primary.$el.addClass('media-toolbar-primary');
2349                         this.secondary.$el.addClass('media-toolbar-secondary');
2350
2351                         this.views.set([ this.secondary, this.primary ]);
2352
2353                         if ( this.options.items )
2354                                 this.set( this.options.items, { silent: true });
2355
2356                         if ( ! this.options.silent )
2357                                 this.render();
2358
2359                         if ( selection )
2360                                 selection.on( 'add remove reset', this.refresh, this );
2361                         if ( library )
2362                                 library.on( 'add remove reset', this.refresh, this );
2363                 },
2364
2365                 dispose: function() {
2366                         if ( this.selection )
2367                                 this.selection.off( null, null, this );
2368                         if ( this.library )
2369                                 this.library.off( null, null, this );
2370                         return media.View.prototype.dispose.apply( this, arguments );
2371                 },
2372
2373                 ready: function() {
2374                         this.refresh();
2375                 },
2376
2377                 set: function( id, view, options ) {
2378                         var list;
2379                         options = options || {};
2380
2381                         // Accept an object with an `id` : `view` mapping.
2382                         if ( _.isObject( id ) ) {
2383                                 _.each( id, function( view, id ) {
2384                                         this.set( id, view, { silent: true });
2385                                 }, this );
2386
2387                         } else {
2388                                 if ( ! ( view instanceof Backbone.View ) ) {
2389                                         view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
2390                                         view = new media.view.Button( view ).render();
2391                                 }
2392
2393                                 view.controller = view.controller || this.controller;
2394
2395                                 this._views[ id ] = view;
2396
2397                                 list = view.options.priority < 0 ? 'secondary' : 'primary';
2398                                 this[ list ].set( id, view, options );
2399                         }
2400
2401                         if ( ! options.silent )
2402                                 this.refresh();
2403
2404                         return this;
2405                 },
2406
2407                 get: function( id ) {
2408                         return this._views[ id ];
2409                 },
2410
2411                 unset: function( id, options ) {
2412                         delete this._views[ id ];
2413                         this.primary.unset( id, options );
2414                         this.secondary.unset( id, options );
2415
2416                         if ( ! options || ! options.silent )
2417                                 this.refresh();
2418                         return this;
2419                 },
2420
2421                 refresh: function() {
2422                         var state = this.controller.state(),
2423                                 library = state.get('library'),
2424                                 selection = state.get('selection');
2425
2426                         _.each( this._views, function( button ) {
2427                                 if ( ! button.model || ! button.options || ! button.options.requires )
2428                                         return;
2429
2430                                 var requires = button.options.requires,
2431                                         disabled = false;
2432
2433                                 if ( requires.selection && selection && ! selection.length )
2434                                         disabled = true;
2435                                 else if ( requires.library && library && ! library.length )
2436                                         disabled = true;
2437
2438                                 button.model.set( 'disabled', disabled );
2439                         });
2440                 }
2441         });
2442
2443         // wp.media.view.Toolbar.Select
2444         // ----------------------------
2445         media.view.Toolbar.Select = media.view.Toolbar.extend({
2446                 initialize: function() {
2447                         var options = this.options,
2448                                 controller = options.controller,
2449                                 selection = controller.state().get('selection');
2450
2451                         _.bindAll( this, 'clickSelect' );
2452
2453                         _.defaults( options, {
2454                                 event: 'select',
2455                                 state: false,
2456                                 reset: true,
2457                                 close: true,
2458                                 text:  l10n.select,
2459
2460                                 // Does the button rely on the selection?
2461                                 requires: {
2462                                         selection: true
2463                                 }
2464                         });
2465
2466                         options.items = _.defaults( options.items || {}, {
2467                                 select: {
2468                                         style:    'primary',
2469                                         text:     options.text,
2470                                         priority: 80,
2471                                         click:    this.clickSelect,
2472                                         requires: options.requires
2473                                 }
2474                         });
2475
2476                         media.view.Toolbar.prototype.initialize.apply( this, arguments );
2477                 },
2478
2479                 clickSelect: function() {
2480                         var options = this.options,
2481                                 controller = this.controller;
2482
2483                         if ( options.close )
2484                                 controller.close();
2485
2486                         if ( options.event )
2487                                 controller.state().trigger( options.event );
2488
2489                         if ( options.reset )
2490                                 controller.reset();
2491
2492                         if ( options.state )
2493                                 controller.setState( options.state );
2494                 }
2495         });
2496
2497         // wp.media.view.Toolbar.Embed
2498         // ---------------------------
2499         media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
2500                 initialize: function() {
2501                         _.defaults( this.options, {
2502                                 text: l10n.insertIntoPost,
2503                                 requires: false
2504                         });
2505
2506                         media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
2507                 },
2508
2509                 refresh: function() {
2510                         var url = this.controller.state().props.get('url');
2511                         this.get('select').model.set( 'disabled', ! url || url === 'http://' );
2512
2513                         media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
2514                 }
2515         });
2516
2517         /**
2518          * wp.media.view.Button
2519          */
2520         media.view.Button = media.View.extend({
2521                 tagName:    'a',
2522                 className:  'media-button',
2523                 attributes: { href: '#' },
2524
2525                 events: {
2526                         'click': 'click'
2527                 },
2528
2529                 defaults: {
2530                         text:     '',
2531                         style:    '',
2532                         size:     'large',
2533                         disabled: false
2534                 },
2535
2536                 initialize: function() {
2537                         // Create a model with the provided `defaults`.
2538                         this.model = new Backbone.Model( this.defaults );
2539
2540                         // If any of the `options` have a key from `defaults`, apply its
2541                         // value to the `model` and remove it from the `options object.
2542                         _.each( this.defaults, function( def, key ) {
2543                                 var value = this.options[ key ];
2544                                 if ( _.isUndefined( value ) )
2545                                         return;
2546
2547                                 this.model.set( key, value );
2548                                 delete this.options[ key ];
2549                         }, this );
2550
2551                         this.model.on( 'change', this.render, this );
2552                 },
2553
2554                 render: function() {
2555                         var classes = [ 'button', this.className ],
2556                                 model = this.model.toJSON();
2557
2558                         if ( model.style )
2559                                 classes.push( 'button-' + model.style );
2560
2561                         if ( model.size )
2562                                 classes.push( 'button-' + model.size );
2563
2564                         classes = _.uniq( classes.concat( this.options.classes ) );
2565                         this.el.className = classes.join(' ');
2566
2567                         this.$el.attr( 'disabled', model.disabled );
2568                         this.$el.text( this.model.get('text') );
2569
2570                         return this;
2571                 },
2572
2573                 click: function( event ) {
2574                         if ( '#' === this.attributes.href )
2575                                 event.preventDefault();
2576
2577                         if ( this.options.click && ! this.model.get('disabled') )
2578                                 this.options.click.apply( this, arguments );
2579                 }
2580         });
2581
2582         /**
2583          * wp.media.view.ButtonGroup
2584          */
2585         media.view.ButtonGroup = media.View.extend({
2586                 tagName:   'div',
2587                 className: 'button-group button-large media-button-group',
2588
2589                 initialize: function() {
2590                         this.buttons = _.map( this.options.buttons || [], function( button ) {
2591                                 if ( button instanceof Backbone.View )
2592                                         return button;
2593                                 else
2594                                         return new media.view.Button( button ).render();
2595                         });
2596
2597                         delete this.options.buttons;
2598
2599                         if ( this.options.classes )
2600                                 this.$el.addClass( this.options.classes );
2601                 },
2602
2603                 render: function() {
2604                         this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
2605                         return this;
2606                 }
2607         });
2608
2609         /**
2610          * wp.media.view.PriorityList
2611          */
2612
2613         media.view.PriorityList = media.View.extend({
2614                 tagName:   'div',
2615
2616                 initialize: function() {
2617                         this._views = {};
2618
2619                         this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
2620                         delete this.options.views;
2621
2622                         if ( ! this.options.silent )
2623                                 this.render();
2624                 },
2625
2626                 set: function( id, view, options ) {
2627                         var priority, views, index;
2628
2629                         options = options || {};
2630
2631                         // Accept an object with an `id` : `view` mapping.
2632                         if ( _.isObject( id ) ) {
2633                                 _.each( id, function( view, id ) {
2634                                         this.set( id, view );
2635                                 }, this );
2636                                 return this;
2637                         }
2638
2639                         if ( ! (view instanceof Backbone.View) )
2640                                 view = this.toView( view, id, options );
2641
2642                         view.controller = view.controller || this.controller;
2643
2644                         this.unset( id );
2645
2646                         priority = view.options.priority || 10;
2647                         views = this.views.get() || [];
2648
2649                         _.find( views, function( existing, i ) {
2650                                 if ( existing.options.priority > priority ) {
2651                                         index = i;
2652                                         return true;
2653                                 }
2654                         });
2655
2656                         this._views[ id ] = view;
2657                         this.views.add( view, {
2658                                 at: _.isNumber( index ) ? index : views.length || 0
2659                         });
2660
2661                         return this;
2662                 },
2663
2664                 get: function( id ) {
2665                         return this._views[ id ];
2666                 },
2667
2668                 unset: function( id ) {
2669                         var view = this.get( id );
2670
2671                         if ( view )
2672                                 view.remove();
2673
2674                         delete this._views[ id ];
2675                         return this;
2676                 },
2677
2678                 toView: function( options ) {
2679                         return new media.View( options );
2680                 }
2681         });
2682
2683         /**
2684          * wp.media.view.MenuItem
2685          */
2686         media.view.MenuItem = media.View.extend({
2687                 tagName:   'a',
2688                 className: 'media-menu-item',
2689
2690                 attributes: {
2691                         href: '#'
2692                 },
2693
2694                 events: {
2695                         'click': '_click'
2696                 },
2697
2698                 _click: function( event ) {
2699                         var clickOverride = this.options.click;
2700
2701                         if ( event )
2702                                 event.preventDefault();
2703
2704                         if ( clickOverride )
2705                                 clickOverride.call( this );
2706                         else
2707                                 this.click();
2708                 },
2709
2710                 click: function() {
2711                         var state = this.options.state;
2712                         if ( state )
2713                                 this.controller.setState( state );
2714                 },
2715
2716                 render: function() {
2717                         var options = this.options;
2718
2719                         if ( options.text )
2720                                 this.$el.text( options.text );
2721                         else if ( options.html )
2722                                 this.$el.html( options.html );
2723
2724                         return this;
2725                 }
2726         });
2727
2728         /**
2729          * wp.media.view.Menu
2730          */
2731         media.view.Menu = media.view.PriorityList.extend({
2732                 tagName:   'div',
2733                 className: 'media-menu',
2734                 property:  'state',
2735                 ItemView:  media.view.MenuItem,
2736                 region:    'menu',
2737
2738                 toView: function( options, id ) {
2739                         options = options || {};
2740                         options[ this.property ] = options[ this.property ] || id;
2741                         return new this.ItemView( options ).render();
2742                 },
2743
2744                 ready: function() {
2745                         media.view.PriorityList.prototype.ready.apply( this, arguments );
2746                         this.visibility();
2747                 },
2748
2749                 set: function() {
2750                         media.view.PriorityList.prototype.set.apply( this, arguments );
2751                         this.visibility();
2752                 },
2753
2754                 unset: function() {
2755                         media.view.PriorityList.prototype.unset.apply( this, arguments );
2756                         this.visibility();
2757                 },
2758
2759                 visibility: function() {
2760                         var region = this.region,
2761                                 view = this.controller[ region ].get(),
2762                                 views = this.views.get(),
2763                                 hide = ! views || views.length < 2;
2764
2765                         if ( this === view )
2766                                 this.controller.$el.toggleClass( 'hide-' + region, hide );
2767                 },
2768
2769                 select: function( id ) {
2770                         var view = this.get( id );
2771
2772                         if ( ! view )
2773                                 return;
2774
2775                         this.deselect();
2776                         view.$el.addClass('active');
2777                 },
2778
2779                 deselect: function() {
2780                         this.$el.children().removeClass('active');
2781                 }
2782         });
2783
2784         /**
2785          * wp.media.view.RouterItem
2786          */
2787         media.view.RouterItem = media.view.MenuItem.extend({
2788                 click: function() {
2789                         var contentMode = this.options.contentMode;
2790                         if ( contentMode )
2791                                 this.controller.content.mode( contentMode );
2792                 }
2793         });
2794
2795         /**
2796          * wp.media.view.Router
2797          */
2798         media.view.Router = media.view.Menu.extend({
2799                 tagName:   'div',
2800                 className: 'media-router',
2801                 property:  'contentMode',
2802                 ItemView:  media.view.RouterItem,
2803                 region:    'router',
2804
2805                 initialize: function() {
2806                         this.controller.on( 'content:render', this.update, this );
2807                         media.view.Menu.prototype.initialize.apply( this, arguments );
2808                 },
2809
2810                 update: function() {
2811                         var mode = this.controller.content.mode();
2812                         if ( mode )
2813                                 this.select( mode );
2814                 }
2815         });
2816
2817
2818         /**
2819          * wp.media.view.Sidebar
2820          */
2821         media.view.Sidebar = media.view.PriorityList.extend({
2822                 className: 'media-sidebar'
2823         });
2824
2825         /**
2826          * wp.media.view.Attachment
2827          */
2828         media.view.Attachment = media.View.extend({
2829                 tagName:   'li',
2830                 className: 'attachment',
2831                 template:  media.template('attachment'),
2832
2833                 events: {
2834                         'click .attachment-preview':      'toggleSelectionHandler',
2835                         'change [data-setting]':          'updateSetting',
2836                         'change [data-setting] input':    'updateSetting',
2837                         'change [data-setting] select':   'updateSetting',
2838                         'change [data-setting] textarea': 'updateSetting',
2839                         'click .close':                   'removeFromLibrary',
2840                         'click .check':                   'removeFromSelection',
2841                         'click a':                        'preventDefault'
2842                 },
2843
2844                 buttons: {},
2845
2846                 initialize: function() {
2847                         var selection = this.options.selection;
2848
2849                         this.model.on( 'change:sizes change:uploading', this.render, this );
2850                         this.model.on( 'change:title', this._syncTitle, this );
2851                         this.model.on( 'change:caption', this._syncCaption, this );
2852                         this.model.on( 'change:percent', this.progress, this );
2853
2854                         // Update the selection.
2855                         this.model.on( 'add', this.select, this );
2856                         this.model.on( 'remove', this.deselect, this );
2857                         if ( selection )
2858                                 selection.on( 'reset', this.updateSelect, this );
2859
2860                         // Update the model's details view.
2861                         this.model.on( 'selection:single selection:unsingle', this.details, this );
2862                         this.details( this.model, this.controller.state().get('selection') );
2863                 },
2864
2865                 dispose: function() {
2866                         var selection = this.options.selection;
2867
2868                         // Make sure all settings are saved before removing the view.
2869                         this.updateAll();
2870
2871                         if ( selection )
2872                                 selection.off( null, null, this );
2873
2874                         media.View.prototype.dispose.apply( this, arguments );
2875                         return this;
2876                 },
2877
2878                 render: function() {
2879                         var options = _.defaults( this.model.toJSON(), {
2880                                         orientation:   'landscape',
2881                                         uploading:     false,
2882                                         type:          '',
2883                                         subtype:       '',
2884                                         icon:          '',
2885                                         filename:      '',
2886                                         caption:       '',
2887                                         title:         '',
2888                                         dateFormatted: '',
2889                                         width:         '',
2890                                         height:        '',
2891                                         compat:        false,
2892                                         alt:           '',
2893                                         description:   ''
2894                                 });
2895
2896                         options.buttons  = this.buttons;
2897                         options.describe = this.controller.state().get('describe');
2898
2899                         if ( 'image' === options.type )
2900                                 options.size = this.imageSize();
2901
2902                         options.can = {};
2903                         if ( options.nonces ) {
2904                                 options.can.remove = !! options.nonces['delete'];
2905                                 options.can.save = !! options.nonces.update;
2906                         }
2907
2908                         if ( this.controller.state().get('allowLocalEdits') )
2909                                 options.allowLocalEdits = true;
2910
2911                         this.views.detach();
2912                         this.$el.html( this.template( options ) );
2913
2914                         this.$el.toggleClass( 'uploading', options.uploading );
2915                         if ( options.uploading )
2916                                 this.$bar = this.$('.media-progress-bar div');
2917                         else
2918                                 delete this.$bar;
2919
2920                         // Check if the model is selected.
2921                         this.updateSelect();
2922
2923                         // Update the save status.
2924                         this.updateSave();
2925
2926                         this.views.render();
2927
2928                         return this;
2929                 },
2930
2931                 progress: function() {
2932                         if ( this.$bar && this.$bar.length )
2933                                 this.$bar.width( this.model.get('percent') + '%' );
2934                 },
2935
2936                 toggleSelectionHandler: function( event ) {
2937                         var method;
2938
2939                         if ( event.shiftKey )
2940                                 method = 'between';
2941                         else if ( event.ctrlKey || event.metaKey )
2942                                 method = 'toggle';
2943
2944                         this.toggleSelection({
2945                                 method: method
2946                         });
2947                 },
2948
2949                 toggleSelection: function( options ) {
2950                         var collection = this.collection,
2951                                 selection = this.options.selection,
2952                                 model = this.model,
2953                                 method = options && options.method,
2954                                 single, between, models, singleIndex, modelIndex;
2955
2956                         if ( ! selection )
2957                                 return;
2958
2959                         single = selection.single();
2960                         method = _.isUndefined( method ) ? selection.multiple : method;
2961
2962                         // If the `method` is set to `between`, select all models that
2963                         // exist between the current and the selected model.
2964                         if ( 'between' === method && single && selection.multiple ) {
2965                                 // If the models are the same, short-circuit.
2966                                 if ( single === model )
2967                                         return;
2968
2969                                 singleIndex = collection.indexOf( single );
2970                                 modelIndex  = collection.indexOf( this.model );
2971
2972                                 if ( singleIndex < modelIndex )
2973                                         models = collection.models.slice( singleIndex, modelIndex + 1 );
2974                                 else
2975                                         models = collection.models.slice( modelIndex, singleIndex + 1 );
2976
2977                                 selection.add( models ).single( model );
2978                                 return;
2979
2980                         // If the `method` is set to `toggle`, just flip the selection
2981                         // status, regardless of whether the model is the single model.
2982                         } else if ( 'toggle' === method ) {
2983                                 selection[ this.selected() ? 'remove' : 'add' ]( model ).single( model );
2984                                 return;
2985                         }
2986
2987                         if ( method !== 'add' )
2988                                 method = 'reset';
2989
2990                         if ( this.selected() ) {
2991                                 // If the model is the single model, remove it.
2992                                 // If it is not the same as the single model,
2993                                 // it now becomes the single model.
2994                                 selection[ single === model ? 'remove' : 'single' ]( model );
2995                         } else {
2996                                 // If the model is not selected, run the `method` on the
2997                                 // selection. By default, we `reset` the selection, but the
2998                                 // `method` can be set to `add` the model to the selection.
2999                                 selection[ method ]( model ).single( model );
3000                         }
3001                 },
3002
3003                 updateSelect: function() {
3004                         this[ this.selected() ? 'select' : 'deselect' ]();
3005                 },
3006
3007                 selected: function() {
3008                         var selection = this.options.selection;
3009                         if ( selection )
3010                                 return !! selection.getByCid( this.model.cid );
3011                 },
3012
3013                 select: function( model, collection ) {
3014                         var selection = this.options.selection;
3015
3016                         // Check if a selection exists and if it's the collection provided.
3017                         // If they're not the same collection, bail; we're in another
3018                         // selection's event loop.
3019                         if ( ! selection || ( collection && collection !== selection ) )
3020                                 return;
3021
3022                         this.$el.addClass('selected');
3023                 },
3024
3025                 deselect: function( model, collection ) {
3026                         var selection = this.options.selection;
3027
3028                         // Check if a selection exists and if it's the collection provided.
3029                         // If they're not the same collection, bail; we're in another
3030                         // selection's event loop.
3031                         if ( ! selection || ( collection && collection !== selection ) )
3032                                 return;
3033
3034                         this.$el.removeClass('selected');
3035                 },
3036
3037                 details: function( model, collection ) {
3038                         var selection = this.options.selection,
3039                                 details;
3040
3041                         if ( selection !== collection )
3042                                 return;
3043
3044                         details = selection.single();
3045                         this.$el.toggleClass( 'details', details === this.model );
3046                 },
3047
3048                 preventDefault: function( event ) {
3049                         event.preventDefault();
3050                 },
3051
3052                 imageSize: function( size ) {
3053                         var sizes = this.model.get('sizes');
3054
3055                         size = size || 'medium';
3056
3057                         // Use the provided image size if possible.
3058                         if ( sizes && sizes[ size ] ) {
3059                                 return _.clone( sizes[ size ] );
3060                         } else {
3061                                 return {
3062                                         url:         this.model.get('url'),
3063                                         width:       this.model.get('width'),
3064                                         height:      this.model.get('height'),
3065                                         orientation: this.model.get('orientation')
3066                                 };
3067                         }
3068                 },
3069
3070                 updateSetting: function( event ) {
3071                         var $setting = $( event.target ).closest('[data-setting]'),
3072                                 setting, value;
3073
3074                         if ( ! $setting.length )
3075                                 return;
3076
3077                         setting = $setting.data('setting');
3078                         value   = event.target.value;
3079
3080                         if ( this.model.get( setting ) !== value )
3081                                 this.save( setting, value );
3082                 },
3083
3084                 // Pass all the arguments to the model's save method.
3085                 //
3086                 // Records the aggregate status of all save requests and updates the
3087                 // view's classes accordingly.
3088                 save: function() {
3089                         var view = this,
3090                                 save = this._save = this._save || { status: 'ready' },
3091                                 request = this.model.save.apply( this.model, arguments ),
3092                                 requests = save.requests ? $.when( request, save.requests ) : request;
3093
3094                         // If we're waiting to remove 'Saved.', stop.
3095                         if ( save.savedTimer )
3096                                 clearTimeout( save.savedTimer );
3097
3098                         this.updateSave('waiting');
3099                         save.requests = requests;
3100                         requests.always( function() {
3101                                 // If we've performed another request since this one, bail.
3102                                 if ( save.requests !== requests )
3103                                         return;
3104
3105                                 view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
3106                                 save.savedTimer = setTimeout( function() {
3107                                         view.updateSave('ready');
3108                                         delete save.savedTimer;
3109                                 }, 2000 );
3110                         });
3111
3112                 },
3113
3114                 updateSave: function( status ) {
3115                         var save = this._save = this._save || { status: 'ready' };
3116
3117                         if ( status && status !== save.status ) {
3118                                 this.$el.removeClass( 'save-' + save.status );
3119                                 save.status = status;
3120                         }
3121
3122                         this.$el.addClass( 'save-' + save.status );
3123                         return this;
3124                 },
3125
3126                 updateAll: function() {
3127                         var $settings = this.$('[data-setting]'),
3128                                 model = this.model,
3129                                 changed;
3130
3131                         changed = _.chain( $settings ).map( function( el ) {
3132                                 var $input = $('input, textarea, select, [value]', el ),
3133                                         setting, value;
3134
3135                                 if ( ! $input.length )
3136                                         return;
3137
3138                                 setting = $(el).data('setting');
3139                                 value = $input.val();
3140
3141                                 // Record the value if it changed.
3142                                 if ( model.get( setting ) !== value )
3143                                         return [ setting, value ];
3144                         }).compact().object().value();
3145
3146                         if ( ! _.isEmpty( changed ) )
3147                                 model.save( changed );
3148                 },
3149
3150                 removeFromLibrary: function( event ) {
3151                         // Stop propagation so the model isn't selected.
3152                         event.stopPropagation();
3153
3154                         this.collection.remove( this.model );
3155                 },
3156
3157                 removeFromSelection: function( event ) {
3158                         var selection = this.options.selection;
3159                         if ( ! selection )
3160                                 return;
3161
3162                         // Stop propagation so the model isn't selected.
3163                         event.stopPropagation();
3164
3165                         selection.remove( this.model );
3166                 }
3167         });
3168
3169         // Ensure settings remain in sync between attachment views.
3170         _.each({
3171                 caption: '_syncCaption',
3172                 title:   '_syncTitle'
3173         }, function( method, setting ) {
3174                 media.view.Attachment.prototype[ method ] = function( model, value ) {
3175                         var $setting = this.$('[data-setting="' + setting + '"]');
3176
3177                         if ( ! $setting.length )
3178                                 return this;
3179
3180                         // If the updated value is in sync with the value in the DOM, there
3181                         // is no need to re-render. If we're currently editing the value,
3182                         // it will automatically be in sync, suppressing the re-render for
3183                         // the view we're editing, while updating any others.
3184                         if ( value === $setting.find('input, textarea, select, [value]').val() )
3185                                 return this;
3186
3187                         return this.render();
3188                 };
3189         });
3190
3191         /**
3192          * wp.media.view.Attachment.Library
3193          */
3194         media.view.Attachment.Library = media.view.Attachment.extend({
3195                 buttons: {
3196                         check: true
3197                 }
3198         });
3199
3200         /**
3201          * wp.media.view.Attachment.EditLibrary
3202          */
3203         media.view.Attachment.EditLibrary = media.view.Attachment.extend({
3204                 buttons: {
3205                         close: true
3206                 }
3207         });
3208
3209         /**
3210          * wp.media.view.Attachments
3211          */
3212         media.view.Attachments = media.View.extend({
3213                 tagName:   'ul',
3214                 className: 'attachments',
3215
3216                 cssTemplate: media.template('attachments-css'),
3217
3218                 events: {
3219                         'scroll': 'scroll'
3220                 },
3221
3222                 initialize: function() {
3223                         this.el.id = _.uniqueId('__attachments-view-');
3224
3225                         _.defaults( this.options, {
3226                                 refreshSensitivity: 200,
3227                                 refreshThreshold:   3,
3228                                 AttachmentView:     media.view.Attachment,
3229                                 sortable:           false,
3230                                 resize:             true
3231                         });
3232
3233                         this._viewsByCid = {};
3234
3235                         this.collection.on( 'add', function( attachment, attachments, options ) {
3236                                 this.views.add( this.createAttachmentView( attachment ), {
3237                                         at: options.index
3238                                 });
3239                         }, this );
3240
3241                         this.collection.on( 'remove', function( attachment, attachments, options ) {
3242                                 var view = this._viewsByCid[ attachment.cid ];
3243                                 delete this._viewsByCid[ attachment.cid ];
3244
3245                                 if ( view )
3246                                         view.remove();
3247                         }, this );
3248
3249                         this.collection.on( 'reset', this.render, this );
3250
3251                         // Throttle the scroll handler.
3252                         this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
3253
3254                         this.initSortable();
3255
3256                         _.bindAll( this, 'css' );
3257                         this.model.on( 'change:edge change:gutter', this.css, this );
3258                         this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity );
3259                         if ( this.options.resize )
3260                                 $(window).on( 'resize.attachments', this._resizeCss );
3261                         this.css();
3262                 },
3263
3264                 dispose: function() {
3265                         this.collection.props.off( null, null, this );
3266                         $(window).off( 'resize.attachments', this._resizeCss );
3267                         media.View.prototype.dispose.apply( this, arguments );
3268                 },
3269
3270                 css: function() {
3271                         var $css = $( '#' + this.el.id + '-css' );
3272
3273                         if ( $css.length )
3274                                 $css.remove();
3275
3276                         media.view.Attachments.$head().append( this.cssTemplate({
3277                                 id:     this.el.id,
3278                                 edge:   this.edge(),
3279                                 gutter: this.model.get('gutter')
3280                         }) );
3281                 },
3282
3283                 edge: function() {
3284                         var edge = this.model.get('edge'),
3285                                 gutter, width, columns;
3286
3287                         if ( ! this.$el.is(':visible') )
3288                                 return edge;
3289
3290                         gutter  = this.model.get('gutter') * 2;
3291                         width   = this.$el.width() - gutter;
3292                         columns = Math.ceil( width / ( edge + gutter ) );
3293                         edge = Math.floor( ( width - ( columns * gutter ) ) / columns );
3294                         return edge;
3295                 },
3296
3297                 initSortable: function() {
3298                         var collection = this.collection;
3299
3300                         if ( ! this.options.sortable || ! $.fn.sortable )
3301                                 return;
3302
3303                         this.$el.sortable( _.extend({
3304                                 // If the `collection` has a `comparator`, disable sorting.
3305                                 disabled: !! collection.comparator,
3306
3307                                 // Prevent attachments from being dragged outside the bounding
3308                                 // box of the list.
3309                                 containment: this.$el,
3310
3311                                 // Change the position of the attachment as soon as the
3312                                 // mouse pointer overlaps a thumbnail.
3313                                 tolerance: 'pointer',
3314
3315                                 // Record the initial `index` of the dragged model.
3316                                 start: function( event, ui ) {
3317                                         ui.item.data('sortableIndexStart', ui.item.index());
3318                                 },
3319
3320                                 // Update the model's index in the collection.
3321                                 // Do so silently, as the view is already accurate.
3322                                 update: function( event, ui ) {
3323                                         var model = collection.at( ui.item.data('sortableIndexStart') ),
3324                                                 comparator = collection.comparator;
3325
3326                                         // Temporarily disable the comparator to prevent `add`
3327                                         // from re-sorting.
3328                                         delete collection.comparator;
3329
3330                                         // Silently shift the model to its new index.
3331                                         collection.remove( model, {
3332                                                 silent: true
3333                                         }).add( model, {
3334                                                 silent: true,
3335                                                 at:     ui.item.index()
3336                                         });
3337
3338                                         // Restore the comparator.
3339                                         collection.comparator = comparator;
3340
3341                                         // Fire the `reset` event to ensure other collections sync.
3342                                         collection.trigger( 'reset', collection );
3343
3344                                         // If the collection is sorted by menu order,
3345                                         // update the menu order.
3346                                         collection.saveMenuOrder();
3347                                 }
3348                         }, this.options.sortable ) );
3349
3350                         // If the `orderby` property is changed on the `collection`,
3351                         // check to see if we have a `comparator`. If so, disable sorting.
3352                         collection.props.on( 'change:orderby', function() {
3353                                 this.$el.sortable( 'option', 'disabled', !! collection.comparator );
3354                         }, this );
3355
3356                         this.collection.props.on( 'change:orderby', this.refreshSortable, this );
3357                         this.refreshSortable();
3358                 },
3359
3360                 refreshSortable: function() {
3361                         if ( ! this.options.sortable || ! $.fn.sortable )
3362                                 return;
3363
3364                         // If the `collection` has a `comparator`, disable sorting.
3365                         var collection = this.collection,
3366                                 orderby = collection.props.get('orderby'),
3367                                 enabled = 'menuOrder' === orderby || ! collection.comparator;
3368
3369                         this.$el.sortable( 'option', 'disabled', ! enabled );
3370                 },
3371
3372                 createAttachmentView: function( attachment ) {
3373                         var view = new this.options.AttachmentView({
3374                                 controller: this.controller,
3375                                 model:      attachment,
3376                                 collection: this.collection,
3377                                 selection:  this.options.selection
3378                         });
3379
3380                         return this._viewsByCid[ attachment.cid ] = view;
3381                 },
3382
3383                 prepare: function() {
3384                         // Create all of the Attachment views, and replace
3385                         // the list in a single DOM operation.
3386                         if ( this.collection.length ) {
3387                                 this.views.set( this.collection.map( this.createAttachmentView, this ) );
3388
3389                         // If there are no elements, clear the views and load some.
3390                         } else {
3391                                 this.views.unset();
3392                                 this.collection.more().done( this.scroll );
3393                         }
3394                 },
3395
3396                 ready: function() {
3397                         // Trigger the scroll event to check if we're within the
3398                         // threshold to query for additional attachments.
3399                         this.scroll();
3400                 },
3401
3402                 scroll: function( event ) {
3403                         // @todo: is this still necessary?
3404                         if ( ! this.$el.is(':visible') )
3405                                 return;
3406
3407                         if ( this.collection.hasMore() && this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
3408                                 this.collection.more().done( this.scroll );
3409                         }
3410                 }
3411         }, {
3412                 $head: (function() {
3413                         var $head;
3414                         return function() {
3415                                 return $head = $head || $('head');
3416                         };
3417                 }())
3418         });
3419
3420         /**
3421          * wp.media.view.Search
3422          */
3423         media.view.Search = media.View.extend({
3424                 tagName:   'input',
3425                 className: 'search',
3426
3427                 attributes: {
3428                         type:        'search',
3429                         placeholder: l10n.search
3430                 },
3431
3432                 events: {
3433                         'input':  'search',
3434                         'keyup':  'search',
3435                         'change': 'search',
3436                         'search': 'search'
3437                 },
3438
3439                 render: function() {
3440                         this.el.value = this.model.escape('search');
3441                         return this;
3442                 },
3443
3444                 search: function( event ) {
3445                         if ( event.target.value )
3446                                 this.model.set( 'search', event.target.value );
3447                         else
3448                                 this.model.unset('search');
3449                 }
3450         });
3451
3452         /**
3453          * wp.media.view.AttachmentFilters
3454          */
3455         media.view.AttachmentFilters = media.View.extend({
3456                 tagName:   'select',
3457                 className: 'attachment-filters',
3458
3459                 events: {
3460                         change: 'change'
3461                 },
3462
3463                 keys: [],
3464
3465                 initialize: function() {
3466                         this.createFilters();
3467                         _.extend( this.filters, this.options.filters );
3468
3469                         // Build `<option>` elements.
3470                         this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
3471                                 return {
3472                                         el: this.make( 'option', { value: value }, filter.text ),
3473                                         priority: filter.priority || 50
3474                                 };
3475                         }, this ).sortBy('priority').pluck('el').value() );
3476
3477                         this.model.on( 'change', this.select, this );
3478                         this.select();
3479                 },
3480
3481                 createFilters: function() {
3482                         this.filters = {};
3483                 },
3484
3485                 change: function( event ) {
3486                         var filter = this.filters[ this.el.value ];
3487
3488                         if ( filter )
3489                                 this.model.set( filter.props );
3490                 },
3491
3492                 select: function() {
3493                         var model = this.model,
3494                                 value = 'all',
3495                                 props = model.toJSON();
3496
3497                         _.find( this.filters, function( filter, id ) {
3498                                 var equal = _.all( filter.props, function( prop, key ) {
3499                                         return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
3500                                 });
3501
3502                                 if ( equal )
3503                                         return value = id;
3504                         });
3505
3506                         this.$el.val( value );
3507                 }
3508         });
3509
3510         media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
3511                 createFilters: function() {
3512                         var type = this.model.get('type'),
3513                                 types = media.view.settings.mimeTypes,
3514                                 text;
3515
3516                         if ( types && type )
3517                                 text = types[ type ];
3518
3519                         this.filters = {
3520                                 all: {
3521                                         text:  text || l10n.allMediaItems,
3522                                         props: {
3523                                                 uploadedTo: null,
3524                                                 orderby: 'date',
3525                                                 order:   'DESC'
3526                                         },
3527                                         priority: 10
3528                                 },
3529
3530                                 uploaded: {
3531                                         text:  l10n.uploadedToThisPost,
3532                                         props: {
3533                                                 uploadedTo: media.view.settings.post.id,
3534                                                 orderby: 'menuOrder',
3535                                                 order:   'ASC'
3536                                         },
3537                                         priority: 20
3538                                 }
3539                         };
3540                 }
3541         });
3542
3543         media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
3544                 createFilters: function() {
3545                         var filters = {};
3546
3547                         _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
3548                                 filters[ key ] = {
3549                                         text: text,
3550                                         props: {
3551                                                 type:    key,
3552                                                 uploadedTo: null,
3553                                                 orderby: 'date',
3554                                                 order:   'DESC'
3555                                         }
3556                                 };
3557                         });
3558
3559                         filters.all = {
3560                                 text:  l10n.allMediaItems,
3561                                 props: {
3562                                         type:    null,
3563                                         uploadedTo: null,
3564                                         orderby: 'date',
3565                                         order:   'DESC'
3566                                 },
3567                                 priority: 10
3568                         };
3569
3570                         filters.uploaded = {
3571                                 text:  l10n.uploadedToThisPost,
3572                                 props: {
3573                                         type:    null,
3574                                         uploadedTo: media.view.settings.post.id,
3575                                         orderby: 'menuOrder',
3576                                         order:   'ASC'
3577                                 },
3578                                 priority: 20
3579                         };
3580
3581                         this.filters = filters;
3582                 }
3583         });
3584
3585
3586
3587         /**
3588          * wp.media.view.AttachmentsBrowser
3589          */
3590         media.view.AttachmentsBrowser = media.View.extend({
3591                 tagName:   'div',
3592                 className: 'attachments-browser',
3593
3594                 initialize: function() {
3595                         _.defaults( this.options, {
3596                                 filters: false,
3597                                 search:  true,
3598                                 display: false,
3599
3600                                 AttachmentView: media.view.Attachment.Library
3601                         });
3602
3603                         this.createToolbar();
3604                         this.updateContent();
3605                         this.createSidebar();
3606
3607                         this.collection.on( 'add remove reset', this.updateContent, this );
3608                 },
3609
3610                 dispose: function() {
3611                         this.options.selection.off( null, null, this );
3612                         media.View.prototype.dispose.apply( this, arguments );
3613                         return this;
3614                 },
3615
3616                 createToolbar: function() {
3617                         var filters, FiltersConstructor;
3618
3619                         this.toolbar = new media.view.Toolbar({
3620                                 controller: this.controller
3621                         });
3622
3623                         this.views.add( this.toolbar );
3624
3625                         filters = this.options.filters;
3626                         if ( 'uploaded' === filters )
3627                                 FiltersConstructor = media.view.AttachmentFilters.Uploaded;
3628                         else if ( 'all' === filters )
3629                                 FiltersConstructor = media.view.AttachmentFilters.All;
3630
3631                         if ( FiltersConstructor ) {
3632                                 this.toolbar.set( 'filters', new FiltersConstructor({
3633                                         controller: this.controller,
3634                                         model:      this.collection.props,
3635                                         priority:   -80
3636                                 }).render() );
3637                         }
3638
3639                         if ( this.options.search ) {
3640                                 this.toolbar.set( 'search', new media.view.Search({
3641                                         controller: this.controller,
3642                                         model:      this.collection.props,
3643                                         priority:   60
3644                                 }).render() );
3645                         }
3646
3647                         if ( this.options.dragInfo ) {
3648                                 this.toolbar.set( 'dragInfo', new media.View({
3649                                         el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
3650                                         priority: -40
3651                                 }) );
3652                         }
3653                 },
3654
3655                 updateContent: function() {
3656                         var view = this;
3657
3658                         if( ! this.attachments )
3659                                 this.createAttachments();
3660
3661                         if ( ! this.collection.length ) {
3662                                 this.collection.more().done( function() {
3663                                         if ( ! view.collection.length )
3664                                                 view.createUploader();
3665                                 });
3666                         }
3667                 },
3668
3669                 removeContent: function() {
3670                         _.each(['attachments','uploader'], function( key ) {
3671                                 if ( this[ key ] ) {
3672                                         this[ key ].remove();
3673                                         delete this[ key ];
3674                                 }
3675                         }, this );
3676                 },
3677
3678                 createUploader: function() {
3679                         this.removeContent();
3680
3681                         this.uploader = new media.view.UploaderInline({
3682                                 controller: this.controller,
3683                                 status:     false,
3684                                 message:    l10n.noItemsFound
3685                         });
3686
3687                         this.views.add( this.uploader );
3688                 },
3689
3690                 createAttachments: function() {
3691                         this.removeContent();
3692
3693                         this.attachments = new media.view.Attachments({
3694                                 controller: this.controller,
3695                                 collection: this.collection,
3696                                 selection:  this.options.selection,
3697                                 model:      this.model,
3698                                 sortable:   this.options.sortable,
3699
3700                                 // The single `Attachment` view to be used in the `Attachments` view.
3701                                 AttachmentView: this.options.AttachmentView
3702                         });
3703
3704                         this.views.add( this.attachments );
3705                 },
3706
3707                 createSidebar: function() {
3708                         var options = this.options,
3709                                 selection = options.selection,
3710                                 sidebar = this.sidebar = new media.view.Sidebar({
3711                                         controller: this.controller
3712                                 });
3713
3714                         this.views.add( sidebar );
3715
3716                         if ( this.controller.uploader ) {
3717                                 sidebar.set( 'uploads', new media.view.UploaderStatus({
3718                                         controller: this.controller,
3719                                         priority:   40
3720                                 }) );
3721                         }
3722
3723                         selection.on( 'selection:single', this.createSingle, this );
3724                         selection.on( 'selection:unsingle', this.disposeSingle, this );
3725
3726                         if ( selection.single() )
3727                                 this.createSingle();
3728                 },
3729
3730                 createSingle: function() {
3731                         var sidebar = this.sidebar,
3732                                 single = this.options.selection.single(),
3733                                 views = {};
3734
3735                         sidebar.set( 'details', new media.view.Attachment.Details({
3736                                 controller: this.controller,
3737                                 model:      single,
3738                                 priority:   80
3739                         }) );
3740
3741                         sidebar.set( 'compat', new media.view.AttachmentCompat({
3742                                 controller: this.controller,
3743                                 model:      single,
3744                                 priority:   120
3745                         }) );
3746
3747                         if ( this.options.display ) {
3748                                 sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
3749                                         controller:   this.controller,
3750                                         model:        this.model.display( single ),
3751                                         attachment:   single,
3752                                         priority:     160,
3753                                         userSettings: this.model.get('displayUserSettings')
3754                                 }) );
3755                         }
3756                 },
3757
3758                 disposeSingle: function() {
3759                         var sidebar = this.sidebar;
3760                         sidebar.unset('details');
3761                         sidebar.unset('compat');
3762                         sidebar.unset('display');
3763                 }
3764         });
3765
3766         /**
3767          * wp.media.view.Selection
3768          */
3769         media.view.Selection = media.View.extend({
3770                 tagName:   'div',
3771                 className: 'media-selection',
3772                 template:  media.template('media-selection'),
3773
3774                 events: {
3775                         'click .edit-selection':  'edit',
3776                         'click .clear-selection': 'clear'
3777                 },
3778
3779                 initialize: function() {
3780                         _.defaults( this.options, {
3781                                 editable:  false,
3782                                 clearable: true
3783                         });
3784
3785                         this.attachments = new media.view.Attachments.Selection({
3786                                 controller: this.controller,
3787                                 collection: this.collection,
3788                                 selection:  this.collection,
3789                                 model:      new Backbone.Model({
3790                                         edge:   40,
3791                                         gutter: 5
3792                                 })
3793                         });
3794
3795                         this.views.set( '.selection-view', this.attachments );
3796                         this.collection.on( 'add remove reset', this.refresh, this );
3797                         this.controller.on( 'content:activate', this.refresh, this );
3798                 },
3799
3800                 ready: function() {
3801                         this.refresh();
3802                 },
3803
3804                 refresh: function() {
3805                         // If the selection hasn't been rendered, bail.
3806                         if ( ! this.$el.children().length )
3807                                 return;
3808
3809                         var collection = this.collection,
3810                                 editing = 'edit-selection' === this.controller.content.mode();
3811
3812                         // If nothing is selected, display nothing.
3813                         this.$el.toggleClass( 'empty', ! collection.length );
3814                         this.$el.toggleClass( 'one', 1 === collection.length );
3815                         this.$el.toggleClass( 'editing', editing );
3816
3817                         this.$('.count').text( l10n.selected.replace('%d', collection.length) );
3818                 },
3819
3820                 edit: function( event ) {
3821                         event.preventDefault();
3822                         if ( this.options.editable )
3823                                 this.options.editable.call( this, this.collection );
3824                 },
3825
3826                 clear: function( event ) {
3827                         event.preventDefault();
3828                         this.collection.reset();
3829                 }
3830         });
3831
3832
3833         /**
3834          * wp.media.view.Attachment.Selection
3835          */
3836         media.view.Attachment.Selection = media.view.Attachment.extend({
3837                 className: 'attachment selection',
3838
3839                 // On click, just select the model, instead of removing the model from
3840                 // the selection.
3841                 toggleSelection: function() {
3842                         this.options.selection.single( this.model );
3843                 }
3844         });
3845
3846         /**
3847          * wp.media.view.Attachments.Selection
3848          */
3849         media.view.Attachments.Selection = media.view.Attachments.extend({
3850                 events: {},
3851                 initialize: function() {
3852                         _.defaults( this.options, {
3853                                 sortable:   true,
3854                                 resize:     false,
3855
3856                                 // The single `Attachment` view to be used in the `Attachments` view.
3857                                 AttachmentView: media.view.Attachment.Selection
3858                         });
3859                         return media.view.Attachments.prototype.initialize.apply( this, arguments );
3860                 }
3861         });
3862
3863         /**
3864          * wp.media.view.Attachments.EditSelection
3865          */
3866         media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
3867                 buttons: {
3868                         close: true
3869                 }
3870         });
3871
3872
3873         /**
3874          * wp.media.view.Settings
3875          */
3876         media.view.Settings = media.View.extend({
3877                 events: {
3878                         'click button':    'updateHandler',
3879                         'change input':    'updateHandler',
3880                         'change select':   'updateHandler',
3881                         'change textarea': 'updateHandler'
3882                 },
3883
3884                 initialize: function() {
3885                         this.model = this.model || new Backbone.Model();
3886                         this.model.on( 'change', this.updateChanges, this );
3887                 },
3888
3889                 prepare: function() {
3890                         return _.defaults({
3891                                 model: this.model.toJSON()
3892                         }, this.options );
3893                 },
3894
3895                 render: function() {
3896                         media.View.prototype.render.apply( this, arguments );
3897                         // Select the correct values.
3898                         _( this.model.attributes ).chain().keys().each( this.update, this );
3899                         return this;
3900                 },
3901
3902                 update: function( key ) {
3903                         var value = this.model.get( key ),
3904                                 $setting = this.$('[data-setting="' + key + '"]'),
3905                                 $buttons, $value;
3906
3907                         // Bail if we didn't find a matching setting.
3908                         if ( ! $setting.length )
3909                                 return;
3910
3911                         // Attempt to determine how the setting is rendered and update
3912                         // the selected value.
3913
3914                         // Handle dropdowns.
3915                         if ( $setting.is('select') ) {
3916                                 $value = $setting.find('[value="' + value + '"]');
3917
3918                                 if ( $value.length ) {
3919                                         $setting.find('option').prop( 'selected', false );
3920                                         $value.prop( 'selected', true );
3921                                 } else {
3922                                         // If we can't find the desired value, record what *is* selected.
3923                                         this.model.set( key, $setting.find(':selected').val() );
3924                                 }
3925
3926
3927                         // Handle button groups.
3928                         } else if ( $setting.hasClass('button-group') ) {
3929                                 $buttons = $setting.find('button').removeClass('active');
3930                                 $buttons.filter( '[value="' + value + '"]' ).addClass('active');
3931
3932                         // Handle text inputs and textareas.
3933                         } else if ( $setting.is('input[type="text"], textarea') ) {
3934                                 if ( ! $setting.is(':focus') )
3935                                         $setting.val( value );
3936
3937                         // Handle checkboxes.
3938                         } else if ( $setting.is('input[type="checkbox"]') ) {
3939                                 $setting.attr( 'checked', !! value );
3940                         }
3941                 },
3942
3943                 updateHandler: function( event ) {
3944                         var $setting = $( event.target ).closest('[data-setting]'),
3945                                 value = event.target.value,
3946                                 userSetting;
3947
3948                         event.preventDefault();
3949
3950                         if ( ! $setting.length )
3951                                 return;
3952
3953                         // Use the correct value for checkboxes.
3954                         if ( $setting.is('input[type="checkbox"]') )
3955                                 value = $setting[0].checked;
3956
3957                         // Update the corresponding setting.
3958                         this.model.set( $setting.data('setting'), value );
3959
3960                         // If the setting has a corresponding user setting,
3961                         // update that as well.
3962                         if ( userSetting = $setting.data('userSetting') )
3963                                 setUserSetting( userSetting, value );
3964                 },
3965
3966                 updateChanges: function( model, options ) {
3967                         if ( options.changes )
3968                                 _( options.changes ).chain().keys().each( this.update, this );
3969                 }
3970         });
3971
3972         /**
3973          * wp.media.view.Settings.AttachmentDisplay
3974          */
3975         media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
3976                 className: 'attachment-display-settings',
3977                 template:  media.template('attachment-display-settings'),
3978
3979                 initialize: function() {
3980                         var attachment = this.options.attachment;
3981
3982                         _.defaults( this.options, {
3983                                 userSettings: false
3984                         });
3985
3986                         media.view.Settings.prototype.initialize.apply( this, arguments );
3987                         this.model.on( 'change:link', this.updateLinkTo, this );
3988
3989                         if ( attachment )
3990                                 attachment.on( 'change:uploading', this.render, this );
3991                 },
3992
3993                 dispose: function() {
3994                         var attachment = this.options.attachment;
3995                         if ( attachment )
3996                                 attachment.off( null, null, this );
3997
3998                         media.view.Settings.prototype.dispose.apply( this, arguments );
3999                 },
4000
4001                 render: function() {
4002                         var attachment = this.options.attachment;
4003                         if ( attachment ) {
4004                                 _.extend( this.options, {
4005                                         sizes: attachment.get('sizes'),
4006                                         type:  attachment.get('type')
4007                                 });
4008                         }
4009
4010                         media.view.Settings.prototype.render.call( this );
4011                         this.updateLinkTo();
4012                         return this;
4013                 },
4014
4015                 updateLinkTo: function() {
4016                         var linkTo = this.model.get('link'),
4017                                 $input = this.$('.link-to-custom'),
4018                                 attachment = this.options.attachment;
4019
4020                         if ( 'none' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
4021                                 $input.hide();
4022                                 return;
4023                         }
4024
4025                         if ( attachment ) {
4026                                 if ( 'post' === linkTo ) {
4027                                         $input.val( attachment.get('link') );
4028                                 } else if ( 'file' === linkTo ) {
4029                                         $input.val( attachment.get('url') );
4030                                 } else if ( ! this.model.get('linkUrl') ) {
4031                                         $input.val('http://');
4032                                 }
4033
4034                                 $input.prop( 'readonly', 'custom' !== linkTo );
4035                         }
4036
4037                         $input.show();
4038
4039                         // If the input is visible, focus and select its contents.
4040                         if ( $input.is(':visible') )
4041                                 $input.focus()[0].select();
4042                 }
4043         });
4044
4045         /**
4046          * wp.media.view.Settings.Gallery
4047          */
4048         media.view.Settings.Gallery = media.view.Settings.extend({
4049                 className: 'gallery-settings',
4050                 template:  media.template('gallery-settings')
4051         });
4052
4053         /**
4054          * wp.media.view.Attachment.Details
4055          */
4056         media.view.Attachment.Details = media.view.Attachment.extend({
4057                 tagName:   'div',
4058                 className: 'attachment-details',
4059                 template:  media.template('attachment-details'),
4060
4061                 events: {
4062                         'change [data-setting]':          'updateSetting',
4063                         'change [data-setting] input':    'updateSetting',
4064                         'change [data-setting] select':   'updateSetting',
4065                         'change [data-setting] textarea': 'updateSetting',
4066                         'click .delete-attachment':       'deleteAttachment',
4067                         'click .edit-attachment':         'editAttachment',
4068                         'click .refresh-attachment':      'refreshAttachment'
4069                 },
4070
4071                 initialize: function() {
4072                         this.focusManager = new media.view.FocusManager({
4073                                 el: this.el
4074                         });
4075
4076                         media.view.Attachment.prototype.initialize.apply( this, arguments );
4077                 },
4078
4079                 render: function() {
4080                         media.view.Attachment.prototype.render.apply( this, arguments );
4081                         this.focusManager.focus();
4082                         return this;
4083                 },
4084
4085                 deleteAttachment: function( event ) {
4086                         event.preventDefault();
4087
4088                         if ( confirm( l10n.warnDelete ) )
4089                                 this.model.destroy();
4090                 },
4091
4092                 editAttachment: function( event ) {
4093                         this.$el.addClass('needs-refresh');
4094                 },
4095
4096                 refreshAttachment: function( event ) {
4097                         this.$el.removeClass('needs-refresh');
4098                         event.preventDefault();
4099                         this.model.fetch();
4100                 }
4101         });
4102
4103         /**
4104          * wp.media.view.AttachmentCompat
4105          */
4106         media.view.AttachmentCompat = media.View.extend({
4107                 tagName:   'form',
4108                 className: 'compat-item',
4109
4110                 events: {
4111                         'submit':          'preventDefault',
4112                         'change input':    'save',
4113                         'change select':   'save',
4114                         'change textarea': 'save'
4115                 },
4116
4117                 initialize: function() {
4118                         this.focusManager = new media.view.FocusManager({
4119                                 el: this.el
4120                         });
4121
4122                         this.model.on( 'change:compat', this.render, this );
4123                 },
4124
4125                 dispose: function() {
4126                         if ( this.$(':focus').length )
4127                                 this.save();
4128
4129                         return media.View.prototype.dispose.apply( this, arguments );
4130                 },
4131
4132                 render: function() {
4133                         var compat = this.model.get('compat');
4134                         if ( ! compat || ! compat.item )
4135                                 return;
4136
4137                         this.views.detach();
4138                         this.$el.html( compat.item );
4139                         this.views.render();
4140
4141                         this.focusManager.focus();
4142                         return this;
4143                 },
4144
4145                 preventDefault: function( event ) {
4146                         event.preventDefault();
4147                 },
4148
4149                 save: function( event ) {
4150                         var data = {};
4151
4152                         if ( event )
4153                                 event.preventDefault();
4154
4155                         _.each( this.$el.serializeArray(), function( pair ) {
4156                                 data[ pair.name ] = pair.value;
4157                         });
4158
4159                         this.model.saveCompat( data );
4160                 }
4161         });
4162
4163         /**
4164          * wp.media.view.Iframe
4165          */
4166         media.view.Iframe = media.View.extend({
4167                 className: 'media-iframe',
4168
4169                 render: function() {
4170                         this.views.detach();
4171                         this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
4172                         this.views.render();
4173                         return this;
4174                 }
4175         });
4176
4177         /**
4178          * wp.media.view.Embed
4179          */
4180         media.view.Embed = media.View.extend({
4181                 className: 'media-embed',
4182
4183                 initialize: function() {
4184                         this.url = new media.view.EmbedUrl({
4185                                 controller: this.controller,
4186                                 model:      this.model.props
4187                         }).render();
4188
4189                         this.views.set([ this.url ]);
4190                         this.refresh();
4191                         this.model.on( 'change:type', this.refresh, this );
4192                         this.model.on( 'change:loading', this.loading, this );
4193                 },
4194
4195                 settings: function( view ) {
4196                         if ( this._settings )
4197                                 this._settings.remove();
4198                         this._settings = view;
4199                         this.views.add( view );
4200                 },
4201
4202                 refresh: function() {
4203                         var type = this.model.get('type'),
4204                                 constructor;
4205
4206                         if ( 'image' === type )
4207                                 constructor = media.view.EmbedImage;
4208                         else if ( 'link' === type )
4209                                 constructor = media.view.EmbedLink;
4210                         else
4211                                 return;
4212
4213                         this.settings( new constructor({
4214                                 controller: this.controller,
4215                                 model:      this.model.props,
4216                                 priority:   40
4217                         }) );
4218                 },
4219
4220                 loading: function() {
4221                         this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
4222                 }
4223         });
4224
4225         /**
4226          * wp.media.view.EmbedUrl
4227          */
4228         media.view.EmbedUrl = media.View.extend({
4229                 tagName:   'label',
4230                 className: 'embed-url',
4231
4232                 events: {
4233                         'input':  'url',
4234                         'keyup':  'url',
4235                         'change': 'url'
4236                 },
4237
4238                 initialize: function() {
4239                         this.input = this.make( 'input', {
4240                                 type:  'text',
4241                                 value: this.model.get('url') || ''
4242                         });
4243
4244                         this.spinner = this.make( 'span', {
4245                                 'class': 'spinner'
4246                         });
4247
4248                         this.$input = $( this.input );
4249                         this.$el.append([ this.input, this.spinner ]);
4250
4251                         this.model.on( 'change:url', this.render, this );
4252                 },
4253
4254                 render: function() {
4255                         var $input = this.$input;
4256
4257                         if ( $input.is(':focus') )
4258                                 return;
4259
4260                         this.input.value = this.model.get('url') || 'http://';
4261                         media.View.prototype.render.apply( this, arguments );
4262                         return this;
4263                 },
4264
4265                 ready: function() {
4266                         this.focus();
4267                 },
4268
4269                 url: function( event ) {
4270                         this.model.set( 'url', event.target.value );
4271                 },
4272
4273                 focus: function() {
4274                         var $input = this.$input;
4275                         // If the input is visible, focus and select its contents.
4276                         if ( $input.is(':visible') )
4277                                 $input.focus()[0].select();
4278                 }
4279         });
4280
4281         /**
4282          * wp.media.view.EmbedLink
4283          */
4284         media.view.EmbedLink = media.view.Settings.extend({
4285                 className: 'embed-link-settings',
4286                 template:  media.template('embed-link-settings')
4287         });
4288
4289         /**
4290          * wp.media.view.EmbedImage
4291          */
4292         media.view.EmbedImage =  media.view.Settings.AttachmentDisplay.extend({
4293                 className: 'embed-image-settings',
4294                 template:  media.template('embed-image-settings'),
4295
4296                 initialize: function() {
4297                         media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
4298                         this.model.on( 'change:url', this.updateImage, this );
4299                 },
4300
4301                 updateImage: function() {
4302                         this.$('img').attr( 'src', this.model.get('url') );
4303                 }
4304         });
4305 }(jQuery));