~canonical-sysadmins/wordpress/4.7.4

« back to all changes in this revision

Viewing changes to wp-includes/js/media-views.js

  • Committer: Jacek Nykis
  • Date: 2015-01-05 16:17:05 UTC
  • Revision ID: jacek.nykis@canonical.com-20150105161705-w544l1h5mcg7u4w9
Initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
                                        priority:  80,
 
2993
                                        displaySettings: true
 
2994
                                }),
 
2995
                                new media.controller.EditImage( {
 
2996
                                        image: this.image,
 
2997
                                        selection: this.options.selection
 
2998
                                } )
 
2999
                        ]);
 
3000
                },
 
3001
 
 
3002
                imageDetailsContent: function( options ) {
 
3003
                        options.view = new media.view.ImageDetails({
 
3004
                                controller: this,
 
3005
                                model: this.state().image,
 
3006
                                attachment: this.state().image.attachment
 
3007
                        });
 
3008
                },
 
3009
 
 
3010
                editImageContent: function() {
 
3011
                        var state = this.state(),
 
3012
                                model = state.get('image'),
 
3013
                                view;
 
3014
 
 
3015
                        if ( ! model ) {
 
3016
                                return;
 
3017
                        }
 
3018
 
 
3019
                        view = new media.view.EditImage( { model: model, controller: this } ).render();
 
3020
 
 
3021
                        this.content.set( view );
 
3022
 
 
3023
                        // after bringing in the frame, load the actual editor via an ajax call
 
3024
                        view.loadEditor();
 
3025
 
 
3026
                },
 
3027
 
 
3028
                renderImageDetailsToolbar: function() {
 
3029
                        this.toolbar.set( new media.view.Toolbar({
 
3030
                                controller: this,
 
3031
                                items: {
 
3032
                                        select: {
 
3033
                                                style:    'primary',
 
3034
                                                text:     l10n.update,
 
3035
                                                priority: 80,
 
3036
 
 
3037
                                                click: function() {
 
3038
                                                        var controller = this.controller,
 
3039
                                                                state = controller.state();
 
3040
 
 
3041
                                                        controller.close();
 
3042
 
 
3043
                                                        // not sure if we want to use wp.media.string.image which will create a shortcode or
 
3044
                                                        // perhaps wp.html.string to at least to build the <img />
 
3045
                                                        state.trigger( 'update', controller.image.toJSON() );
 
3046
 
 
3047
                                                        // Restore and reset the default state.
 
3048
                                                        controller.setState( controller.options.state );
 
3049
                                                        controller.reset();
 
3050
                                                }
 
3051
                                        }
 
3052
                                }
 
3053
                        }) );
 
3054
                },
 
3055
 
 
3056
                renderReplaceImageToolbar: function() {
 
3057
                        var frame = this,
 
3058
                                lastState = frame.lastState(),
 
3059
                                previous = lastState && lastState.id;
 
3060
 
 
3061
                        this.toolbar.set( new media.view.Toolbar({
 
3062
                                controller: this,
 
3063
                                items: {
 
3064
                                        back: {
 
3065
                                                text:     l10n.back,
 
3066
                                                priority: 20,
 
3067
                                                click:    function() {
 
3068
                                                        if ( previous ) {
 
3069
                                                                frame.setState( previous );
 
3070
                                                        } else {
 
3071
                                                                frame.close();
 
3072
                                                        }
 
3073
                                                }
 
3074
                                        },
 
3075
 
 
3076
                                        replace: {
 
3077
                                                style:    'primary',
 
3078
                                                text:     l10n.replace,
 
3079
                                                priority: 80,
 
3080
 
 
3081
                                                click: function() {
 
3082
                                                        var controller = this.controller,
 
3083
                                                                state = controller.state(),
 
3084
                                                                selection = state.get( 'selection' ),
 
3085
                                                                attachment = selection.single();
 
3086
 
 
3087
                                                        controller.close();
 
3088
 
 
3089
                                                        controller.image.changeAttachment( attachment, state.display( attachment ) );
 
3090
 
 
3091
                                                        // not sure if we want to use wp.media.string.image which will create a shortcode or
 
3092
                                                        // perhaps wp.html.string to at least to build the <img />
 
3093
                                                        state.trigger( 'replace', controller.image.toJSON() );
 
3094
 
 
3095
                                                        // Restore and reset the default state.
 
3096
                                                        controller.setState( controller.options.state );
 
3097
                                                        controller.reset();
 
3098
                                                }
 
3099
                                        }
 
3100
                                }
 
3101
                        }) );
 
3102
                }
 
3103
 
 
3104
        });
 
3105
 
 
3106
        /**
 
3107
         * wp.media.view.Modal
 
3108
         *
 
3109
         * @constructor
 
3110
         * @augments wp.media.View
 
3111
         * @augments wp.Backbone.View
 
3112
         * @augments Backbone.View
 
3113
         */
 
3114
        media.view.Modal = media.View.extend({
 
3115
                tagName:  'div',
 
3116
                template: media.template('media-modal'),
 
3117
 
 
3118
                attributes: {
 
3119
                        tabindex: 0
 
3120
                },
 
3121
 
 
3122
                events: {
 
3123
                        'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
 
3124
                        'keydown': 'keydown'
 
3125
                },
 
3126
 
 
3127
                initialize: function() {
 
3128
                        _.defaults( this.options, {
 
3129
                                container: document.body,
 
3130
                                title:     '',
 
3131
                                propagate: true,
 
3132
                                freeze:    true
 
3133
                        });
 
3134
 
 
3135
                        this.focusManager = new media.view.FocusManager({
 
3136
                                el: this.el
 
3137
                        });
 
3138
                },
 
3139
                /**
 
3140
                 * @returns {Object}
 
3141
                 */
 
3142
                prepare: function() {
 
3143
                        return {
 
3144
                                title: this.options.title
 
3145
                        };
 
3146
                },
 
3147
 
 
3148
                /**
 
3149
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3150
                 */
 
3151
                attach: function() {
 
3152
                        if ( this.views.attached ) {
 
3153
                                return this;
 
3154
                        }
 
3155
 
 
3156
                        if ( ! this.views.rendered ) {
 
3157
                                this.render();
 
3158
                        }
 
3159
 
 
3160
                        this.$el.appendTo( this.options.container );
 
3161
 
 
3162
                        // Manually mark the view as attached and trigger ready.
 
3163
                        this.views.attached = true;
 
3164
                        this.views.ready();
 
3165
 
 
3166
                        return this.propagate('attach');
 
3167
                },
 
3168
 
 
3169
                /**
 
3170
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3171
                 */
 
3172
                detach: function() {
 
3173
                        if ( this.$el.is(':visible') ) {
 
3174
                                this.close();
 
3175
                        }
 
3176
 
 
3177
                        this.$el.detach();
 
3178
                        this.views.attached = false;
 
3179
                        return this.propagate('detach');
 
3180
                },
 
3181
 
 
3182
                /**
 
3183
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3184
                 */
 
3185
                open: function() {
 
3186
                        var $el = this.$el,
 
3187
                                options = this.options,
 
3188
                                mceEditor;
 
3189
 
 
3190
                        if ( $el.is(':visible') ) {
 
3191
                                return this;
 
3192
                        }
 
3193
 
 
3194
                        if ( ! this.views.attached ) {
 
3195
                                this.attach();
 
3196
                        }
 
3197
 
 
3198
                        // If the `freeze` option is set, record the window's scroll position.
 
3199
                        if ( options.freeze ) {
 
3200
                                this._freeze = {
 
3201
                                        scrollTop: $( window ).scrollTop()
 
3202
                                };
 
3203
                        }
 
3204
 
 
3205
                        // Disable page scrolling.
 
3206
                        $( 'body' ).addClass( 'modal-open' );
 
3207
 
 
3208
                        $el.show();
 
3209
 
 
3210
                        // Try to close the onscreen keyboard
 
3211
                        if ( 'ontouchend' in document ) {
 
3212
                                if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor )  && ! mceEditor.isHidden() && mceEditor.iframeElement ) {
 
3213
                                        mceEditor.iframeElement.focus();
 
3214
                                        mceEditor.iframeElement.blur();
 
3215
 
 
3216
                                        setTimeout( function() {
 
3217
                                                mceEditor.iframeElement.blur();
 
3218
                                        }, 100 );
 
3219
                                }
 
3220
                        }
 
3221
 
 
3222
                        this.$el.focus();
 
3223
 
 
3224
                        return this.propagate('open');
 
3225
                },
 
3226
 
 
3227
                /**
 
3228
                 * @param {Object} options
 
3229
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3230
                 */
 
3231
                close: function( options ) {
 
3232
                        var freeze = this._freeze;
 
3233
 
 
3234
                        if ( ! this.views.attached || ! this.$el.is(':visible') ) {
 
3235
                                return this;
 
3236
                        }
 
3237
 
 
3238
                        // Enable page scrolling.
 
3239
                        $( 'body' ).removeClass( 'modal-open' );
 
3240
 
 
3241
                        // Hide modal and remove restricted media modal tab focus once it's closed
 
3242
                        this.$el.hide().undelegate( 'keydown' );
 
3243
 
 
3244
                        // Put focus back in useful location once modal is closed
 
3245
                        $('#wpbody-content').focus();
 
3246
 
 
3247
                        this.propagate('close');
 
3248
 
 
3249
                        // If the `freeze` option is set, restore the container's scroll position.
 
3250
                        if ( freeze ) {
 
3251
                                $( window ).scrollTop( freeze.scrollTop );
 
3252
                        }
 
3253
 
 
3254
                        if ( options && options.escape ) {
 
3255
                                this.propagate('escape');
 
3256
                        }
 
3257
 
 
3258
                        return this;
 
3259
                },
 
3260
                /**
 
3261
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3262
                 */
 
3263
                escape: function() {
 
3264
                        return this.close({ escape: true });
 
3265
                },
 
3266
                /**
 
3267
                 * @param {Object} event
 
3268
                 */
 
3269
                escapeHandler: function( event ) {
 
3270
                        event.preventDefault();
 
3271
                        this.escape();
 
3272
                },
 
3273
 
 
3274
                /**
 
3275
                 * @param {Array|Object} content Views to register to '.media-modal-content'
 
3276
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3277
                 */
 
3278
                content: function( content ) {
 
3279
                        this.views.set( '.media-modal-content', content );
 
3280
                        return this;
 
3281
                },
 
3282
 
 
3283
                /**
 
3284
                 * Triggers a modal event and if the `propagate` option is set,
 
3285
                 * forwards events to the modal's controller.
 
3286
                 *
 
3287
                 * @param {string} id
 
3288
                 * @returns {wp.media.view.Modal} Returns itself to allow chaining
 
3289
                 */
 
3290
                propagate: function( id ) {
 
3291
                        this.trigger( id );
 
3292
 
 
3293
                        if ( this.options.propagate ) {
 
3294
                                this.controller.trigger( id );
 
3295
                        }
 
3296
 
 
3297
                        return this;
 
3298
                },
 
3299
                /**
 
3300
                 * @param {Object} event
 
3301
                 */
 
3302
                keydown: function( event ) {
 
3303
                        // Close the modal when escape is pressed.
 
3304
                        if ( 27 === event.which && this.$el.is(':visible') ) {
 
3305
                                this.escape();
 
3306
                                event.stopImmediatePropagation();
 
3307
                        }
 
3308
                }
 
3309
        });
 
3310
 
 
3311
        /**
 
3312
         * wp.media.view.FocusManager
 
3313
         *
 
3314
         * @constructor
 
3315
         * @augments wp.media.View
 
3316
         * @augments wp.Backbone.View
 
3317
         * @augments Backbone.View
 
3318
         */
 
3319
        media.view.FocusManager = media.View.extend({
 
3320
 
 
3321
                events: {
 
3322
                        'keydown': 'constrainTabbing'
 
3323
                },
 
3324
 
 
3325
                focus: function() { // Reset focus on first left menu item
 
3326
                        this.$('.media-menu-item').first().focus();
 
3327
                },
 
3328
                /**
 
3329
                 * @param {Object} event
 
3330
                 */
 
3331
                constrainTabbing: function( event ) {
 
3332
                        var tabbables;
 
3333
 
 
3334
                        // Look for the tab key.
 
3335
                        if ( 9 !== event.keyCode ) {
 
3336
                                return;
 
3337
                        }
 
3338
 
 
3339
                        tabbables = this.$( ':tabbable' );
 
3340
 
 
3341
                        // Keep tab focus within media modal while it's open
 
3342
                        if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
 
3343
                                tabbables.first().focus();
 
3344
                                return false;
 
3345
                        } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
 
3346
                                tabbables.last().focus();
 
3347
                                return false;
 
3348
                        }
 
3349
                }
 
3350
 
 
3351
        });
 
3352
 
 
3353
        /**
 
3354
         * wp.media.view.UploaderWindow
 
3355
         *
 
3356
         * @constructor
 
3357
         * @augments wp.media.View
 
3358
         * @augments wp.Backbone.View
 
3359
         * @augments Backbone.View
 
3360
         */
 
3361
        media.view.UploaderWindow = media.View.extend({
 
3362
                tagName:   'div',
 
3363
                className: 'uploader-window',
 
3364
                template:  media.template('uploader-window'),
 
3365
 
 
3366
                initialize: function() {
 
3367
                        var uploader;
 
3368
 
 
3369
                        this.$browser = $('<a href="#" class="browser" />').hide().appendTo('body');
 
3370
 
 
3371
                        uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
 
3372
                                dropzone:  this.$el,
 
3373
                                browser:   this.$browser,
 
3374
                                params:    {}
 
3375
                        });
 
3376
 
 
3377
                        // Ensure the dropzone is a jQuery collection.
 
3378
                        if ( uploader.dropzone && ! (uploader.dropzone instanceof $) ) {
 
3379
                                uploader.dropzone = $( uploader.dropzone );
 
3380
                        }
 
3381
 
 
3382
                        this.controller.on( 'activate', this.refresh, this );
 
3383
 
 
3384
                        this.controller.on( 'detach', function() {
 
3385
                                this.$browser.remove();
 
3386
                        }, this );
 
3387
                },
 
3388
 
 
3389
                refresh: function() {
 
3390
                        if ( this.uploader ) {
 
3391
                                this.uploader.refresh();
 
3392
                        }
 
3393
                },
 
3394
 
 
3395
                ready: function() {
 
3396
                        var postId = media.view.settings.post.id,
 
3397
                                dropzone;
 
3398
 
 
3399
                        // If the uploader already exists, bail.
 
3400
                        if ( this.uploader ) {
 
3401
                                return;
 
3402
                        }
 
3403
 
 
3404
                        if ( postId ) {
 
3405
                                this.options.uploader.params.post_id = postId;
 
3406
                        }
 
3407
                        this.uploader = new wp.Uploader( this.options.uploader );
 
3408
 
 
3409
                        dropzone = this.uploader.dropzone;
 
3410
                        dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
 
3411
                        dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
 
3412
 
 
3413
                        $( this.uploader ).on( 'uploader:ready', _.bind( this._ready, this ) );
 
3414
                },
 
3415
 
 
3416
                _ready: function() {
 
3417
                        this.controller.trigger( 'uploader:ready' );
 
3418
                },
 
3419
 
 
3420
                show: function() {
 
3421
                        var $el = this.$el.show();
 
3422
 
 
3423
                        // Ensure that the animation is triggered by waiting until
 
3424
                        // the transparent element is painted into the DOM.
 
3425
                        _.defer( function() {
 
3426
                                $el.css({ opacity: 1 });
 
3427
                        });
 
3428
                },
 
3429
 
 
3430
                hide: function() {
 
3431
                        var $el = this.$el.css({ opacity: 0 });
 
3432
 
 
3433
                        media.transition( $el ).done( function() {
 
3434
                                // Transition end events are subject to race conditions.
 
3435
                                // Make sure that the value is set as intended.
 
3436
                                if ( '0' === $el.css('opacity') ) {
 
3437
                                        $el.hide();
 
3438
                                }
 
3439
                        });
 
3440
 
 
3441
                        // https://core.trac.wordpress.org/ticket/27341
 
3442
                        _.delay( function() {
 
3443
                                if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
 
3444
                                        $el.hide();
 
3445
                                }
 
3446
                        }, 500 );
 
3447
                }
 
3448
        });
 
3449
 
 
3450
        /**
 
3451
         * wp.media.view.EditorUploader
 
3452
         *
 
3453
         * @constructor
 
3454
         * @augments wp.media.View
 
3455
         * @augments wp.Backbone.View
 
3456
         * @augments Backbone.View
 
3457
         */
 
3458
        media.view.EditorUploader = media.View.extend({
 
3459
                tagName:   'div',
 
3460
                className: 'uploader-editor',
 
3461
                template:  media.template( 'uploader-editor' ),
 
3462
 
 
3463
                localDrag: false,
 
3464
                overContainer: false,
 
3465
                overDropzone: false,
 
3466
                draggingFile: null,
 
3467
 
 
3468
                initialize: function() {
 
3469
                        var self = this;
 
3470
 
 
3471
                        this.initialized = false;
 
3472
 
 
3473
                        // Bail if not enabled or UA does not support drag'n'drop or File API.
 
3474
                        if ( ! window.tinyMCEPreInit || ! window.tinyMCEPreInit.dragDropUpload || ! this.browserSupport() ) {
 
3475
                                return this;
 
3476
                        }
 
3477
 
 
3478
                        this.$document = $(document);
 
3479
                        this.dropzones = [];
 
3480
                        this.files = [];
 
3481
 
 
3482
                        this.$document.on( 'drop', '.uploader-editor', _.bind( this.drop, this ) );
 
3483
                        this.$document.on( 'dragover', '.uploader-editor', _.bind( this.dropzoneDragover, this ) );
 
3484
                        this.$document.on( 'dragleave', '.uploader-editor', _.bind( this.dropzoneDragleave, this ) );
 
3485
                        this.$document.on( 'click', '.uploader-editor', _.bind( this.click, this ) );
 
3486
 
 
3487
                        this.$document.on( 'dragover', _.bind( this.containerDragover, this ) );
 
3488
                        this.$document.on( 'dragleave', _.bind( this.containerDragleave, this ) );
 
3489
 
 
3490
                        this.$document.on( 'dragstart dragend drop', function( event ) {
 
3491
                                self.localDrag = event.type === 'dragstart';
 
3492
                        });
 
3493
 
 
3494
                        this.initialized = true;
 
3495
                        return this;
 
3496
                },
 
3497
 
 
3498
                browserSupport: function() {
 
3499
                        var supports = false, div = document.createElement('div');
 
3500
 
 
3501
                        supports = ( 'draggable' in div ) || ( 'ondragstart' in div && 'ondrop' in div );
 
3502
                        supports = supports && !! ( window.File && window.FileList && window.FileReader );
 
3503
                        return supports;
 
3504
                },
 
3505
 
 
3506
                isDraggingFile: function( event ) {
 
3507
                        if ( this.draggingFile !== null ) {
 
3508
                                return this.draggingFile;
 
3509
                        }
 
3510
 
 
3511
                        if ( _.isUndefined( event.originalEvent ) || _.isUndefined( event.originalEvent.dataTransfer ) ) {
 
3512
                                return false;
 
3513
                        }
 
3514
 
 
3515
                        this.draggingFile = _.indexOf( event.originalEvent.dataTransfer.types, 'Files' ) > -1 &&
 
3516
                                _.indexOf( event.originalEvent.dataTransfer.types, 'text/plain' ) === -1;
 
3517
 
 
3518
                        return this.draggingFile;
 
3519
                },
 
3520
 
 
3521
                refresh: function( e ) {
 
3522
                        var dropzone_id;
 
3523
                        for ( dropzone_id in this.dropzones ) {
 
3524
                                // Hide the dropzones only if dragging has left the screen.
 
3525
                                this.dropzones[ dropzone_id ].toggle( this.overContainer || this.overDropzone );
 
3526
                        }
 
3527
 
 
3528
                        if ( ! _.isUndefined( e ) ) {
 
3529
                                $( e.target ).closest( '.uploader-editor' ).toggleClass( 'droppable', this.overDropzone );
 
3530
                        }
 
3531
 
 
3532
                        if ( ! this.overContainer && ! this.overDropzone ) {
 
3533
                                this.draggingFile = null;
 
3534
                        }
 
3535
 
 
3536
                        return this;
 
3537
                },
 
3538
 
 
3539
                render: function() {
 
3540
                        if ( ! this.initialized ) {
 
3541
                                return this;
 
3542
                        }
 
3543
 
 
3544
                        media.View.prototype.render.apply( this, arguments );
 
3545
                        $( '.wp-editor-wrap, #wp-fullscreen-body' ).each( _.bind( this.attach, this ) );
 
3546
                        return this;
 
3547
                },
 
3548
 
 
3549
                attach: function( index, editor ) {
 
3550
                        // Attach a dropzone to an editor.
 
3551
                        var dropzone = this.$el.clone();
 
3552
                        this.dropzones.push( dropzone );
 
3553
                        $( editor ).append( dropzone );
 
3554
                        return this;
 
3555
                },
 
3556
 
 
3557
                drop: function( event ) {
 
3558
                        var $wrap = null;
 
3559
 
 
3560
                        this.containerDragleave( event );
 
3561
                        this.dropzoneDragleave( event );
 
3562
 
 
3563
                        this.files = event.originalEvent.dataTransfer.files;
 
3564
                        if ( this.files.length < 1 ) {
 
3565
                                return;
 
3566
                        }
 
3567
 
 
3568
                        // Set the active editor to the drop target.
 
3569
                        $wrap = $( event.target ).parents( '.wp-editor-wrap' );
 
3570
                        if ( $wrap.length > 0 && $wrap[0].id ) {
 
3571
                                window.wpActiveEditor = $wrap[0].id.slice( 3, -5 );
 
3572
                        }
 
3573
 
 
3574
                        if ( ! this.workflow ) {
 
3575
                                this.workflow = wp.media.editor.open( 'content', {
 
3576
                                        frame:    'post',
 
3577
                                        state:    'insert',
 
3578
                                        title:    wp.media.view.l10n.addMedia,
 
3579
                                        multiple: true
 
3580
                                });
 
3581
                                this.workflow.on( 'uploader:ready', this.addFiles, this );
 
3582
                        } else {
 
3583
                                this.workflow.state().reset();
 
3584
                                this.addFiles.apply( this );
 
3585
                                this.workflow.open();
 
3586
                        }
 
3587
 
 
3588
                        return false;
 
3589
                },
 
3590
 
 
3591
                addFiles: function() {
 
3592
                        if ( this.files.length ) {
 
3593
                                this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) );
 
3594
                                this.files = [];
 
3595
                        }
 
3596
                        return this;
 
3597
                },
 
