]> scripts.mit.edu Git - autoinstalls/wordpress.git/blobdiff - wp-includes/js/media-views.js
WordPress 4.0
[autoinstalls/wordpress.git] / wp-includes / js / media-views.js
index 0d70ebf2381a164c76862dee14cb6e1205faf2d7..84e1f3655dda83c6a4397fa0a2e44f4c8ab8e387 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;
 
        _.extend( media.controller.Region.prototype, {
                /**
-                * Switch modes
+                * Activate a mode.
                 *
                 * @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
-                *
-                * If no mode is provided, just re-render the current mode.
-                * If the provided mode isn't active, perform a full switch.
+                * Render a mode.
                 *
                 * @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.
+                *
+                * @returns {wp.media.View}
                 */
                get: function() {
                        return this.view.views.first( this.selector );
                },
 
                /**
+                * Set the region's view as a subview of the frame.
+                *
                 * @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.
                 *
                 * @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;
        // 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.
                }
        });
 
-       // 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. This is the base
+        * class that the various states used in wp.media extend.
         *
         * @constructor
         * @augments Backbone.Model
                        Backbone.Model.apply( this, arguments );
                        this.on( 'change:menu', this._updateMenu, this );
                },
-
                /**
                 * @abstract
                 */
                },
                /**
                 * @access private
-                */
+               */
                _preActivate: function() {
                        this.active = true;
                },
                                mode = this.get('menu'),
                                view;
 
+                       this.frame.$el.toggleClass( 'hide-menu', ! mode );
                        if ( ! mode ) {
                                return;
                        }
        };
 
        /**
-        * wp.media.controller.Library
+        * A state for choosing an attachment from the media library.
         *
         * @constructor
         * @augments wp.media.controller.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,
-
+                       id:                 'library',
+                       title:              l10n.mediaLibraryTitle,
+                       // Selection defaults. @see media.model.Selection
+                       multiple:           false,
+                       // Initial region modes.
+                       content:            'upload',
+                       menu:               'default',
+                       router:             'browse',
+                       toolbar:            'select',
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       searchable:         true,
+                       filterable:         false,
+                       sortable:           true,
+
+                       autoSelect:         true,
+                       describe:           false,
                        // Uses a user setting to override the content mode.
                        contentUserSetting: true,
-
                        // Sync the selection from the last state when 'multiple' matches.
-                       syncSelection: true
+                       syncSelection:      true
                },
 
                /**
                                }) );
                        }
 
-                       if ( ! this.get('edge') ) {
-                               this.set( 'edge', 120 );
-                       }
-
-                       if ( ! this.get('gutter') ) {
-                               this.set( 'gutter', 8 );
-                       }
-
                        this.resetDisplays();
                },
 
 
                        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') ) );
                        }
                        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' );
+                       }
                },
 
                /**
        _.extend( media.controller.Library.prototype, media.selectionSync );
 
        /**
-        * wp.media.controller.ImageDetails
+        * A state for editing the settings of an image within a content editor.
         *
         * @constructor
         * @augments wp.media.controller.State
         */
        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,
+                       // Initial region modes.
+                       content:  'image-details',
+                       menu:     false,
+                       router:   false,
+                       toolbar:  'image-details',
+
+                       editing:  false,
+                       priority: 60
                }, media.controller.Library.prototype.defaults ),
 
                initialize: function( options ) {
        });
 
        /**
-        * wp.media.controller.GalleryEdit
+        * A state for editing a gallery's images and settings.
         *
         * @constructor
         * @augments wp.media.controller.Library
         */
        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,
+                       id:              'gallery-edit',
+                       title:           l10n.editGalleryTitle,
+                       // Selection defaults. @see media.model.Selection
+                       multiple:        false,
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       searchable:      false,
+                       sortable:        true,
+                       display:         false,
+                       // Initial region modes.
+                       content:         'browse',
+                       toolbar:         'gallery-edit',
+
+                       describe:         true,
+                       displaySettings:  true,
+                       dragInfo:         true,
+                       idealColumnWidth: 170,
+                       editing:          false,
+                       priority:         60,
 
                        // Don't sync the selection, as the Edit Gallery library
                        // *is* the selection.
                },
 
                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 adding an image to a gallery.
         *
         * @constructor
         * @augments wp.media.controller.Library
         */
        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,
