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