3598
 
 
3599
                containerDragover: function( event ) {
 
3600
                        if ( this.localDrag || ! this.isDraggingFile( event ) ) {
 
3601
                                return;
 
3602
                        }
 
3603
 
 
3604
                        this.overContainer = true;
 
3605
                        this.refresh();
 
3606
                },
 
3607
 
 
3608
                containerDragleave: function() {
 
3609
                        this.overContainer = false;
 
3610
 
 
3611
                        // Throttle dragleave because it's called when bouncing from some elements to others.
 
3612
                        _.delay( _.bind( this.refresh, this ), 50 );
 
3613
                },
 
3614
 
 
3615
                dropzoneDragover: function( event ) {
 
3616
                        if ( this.localDrag || ! this.isDraggingFile( event ) ) {
 
3617
                                return;
 
3618
                        }
 
3619
 
 
3620
                        this.overDropzone = true;
 
3621
                        this.refresh( event );
 
3622
                        return false;
 
3623
                },
 
3624
 
 
3625
                dropzoneDragleave: function( e ) {
 
3626
                        this.overDropzone = false;
 
3627
                        _.delay( _.bind( this.refresh, this, e ), 50 );
 
3628
                },
 
3629
 
 
3630
                click: function( e ) {
 
3631
                        // In the rare case where the dropzone gets stuck, hide it on click.
 
3632
                        this.containerDragleave( e );
 
3633
                        this.dropzoneDragleave( e );
 
3634
                        this.localDrag = false;
 
3635
                }
 
3636
        });
 
3637
 
 
3638
        /**
 
3639
         * wp.media.view.UploaderInline
 
3640
         *
 
3641
         * @constructor
 
3642
         * @augments wp.media.View
 
3643
         * @augments wp.Backbone.View
 
3644
         * @augments Backbone.View
 
3645
         */
 
3646
        media.view.UploaderInline = media.View.extend({
 
3647
                tagName:   'div',
 
3648
                className: 'uploader-inline',
 
3649
                template:  media.template('uploader-inline'),
 
3650
 
 
3651
                events: {
 
3652
                        'click .close': 'hide'
 
3653
                },
 
3654
 
 
3655
                initialize: function() {
 
3656
                        _.defaults( this.options, {
 
3657
                                message: '',
 
3658
                                status:  true,
 
3659
                                canClose: false
 
3660
                        });
 
3661
 
 
3662
                        if ( ! this.options.$browser && this.controller.uploader ) {
 
3663
                                this.options.$browser = this.controller.uploader.$browser;
 
3664
                        }
 
3665
 
 
3666
                        if ( _.isUndefined( this.options.postId ) ) {
 
3667
                                this.options.postId = media.view.settings.post.id;
 
3668
                        }
 
3669
 
 
3670
                        if ( this.options.status ) {
 
3671
                                this.views.set( '.upload-inline-status', new media.view.UploaderStatus({
 
3672
                                        controller: this.controller
 
3673
                                }) );
 
3674
                        }
 
3675
                },
 
3676
 
 
3677
                prepare: function() {
 
3678
                        var suggestedWidth = this.controller.state().get('suggestedWidth'),
 
3679
                                suggestedHeight = this.controller.state().get('suggestedHeight'),
 
3680
                                data = {};
 
3681
 
 
3682
                        data.message = this.options.message;
 
3683
                        data.canClose = this.options.canClose;
 
3684
 
 
3685
                        if ( suggestedWidth && suggestedHeight ) {
 
3686
                                data.suggestedWidth = suggestedWidth;
 
3687
                                data.suggestedHeight = suggestedHeight;
 
3688
                        }
 
3689
 
 
3690
                        return data;
 
3691
                },
 
3692
                /**
 
3693
                 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
 
3694
                 */
 
3695
                dispose: function() {
 
3696
                        if ( this.disposing ) {
 
3697
                                /**
 
3698
                                 * call 'dispose' directly on the parent class
 
3699
                                 */
 
3700
                                return media.View.prototype.dispose.apply( this, arguments );
 
3701
                        }
 
3702
 
 
3703
                        // Run remove on `dispose`, so we can be sure to refresh the
 
3704
                        // uploader with a view-less DOM. Track whether we're disposing
 
3705
                        // so we don't trigger an infinite loop.
 
3706
                        this.disposing = true;
 
3707
                        return this.remove();
 
3708
                },
 
3709
                /**
 
3710
                 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
 
3711
                 */
 
3712
                remove: function() {
 
3713
                        /**
 
3714
                         * call 'remove' directly on the parent class
 
3715
                         */
 
3716
                        var result = media.View.prototype.remove.apply( this, arguments );
 
3717
 
 
3718
                        _.defer( _.bind( this.refresh, this ) );
 
3719
                        return result;
 
3720
                },
 
3721
 
 
3722
                refresh: function() {
 
3723
                        var uploader = this.controller.uploader;
 
3724
 
 
3725
                        if ( uploader ) {
 
3726
                                uploader.refresh();
 
3727
                        }
 
3728
                },
 
3729
                /**
 
3730
                 * @returns {wp.media.view.UploaderInline}
 
3731
                 */
 
3732
                ready: function() {
 
3733
                        var $browser = this.options.$browser,
 
3734
                                $placeholder;
 
3735
 
 
3736
                        if ( this.controller.uploader ) {
 
3737
                                $placeholder = this.$('.browser');
 
3738
 
 
3739
                                // Check if we've already replaced the placeholder.
 
3740
                                if ( $placeholder[0] === $browser[0] ) {
 
3741
                                        return;
 
3742
                                }
 
3743
 
 
3744
                                $browser.detach().text( $placeholder.text() );
 
3745
                                $browser[0].className = $placeholder[0].className;
 
3746
                                $placeholder.replaceWith( $browser.show() );
 
3747
                        }
 
3748
 
 
3749
                        this.refresh();
 
3750
                        return this;
 
3751
                },
 
3752
                show: function() {
 
3753
                        this.$el.removeClass( 'hidden' );
 
3754
                },
 
3755
                hide: function() {
 
3756
                        this.$el.addClass( 'hidden' );
 
3757
                }
 
3758
 
 
3759
        });
 
3760
 
 
3761
        /**
 
3762
         * wp.media.view.UploaderStatus
 
3763
         *
 
3764
         * @constructor
 
3765
         * @augments wp.media.View
 
3766
         * @augments wp.Backbone.View
 
3767
         * @augments Backbone.View
 
3768
         */
 
3769
        media.view.UploaderStatus = media.View.extend({
 
3770
                className: 'media-uploader-status',
 
3771
                template:  media.template('uploader-status'),
 
3772
 
 
3773
                events: {
 
3774
                        'click .upload-dismiss-errors': 'dismiss'
 
3775
                },
 
3776
 
 
3777
                initialize: function() {
 
3778
                        this.queue = wp.Uploader.queue;
 
3779
                        this.queue.on( 'add remove reset', this.visibility, this );
 
3780
                        this.queue.on( 'add remove reset change:percent', this.progress, this );
 
3781
                        this.queue.on( 'add remove reset change:uploading', this.info, this );
 
3782
 
 
3783
                        this.errors = wp.Uploader.errors;
 
3784
                        this.errors.reset();
 
3785
                        this.errors.on( 'add remove reset', this.visibility, this );
 
3786
                        this.errors.on( 'add', this.error, this );
 
3787
                },
 
3788
                /**
 
3789
                 * @global wp.Uploader
 
3790
                 * @returns {wp.media.view.UploaderStatus}
 
3791
                 */
 
3792
                dispose: function() {
 
3793
                        wp.Uploader.queue.off( null, null, this );
 
3794
                        /**
 
3795
                         * call 'dispose' directly on the parent class
 
3796
                         */
 
3797
                        media.View.prototype.dispose.apply( this, arguments );
 
3798
                        return this;
 
3799
                },
 
3800
 
 
3801
                visibility: function() {
 
3802
                        this.$el.toggleClass( 'uploading', !! this.queue.length );
 
3803
                        this.$el.toggleClass( 'errors', !! this.errors.length );
 
3804
                        this.$el.toggle( !! this.queue.length || !! this.errors.length );
 
3805
                },
 
3806
 
 
3807
                ready: function() {
 
3808
                        _.each({
 
3809
                                '$bar':      '.media-progress-bar div',
 
3810
                                '$index':    '.upload-index',
 
3811
                                '$total':    '.upload-total',
 
3812
                                '$filename': '.upload-filename'
 
3813
                        }, function( selector, key ) {
 
3814
                                this[ key ] = this.$( selector );
 
3815
                        }, this );
 
3816
 
 
3817
                        this.visibility();
 
3818
                        this.progress();
 
3819
                        this.info();
 
3820
                },
 
3821
 
 
3822
                progress: function() {
 
3823
                        var queue = this.queue,
 
3824
                                $bar = this.$bar;
 
3825
 
 
3826
                        if ( ! $bar || ! queue.length ) {
 
3827
                                return;
 
3828
                        }
 
3829
 
 
3830
                        $bar.width( ( queue.reduce( function( memo, attachment ) {
 
3831
                                if ( ! attachment.get('uploading') ) {
 
3832
                                        return memo + 100;
 
3833
                                }
 
3834
 
 
3835
                                var percent = attachment.get('percent');
 
3836
                                return memo + ( _.isNumber( percent ) ? percent : 100 );
 
3837
                        }, 0 ) / queue.length ) + '%' );
 
3838
                },
 
3839
 
 
3840
                info: function() {
 
3841
                        var queue = this.queue,
 
3842
                                index = 0, active;
 
3843
 
 
3844
                        if ( ! queue.length ) {
 
3845
                                return;
 
3846
                        }
 
3847
 
 
3848
                        active = this.queue.find( function( attachment, i ) {
 
3849
                                index = i;
 
3850
                                return attachment.get('uploading');
 
3851
                        });
 
3852
 
 
3853
                        this.$index.text( index + 1 );
 
3854
                        this.$total.text( queue.length );
 
3855
                        this.$filename.html( active ? this.filename( active.get('filename') ) : '' );
 
3856
                },
 
3857
                /**
 
3858
                 * @param {string} filename
 
3859
                 * @returns {string}
 
3860
                 */
 
3861
                filename: function( filename ) {
 
3862
                        return media.truncate( _.escape( filename ), 24 );
 
3863
                },
 
3864
                /**
 
3865
                 * @param {Backbone.Model} error
 
3866
                 */
 
3867
                error: function( error ) {
 
3868
                        this.views.add( '.upload-errors', new media.view.UploaderStatusError({
 
3869
                                filename: this.filename( error.get('file').name ),
 
3870
                                message:  error.get('message')
 
3871
                        }), { at: 0 });
 
3872
                },
 
3873
 
 
3874
                /**
 
3875
                 * @global wp.Uploader
 
3876
                 *
 
3877
                 * @param {Object} event
 
3878
                 */
 
3879
                dismiss: function( event ) {
 
3880
                        var errors = this.views.get('.upload-errors');
 
3881
 
 
3882
                        event.preventDefault();
 
3883
 
 
3884
                        if ( errors ) {
 
3885
                                _.invoke( errors, 'remove' );
 
3886
                        }
 
3887
                        wp.Uploader.errors.reset();
 
3888
                }
 
3889
        });
 
3890
 
 
3891
        /**
 
3892
         * wp.media.view.UploaderStatusError
 
3893
         *
 
3894
         * @constructor
 
3895
         * @augments wp.media.View
 
3896
         * @augments wp.Backbone.View
 
3897
         * @augments Backbone.View
 
3898
         */
 
3899
        media.view.UploaderStatusError = media.View.extend({
 
3900
                className: 'upload-error',
 
3901
                template:  media.template('uploader-status-error')
 
3902
        });
 
3903
 
 
3904
        /**
 
3905
         * wp.media.view.Toolbar
 
3906
         *
 
3907
         * @constructor
 
3908
         * @augments wp.media.View
 
3909
         * @augments wp.Backbone.View
 
3910
         * @augments Backbone.View
 
3911
         */
 
3912
        media.view.Toolbar = media.View.extend({
 
3913
                tagName:   'div',
 
3914
                className: 'media-toolbar',
 
3915
 
 
3916
                initialize: function() {
 
3917
                        var state = this.controller.state(),
 
3918
                                selection = this.selection = state.get('selection'),
 
3919
                                library = this.library = state.get('library');
 
3920
 
 
3921
                        this._views = {};
 
3922
 
 
3923
                        // The toolbar is composed of two `PriorityList` views.
 
3924
                        this.primary   = new media.view.PriorityList();
 
3925
                        this.secondary = new media.view.PriorityList();
 
3926
                        this.primary.$el.addClass('media-toolbar-primary search-form');
 
3927
                        this.secondary.$el.addClass('media-toolbar-secondary');
 
3928
 
 
3929
                        this.views.set([ this.secondary, this.primary ]);
 
3930
 
 
3931
                        if ( this.options.items ) {
 
3932
                                this.set( this.options.items, { silent: true });
 
3933
                        }
 
3934
 
 
3935
                        if ( ! this.options.silent ) {
 
3936
                                this.render();
 
3937
                        }
 
3938
 
 
3939
                        if ( selection ) {
 
3940
                                selection.on( 'add remove reset', this.refresh, this );
 
3941
                        }
 
3942
 
 
3943
                        if ( library ) {
 
3944
                                library.on( 'add remove reset', this.refresh, this );
 
3945
                        }
 
3946
                },
 
3947
                /**
 
3948
                 * @returns {wp.media.view.Toolbar} Returns itsef to allow chaining
 
3949
                 */
 
3950
                dispose: function() {
 
3951
                        if ( this.selection ) {
 
3952
                                this.selection.off( null, null, this );
 
3953
                        }
 
3954
 
 
3955
                        if ( this.library ) {
 
3956
                                this.library.off( null, null, this );
 
3957
                        }
 
3958
                        /**
 
3959
                         * call 'dispose' directly on the parent class
 
3960
                         */
 
3961
                        return media.View.prototype.dispose.apply( this, arguments );
 
3962
                },
 
3963
 
 
3964
                ready: function() {
 
3965
                        this.refresh();
 
3966
                },
 
3967
 
 
3968
                /**
 
3969
                 * @param {string} id
 
3970
                 * @param {Backbone.View|Object} view
 
3971
                 * @param {Object} [options={}]
 
3972
                 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining
 
3973
                 */
 
3974
                set: function( id, view, options ) {
 
3975
                        var list;
 
3976
                        options = options || {};
 
3977
 
 
3978
                        // Accept an object with an `id` : `view` mapping.
 
3979
                        if ( _.isObject( id ) ) {
 
3980
                                _.each( id, function( view, id ) {
 
3981
                                        this.set( id, view, { silent: true });
 
3982
                                }, this );
 
3983
 
 
3984
                        } else {
 
3985
                                if ( ! ( view instanceof Backbone.View ) ) {
 
3986
                                        view.classes = [ 'media-button-' + id ].concat( view.classes || [] );
 
3987
                                        view = new media.view.Button( view ).render();
 
3988
                                }
 
3989
 
 
3990
                                view.controller = view.controller || this.controller;
 
3991
 
 
3992
                                this._views[ id ] = view;
 
3993
 
 
3994
                                list = view.options.priority < 0 ? 'secondary' : 'primary';
 
3995
                                this[ list ].set( id, view, options );
 
3996
                        }
 
3997
 
 
3998
                        if ( ! options.silent ) {
 
3999
                                this.refresh();
 
4000
                        }
 
4001
 
 
4002
                        return this;
 
4003
                },
 
4004
                /**
 
4005
                 * @param {string} id
 
4006
                 * @returns {wp.media.view.Button}
 
4007
                 */
 
4008
                get: function( id ) {
 
4009
                        return this._views[ id ];
 
4010
                },
 
4011
                /**
 
4012
                 * @param {string} id
 
4013
                 * @param {Object} options
 
4014
                 * @returns {wp.media.view.Toolbar} Returns itself to allow chaining
 
4015
                 */
 
4016
                unset: function( id, options ) {
 
4017
                        delete this._views[ id ];
 
4018
                        this.primary.unset( id, options );
 
4019
                        this.secondary.unset( id, options );
 
4020
 
 
4021
                        if ( ! options || ! options.silent ) {
 
4022
                                this.refresh();
 
4023
                        }
 
4024
                        return this;
 
4025
                },
 
4026
 
 
4027
                refresh: function() {
 
4028
                        var state = this.controller.state(),
 
4029
                                library = state.get('library'),
 
4030
                                selection = state.get('selection');
 
4031
 
 
4032
                        _.each( this._views, function( button ) {
 
4033
                                if ( ! button.model || ! button.options || ! button.options.requires ) {
 
4034
                                        return;
 
4035
                                }
 
4036
 
 
4037
                                var requires = button.options.requires,
 
4038
                                        disabled = false;
 
4039
 
 
4040
                                // Prevent insertion of attachments if any of them are still uploading
 
4041
                                disabled = _.some( selection.models, function( attachment ) {
 
4042
                                        return attachment.get('uploading') === true;
 
4043
                                });
 
4044
 
 
4045
                                if ( requires.selection && selection && ! selection.length ) {
 
4046
                                        disabled = true;
 
4047
                                } else if ( requires.library && library && ! library.length ) {
 
4048
                                        disabled = true;
 
4049
                                }
 
4050
                                button.model.set( 'disabled', disabled );
 
4051
                        });
 
4052
                }
 
4053
        });
 
4054
 
 
4055
        /**
 
4056
         * wp.media.view.Toolbar.Select
 
4057
         *
 
4058
         * @constructor
 
4059
         * @augments wp.media.view.Toolbar
 
4060
         * @augments wp.media.View
 
4061
         * @augments wp.Backbone.View
 
4062
         * @augments Backbone.View
 
4063
         */
 