+                       id:            'gallery-library',
+                       title:         l10n.addToGalleryTitle,
+                       // Selection defaults. @see media.model.Selection
+                       multiple:      'add',
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       filterable:    'uploaded',
+                       // Initial region modes.
+                       menu:          'gallery',
+                       toolbar:       'gallery-add',
 
+                       priority:      100,
                        // Don't sync the selection, as the Edit Gallery library
                        // *is* the selection.
                        syncSelection: false
         */
        media.controller.CollectionEdit = media.controller.Library.extend({
                defaults: {
+                       // Selection defaults. @see media.model.Selection
                        multiple:     false,
-                       describe:     true,
-                       edge:         199,
-                       editing:      false,
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
                        sortable:     true,
                        searchable:   false,
+                       // Region mode defaults.
                        content:      'browse',
-                       priority:     60,
-                       dragInfo:     true,
-                       SettingsView: false,
+
+                       describe:         true,
+                       dragInfo:         true,
+                       idealColumnWidth: 170,
+                       editing:          false,
+                       priority:         60,
+                       SettingsView:     false,
 
                        // Don't sync the selection, as the Edit {Collection} library
                        // *is* the selection.
         */
        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 ),
        });
 
        /**
-        * wp.media.controller.FeaturedImage
+        * A state for selecting a featured image for a post.
         *
         * @constructor
         * @augments wp.media.controller.Library
         */
        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,
+                       // Selection defaults. @see media.model.Selection
+                       multiple:      false,
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       filterable:    'uploaded',
+                       // Region mode defaults.
+                       toolbar:       'featured-image',
+
+                       priority:      60,
                        syncSelection: true
                }, media.controller.Library.prototype.defaults ),
 
        });
 
        /**
-        * wp.media.controller.ReplaceImage
-        *
-        * Replace a selected single image
+        * A state for replacing an image.
         *
         * @constructor
         * @augments wp.media.controller.Library
         */
        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,
+                       // Selection defaults. @see media.model.Selection
+                       multiple:      false,
+                       // Attachments browser defaults. @see media.view.AttachmentsBrowser
+                       filterable:    'uploaded',
+                       // Region mode defaults.
+                       toolbar:       'replace',
+                       menu:          false,
+
+                       priority:      60,
                        syncSelection: true
                }, media.controller.Library.prototype.defaults ),
 
        });
 
        /**
-        * wp.media.controller.EditImage
+        * A state for editing (cropping, etc.) an image.
         *
         * @constructor
         * @augments wp.media.controller.State
         */
        media.controller.EditImage = media.controller.State.extend({
                defaults: {
-                       id: 'edit-image',
-                       url: '',
-                       menu: false,
+                       id:      'edit-image',
+                       title:   l10n.editImage,
+                       // Region mode defaults.
+                       menu:    false,
                        toolbar: 'edit-image',
-                       title: l10n.editImage,
-                       content: 'edit-image'
+                       content: 'edit-image',
+
+                       url:     ''
                },
 
                activate: function() {
         */
        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 ),
 
                initialize: function( options ) {
         */
        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
+                       // Region mode defaults.
+                       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 );
         */
        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
                },
 
         */
        media.view.Frame = media.View.extend({
                initialize: function() {
+                       _.defaults( this.options, {
+                               mode: [ 'select' ]
+                       });
                        this._createRegions();
                        this._createStates();
+                       this._createModes();
                },
 
                _createRegions: function() {
                                this.states.add( this.options.states );
                        }
                },
+               _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 );
+               },
                /**
                 * @returns {wp.media.view.Frame} Returns itself to allow chaining
                 */
                reset: function() {
                        this.states.invoke( 'trigger', 'reset' );
                        return this;
+               },
+               /**
+                * Map activeMode collection events to the frame.
+                */
+               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 );
                }
        });
 
                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.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'),
