]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-includes/js/media-views.js
WordPress 4.1.4
[autoinstalls/wordpress.git] / wp-includes / js / media-views.js
index 0d70ebf2381a164c76862dee14cb6e1205faf2d7..ffe65f5c79bbd7a8b355f144e0923c3c2404220d 100644 (file)
@@ -1,6 +1,8 @@
 /* global _wpMediaViewsL10n, confirm, getUserSetting, setUserSetting */
-(function($, _){
-       var media = wp.media, l10n;
+( function( $, _ ) {
+       var l10n,
+               media = wp.media,
+               isTouchDevice = ( 'ontouchend' in document );
 
        // Link any localized strings.
        l10n = media.view.l10n = typeof _wpMediaViewsL10n === 'undefined' ? {} : _wpMediaViewsL10n;
                return deferred.promise();
        };
 
-       /**
-        * ========================================================================
-        * CONTROLLERS
-        * ========================================================================
-        */
-
        /**
         * wp.media.controller.Region
         *
-        * @constructor
-        * @augments Backbone.Model
+        * A region is a persistent application layout area.
         *
-        * @param {Object} [options={}]
+        * A region assumes one mode at any time, and can be switched to another.
+        *
+        * When mode changes, events are triggered on the region's parent view.
+        * The parent view will listen to specific events and fill the region with an
+        * appropriate view depending on mode. For example, a frame listens for the
+        * 'browse' mode t be activated on the 'content' view and then fills the region
+        * with an AttachmentsBrowser view.
+        *
+        * @class
+        *
+        * @param {object}        options          Options hash for the region.
+        * @param {string}        options.id       Unique identifier for the region.
+        * @param {Backbone.View} options.view     A parent view the region exists within.
+        * @param {string}        options.selector jQuery selector for the region within the parent view.
         */
        media.controller.Region = function( options ) {
                _.extend( this, _.pick( options || {}, 'id', 'view', 'selector' ) );
 
        _.extend( media.controller.Region.prototype, {
                /**
-                * Switch modes
+                * Activate a mode.
+                *
+                * @since 3.5.0
                 *
                 * @param {string} mode
                 *
-                * @fires wp.media.controller.Region#{id}:activate:{mode}
-                * @fires wp.media.controller.Region#{id}:deactivate:{mode}
+                * @fires this.view#{this.id}:activate:{this._mode}
+                * @fires this.view#{this.id}:activate
+                * @fires this.view#{this.id}:deactivate:{this._mode}
+                * @fires this.view#{this.id}:deactivate
                 *
-                * @returns {wp.media.controller.Region} Returns itself to allow chaining
+                * @returns {wp.media.controller.Region} Returns itself to allow chaining.
                 */
                mode: function( mode ) {
                        if ( ! mode ) {
                                return this;
                        }
 
+                       /**
+                        * Region mode deactivation event.
+                        *
+                        * @event this.view#{this.id}:deactivate:{this._mode}
+                        * @event this.view#{this.id}:deactivate
+                        */
                        this.trigger('deactivate');
+
                        this._mode = mode;
                        this.render( mode );
+
+                       /**
+                        * Region mode activation event.
+                        *
+                        * @event this.view#{this.id}:activate:{this._mode}
+                        * @event this.view#{this.id}:activate
+                        */
                        this.trigger('activate');
                        return this;
                },
                /**
-                * Render a new mode, the view is set in the `create` callback method
-                *   of the extending class
+                * Render a mode.
                 *
-                * If no mode is provided, just re-render the current mode.
-                * If the provided mode isn't active, perform a full switch.
+                * @since 3.5.0
                 *
                 * @param {string} mode
                 *
-                * @fires wp.media.controller.Region#{id}:create:{mode}
-                * @fires wp.media.controller.Region#{id}:render:{mode}
+                * @fires this.view#{this.id}:create:{this._mode}
+                * @fires this.view#{this.id}:create
+                * @fires this.view#{this.id}:render:{this._mode}
+                * @fires this.view#{this.id}:render
                 *
                 * @returns {wp.media.controller.Region} Returns itself to allow chaining
                 */
                render: function( mode ) {
+                       // If the mode isn't active, activate it.
                        if ( mode && mode !== this._mode ) {
                                return this.mode( mode );
                        }
                        var set = { view: null },
                                view;
 
+                       /**
+                        * Create region view event.
+                        *
+                        * Region view creation takes place in an event callback on the frame.
+                        *
+                        * @event this.view#{this.id}:create:{this._mode}
+                        * @event this.view#{this.id}:create
+                        */
                        this.trigger( 'create', set );
                        view = set.view;
+
+                       /**
+                        * Render region view event.
+                        *
+                        * Region view creation takes place in an event callback on the frame.
+                        *
+                        * @event this.view#{this.id}:create:{this._mode}
+                        * @event this.view#{this.id}:create
+                        */
                        this.trigger( 'render', view );
                        if ( view ) {
                                this.set( view );
                },
 
                /**
-                * @returns {wp.media.View} Returns the selector's first subview
+                * Get the region's view.
+                *
+                * @since 3.5.0
+                *
+                * @returns {wp.media.View}
                 */
                get: function() {
                        return this.view.views.first( this.selector );
                },
 
                /**
+                * Set the region's view as a subview of the frame.
+                *
+                * @since 3.5.0
+                *
                 * @param {Array|Object} views
                 * @param {Object} [options={}]
                 * @returns {wp.Backbone.Subviews} Subviews is returned to allow chaining
                },
 
                /**
-                * Helper function to trigger view events based on {id}:{event}:{mode}
+                * Trigger regional view events on the frame.
+                *
+                * @since 3.5.0
                 *
                 * @param {string} event
-                * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining
+                * @returns {undefined|wp.media.controller.Region} Returns itself to allow chaining.
                 */
                trigger: function( event ) {
                        var base, args;
                        args = _.toArray( arguments );
                        base = this.id + ':' + event;
 
-                       // Trigger `region:action:mode` event.
+                       // Trigger `{this.id}:{event}:{this._mode}` event on the frame.
                        args[0] = base + ':' + this._mode;
                        this.view.trigger.apply( this.view, args );
 
-                       // Trigger `region:action` event.
+                       // Trigger `{this.id}:{event}` event on the frame.
                        args[0] = base;
                        this.view.trigger.apply( this.view, args );
                        return this;
        /**
         * wp.media.controller.StateMachine
         *
-        * @constructor
+        * A state machine keeps track of state. It is in one state at a time,
+        * and can change from one state to another.
+        *
+        * States are stored as models in a Backbone collection.
+        *
+        * @since 3.5.0
+        *
+        * @class
         * @augments Backbone.Model
         * @mixin
         * @mixes Backbone.Events
         * @param {Array} states
         */
        media.controller.StateMachine = function( states ) {
+               // @todo This is dead code. The states collection gets created in media.view.Frame._createStates.
                this.states = new Backbone.Collection( states );
        };
 
        // Use Backbone's self-propagating `extend` inheritance method.
        media.controller.StateMachine.extend = Backbone.Model.extend;
 
-       // Add events to the `StateMachine`.
        _.extend( media.controller.StateMachine.prototype, Backbone.Events, {
                /**
                 * Fetch a state.
                 * Ensure that the `states` collection exists so the `StateMachine`
                 *   can be used as a mixin.
                 *
+                * @since 3.5.0
+                *
                 * @param {string} id
                 * @returns {wp.media.controller.State} Returns a State model
                 *   from the StateMachine collection
                 * created the `states` collection, or are trying to select a state
                 * that does not exist.
                 *
+                * @since 3.5.0
+                *
                 * @param {string} id
                 *
                 * @fires wp.media.controller.State#deactivate
                 * Call the `state()` method with no parameters to retrieve the current
                 * active state.
                 *
+                * @since 3.5.0
+                *
                 * @returns {wp.media.controller.State} Returns a State model
                 *    from the StateMachine collection
                 */
                }
        });
 
-       // Map methods from the `states` collection to the `StateMachine` itself.
+       // Map all event binding and triggering on a StateMachine to its `states` collection.
        _.each([ 'on', 'off', 'trigger' ], function( method ) {
                /**
-                * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining
+                * @returns {wp.media.controller.StateMachine} Returns itself to allow chaining.
                 */
                media.controller.StateMachine.prototype[ method ] = function() {
                        // Ensure that the `states` collection exists so the `StateMachine`
        /**
         * wp.media.controller.State
         *
-        * A state is a step in a workflow that when set will trigger
-        * the controllers for the regions to be updated as specified. This
-        * class is the base class that the various states used in the media
-        * modals extend.
+        * A state is a step in a workflow that when set will trigger the controllers
+        * for the regions to be updated as specified in the frame.
+        *
+        * A state has an event-driven lifecycle:
         *
-        * @constructor
+        *     'ready'      triggers when a state is added to a state machine's collection.
+        *     'activate'   triggers when a state is activated by a state machine.
+        *     'deactivate' triggers when a state is deactivated by a state machine.
+        *     'reset'      is not triggered automatically. It should be invoked by the
+        *                  proper controller to reset the state to its default.
+        *
+        * @class
         * @augments Backbone.Model
         */
        media.controller.State = Backbone.Model.extend({
+               /**
+                * Constructor.
+                *
+                * @since 3.5.0
+                */
                constructor: function() {
                        this.on( 'activate', this._preActivate, this );
                        this.on( 'activate', this.activate, this );
                        Backbone.Model.apply( this, arguments );
                        this.on( 'change:menu', this._updateMenu, this );
                },
-
                /**
+                * Ready event callback.
+                *
                 * @abstract
+                * @since 3.5.0
                 */
                ready: function() {},
+
                /**
+                * Activate event callback.
+                *
                 * @abstract
+                * @since 3.5.0
                 */
                activate: function() {},
+
                /**
+                * Deactivate event callback.
+                *
                 * @abstract
+                * @since 3.5.0
                 */
                deactivate: function() {},
+
                /**
+                * Reset event callback.
+                *
                 * @abstract
+                * @since 3.5.0
                 */
                reset: function() {},
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _ready: function() {
                        this._updateMenu();
                },
+
                /**
                 * @access private
-                */
+                * @since 3.5.0
+               */
                _preActivate: function() {
                        this.active = true;
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _postActivate: function() {
                        this.on( 'change:menu', this._menu, this );
                        this._content();
                        this._router();
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _deactivate: function() {
                        this.active = false;
                        this.off( 'change:content', this._content, this );
                        this.off( 'change:toolbar', this._toolbar, this );
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _title: function() {
                        this.frame.title.render( this.get('titleMode') || 'default' );
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _renderTitle: function( view ) {
                        view.$el.text( this.get('title') || '' );
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _router: function() {
                        var router = this.frame.router,
                                view.select( this.frame.content.mode() );
                        }
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _menu: function() {
                        var menu = this.frame.menu,
                                mode = this.get('menu'),
                                view;
 
+                       this.frame.$el.toggleClass( 'hide-menu', ! mode );
                        if ( ! mode ) {
                                return;
                        }
                                view.select( this.id );
                        }
                },
+
                /**
                 * @access private
+                * @since 3.5.0
                 */
                _updateMenu: function() {
                        var previous = this.previous('menu'),
                                this.frame.on( 'menu:render:' + menu, this._renderMenu, this );
                        }
                },
+
                /**
+                * Create a view in the media menu for the state.
+                *
                 * @access private
+                * @since 3.5.0
+                *
+                * @param {media.view.Menu} view The menu view.
                 */
                _renderMenu: function( view ) {
                        var menuItem = this.get('menuItem'),
                };
        });
 
+       /**
+        * wp.media.selectionSync
+        *
+        * Sync an attachments selection in a state with another state.
+        *
+        * Allows for selecting multiple images in the Insert Media workflow, and then
+        * switching to the Insert Gallery workflow while preserving the attachments selection.
+        *
+        * @mixin
+        */
        media.selectionSync = {
+               /**
+                * @since 3.5.0
+                */
                syncSelection: function() {
                        var selection = this.get('selection'),
                                manager = this.frame._selection;
                 * of the selection's attachments and the set of selected
                 * attachments that this specific selection considered invalid.
                 * Reset the difference and record the single attachment.
+                *
+                * @since 3.5.0
                 */
                recordSelection: function() {
                        var selection = this.get('selection'),
        /**
         * wp.media.controller.Library
         *
-        * @constructor
+        * A state for choosing an attachment or group of attachments from the media library.
+        *
+        * @class
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        * @mixes media.selectionSync
+        *
+        * @param {object}                          [attributes]                         The attributes hash passed to the state.
+        * @param {string}                          [attributes.id=library]              Unique identifier.
+        * @param {string}                          [attributes.title=Media library]     Title for the state. Displays in the media menu and the frame's title region.
+        * @param {wp.media.model.Attachments}      [attributes.library]                 The attachments collection to browse.
+        *                                                                               If one is not supplied, a collection of all attachments will be created.
+        * @param {wp.media.model.Selection|object} [attributes.selection]               A collection to contain attachment selections within the state.
+        *                                                                               If the 'selection' attribute is a plain JS object,
+        *                                                                               a Selection will be created using its values as the selection instance's `props` model.
+        *                                                                               Otherwise, it will copy the library's `props` model.
+        * @param {boolean}                         [attributes.multiple=false]          Whether multi-select is enabled.
+        * @param {string}                          [attributes.content=upload]          Initial mode for the content region.
+        *                                                                               Overridden by persistent user setting if 'contentUserSetting' is true.
+        * @param {string}                          [attributes.menu=default]            Initial mode for the menu region.
+        * @param {string}                          [attributes.router=browse]           Initial mode for the router region.
+        * @param {string}                          [attributes.toolbar=select]          Initial mode for the toolbar region.
+        * @param {boolean}                         [attributes.searchable=true]         Whether the library is searchable.
+        * @param {boolean|string}                  [attributes.filterable=false]        Whether the library is filterable, and if so what filters should be shown.
+        *                                                                               Accepts 'all', 'uploaded', or 'unattached'.
+        * @param {boolean}                         [attributes.sortable=true]           Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {boolean}                         [attributes.autoSelect=true]         Whether an uploaded attachment should be automatically added to the selection.
+        * @param {boolean}                         [attributes.describe=false]          Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+        * @param {boolean}                         [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+        * @param {boolean}                         [attributes.syncSelection=true]      Whether the Attachments selection should be persisted from the last state.
         */
        media.controller.Library = media.controller.State.extend({
                defaults: {
-                       id:         'library',
-                       multiple:   false, // false, 'add', 'reset'
-                       describe:   false,
-                       toolbar:    'select',
-                       sidebar:    'settings',
-                       content:    'upload',
-                       router:     'browse',
-                       menu:       'default',
-                       searchable: true,
-                       filterable: false,
-                       sortable:   true,
-                       title:      l10n.mediaLibraryTitle,
-
-                       // Uses a user setting to override the content mode.
+                       id:                 'library',
+                       title:              l10n.mediaLibraryTitle,
+                       multiple:           false,
+                       content:            'upload',
+                       menu:               'default',
+                       router:             'browse',
+                       toolbar:            'select',
+                       searchable:         true,
+                       filterable:         false,
+                       sortable:           true,
+                       autoSelect:         true,
+                       describe:           false,
                        contentUserSetting: true,
-
-                       // Sync the selection from the last state when 'multiple' matches.
-                       syncSelection: true
+                       syncSelection:      true
                },
 
                /**
                 * If a library isn't provided, query all media items.
                 * If a selection instance isn't provided, create one.
+                *
+                * @since 3.5.0
                 */
                initialize: function() {
                        var selection = this.get('selection'),
                                        props = _.omit( props, 'orderby', 'query' );
                                }
 
-                               // If the `selection` attribute is set to an object,
-                               // it will use those values as the selection instance's
-                               // `props` model. Otherwise, it will copy the library's
-                               // `props` model.
                                this.set( 'selection', new media.model.Selection( null, {
                                        multiple: this.get('multiple'),
                                        props: props
                                }) );
                        }
 
-                       if ( ! this.get('edge') ) {
-                               this.set( 'edge', 120 );
-                       }
-
-                       if ( ! this.get('gutter') ) {
-                               this.set( 'gutter', 8 );
-                       }
-
                        this.resetDisplays();
                },
 
+               /**
+                * @since 3.5.0
+                */
                activate: function() {
                        this.syncSelection();
 
 
                        this.get('selection').on( 'add remove reset', this.refreshContent, this );
 
-                       if ( this.get('contentUserSetting') ) {
+                       if ( this.get( 'router' ) && this.get('contentUserSetting') ) {
                                this.frame.on( 'content:activate', this.saveContentMode, this );
                                this.set( 'content', getUserSetting( 'libraryContent', this.get('content') ) );
                        }
                },
 
+               /**
+                * @since 3.5.0
+                */
                deactivate: function() {
                        this.recordSelection();
 
                        wp.Uploader.queue.off( null, null, this );
                },
 
+               /**
+                * Reset the library to its initial state.
+                *
+                * @since 3.5.0
+                */
                reset: function() {
                        this.get('selection').reset();
                        this.resetDisplays();
                        this.refreshContent();
                },
 
+               /**
+                * Reset the attachment display settings defaults to the site options.
+                *
+                * If site options don't define them, fall back to a persistent user setting.
+                *
+                * @since 3.5.0
+                */
                resetDisplays: function() {
                        var defaultProps = media.view.settings.defaultProps;
                        this._displays = [];
                },
 
                /**
+                * Create a model to represent display settings (alignment, etc.) for an attachment.
+                *
+                * @since 3.5.0
+                *
                 * @param {wp.media.model.Attachment} attachment
                 * @returns {Backbone.Model}
                 */
                },
 
                /**
+                * Given an attachment, create attachment display settings properties.
+                *
+                * @since 3.6.0
+                *
                 * @param {wp.media.model.Attachment} attachment
                 * @returns {Object}
                 */
                },
 
                /**
+                * Whether an attachment can be embedded (audio or video).
+                *
+                * @since 3.6.0
+                *
                 * @param {wp.media.model.Attachment} attachment
                 * @returns {Boolean}
                 */
                 * If the state is active, no items are selected, and the current
                 * content mode is not an option in the state's router (provided
                 * the state has a router), reset the content mode to the default.
+                *
+                * @since 3.5.0
                 */
                refreshContent: function() {
                        var selection = this.get('selection'),
                },
 
                /**
-                * If the uploader was selected, navigate to the browser.
+                * Callback handler when an attachment is uploaded.
+                *
+                * Switch to the Media Library if uploaded from the 'Upload Files' tab.
                 *
-                * Automatically select any uploading attachments.
+                * Adds any uploading attachments to the selection.
                 *
-                * Selections that don't support multiple attachments automatically
-                * limit themselves to one attachment (in this case, the last
-                * attachment in the upload queue).
+                * If the state only supports one attachment to be selected and multiple
+                * attachments are uploaded, the last attachment in the upload queue will
+                * be selected.
+                *
+                * @since 3.5.0
                 *
                 * @param {wp.media.model.Attachment} attachment
                 */
                        if ( 'upload' === content.mode() ) {
                                this.frame.content.mode('browse');
                        }
-                       this.get('selection').add( attachment );
+
+                       if ( this.get( 'autoSelect' ) ) {
+                               this.get('selection').add( attachment );
+                               this.frame.trigger( 'library:selection:add' );
+                       }
                },
 
                /**
-                * Only track the browse router on library states.
+                * Persist the mode of the content region as a user setting.
+                *
+                * @since 3.5.0
                 */
                saveContentMode: function() {
                        if ( 'browse' !== this.get('router') ) {
                }
        });
 
+       // Make selectionSync available on any Media Library state.
        _.extend( media.controller.Library.prototype, media.selectionSync );
 
        /**
         * wp.media.controller.ImageDetails
         *
-        * @constructor
+        * A state for editing the attachment display settings of an image that's been
+        * inserted into the editor.
+        *
+        * @class
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                    [attributes]                       The attributes hash passed to the state.
+        * @param {string}                    [attributes.id=image-details]      Unique identifier.
+        * @param {string}                    [attributes.title=Image Details]   Title for the state. Displays in the frame's title region.
+        * @param {wp.media.model.Attachment} attributes.image                   The image's model.
+        * @param {string|false}              [attributes.content=image-details] Initial mode for the content region.
+        * @param {string|false}              [attributes.menu=false]            Initial mode for the menu region.
+        * @param {string|false}              [attributes.router=false]          Initial mode for the router region.
+        * @param {string|false}              [attributes.toolbar=image-details] Initial mode for the toolbar region.
+        * @param {boolean}                   [attributes.editing=false]         Unused.
+        * @param {int}                       [attributes.priority=60]           Unused.
+        *
+        * @todo This state inherits some defaults from media.controller.Library.prototype.defaults,
+        *       however this may not do anything.
         */
        media.controller.ImageDetails = media.controller.State.extend({
                defaults: _.defaults({
-                       id: 'image-details',
-                       toolbar: 'image-details',
-                       title: l10n.imageDetailsTitle,
-                       content: 'image-details',
-                       menu: 'image-details',
-                       router: false,
-                       attachment: false,
-                       priority: 60,
-                       editing: false
+                       id:       'image-details',
+                       title:    l10n.imageDetailsTitle,
+                       content:  'image-details',
+                       menu:     false,
+                       router:   false,
+                       toolbar:  'image-details',
+                       editing:  false,
+                       priority: 60
                }, media.controller.Library.prototype.defaults ),
 
+               /**
+                * @since 3.9.0
+                *
+                * @param options Attributes
+                */
                initialize: function( options ) {
                        this.image = options.image;
                        media.controller.State.prototype.initialize.apply( this, arguments );
                },
 
+               /**
+                * @since 3.9.0
+                */
                activate: function() {
                        this.frame.modal.$el.addClass('image-details');
                }
        /**
         * wp.media.controller.GalleryEdit
         *
-        * @constructor
+        * A state for editing a gallery's images and settings.
+        *
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                     [attributes]                       The attributes hash passed to the state.
+        * @param {string}                     [attributes.id=gallery-edit]       Unique identifier.
+        * @param {string}                     [attributes.title=Edit Gallery]    Title for the state. Displays in the frame's title region.
+        * @param {wp.media.model.Attachments} [attributes.library]               The collection of attachments in the gallery.
+        *                                                                        If one is not supplied, an empty media.model.Selection collection is created.
+        * @param {boolean}                    [attributes.multiple=false]        Whether multi-select is enabled.
+        * @param {boolean}                    [attributes.searchable=false]      Whether the library is searchable.
+        * @param {boolean}                    [attributes.sortable=true]         Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {string|false}               [attributes.content=browse]        Initial mode for the content region.
+        * @param {string|false}               [attributes.toolbar=image-details] Initial mode for the toolbar region.
+        * @param {boolean}                    [attributes.describe=true]         Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+        * @param {boolean}                    [attributes.displaySettings=true]  Whether to show the attachment display settings interface.
+        * @param {boolean}                    [attributes.dragInfo=true]         Whether to show instructional text about the attachments being sortable.
+        * @param {int}                        [attributes.idealColumnWidth=170]  The ideal column width in pixels for attachments.
+        * @param {boolean}                    [attributes.editing=false]         Whether the gallery is being created, or editing an existing instance.
+        * @param {int}                        [attributes.priority=60]           The priority for the state link in the media menu.
+        * @param {boolean}                    [attributes.syncSelection=false]   Whether the Attachments selection should be persisted from the last state.
+        *                                                                        Defaults to false for this state, because the library passed in  *is* the selection.
+        * @param {view}                       [attributes.AttachmentView]        The single `Attachment` view to be used in the `Attachments`.
+        *                                                                        If none supplied, defaults to wp.media.view.Attachment.EditLibrary.
         */
        media.controller.GalleryEdit = media.controller.Library.extend({
                defaults: {
-                       id:         'gallery-edit',
-                       multiple:   false,
-                       describe:   true,
-                       edge:       199,
-                       editing:    false,
-                       sortable:   true,
-                       searchable: false,
-                       toolbar:    'gallery-edit',
-                       content:    'browse',
-                       title:      l10n.editGalleryTitle,
-                       priority:   60,
-                       dragInfo:   true,
-
-                       // Don't sync the selection, as the Edit Gallery library
-                       // *is* the selection.
-                       syncSelection: false
+                       id:               'gallery-edit',
+                       title:            l10n.editGalleryTitle,
+                       multiple:         false,
+                       searchable:       false,
+                       sortable:         true,
+                       display:          false,
+                       content:          'browse',
+                       toolbar:          'gallery-edit',
+                       describe:         true,
+                       displaySettings:  true,
+                       dragInfo:         true,
+                       idealColumnWidth: 170,
+                       editing:          false,
+                       priority:         60,
+                       syncSelection:    false
                },
 
+               /**
+                * @since 3.5.0
+                */
                initialize: function() {
                        // If we haven't been provided a `library`, create a `Selection`.
                        if ( ! this.get('library') )
                        media.controller.Library.prototype.initialize.apply( this, arguments );
                },
 
+               /**
+                * @since 3.5.0
+                */
                activate: function() {
                        var library = this.get('library');
 
                        media.controller.Library.prototype.activate.apply( this, arguments );
                },
 
+               /**
+                * @since 3.5.0
+                */
                deactivate: function() {
                        // Stop watching for uploaded attachments.
                        this.get('library').unobserve( wp.Uploader.queue );
                        media.controller.Library.prototype.deactivate.apply( this, arguments );
                },
 
+               /**
+                * @since 3.5.0
+                *
+                * @param browser
+                */
                gallerySettings: function( browser ) {
+                       if ( ! this.get('displaySettings') ) {
+                               return;
+                       }
+
                        var library = this.get('library');
 
-                       if ( ! library || ! browser )
+                       if ( ! library || ! browser ) {
                                return;
+                       }
 
                        library.gallery = library.gallery || new Backbone.Model();
 
        });
 
        /**
-        * wp.media.controller.GalleryAdd
+        * A state for selecting more images to add to a gallery.
         *
-        * @constructor
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                     [attributes]                         The attributes hash passed to the state.
+        * @param {string}                     [attributes.id=gallery-library]      Unique identifier.
+        * @param {string}                     [attributes.title=Add to Gallery]    Title for the state. Displays in the frame's title region.
+        * @param {boolean}                    [attributes.multiple=add]            Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean.
+        * @param {wp.media.model.Attachments} [attributes.library]                 The attachments collection to browse.
+        *                                                                          If one is not supplied, a collection of all images will be created.
+        * @param {boolean|string}             [attributes.filterable=uploaded]     Whether the library is filterable, and if so what filters should be shown.
+        *                                                                          Accepts 'all', 'uploaded', or 'unattached'.
+        * @param {string}                     [attributes.menu=gallery]            Initial mode for the menu region.
+        * @param {string}                     [attributes.content=upload]          Initial mode for the content region.
+        *                                                                          Overridden by persistent user setting if 'contentUserSetting' is true.
+        * @param {string}                     [attributes.router=browse]           Initial mode for the router region.
+        * @param {string}                     [attributes.toolbar=gallery-add]     Initial mode for the toolbar region.
+        * @param {boolean}                    [attributes.searchable=true]         Whether the library is searchable.
+        * @param {boolean}                    [attributes.sortable=true]           Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {boolean}                    [attributes.autoSelect=true]         Whether an uploaded attachment should be automatically added to the selection.
+        * @param {boolean}                    [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+        * @param {int}                        [attributes.priority=100]            The priority for the state link in the media menu.
+        * @param {boolean}                    [attributes.syncSelection=false]     Whether the Attachments selection should be persisted from the last state.
+        *                                                                          Defaults to false because for this state, because the library of the Edit Gallery state is the selection.
         */
        media.controller.GalleryAdd = media.controller.Library.extend({
                defaults: _.defaults({
-                       id:           'gallery-library',
-                       filterable:   'uploaded',
-                       multiple:     'add',
-                       menu:         'gallery',
-                       toolbar:      'gallery-add',
-                       title:        l10n.addToGalleryTitle,
-                       priority:     100,
-
-                       // Don't sync the selection, as the Edit Gallery library
-                       // *is* the selection.
+                       id:            'gallery-library',
+                       title:         l10n.addToGalleryTitle,
+                       multiple:      'add',
+                       filterable:    'uploaded',
+                       menu:          'gallery',
+                       toolbar:       'gallery-add',
+                       priority:      100,
                        syncSelection: false
                }, media.controller.Library.prototype.defaults ),
 
+               /**
+                * @since 3.5.0
+                */
                initialize: function() {
-                       // If we haven't been provided a `library`, create a `Selection`.
+                       // If a library wasn't supplied, create a library of images.
                        if ( ! this.get('library') )
                                this.set( 'library', media.query({ type: 'image' }) );
 
                        media.controller.Library.prototype.initialize.apply( this, arguments );
                },
 
+               /**
+                * @since 3.5.0
+                */
                activate: function() {
                        var library = this.get('library'),
                                edit    = this.frame.state('gallery-edit').get('library');
        /**
         * wp.media.controller.CollectionEdit
         *
-        * @constructor
+        * A state for editing a collection, which is used by audio and video playlists,
+        * and can be used for other collections.
+        *
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                     [attributes]                      The attributes hash passed to the state.
+        * @param {string}                     attributes.title                  Title for the state. Displays in the media menu and the frame's title region.
+        * @param {wp.media.model.Attachments} [attributes.library]              The attachments collection to edit.
+        *                                                                       If one is not supplied, an empty media.model.Selection collection is created.
+        * @param {boolean}                    [attributes.multiple=false]       Whether multi-select is enabled.
+        * @param {string}                     [attributes.content=browse]       Initial mode for the content region.
+        * @param {string}                     attributes.menu                   Initial mode for the menu region. @todo this needs a better explanation.
+        * @param {boolean}                    [attributes.searchable=false]     Whether the library is searchable.
+        * @param {boolean}                    [attributes.sortable=true]        Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {boolean}                    [attributes.describe=true]        Whether to offer UI to describe the attachments - e.g. captioning images in a gallery.
+        * @param {boolean}                    [attributes.dragInfo=true]        Whether to show instructional text about the attachments being sortable.
+        * @param {boolean}                    [attributes.dragInfoText]         Instructional text about the attachments being sortable.
+        * @param {int}                        [attributes.idealColumnWidth=170] The ideal column width in pixels for attachments.
+        * @param {boolean}                    [attributes.editing=false]        Whether the gallery is being created, or editing an existing instance.
+        * @param {int}                        [attributes.priority=60]          The priority for the state link in the media menu.
+        * @param {boolean}                    [attributes.syncSelection=false]  Whether the Attachments selection should be persisted from the last state.
+        *                                                                       Defaults to false for this state, because the library passed in  *is* the selection.
+        * @param {view}                       [attributes.SettingsView]         The view to edit the collection instance settings (e.g. Playlist settings with "Show tracklist" checkbox).
+        * @param {view}                       [attributes.AttachmentView]       The single `Attachment` view to be used in the `Attachments`.
+        *                                                                       If none supplied, defaults to wp.media.view.Attachment.EditLibrary.
+        * @param {string}                     attributes.type                   The collection's media type. (e.g. 'video').
+        * @param {string}                     attributes.collectionType         The collection type. (e.g. 'playlist').
         */
        media.controller.CollectionEdit = media.controller.Library.extend({
                defaults: {
-                       multiple:     false,
-                       describe:     true,
-                       edge:         199,
-                       editing:      false,
-                       sortable:     true,
-                       searchable:   false,
-                       content:      'browse',
-                       priority:     60,
-                       dragInfo:     true,
-                       SettingsView: false,
-
-                       // Don't sync the selection, as the Edit {Collection} library
-                       // *is* the selection.
-                       syncSelection: false
+                       multiple:         false,
+                       sortable:         true,
+                       searchable:       false,
+                       content:          'browse',
+                       describe:         true,
+                       dragInfo:         true,
+                       idealColumnWidth: 170,
+                       editing:          false,
+                       priority:         60,
+                       SettingsView:     false,
+                       syncSelection:    false
                },
 
+               /**
+                * @since 3.9.0
+                */
                initialize: function() {
                        var collectionType = this.get('collectionType');
 
                        media.controller.Library.prototype.initialize.apply( this, arguments );
                },
 
+               /**
+                * @since 3.9.0
+                */
                activate: function() {
                        var library = this.get('library');
 
                        media.controller.Library.prototype.activate.apply( this, arguments );
                },
 
+               /**
+                * @since 3.9.0
+                */
                deactivate: function() {
                        // Stop watching for uploaded attachments.
                        this.get('library').unobserve( wp.Uploader.queue );
                        media.controller.Library.prototype.deactivate.apply( this, arguments );
                },
 
-               renderSettings: function( browser ) {
+               /**
+                * Render the collection embed settings view in the browser sidebar.
+                *
+                * @todo This is against the pattern elsewhere in media. Typically the frame
+                *       is responsible for adding region mode callbacks. Explain.
+                *
+                * @since 3.9.0
+                *
+                * @param {wp.media.view.attachmentsBrowser} The attachments browser view.
+                */
+               renderSettings: function( attachmentsBrowserView ) {
                        var library = this.get('library'),
                                collectionType = this.get('collectionType'),
                                dragInfoText = this.get('dragInfoText'),
                                SettingsView = this.get('SettingsView'),
                                obj = {};
 
-                       if ( ! library || ! browser ) {
+                       if ( ! library || ! attachmentsBrowserView ) {
                                return;
                        }
 
                                priority:   40
                        });
 
-                       browser.sidebar.set( obj );
+                       attachmentsBrowserView.sidebar.set( obj );
 
                        if ( dragInfoText ) {
-                               browser.toolbar.set( 'dragInfo', new media.View({
+                               attachmentsBrowserView.toolbar.set( 'dragInfo', new media.View({
                                        el: $( '<div class="instructions">' + dragInfoText + '</div>' )[0],
                                        priority: -40
                                }) );
                        }
 
-                       browser.toolbar.set( 'reverse', {
+                       // Add the 'Reverse order' button to the toolbar.
+                       attachmentsBrowserView.toolbar.set( 'reverse', {
                                text:     l10n.reverseOrder,
                                priority: 80,
 
        /**
         * wp.media.controller.CollectionAdd
         *
-        * @constructor
+        * A state for adding attachments to a collection (e.g. video playlist).
+        *
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                     [attributes]                         The attributes hash passed to the state.
+        * @param {string}                     [attributes.id=library]      Unique identifier.
+        * @param {string}                     attributes.title                    Title for the state. Displays in the frame's title region.
+        * @param {boolean}                    [attributes.multiple=add]            Whether multi-select is enabled. @todo 'add' doesn't seem do anything special, and gets used as a boolean.
+        * @param {wp.media.model.Attachments} [attributes.library]                 The attachments collection to browse.
+        *                                                                          If one is not supplied, a collection of attachments of the specified type will be created.
+        * @param {boolean|string}             [attributes.filterable=uploaded]     Whether the library is filterable, and if so what filters should be shown.
+        *                                                                          Accepts 'all', 'uploaded', or 'unattached'.
+        * @param {string}                     [attributes.menu=gallery]            Initial mode for the menu region.
+        * @param {string}                     [attributes.content=upload]          Initial mode for the content region.
+        *                                                                          Overridden by persistent user setting if 'contentUserSetting' is true.
+        * @param {string}                     [attributes.router=browse]           Initial mode for the router region.
+        * @param {string}                     [attributes.toolbar=gallery-add]     Initial mode for the toolbar region.
+        * @param {boolean}                    [attributes.searchable=true]         Whether the library is searchable.
+        * @param {boolean}                    [attributes.sortable=true]           Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {boolean}                    [attributes.autoSelect=true]         Whether an uploaded attachment should be automatically added to the selection.
+        * @param {boolean}                    [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+        * @param {int}                        [attributes.priority=100]            The priority for the state link in the media menu.
+        * @param {boolean}                    [attributes.syncSelection=false]     Whether the Attachments selection should be persisted from the last state.
+        *                                                                          Defaults to false because for this state, because the library of the Edit Gallery state is the selection.
+        * @param {string}                     attributes.type                   The collection's media type. (e.g. 'video').
+        * @param {string}                     attributes.collectionType         The collection type. (e.g. 'playlist').
         */
        media.controller.CollectionAdd = media.controller.Library.extend({
                defaults: _.defaults( {
-                       filterable:    'uploaded',
+                       // Selection defaults. @see media.model.Selection
                        multiple:      'add',
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       filterable:    'uploaded',
+
                        priority:      100,
                        syncSelection: false
                }, media.controller.Library.prototype.defaults ),
 
+               /**
+                * @since 3.9.0
+                */
                initialize: function() {
                        var collectionType = this.get('collectionType');
 
                        media.controller.Library.prototype.initialize.apply( this, arguments );
                },
 
+               /**
+                * @since 3.9.0
+                */
                activate: function() {
                        var library = this.get('library'),
                                editLibrary = this.get('editLibrary'),
        /**
         * wp.media.controller.FeaturedImage
         *
-        * @constructor
+        * A state for selecting a featured image for a post.
+        *
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                     [attributes]                          The attributes hash passed to the state.
+        * @param {string}                     [attributes.id=featured-image]        Unique identifier.
+        * @param {string}                     [attributes.title=Set Featured Image] Title for the state. Displays in the media menu and the frame's title region.
+        * @param {wp.media.model.Attachments} [attributes.library]                  The attachments collection to browse.
+        *                                                                           If one is not supplied, a collection of all images will be created.
+        * @param {boolean}                    [attributes.multiple=false]           Whether multi-select is enabled.
+        * @param {string}                     [attributes.content=upload]           Initial mode for the content region.
+        *                                                                           Overridden by persistent user setting if 'contentUserSetting' is true.
+        * @param {string}                     [attributes.menu=default]             Initial mode for the menu region.
+        * @param {string}                     [attributes.router=browse]            Initial mode for the router region.
+        * @param {string}                     [attributes.toolbar=featured-image]   Initial mode for the toolbar region.
+        * @param {int}                        [attributes.priority=60]              The priority for the state link in the media menu.
+        * @param {boolean}                    [attributes.searchable=true]          Whether the library is searchable.
+        * @param {boolean|string}             [attributes.filterable=false]         Whether the library is filterable, and if so what filters should be shown.
+        *                                                                           Accepts 'all', 'uploaded', or 'unattached'.
+        * @param {boolean}                    [attributes.sortable=true]            Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {boolean}                    [attributes.autoSelect=true]          Whether an uploaded attachment should be automatically added to the selection.
+        * @param {boolean}                    [attributes.describe=false]           Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+        * @param {boolean}                    [attributes.contentUserSetting=true]  Whether the content region's mode should be set and persisted per user.
+        * @param {boolean}                    [attributes.syncSelection=true]       Whether the Attachments selection should be persisted from the last state.
         */
        media.controller.FeaturedImage = media.controller.Library.extend({
                defaults: _.defaults({
-                       id:         'featured-image',
-                       filterable: 'uploaded',
-                       multiple:   false,
-                       toolbar:    'featured-image',
-                       title:      l10n.setFeaturedImageTitle,
-                       priority:   60,
+                       id:            'featured-image',
+                       title:         l10n.setFeaturedImageTitle,
+                       multiple:      false,
+                       filterable:    'uploaded',
+                       toolbar:       'featured-image',
+                       priority:      60,
                        syncSelection: true
                }, media.controller.Library.prototype.defaults ),
 
+               /**
+                * @since 3.5.0
+                */
                initialize: function() {
                        var library, comparator;
 
                        library.observe( this.get('selection') );
                },
 
+               /**
+                * @since 3.5.0
+                */
                activate: function() {
                        this.updateSelection();
                        this.frame.on( 'open', this.updateSelection, this );
                        media.controller.Library.prototype.activate.apply( this, arguments );
                },
 
+               /**
+                * @since 3.5.0
+                */
                deactivate: function() {
                        this.frame.off( 'open', this.updateSelection, this );
 
                        media.controller.Library.prototype.deactivate.apply( this, arguments );
                },
 
+               /**
+                * @since 3.5.0
+                */
                updateSelection: function() {
                        var selection = this.get('selection'),
                                id = media.view.settings.post.featuredImageId,
        /**
         * wp.media.controller.ReplaceImage
         *
-        * Replace a selected single image
+        * A state for replacing an image.
         *
-        * @constructor
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                     [attributes]                         The attributes hash passed to the state.
+        * @param {string}                     [attributes.id=replace-image]        Unique identifier.
+        * @param {string}                     [attributes.title=Replace Image]     Title for the state. Displays in the media menu and the frame's title region.
+        * @param {wp.media.model.Attachments} [attributes.library]                 The attachments collection to browse.
+        *                                                                          If one is not supplied, a collection of all images will be created.
+        * @param {boolean}                    [attributes.multiple=false]          Whether multi-select is enabled.
+        * @param {string}                     [attributes.content=upload]          Initial mode for the content region.
+        *                                                                          Overridden by persistent user setting if 'contentUserSetting' is true.
+        * @param {string}                     [attributes.menu=default]            Initial mode for the menu region.
+        * @param {string}                     [attributes.router=browse]           Initial mode for the router region.
+        * @param {string}                     [attributes.toolbar=replace]         Initial mode for the toolbar region.
+        * @param {int}                        [attributes.priority=60]             The priority for the state link in the media menu.
+        * @param {boolean}                    [attributes.searchable=true]         Whether the library is searchable.
+        * @param {boolean|string}             [attributes.filterable=uploaded]     Whether the library is filterable, and if so what filters should be shown.
+        *                                                                          Accepts 'all', 'uploaded', or 'unattached'.
+        * @param {boolean}                    [attributes.sortable=true]           Whether the Attachments should be sortable. Depends on the orderby property being set to menuOrder on the attachments collection.
+        * @param {boolean}                    [attributes.autoSelect=true]         Whether an uploaded attachment should be automatically added to the selection.
+        * @param {boolean}                    [attributes.describe=false]          Whether to offer UI to describe attachments - e.g. captioning images in a gallery.
+        * @param {boolean}                    [attributes.contentUserSetting=true] Whether the content region's mode should be set and persisted per user.
+        * @param {boolean}                    [attributes.syncSelection=true]      Whether the Attachments selection should be persisted from the last state.
         */
        media.controller.ReplaceImage = media.controller.Library.extend({
                defaults: _.defaults({
-                       id:         'replace-image',
-                       filterable: 'uploaded',
-                       multiple:   false,
-                       toolbar:    'replace',
-                       title:      l10n.replaceImageTitle,
-                       priority:   60,
+                       id:            'replace-image',
+                       title:         l10n.replaceImageTitle,
+                       multiple:      false,
+                       filterable:    'uploaded',
+                       toolbar:       'replace',
+                       menu:          false,
+                       priority:      60,
                        syncSelection: true
                }, media.controller.Library.prototype.defaults ),
 
+               /**
+                * @since 3.9.0
+                *
+                * @param options
+                */
                initialize: function( options ) {
                        var library, comparator;
 
                        library.observe( this.get('selection') );
                },
 
+               /**
+                * @since 3.9.0
+                */
                activate: function() {
                        this.updateSelection();
                        media.controller.Library.prototype.activate.apply( this, arguments );
                },
 
+               /**
+                * @since 3.9.0
+                */
                updateSelection: function() {
                        var selection = this.get('selection'),
                                attachment = this.image.attachment;
        /**
         * wp.media.controller.EditImage
         *
-        * @constructor
+        * A state for editing (cropping, etc.) an image.
+        *
+        * @class
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object}                    attributes                      The attributes hash passed to the state.
+        * @param {wp.media.model.Attachment} attributes.model                The attachment.
+        * @param {string}                    [attributes.id=edit-image]      Unique identifier.
+        * @param {string}                    [attributes.title=Edit Image]   Title for the state. Displays in the media menu and the frame's title region.
+        * @param {string}                    [attributes.content=edit-image] Initial mode for the content region.
+        * @param {string}                    [attributes.toolbar=edit-image] Initial mode for the toolbar region.
+        * @param {string}                    [attributes.menu=false]         Initial mode for the menu region.
+        * @param {string}                    [attributes.url]                Unused. @todo Consider removal.
         */
        media.controller.EditImage = media.controller.State.extend({
                defaults: {
-                       id: 'edit-image',
-                       url: '',
-                       menu: false,
+                       id:      'edit-image',
+                       title:   l10n.editImage,
+                       menu:    false,
                        toolbar: 'edit-image',
-                       title: l10n.editImage,
-                       content: 'edit-image'
+                       content: 'edit-image',
+                       url:     ''
                },
 
+               /**
+                * @since 3.9.0
+                */
                activate: function() {
                        this.listenTo( this.frame, 'toolbar:render:edit-image', this.toolbar );
                },
 
+               /**
+                * @since 3.9.0
+                */
                deactivate: function() {
                        this.stopListening( this.frame );
                },
 
+               /**
+                * @since 3.9.0
+                */
                toolbar: function() {
                        var frame = this.frame,
                                lastState = frame.lastState(),
        /**
         * wp.media.controller.MediaLibrary
         *
-        * @constructor
+        * @class
         * @augments wp.media.controller.Library
         * @augments wp.media.controller.State
         * @augments Backbone.Model
         */
        media.controller.MediaLibrary = media.controller.Library.extend({
                defaults: _.defaults({
-                       filterable: 'uploaded',
-                       priority:   80,
-                       syncSelection: false,
-                       displaySettings: false
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       filterable:      'uploaded',
+
+                       displaySettings: false,
+                       priority:        80,
+                       syncSelection:   false
                }, media.controller.Library.prototype.defaults ),
 
+               /**
+                * @since 3.9.0
+                *
+                * @param options
+                */
                initialize: function( options ) {
                        this.media = options.media;
                        this.type = options.type;
                        media.controller.Library.prototype.initialize.apply( this, arguments );
                },
 
+               /**
+                * @since 3.9.0
+                */
                activate: function() {
+                       // @todo this should use this.frame.
                        if ( media.frame.lastMime ) {
                                this.set( 'library', media.query({ type: media.frame.lastMime }) );
                                delete media.frame.lastMime;
        /**
         * wp.media.controller.Embed
         *
-        * @constructor
+        * A state for embedding media from a URL.
+        *
+        * @class
         * @augments wp.media.controller.State
         * @augments Backbone.Model
+        *
+        * @param {object} attributes                         The attributes hash passed to the state.
+        * @param {string} [attributes.id=embed]              Unique identifier.
+        * @param {string} [attributes.title=Insert From URL] Title for the state. Displays in the media menu and the frame's title region.
+        * @param {string} [attributes.content=embed]         Initial mode for the content region.
+        * @param {string} [attributes.menu=default]          Initial mode for the menu region.
+        * @param {string} [attributes.toolbar=main-embed]    Initial mode for the toolbar region.
+        * @param {string} [attributes.menu=false]            Initial mode for the menu region.
+        * @param {int}    [attributes.priority=120]          The priority for the state link in the media menu.
+        * @param {string} [attributes.type=link]             The type of embed. Currently only link is supported.
+        * @param {string} [attributes.url]                   The embed URL.
+        * @param {object} [attributes.metadata={}]           Properties of the embed, which will override attributes.url if set.
         */
        media.controller.Embed = media.controller.State.extend({
                defaults: {
-                       id:      'embed',
-                       url:     '',
-                       menu:    'default',
-                       content: 'embed',
-                       toolbar: 'main-embed',
-                       type:    'link',
-
+                       id:       'embed',
                        title:    l10n.insertFromUrlTitle,
-                       priority: 120
+                       content:  'embed',
+                       menu:     'default',
+                       toolbar:  'main-embed',
+                       priority: 120,
+                       type:     'link',
+                       url:      '',
+                       metadata: {}
                },
 
                // The amount of time used when debouncing the scan.
                sensitivity: 200,
 
-               initialize: function() {
+               initialize: function(options) {
+                       this.metadata = options.metadata;
                        this.debouncedScan = _.debounce( _.bind( this.scan, this ), this.sensitivity );
-                       this.props = new Backbone.Model({ url: '' });
+                       this.props = new Backbone.Model( this.metadata || { url: '' });
                        this.props.on( 'change:url', this.debouncedScan, this );
                        this.props.on( 'change:url', this.refresh, this );
                        this.on( 'scan', this.scanImage, this );
                },
 
                /**
+                * Trigger a scan of the embedded URL's content for metadata required to embed.
+                *
                 * @fires wp.media.controller.Embed#scan
                 */
                scan: function() {
                        this.set( attributes );
                },
                /**
+                * Try scanning the embed as an image to discover its dimensions.
+                *
                 * @param {Object} attributes
                 */
                scanImage: function( attributes ) {
        /**
         * wp.media.controller.Cropper
         *
-        * Allows for a cropping step.
+        * A state for cropping an image.
         *
-        * @constructor
+        * @class
         * @augments wp.media.controller.State
         * @augments Backbone.Model
         */
        media.controller.Cropper = media.controller.State.extend({
                defaults: {
-                       id: 'cropper',
-                       title: l10n.cropImage,
-                       toolbar: 'crop',
-                       content: 'crop',
-                       router: false,
+                       id:          'cropper',
+                       title:       l10n.cropImage,
+                       // Region mode defaults.
+                       toolbar:     'crop',
+                       content:     'crop',
+                       router:      false,
+
                        canSkipCrop: false
                },
 
                }
        });
 
-       /**
-        * ========================================================================
-        * VIEWS
-        * ========================================================================
-        */
-
        /**
         * wp.media.View
-        * -------------
         *
-        * The base view class.
+        * The base view class for media.
         *
         * Undelegating events, removing events from the model, and
         * removing events from the controller mirror the code for
         * This behavior has since been removed, and should not be used
         * outside of the media manager.
         *
-        * @constructor
+        * @class
         * @augments wp.Backbone.View
         * @augments Backbone.View
         */
                        wp.Backbone.View.apply( this, arguments );
                },
                /**
+                * @todo The internal comment mentions this might have been a stop-gap
+                *       before Backbone 0.9.8 came out. Figure out if Backbone core takes
+                *       care of this in Backbone.View now.
+                *
                 * @returns {wp.media.View} Returns itself to allow chaining
                 */
                dispose: function() {
         * wp.media.view.Frame
         *
         * A frame is a composite view consisting of one or more regions and one or more
-        * states. Only one state can be active at any given moment.
+        * states.
         *
-        * @constructor
+        * @see wp.media.controller.State
+        * @see wp.media.controller.Region
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
         */
        media.view.Frame = media.View.extend({
                initialize: function() {
+                       _.defaults( this.options, {
+                               mode: [ 'select' ]
+                       });
                        this._createRegions();
                        this._createStates();
+                       this._createModes();
                },
 
                _createRegions: function() {
                        }, this );
                },
                /**
+                * Create the frame's states.
+                *
+                * @see wp.media.controller.State
+                * @see wp.media.controller.StateMachine
+                *
                 * @fires wp.media.controller.State#ready
                 */
                _createStates: function() {
                                this.states.add( this.options.states );
                        }
                },
+
+               /**
+                * A frame can be in a mode or multiple modes at one time.
+                *
+                * For example, the manage media frame can be in the `Bulk Select` or `Edit` mode.
+                */
+               _createModes: function() {
+                       // Store active "modes" that the frame is in. Unrelated to region modes.
+                       this.activeModes = new Backbone.Collection();
+                       this.activeModes.on( 'add remove reset', _.bind( this.triggerModeEvents, this ) );
+
+                       _.each( this.options.mode, function( mode ) {
+                               this.activateMode( mode );
+                       }, this );
+               },
                /**
+                * Reset all states on the frame to their defaults.
+                *
                 * @returns {wp.media.view.Frame} Returns itself to allow chaining
                 */
                reset: function() {
                        this.states.invoke( 'trigger', 'reset' );
                        return this;
-               }
-       });
-
-       // Make the `Frame` a `StateMachine`.
-       _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
-
-       /**
-        * wp.media.view.MediaFrame
-        *
-        * Type of frame used to create the media modal.
-        *
-        * @constructor
-        * @augments wp.media.view.Frame
-        * @augments wp.media.View
-        * @augments wp.Backbone.View
-        * @augments Backbone.View
-        * @mixes wp.media.controller.StateMachine
-        */
-       media.view.MediaFrame = media.view.Frame.extend({
-               className: 'media-frame',
-               template:  media.template('media-frame'),
-               regions:   ['menu','title','content','toolbar','router'],
-
+               },
                /**
-                * @global wp.Uploader
+                * Map activeMode collection events to the frame.
                 */
-               initialize: function() {
+               triggerModeEvents: function( model, collection, options ) {
+                       var collectionEvent,
+                               modeEventMap = {
+                                       add: 'activate',
+                                       remove: 'deactivate'
+                               },
+                               eventToTrigger;
+                       // Probably a better way to do this.
+                       _.each( options, function( value, key ) {
+                               if ( value ) {
+                                       collectionEvent = key;
+                               }
+                       } );
 
+                       if ( ! _.has( modeEventMap, collectionEvent ) ) {
+                               return;
+                       }
+
+                       eventToTrigger = model.get('id') + ':' + modeEventMap[collectionEvent];
+                       this.trigger( eventToTrigger );
+               },
+               /**
+                * Activate a mode on the frame.
+                *
+                * @param string mode Mode ID.
+                * @returns {this} Returns itself to allow chaining.
+                */
+               activateMode: function( mode ) {
+                       // Bail if the mode is already active.
+                       if ( this.isModeActive( mode ) ) {
+                               return;
+                       }
+                       this.activeModes.add( [ { id: mode } ] );
+                       // Add a CSS class to the frame so elements can be styled for the mode.
+                       this.$el.addClass( 'mode-' + mode );
+
+                       return this;
+               },
+               /**
+                * Deactivate a mode on the frame.
+                *
+                * @param string mode Mode ID.
+                * @returns {this} Returns itself to allow chaining.
+                */
+               deactivateMode: function( mode ) {
+                       // Bail if the mode isn't active.
+                       if ( ! this.isModeActive( mode ) ) {
+                               return this;
+                       }
+                       this.activeModes.remove( this.activeModes.where( { id: mode } ) );
+                       this.$el.removeClass( 'mode-' + mode );
+                       /**
+                        * Frame mode deactivation event.
+                        *
+                        * @event this#{mode}:deactivate
+                        */
+                       this.trigger( mode + ':deactivate' );
+
+                       return this;
+               },
+               /**
+                * Check if a mode is enabled on the frame.
+                *
+                * @param  string mode Mode ID.
+                * @return bool
+                */
+               isModeActive: function( mode ) {
+                       return Boolean( this.activeModes.where( { id: mode } ).length );
+               }
+       });
+
+       // Make the `Frame` a `StateMachine`.
+       _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
+
+       /**
+        * wp.media.view.MediaFrame
+        *
+        * The frame used to create the media modal.
+        *
+        * @class
+        * @augments wp.media.view.Frame
+        * @augments wp.media.View
+        * @augments wp.Backbone.View
+        * @augments Backbone.View
+        * @mixes wp.media.controller.StateMachine
+        */
+       media.view.MediaFrame = media.view.Frame.extend({
+               className: 'media-frame',
+               template:  media.template('media-frame'),
+               regions:   ['menu','title','content','toolbar','router'],
+
+               events: {
+                       'click div.media-frame-title h1': 'toggleMenu'
+               },
+
+               /**
+                * @global wp.Uploader
+                */
+               initialize: function() {
                        media.view.Frame.prototype.initialize.apply( this, arguments );
 
                        _.defaults( this.options, {
                        this.on( 'title:create:default', this.createTitle, this );
                        this.title.mode('default');
 
+                       this.on( 'title:render', function( view ) {
+                               view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );
+                       });
+
                        // Bind default menu.
                        this.on( 'menu:create:default', this.createMenu, this );
                },
                                controller: this
                        });
                },
+
+               toggleMenu: function() {
+                       this.$el.find( '.media-menu' ).toggleClass( 'visible' );
+               },
+
                /**
                 * @param {Object} toolbar
                 * @this wp.media.controller.Region
                        }, this );
 
                        this.on( 'content:create:iframe', this.iframeContent, this );
+                       this.on( 'content:deactivate:iframe', this.iframeContentCleanup, this );
                        this.on( 'menu:render:default', this.iframeMenu, this );
                        this.on( 'open', this.hijackThickbox, this );
                        this.on( 'close', this.restoreThickbox, this );
                        });
                },
 
+               iframeContentCleanup: function() {
+                       this.$el.removeClass('hide-toolbar');
+               },
+
                iframeMenu: function( view ) {
                        var views = {};
 
        /**
         * wp.media.view.MediaFrame.Select
         *
-        * Type of media frame that is used to select an item or items from the media library
+        * A frame for selecting an item or items from the media library.
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.MediaFrame
         * @augments wp.media.view.Frame
         * @augments wp.media.View
         */
        media.view.MediaFrame.Select = media.view.MediaFrame.extend({
                initialize: function() {
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+                       // Call 'initialize' directly on the parent class.
                        media.view.MediaFrame.prototype.initialize.apply( this, arguments );
 
                        _.defaults( this.options, {
                        this.bindHandlers();
                },
 
+               /**
+                * Attach a selection collection to the frame.
+                *
+                * A selection is a collection of attachments used for a specific purpose
+                * by a media frame. e.g. Selecting an attachment (or many) to insert into
+                * post content.
+                *
+                * @see media.model.Selection
+                */
                createSelection: function() {
                        var selection = this.options.selection;
 
                        };
                },
 
+               /**
+                * Create the default states on the frame.
+                */
                createStates: function() {
                        var options = this.options;
 
                        ]);
                },
 
+               /**
+                * Bind region mode event callbacks.
+                *
+                * @see media.controller.Region.render
+                */
                bindHandlers: function() {
                        this.on( 'router:create:browse', this.createRouter, this );
                        this.on( 'router:render:browse', this.browseRouter, this );
                        this.on( 'toolbar:create:select', this.createSelectToolbar, this );
                },
 
-               // Routers
-               browseRouter: function( view ) {
-                       view.set({
+               /**
+                * Render callback for the router region in the `browse` mode.
+                *
+                * @param {wp.media.view.Router} routerView
+                */
+               browseRouter: function( routerView ) {
+                       routerView.set({
                                upload: {
                                        text:     l10n.uploadFilesTitle,
                                        priority: 20
                },
 
                /**
-                * Content
+                * Render callback for the content region in the `browse` mode.
                 *
-                * @param {Object} content
-                * @this wp.media.controller.Region
+                * @param {wp.media.controller.Region} contentRegion
                 */
-               browseContent: function( content ) {
+               browseContent: function( contentRegion ) {
                        var state = this.state();
 
                        this.$el.removeClass('hide-toolbar');
 
                        // Browse our library of attachments.
-                       content.view = new media.view.AttachmentsBrowser({
+                       contentRegion.view = new media.view.AttachmentsBrowser({
                                controller: this,
                                collection: state.get('library'),
                                selection:  state.get('selection'),
                                sortable:   state.get('sortable'),
                                search:     state.get('searchable'),
                                filters:    state.get('filterable'),
-                               display:    state.get('displaySettings'),
+                               date:       state.get('date'),
+                               display:    state.has('display') ? state.get('display') : state.get('displaySettings'),
                                dragInfo:   state.get('dragInfo'),
 
-                               suggestedWidth:  state.get('suggestedWidth'),
-                               suggestedHeight: state.get('suggestedHeight'),
+                               idealColumnWidth: state.get('idealColumnWidth'),
+                               suggestedWidth:   state.get('suggestedWidth'),
+                               suggestedHeight:  state.get('suggestedHeight'),
 
                                AttachmentView: state.get('AttachmentView')
                        });
                },
 
                /**
-                *
-                * @this wp.media.controller.Region
+                * Render callback for the content region in the `upload` mode.
                 */
                uploadContent: function() {
-                       this.$el.removeClass('hide-toolbar');
+                       this.$el.removeClass( 'hide-toolbar' );
                        this.content.set( new media.view.UploaderInline({
                                controller: this
                        }) );
        /**
         * wp.media.view.MediaFrame.Post
         *
-        * @constructor
+        * The frame for manipulating media on the Edit Post page.
+        *
+        * @class
         * @augments wp.media.view.MediaFrame.Select
         * @augments wp.media.view.MediaFrame
         * @augments wp.media.view.Frame
                        _.defaults( this.options, {
                                multiple:  true,
                                editing:   false,
-                               state:    'insert'
+                               state:    'insert',
+                               metadata:  {}
                        });
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+
+                       // Call 'initialize' directly on the parent class.
                        media.view.MediaFrame.Select.prototype.initialize.apply( this, arguments );
                        this.createIframeStates();
 
                },
 
+               /**
+                * Create the default states.
+                */
                createStates: function() {
                        var options = this.options;
 
-                       // Add the default states.
                        this.states.add([
                                // Main states.
                                new media.controller.Library({
                                }),
 
                                // Embed states.
-                               new media.controller.Embed(),
+                               new media.controller.Embed( { metadata: options.metadata } ),
 
                                new media.controller.EditImage( { model: options.editImage } ),
 
                                                } else {
                                                        frame.close();
                                                }
+
+                                               // Keep focus inside media modal
+                                               // after canceling a gallery
+                                               this.controller.modal.focusManager.focus();
                                        }
                                },
                                separateCancel: new media.View({
                        }).render();
 
                        this.content.set( view );
-                       view.url.focus();
+
+                       if ( ! isTouchDevice ) {
+                               view.url.focus();
+                       }
                },
 
                editSelectionContent: function() {
 
                        // Browse our library of attachments.
                        this.content.set( view );
+
+                       // Trigger the controller to set focus
+                       this.trigger( 'edit:selection', this );
                },
 
                editImageContent: function() {
                                        }) );
 
                                        this.controller.setState('gallery-edit');
+
+                                       // Keep focus inside media modal
+                                       // after jumping to gallery view
+                                       this.controller.modal.focusManager.focus();
                                }
                        });
                },
                                        }) );
 
                                        this.controller.setState('playlist-edit');
+
+                                       // Keep focus inside media modal
+                                       // after jumping to playlist view
+                                       this.controller.modal.focusManager.focus();
                                }
                        });
                },
                                        }) );
 
                                        this.controller.setState('video-playlist-edit');
+
+                                       // Keep focus inside media modal
+                                       // after jumping to video playlist view
+                                       this.controller.modal.focusManager.focus();
                                }
                        });
                },
        /**
         * wp.media.view.MediaFrame.ImageDetails
         *
-        * @constructor
+        * A media frame for manipulating an image that's already been inserted
+        * into a post.
+        *
+        * @class
         * @augments wp.media.view.MediaFrame.Select
         * @augments wp.media.view.MediaFrame
         * @augments wp.media.view.Frame
                        this.on( 'menu:create:image-details', this.createMenu, this );
                        this.on( 'content:create:image-details', this.imageDetailsContent, this );
                        this.on( 'content:render:edit-image', this.editImageContent, this );
-                       this.on( 'menu:render:image-details', this.renderMenu, this );
                        this.on( 'toolbar:render:image-details', this.renderImageDetailsToolbar, this );
                        // override the select toolbar
                        this.on( 'toolbar:render:replace', this.renderReplaceImageToolbar, this );
                        this.states.add([
                                new media.controller.ImageDetails({
                                        image: this.image,
-                                       editable: false,
-                                       menu: 'image-details'
+                                       editable: false
                                }),
                                new media.controller.ReplaceImage({
                                        id: 'replace-image',
                                        image: this.image,
                                        multiple:  false,
                                        title:     l10n.imageReplaceTitle,
-                                       menu: 'image-details',
                                        toolbar: 'replace',
                                        priority:  80,
                                        displaySettings: true
 
                },
 
-               renderMenu: function( view ) {
-                       var lastState = this.lastState(),
-                               previous = lastState && lastState.id,
-                               frame = this;
-
-                       view.set({
-                               cancel: {
-                                       text:     l10n.imageDetailsCancel,
-                                       priority: 20,
-                                       click:    function() {
-                                               if ( previous ) {
-                                                       frame.setState( previous );
-                                               } else {
-                                                       frame.close();
-                                               }
-                                       }
-                               },
-                               separateCancel: new media.View({
-                                       className: 'separator',
-                                       priority: 40
-                               })
-                       });
-
-               },
-
                renderImageDetailsToolbar: function() {
                        this.toolbar.set( new media.view.Toolbar({
                                controller: this,
        /**
         * wp.media.view.Modal
         *
-        * @constructor
+        * A modal view, which the media modal uses as its default container.
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                                propagate: true,
                                freeze:    true
                        });
+
+                       this.focusManager = new media.view.FocusManager({
+                               el: this.el
+                       });
                },
                /**
                 * @returns {Object}
                 */
                open: function() {
                        var $el = this.$el,
-                               options = this.options;
+                               options = this.options,
+                               mceEditor;
 
                        if ( $el.is(':visible') ) {
                                return this;
                                };
                        }
 
-                       $el.show().focus();
+                       // Disable page scrolling.
+                       $( 'body' ).addClass( 'modal-open' );
+
+                       $el.show();
+
+                       // Try to close the onscreen keyboard
+                       if ( 'ontouchend' in document ) {
+                               if ( ( mceEditor = window.tinymce && window.tinymce.activeEditor )  && ! mceEditor.isHidden() && mceEditor.iframeElement ) {
+                                       mceEditor.iframeElement.focus();
+                                       mceEditor.iframeElement.blur();
+
+                                       setTimeout( function() {
+                                               mceEditor.iframeElement.blur();
+                                       }, 100 );
+                               }
+                       }
+
+                       this.$el.focus();
+
                        return this.propagate('open');
                },
 
                                return this;
                        }
 
-                       this.$el.hide();
+                       // Enable page scrolling.
+                       $( 'body' ).removeClass( 'modal-open' );
+
+                       // Hide modal and remove restricted media modal tab focus once it's closed
+                       this.$el.hide().undelegate( 'keydown' );
+
+                       // Put focus back in useful location once modal is closed
+                       $('#wpbody-content').focus();
+
                        this.propagate('close');
 
                        // If the `freeze` option is set, restore the container's scroll position.
        /**
         * wp.media.view.FocusManager
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
         */
        media.view.FocusManager = media.View.extend({
+
                events: {
-                       keydown: 'recordTab',
-                       focusin: 'updateIndex'
+                       'keydown': 'constrainTabbing'
                },
 
-               focus: function() {
-                       if ( _.isUndefined( this.index ) ) {
-                               return;
-                       }
-
-                       // Update our collection of `$tabbables`.
-                       this.$tabbables = this.$(':tabbable');
-
-                       // If tab is saved, focus it.
-                       this.$tabbables.eq( this.index ).focus();
+               focus: function() { // Reset focus on first left menu item
+                       this.$('.media-menu-item').first().focus();
                },
                /**
                 * @param {Object} event
                 */
-               recordTab: function( event ) {
+               constrainTabbing: function( event ) {
+                       var tabbables;
+
                        // Look for the tab key.
                        if ( 9 !== event.keyCode ) {
                                return;
                        }
 
-                       // First try to update the index.
-                       if ( _.isUndefined( this.index ) ) {
-                               this.updateIndex( event );
-                       }
-
-                       // If we still don't have an index, bail.
-                       if ( _.isUndefined( this.index ) ) {
-                               return;
-                       }
-
-                       var index = this.index + ( event.shiftKey ? -1 : 1 );
-
-                       if ( index >= 0 && index < this.$tabbables.length ) {
-                               this.index = index;
-                       } else {
-                               delete this.index;
-                       }
-               },
-               /**
-                * @param {Object} event
-                */
-               updateIndex: function( event ) {
-                       this.$tabbables = this.$(':tabbable');
-
-                       var index = this.$tabbables.index( event.target );
+                       tabbables = this.$( ':tabbable' );
 
-                       if ( -1 === index ) {
-                               delete this.index;
-                       } else {
-                               this.index = index;
+                       // Keep tab focus within media modal while it's open
+                       if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
+                               tabbables.first().focus();
+                               return false;
+                       } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
+                               tabbables.last().focus();
+                               return false;
                        }
                }
+
        });
 
        /**
         * wp.media.view.UploaderWindow
         *
-        * @constructor
+        * An uploader window that allows for dragging and dropping media.
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
+        *
+        * @param {object} [options]                   Options hash passed to the view.
+        * @param {object} [options.uploader]          Uploader properties.
+        * @param {jQuery} [options.uploader.browser]
+        * @param {jQuery} [options.uploader.dropzone] jQuery collection of the dropzone.
+        * @param {object} [options.uploader.params]
         */
        media.view.UploaderWindow = media.View.extend({
                tagName:   'div',
                                        $el.hide();
                                }
                        });
+
+                       // https://core.trac.wordpress.org/ticket/27341
+                       _.delay( function() {
+                               if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
+                                       $el.hide();
+                               }
+                       }, 500 );
                }
        });
 
        /**
+        * Creates a dropzone on WP editor instances (elements with .wp-editor-wrap
+        * or #wp-fullscreen-body) and relays drag'n'dropped files to a media workflow.
+        *
         * wp.media.view.EditorUploader
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                overDropzone: false,
                draggingFile: null,
 
+               /**
+                * Bind drag'n'drop events to callbacks.
+                */
                initialize: function() {
                        var self = this;
 
                        return this;
                },
 
+               /**
+                * Check browser support for drag'n'drop.
+                *
+                * @return Boolean
+                */
                browserSupport: function() {
                        var supports = false, div = document.createElement('div');
 
                        return this;
                },
 
+               /**
+                * When a file is dropped on the editor uploader, open up an editor media workflow
+                * and upload the file immediately.
+                *
+                * @param  {jQuery.Event} event The 'drop' event.
+                */
                drop: function( event ) {
-                       var $wrap = null;
+                       var $wrap = null, uploadView;
 
                        this.containerDragleave( event );
                        this.dropzoneDragleave( event );
                                        title:    wp.media.view.l10n.addMedia,
                                        multiple: true
                                });
-                               this.workflow.on( 'uploader:ready', this.addFiles, this );
+                               uploadView = this.workflow.uploader;
+                               if ( uploadView.uploader && uploadView.uploader.ready ) {
+                                       this.addFiles.apply( this );
+                               } else {
+                                       this.workflow.on( 'uploader:ready', this.addFiles, this );
+                               }
                        } else {
                                this.workflow.state().reset();
                                this.addFiles.apply( this );
                        return false;
                },
 
+               /**
+                * Add the files to the uploader.
+                */
                addFiles: function() {
                        if ( this.files.length ) {
                                this.workflow.uploader.uploader.uploader.addFile( _.toArray( this.files ) );
        /**
         * wp.media.view.UploaderInline
         *
-        * @constructor
+        * The inline uploader that shows up in the 'Upload Files' tab.
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                className: 'uploader-inline',
                template:  media.template('uploader-inline'),
 
+               events: {
+                       'click .close': 'hide'
+               },
+
                initialize: function() {
                        _.defaults( this.options, {
                                message: '',
-                               status:  true
+                               status:  true,
+                               canClose: false
                        });
 
                        if ( ! this.options.$browser && this.controller.uploader ) {
 
                prepare: function() {
                        var suggestedWidth = this.controller.state().get('suggestedWidth'),
-                               suggestedHeight = this.controller.state().get('suggestedHeight');
+                               suggestedHeight = this.controller.state().get('suggestedHeight'),
+                               data = {};
+
+                       data.message = this.options.message;
+                       data.canClose = this.options.canClose;
 
                        if ( suggestedWidth && suggestedHeight ) {
-                               return {
-                                       suggestedWidth: suggestedWidth,
-                                       suggestedHeight: suggestedHeight
-                               };
+                               data.suggestedWidth = suggestedWidth;
+                               data.suggestedHeight = suggestedHeight;
                        }
+
+                       return data;
                },
                /**
                 * @returns {wp.media.view.UploaderInline} Returns itself to allow chaining
 
                        this.refresh();
                        return this;
+               },
+               show: function() {
+                       this.$el.removeClass( 'hidden' );
+               },
+               hide: function() {
+                       this.$el.addClass( 'hidden' );
                }
+
        });
 
        /**
         * wp.media.view.UploaderStatus
         *
-        * @constructor
+        * An uploader status for on-going uploads.
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        /**
         * wp.media.view.UploaderStatusError
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        /**
         * wp.media.view.Toolbar
         *
-        * @constructor
+        * A toolbar which consists of a primary and a secondary section. Each sections
+        * can be filled with views.
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                        // The toolbar is composed of two `PriorityList` views.
                        this.primary   = new media.view.PriorityList();
                        this.secondary = new media.view.PriorityList();
-                       this.primary.$el.addClass('media-toolbar-primary');
+                       this.primary.$el.addClass('media-toolbar-primary search-form');
                        this.secondary.$el.addClass('media-toolbar-secondary');
 
                        this.views.set([ this.secondary, this.primary ]);
        /**
         * wp.media.view.Toolbar.Select
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Toolbar
         * @augments wp.media.View
         * @augments wp.Backbone.View
                                        requires: options.requires
                                }
                        });
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+                       // Call 'initialize' directly on the parent class.
                        media.view.Toolbar.prototype.initialize.apply( this, arguments );
                },
 
        /**
         * wp.media.view.Toolbar.Embed
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Toolbar.Select
         * @augments wp.media.view.Toolbar
         * @augments wp.media.View
                                text: l10n.insertIntoPost,
                                requires: false
                        });
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+                       // Call 'initialize' directly on the parent class.
                        media.view.Toolbar.Select.prototype.initialize.apply( this, arguments );
                },
 
        /**
         * wp.media.view.Button
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        /**
         * wp.media.view.ButtonGroup
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        /**
         * wp.media.view.PriorityList
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        /**
         * wp.media.view.MenuItem
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                        } else {
                                this.click();
                        }
+
+                       // When selecting a tab along the left side,
+                       // focus should be transferred into the main panel
+                       if ( ! isTouchDevice ) {
+                               $('.media-frame-content input').first().focus();
+                       }
                },
 
                click: function() {
                        var state = this.options.state;
+
                        if ( state ) {
                                this.controller.setState( state );
+                               this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
                        }
                },
                /**
        /**
         * wp.media.view.Menu
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.PriorityList
         * @augments wp.media.View
         * @augments wp.Backbone.View
                property:  'state',
                ItemView:  media.view.MenuItem,
                region:    'menu',
+
+               /* TODO: alternatively hide on any click anywhere
+               events: {
+                       'click': 'click'
+               },
+
+               click: function() {
+                       this.$el.removeClass( 'visible' );
+               },
+               */
+
                /**
                 * @param {Object} options
                 * @param {string} id
        /**
         * wp.media.view.RouterItem
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.MenuItem
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
         */
        media.view.RouterItem = media.view.MenuItem.extend({
+               /**
+                * On click handler to activate the content region's corresponding mode.
+                */
                click: function() {
                        var contentMode = this.options.contentMode;
                        if ( contentMode ) {
        /**
         * wp.media.view.Router
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Menu
         * @augments wp.media.view.PriorityList
         * @augments wp.media.View
 
                initialize: function() {
                        this.controller.on( 'content:render', this.update, this );
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+                       // Call 'initialize' directly on the parent class.
                        media.view.Menu.prototype.initialize.apply( this, arguments );
                },
 
        /**
         * wp.media.view.Sidebar
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.PriorityList
         * @augments wp.media.View
         * @augments wp.Backbone.View
        /**
         * wp.media.view.Attachment
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                className: 'attachment',
                template:  media.template('attachment'),
 
+               attributes: function() {
+                       return {
+                               'tabIndex':     0,
+                               'role':         'checkbox',
+                               'aria-label':   this.model.get( 'title' ),
+                               'aria-checked': false,
+                               'data-id':      this.model.get( 'id' )
+                       };
+               },
+
                events: {
-                       'click .attachment-preview':      'toggleSelectionHandler',
+                       'click .js--select-attachment':   'toggleSelectionHandler',
                        'change [data-setting]':          'updateSetting',
                        'change [data-setting] input':    'updateSetting',
                        'change [data-setting] select':   'updateSetting',
                        'change [data-setting] textarea': 'updateSetting',
                        'click .close':                   'removeFromLibrary',
-                       'click .check':                   'removeFromSelection',
-                       'click a':                        'preventDefault'
+                       'click .check':                   'checkClickHandler',
+                       'click a':                        'preventDefault',
+                       'keydown .close':                 'removeFromLibrary',
+                       'keydown':                        'toggleSelectionHandler'
                },
 
                buttons: {},
 
                initialize: function() {
-                       var selection = this.options.selection;
+                       var selection = this.options.selection,
+                               options = _.defaults( this.options, {
+                                       rerenderOnModelChange: true
+                               } );
 
-                       this.model.on( 'change:sizes change:uploading', this.render, this );
+                       if ( options.rerenderOnModelChange ) {
+                               this.model.on( 'change', this.render, this );
+                       } else {
+                               this.model.on( 'change:percent', this.progress, this );
+                       }
                        this.model.on( 'change:title', this._syncTitle, this );
                        this.model.on( 'change:caption', this._syncCaption, this );
-                       this.model.on( 'change:percent', this.progress, this );
+                       this.model.on( 'change:artist', this._syncArtist, this );
+                       this.model.on( 'change:album', this._syncAlbum, this );
 
                        // Update the selection.
                        this.model.on( 'add', this.select, this );
                        this.model.on( 'remove', this.deselect, this );
                        if ( selection ) {
                                selection.on( 'reset', this.updateSelect, this );
+                               // Update the model's details view.
+                               this.model.on( 'selection:single selection:unsingle', this.details, this );
+                               this.details( this.model, this.controller.state().get('selection') );
                        }
 
-                       // Update the model's details view.
-                       this.model.on( 'selection:single selection:unsingle', this.details, this );
-                       this.details( this.model, this.controller.state().get('selection') );
+                       this.listenTo( this.controller, 'attachment:compat:waiting attachment:compat:ready', this.updateSave );
                },
                /**
                 * @returns {wp.media.view.Attachment} Returns itself to allow chaining
                                        compat:        false,
                                        alt:           '',
                                        description:   ''
-                               });
+                               }, this.options );
 
                        options.buttons  = this.buttons;
                        options.describe = this.controller.state().get('describe');
                                options.allowLocalEdits = true;
                        }
 
+                       if ( options.uploading && ! options.percent ) {
+                               options.percent = 0;
+                       }
+
                        this.views.detach();
                        this.$el.html( this.template( options ) );
 
                        this.$el.toggleClass( 'uploading', options.uploading );
+
                        if ( options.uploading ) {
                                this.$bar = this.$('.media-progress-bar div');
                        } else {
                                this.$bar.width( this.model.get('percent') + '%' );
                        }
                },
+
                /**
                 * @param {Object} event
                 */
                toggleSelectionHandler: function( event ) {
                        var method;
 
+                       // Don't do anything inside inputs.
+                       if ( 'INPUT' === event.target.nodeName ) {
+                               return;
+                       }
+
+                       // Catch arrow events
+                       if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
+                               this.controller.trigger( 'attachment:keydown:arrow', event );
+                               return;
+                       }
+
+                       // Catch enter and space events
+                       if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
+                               return;
+                       }
+
+                       event.preventDefault();
+
+                       // In the grid view, bubble up an edit:attachment event to the controller.
+                       if ( this.controller.isModeActive( 'grid' ) ) {
+                               if ( this.controller.isModeActive( 'edit' ) ) {
+                                       // Pass the current target to restore focus when closing
+                                       this.controller.trigger( 'edit:attachment', this.model, event.currentTarget );
+                                       return;
+                               }
+
+                               if ( this.controller.isModeActive( 'select' ) ) {
+                                       method = 'toggle';
+                               }
+                       }
+
                        if ( event.shiftKey ) {
                                method = 'between';
                        } else if ( event.ctrlKey || event.metaKey ) {
                        this.toggleSelection({
                                method: method
                        });
+
+                       this.controller.trigger( 'selection:toggle' );
                },
                /**
                 * @param {Object} options
                                selection[ this.selected() ? 'remove' : 'add' ]( model );
                                selection.single( model );
                                return;
+                       } else if ( 'add' === method ) {
+                               selection.add( model );
+                               selection.single( model );
+                               return;
+                       }
+
+                       // Fixes bug that loses focus when selecting a featured image
+                       if ( ! method ) {
+                               method = 'add';
                        }
 
                        if ( method !== 'add' ) {
                 * @param {Backbone.Collection} collection
                 */
                select: function( model, collection ) {
-                       var selection = this.options.selection;
+                       var selection = this.options.selection,
+                               controller = this.controller;
 
                        // Check if a selection exists and if it's the collection provided.
                        // If they're not the same collection, bail; we're in another
                                return;
                        }
 
-                       this.$el.addClass('selected');
+                       // Bail if the model is already selected.
+                       if ( this.$el.hasClass( 'selected' ) ) {
+                               return;
+                       }
+
+                       // Add 'selected' class to model, set aria-checked to true.
+                       this.$el.addClass( 'selected' ).attr( 'aria-checked', true );
+                       //  Make the checkbox tabable, except in media grid (bulk select mode).
+                       if ( ! ( controller.isModeActive( 'grid' ) && controller.isModeActive( 'select' ) ) ) {
+                               this.$( '.check' ).attr( 'tabindex', '0' );
+                       }
                },
                /**
                 * @param {Backbone.Model} model
                        if ( ! selection || ( collection && collection !== selection ) ) {
                                return;
                        }
-                       this.$el.removeClass('selected');
+                       this.$el.removeClass( 'selected' ).attr( 'aria-checked', false )
+                               .find( '.check' ).attr( 'tabindex', '-1' );
                },
                /**
                 * @param {Backbone.Model} model
                 * @param {Object} event
                 */
                removeFromLibrary: function( event ) {
-                       // Stop propagation so the model isn't selected.
+                       // Catch enter and space events
+                       if ( 'keydown' === event.type && 13 !== event.keyCode && 32 !== event.keyCode ) {
+                               return;
+                       }
+
+                       // Stop propagation so the model isn't selected.
                        event.stopPropagation();
 
                        this.collection.remove( this.model );
                },
+
                /**
-                * @param {Object} event
+                * Add the model if it isn't in the selection, if it is in the selection,
+                * remove it.
+                *
+                * @param  {[type]} event [description]
+                * @return {[type]}       [description]
                 */
-               removeFromSelection: function( event ) {
+               checkClickHandler: function ( event ) {
                        var selection = this.options.selection;
                        if ( ! selection ) {
                                return;
                        }
-
-                       // Stop propagation so the model isn't selected.
                        event.stopPropagation();
-
-                       selection.remove( this.model );
+                       if ( selection.where( { id: this.model.get( 'id' ) } ).length ) {
+                               selection.remove( this.model );
+                               // Move focus back to the attachment tile (from the check).
+                               this.$el.focus();
+                       } else {
+                               selection.add( this.model );
+                       }
                }
        });
 
        // Ensure settings remain in sync between attachment views.
        _.each({
                caption: '_syncCaption',
-               title:   '_syncTitle'
+               title:   '_syncTitle',
+               artist:  '_syncArtist',
+               album:   '_syncAlbum'
        }, function( method, setting ) {
                /**
                 * @param {Backbone.Model} model
        /**
         * wp.media.view.Attachment.Library
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Attachment
         * @augments wp.media.View
         * @augments wp.Backbone.View
        /**
         * wp.media.view.Attachment.EditLibrary
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Attachment
         * @augments wp.media.View
         * @augments wp.Backbone.View
        /**
         * wp.media.view.Attachments
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                tagName:   'ul',
                className: 'attachments',
 
-               cssTemplate: media.template('attachments-css'),
-
-               events: {
-                       'scroll': 'scroll'
+               attributes: {
+                       tabIndex: -1
                },
 
                initialize: function() {
                        this.el.id = _.uniqueId('__attachments-view-');
 
                        _.defaults( this.options, {
-                               refreshSensitivity: 200,
+                               refreshSensitivity: isTouchDevice ? 300 : 200,
                                refreshThreshold:   3,
                                AttachmentView:     media.view.Attachment,
                                sortable:           false,
-                               resize:             true
+                               resize:             true,
+                               idealColumnWidth:   $( window ).width() < 640 ? 135 : 150
                        });
 
                        this._viewsByCid = {};
+                       this.$window = $( window );
+                       this.resizeEvent = 'resize.media-modal-columns';
 
                        this.collection.on( 'add', function( attachment ) {
                                this.views.add( this.createAttachmentView( attachment ), {
 
                        this.collection.on( 'reset', this.render, this );
 
-                       // Throttle the scroll handler.
+                       this.listenTo( this.controller, 'library:selection:add',    this.attachmentFocus );
+
+                       // Throttle the scroll handler and bind this.
                        this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
 
+                       this.options.scrollElement = this.options.scrollElement || this.el;
+                       $( this.options.scrollElement ).on( 'scroll', this.scroll );
+
                        this.initSortable();
 
-                       _.bindAll( this, 'css' );
-                       this.model.on( 'change:edge change:gutter', this.css, this );
-                       this._resizeCss = _.debounce( _.bind( this.css, this ), this.refreshSensitivity );
+                       _.bindAll( this, 'setColumns' );
+
                        if ( this.options.resize ) {
-                               $(window).on( 'resize.attachments', this._resizeCss );
+                               this.on( 'ready', this.bindEvents );
+                               this.controller.on( 'open', this.setColumns );
+
+                               // Call this.setColumns() after this view has been rendered in the DOM so
+                               // attachments get proper width applied.
+                               _.defer( this.setColumns, this );
+                       }
+               },
+
+               bindEvents: function() {
+                       this.$window.off( this.resizeEvent ).on( this.resizeEvent, _.debounce( this.setColumns, 50 ) );
+               },
+
+               attachmentFocus: function() {
+                       this.$( 'li:first' ).focus();
+               },
+
+               restoreFocus: function() {
+                       this.$( 'li.selected:first' ).focus();
+               },
+
+               arrowEvent: function( event ) {
+                       var attachments = this.$el.children( 'li' ),
+                               perRow = this.columns,
+                               index = attachments.filter( ':focus' ).index(),
+                               row = ( index + 1 ) <= perRow ? 1 : Math.ceil( ( index + 1 ) / perRow );
+
+                       if ( index === -1 ) {
+                               return;
+                       }
+
+                       // Left arrow
+                       if ( 37 === event.keyCode ) {
+                               if ( 0 === index ) {
+                                       return;
+                               }
+                               attachments.eq( index - 1 ).focus();
+                       }
+
+                       // Up arrow
+                       if ( 38 === event.keyCode ) {
+                               if ( 1 === row ) {
+                                       return;
+                               }
+                               attachments.eq( index - perRow ).focus();
+                       }
+
+                       // Right arrow
+                       if ( 39 === event.keyCode ) {
+                               if ( attachments.length === index ) {
+                                       return;
+                               }
+                               attachments.eq( index + 1 ).focus();
+                       }
+
+                       // Down arrow
+                       if ( 40 === event.keyCode ) {
+                               if ( Math.ceil( attachments.length / perRow ) === row ) {
+                                       return;
+                               }
+                               attachments.eq( index + perRow ).focus();
                        }
-                       this.css();
                },
 
                dispose: function() {
                        this.collection.props.off( null, null, this );
-                       $(window).off( 'resize.attachments', this._resizeCss );
+                       if ( this.options.resize ) {
+                               this.$window.off( this.resizeEvent );
+                       }
+
                        /**
                         * call 'dispose' directly on the parent class
                         */
                        media.View.prototype.dispose.apply( this, arguments );
                },
 
-               css: function() {
-                       var $css = $( '#' + this.el.id + '-css' );
+               setColumns: function() {
+                       var prev = this.columns,
+                               width = this.$el.width();
 
-                       if ( $css.length ) {
-                               $css.remove();
-                       }
+                       if ( width ) {
+                               this.columns = Math.min( Math.round( width / this.options.idealColumnWidth ), 12 ) || 1;
 
-                       media.view.Attachments.$head().append( this.cssTemplate({
-                               id:     this.el.id,
-                               edge:   this.edge(),
-                               gutter: this.model.get('gutter')
-                       }) );
-               },
-               /**
-                * @returns {Number}
-                */
-               edge: function() {
-                       var edge = this.model.get('edge'),
-                               gutter, width, columns;
-
-                       if ( ! this.$el.is(':visible') ) {
-                               return edge;
+                               if ( ! prev || prev !== this.columns ) {
+                                       this.$el.closest( '.media-frame-content' ).attr( 'data-columns', this.columns );
+                               }
                        }
-
-                       gutter  = this.model.get('gutter') * 2;
-                       width   = this.$el.width() - gutter;
-                       columns = Math.ceil( width / ( edge + gutter ) );
-                       edge = Math.floor( ( width - ( columns * gutter ) ) / columns );
-                       return edge;
                },
 
                initSortable: function() {
                        var collection = this.collection;
 
-                       if ( ! this.options.sortable || ! $.fn.sortable ) {
+                       if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
                                return;
                        }
 
                                // If the `collection` has a `comparator`, disable sorting.
                                disabled: !! collection.comparator,
 
-                               // Prevent attachments from being dragged outside the bounding
-                               // box of the list.
-                               containment: this.$el,
-
                                // Change the position of the attachment as soon as the
                                // mouse pointer overlaps a thumbnail.
                                tolerance: 'pointer',
                },
 
                refreshSortable: function() {
-                       if ( ! this.options.sortable || ! $.fn.sortable ) {
+                       if ( isTouchDevice || ! this.options.sortable || ! $.fn.sortable ) {
                                return;
                        }
 
                 */
                createAttachmentView: function( attachment ) {
                        var view = new this.options.AttachmentView({
-                               controller: this.controller,
-                               model:      attachment,
-                               collection: this.collection,
-                               selection:  this.options.selection
+                               controller:           this.controller,
+                               model:                attachment,
+                               collection:           this.collection,
+                               selection:            this.options.selection
                        });
 
                        return this._viewsByCid[ attachment.cid ] = view;
 
                scroll: function() {
                        var view = this,
+                               el = this.options.scrollElement,
+                               scrollTop = el.scrollTop,
                                toolbar;
 
-                       if ( ! this.$el.is(':visible') || ! this.collection.hasMore() ) {
+                       // The scroll event occurs on the document, but the element
+                       // that should be checked is the document body.
+                       if ( el == document ) {
+                               el = document.body;
+                               scrollTop = $(document).scrollTop();
+                       }
+
+                       if ( ! $(el).is(':visible') || ! this.collection.hasMore() ) {
                                return;
                        }
 
                        toolbar = this.views.parent.toolbar;
 
                        // Show the spinner only if we are close to the bottom.
-                       if ( this.el.scrollHeight - ( this.el.scrollTop + this.el.clientHeight ) < this.el.clientHeight / 3 ) {
+                       if ( el.scrollHeight - ( scrollTop + el.clientHeight ) < el.clientHeight / 3 ) {
                                toolbar.get('spinner').show();
                        }
 
-                       if ( this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
+                       if ( el.scrollHeight < scrollTop + ( el.clientHeight * this.options.refreshThreshold ) ) {
                                this.collection.more().done(function() {
                                        view.scroll();
                                        toolbar.get('spinner').hide();
                                });
                        }
                }
-       }, {
-               $head: (function() {
-                       var $head;
-                       return function() {
-                               return $head = $head || $('head');
-                       };
-               }())
        });
 
        /**
         * wp.media.view.Search
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        media.view.Search = media.View.extend({
                tagName:   'input',
                className: 'search',
+               id:        'media-search-input',
 
                attributes: {
                        type:        'search',
        /**
         * wp.media.view.AttachmentFilters
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        media.view.AttachmentFilters = media.View.extend({
                tagName:   'select',
                className: 'attachment-filters',
+               id:        'media-attachment-filters',
 
                events: {
                        change: 'change'
                        this.select();
                },
 
+               /**
+                * @abstract
+                */
                createFilters: function() {
                        this.filters = {};
                },
 
+               /**
+                * When the selected filter changes, update the Attachment Query properties to match.
+                */
                change: function() {
                        var filter = this.filters[ this.el.value ];
-
                        if ( filter ) {
                                this.model.set( filter.props );
                        }
                }
        });
 
+       /**
+        * A filter dropdown for month/dates.
+        *
+        * @class
+        * @augments wp.media.view.AttachmentFilters
+        * @augments wp.media.View
+        * @augments wp.Backbone.View
+        * @augments Backbone.View
+        */
+       media.view.DateFilter = media.view.AttachmentFilters.extend({
+               id: 'media-attachment-date-filters',
+
+               createFilters: function() {
+                       var filters = {};
+                       _.each( media.view.settings.months || {}, function( value, index ) {
+                               filters[ index ] = {
+                                       text: value.text,
+                                       props: {
+                                               year: value.year,
+                                               monthnum: value.month
+                                       }
+                               };
+                       });
+                       filters.all = {
+                               text:  l10n.allDates,
+                               props: {
+                                       monthnum: false,
+                                       year:  false
+                               },
+                               priority: 10
+                       };
+                       this.filters = filters;
+               }
+       });
+
        /**
         * wp.media.view.AttachmentFilters.Uploaded
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.AttachmentFilters
         * @augments wp.media.View
         * @augments wp.Backbone.View
                                                order:   'ASC'
                                        },
                                        priority: 20
+                               },
+
+                               unattached: {
+                                       text:  l10n.unattached,
+                                       props: {
+                                               uploadedTo: 0,
+                                               orderby: 'menuOrder',
+                                               order:   'ASC'
+                                       },
+                                       priority: 50
                                }
                        };
                }
        /**
         * wp.media.view.AttachmentFilters.All
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.AttachmentFilters
         * @augments wp.media.View
         * @augments wp.Backbone.View
                                filters[ key ] = {
                                        text: text,
                                        props: {
+                                               status:  null,
                                                type:    key,
                                                uploadedTo: null,
                                                orderby: 'date',
                        filters.all = {
                                text:  l10n.allMediaItems,
                                props: {
+                                       status:  null,
                                        type:    null,
                                        uploadedTo: null,
                                        orderby: 'date',
                                priority: 10
                        };
 
-                       filters.uploaded = {
-                               text:  l10n.uploadedToThisPost,
+                       if ( media.view.settings.post.id ) {
+                               filters.uploaded = {
+                                       text:  l10n.uploadedToThisPost,
+                                       props: {
+                                               status:  null,
+                                               type:    null,
+                                               uploadedTo: media.view.settings.post.id,
+                                               orderby: 'menuOrder',
+                                               order:   'ASC'
+                                       },
+                                       priority: 20
+                               };
+                       }
+
+                       filters.unattached = {
+                               text:  l10n.unattached,
                                props: {
-                                       type:    null,
-                                       uploadedTo: media.view.settings.post.id,
-                                       orderby: 'menuOrder',
-                                       order:   'ASC'
+                                       status:     null,
+                                       uploadedTo: 0,
+                                       type:       null,
+                                       orderby:    'menuOrder',
+                                       order:      'ASC'
                                },
-                               priority: 20
+                               priority: 50
                        };
 
+                       if ( media.view.settings.mediaTrash &&
+                               this.controller.isModeActive( 'grid' ) ) {
+
+                               filters.trash = {
+                                       text:  l10n.trash,
+                                       props: {
+                                               uploadedTo: null,
+                                               status:     'trash',
+                                               type:       null,
+                                               orderby:    'date',
+                                               order:      'DESC'
+                                       },
+                                       priority: 50
+                               };
+                       }
+
                        this.filters = filters;
                }
        });
 
-
        /**
         * wp.media.view.AttachmentsBrowser
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
+        *
+        * @param {object}      options
+        * @param {object}      [options.filters=false] Which filters to show in the browser's toolbar.
+        *                                              Accepts 'uploaded' and 'all'.
+        * @param {object}      [options.search=true]   Whether to show the search interface in the
+        *                                              browser's toolbar.
+        * @param {object}      [options.date=true]     Whether to show the date filter in the
+        *                                              browser's toolbar.
+        * @param {object}      [options.display=false] Whether to show the attachments display settings
+        *                                              view in the sidebar.
+        * @param {bool|string} [options.sidebar=true]  Whether to create a sidebar for the browser.
+        *                                              Accepts true, false, and 'errors'.
         */
        media.view.AttachmentsBrowser = media.View.extend({
                tagName:   'div',
                        _.defaults( this.options, {
                                filters: false,
                                search:  true,
+                               date:    true,
                                display: false,
-
+                               sidebar: true,
                                AttachmentView: media.view.Attachment.Library
                        });
 
+                       this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
+                       this.controller.on( 'edit:selection', this.editSelection );
                        this.createToolbar();
+                       if ( this.options.sidebar ) {
+                               this.createSidebar();
+                       }
+                       this.createUploader();
+                       this.createAttachments();
                        this.updateContent();
-                       this.createSidebar();
+
+                       if ( ! this.options.sidebar || 'errors' === this.options.sidebar ) {
+                               this.$el.addClass( 'hide-sidebar' );
+
+                               if ( 'errors' === this.options.sidebar ) {
+                                       this.$el.addClass( 'sidebar-for-errors' );
+                               }
+                       }
 
                        this.collection.on( 'add remove reset', this.updateContent, this );
                },
+
+               editSelection: function( modal ) {
+                       modal.$( '.media-button-backToLibrary' ).focus();
+               },
+
                /**
                 * @returns {wp.media.view.AttachmentsBrowser} Returns itself to allow chaining
                 */
                },
 
                createToolbar: function() {
-                       var filters, FiltersConstructor;
+                       var LibraryViewSwitcher, Filters, toolbarOptions;
 
-                       /**
-                        * @member {wp.media.view.Toolbar}
-                        */
-                       this.toolbar = new media.view.Toolbar({
+                       toolbarOptions = {
                                controller: this.controller
-                       });
+                       };
+
+                       if ( this.controller.isModeActive( 'grid' ) ) {
+                               toolbarOptions.className = 'media-toolbar wp-filter';
+                       }
+
+                       /**
+                       * @member {wp.media.view.Toolbar}
+                       */
+                       this.toolbar = new media.view.Toolbar( toolbarOptions );
 
                        this.views.add( this.toolbar );
 
-                       filters = this.options.filters;
-                       if ( 'uploaded' === filters ) {
-                               FiltersConstructor = media.view.AttachmentFilters.Uploaded;
-                       } else if ( 'all' === filters ) {
-                               FiltersConstructor = media.view.AttachmentFilters.All;
+                       this.toolbar.set( 'spinner', new media.view.Spinner({
+                               priority: -60
+                       }) );
+
+                       if ( -1 !== $.inArray( this.options.filters, [ 'uploaded', 'all' ] ) ) {
+                               // "Filters" will return a <select>, need to render
+                               // screen reader text before
+                               this.toolbar.set( 'filtersLabel', new media.view.Label({
+                                       value: l10n.filterByType,
+                                       attributes: {
+                                               'for':  'media-attachment-filters'
+                                       },
+                                       priority:   -80
+                               }).render() );
+
+                               if ( 'uploaded' === this.options.filters ) {
+                                       this.toolbar.set( 'filters', new media.view.AttachmentFilters.Uploaded({
+                                               controller: this.controller,
+                                               model:      this.collection.props,
+                                               priority:   -80
+                                       }).render() );
+                               } else {
+                                       Filters = new media.view.AttachmentFilters.All({
+                                               controller: this.controller,
+                                               model:      this.collection.props,
+                                               priority:   -80
+                                       });
+
+                                       this.toolbar.set( 'filters', Filters.render() );
+                               }
                        }
 
-                       if ( FiltersConstructor ) {
-                               this.toolbar.set( 'filters', new FiltersConstructor({
+                       // Feels odd to bring the global media library switcher into the Attachment
+                       // browser view. Is this a use case for doAction( 'add:toolbar-items:attachments-browser', this.toolbar );
+                       // which the controller can tap into and add this view?
+                       if ( this.controller.isModeActive( 'grid' ) ) {
+                               LibraryViewSwitcher = media.View.extend({
+                                       className: 'view-switch media-grid-view-switch',
+                                       template: media.template( 'media-library-view-switcher')
+                               });
+
+                               this.toolbar.set( 'libraryViewSwitcher', new LibraryViewSwitcher({
+                                       controller: this.controller,
+                                       priority: -90
+                               }).render() );
+
+                               // DateFilter is a <select>, screen reader text needs to be rendered before
+                               this.toolbar.set( 'dateFilterLabel', new media.view.Label({
+                                       value: l10n.filterByDate,
+                                       attributes: {
+                                               'for': 'media-attachment-date-filters'
+                                       },
+                                       priority: -75
+                               }).render() );
+                               this.toolbar.set( 'dateFilter', new media.view.DateFilter({
                                        controller: this.controller,
                                        model:      this.collection.props,
-                                       priority:   -80
+                                       priority: -75
                                }).render() );
-                       }
 
-                       this.toolbar.set( 'spinner', new media.view.Spinner({
-                               priority: -70
-                       }) );
+                               // BulkSelection is a <div> with subviews, including screen reader text
+                               this.toolbar.set( 'selectModeToggleButton', new media.view.SelectModeToggleButton({
+                                       text: l10n.bulkSelect,
+                                       controller: this.controller,
+                                       priority: -70
+                               }).render() );
+
+                               this.toolbar.set( 'deleteSelectedButton', new media.view.DeleteSelectedButton({
+                                       filters: Filters,
+                                       style: 'primary',
+                                       disabled: true,
+                                       text: media.view.settings.mediaTrash ? l10n.trashSelected : l10n.deleteSelected,
+                                       controller: this.controller,
+                                       priority: -60,
+                                       click: function() {
+                                               var changed = [], removed = [], self = this,
+                                                       selection = this.controller.state().get( 'selection' ),
+                                                       library = this.controller.state().get( 'library' );
+
+                                               if ( ! selection.length ) {
+                                                       return;
+                                               }
+
+                                               if ( ! media.view.settings.mediaTrash && ! confirm( l10n.warnBulkDelete ) ) {
+                                                       return;
+                                               }
+
+                                               if ( media.view.settings.mediaTrash &&
+                                                       'trash' !== selection.at( 0 ).get( 'status' ) &&
+                                                       ! confirm( l10n.warnBulkTrash ) ) {
+
+                                                       return;
+                                               }
+
+                                               selection.each( function( model ) {
+                                                       if ( ! model.get( 'nonces' )['delete'] ) {
+                                                               removed.push( model );
+                                                               return;
+                                                       }
+
+                                                       if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
+                                                               model.set( 'status', 'inherit' );
+                                                               changed.push( model.save() );
+                                                               removed.push( model );
+                                                       } else if ( media.view.settings.mediaTrash ) {
+                                                               model.set( 'status', 'trash' );
+                                                               changed.push( model.save() );
+                                                               removed.push( model );
+                                                       } else {
+                                                               model.destroy({wait: true});
+                                                       }
+                                               } );
+
+                                               if ( changed.length ) {
+                                                       selection.remove( removed );
+
+                                                       $.when.apply( null, changed ).then( function() {
+                                                               library._requery( true );
+                                                               self.controller.trigger( 'selection:action:done' );
+                                                       } );
+                                               } else {
+                                                       this.controller.trigger( 'selection:action:done' );
+                                               }
+                                       }
+                               }).render() );
+
+                               if ( media.view.settings.mediaTrash ) {
+                                       this.toolbar.set( 'deleteSelectedPermanentlyButton', new media.view.DeleteSelectedPermanentlyButton({
+                                               filters: Filters,
+                                               style: 'primary',
+                                               disabled: true,
+                                               text: l10n.deleteSelected,
+                                               controller: this.controller,
+                                               priority: -55,
+                                               click: function() {
+                                                       var removed = [], selection = this.controller.state().get( 'selection' );
+
+                                                       if ( ! selection.length || ! confirm( l10n.warnBulkDelete ) ) {
+                                                               return;
+                                                       }
+
+                                                       selection.each( function( model ) {
+                                                               if ( ! model.get( 'nonces' )['delete'] ) {
+                                                                       removed.push( model );
+                                                                       return;
+                                                               }
+
+                                                               model.destroy();
+                                                       } );
+
+                                                       selection.remove( removed );
+                                                       this.controller.trigger( 'selection:action:done' );
+                                               }
+                                       }).render() );
+                               }
+
+                       } else if ( this.options.date ) {
+                               // DateFilter is a <select>, screen reader text needs to be rendered before
+                               this.toolbar.set( 'dateFilterLabel', new media.view.Label({
+                                       value: l10n.filterByDate,
+                                       attributes: {
+                                               'for': 'media-attachment-date-filters'
+                                       },
+                                       priority: -75
+                               }).render() );
+                               this.toolbar.set( 'dateFilter', new media.view.DateFilter({
+                                       controller: this.controller,
+                                       model:      this.collection.props,
+                                       priority: -75
+                               }).render() );
+                       }
 
                        if ( this.options.search ) {
+                               // Search is an input, screen reader text needs to be rendered before
+                               this.toolbar.set( 'searchLabel', new media.view.Label({
+                                       value: l10n.searchMediaLabel,
+                                       attributes: {
+                                               'for': 'media-search-input'
+                                       },
+                                       priority:   60
+                               }).render() );
                                this.toolbar.set( 'search', new media.view.Search({
                                        controller: this.controller,
                                        model:      this.collection.props,
                },
 
                updateContent: function() {
-                       var view = this;
+                       var view = this,
+                               noItemsView;
 
-                       if( ! this.attachments ) {
-                               this.createAttachments();
+                       if ( this.controller.isModeActive( 'grid' ) ) {
+                               noItemsView = view.attachmentsNoResults;
+                       } else {
+                               noItemsView = view.uploader;
                        }
 
                        if ( ! this.collection.length ) {
                                this.toolbar.get( 'spinner' ).show();
-                               this.collection.more().done(function() {
+                               this.dfd = this.collection.more().done( function() {
                                        if ( ! view.collection.length ) {
-                                               view.createUploader();
+                                               noItemsView.$el.removeClass( 'hidden' );
+                                       } else {
+                                               noItemsView.$el.addClass( 'hidden' );
                                        }
                                        view.toolbar.get( 'spinner' ).hide();
-                               });
+                               } );
                        } else {
+                               noItemsView.$el.addClass( 'hidden' );
                                view.toolbar.get( 'spinner' ).hide();
                        }
                },
 
-               removeContent: function() {
-                       _.each(['attachments','uploader'], function( key ) {
-                               if ( this[ key ] ) {
-                                       this[ key ].remove();
-                                       delete this[ key ];
-                               }
-                       }, this );
-               },
-
                createUploader: function() {
-                       this.removeContent();
-
                        this.uploader = new media.view.UploaderInline({
                                controller: this.controller,
                                status:     false,
-                               message:    l10n.noItemsFound
+                               message:    this.controller.isModeActive( 'grid' ) ? '' : l10n.noItemsFound,
+                               canClose:   this.controller.isModeActive( 'grid' )
                        });
 
+                       this.uploader.hide();
                        this.views.add( this.uploader );
                },
 
-               createAttachments: function() {
-                       this.removeContent();
+               toggleUploader: function() {
+                       if ( this.uploader.$el.hasClass( 'hidden' ) ) {
+                               this.uploader.show();
+                       } else {
+                               this.uploader.hide();
+                       }
+               },
 
+               createAttachments: function() {
                        this.attachments = new media.view.Attachments({
-                               controller: this.controller,
-                               collection: this.collection,
-                               selection:  this.options.selection,
-                               model:      this.model,
-                               sortable:   this.options.sortable,
+                               controller:           this.controller,
+                               collection:           this.collection,
+                               selection:            this.options.selection,
+                               model:                this.model,
+                               sortable:             this.options.sortable,
+                               scrollElement:        this.options.scrollElement,
+                               idealColumnWidth:     this.options.idealColumnWidth,
 
                                // The single `Attachment` view to be used in the `Attachments` view.
                                AttachmentView: this.options.AttachmentView
                        });
 
+                       // Add keydown listener to the instance of the Attachments view
+                       this.attachments.listenTo( this.controller, 'attachment:keydown:arrow',     this.attachments.arrowEvent );
+                       this.attachments.listenTo( this.controller, 'attachment:details:shift-tab', this.attachments.restoreFocus );
+
                        this.views.add( this.attachments );
+
+
+                       if ( this.controller.isModeActive( 'grid' ) ) {
+                               this.attachmentsNoResults = new media.View({
+                                       controller: this.controller,
+                                       tagName: 'p'
+                               });
+
+                               this.attachmentsNoResults.$el.addClass( 'hidden no-media' );
+                               this.attachmentsNoResults.$el.html( l10n.noMedia );
+
+                               this.views.add( this.attachmentsNoResults );
+                       }
                },
 
                createSidebar: function() {
                                        userSettings: this.model.get('displayUserSettings')
                                }) );
                        }
+
+                       // Show the sidebar on mobile
+                       if ( this.model.id === 'insert' ) {
+                               sidebar.$el.addClass( 'visible' );
+                       }
                },
 
                disposeSingle: function() {
                        sidebar.unset('details');
                        sidebar.unset('compat');
                        sidebar.unset('display');
+                       // Hide the sidebar on mobile
+                       sidebar.$el.removeClass( 'visible' );
                }
        });
 
        /**
         * wp.media.view.Selection
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                                controller: this.controller,
                                collection: this.collection,
                                selection:  this.collection,
-                               model:      new Backbone.Model({
-                                       edge:   40,
-                                       gutter: 5
-                               })
+                               model:      new Backbone.Model()
                        });
 
                        this.views.set( '.selection-view', this.attachments );
                clear: function( event ) {
                        event.preventDefault();
                        this.collection.reset();
+
+                       // Keep focus inside media modal
+                       // after clear link is selected
+                       this.controller.modal.focusManager.focus();
                }
        });
 
        /**
         * wp.media.view.Attachment.Selection
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Attachment
         * @augments wp.media.View
         * @augments wp.Backbone.View
        /**
         * wp.media.view.Attachments.Selection
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Attachments
         * @augments wp.media.View
         * @augments wp.Backbone.View
                events: {},
                initialize: function() {
                        _.defaults( this.options, {
-                               sortable:   true,
+                               sortable:   false,
                                resize:     false,
 
                                // The single `Attachment` view to be used in the `Attachments` view.
                                AttachmentView: media.view.Attachment.Selection
                        });
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+                       // Call 'initialize' directly on the parent class.
                        return media.view.Attachments.prototype.initialize.apply( this, arguments );
                }
        });
        /**
         * wp.media.view.Attachments.EditSelection
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Attachment.Selection
         * @augments wp.media.view.Attachment
         * @augments wp.media.View
        /**
         * wp.media.view.Settings
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                                }
                        // Handle checkboxes.
                        } else if ( $setting.is('input[type="checkbox"]') ) {
-                               $setting.prop( 'checked', !! value );
+                               $setting.prop( 'checked', !! value && 'false' !== value );
                        }
                },
                /**
        /**
         * wp.media.view.Settings.AttachmentDisplay
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Settings
         * @augments wp.media.View
         * @augments wp.Backbone.View
                        _.defaults( this.options, {
                                userSettings: false
                        });
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+                       // Call 'initialize' directly on the parent class.
                        media.view.Settings.prototype.initialize.apply( this, arguments );
                        this.model.on( 'change:link', this.updateLinkTo, this );
 
                        $input.removeClass( 'hidden' );
 
                        // If the input is visible, focus and select its contents.
-                       if ( $input.is(':visible') ) {
+                       if ( ! isTouchDevice && $input.is(':visible') ) {
                                $input.focus()[0].select();
                        }
                }
        /**
         * wp.media.view.Settings.Gallery
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Settings
         * @augments wp.media.View
         * @augments wp.Backbone.View
        /**
         * wp.media.view.Settings.Playlist
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Settings
         * @augments wp.media.View
         * @augments wp.Backbone.View
        /**
         * wp.media.view.Attachment.Details
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Attachment
         * @augments wp.media.View
         * @augments wp.Backbone.View
                className: 'attachment-details',
                template:  media.template('attachment-details'),
 
+               attributes: function() {
+                       return {
+                               'tabIndex':     0,
+                               'data-id':      this.model.get( 'id' )
+                       };
+               },
+
                events: {
                        'change [data-setting]':          'updateSetting',
                        'change [data-setting] input':    'updateSetting',
                        'change [data-setting] textarea': 'updateSetting',
                        'click .delete-attachment':       'deleteAttachment',
                        'click .trash-attachment':        'trashAttachment',
+                       'click .untrash-attachment':      'untrashAttachment',
                        'click .edit-attachment':         'editAttachment',
-                       'click .refresh-attachment':      'refreshAttachment'
+                       'click .refresh-attachment':      'refreshAttachment',
+                       'keydown':                        'toggleSelectionHandler'
                },
 
                initialize: function() {
-                       /**
-                        * @member {wp.media.view.FocusManager}
-                        */
-                       this.focusManager = new media.view.FocusManager({
-                               el: this.el
+                       this.options = _.defaults( this.options, {
+                               rerenderOnModelChange: false
                        });
-                       /**
-                        * call 'initialize' directly on the parent class
-                        */
+
+                       this.on( 'ready', this.initialFocus );
+                       // Call 'initialize' directly on the parent class.
                        media.view.Attachment.prototype.initialize.apply( this, arguments );
                },
-               /**
-                * @returns {wp.media.view..Attachment.Details} Returns itself to allow chaining
-                */
-               render: function() {
-                       /**
-                        * call 'render' directly on the parent class
-                        */
-                       media.view.Attachment.prototype.render.apply( this, arguments );
-                       this.focusManager.focus();
-                       return this;
+
+               initialFocus: function() {
+                       if ( ! isTouchDevice ) {
+                               this.$( ':input' ).eq( 0 ).focus();
+                       }
                },
                /**
                 * @param {Object} event
 
                        if ( confirm( l10n.warnDelete ) ) {
                                this.model.destroy();
+                               // Keep focus inside media modal
+                               // after image is deleted
+                               this.controller.modal.focusManager.focus();
                        }
                },
                /**
                 * @param {Object} event
                 */
                trashAttachment: function( event ) {
+                       var library = this.controller.library;
                        event.preventDefault();
 
-                       this.model.destroy();
+                       if ( media.view.settings.mediaTrash &&
+                               'edit-metadata' === this.controller.content.mode() ) {
+
+                               this.model.set( 'status', 'trash' );
+                               this.model.save().done( function() {
+                                       library._requery( true );
+                               } );
+                       }  else {
+                               this.model.destroy();
+                       }
+               },
+               /**
+                * @param {Object} event
+                */
+               untrashAttachment: function( event ) {
+                       var library = this.controller.library;
+                       event.preventDefault();
+
+                       this.model.set( 'status', 'inherit' );
+                       this.model.save().done( function() {
+                               library._requery( true );
+                       } );
                },
                /**
                 * @param {Object} event
                        this.$el.removeClass('needs-refresh');
                        event.preventDefault();
                        this.model.fetch();
-               }
+               },
+               /**
+                * When reverse tabbing(shift+tab) out of the right details panel, deliver
+                * the focus to the item in the list that was being edited.
+                *
+                * @param {Object} event
+                */
+               toggleSelectionHandler: function( event ) {
+                       if ( 'keydown' === event.type && 9 === event.keyCode && event.shiftKey && event.target === this.$( ':tabbable' ).get( 0 ) ) {
+                               this.controller.trigger( 'attachment:details:shift-tab', event );
+                               return false;
+                       }
 
+                       if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
+                               this.controller.trigger( 'attachment:keydown:arrow', event );
+                               return;
+                       }
+               }
        });
 
        /**
         * wp.media.view.AttachmentCompat
         *
-        * @constructor
+        * A view to display fields added via the `attachment_fields_to_edit` filter.
+        *
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                },
 
                initialize: function() {
-                       /**
-                        * @member {wp.media.view.FocusManager}
-                        */
-                       this.focusManager = new media.view.FocusManager({
-                               el: this.el
-                       });
-
                        this.model.on( 'change:compat', this.render, this );
                },
                /**
                        this.views.detach();
                        this.$el.html( compat.item );
                        this.views.render();
-
-                       this.focusManager.focus();
                        return this;
                },
                /**
                                data[ pair.name ] = pair.value;
                        });
 
-                       this.model.saveCompat( data );
+                       this.controller.trigger( 'attachment:compat:waiting', ['waiting'] );
+                       this.model.saveCompat( data ).always( _.bind( this.postSave, this ) );
+               },
+
+               postSave: function() {
+                       this.controller.trigger( 'attachment:compat:ready', ['ready'] );
                }
        });
 
        /**
         * wp.media.view.Iframe
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
        /**
         * wp.media.view.Embed
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                }
        });
 
+       /**
+        * @class
+        * @augments wp.media.View
+        * @augments wp.Backbone.View
+        * @augments Backbone.View
+        */
+       media.view.Label = media.View.extend({
+               tagName: 'label',
+               className: 'screen-reader-text',
+
+               initialize: function() {
+                       this.value = this.options.value;
+               },
+
+               render: function() {
+                       this.$el.html( this.value );
+
+                       return this;
+               }
+       });
+
        /**
         * wp.media.view.EmbedUrl
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                },
 
                initialize: function() {
-                       this.$input = $('<input/>').attr( 'type', 'text' ).val( this.model.get('url') );
+                       var self = this;
+
+                       this.$input = $('<input id="embed-url-field" type="url" />').val( this.model.get('url') );
                        this.input = this.$input[0];
 
                        this.spinner = $('<span class="spinner" />')[0];
                        this.$el.append([ this.input, this.spinner ]);
 
                        this.model.on( 'change:url', this.render, this );
+
+                       if ( this.model.get( 'url' ) ) {
+                               _.delay( function () {
+                                       self.model.trigger( 'change:url' );
+                               }, 500 );
+                       }
                },
                /**
                 * @returns {wp.media.view.EmbedUrl} Returns itself to allow chaining
                },
 
                ready: function() {
-                       this.focus();
+                       if ( ! isTouchDevice ) {
+                               this.focus();
+                       }
                },
 
                url: function( event ) {
        /**
         * wp.media.view.EmbedLink
         *
-        * @constructor
+        * @class
         * @augments wp.media.view.Settings
         * @augments wp.media.View
         * @augments wp.Backbone.View
         */
        media.view.EmbedLink = media.view.Settings.extend({
                className: 'embed-link-settings',
-               template:  media.template('embed-link-settings')
+               template:  media.template('embed-link-settings'),
+
+               initialize: function() {
+                       this.spinner = $('<span class="spinner" />');
+                       this.$el.append( this.spinner[0] );
+                       this.listenTo( this.model, 'change:url', this.updateoEmbed );
+               },
+
+               updateoEmbed: function() {
+                       var url = this.model.get( 'url' );
+
+                       this.$('.setting.title').show();
+                       // clear out previous results
+                       this.$('.embed-container').hide().find('.embed-preview').html('');
+
+                       // only proceed with embed if the field contains more than 6 characters
+                       if ( url && url.length < 6 ) {
+                               return;
+                       }
+
+                       this.spinner.show();
+
+                       setTimeout( _.bind( this.fetch, this ), 500 );
+               },
+
+               fetch: function() {
+                       // check if they haven't typed in 500 ms
+                       if ( $('#embed-url-field').val() !== this.model.get('url') ) {
+                               return;
+                       }
+
+                       wp.ajax.send( 'parse-embed', {
+                               data : {
+                                       post_ID: media.view.settings.post.id,
+                                       shortcode: '[embed]' + this.model.get('url') + '[/embed]'
+                               }
+                       } ).done( _.bind( this.renderoEmbed, this ) );
+               },
+
+               renderoEmbed: function( response ) {
+                       var html = ( response && response.body ) || '';
+
+                       this.spinner.hide();
+
+                       this.$('.setting.title').hide();
+                       this.$('.embed-container').show().find('.embed-preview').html( html );
+               }
        });
 
        /**
         * wp.media.view.EmbedImage
         *
-        * @contructor
+        * @class
         * @augments wp.media.view.Settings.AttachmentDisplay
         * @augments wp.media.view.Settings
         * @augments wp.media.View
        /**
         * wp.media.view.ImageDetails
         *
-        * @contructor
+        * @class
         * @augments wp.media.view.Settings.AttachmentDisplay
         * @augments wp.media.view.Settings
         * @augments wp.media.View
                                value = Math.round( this.model.get( 'aspectRatio' ) * num );
                                this.model.set( 'customWidth', value, { silent: true  } );
                                this.$( '[data-setting="customWidth"]' ).val( value );
-
                        }
                },
 
         * wp.customize.HeaderControl.calculateImageSelectOptions via
         * wp.customize.HeaderControl.openMM.
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View
                },
 
                loadEditor: function() {
-                       this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
+                       var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
+                       dfd.done( _.bind( this.focus, this ) );
+               },
+
+               focus: function() {
+                       this.$( '.imgedit-submit .button' ).eq( 0 ).focus();
                },
 
                back: function() {
        /**
         * wp.media.view.Spinner
         *
-        * @constructor
+        * @class
         * @augments wp.media.View
         * @augments wp.Backbone.View
         * @augments Backbone.View