4064
        media.view.Toolbar.Select = media.view.Toolbar.extend({
 
4065
                initialize: function() {
 
4066
                        var options = this.options;
 
4067
 
 
4068
                        _.bindAll( this, 'clickSelect' );
 
4069
 
 
4070
                        _.defaults( options, {
 
4071
                                event: 'select',
 
4072
                                state: false,
 
4073
                                reset: true,
 
4074
                                close: true,
 
4075
                                text:  l10n.select,
 
4076
 
 
4077
                                // Does the button rely on the selection?
 
4078
                                requires: {
 
4079
                                        selection: true
 
4080
                                }
 
4081
                        });
 
4082
 
 
4083
                        options.items = _.defaults( options.items || {}, {
 
4084
                                select: {
 
4085
                                        style:    'primary',
 
4086
                                        text:     options.text,
 
4087
                                        priority: 80,
 
4088
                                        click:    this.clickSelect,
 
4089
                                        requires: options.requires
 
4090
                                }
 
4091
                        });
 
4092
                        /**
 
4093
                         * call 'initialize' directly on the parent class
 
4094
                         */
 
4095
                        media.view.Toolbar.prototype.initialize.apply( this, arguments );
 
4096
                },
 
4097
 
 
4098
                clickSelect: function() {
 
4099
                        var options = this.options,
 
4100
                                controller = this.controller;
 
4101
 
 
4102
                        if ( options.close ) {
 
4103
                                controller.close();
 
4104
                        }
 
4105
 
 
4106
                        if ( options.event ) {
 
4107
                                controller.state().trigger( options.event );
 
4108
                        }
 
4109
 
 
4110
                        if ( options.state ) {
 
4111
                                controller.setState( options.state );
 
4112
                        }
 
4113
 
 
4114
                        if ( options.reset ) {
 
4115
                                controller.reset();
 
4116
                        }
 
4117
                }
 
4118
        });
 
4119
 
 
4120
        /**
 
4121
         * wp.media.view.Toolbar.Embed
 
4122
         *
 
4123
         * @constructor
 
4124
         * @augments wp.media.view.Toolbar.Select
 
4125
         * @augments wp.media.view.Toolbar
 
4126
         * @augments wp.media.View
 
4127
         * @augments wp.Backbone.View
 
4128
         * @augments Backbone.View
 
4129
         */
 
4130
        media.view.Toolbar.Embed = media.view.Toolbar.Select.extend({
 
4131
                initialize: function() {
 
4132
                        _.defaults( this.options, {
 
4133
                                text: l10n.insertIntoPost,
 
4134
                                requires: false
 
4135
                        });
 
4136
                        /**
 
4137
                         * call 'initialize' directly on the parent class
 
4138
                         */
 
4139
                        media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
 
4140
                },
 
4141
 
 
4142
                refresh: function() {
 
4143
                        var url = this.controller.state().props.get('url');
 
4144
                        this.get('select').model.set( 'disabled', ! url || url === 'http://' );
 
4145
                        /**
 
4146
                         * call 'refresh' directly on the parent class
 
4147
                         */
 
4148
                        media.view.Toolbar.Select.prototype.refresh.apply( this, arguments );
 
4149
                }
 
4150
        });
 
4151
 
 
4152
        /**
 
4153
         * wp.media.view.Button
 
4154
         *
 
4155
         * @constructor
 
4156
         * @augments wp.media.View
 
4157
         * @augments wp.Backbone.View
 
4158
         * @augments Backbone.View
 
4159
         */
 
4160
        media.view.Button = media.View.extend({
 
4161
                tagName:    'a',
 
4162
                className:  'media-button',
 
4163
                attributes: { href: '#' },
 
4164
 
 
4165
                events: {
 
4166
                        'click': 'click'
 
4167
                },
 
4168
 
 
4169
                defaults: {
 
4170
                        text:     '',
 
4171
                        style:    '',
 
4172
                        size:     'large',
 
4173
                        disabled: false
 
4174
                },
 
4175
 
 
4176
                initialize: function() {
 
4177
                        /**
 
4178
                         * Create a model with the provided `defaults`.
 
4179
                         *
 
4180
                         * @member {Backbone.Model}
 
4181
                         */
 
4182
                        this.model = new Backbone.Model( this.defaults );
 
4183
 
 
4184
                        // If any of the `options` have a key from `defaults`, apply its
 
4185
                        // value to the `model` and remove it from the `options object.
 
4186
                        _.each( this.defaults, function( def, key ) {
 
4187
                                var value = this.options[ key ];
 
4188
                                if ( _.isUndefined( value ) ) {
 
4189
                                        return;
 
4190
                                }
 
4191
 
 
4192
                                this.model.set( key, value );
 
4193
                                delete this.options[ key ];
 
4194
                        }, this );
 
4195
 
 
4196
                        this.model.on( 'change', this.render, this );
 
4197
                },
 
4198
                /**
 
4199
                 * @returns {wp.media.view.Button} Returns itself to allow chaining
 
4200
                 */
 
4201
                render: function() {
 
4202
                        var classes = [ 'button', this.className ],
 
4203
                                model = this.model.toJSON();
 
4204
 
 
4205
                        if ( model.style ) {
 
4206
                                classes.push( 'button-' + model.style );
 
4207
                        }
 
4208
 
 
4209
                        if ( model.size ) {
 
4210
                                classes.push( 'button-' + model.size );
 
4211
                        }
 
4212
 
 
4213
                        classes = _.uniq( classes.concat( this.options.classes ) );
 
4214
                        this.el.className = classes.join(' ');
 
4215
 
 
4216
                        this.$el.attr( 'disabled', model.disabled );
 
4217
                        this.$el.text( this.model.get('text') );
 
4218
 
 
4219
                        return this;
 
4220
                },
 
4221
                /**
 
4222
                 * @param {Object} event
 
4223
                 */
 
4224
                click: function( event ) {
 
4225
                        if ( '#' === this.attributes.href ) {
 
4226
                                event.preventDefault();
 
4227
                        }
 
4228
 
 
4229
                        if ( this.options.click && ! this.model.get('disabled') ) {
 
4230
                                this.options.click.apply( this, arguments );
 
4231
                        }
 
4232
                }
 
4233
        });
 
4234
 
 
4235
        /**
 
4236
         * wp.media.view.ButtonGroup
 
4237
         *
 
4238
         * @constructor
 
4239
         * @augments wp.media.View
 
4240
         * @augments wp.Backbone.View
 
4241
         * @augments Backbone.View
 
4242
         */
 
4243
        media.view.ButtonGroup = media.View.extend({
 
4244
                tagName:   'div',
 
4245
                className: 'button-group button-large media-button-group',
 
4246
 
 
4247
                initialize: function() {
 
4248
                        /**
 
4249
                         * @member {wp.media.view.Button[]}
 
4250
                         */
 
4251
                        this.buttons = _.map( this.options.buttons || [], function( button ) {
 
4252
                                if ( button instanceof Backbone.View ) {
 
4253
                                        return button;
 
4254
                                } else {
 
4255
                                        return new media.view.Button( button ).render();
 
4256
                                }
 
4257
                        });
 
4258
 
 
4259
                        delete this.options.buttons;
 
4260
 
 
4261
                        if ( this.options.classes ) {
 
4262
                                this.$el.addClass( this.options.classes );
 
4263
                        }
 
4264
                },
 
4265
 
 
4266
                /**
 
4267
                 * @returns {wp.media.view.ButtonGroup}
 
4268
                 */
 
4269
                render: function() {
 
4270
                        this.$el.html( $( _.pluck( this.buttons, 'el' ) ).detach() );
 
4271
                        return this;
 
4272
                }
 
4273
        });
 
4274
 
 
4275
        /**
 
4276
         * wp.media.view.PriorityList
 
4277
         *
 
4278
         * @constructor
 
4279
         * @augments wp.media.View
 
4280
         * @augments wp.Backbone.View
 
4281
         * @augments Backbone.View
 
4282
         */
 
4283
        media.view.PriorityList = media.View.extend({
 
4284
                tagName:   'div',
 
4285
 
 
4286
                initialize: function() {
 
4287
                        this._views = {};
 
4288
 
 
4289
                        this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
 
4290
                        delete this.options.views;
 
4291
 
 
4292
                        if ( ! this.options.silent ) {
 
4293
                                this.render();
 
4294
                        }
 
4295
                },
 
4296
                /**
 
4297
                 * @param {string} id
 
4298
                 * @param {wp.media.View|Object} view
 
4299
                 * @param {Object} options
 
4300
                 * @returns {wp.media.view.PriorityList} Returns itself to allow chaining
 
4301
                 */
 
4302
                set: function( id, view, options ) {
 
4303
                        var priority, views, index;
 
4304
 
 
4305
                        options = options || {};
 
4306
 
 
4307
                        // Accept an object with an `id` : `view` mapping.
 
4308
                        if ( _.isObject( id ) ) {
 
4309
                                _.each( id, function( view, id ) {
 
4310
                                        this.set( id, view );
 
4311
                                }, this );
 
4312
                                return this;
 
4313
                        }
 
4314
 
 
4315
                        if ( ! (view instanceof Backbone.View) ) {
 
4316
                                view = this.toView( view, id, options );
 
4317
                        }
 
4318
                        view.controller = view.controller || this.controller;
 
4319
 
 
4320
                        this.unset( id );
 
4321
 
 
4322
                        priority = view.options.priority || 10;
 
4323
                        views = this.views.get() || [];
 
4324
 
 
4325
                        _.find( views, function( existing, i ) {
 
4326
                                if ( existing.options.priority > priority ) {
 
4327
                                        index = i;
 
4328
                                        return true;
 
4329
                                }
 
4330
                        });
 
4331
 
 
4332
                        this._views[ id ] = view;
 
4333
                        this.views.add( view, {
 
4334
                                at: _.isNumber( index ) ? index : views.length || 0
 
4335
                        });
 
4336
 
 
4337
                        return this;
 
4338
                },
 
4339
                /**
 
4340
                 * @param {string} id
 
4341
                 * @returns {wp.media.View}
 
4342
                 */
 
4343
                get: function( id ) {
 
4344
                        return this._views[ id ];
 
4345
                },
 
4346
                /**
 
4347
                 * @param {string} id
 
4348
                 * @returns {wp.media.view.PriorityList}
 
4349
                 */
 
4350
                unset: function( id ) {
 
4351
                        var view = this.get( id );
 
4352
 
 
4353
                        if ( view ) {
 
4354
                                view.remove();
 
4355
                        }
 
4356
 
 
4357
                        delete this._views[ id ];
 
4358
                        return this;
 
4359
                },
 
4360
                /**
 
4361
                 * @param {Object} options
 
4362
                 * @returns {wp.media.View}
 
4363
                 */
 
4364
                toView: function( options ) {
 
4365
                        return new media.View( options );
 
4366
                }
 
4367
        });
 
4368
 
 
4369
        /**
 
4370
         * wp.media.view.MenuItem
 
4371
         *
 
4372
         * @constructor
 
4373
         * @augments wp.media.View
 
4374
         * @augments wp.Backbone.View
 
4375
         * @augments Backbone.View
 
4376
         */
 
4377
        media.view.MenuItem = media.View.extend({
 
4378
                tagName:   'a',
 
4379
                className: 'media-menu-item',
 
4380
 
 
4381
                attributes: {
 
4382
                        href: '#'
 
4383
                },
 
4384
 
 
4385
                events: {
 
4386
                        'click': '_click'
 
4387
                },
 
4388
                /**
 
4389
                 * @param {Object} event
 
4390
                 */
 
4391
                _click: function( event ) {
 
4392
                        var clickOverride = this.options.click;
 
4393
 
 
4394
                        if ( event ) {
 
4395
                                event.preventDefault();
 
4396
                        }
 
4397
 
 
4398
                        if ( clickOverride ) {
 
4399
                                clickOverride.call( this );
 
4400
                        } else {
 
4401
                                this.click();
 
4402
                        }
 
4403
 
 
4404
                        // When selecting a tab along the left side,
 
4405
                        // focus should be transferred into the main panel
 
4406
                        if ( ! isTouchDevice ) {
 
4407
                                $('.media-frame-content input').first().focus();
 
4408
                        }
 
4409
                },
 
4410
 
 
4411
                click: function() {
 
4412
                        var state = this.options.state;
 
4413
 
 
4414
                        if ( state ) {
 
4415
                                this.controller.setState( state );
 
4416
                                this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
 
4417
                        }
 
4418
                },
 
4419
                /**
 
4420
                 * @returns {wp.media.view.MenuItem} returns itself to allow chaining
 
4421
                 */
 
4422
                render: function() {
 
4423
                        var options = this.options;
 
4424
 
 
4425
                        if ( options.text ) {
 
4426
                                this.$el.text( options.text );
 
4427
                        } else if ( options.html ) {
 
4428
                                this.$el.html( options.html );
 
4429
                        }
 
4430
 
 
4431
                        return this;
 
4432
                }
 
4433
        });
 
4434
 
 
4435
        /**
 
4436
         * wp.media.view.Menu
 
4437
         *
 
4438
         * @constructor
 
4439
         * @augments wp.media.view.PriorityList
 
4440
         * @augments wp.media.View
 
4441
         * @augments wp.Backbone.View
 
4442
         * @augments Backbone.View
 
4443
         */
 
4444
        media.view.Menu = media.view.PriorityList.extend({
 
4445
                tagName:   'div',
 
4446
                className: 'media-menu',
 
4447
                property:  'state',
 
4448
                ItemView:  media.view.MenuItem,
 
4449
                region:    'menu',
 
4450
 
 
4451
                /* TODO: alternatively hide on any click anywhere
 
4452
                events: {
 
4453
                        'click': 'click'
 
4454
                },
 
4455
 
 
4456
                click: function() {
 
4457
                        this.$el.removeClass( 'visible' );
 
4458
                },
 
4459
                */
 
4460
 
 
4461
                /**
 
4462
                 * @param {Object} options
 
4463
                 * @param {string} id
 
4464
                 * @returns {wp.media.View}
 
4465
                 */
 
4466
                toView: function( options, id ) {
 
4467
                        options = options || {};
 
4468
                        options[ this.property ] = options[ this.property ] || id;
 
4469
                        return new this.ItemView( options ).render();
 
4470
                },
 
4471
 
 
4472
                ready: function() {
 
4473
                        /**
 
4474
                         * call 'ready' directly on the parent class
 
4475
                         */
 
4476
                        media.view.PriorityList.prototype.ready.apply( this, arguments );
 
4477
                        this.visibility();
 
4478
                },
 
4479
 
 
4480
                set: function() {
 
4481
                        /**
 
4482
                         * call 'set' directly on the parent class
 
4483
                         */
 
4484
                        media.view.PriorityList.prototype.set.apply( this, arguments );
 
4485
                        this.visibility();
 
4486
                },
 
4487
 
 
4488
                unset: function() {
 
4489
                        /**
 
4490
                         * call 'unset' directly on the parent class
 
4491
                         */
 
4492
                        media.view.PriorityList.prototype.unset.apply( this, arguments );
 
4493
                        this.visibility();
 
4494
                },
 
4495
 
 
4496
                visibility: function() {
 
4497
                        var region = this.region,
 
4498
                                view = this.controller[ region ].get(),
 
4499
                                views = this.views.get(),
 
4500
                                hide = ! views || views.length < 2;
 
4501
 
 
4502
                        if ( this === view ) {
 
4503
                                this.controller.$el.toggleClass( 'hide-' + region, hide );
 
4504
                        }
 
4505
                },
 
4506
                /**
 
4507
                 * @param {string} id
 
4508
                 */
 
4509
                select: function( id ) {
 
4510
                        var view = this.get( id );
 
4511
 
 
4512
                        if ( ! view ) {
 
4513
                                return;
 
4514
                        }
 
4515
 
 
4516
                        this.deselect();
 
4517
                        view.$el.addClass('active');
 
4518
                },
 
4519
 
 
4520
                deselect: function() {
 
4521
                        this.$el.children().removeClass('active');
 
4522
                },
 
4523
 
 
4524
                hide: function( id ) {
 
4525
                        var view = this.get( id );
 
4526
 
 
4527
                        if ( ! view ) {
 
4528
                                return;
 
4529
                        }
 
4530
 
 
4531
                        view.$el.addClass('hidden');
 
4532
                },
 
4533
 
 
4534
                show: function( id ) {
 
4535
                        var view = this.get( id );
 
4536
 
 
4537
                        if ( ! view ) {
 
4538
                                return;
 
4539
                        }
 
4540
 
 
4541
                        view.$el.removeClass('hidden');
 
4542
                }
 
4543
        });
 
4544
 
 
4545
        /**
 
4546
         * wp.media.view.RouterItem
 
4547
         *
 
4548
         * @constructor
 
4549
         * @augments wp.media.view.MenuItem
 
4550
         * @augments wp.media.View
 
4551
         * @augments wp.Backbone.View
 
4552
         * @augments Backbone.View
 
4553
         */
 
4554
        media.view.RouterItem = media.view.MenuItem.extend({
 
4555
                /**
 
4556
                 * On click handler to activate the content region's corresponding mode.
 
4557
                 */
 
4558
                click: function() {
 
4559
                        var contentMode = this.options.contentMode;
 
4560
                        if ( contentMode ) {
 
4561
                                this.controller.content.mode( contentMode );
 
4562
                        }
 
4563
                }
 
4564
        });
 
4565
 
 
4566
        /**
 
4567
         * wp.media.view.Router
 
4568
         *
 
4569
         * @constructor
 
4570
         * @augments wp.media.view.Menu
 
4571
         * @augments wp.media.view.PriorityList
 
4572
         * @augments wp.media.View
 
4573
         * @augments wp.Backbone.View
 
4574
         * @augments Backbone.View
 
4575
         */
 
4576
        media.view.Router = media.view.Menu.extend({
 
4577
                tagName:   'div',
 
4578
                className: 'media-router',
 
4579
                property:  'contentMode',
 
4580
                ItemView:  media.view.RouterItem,
 
4581
                region:    'router',
 
4582
 
 
4583
                initialize: function() {
 
4584
                        this.controller.on( 'content:render', this.update, this );
 
4585
                        /**
 
4586
                         * call 'initialize' directly on the parent class
 
4587
                         */
 
4588
                        media.view.Menu.prototype.initialize.apply( this, arguments );
 
4589
                },
 
4590
 
 
4591
                update: function() {
 
4592
                        var mode = this.controller.content.mode();
 
4593
                        if ( mode ) {
 
4594
                                this.select( mode );
 
4595
                        }
 
4596
                }
 
4597
        });
 
4598
 
 
4599
        /**
 
4600
         * wp.media.view.Sidebar
 
4601
         *
 
4602
         * @constructor
 
4603
         * @augments wp.media.view.PriorityList
 
4604
         * @augments wp.media.View
 
4605
         * @augments wp.Backbone.View
 
4606
         * @augments Backbone.View
 
4607
         */
 
4608
        media.view.Sidebar = media.view.PriorityList.extend({
 
4609
                className: 'media-sidebar'
 
4610
        });
 
4611
 
 
4612
        /**
 
4613
         * wp.media.view.Attachment
 
4614
         *
 
4615
         * @constructor
 
4616
         * @augments wp.media.View
 
4617
         * @augments wp.Backbone.View
 
4618
         * @augments Backbone.View
 
4619
         */
 