+                               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
                        }) );
                        _.defaults( this.options, {
                                multiple:  true,
                                editing:   false,
-                               state:    'insert'
+                               state:    'insert',
+                               metadata:  {}
                        });
                        /**
                         * call 'initialize' directly on the parent class
                                }),
 
                                // 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() {
                                        }) );
 
                                        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();
                                }
                        });
                },
                        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,
                                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.
         * @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;
                        }
                }
+
        });
 
        /**
                                        $el.hide();
                                }
                        });
+
+                       // https://core.trac.wordpress.org/ticket/27341
+                       _.delay( function() {
+                               if ( '0' === $el.css('opacity') && $el.is(':visible') ) {
+                                       $el.hide();
+                               }
+                       }, 500 );
                }
        });
 
                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' );
                }
+
        });
 
        /**
                        // 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 ]);
                        } 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
                        }
                },
                /**
                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
         * @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 ) {
                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':                        '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') );
                },
                /**
                 * @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;
+                       }
+
+                       // 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 );
+
+                                       // Don't scroll the view and don't attempt to submit anything.
+                                       event.stopPropagation();
+                                       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' );
+
+                       // Don't scroll the view and don't attempt to submit anything.
+                       event.stopPropagation();
+               },
                /**
                 * @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
 
                        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
                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;
                        }
 
                },
 
                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');
-                       };
-               }())
        });
 
        /**
        media.view.Search = media.View.extend({
                tagName:   'input',
                className: 'search',
+               id:        'media-search-input',
 
                attributes: {
                        type:        'search',
        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 selection changes, set the Query properties
+                * accordingly for the selected filter.
+                */
                change: function() {
                        var filter = this.filters[ this.el.value ];
-
                        if ( filter ) {
                                this.model.set( filter.props );
                        }
                                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
         *
                                filters: false,
                                search:  true,
                                display: false,
-
+                               sidebar: true,
                                AttachmentView: media.view.Attachment.Library
                        });
 
+                       this.listenTo( this.controller, 'toggle:upload:attachment', _.bind( this.toggleUploader, this ) );
+
                        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 );
                },
                },
 
                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 model, changed = [], 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;
+                                               }
+
+                                               while ( selection.length > 0 ) {
+                                                       model = selection.at( 0 );
+                                                       if ( media.view.settings.mediaTrash && 'trash' === model.get( 'status' ) ) {
+                                                               model.set( 'status', 'inherit' );
+                                                               changed.push( model.save() );
+                                                               selection.remove( model );
+                                                       } else if ( media.view.settings.mediaTrash ) {
+                                                               model.set( 'status', 'trash' );
+                                                               changed.push( model.save() );
+                                                               selection.remove( model );
+                                                       } else {
+                                                               model.destroy();
+                                                       }
+                                               }
+
+                                               if ( changed.length ) {
+                                                       $.when.apply( null, changed ).then( function() {
+                                                               library._requery( true );
+                                                               self.controller.trigger( 'selection:action:done' );
+                                                       } );
+                                               } else {
+                                                       this.controller.trigger( 'selection:action:done' );
+                                               }
+                                       }
+                               }).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' );
                }
        });
 
                                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();
                }
        });
 
                                }
                        // Handle checkboxes.
                        } else if ( $setting.is('input[type="checkbox"]') ) {
-                               $setting.prop( 'checked', !! value );
+                               $setting.prop( 'checked', !! value && 'false' !== value );
                        }
                },
                /**
                        $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();
                        }
                }
                        '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
                        });
+
+                       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
         *
+        * A view to display fields added via the `attachment_fields_to_edit` filter.
+        *
         * @constructor
         * @augments wp.media.View
         * @augments wp.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;
                },
                /**
                }
        });
 
+       /**
+        * @constructor
+        * @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
         *
                },
 
                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 ) {
         */
        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
+        * @constructor
         * @augments wp.media.view.Settings.AttachmentDisplay
         * @augments wp.media.view.Settings
         * @augments wp.media.View
        /**
         * wp.media.view.ImageDetails
         *
-        * @contructor
+        * @constructor
         * @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 );
-
                        }
                },
 
                },
 
                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() {