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