4620
        media.view.Attachment = media.View.extend({
 
4621
                tagName:   'li',
 
4622
                className: 'attachment',
 
4623
                template:  media.template('attachment'),
 
4624
 
 
4625
                attributes: function() {
 
4626
                        return {
 
4627
                                'tabIndex':     0,
 
4628
                                'role':         'checkbox',
 
4629
                                'aria-label':   this.model.get( 'title' ),
 
4630
                                'aria-checked': false,
 
4631
                                'data-id':      this.model.get( 'id' )
 
4632
                        };
 
4633
                },
 
4634
 
 
4635
                events: {
 
4636
                        'click .js--select-attachment':   'toggleSelectionHandler',
 
4637
                        'change [data-setting]':          'updateSetting',
 
4638
                        'change [data-setting] input':    'updateSetting',
 
4639
                        'change [data-setting] select':   'updateSetting',
 
4640
                        'change [data-setting] textarea': 'updateSetting',
 
4641
                        'click .close':                   'removeFromLibrary',
 
4642
                        'click .check':                   'checkClickHandler',
 
4643
                        'click a':                        'preventDefault',
 
4644
                        'keydown':                        'toggleSelectionHandler'
 
4645
                },
 
4646
 
 
4647
                buttons: {},
 
4648
 
 
4649
                initialize: function() {
 
4650
                        var selection = this.options.selection,
 
4651
                                options = _.defaults( this.options, {
 
4652
                                        rerenderOnModelChange: true
 
4653
                                } );
 
4654
 
 
4655
                        if ( options.rerenderOnModelChange ) {
 
4656
                                this.model.on( 'change', this.render, this );
 
4657
                        } else {
 
4658
                                this.model.on( 'change:percent', this.progress, this );
 
4659
                        }
 
4660
                        this.model.on( 'change:title', this._syncTitle, this );
 
4661
                        this.model.on( 'change:caption', this._syncCaption, this );
 
4662
                        this.model.on( 'change:artist', this._syncArtist, this );
 
4663
                        this.model.on( 'change:album', this._syncAlbum, this );
 
4664
 
 
4665
                        // Update the selection.
 
4666
                        this.model.on( 'add', this.select, this );
 
4667
                        this.model.on( 'remove', this.deselect, this );
 
4668
                        if ( selection ) {
 
4669
                                selection.on( 'reset', this.updateSelect, this );
 
4670
                                // Update the model's details view.
 
4671
                                this.model.on( 'selection:single selection:unsingle', this.details, this );
 
4672
                                this.details( this.model, this.controller.state().get('selection') );
 
4673
                        }
 
4674
                },
 
4675
                /**
 
4676
                 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
 
4677
                 */
 
4678
                dispose: function() {
 
4679
                        var selection = this.options.selection;
 
4680
 
 
4681
                        // Make sure all settings are saved before removing the view.
 
4682
                        this.updateAll();
 
4683
 
 
4684
                        if ( selection ) {
 
4685
                                selection.off( null, null, this );
 
4686
                        }
 
4687
                        /**
 
4688
                         * call 'dispose' directly on the parent class
 
4689
                         */
 
4690
                        media.View.prototype.dispose.apply( this, arguments );
 
4691
                        return this;
 
4692
                },
 
4693
                /**
 
4694
                 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
 
4695
                 */
 
4696
                render: function() {
 
4697
                        var options = _.defaults( this.model.toJSON(), {
 
4698
                                        orientation:   'landscape',
 
4699
                                        uploading:     false,
 
4700
                                        type:          '',
 
4701
                                        subtype:       '',
 
4702
                                        icon:          '',
 
4703
                                        filename:      '',
 
4704
                                        caption:       '',
 
4705
                                        title:         '',
 
4706
                                        dateFormatted: '',
 
4707
                                        width:         '',
 
4708
                                        height:        '',
 
4709
                                        compat:        false,
 
4710
                                        alt:           '',
 
4711
                                        description:   ''
 
4712
                                }, this.options );
 
4713
 
 
4714
                        options.buttons  = this.buttons;
 
4715
                        options.describe = this.controller.state().get('describe');
 
4716
 
 
4717
                        if ( 'image' === options.type ) {
 
4718
                                options.size = this.imageSize();
 
4719
                        }
 
4720
 
 
4721
                        options.can = {};
 
4722
                        if ( options.nonces ) {
 
4723
                                options.can.remove = !! options.nonces['delete'];
 
4724
                                options.can.save = !! options.nonces.update;
 
4725
                        }
 
4726
 
 
4727
                        if ( this.controller.state().get('allowLocalEdits') ) {
 
4728
                                options.allowLocalEdits = true;
 
4729
                        }
 
4730
 
 
4731
                        if ( options.uploading && ! options.percent ) {
 
4732
                                options.percent = 0;
 
4733
                        }
 
4734
 
 
4735
                        this.views.detach();
 
4736
                        this.$el.html( this.template( options ) );
 
4737
 
 
4738
                        this.$el.toggleClass( 'uploading', options.uploading );
 
4739
 
 
4740
                        if ( options.uploading ) {
 
4741
                                this.$bar = this.$('.media-progress-bar div');
 
4742
                        } else {
 
4743
                                delete this.$bar;
 
4744
                        }
 
4745
 
 
4746
                        // Check if the model is selected.
 
4747
                        this.updateSelect();
 
4748
 
 
4749
                        // Update the save status.
 
4750
                        this.updateSave();
 
4751
 
 
4752
                        this.views.render();
 
4753
 
 
4754
                        return this;
 
4755
                },
 
4756
 
 
4757
                progress: function() {
 
4758
                        if ( this.$bar && this.$bar.length ) {
 
4759
                                this.$bar.width( this.model.get('percent') + '%' );
 
4760
                        }
 
4761
                },
 
4762
 
 
4763
                /**
 
4764
                 * @param {Object} event
 
4765
                 */
 
4766
                toggleSelectionHandler: function( event ) {
 
4767
                        var method;
 
4768
 
 
4769
                        // Don't do anything inside inputs.
 
4770
                        if ( 'INPUT' === event.target.nodeName ) {
 
4771
                                return;
 
4772
                        }
 
4773
 
 
4774
                        // Catch arrow events
 
4775
                        if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
 
4776
                                this.controller.trigger( 'attachment:keydown:arrow', event );
 
4777
                                return;
 
4778
                        }
 
4779
 
 
4780
                        // Catch enter and space events
 
4781
                        if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
 
4782
                                return;
 
4783
                        }
 
4784
 
 
4785
                        // In the grid view, bubble up an edit:attachment event to the controller.
 
4786
                        if ( this.controller.isModeActive( 'grid' ) ) {
 
4787
                                if ( this.controller.isModeActive( 'edit' ) ) {
 
4788
                                        // Pass the current target to restore focus when closing
 
4789
                                        this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
 
4790
 
 
4791
                                        // Don't scroll the view and don't attempt to submit anything.
 
4792
                                        event.stopPropagation();
 
4793
                                        return;
 
4794
                                }
 
4795
 
 
4796
                                if ( this.controller.isModeActive( 'select' ) ) {
 
4797
                                        method = 'toggle';
 
4798
                                }
 
4799
                        }
 
4800
 
 
4801
                        if ( event.shiftKey ) {
 
4802
                                method = 'between';
 
4803
                        } else if ( event.ctrlKey || event.metaKey ) {
 
4804
                                method = 'toggle';
 
4805
                        }
 
4806
 
 
4807
                        this.toggleSelection({
 
4808
                                method: method
 
4809
                        });
 
4810
 
 
4811
                        this.controller.trigger( 'selection:toggle' );
 
4812
 
 
4813
                        // Don't scroll the view and don't attempt to submit anything.
 
4814
                        event.stopPropagation();
 
4815
                },
 
4816
                /**
 
4817
                 * @param {Object} options
 
4818
                 */
 
4819
                toggleSelection: function( options ) {
 
4820
                        var collection = this.collection,
 
4821
                                selection = this.options.selection,
 
4822
                                model = this.model,
 
4823
                                method = options && options.method,
 
4824
                                single, models, singleIndex, modelIndex;
 
4825
 
 
4826
                        if ( ! selection ) {
 
4827
                                return;
 
4828
                        }
 
4829
 
 
4830
                        single = selection.single();
 
4831
                        method = _.isUndefined( method ) ? selection.multiple : method;
 
4832
 
 
4833
                        // If the `method` is set to `between`, select all models that
 
4834
                        // exist between the current and the selected model.
 
4835
                        if ( 'between' === method && single && selection.multiple ) {
 
4836
                                // If the models are the same, short-circuit.
 
4837
                                if ( single === model ) {
 
4838
                                        return;
 
4839
                                }
 
4840
 
 
4841
                                singleIndex = collection.indexOf( single );
 
4842
                                modelIndex  = collection.indexOf( this.model );
 
4843
 
 
4844
                                if ( singleIndex < modelIndex ) {
 
4845
                                        models = collection.models.slice( singleIndex, modelIndex + 1 );
 
4846
                                } else {
 
4847
                                        models = collection.models.slice( modelIndex, singleIndex + 1 );
 
4848
                                }
 
4849
 
 
4850
                                selection.add( models );
 
4851
                                selection.single( model );
 
4852
                                return;
 
4853
 
 
4854
                        // If the `method` is set to `toggle`, just flip the selection
 
4855
                        // status, regardless of whether the model is the single model.
 
4856
                        } else if ( 'toggle' === method ) {
 
4857
                                selection[ this.selected() ? 'remove' : 'add' ]( model );
 
4858
                                selection.single( model );
 
4859
                                return;
 
4860
                        } else if ( 'add' === method ) {
 
4861
                                selection.add( model );
 
4862
                                selection.single( model );
 
4863
                                return;
 
4864
                        }
 
4865
 
 
4866
                        // Fixes bug that loses focus when selecting a featured image
 
4867
                        if ( ! method ) {
 
4868
                                method = 'add';
 
4869
                        }
 
4870
 
 
4871
                        if ( method !== 'add' ) {
 
4872
                                method = 'reset';
 
4873
                        }
 
4874
 
 
4875
                        if ( this.selected() ) {
 
4876
                                // If the model is the single model, remove it.
 
4877
                                // If it is not the same as the single model,
 
4878
                                // it now becomes the single model.
 
4879
                                selection[ single === model ? 'remove' : 'single' ]( model );
 
4880
                        } else {
 
4881
                                // If the model is not selected, run the `method` on the
 
4882
                                // selection. By default, we `reset` the selection, but the
 
4883
                                // `method` can be set to `add` the model to the selection.
 
4884
                                selection[ method ]( model );
 
4885
                                selection.single( model );
 
4886
                        }
 
4887
                },
 
4888
 
 
4889
                updateSelect: function() {
 
4890
                        this[ this.selected() ? 'select' : 'deselect' ]();
 
4891
                },
 
4892
                /**
 
4893
                 * @returns {unresolved|Boolean}
 
4894
                 */
 
4895
                selected: function() {
 
4896
                        var selection = this.options.selection;
 
4897
                        if ( selection ) {
 
4898
                                return !! selection.get( this.model.cid );
 
4899
                        }
 
4900
                },
 
4901
                /**
 
4902
                 * @param {Backbone.Model} model
 
4903
                 * @param {Backbone.Collection} collection
 
4904
                 */
 
4905
                select: function( model, collection ) {
 
4906
                        var selection = this.options.selection,
 
4907
                                controller = this.controller;
 
4908
 
 
4909
                        // Check if a selection exists and if it's the collection provided.
 
4910
                        // If they're not the same collection, bail; we're in another
 
4911
                        // selection's event loop.
 
4912
                        if ( ! selection || ( collection && collection !== selection ) ) {
 
4913
                                return;
 
4914
                        }
 
4915
 
 
4916
                        // Bail if the model is already selected.
 
4917
                        if ( this.$el.hasClass( 'selected' ) ) {
 
4918
                                return;
 
4919
                        }
 
4920
 
 
4921
                        // Add 'selected' class to model, set aria-checked to true.
 
4922
                        this.$el.addClass( 'selected' ).attr( 'aria-checked', true );
 
4923
                        //  Make the checkbox tabable, except in media grid (bulk select mode).
 
4924
                        if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) {
 
4925
                                this.$( '.check' ).attr( 'tabindex', '0' );
 
4926
                        }
 
4927
                },
 
4928
                /**
 
4929
                 * @param {Backbone.Model} model
 
4930
                 * @param {Backbone.Collection} collection
 
4931
                 */
 
4932
                deselect: function( model, collection ) {
 
4933
                        var selection = this.options.selection;
 
4934
 
 
4935
                        // Check if a selection exists and if it's the collection provided.
 
4936
                        // If they're not the same collection, bail; we're in another
 
4937
                        // selection's event loop.
 
4938
                        if ( ! selection || ( collection && collection !== selection ) ) {
 
4939
                                return;
 
4940
                        }
 
4941
                        this.$el.removeClass( 'selected' ).attr( 'aria-checked', false )
 
4942
                                .find( '.check' ).attr( 'tabindex', '-1' );
 
4943
                },
 
4944
                /**
 
4945
                 * @param {Backbone.Model} model
 
4946
                 * @param {Backbone.Collection} collection
 
4947
                 */
 
4948
                details: function( model, collection ) {
 
4949
                        var selection = this.options.selection,
 
4950
                                details;
 
4951
 
 
4952
                        if ( selection !== collection ) {
 
4953
                                return;
 
4954
                        }
 
4955
 
 
4956
                        details = selection.single();
 
4957
                        this.$el.toggleClass( 'details', details === this.model );
 
4958
                },
 
4959
                /**
 
4960
                 * @param {Object} event
 
4961
                 */
 
4962
                preventDefault: function( event ) {
 
4963
                        event.preventDefault();
 
4964
                },
 
4965
                /**
 
4966
                 * @param {string} size
 
4967
                 * @returns {Object}
 
4968
                 */
 
4969
                imageSize: function( size ) {
 
4970
                        var sizes = this.model.get('sizes');
 
4971
 
 
4972
                        size = size || 'medium';
 
4973
 
 
4974
                        // Use the provided image size if possible.
 
4975
                        if ( sizes && sizes[ size ] ) {
 
4976
                                return _.clone( sizes[ size ] );
 
4977
                        } else {
 
4978
                                return {
 
4979
                                        url:         this.model.get('url'),
 
4980
                                        width:       this.model.get('width'),
 
4981
                                        height:      this.model.get('height'),
 
4982
                                        orientation: this.model.get('orientation')
 
4983
                                };
 
4984
                        }
 
4985
                },
 
4986
                /**
 
4987
                 * @param {Object} event
 
4988
                 */
 
4989
                updateSetting: function( event ) {
 
4990
                        var $setting = $( event.target ).closest('[data-setting]'),
 
4991
                                setting, value;
 
4992
 
 
4993
                        if ( ! $setting.length ) {
 
4994
                                return;
 
4995
                        }
 
4996
 
 
4997
                        setting = $setting.data('setting');
 
4998
                        value   = event.target.value;
 
4999
 
 
5000
                        if ( this.model.get( setting ) !== value ) {
 
5001
                                this.save( setting, value );
 
5002
                        }
 
5003
                },
 
5004
 
 
5005
                /**
 
5006
                 * Pass all the arguments to the model's save method.
 
5007
                 *
 
5008
                 * Records the aggregate status of all save requests and updates the
 
5009
                 * view's classes accordingly.
 
5010
                 */
 
5011
                save: function() {
 
5012
                        var view = this,
 
5013
                                save = this._save = this._save || { status: 'ready' },
 
5014
                                request = this.model.save.apply( this.model, arguments ),
 
5015
                                requests = save.requests ? $.when( request, save.requests ) : request;
 
5016
 
 
5017
                        // If we're waiting to remove 'Saved.', stop.
 
5018
                        if ( save.savedTimer ) {
 
5019
                                clearTimeout( save.savedTimer );
 
5020
                        }
 
5021
 
 
5022
                        this.updateSave('waiting');
 
5023
                        save.requests = requests;
 
5024
                        requests.always( function() {
 
5025
                                // If we've performed another request since this one, bail.
 
5026
                                if ( save.requests !== requests ) {
 
5027
                                        return;
 
5028
                                }
 
5029
 
 
5030
                                view.updateSave( requests.state() === 'resolved' ? 'complete' : 'error' );
 
5031
                                save.savedTimer = setTimeout( function() {
 
5032
                                        view.updateSave('ready');
 
5033
                                        delete save.savedTimer;
 
5034
                                }, 2000 );
 
5035
                        });
 
5036
                },
 
5037
                /**
 
5038
                 * @param {string} status
 
5039
                 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
 
5040
                 */
 
5041
                updateSave: function( status ) {
 
5042
                        var save = this._save = this._save || { status: 'ready' };
 
5043
 
 
5044
                        if ( status && status !== save.status ) {
 
5045
                                this.$el.removeClass( 'save-' + save.status );
 
5046
                                save.status = status;
 
5047
                        }
 
5048
 
 
5049
                        this.$el.addClass( 'save-' + save.status );
 
5050
                        return this;
 
5051
                },
 
5052
 
 
5053
                updateAll: function() {
 
5054
                        var $settings = this.$('[data-setting]'),
 
5055
                                model = this.model,
 
5056
                                changed;
 
5057
 
 
5058
                        changed = _.chain( $settings ).map( function( el ) {
 
5059
                                var $input = $('input, textarea, select, [value]', el ),
 
5060
                                        setting, value;
 
5061
 
 
5062
                                if ( ! $input.length ) {
 
5063
                                        return;
 
5064
                                }
 
5065
 
 
5066
                                setting = $(el).data('setting');
 
5067
                                value = $input.val();
 
5068
 
 
5069
                                // Record the value if it changed.
 
5070
                                if ( model.get( setting ) !== value ) {
 
5071
                                        return [ setting, value ];
 
5072
                                }
 
5073
                        }).compact().object().value();
 
5074
 
 
5075
                        if ( ! _.isEmpty( changed ) ) {
 
5076
                                model.save( changed );
 
5077
                        }
 
5078
                },
 
5079
                /**
 
5080
                 * @param {Object} event
 
5081
                 */
 
5082
                removeFromLibrary: function( event ) {
 
5083
                        // Stop propagation so the model isn't selected.
 
5084
                        event.stopPropagation();
 
5085
 
 
5086
                        this.collection.remove( this.model );
 
5087
                },
 
5088
 
 
5089
                /**
 
5090
                 * Add the model if it isn't in the selection, if it is in the selection,
 
5091
                 * remove it.
 
5092
                 *
 
5093
                 * @param  {[type]} event [description]
 
5094
                 * @return {[type]}       [description]
 
5095
                 */
 
5096
                checkClickHandler: function ( event ) {
 
5097
                        var selection = this.options.selection;
 
5098
                        if ( ! selection ) {
 
5099
                                return;
 
5100
                        }
 
5101
                        event.stopPropagation();
 
5102
                        if ( selection.where( { id: this.model.get( 'id' ) } ).length ) {
 
5103
                                selection.remove( this.model );
 
5104
                                // Move focus back to the attachment tile (from the check).
 
5105
                                this.$el.focus();
 
5106
                        } else {
 
5107
                                selection.add( this.model );
 
5108
                        }
 
5109
                }
 
5110
        });
 
5111
 
 
5112
        // Ensure settings remain in sync between attachment views.
 
5113
        _.each({
 
5114
                caption: '_syncCaption',
 
5115
                title:   '_syncTitle',
 
5116
                artist:  '_syncArtist',
 
5117
                album:   '_syncAlbum'
 
5118
        }, function( method, setting ) {
 
5119
                /**
 
5120
                 * @param {Backbone.Model} model
 
5121
                 * @param {string} value
 
5122
                 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
 
5123
                 */
 
5124
                media.view.Attachment.prototype[ method ] = function( model, value ) {
 
5125
                        var $setting = this.$('[data-setting="' + setting + '"]');
 
5126
 
 
5127
                        if ( ! $setting.length ) {
 
5128
                                return this;
 
5129
                        }
 
5130
 
 
5131
                        // If the updated value is in sync with the value in the DOM, there
 
5132
                        // is no need to re-render. If we're currently editing the value,
 
5133
                        // it will automatically be in sync, suppressing the re-render for
 
5134
                        // the view we're editing, while updating any others.
 
5135
                        if ( value === $setting.find('input, textarea, select, [value]').val() ) {
 
5136
                                return this;
 
5137
                        }
 
5138
 
 
5139
                        return this.render();
 
5140
                };
 
5141
        });
 
5142
 
 
5143
        /**
 
5144
         * wp.media.view.Attachment.Library
 
5145
         *
 
5146
         * @constructor
 
5147
         * @augments wp.media.view.Attachment
 
5148
         * @augments wp.media.View
 
5149
         * @augments wp.Backbone.View
 
5150
         * @augments Backbone.View
 
5151
         */
 
