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