5152
        media.view.Attachment.Library = media.view.Attachment.extend({
 
5153
                buttons: {
 
5154
                        check: true
 
5155
                }
 
5156
        });
 
5157
 
 
5158
        /**
 
5159
         * wp.media.view.Attachment.EditLibrary
 
5160
         *
 
5161
         * @constructor
 
5162
         * @augments wp.media.view.Attachment
 
5163
         * @augments wp.media.View
 
5164
         * @augments wp.Backbone.View
 
5165
         * @augments Backbone.View
 
5166
         */
 
5167
        media.view.Attachment.EditLibrary = media.view.Attachment.extend({
 
5168
                buttons: {
 
5169
                        close: true
 
5170
                }
 
5171
        });
 
5172
 
 
5173
        /**
 
5174
         * wp.media.view.Attachments
 
5175
         *
 
5176
         * @constructor
 
5177
         * @augments wp.media.View
 
5178
         * @augments wp.Backbone.View
 
5179
         * @augments Backbone.View
 
5180
         */
 
5181
        media.view.Attachments = media.View.extend({
 
5182
                tagName:   'ul',
 
5183
                className: 'attachments',
 
5184
 
 
5185
                attributes: {
 
5186
                        tabIndex: -1
 
5187
                },
 
5188
 
 
5189
                initialize: function() {
 
5190
                        this.el.id = _.uniqueId('__attachments-view-');
 
5191
 
 
5192
                        _.defaults( this.options, {
 
5193
                                refreshSensitivity: isTouchDevice ? 300 : 200,
 
5194
                                refreshThreshold:   3,
 
5195
                                AttachmentView:     media.view.Attachment,
 
5196
                                sortable:           false,
 
5197
                                resize:             true,
 
5198
                                idealColumnWidth:   $( window ).width() < 640 ? 135 : 150
 
5199
                        });
 
5200
 
 
5201
                        this._viewsByCid = {};
 
5202
                        this.$window = $( window );
 
5203
                        this.resizeEvent = 'resize.media-modal-columns';
 
5204
 
 
5205
                        this.collection.on( 'add', function( attachment ) {
 
5206
                                this.views.add( this.createAttachmentView( attachment ), {
 
5207
                                        at: this.collection.indexOf( attachment )
 
5208
                                });
 
5209
                        }, this );
 
5210
 
 
5211
                        this.collection.on( 'remove', function( attachment ) {
 
5212
                                var view = this._viewsByCid[ attachment.cid ];
 
5213
                                delete this._viewsByCid[ attachment.cid ];
 
5214
 
 
5215
                                if ( view ) {
 
5216
                                        view.remove();
 
5217
                                }
 
5218
                        }, this );
 
5219
 
 
5220
                        this.collection.on( 'reset', this.render, this );
 
5221
 
 
5222
                        this.listenTo( this.controller, 'library:selection:add',    this.attachmentFocus );
 
5223
 
 
5224
                        // Throttle the scroll handler and bind this.
 
5225
                        this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
 
5226
 
 
5227
                        this.options.scrollElement = this.options.scrollElement || this.el;
 
5228
                        $( this.options.scrollElement ).on( 'scroll', this.scroll );
 
5229
 
 
5230
                        this.initSortable();
 
5231
 
 
5232
                        _.bindAll( this, 'setColumns' );
 
5233
 
 
5234
                        if ( this.options.resize ) {
 
5235
                                this.on( 'ready', this.bindEvents );
 
5236
                                this.controller.on( 'open', this.setColumns );
 
5237
 
 
5238
                                // Call this.setColumns() after this view has been rendered in the DOM so
 
5239
                                // attachments get proper width applied.
 
5240
                                _.defer( this.setColumns, this );
 
5241
                        }
 
5242
                },
 
5243
 
 
5244
                bindEvents: function() {
 
5245
                        this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) );
 
5246
                },
 
5247
 
 
5248
                attachmentFocus: function() {
 
5249
                        this.$( 'li:first' ).focus();
 
5250
                },
 
5251
 
 
5252
                restoreFocus: function() {
 
5253
                        this.$( 'li.selected:first' ).focus();
 
5254
                },
 
5255
 
 
5256
                arrowEvent: function( event ) {
 
5257
                        var attachments = this.$el.children( 'li' ),
 
5258
                                perRow = this.columns,
 
5259
                                index = attachments.filter( ':focus' ).index(),
 
5260
                                row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow );
 
5261
 
 
5262
                        if ( index === -1 ) {
 
5263
                                return;
 
5264
                        }
 
5265
 
 
5266
                        // Left arrow
 
5267
                        if ( 37 === event.keyCode ) {
 
5268
                                if ( 0 === index ) {
 
5269
                                        return;
 
5270
                                }
 
5271
                                attachments.eq( index - 1 ).focus();
 
5272
                        }
 
5273
 
 
5274
                        // Up arrow
 
5275
                        if ( 38 === event.keyCode ) {
 
5276
                                if ( 1 === row ) {
 
5277
                                        return;
 
5278
                                }
 
5279
                                attachments.eq( index - perRow ).focus();
 
5280
                        }
 
5281
 
 
5282
                        // Right arrow
 
5283
                        if ( 39 === event.keyCode ) {
 
5284
                                if ( attachments.length === index ) {
 
5285
                                        return;
 
5286
                                }
 
5287
                                attachments.eq( index + 1 ).focus();
 
5288
                        }
 
5289
 
 
5290
                        // Down arrow
 
5291
                        if ( 40 === event.keyCode ) {
 
5292
                                if ( Math.ceil( attachments.length / perRow ) === row ) {
 
5293
                                        return;
 
5294
                                }
 
5295
                                attachments.eq( index + perRow ).focus();
 
5296
                        }
 
5297
                },
 
5298
 
 
5299
                dispose: function() {
 
5300
                        this.collection.props.off( null, null, this );
 
5301
                        if ( this.options.resize ) {
 
5302
                                this.$window.off( this.resizeEvent );
 
5303
                        }
 
5304
 
 
5305
                        /**
 
5306
                         * call 'dispose' directly on the parent class
 
5307
                         */
 
5308
                        media.View.prototype.dispose.apply( this, arguments );
 
5309
                },
 
5310
 
 
5311
                setColumns: function() {
 
5312
                        var prev = this.columns,
 
5313
                                width = this.$el.width();
 
5314
 
 
5315
                        if ( width ) {
 
5316
                                this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1;
 
5317
 
 
5318
                                if ( ! prev || prev !== this.columns ) {
 
5319
                                        this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns );
 
5320
                                }
 
5321
                        }
 
5322
                },
 
5323
 
 
5324
                initSortable: function() {
 
5325
                        var collection = this.collection;
 
5326
 
 
5327
                        if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
 
5328
                                return;
 
5329
                        }
 
5330
 
 
5331
                        this.$el.sortable( _.extend({
 
5332
                                // If the `collection` has a `comparator`, disable sorting.
 
5333
                                disabled: !! collection.comparator,
 
5334
 
 
5335
                                // Prevent attachments from being dragged outside the bounding
 
5336
                                // box of the list.
 
5337
                                containment: this.$el,
 
5338
 
 
5339
                                // Change the position of the attachment as soon as the
 
5340
                                // mouse pointer overlaps a thumbnail.
 
5341
                                tolerance: 'pointer',
 
5342
 
 
5343
                                // Record the initial `index` of the dragged model.
 
5344
                                start: function( event, ui ) {
 
5345
                                        ui.item.data('sortableIndexStart', ui.item.index());
 
5346
                                },
 
5347
 
 
5348
                                // Update the model's index in the collection.
 
5349
                                // Do so silently, as the view is already accurate.
 
5350
                                update: function( event, ui ) {
 
5351
                                        var model = collection.at( ui.item.data('sortableIndexStart') ),
 
5352
                                                comparator = collection.comparator;
 
5353
 
 
5354
                                        // Temporarily disable the comparator to prevent `add`
 
5355
                                        // from re-sorting.
 
5356
                                        delete collection.comparator;
 
5357
 
 
5358
                                        // Silently shift the model to its new index.
 
5359
                                        collection.remove( model, {
 
5360
                                                silent: true
 
5361
                                        });
 
5362
                                        collection.add( model, {
 
5363
                                                silent: true,
 
5364
                                                at:     ui.item.index()
 
5365
                                        });
 
5366
 
 
5367
                                        // Restore the comparator.
 
5368
                                        collection.comparator = comparator;
 
5369
 
 
5370
                                        // Fire the `reset` event to ensure other collections sync.
 
5371
                                        collection.trigger( 'reset', collection );
 
5372
 
 
5373
                                        // If the collection is sorted by menu order,
 
5374
                                        // update the menu order.
 
5375
                                        collection.saveMenuOrder();
 
5376
                                }
 
5377
                        }, this.options.sortable ) );
 
5378
 
 
5379
                        // If the `orderby` property is changed on the `collection`,
 
5380
                        // check to see if we have a `comparator`. If so, disable sorting.
 
5381
                        collection.props.on( 'change:orderby', function() {
 
5382
                                this.$el.sortable( 'option', 'disabled', !! collection.comparator );
 
5383
                        }, this );
 
5384
 
 
5385
                        this.collection.props.on( 'change:orderby', this.refreshSortable, this );
 
5386
                        this.refreshSortable();
 
5387
                },
 
5388
 
 
5389
                refreshSortable: function() {
 
5390
                        if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
 
5391
                                return;
 
5392
                        }
 
5393
 
 
5394
                        // If the `collection` has a `comparator`, disable sorting.
 
5395
                        var collection = this.collection,
 
5396
                                orderby = collection.props.get('orderby'),
 
5397
                                enabled = 'menuOrder' === orderby || ! collection.comparator;
 
5398
 
 
5399
                        this.$el.sortable( 'option', 'disabled', ! enabled );
 
5400
                },
 
5401
 
 
5402
                /**
 
5403
                 * @param {wp.media.model.Attachment} attachment
 
5404
                 * @returns {wp.media.View}
 
5405
                 */
 
5406
                createAttachmentView: function( attachment ) {
 
5407
                        var view = new this.options.AttachmentView({
 
5408
                                controller:           this.controller,
 
5409
                                model:                attachment,
 
5410
                                collection:           this.collection,
 
5411
                                selection:            this.options.selection
 
5412
                        });
 
5413
 
 
5414
                        return this._viewsByCid[ attachment.cid ] = view;
 
5415
                },
 
5416
 
 
5417
                prepare: function() {
 
5418
                        // Create all of the Attachment views, and replace
 
5419
                        // the list in a single DOM operation.
 
5420
                        if ( this.collection.length ) {
 
5421
                                this.views.set( this.collection.map( this.createAttachmentView, this ) );
 
5422
 
 
5423
                        // If there are no elements, clear the views and load some.
 
5424
                        } else {
 
5425
                                this.views.unset();
 
5426
                                this.collection.more().done( this.scroll );
 
5427
                        }
 
5428
                },
 
5429
 
 
5430
                ready: function() {
 
5431
                        // Trigger the scroll event to check if we're within the
 
5432
                        // threshold to query for additional attachments.
 
5433
                        this.scroll();
 
5434
                },
 
5435
 
 
5436
                scroll: function() {
 
5437
                        var view = this,
 
5438
                                el = this.options.scrollElement,
 
5439
                                scrollTop = el.scrollTop,
 
5440
                                toolbar;
 
5441
 
 
5442
                        // The scroll event occurs on the document, but the element
 
5443
                        // that should be checked is the document body.
 
5444
                        if ( el == document ) {
 
5445
                                el = document.body;
 
5446
                                scrollTop = $(document).scrollTop();
 
5447
                        }
 
5448
 
 
5449
                        if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) {
 
5450
                                return;
 
5451
                        }
 
5452
 
 
5453
                        toolbar = this.views.parent.toolbar;
 
5454
 
 
5455
                        // Show the spinner only if we are close to the bottom.
 
5456
                        if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) {
 
5457
                                toolbar.get('spinner').show();
 
5458
                        }
 
5459
 
 
5460
                        if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) {
 
5461
                                this.collection.more().done(function() {
 
5462
                                        view.scroll();
 
5463
                                        toolbar.get('spinner').hide();
 
5464
                                });
 
5465
                        }
 
5466
                }
 
5467
        });
 
5468
 
 
5469
        /**
 
5470
         * wp.media.view.Search
 
5471
         *
 
5472
         * @constructor
 
5473
         * @augments wp.media.View
 
5474
         * @augments wp.Backbone.View
 
5475
         * @augments Backbone.View
 
5476
         */
 
5477
        media.view.Search = media.View.extend({
 
5478
                tagName:   'input',
 
5479
                className: 'search',
 
5480
                id:        'media-search-input',
 
5481
 
 
5482
                attributes: {
 
5483
                        type:        'search',
 
5484
                        placeholder: l10n.search
 
5485
                },
 
5486
 
 
5487
                events: {
 
5488
                        'input':  'search',
 
5489
                        'keyup':  'search',
 
5490
                        'change': 'search',
 
5491
                        'search': 'search'
 
5492
                },
 
5493
 
 
5494
                /**
 
5495
                 * @returns {wp.media.view.Search} Returns itself to allow chaining
 
5496
                 */
 
5497
                render: function() {
 
5498
                        this.el.value = this.model.escape('search');
 
5499
                        return this;
 
5500
                },
 
5501
 
 
5502
                search: function( event ) {
 
5503
                        if ( event.target.value ) {
 
5504
                                this.model.set( 'search', event.target.value );
 
5505
                        } else {
 
5506
                                this.model.unset('search');
 
5507
                        }
 
5508
                }
 
5509
        });
 
5510
 
 
5511
        /**
 
5512
         * wp.media.view.AttachmentFilters
 
5513
         *
 
5514
         * @constructor
 
5515
         * @augments wp.media.View
 
5516
         * @augments wp.Backbone.View
 
5517
         * @augments Backbone.View
 
5518
         */
 
5519
        media.view.AttachmentFilters = media.View.extend({
 
5520
                tagName:   'select',
 
5521
                className: 'attachment-filters',
 
5522
                id:        'media-attachment-filters',
 
5523
 
 
5524
                events: {
 
5525
                        change: 'change'
 
5526
                },
 
5527
 
 
5528
                keys: [],
 
5529
 
 
5530
                initialize: function() {
 
5531
                        this.createFilters();
 
5532
                        _.extend( this.filters, this.options.filters );
 
5533
 
 
5534
                        // Build `<option>` elements.
 
5535
                        this.$el.html( _.chain( this.filters ).map( function( filter, value ) {
 
5536
                                return {
 
5537
                                        el: $( '<option></option>' ).val( value ).html( filter.text )[0],
 
5538
                                        priority: filter.priority || 50
 
5539
                                };
 
5540
                        }, this ).sortBy('priority').pluck('el').value() );
 
5541
 
 
5542
                        this.model.on( 'change', this.select, this );
 
5543
                        this.select();
 
5544
                },
 
5545
 
 
5546
                /**
 
5547
                 * @abstract
 
5548
                 */
 
5549
                createFilters: function() {
 
5550
                        this.filters = {};
 
5551
                },
 
5552
 
 
5553
                /**
 
5554
                 * When the selection changes, set the Query properties
 
5555
                 * accordingly for the selected filter.
 
5556
                 */
 
5557
                change: function() {
 
5558
                        var filter = this.filters[ this.el.value ];
 
5559
                        if ( filter ) {
 
5560
                                this.model.set( filter.props );
 
5561
                        }
 
5562
                },
 
5563
 
 
5564
                select: function() {
 
5565
                        var model = this.model,
 
5566
                                value = 'all',
 
5567
                                props = model.toJSON();
 
5568
 
 
5569
                        _.find( this.filters, function( filter, id ) {
 
5570
                                var equal = _.all( filter.props, function( prop, key ) {
 
5571
                                        return prop === ( _.isUndefined( props[ key ] ) ? null : props[ key ] );
 
5572
                                });
 
5573
 
 
5574
                                if ( equal ) {
 
5575
                                        return value = id;
 
5576
                                }
 
5577
                        });
 
5578
 
 
5579
                        this.$el.val( value );
 
5580
                }
 
5581
        });
 
5582
 
 
5583
        /**
 
5584
         * wp.media.view.AttachmentFilters.Uploaded
 
5585
         *
 
5586
         * @constructor
 
5587
         * @augments wp.media.view.AttachmentFilters
 
5588
         * @augments wp.media.View
 
5589
         * @augments wp.Backbone.View
 
5590
         * @augments Backbone.View
 
5591
         */
 
5592
        media.view.AttachmentFilters.Uploaded = media.view.AttachmentFilters.extend({
 
5593
                createFilters: function() {
 
5594
                        var type = this.model.get('type'),
 
5595
                                types = media.view.settings.mimeTypes,
 
5596
                                text;
 
5597
 
 
5598
                        if ( types && type ) {
 
5599
                                text = types[ type ];
 
5600
                        }
 
5601
 
 
5602
                        this.filters = {
 
5603
                                all: {
 
5604
                                        text:  text || l10n.allMediaItems,
 
5605
                                        props: {
 
5606
                                                uploadedTo: null,
 
5607
                                                orderby: 'date',
 
5608
                                                order:   'DESC'
 
5609
                                        },
 
5610
                                        priority: 10
 
5611
                                },
 
5612
 
 
5613
                                uploaded: {
 
5614
                                        text:  l10n.uploadedToThisPost,
 
5615
                                        props: {
 
5616
                                                uploadedTo: media.view.settings.post.id,
 
5617
                                                orderby: 'menuOrder',
 
5618
                                                order:   'ASC'
 
5619
                                        },
 
5620
                                        priority: 20
 
5621
                                }
 
5622
                        };
 
5623
                }
 
5624
        });
 
5625
 
 
5626
        /**
 
5627
         * wp.media.view.AttachmentFilters.All
 
5628
         *
 
5629
         * @constructor
 
5630
         * @augments wp.media.view.AttachmentFilters
 
5631
         * @augments wp.media.View
 
5632
         * @augments wp.Backbone.View
 
5633
         * @augments Backbone.View
 
5634
         */
 
5635
        media.view.AttachmentFilters.All = media.view.AttachmentFilters.extend({
 
5636
                createFilters: function() {
 
5637
                        var filters = {};
 
5638
 
 
5639
                        _.each( media.view.settings.mimeTypes || {}, function( text, key ) {
 
5640
                                filters[ key ] = {
 
5641
                                        text: text,
 
5642
                                        props: {
 
5643
                                                status:  null,
 
5644
                                                type:    key,
 
5645
                                                uploadedTo: null,
 
5646
                                                orderby: 'date',
 
5647
                                                order:   'DESC'
 
5648
                                        }
 
5649
                                };
 
5650
                        });
 
5651
 
 
5652
                        filters.all = {
 
5653
                                text:  l10n.allMediaItems,
 
5654
                                props: {
 
5655
                                        status:  null,
 
5656
                                        type:    null,
 
5657
                                        uploadedTo: null,
 
5658
                                        orderby: 'date',
 
5659
                                        order:   'DESC'
 
5660
                                },
 
5661
                                priority: 10
 
5662
                        };
 
5663
 
 
5664
                        if ( media.view.settings.post.id ) {
 
5665
                                filters.uploaded = {
 
5666
                                        text:  l10n.uploadedToThisPost,
 
5667
                                        props: {
 
5668
                                                status:  null,
 
5669
                                                type:    null,
 
5670
                                                uploadedTo: media.view.settings.post.id,
 
5671
                                                orderby: 'menuOrder',
 
5672
                                                order:   'ASC'
 
5673
                                        },
 
5674
                                        priority: 20
 
5675
                                };
 
5676
                        }
 
5677
 
 
5678
                        filters.unattached = {
 
5679
                                text:  l10n.unattached,
 
5680
                                props: {
 
5681
                                        status:     null,
 
5682
                                        uploadedTo: 0,
 
5683
                                        type:       null,
 
5684
                                        orderby:    'menuOrder',
 
5685
                                        order:      'ASC'
 
5686
                                },
 
5687
                                priority: 50
 
5688
                        };
 
5689
 
 
5690
                        if ( media.view.settings.mediaTrash &&
 
5691
                                this.controller.isModeActive( 'grid' ) ) {
 
5692
 
 
5693
                                filters.trash = {
 
5694
                                        text:  l10n.trash,
 
5695
                                        props: {
 
5696
                                                uploadedTo: null,
 
5697
                                                status:     'trash',
 
5698
                                                type:       null,
 
5699
                                                orderby:    'date',
 
5700
                                                order:      'DESC'
 
5701
                                        },
 
5702
                                        priority: 50
 
5703
                                };
 
5704
                        }
 
5705
 
 
5706
                        this.filters = filters;
 
5707
                }
 
5708
        });
 
5709
 
 
5710
        /**
 
5711
         * wp.media.view.AttachmentsBrowser
 
5712
         *
 
5713
         * @constructor
 
5714
         * @augments wp.media.View
 
5715
         * @augments wp.Backbone.View
 
5716
         * @augments Backbone.View
 
5717
         */
 
5718
        media.view.AttachmentsBrowser = media.View.extend({
 
5719
                tagName:   'div',
 
5720
                className: 'attachments-browser',
 
5721
 
 
5722
                initialize: function() {
 
5723
                        _.defaults( this.options, {
 
5724
                                filters: false,
 
5725
                                search:  true,
 
5726
                                display: false,
 
5727
                                sidebar: true,
 
5728
                                AttachmentView: media.view.Attachment.Library
 
5729
                        });
 
5730
 
 
5731
                        this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
 
5732
 
 
5733
                        this.createToolbar();
 
5734
                        if ( this.options.sidebar ) {
 
5735
                                this.createSidebar();
 
5736
                        }
 
5737
                        this.createUploader();
 
5738
                        this.createAttachments();
 
5739
                        this.updateContent();
 
5740
 
 
5741
                        if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) {
 
5742
                                this.$el.addClass( 'hide-sidebar' );
 
5743
 
 
5744
                                if ( 'errors' === this.options.sidebar ) {
 
5745
                                        this.$el.addClass( 'sidebar-for-errors' );
 
5746
                                }
 
5747
                        }
 
5748
 
 
5749
                        this.collection.on( 'add remove reset', this.updateContent, this );
 
5750
                },
 
5751
                /**
 
5752
                 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining
 
5753
                 */
 
5754
                dispose: function() {
 
5755
                        this.options.selection.off( null, null, this );
 
5756
                        media.View.prototype.dispose.apply( this, arguments );
 
5757
                        return this;
 
5758
                },
 
5759
 
 
5760
                createToolbar: function() {
 
5761
                        var LibraryViewSwitcher, Filters, toolbarOptions;
 
5762
 
 
5763
                        toolbarOptions = {
 
5764
                                controller: this.controller
 
5765
                        };
 
5766
 
 
5767
                        if ( this.controller.isModeActive( 'grid' ) ) {
 
5768
                                toolbarOptions.className = 'media-toolbar wp-filter';
 
5769
                        }
 
5770
 
 
5771
                        /**
 
5772
                        * @member {wp.media.view.Toolbar}
 
5773
                        */
 
5774
                        this.toolbar = new media.view.Toolbar( toolbarOptions );
 
5775
 
 
5776
                        this.views.add( this.toolbar );
 
5777
 
 
5778
                        this.toolbar.set( 'spinner', new media.view.Spinner({
 
5779
                                priority: -60
 
5780
                        }) );
 
5781
 
 
5782
                        if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) {
 
5783
                                // "Filters" will return a <select>, need to render
 
5784
                                // screen reader text before
 
5785
                                this.toolbar.set( 'filtersLabel', new media.view.Label({
 
5786
                                        value: l10n.filterByType,
 
5787
                                        attributes: {
 
5788
                                                'for':  'media-attachment-filters'
 
5789
                                        },
 
5790
                                        priority:   -80
 
5791
                                }).render() );
 
5792
 
 
5793
                                if ( 'uploaded' === this.options.filters ) {
 
5794
                                        this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({
 
5795
                                                controller: this.controller,
 
5796
                                                model:      this.collection.props,
 
5797
                                                priority:   -80
 
5798
                                        }).render() );
 
5799
                                } else {
 
5800
                                        Filters = new media.view.AttachmentFilters.All({
 
5801
                                                controller: this.controller,
 
5802
                                                model:      this.collection.props,
 
5803
                                                priority:   -80
 
5804
                                        });
 
5805
 
 
5806
                                        this.toolbar.set( 'filters', Filters.render() );
 
5807
                                }
 
5808
                        }
 
5809
 
 
5810
                        // Feels odd to bring the global media library switcher into the Attachment
 
5811
                        // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar );
 
5812
                        // which the controller can tap into and add this view?
 
5813
                        if ( this.controller.isModeActive( 'grid' ) ) {
 
5814
                                LibraryViewSwitcher = media.View.extend({
 
5815
                                        className: 'view-switch media-grid-view-switch',
 
5816
                                        template: media.template( 'media-library-view-switcher')
 
5817
                                });
 
5818
 
 
5819
                                this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({
 
5820
                                        controller: this.controller,
 
5821
                                        priority: -90
 
5822
                                }).render() );
 
5823
 
 
5824
                                // DateFilter is a <select>, screen reader text needs to be rendered before
 
5825
                                this.toolbar.set( 'dateFilterLabel', new media.view.Label({
 
5826
                                        value: l10n.filterByDate,
 
5827
                                        attributes: {
 
5828
                                                'for': 'media-attachment-date-filters'
 
5829
                                        },
 
5830
                                        priority: -75
 
5831
                                }).render() );
 
5832
                                this.toolbar.set( 'dateFilter', new media.view.DateFilter({
 
5833
                                        controller: this.controller,
 
5834
                                        model:      this.collection.props,
 
5835
                                        priority: -75
 
5836
                                }).render() );
 
5837
 
 
5838
                                // BulkSelection is a <div> with subviews, including screen reader text
 
5839
                                this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({
 
5840
                                        text: l10n.bulkSelect,
 
5841
                                        controller: this.controller,
 
5842
                                        priority: -70
 
5843
                                }).render() );
 
5844
 
 
5845
                                this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
 
5846
                                        filters: Filters,
 
5847
                                        style: 'primary',
 
5848
                                        disabled: true,
 
5849
                                        text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
 
5850
                                        controller: this.controller,
 
5851
                                        priority: -60,
 
5852
                                        click: function() {
 
5853
                                                var changed = [], removed = [], self = this,
 
5854
                                                        selection = this.controller.state().get( 'selection' ),
 
5855
                                                        library = this.controller.state().get( 'library' );
 
5856
 
 
5857
                                                if ( ! selection.length ) {
 
5858
                                                        return;
 
5859
                                                }
 
5860
 
 
5861
                                                if ( ! media.view.settings.mediaTrash && ! confirm( l10n.warnBulkDelete ) ) {
 
5862
                                                        return;
 
5863
                                                }
 
5864
 
 
5865
                                                if ( media.view.settings.mediaTrash &&
 
5866
                                                        'trash' !== selection.at( 0 ).get( 'status' ) &&
 
5867
                                                        ! confirm( l10n.warnBulkTrash ) ) {
 
5868
 
 
5869
                                                        return;
 
5870
                                                }
 
5871
 
 
5872
                                                selection.each( function( model ) {
 
5873
                                                        if ( ! model.get( 'nonces' )['delete'] ) {
 
5874
                                                                removed.push( model );
 
5875
                                                                return;
 
5876
                                                        }
 
5877
 
 
5878
                                                        if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
 
5879
                                                                model.set( 'status', 'inherit' );
 
5880
                                                                changed.push( model.save() );
 
5881
                                                                removed.push( model );
 
5882
                                                        } else if ( media.view.settings.mediaTrash ) {
 
5883
                                                                model.set( 'status', 'trash' );
 
5884
                                                                changed.push( model.save() );
 
5885
                                                                removed.push( model );
 
5886
                                                        } else {
 
5887
                                                                model.destroy();
 
5888
                                                        }
 
5889
                                                } );
 
5890
 
 
5891
                                                if ( changed.length ) {
 
5892
                                                        selection.remove( removed );
 
5893
 
 
5894
                                                        $.when.apply( null, changed ).then( function() {
 
5895
                                                                library._requery( true );
 
5896
                                                                self.controller.trigger( 'selection:action:done' );
 
5897
                                                        } );
 
5898
                                                } else {
 
5899
                                                        this.controller.trigger( 'selection:action:done' );
 
5900
                                                }
 
5901
                                        }
 
5902
                                }).render() );
 
5903
                        }
 
5904
 
 
5905
                        if ( this.options.search ) {
 
5906
                                // Search is an input, screen reader text needs to be rendered before
 
5907
                                this.toolbar.set( 'searchLabel', new media.view.Label({
 
5908
                                        value: l10n.searchMediaLabel,
 
5909
                                        attributes: {
 
5910
                                                'for': 'media-search-input'
 
5911
                                        },
 
5912
                                        priority:   60
 
5913
                                }).render() );
 
5914
                                this.toolbar.set( 'search', new media.view.Search({
 
5915
                                        controller: this.controller,
 
5916
                                        model:      this.collection.props,
 
5917
                                        priority:   60
 
5918
                                }).render() );
 
5919
                        }
 
5920
 
 
5921
                        if ( this.options.dragInfo ) {
 
5922
                                this.toolbar.set( 'dragInfo', new media.View({
 
5923
                                        el: $( '<div class="instructions">' + l10n.dragInfo + '</div>' )[0],
 
5924
                                        priority: -40
 
5925
                                }) );
 
5926
                        }
 
5927
 
 
5928
                        if ( this.options.suggestedWidth && this.options.suggestedHeight ) {
 
5929
                                this.toolbar.set( 'suggestedDimensions', new media.View({
 
5930
                                        el: $( '<div class="instructions">' + l10n.suggestedDimensions + ' ' + this.options.suggestedWidth + ' &times; ' + this.options.suggestedHeight + '</div>' )[0],
 
5931
                                        priority: -40
 
5932
                                }) );
 
5933
                        }
 
5934
                },
 
5935
 
 
5936
                updateContent: function() {
 
5937
                        var view = this,
 
5938
                                noItemsView;
 
5939
 
 
5940
                        if ( this.controller.isModeActive( 'grid' ) ) {
 
5941
                                noItemsView = view.attachmentsNoResults;
 
5942
                        } else {
 
5943
                                noItemsView = view.uploader;
 
5944
                        }
 
5945
 
 
5946
                        if ( ! this.collection.length ) {
 
5947
                                this.toolbar.get( 'spinner' ).show();
 
5948
                                this.dfd = this.collection.more().done( function() {
 
5949
                                        if ( ! view.collection.length ) {
 
5950
                                                noItemsView.$el.removeClass( 'hidden' );
 
5951
                                        } else {
 
5952
                                                noItemsView.$el.addClass( 'hidden' );
 
5953
                                        }
 
5954
                                        view.toolbar.get( 'spinner' ).hide();
 
5955
                                } );
 
5956
                        } else {
 
5957
                                noItemsView.$el.addClass( 'hidden' );
 
5958
                                view.toolbar.get( 'spinner' ).hide();
 
5959
                        }
 
5960
                },
 
5961
 
 
5962
                createUploader: function() {
 
5963
                        this.uploader = new media.view.UploaderInline({
 
5964
                                controller: this.controller,
 
5965
                                status:     false,
 
5966
                                message:    this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound,
 
5967
                                canClose:   this.controller.isModeActive( 'grid' )
 
5968
                        });
 
5969
 
 
5970
                        this.uploader.hide();
 
5971
                        this.views.add( this.uploader );
 
5972
                },
 
5973
 
 
5974
                toggleUploader: function() {
 
5975
                        if ( this.uploader.$el.hasClass( 'hidden' ) ) {
 
5976
                                this.uploader.show();
 
5977
                        } else {
 
5978
                                this.uploader.hide();
 
5979
                        }
 
5980
                },
 
5981
 
 
5982
                createAttachments: function() {
 
5983
                        this.attachments = new media.view.Attachments({
 
5984
                                controller:           this.controller,
 
5985
                                collection:           this.collection,
 
5986
                                selection:            this.options.selection,
 
5987
                                model:                this.model,
 
5988
                                sortable:             this.options.sortable,
 
5989
                                scrollElement:        this.options.scrollElement,
 
5990
                                idealColumnWidth:     this.options.idealColumnWidth,
 
5991
 
 
5992
                                // The single `Attachment` view to be used in the `Attachments` view.
 
5993
                                AttachmentView: this.options.AttachmentView
 
5994
                        });
 
5995
 
 
5996
                        // Add keydown listener to the instance of the Attachments view
 
5997
                        this.attachments.listenTo( this.controller, 'attachment:keydown:arrow',     this.attachments.arrowEvent );
 
5998
                        this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus );
 
5999
 
 
6000
                        this.views.add( this.attachments );
 
6001
 
 
6002
 
 
6003
                        if ( this.controller.isModeActive( 'grid' ) ) {
 
6004
                                this.attachmentsNoResults = new media.View({
 
6005
                                        controller: this.controller,
 
6006
                                        tagName: 'p'
 
6007
                                });
 
6008
 
 
6009
                                this.attachmentsNoResults.$el.addClass( 'hidden no-media' );
 
6010
                                this.attachmentsNoResults.$el.html( l10n.noMedia );
 
6011
 
 
6012
                                this.views.add( this.attachmentsNoResults );
 
6013
                        }
 
6014
                },
 
6015
 
 
6016
                createSidebar: function() {
 
6017
                        var options = this.options,
 
6018
                                selection = options.selection,
 
6019
                                sidebar = this.sidebar = new media.view.Sidebar({
 
6020
                                        controller: this.controller
 
6021
                                });
 
6022
 
 
6023
                        this.views.add( sidebar );
 
6024
 
 
6025
                        if ( this.controller.uploader ) {
 
6026
                                sidebar.set( 'uploads', new media.view.UploaderStatus({
 
6027
                                        controller: this.controller,
 
6028
                                        priority:   40
 
6029
                                }) );
 
6030
                        }
 
6031
 
 
6032
                        selection.on( 'selection:single', this.createSingle, this );
 
6033
                        selection.on( 'selection:unsingle', this.disposeSingle, this );
 
6034
 
 
6035
                        if ( selection.single() ) {
 
6036
                                this.createSingle();
 
6037
                        }
 
6038
                },
 
6039
 
 
6040
                createSingle: function() {
 
6041
                        var sidebar = this.sidebar,
 
6042
                                single = this.options.selection.single();
 
6043
 
 
6044
                        sidebar.set( 'details', new media.view.Attachment.Details({
 
6045
                                controller: this.controller,
 
6046
                                model:      single,
 
6047
                                priority:   80
 
6048
                        }) );
 
6049
 
 
6050
                        sidebar.set( 'compat', new media.view.AttachmentCompat({
 
6051
                                controller: this.controller,
 
6052
                                model:      single,
 
6053
                                priority:   120
 
6054
                        }) );
 
6055
 
 
6056
                        if ( this.options.display ) {
 
6057
                                sidebar.set( 'display', new media.view.Settings.AttachmentDisplay({
 
6058
                                        controller:   this.controller,
 
6059
                                        model:        this.model.display( single ),
 
6060
                                        attachment:   single,
 
6061
                                        priority:     160,
 
6062
                                        userSettings: this.model.get('displayUserSettings')
 
6063
                                }) );
 
6064
                        }
 
6065
 
 
6066
                        // Show the sidebar on mobile
 
6067
                        if ( this.model.id === 'insert' ) {
 
6068
                                sidebar.$el.addClass( 'visible' );
 
6069
                        }
 
6070
                },
 
6071
 
 
6072
                disposeSingle: function() {
 
6073
                        var sidebar = this.sidebar;
 
6074
                        sidebar.unset('details');
 
6075
                        sidebar.unset('compat');
 
6076
                        sidebar.unset('display');
 
6077
                        // Hide the sidebar on mobile
 
6078
                        sidebar.$el.removeClass( 'visible' );
 
6079
                }
 
6080
        });
 
6081
 
 
6082
        /**
 
6083
         * wp.media.view.Selection
 
6084
         *
 
6085
         * @constructor
 
6086
         * @augments wp.media.View
 
6087
         * @augments wp.Backbone.View
 
6088
         * @augments Backbone.View
 
6089
         */
 
6090
        media.view.Selection = media.View.extend({
 
6091
                tagName:   'div',
 
6092
                className: 'media-selection',
 
6093
                template:  media.template('media-selection'),
 
6094
 
 
6095
                events: {
 
6096
                        'click .edit-selection':  'edit',
 
6097
                        'click .clear-selection': 'clear'
 
6098
                },
 
6099
 
 
6100
                initialize: function() {
 
6101
                        _.defaults( this.options, {
 
6102
                                editable:  false,
 
6103
                                clearable: true
 
6104
                        });
 
6105
 
 
6106
                        /**
 
6107
                         * @member {wp.media.view.Attachments.Selection}
 
6108
                         */
 
6109
                        this.attachments = new media.view.Attachments.Selection({
 
6110
                                controller: this.controller,
 
6111
                                collection: this.collection,
 
6112
                                selection:  this.collection,
 
6113
                                model:      new Backbone.Model()
 
6114
                        });
 
6115
 
 
6116
                        this.views.set( '.selection-view', this.attachments );
 
6117
                        this.collection.on( 'add remove reset', this.refresh, this );
 
6118
                        this.controller.on( 'content:activate', this.refresh, this );
 
6119
                },
 
6120
 
 
6121
                ready: function() {
 
6122
                        this.refresh();
 
6123
                },
 
6124
 
 
6125
                refresh: function() {
 
6126
                        // If the selection hasn't been rendered, bail.
 
6127
                        if ( ! this.$el.children().length ) {
 
6128
                                return;
 
6129
                        }
 
6130
 
 
6131
                        var collection = this.collection,
 
6132
                                editing = 'edit-selection' === this.controller.content.mode();
 
6133
 
 
6134
                        // If nothing is selected, display nothing.
 
6135
                        this.$el.toggleClass( 'empty', ! collection.length );
 
6136
                        this.$el.toggleClass( 'one', 1 === collection.length );
 
6137
                        this.$el.toggleClass( 'editing', editing );
 
6138
 
 
6139
                        this.$('.count').text( l10n.selected.replace('%d', collection.length) );
 
6140
                },
 
6141
 
 
6142
                edit: function( event ) {
 
6143
                        event.preventDefault();
 
6144
                        if ( this.options.editable ) {
 
6145
                                this.options.editable.call( this, this.collection );
 
6146
                        }
 
6147
                },
 
6148
 
 
6149
                clear: function( event ) {
 
6150
                        event.preventDefault();
 
6151
                        this.collection.reset();
 
6152
 
 
6153
                        // Keep focus inside media modal
 
6154
                        // after clear link is selected
 
6155
                        this.controller.modal.focusManager.focus();
 
6156
                }
 
6157
        });
 
6158
 
 
6159
 
 
6160
        /**
 
6161
         * wp.media.view.Attachment.Selection
 
6162
         *
 
6163
         * @constructor
 
6164
         * @augments wp.media.view.Attachment
 
6165
         * @augments wp.media.View
 
6166
         * @augments wp.Backbone.View
 
6167
         * @augments Backbone.View
 
6168
         */
 
6169
        media.view.Attachment.Selection = media.view.Attachment.extend({
 
6170
                className: 'attachment selection',
 
6171
 
 
6172
                // On click, just select the model, instead of removing the model from
 
6173
                // the selection.
 
6174
                toggleSelection: function() {
 
6175
                        this.options.selection.single( this.model );
 
6176
                }
 
6177
        });
 
6178
 
 
6179
        /**
 
6180
         * wp.media.view.Attachments.Selection
 
6181
         *
 
6182
         * @constructor
 
6183
         * @augments wp.media.view.Attachments
 
6184
         * @augments wp.media.View
 
6185
         * @augments wp.Backbone.View
 
6186
         * @augments Backbone.View
 
6187
         */
 
6188
        media.view.Attachments.Selection = media.view.Attachments.extend({
 
6189
                events: {},
 
6190
                initialize: function() {
 
6191
                        _.defaults( this.options, {
 
6192
                                sortable:   true,
 
6193
                                resize:     false,
 
6194
 
 
6195
                                // The single `Attachment` view to be used in the `Attachments` view.
 
6196
                                AttachmentView: media.view.Attachment.Selection
 
6197
                        });
 
6198
                        /**
 
6199
                         * call 'initialize' directly on the parent class
 
6200
                         */
 
6201
                        return media.view.Attachments.prototype.initialize.apply( this, arguments );
 
6202
                }
 
6203
        });
 
6204
 
 
6205
        /**
 
6206
         * wp.media.view.Attachments.EditSelection
 
6207
         *
 
6208
         * @constructor
 
6209
         * @augments wp.media.view.Attachment.Selection
 
6210
         * @augments wp.media.view.Attachment
 
6211
         * @augments wp.media.View
 
6212
         * @augments wp.Backbone.View
 
6213
         * @augments Backbone.View
 
6214
         */
 
6215
        media.view.Attachment.EditSelection = media.view.Attachment.Selection.extend({
 
6216
                buttons: {
 
6217
                        close: true
 
6218
                }
 
6219
        });
 
6220
 
 
6221
 
 
6222
        /**
 
6223
         * wp.media.view.Settings
 
6224
         *
 
6225
         * @constructor
 
6226
         * @augments wp.media.View
 
6227
         * @augments wp.Backbone.View
 
6228
         * @augments Backbone.View
 
6229
         */
 
6230
        media.view.Settings = media.View.extend({
 
6231
                events: {
 
6232
                        'click button':    'updateHandler',
 
6233
                        'change input':    'updateHandler',
 
6234
                        'change select':   'updateHandler',
 
6235
                        'change textarea': 'updateHandler'
 
6236
                },
 
6237
 
 
6238
                initialize: function() {
 
6239
                        this.model = this.model || new Backbone.Model();
 
6240
                        this.model.on( 'change', this.updateChanges, this );
 
6241
                },
 
6242
 
 
6243
                prepare: function() {
 
6244
                        return _.defaults({
 
6245
                                model: this.model.toJSON()
 
6246
                        }, this.options );
 
6247
                },
 
6248
                /**
 
6249
                 * @returns {wp.media.view.Settings} Returns itself to allow chaining
 
6250
                 */
 
6251
                render: function() {
 
6252
                        media.View.prototype.render.apply( this, arguments );
 
6253
                        // Select the correct values.
 
6254
                        _( this.model.attributes ).chain().keys().each( this.update, this );
 
6255
                        return this;
 
6256
                },
 
6257
                /**
 
6258
                 * @param {string} key
 
6259
                 */
 
6260
                update: function( key ) {
 
6261
                        var value = this.model.get( key ),
 
6262
                                $setting = this.$('[data-setting="' + key + '"]'),
 
6263
                                $buttons, $value;
 
6264
 
 
6265
                        // Bail if we didn't find a matching setting.
 
6266
                        if ( ! $setting.length ) {
 
6267
                                return;
 
6268
                        }
 
6269
 
 
6270
                        // Attempt to determine how the setting is rendered and update
 
6271
                        // the selected value.
 
6272
 
 
6273
                        // Handle dropdowns.
 
6274
                        if ( $setting.is('select') ) {
 
6275
                                $value = $setting.find('[value="' + value + '"]');
 
6276
 
 
6277
                                if ( $value.length ) {
 
6278
                                        $setting.find('option').prop( 'selected', false );
 
6279
                                        $value.prop( 'selected', true );
 
6280
                                } else {
 
6281
                                        // If we can't find the desired value, record what *is* selected.
 
6282
                                        this.model.set( key, $setting.find(':selected').val() );
 
6283
                                }
 
6284
 
 
6285
                        // Handle button groups.
 
6286
                        } else if ( $setting.hasClass('button-group') ) {
 
6287
                                $buttons = $setting.find('button').removeClass('active');
 
6288
                                $buttons.filter( '[value="' + value + '"]' ).addClass('active');
 
6289
 
 
6290
                        // Handle text inputs and textareas.
 
6291
                        } else if ( $setting.is('input[type="text"], textarea') ) {
 
6292
                                if ( ! $setting.is(':focus') ) {
 
6293
                                        $setting.val( value );
 
6294
                                }
 
6295
                        // Handle checkboxes.
 
6296
                        } else if ( $setting.is('input[type="checkbox"]') ) {
 
6297
                                $setting.prop( 'checked', !! value && 'false' !== value );
 
6298
                        }
 
6299
                },
 
6300
                /**
 
6301
                 * @param {Object} event
 
6302
                 */
 
6303
                updateHandler: function( event ) {
 
6304
                        var $setting = $( event.target ).closest('[data-setting]'),
 
6305
                                value = event.target.value,
 
6306
                                userSetting;
 
6307
 
 
6308
                        event.preventDefault();
 
6309
 
 
6310
                        if ( ! $setting.length ) {
 
6311
                                return;
 
6312
                        }
 
6313
 
 
6314
                        // Use the correct value for checkboxes.
 
6315
                        if ( $setting.is('input[type="checkbox"]') ) {
 
6316
                                value = $setting[0].checked;
 
6317
                        }
 
6318
 
 
6319
                        // Update the corresponding setting.
 
6320
                        this.model.set( $setting.data('setting'), value );
 
6321
 
 
6322
                        // If the setting has a corresponding user setting,
 
6323
                        // update that as well.
 
6324
                        if ( userSetting = $setting.data('userSetting') ) {
 
6325
                                setUserSetting( userSetting, value );
 
6326
                        }
 
6327
                },
 
6328
 
 
6329
                updateChanges: function( model ) {
 
6330
                        if ( model.hasChanged() ) {
 
6331
                                _( model.changed ).chain().keys().each( this.update, this );
 
6332
                        }
 
6333
                }
 
6334
        });
 
6335
 
 
6336
        /**
 
6337
         * wp.media.view.Settings.AttachmentDisplay
 
6338
         *
 
6339
         * @constructor
 
6340
         * @augments wp.media.view.Settings
 
6341
         * @augments wp.media.View
 
6342
         * @augments wp.Backbone.View
 
6343
         * @augments Backbone.View
 
6344
         */
 
6345
        media.view.Settings.AttachmentDisplay = media.view.Settings.extend({
 
6346
                className: 'attachment-display-settings',
 
6347
                template:  media.template('attachment-display-settings'),
 
6348
 
 
6349
                initialize: function() {
 
6350
                        var attachment = this.options.attachment;
 
6351
 
 
6352
                        _.defaults( this.options, {
 
6353
                                userSettings: false
 
6354
                        });
 
6355
                        /**
 
6356
                         * call 'initialize' directly on the parent class
 
6357
                         */
 
6358
                        media.view.Settings.prototype.initialize.apply( this, arguments );
 
6359
                        this.model.on( 'change:link', this.updateLinkTo, this );
 
6360
 
 
6361
                        if ( attachment ) {
 
6362
                                attachment.on( 'change:uploading', this.render, this );
 
6363
                        }
 
6364
                },
 
6365
 
 
6366
                dispose: function() {
 
6367
                        var attachment = this.options.attachment;
 
6368
                        if ( attachment ) {
 
6369
                                attachment.off( null, null, this );
 
6370
                        }
 
6371
                        /**
 
6372
                         * call 'dispose' directly on the parent class
 
6373
                         */
 
6374
                        media.view.Settings.prototype.dispose.apply( this, arguments );
 
6375
                },
 
6376
                /**
 
6377
                 * @returns {wp.media.view.AttachmentDisplay} Returns itself to allow chaining
 
6378
                 */
 
6379
                render: function() {
 
6380
                        var attachment = this.options.attachment;
 
6381
                        if ( attachment ) {
 
6382
                                _.extend( this.options, {
 
6383
                                        sizes: attachment.get('sizes'),
 
6384
                                        type:  attachment.get('type')
 
6385
                                });
 
6386
                        }
 
6387
                        /**
 
6388
                         * call 'render' directly on the parent class
 
6389
                         */
 
6390
                        media.view.Settings.prototype.render.call( this );
 
6391
                        this.updateLinkTo();
 
6392
                        return this;
 
6393
                },
 
6394
 
 
6395
                updateLinkTo: function() {
 
6396
                        var linkTo = this.model.get('link'),
 
6397
                                $input = this.$('.link-to-custom'),
 
6398
                                attachment = this.options.attachment;
 
6399
 
 
6400
                        if ( 'none' === linkTo || 'embed' === linkTo || ( ! attachment && 'custom' !== linkTo ) ) {
 
6401
                                $input.addClass( 'hidden' );
 
6402
                                return;
 
6403
                        }
 
6404
 
 
6405
                        if ( attachment ) {
 
6406
                                if ( 'post' === linkTo ) {
 
6407
                                        $input.val( attachment.get('link') );
 
6408
                                } else if ( 'file' === linkTo ) {
 
6409
                                        $input.val( attachment.get('url') );
 
6410
                                } else if ( ! this.model.get('linkUrl') ) {
 
6411
                                        $input.val('http://');
 
6412
                                }
 
6413
 
 
6414
                                $input.prop( 'readonly', 'custom' !== linkTo );
 
6415
                        }
 
6416
 
 
6417
                        $input.removeClass( 'hidden' );
 
6418
 
 
6419
                        // If the input is visible, focus and select its contents.
 
6420
                        if ( ! isTouchDevice && $input.is(':visible') ) {
 
6421
                                $input.focus()[0].select();
 
6422
                        }
 
6423
                }
 
6424
        });
 
6425
 
 
6426
        /**
 
6427
         * wp.media.view.Settings.Gallery
 
6428
         *
 
6429
         * @constructor
 
6430
         * @augments wp.media.view.Settings
 
6431
         * @augments wp.media.View
 
6432
         * @augments wp.Backbone.View
 
6433
         * @augments Backbone.View
 
6434
         */
 
6435
        media.view.Settings.Gallery = media.view.Settings.extend({
 
6436
                className: 'collection-settings gallery-settings',
 
6437
                template:  media.template('gallery-settings')
 
6438
        });
 
6439
 
 
6440
        /**
 
6441
         * wp.media.view.Settings.Playlist
 
6442
         *
 
6443
         * @constructor
 
6444
         * @augments wp.media.view.Settings
 
6445
         * @augments wp.media.View
 
6446
         * @augments wp.Backbone.View
 
6447
         * @augments Backbone.View
 
6448
         */
 
6449
        media.view.Settings.Playlist = media.view.Settings.extend({
 
6450
                className: 'collection-settings playlist-settings',
 
6451
                template:  media.template('playlist-settings')
 
6452
        });
 
6453
 
 
6454
        /**
 
6455
         * wp.media.view.Attachment.Details
 
6456
         *
 
6457
         * @constructor
 
6458
         * @augments wp.media.view.Attachment
 
6459
         * @augments wp.media.View
 
6460
         * @augments wp.Backbone.View
 
6461
         * @augments Backbone.View
 
6462
         */
 
6463
        media.view.Attachment.Details = media.view.Attachment.extend({
 
6464
                tagName:   'div',
 
6465
                className: 'attachment-details',
 
6466
                template:  media.template('attachment-details'),
 
6467
 
 
6468
                events: {
 
6469
                        'change [data-setting]':          'updateSetting',
 
6470
                        'change [data-setting] input':    'updateSetting',
 
6471
                        'change [data-setting] select':   'updateSetting',
 
6472
                        'change [data-setting] textarea': 'updateSetting',
 
6473
                        'click .delete-attachment':       'deleteAttachment',
 
6474
                        'click .trash-attachment':        'trashAttachment',
 
6475
                        'click .untrash-attachment':      'untrashAttachment',
 
6476
                        'click .edit-attachment':         'editAttachment',
 
6477
                        'click .refresh-attachment':      'refreshAttachment',
 
6478
                        'keydown':                        'toggleSelectionHandler'
 
6479
                },
 
6480
 
 
6481
                initialize: function() {
 
6482
                        this.options = _.defaults( this.options, {
 
6483
                                rerenderOnModelChange: false
 
6484
                        });
 
6485
 
 
6486
                        this.on( 'ready', this.initialFocus );
 
6487
                        /**
 
6488
                         * call 'initialize' directly on the parent class
 
6489
                         */
 
6490
                        media.view.Attachment.prototype.initialize.apply( this, arguments );
 
6491
                },
 
6492
 
 
6493
                initialFocus: function() {
 
6494
                        if ( ! isTouchDevice ) {
 
6495
                                this.$( ':input' ).eq( 0 ).focus();
 
6496
                        }
 
6497
                },
 
6498
                /**
 
6499
                 * @param {Object} event
 
6500
                 */
 
6501
                deleteAttachment: function( event ) {
 
6502
                        event.preventDefault();
 
6503
 
 
6504
                        if ( confirm( l10n.warnDelete ) ) {
 
6505
                                this.model.destroy();
 
6506
                                // Keep focus inside media modal
 
6507
                                // after image is deleted
 
6508
                                this.controller.modal.focusManager.focus();
 
6509
                        }
 
6510
                },
 
6511
                /**
 
6512
                 * @param {Object} event
 
6513
                 */
 
6514
                trashAttachment: function( event ) {
 
6515
                        var library = this.controller.library;
 
6516
                        event.preventDefault();
 
6517
 
 
6518
                        if ( media.view.settings.mediaTrash &&
 
6519
                                'edit-metadata' === this.controller.content.mode() ) {
 
6520
 
 
6521
                                this.model.set( 'status', 'trash' );
 
6522
                                this.model.save().done( function() {
 
6523
                                        library._requery( true );
 
6524
                                } );
 
6525
                        }  else {
 
6526
                                this.model.destroy();
 
6527
                        }
 
6528
                },
 
6529
                /**
 
6530
                 * @param {Object} event
 
6531
                 */
 
6532
                untrashAttachment: function( event ) {
 
6533
                        var library = this.controller.library;
 
6534
                        event.preventDefault();
 
6535
 
 
6536
                        this.model.set( 'status', 'inherit' );
 
6537
                        this.model.save().done( function() {
 
6538
                                library._requery( true );
 
6539
                        } );
 
6540
                },
 
6541
                /**
 
6542
                 * @param {Object} event
 
6543
                 */
 
6544
                editAttachment: function( event ) {
 
6545
                        var editState = this.controller.states.get( 'edit-image' );
 
6546
                        if ( window.imageEdit && editState ) {
 
6547
                                event.preventDefault();
 
6548
 
 
6549
                                editState.set( 'image', this.model );
 
6550
                                this.controller.setState( 'edit-image' );
 
6551
                        } else {
 
6552
                                this.$el.addClass('needs-refresh');
 
6553
                        }
 
6554
                },
 
6555
                /**
 
6556
                 * @param {Object} event
 
6557
                 */
 
6558
                refreshAttachment: function( event ) {
 
6559
                        this.$el.removeClass('needs-refresh');
 
6560
                        event.preventDefault();
 
6561
                        this.model.fetch();
 
6562
                },
 
6563
                /**
 
6564
                 * When reverse tabbing(shift+tab) out of the right details panel, deliver
 
6565
                 * the focus to the item in the list that was being edited.
 
6566
                 *
 
6567
                 * @param {Object} event
 
6568
                 */
 
6569
                toggleSelectionHandler: function( event ) {
 
6570
                        if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) {
 
6571
                                this.controller.trigger( 'attachment:details:shift-tab', event );
 
6572
                                return false;
 
6573
                        }
 
6574
 
 
6575
                        if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
 
6576
                                this.controller.trigger( 'attachment:keydown:arrow', event );
 
6577
                                return;
 
6578
                        }
 
6579
                }
 
6580
        });
 
6581
 
 
6582
        /**
 
6583
         * wp.media.view.AttachmentCompat
 
6584
         *
 
6585
         * A view to display fields added via the `attachment_fields_to_edit` filter.
 
6586
         *
 
6587
         * @constructor
 
6588
         * @augments wp.media.View
 
6589
         * @augments wp.Backbone.View
 
6590
         * @augments Backbone.View
 
6591
         */
 
6592
        media.view.AttachmentCompat = media.View.extend({
 
6593
                tagName:   'form',
 
6594
                className: 'compat-item',
 
6595
 
 
6596
                events: {
 
6597
                        'submit':          'preventDefault',
 
6598
                        'change input':    'save',
 
6599
                        'change select':   'save',
 
6600
                        'change textarea': 'save'
 
6601
                },
 
6602
 
 
6603
                initialize: function() {
 
6604
                        this.model.on( 'change:compat', this.render, this );
 
6605
                },
 
6606
                /**
 
6607
                 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining
 
6608
                 */
 
6609
                dispose: function() {
 
6610
                        if ( this.$(':focus').length ) {
 
6611
                                this.save();
 
6612
                        }
 
6613
                        /**
 
6614
                         * call 'dispose' directly on the parent class
 
6615
                         */
 
6616
                        return media.View.prototype.dispose.apply( this, arguments );
 
6617
                },
 
6618
                /**
 
6619
                 * @returns {wp.media.view.AttachmentCompat} Returns itself to allow chaining
 
6620
                 */
 
6621
                render: function() {
 
6622
                        var compat = this.model.get('compat');
 
6623
                        if ( ! compat || ! compat.item ) {
 
6624
                                return;
 
6625
                        }
 
6626
 
 
6627
                        this.views.detach();
 
6628
                        this.$el.html( compat.item );
 
6629
                        this.views.render();
 
6630
                        return this;
 
6631
                },
 
6632
                /**
 
6633
                 * @param {Object} event
 
6634
                 */
 
6635
                preventDefault: function( event ) {
 
6636
                        event.preventDefault();
 
6637
                },
 
6638
                /**
 
6639
                 * @param {Object} event
 
6640
                 */
 
6641
                save: function( event ) {
 
6642
                        var data = {};
 
6643
 
 
6644
                        if ( event ) {
 
6645
                                event.preventDefault();
 
6646
                        }
 
6647
 
 
6648
                        _.each( this.$el.serializeArray(), function( pair ) {
 
6649
                                data[ pair.name ] = pair.value;
 
6650
                        });
 
6651
 
 
6652
                        this.model.saveCompat( data );
 
6653
                }
 
6654
        });
 
6655
 
 
6656
        /**
 
6657
         * wp.media.view.Iframe
 
6658
         *
 
6659
         * @constructor
 
6660
         * @augments wp.media.View
 
6661
         * @augments wp.Backbone.View
 
6662
         * @augments Backbone.View
 
6663
         */
 
6664
        media.view.Iframe = media.View.extend({
 
6665
                className: 'media-iframe',
 
6666
                /**
 
6667
                 * @returns {wp.media.view.Iframe} Returns itself to allow chaining
 
6668
                 */
 
6669
                render: function() {
 
6670
                        this.views.detach();
 
6671
                        this.$el.html( '<iframe src="' + this.controller.state().get('src') + '" />' );
 
6672
                        this.views.render();
 
6673
                        return this;
 
6674
                }
 
6675
        });
 
6676
 
 
6677
        /**
 
6678
         * wp.media.view.Embed
 
6679
         *
 
6680
         * @constructor
 
6681
         * @augments wp.media.View
 
6682
         * @augments wp.Backbone.View
 
6683
         * @augments Backbone.View
 
6684
         */
 
6685
        media.view.Embed = media.View.extend({
 
6686
                className: 'media-embed',
 
6687
 
 
6688
                initialize: function() {
 
6689
                        /**
 
6690
                         * @member {wp.media.view.EmbedUrl}
 
6691
                         */
 
6692
                        this.url = new media.view.EmbedUrl({
 
6693
                                controller: this.controller,
 
6694
                                model:      this.model.props
 
6695
                        }).render();
 
6696
 
 
6697
                        this.views.set([ this.url ]);
 
6698
                        this.refresh();
 
6699
                        this.model.on( 'change:type', this.refresh, this );
 
6700
                        this.model.on( 'change:loading', this.loading, this );
 
6701
                },
 
6702
 
 
6703
                /**
 
6704
                 * @param {Object} view
 
6705
                 */
 
6706
                settings: function( view ) {
 
6707
                        if ( this._settings ) {
 
6708
                                this._settings.remove();
 
6709
                        }
 
6710
                        this._settings = view;
 
6711
                        this.views.add( view );
 
6712
                },
 
6713
 
 
6714
                refresh: function() {
 
6715
                        var type = this.model.get('type'),
 
6716
                                constructor;
 
6717
 
 
6718
                        if ( 'image' === type ) {
 
6719
                                constructor = media.view.EmbedImage;
 
6720
                        } else if ( 'link' === type ) {
 
6721
                                constructor = media.view.EmbedLink;
 
6722
                        } else {
 
6723
                                return;
 
6724
                        }
 
6725
 
 
6726
                        this.settings( new constructor({
 
6727
                                controller: this.controller,
 
6728
                                model:      this.model.props,
 
6729
                                priority:   40
 
6730
                        }) );
 
6731
                },
 
6732
 
 
6733
                loading: function() {
 
6734
                        this.$el.toggleClass( 'embed-loading', this.model.get('loading') );
 
6735
                }
 
6736
        });
 
6737
 
 
6738
        /**
 
6739
         * @constructor
 
6740
         * @augments wp.media.View
 
6741
         * @augments wp.Backbone.View
 
6742
         * @augments Backbone.View
 
6743
         */
 
6744
        media.view.Label = media.View.extend({
 
6745
                tagName: 'label',
 
6746
                className: 'screen-reader-text',
 
6747
 
 
6748
                initialize: function() {
 
6749
                        this.value = this.options.value;
 
6750
                },
 
6751
 
 
6752
                render: function() {
 
6753
                        this.$el.html( this.value );
 
6754
 
 
6755
                        return this;
 
6756
                }
 
6757
        });
 
6758
 
 
6759
        /**
 
6760
         * wp.media.view.EmbedUrl
 
6761
         *
 
6762
         * @constructor
 
6763
         * @augments wp.media.View
 
6764
         * @augments wp.Backbone.View
 
6765
         * @augments Backbone.View
 
6766
         */
 
6767
        media.view.EmbedUrl = media.View.extend({
 
6768
                tagName:   'label',
 
6769
                className: 'embed-url',
 
6770
 
 
6771
                events: {
 
6772
                        'input':  'url',
 
6773
                        'keyup':  'url',
 
6774
                        'change': 'url'
 
6775
                },
 
6776
 
 
6777
                initialize: function() {
 
6778
                        var self = this;
 
6779
 
 
6780
                        this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') );
 
6781
                        this.input = this.$input[0];
 
6782
 
 
6783
                        this.spinner = $('<span class="spinner" />')[0];
 
6784
                        this.$el.append([ this.input, this.spinner ]);
 
6785
 
 
6786
                        this.model.on( 'change:url', this.render, this );
 
6787
 
 
6788
                        if ( this.model.get( 'url' ) ) {
 
6789
                                _.delay( function () {
 
6790
                                        self.model.trigger( 'change:url' );
 
6791
                                }, 500 );
 
6792
                        }
 
6793
                },
 
6794
                /**
 
6795
                 * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining
 
6796
                 */
 
6797
                render: function() {
 
6798
                        var $input = this.$input;
 
6799
 
 
6800
                        if ( $input.is(':focus') ) {
 
6801
                                return;
 
6802
                        }
 
6803
 
 
6804
                        this.input.value = this.model.get('url') || 'http://';
 
6805
                        /**
 
6806
                         * Call `render` directly on parent class with passed arguments
 
6807
                         */
 
6808
                        media.View.prototype.render.apply( this, arguments );
 
6809
                        return this;
 
6810
                },
 
6811
 
 
6812
                ready: function() {
 
6813
                        if ( ! isTouchDevice ) {
 
6814
                                this.focus();
 
6815
                        }
 
6816
                },
 
6817
 
 
6818
                url: function( event ) {
 
6819
                        this.model.set( 'url', event.target.value );
 
6820
                },
 
6821
 
 
6822
                /**
 
6823
                 * If the input is visible, focus and select its contents.
 
6824
                 */
 
6825
                focus: function() {
 
6826
                        var $input = this.$input;
 
6827
                        if ( $input.is(':visible') ) {
 
6828
                                $input.focus()[0].select();
 
6829
                        }
 
6830
                }
 
6831
        });
 
6832
 
 
6833
        /**
 
6834
         * wp.media.view.EmbedLink
 
6835
         *
 
6836
         * @constructor
 
6837
         * @augments wp.media.view.Settings
 
6838
         * @augments wp.media.View
 
6839
         * @augments wp.Backbone.View
 
6840
         * @augments Backbone.View
 
6841
         */
 
6842
        media.view.EmbedLink = media.view.Settings.extend({
 
6843
                className: 'embed-link-settings',
 
6844
                template:  media.template('embed-link-settings'),
 
6845
 
 
6846
                initialize: function() {
 
6847
                        this.spinner = $('<span class="spinner" />');
 
6848
                        this.$el.append( this.spinner[0] );
 
6849
                        this.listenTo( this.model, 'change:url', this.updateoEmbed );
 
6850
                },
 
6851
 
 
6852
                updateoEmbed: function() {
 
6853
                        var url = this.model.get( 'url' );
 
6854
 
 
6855
                        this.$('.setting.title').show();
 
6856
                        // clear out previous results
 
6857
                        this.$('.embed-container').hide().find('.embed-preview').html('');
 
6858
 
 
6859
                        // only proceed with embed if the field contains more than 6 characters
 
6860
                        if ( url && url.length < 6 ) {
 
6861
                                return;
 
6862
                        }
 
6863
 
 
6864
                        this.spinner.show();
 
6865
 
 
6866
                        setTimeout( _.bind( this.fetch, this ), 500 );
 
6867
                },
 
6868
 
 
6869
                fetch: function() {
 
6870
                        // check if they haven't typed in 500 ms
 
6871
                        if ( $('#embed-url-field').val() !== this.model.get('url') ) {
 
6872
                                return;
 
6873
                        }
 
6874
 
 
6875
                        wp.ajax.send( 'parse-embed', {
 
6876
                                data : {
 
6877
                                        post_ID: media.view.settings.post.id,
 
6878
                                        shortcode: '[embed]' + this.model.get('url') + '[/embed]'
 
6879
                                }
 
6880
                        } ).done( _.bind( this.renderoEmbed, this ) );
 
6881
                },
 
6882
 
 
6883
                renderoEmbed: function( response ) {
 
6884
                        var html = ( response && response.body ) || '';
 
6885
 
 
6886
                        this.spinner.hide();
 
6887
 
 
6888
                        this.$('.setting.title').hide();
 
6889
                        this.$('.embed-container').show().find('.embed-preview').html( html );
 
6890
                }
 
6891
        });
 
6892
 
 
6893
        /**
 
6894
         * wp.media.view.EmbedImage
 
6895
         *
 
6896
         * @constructor
 
6897
         * @augments wp.media.view.Settings.AttachmentDisplay
 
6898
         * @augments wp.media.view.Settings
 
6899
         * @augments wp.media.View
 
6900
         * @augments wp.Backbone.View
 
6901
         * @augments Backbone.View
 
6902
         */
 
6903
        media.view.EmbedImage =  media.view.Settings.AttachmentDisplay.extend({
 
6904
                className: 'embed-media-settings',
 
6905
                template:  media.template('embed-image-settings'),
 
6906
 
 
6907
                initialize: function() {
 
6908
                        /**
 
6909
                         * Call `initialize` directly on parent class with passed arguments
 
6910
                         */
 
6911
                        media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
 
6912
                        this.model.on( 'change:url', this.updateImage, this );
 
6913
                },
 
6914
 
 
6915
                updateImage: function() {
 
6916
                        this.$('img').attr( 'src', this.model.get('url') );
 
6917
                }
 
6918
        });
 
6919
 
 
6920
        /**
 
6921
         * wp.media.view.ImageDetails
 
6922
         *
 
6923
         * @constructor
 
6924
         * @augments wp.media.view.Settings.AttachmentDisplay
 
6925
         * @augments wp.media.view.Settings
 
6926
         * @augments wp.media.View
 
6927
         * @augments wp.Backbone.View
 
6928
         * @augments Backbone.View
 
6929
         */
 
6930
        media.view.ImageDetails = media.view.Settings.AttachmentDisplay.extend({
 
6931
                className: 'image-details',
 
6932
                template:  media.template('image-details'),
 
6933
                events: _.defaults( media.view.Settings.AttachmentDisplay.prototype.events, {
 
6934
                        'click .edit-attachment': 'editAttachment',
 
6935
                        'click .replace-attachment': 'replaceAttachment',
 
6936
                        'click .advanced-toggle': 'onToggleAdvanced',
 
6937
                        'change [data-setting="customWidth"]': 'onCustomSize',
 
6938
                        'change [data-setting="customHeight"]': 'onCustomSize',
 
6939
                        'keyup [data-setting="customWidth"]': 'onCustomSize',
 
6940
                        'keyup [data-setting="customHeight"]': 'onCustomSize'
 
6941
                } ),
 
6942
                initialize: function() {
 
6943
                        // used in AttachmentDisplay.prototype.updateLinkTo
 
6944
                        this.options.attachment = this.model.attachment;
 
6945
                        this.listenTo( this.model, 'change:url', this.updateUrl );
 
6946
                        this.listenTo( this.model, 'change:link', this.toggleLinkSettings );
 
6947
                        this.listenTo( this.model, 'change:size', this.toggleCustomSize );
 
6948
 
 
6949
                        media.view.Settings.AttachmentDisplay.prototype.initialize.apply( this, arguments );
 
6950
                },
 
6951
 
 
6952
                prepare: function() {
 
6953
                        var attachment = false;
 
6954
 
 
6955
                        if ( this.model.attachment ) {
 
6956
                                attachment = this.model.attachment.toJSON();
 
6957
                        }
 
6958
                        return _.defaults({
 
6959
                                model: this.model.toJSON(),
 
6960
                                attachment: attachment
 
6961
                        }, this.options );
 
6962
                },
 
6963
 
 
6964
                render: function() {
 
6965
                        var self = this,
 
6966
                                args = arguments;
 
6967
 
 
6968
                        if ( this.model.attachment && 'pending' === this.model.dfd.state() ) {
 
6969
                                this.model.dfd.done( function() {
 
6970
                                        media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
 
6971
                                        self.postRender();
 
6972
                                } ).fail( function() {
 
6973
                                        self.model.attachment = false;
 
6974
                                        media.view.Settings.AttachmentDisplay.prototype.render.apply( self, args );
 
6975
                                        self.postRender();
 
6976
                                } );
 
6977
                        } else {
 
6978
                                media.view.Settings.AttachmentDisplay.prototype.render.apply( this, arguments );
 
6979
                                this.postRender();
 
6980
                        }
 
6981
 
 
6982
                        return this;
 
6983
                },
 
6984
 
 
6985
                postRender: function() {
 
6986
                        setTimeout( _.bind( this.resetFocus, this ), 10 );
 
6987
                        this.toggleLinkSettings();
 
6988
                        if ( getUserSetting( 'advImgDetails' ) === 'show' ) {
 
6989
                                this.toggleAdvanced( true );
 
6990
                        }
 
6991
                        this.trigger( 'post-render' );
 
6992
                },
 
6993
 
 
6994
                resetFocus: function() {
 
6995
                        this.$( '.link-to-custom' ).blur();
 
6996
                        this.$( '.embed-media-settings' ).scrollTop( 0 );
 
6997
                },
 
6998
 
 
6999
                updateUrl: function() {
 
7000
                        this.$( '.image img' ).attr( 'src', this.model.get( 'url' ) );
 
7001
                        this.$( '.url' ).val( this.model.get( 'url' ) );
 
7002
                },
 
7003
 
 
7004
                toggleLinkSettings: function() {
 
7005
                        if ( this.model.get( 'link' ) === 'none' ) {
 
7006
                                this.$( '.link-settings' ).addClass('hidden');
 
7007
                        } else {
 
7008
                                this.$( '.link-settings' ).removeClass('hidden');
 
7009
                        }
 
7010
                },
 
7011
 
 
7012
                toggleCustomSize: function() {
 
7013
                        if ( this.model.get( 'size' ) !== 'custom' ) {
 
7014
                                this.$( '.custom-size' ).addClass('hidden');
 
7015
                        } else {
 
7016
                                this.$( '.custom-size' ).removeClass('hidden');
 
7017
                        }
 
7018
                },
 
7019
 
 
7020
                onCustomSize: function( event ) {
 
7021
                        var dimension = $( event.target ).data('setting'),
 
7022
                                num = $( event.target ).val(),
 
7023
                                value;
 
7024
 
 
7025
                        // Ignore bogus input
 
7026
                        if ( ! /^\d+/.test( num ) || parseInt( num, 10 ) < 1 ) {
 
7027
                                event.preventDefault();
 
7028
                                return;
 
7029
                        }
 
7030
 
 
7031
                        if ( dimension === 'customWidth' ) {
 
7032
                                value = Math.round( 1 / this.model.get( 'aspectRatio' ) * num );
 
7033
                                this.model.set( 'customHeight', value, { silent: true } );
 
7034
                                this.$( '[data-setting="customHeight"]' ).val( value );
 
7035
                        } else {
 
7036
                                value = Math.round( this.model.get( 'aspectRatio' ) * num );
 
7037
                                this.model.set( 'customWidth', value, { silent: true  } );
 
7038
                                this.$( '[data-setting="customWidth"]' ).val( value );
 
7039
                        }
 
7040
                },
 
7041
 
 
7042
                onToggleAdvanced: function( event ) {
 
7043
                        event.preventDefault();
 
7044
                        this.toggleAdvanced();
 
7045
                },
 
7046
 
 
7047
                toggleAdvanced: function( show ) {
 
7048
                        var $advanced = this.$el.find( '.advanced-section' ),
 
7049
                                mode;
 
7050
 
 
7051
                        if ( $advanced.hasClass('advanced-visible') || show === false ) {
 
7052
                                $advanced.removeClass('advanced-visible');
 
7053
                                $advanced.find('.advanced-settings').addClass('hidden');
 
7054
                                mode = 'hide';
 
7055
                        } else {
 
7056
                                $advanced.addClass('advanced-visible');
 
7057
                                $advanced.find('.advanced-settings').removeClass('hidden');
 
7058
                                mode = 'show';
 
7059
                        }
 
7060
 
 
7061
                        setUserSetting( 'advImgDetails', mode );
 
7062
                },
 
7063
 
 
7064
                editAttachment: function( event ) {
 
7065
                        var editState = this.controller.states.get( 'edit-image' );
 
7066
 
 
7067
                        if ( window.imageEdit && editState ) {
 
7068
                                event.preventDefault();
 
7069
                                editState.set( 'image', this.model.attachment );
 
7070
                                this.controller.setState( 'edit-image' );
 
7071
                        }
 
7072
                },
 
7073
 
 
7074
                replaceAttachment: function( event ) {
 
7075
                        event.preventDefault();
 
7076
                        this.controller.setState( 'replace-image' );
 
7077
                }
 
7078
        });
 
7079
 
 
7080
        /**
 
7081
         * wp.media.view.Cropper
 
7082
         *
 
7083
         * Uses the imgAreaSelect plugin to allow a user to crop an image.
 
7084
         *
 
7085
         * Takes imgAreaSelect options from
 
7086
         * wp.customize.HeaderControl.calculateImageSelectOptions via
 
7087
         * wp.customize.HeaderControl.openMM.
 
7088
         *
 
7089
         * @constructor
 
7090
         * @augments wp.media.View
 
7091
         * @augments wp.Backbone.View
 
7092
         * @augments Backbone.View
 
7093
         */
 
7094
        media.view.Cropper = media.View.extend({
 
7095
                className: 'crop-content',
 
7096
                template: media.template('crop-content'),
 
7097
                initialize: function() {
 
7098
                        _.bindAll(this, 'onImageLoad');
 
7099
                },
 
7100
                ready: function() {
 
7101
                        this.controller.frame.on('content:error:crop', this.onError, this);
 
7102
                        this.$image = this.$el.find('.crop-image');
 
7103
                        this.$image.on('load', this.onImageLoad);
 
7104
                        $(window).on('resize.cropper', _.debounce(this.onImageLoad, 250));
 
7105
                },
 
7106
                remove: function() {
 
7107
                        $(window).off('resize.cropper');
 
7108
                        this.$el.remove();
 
7109
                        this.$el.off();
 
7110
                        wp.media.View.prototype.remove.apply(this, arguments);
 
7111
                },
 
7112
                prepare: function() {
 
7113
                        return {
 
7114
                                title: l10n.cropYourImage,
 
7115
                                url: this.options.attachment.get('url')
 
7116
                        };
 
7117
                },
 
7118
                onImageLoad: function() {
 
7119
                        var imgOptions = this.controller.get('imgSelectOptions');
 
7120
                        if (typeof imgOptions === 'function') {
 
7121
                                imgOptions = imgOptions(this.options.attachment, this.controller);
 
7122
                        }
 
7123
 
 
7124
                        imgOptions = _.extend(imgOptions, {parent: this.$el});
 
7125
                        this.trigger('image-loaded');
 
7126
                        this.controller.imgSelect = this.$image.imgAreaSelect(imgOptions);
 
7127
                },
 
7128
                onError: function() {
 
7129
                        var filename = this.options.attachment.get('filename');
 
7130
 
 
7131
                        this.views.add( '.upload-errors', new media.view.UploaderStatusError({
 
7132
                                filename: media.view.UploaderStatus.prototype.filename(filename),
 
7133
                                message: _wpMediaViewsL10n.cropError
 
7134
                        }), { at: 0 });
 
7135
                }
 
7136
        });
 
7137
 
 
7138
        media.view.EditImage = media.View.extend({
 
7139
 
 
7140
                className: 'image-editor',
 
7141
                template: media.template('image-editor'),
 
7142
 
 
7143
                initialize: function( options ) {
 
7144
                        this.editor = window.imageEdit;
 
7145
                        this.controller = options.controller;
 
7146
                        media.View.prototype.initialize.apply( this, arguments );
 
7147
                },
 
7148
 
 
7149
                prepare: function() {
 
7150
                        return this.model.toJSON();
 
7151
                },
 
7152
 
 
7153
                render: function() {
 
7154
                        media.View.prototype.render.apply( this, arguments );
 
7155
                        return this;
 
7156
                },
 
7157
 
 
7158
                loadEditor: function() {
 
7159
                        var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
 
7160
                        dfd.done( _.bind( this.focus, this ) );
 
7161
                },
 
7162
 
 
7163
                focus: function() {
 
7164
                        this.$( '.imgedit-submit .button' ).eq( 0 ).focus();
 
7165
                },
 
7166
 
 
7167
                back: function() {
 
7168
                        var lastState = this.controller.lastState();
 
7169
                        this.controller.setState( lastState );
 
7170
                },
 
7171
 
 
7172
                refresh: function() {
 
7173
                        this.model.fetch();
 
7174
                },
 
7175
 
 
7176
                save: function() {
 
7177
                        var self = this,
 
7178
                                lastState = this.controller.lastState();
 
7179
 
 
7180
                        this.model.fetch().done( function() {
 
7181
                                self.controller.setState( lastState );
 
7182
                        });
 
7183
                }
 
7184
 
 
7185
        });
 
7186
 
 
7187
        /**
 
7188
         * wp.media.view.Spinner
 
7189
         *
 
7190
         * @constructor
 
7191
         * @augments wp.media.View
 
7192
         * @augments wp.Backbone.View
 
7193
         * @augments Backbone.View
 
7194
         */
 
7195
        media.view.Spinner = media.View.extend({
 
7196
                tagName:   'span',
 
7197
                className: 'spinner',
 
7198
                spinnerTimeout: false,
 
7199
                delay: 400,
 
7200
 
 
7201
                show: function() {
 
7202
                        if ( ! this.spinnerTimeout ) {
 
7203
                                this.spinnerTimeout = _.delay(function( $el ) {
 
7204
                                        $el.show();
 
7205
                                }, this.delay, this.$el );
 
7206
                        }
 
7207
 
 
7208
                        return this;
 
7209
                },
 
7210
 
 
7211
                hide: function() {
 
7212
                        this.$el.hide();
 
7213
                        this.spinnerTimeout = clearTimeout( this.spinnerTimeout );
 
7214
 
 
7215
                        return this;
 
7216
                }
 
7217
        });
 
7218
}(jQuery, _));