<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[22320] trunk: Give media a heart transplant.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg > ul, #logmsg > ol { margin-left: 0; margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://core.trac.wordpress.org/changeset/22320">22320</a></dd>
<dt>Author</dt> <dd>koopersmith</dd>
<dt>Date</dt> <dd>2012-10-28 23:29:17 +0000 (Sun, 28 Oct 2012)</dd>
</dl>
<h3>Log Message</h3>
<pre>Give media a heart transplant.
Revises the concept of the media controller and workspace views (i.e. two central points of control) to be more granular. The main media object is now the `Frame`, which is a hybrid view and state machine.
The state machine is a collection of states, which are just generic instances of `Backbone.Model`. This circumvents the problem of juggling global parameters when changing states. Each state contains its own event loop. All events are also forwarded to the frame itself (as is the case in all model/collection relationships).
The frame view contains several regions, each of which can be overridden without harming or re-rendering the other regions. These work well when used in conjunction with the state machine events.
This removes the upload sidebar (don't worry, visible upload UI will return). Drag and drop uploading still works. The ability to upload has been abstracted into its own view (instead of being attached to the central workspace view).
Editing galleries is temporarily broken ?\226?\128?\148?\194?\160the gallery creation and editing experiences will be unified in a future patch.
Adds events to detect dragging changes in `wp.Uploader` and adds methods to detect and leverage browser support for CSS3 transitions.
see <a href="http://core.trac.wordpress.org/ticket/21390">#21390</a>, <a href="http://core.trac.wordpress.org/ticket/21809">#21809</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkwpadminincludesmetaboxesphp">trunk/wp-admin/includes/meta-boxes.php</a></li>
<li><a href="#trunkwpadminjsmediauploadjs">trunk/wp-admin/js/media-upload.js</a></li>
<li><a href="#trunkwpincludescssmediaviewscss">trunk/wp-includes/css/media-views.css</a></li>
<li><a href="#trunkwpincludesjsmceviewjs">trunk/wp-includes/js/mce-view.js</a></li>
<li><a href="#trunkwpincludesjsmediamodelsjs">trunk/wp-includes/js/media-models.js</a></li>
<li><a href="#trunkwpincludesjsmediaviewsjs">trunk/wp-includes/js/media-views.js</a></li>
<li><a href="#trunkwpincludesjspluploadwppluploadjs">trunk/wp-includes/js/plupload/wp-plupload.js</a></li>
<li><a href="#trunkwpincludesmediaphp">trunk/wp-includes/media.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkwpadminincludesmetaboxesphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/includes/meta-boxes.php (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/includes/meta-boxes.php        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-admin/includes/meta-boxes.php        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -1018,7 +1018,7 @@
</span><span class="cx">                 var $element = $('#select-featured-image'),
</span><span class="cx">                         $thumbnailId = $element.find('input[name="thumbnail_id"]'),
</span><span class="cx">                         title = '<?php _e( "Choose a Featured Image" ); ?>',
</span><del>-                        workflow, setFeaturedImage;
</del><ins>+                        workflow, selection, setFeaturedImage;
</ins><span class="cx">
</span><span class="cx">                 setFeaturedImage = function( thumbnailId ) {
</span><span class="cx">                         $element.find('img').remove();
</span><span class="lines">@@ -1037,7 +1037,9 @@
</span><span class="cx">                                         }
</span><span class="cx">                                 });
</span><span class="cx">
</span><del>-                                workflow.selection.on( 'add', function( model ) {
</del><ins>+                                selection = workflow.state().get('selection');
+
+                                selection.on( 'add', function( model ) {
</ins><span class="cx">                                         var sizes = model.get('sizes'),
</span><span class="cx">                                                 size;
</span><span class="cx">
</span><span class="lines">@@ -1051,8 +1053,8 @@
</span><span class="cx">                                         // data besides just calling toJSON().
</span><span class="cx">                                         size = size || model.toJSON();
</span><span class="cx">
</span><del>-                                        workflow.modal.close();
-                                        workflow.selection.clear();
</del><ins>+                                        workflow.close();
+                                        selection.clear();
</ins><span class="cx">
</span><span class="cx">                                         $( '<img />', {
</span><span class="cx">                                                 src: size.url,
</span><span class="lines">@@ -1061,7 +1063,7 @@
</span><span class="cx">                                 });
</span><span class="cx">                         }
</span><span class="cx">
</span><del>-                        workflow.modal.open();
</del><ins>+                        workflow.open();
</ins><span class="cx">                 });
</span><span class="cx">
</span><span class="cx">                 $element.on( 'click', '.remove', function( event ) {
</span></span></pre></div>
<a id="trunkwpadminjsmediauploadjs"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/js/media-upload.js (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/js/media-upload.js        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-admin/js/media-upload.js        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -104,11 +104,10 @@
</span><span class="cx">
</span><span class="cx">                         workflow = workflows[ id ] = wp.media( _.defaults( options || {}, {
</span><span class="cx">                                 title: wp.media.view.l10n.insertMedia,
</span><del>-                                multiple: true,
-                                describe: true
</del><ins>+                                multiple: true
</ins><span class="cx">                         } ) );
</span><span class="cx">
</span><del>-                        workflow.on( 'update:insert', function( selection ) {
</del><ins>+                        workflow.get('library').on( 'insert', function( selection ) {
</ins><span class="cx">                                 this.insert( selection.map( function( attachment ) {
</span><span class="cx">                                         if ( 'image' === attachment.get('type') )
</span><span class="cx">                                                 return wp.media.string.image( attachment ) + ' ';
</span><span class="lines">@@ -117,7 +116,7 @@
</span><span class="cx">                                 }).join('') );
</span><span class="cx">                         }, this );
</span><span class="cx">
</span><del>-                        workflow.on( 'update:gallery', function( selection ) {
</del><ins>+                        workflow.get('gallery').on( 'update', function( selection ) {
</ins><span class="cx">                                 var view = wp.mce.view.get('gallery'),
</span><span class="cx">                                         shortcode;
</span><span class="cx">
</span></span></pre></div>
<a id="trunkwpincludescssmediaviewscss"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/css/media-views.css (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/css/media-views.css        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-includes/css/media-views.css        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -75,6 +75,7 @@
</span><span class="cx">         position: relative;
</span><span class="cx">         z-index: 50;
</span><span class="cx">         height: 60px;
</span><ins>+        padding: 0 10px;
</ins><span class="cx">         border-bottom: 1px solid #dfdfdf;
</span><span class="cx"> }
</span><span class="cx">
</span><span class="lines">@@ -101,64 +102,11 @@
</span><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> /**
</span><del>- * Workspace
</del><ins>+ * Frame
</ins><span class="cx"> */
</span><del>-.media-workspace {
-        position: relative;
-        width: 100%;
-        height: 100%;
-}
</del><span class="cx">
</span><del>-.upload-attachments {
-        position: absolute;
-        top: 0;
-        left: 0;
-        bottom: 0;
-        width: 180px;
-        margin: 10px;
-        text-align: center;
-        border: 3px dashed #dfdfdf;
-        background: #fff;
-        z-index: 100;
-}
-
-.upload-attachments h3 {
-        font-size: 18px;
-        font-weight: 200;
-        color: #777;
-        padding: 40px 0 0;
-        margin: 0;
-}
-
-.upload-attachments span {
-        display: block;
-        color: #777;
-        margin: 10px 0;
-}
-
-.upload-attachments a {
-        display: inline-block;
-        margin: 0 auto;
-}
-
-.drag-over .upload-attachments {
-        width: auto;
-        right: 0;
-        border-color: #83B4D8;
-        box-shadow: 0 0 0 10px #fff;
-}
-
-.existing-attachments {
-        position: absolute;
-        top: 0;
-        left: 200px;
-        right: 0;
-        bottom: 0;
-        margin: 0 20px;
-}
-
-.media-workspace .attachments,
-.media-workspace .media-toolbar {
</del><ins>+.media-frame .attachments,
+.media-frame .media-toolbar {
</ins><span class="cx">         -webkit-transition-property: left, right, top, bottom, margin;
</span><span class="cx">         -moz-transition-property: left, right, top, bottom, margin;
</span><span class="cx">         -ms-transition-property: left, right, top, bottom, margin;
</span><span class="lines">@@ -172,9 +120,9 @@
</span><span class="cx">         transition-duration: 0.2s;
</span><span class="cx"> }
</span><span class="cx">
</span><del>-.media-workspace .attachments {
</del><ins>+.media-frame .attachments {
</ins><span class="cx">         position: absolute;
</span><del>-        top: 0;
</del><ins>+        top: 61px;
</ins><span class="cx">         left: 0;
</span><span class="cx">         right: 0;
</span><span class="cx">         bottom: 0;
</span><span class="lines">@@ -182,22 +130,21 @@
</span><span class="cx">         width: auto;
</span><span class="cx"> }
</span><span class="cx">
</span><del>-.media-workspace.with-toolbar .attachments {
-        top: 61px;
</del><ins>+.media-frame.hide-toolbar .attachments {
+        top: 0;
</ins><span class="cx"> }
</span><span class="cx">
</span><del>-.media-workspace .media-toolbar {
-        margin-top: -61px;
</del><ins>+.media-frame .media-toolbar {
+        margin-top: 0;
</ins><span class="cx"> }
</span><span class="cx">
</span><del>-.media-workspace.with-toolbar .media-toolbar {
-        margin-top: 0;
</del><ins>+.media-frame.hide-toolbar .media-toolbar {
+        margin-top: -61px;
</ins><span class="cx"> }
</span><span class="cx">
</span><del>-.media-workspace .media-toolbar .add-to-gallery {
</del><ins>+.media-frame .media-toolbar .add-to-gallery {
</ins><span class="cx">         display: none;
</span><span class="cx"> }
</span><del>-
</del><span class="cx"> /**
</span><span class="cx"> * Attachments
</span><span class="cx"> */
</span><span class="lines">@@ -213,6 +160,7 @@
</span><span class="cx">         left: 0;
</span><span class="cx">         right: 0;
</span><span class="cx">         height: 50px;
</span><ins>+        padding: 0 10px;
</ins><span class="cx">         background: #fff;
</span><span class="cx"> }
</span><span class="cx">
</span><span class="lines">@@ -242,7 +190,7 @@
</span><span class="cx">         right: 0;
</span><span class="cx">         bottom: 0;
</span><span class="cx">         overflow: auto;
</span><del>-        margin: 0 -10px 20px;
</del><ins>+        margin: 0 0 20px;
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> /**
</span><span class="lines">@@ -445,12 +393,70 @@
</span><span class="cx">         margin: -8px 0 0 -4px;
</span><span class="cx"> }
</span><span class="cx">
</span><del>-.upload-attachments .media-progress-bar {
-        margin-top: 80px;
</del><ins>+.uploader-window {
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: rgba( 0, 86, 132, 0.9 );
+
+        /*z-index: -200;*/
+        z-index: 250000;
</ins><span class="cx">         display: none;
</span><ins>+        text-align: center;
+        opacity: 0;
+
+        -webkit-transition: opacity 250ms;
+        -moz-transition: opacity 250ms;
+        -ms-transition: opacity 250ms;
+        -o-transition: opacity 250ms;
+        transition: opacity 250ms;
</ins><span class="cx"> }
</span><span class="cx">
</span><del>-.uploading .upload-attachments .media-progress-bar {
</del><ins>+/*.drag-over .uploader-window {
+        z-index: 250000;
+}*/
+
+.uploader-window-content {
+        position: absolute;
+        top: 30px;
+        left: 30px;
+        right: 30px;
+        bottom: 30px;
+        border: 1px dashed #fff;
+}
+
+.uploader-window h3 {
+        position: absolute;
+        top: 50%;
+        left: 0;
+        right: 0;
+        -webkit-transform: translateY( -50% );
+        -moz-transform: translateY( -50% );
+        -ms-transform: translateY( -50% );
+        -o-transform: translateY( -50% );
+        transform: translateY( -50% );
+
+        font-size: 18px;
+        font-weight: 200;
+        color: #fff;
+        padding: 0;
+}
+
+.uploader-window .media-progress-bar {
+        margin-top: 20px;
+        max-width: 300px;
+        background: transparent;
+        border-color: #fff;
+        /*display: none;*/
+}
+
+.uploader-window .media-progress-bar div {
+        background: #fff;
+}
+
+.uploading .uploader-window .media-progress-bar {
</ins><span class="cx">         display: block;
</span><span class="cx"> }
</span><span class="cx">
</span></span></pre></div>
<a id="trunkwpincludesjsmceviewjs"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/js/mce-view.js (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/js/mce-view.js        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-includes/js/mce-view.js        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -692,12 +692,11 @@
</span><span class="cx">                                         return;
</span><span class="cx">
</span><span class="cx">                                 this.workflow = wp.media({
</span><del>-                                        view: 'gallery',
</del><ins>+                                        state: 'gallery',
</ins><span class="cx">                                         selection: this.attachments.models,
</span><span class="cx">                                         title: mceview.l10n.editGallery,
</span><span class="cx">                                         editing: true,
</span><del>-                                        multiple: true,
-                                        describe: true
</del><ins>+                                        multiple: true
</ins><span class="cx">                                 });
</span><span class="cx">
</span><span class="cx">                                 // Create a single-use workflow. If the workflow is closed,
</span></span></pre></div>
<a id="trunkwpincludesjsmediamodelsjs"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/js/media-models.js (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/js/media-models.js        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-includes/js/media-models.js        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -14,8 +14,8 @@
</span><span class="cx">          * @return {object} A media workflow.
</span><span class="cx">          */
</span><span class="cx">         media = wp.media = function( attributes ) {
</span><del>-                if ( media.controller.Workflow )
-                        return new media.controller.Workflow( attributes ).attach().render();
</del><ins>+                if ( media.view.Frame )
+                        return new media.view.Frame( attributes ).render().attach().open();
</ins><span class="cx">         };
</span><span class="cx">
</span><span class="cx">         _.extend( media, { model: {}, view: {}, controller: {} });
</span></span></pre></div>
<a id="trunkwpincludesjsmediaviewsjs"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/js/media-views.js (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/js/media-views.js        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-includes/js/media-views.js        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -8,6 +8,44 @@
</span><span class="cx">         // Link any localized strings.
</span><span class="cx">         l10n = media.view.l10n = _.isUndefined( _wpMediaViewsL10n ) ? {} : _wpMediaViewsL10n;
</span><span class="cx">
</span><ins>+        // Check if the browser supports CSS 3.0 transitions
+        $.support.transition = (function(){
+                var style = document.documentElement.style,
+                        transitions = {
+                                WebkitTransition: 'webkitTransitionEnd',
+                                MozTransition: 'transitionend',
+                                OTransition: 'oTransitionEnd otransitionend',
+                                transition: 'transitionend'
+                        }, transition;
+
+                transition = _.find( _.keys( transitions ), function( transition ) {
+                        return ! _.isUndefined( style[ transition ] );
+                });
+
+                return transition && {
+                        end: transitions[ transition ]
+                };
+        }());
+
+        // Makes it easier to bind events using transitions.
+        media.transition = function( selector ) {
+                var deferred = $.Deferred();
+
+                if ( $.support.transition ) {
+                        if ( ! (selector instanceof $) )
+                                selector = $( selector );
+
+                        // Resolve the deferred when the first element finishes animating.
+                        selector.first().one( $.support.transition.end, deferred.resolve );
+
+                // Otherwise, execute on the spot.
+                } else {
+                        deferred.resolve();
+                }
+
+                return deferred.promise();
+        };
+
</ins><span class="cx">         /**
</span><span class="cx">          * ========================================================================
</span><span class="cx">          * CONTROLLERS
</span><span class="lines">@@ -15,135 +53,227 @@
</span><span class="cx">          */
</span><span class="cx">
</span><span class="cx">         /**
</span><del>-         * wp.media.controller.Workflow
</del><ins>+         * wp.media.controller.StateMachine
</ins><span class="cx">          */
</span><del>-        media.controller.Workflow = Backbone.Model.extend({
</del><ins>+        media.controller.StateMachine = function( states ) {
+                this.states = new Backbone.Collection( states );
+        };
+
+        // Use Backbone's self-propagating `extend` inheritance method.
+        media.controller.StateMachine.extend = Backbone.Model.extend;
+
+        _.extend( media.controller.StateMachine.prototype, {
+                // Fetch a state model.
+                //
+                // Implicitly creates states.
+                get: function( id ) {
+                        // Ensure that the `states` collection exists so the `StateMachine`
+                        // can be used as a mixin.
+                        this.states = this.states || new Backbone.Collection();
+
+                        if ( ! this.states.get( id ) )
+                                this.states.add({ id: id });
+                        return this.states.get( id );
+                },
+
+                // Selects or returns the active state.
+                //
+                // If a `id` is provided, sets that as the current state.
+                // If no parameters are provided, returns the current state object.
+                state: function( id ) {
+                        var previous;
+
+                        if ( id ) {
+                                if ( previous = this.state() )
+                                        previous.trigger('deactivate');
+                                this._state = id;
+                                return this.state().trigger('activate');
+                        }
+
+                        if ( this._state )
+                                return this.get( this._state );
+                }
+        });
+
+        // Map methods from the `states` collection to the `StateMachine` itself.
+        _.each([ 'on', 'off', 'trigger' ], function( method ) {
+                media.controller.StateMachine.prototype[ method ] = function() {
+                        // Ensure that the `states` collection exists so the `StateMachine`
+                        // can be used as a mixin.
+                        this.states = this.states || new Backbone.Collection();
+                        // Forward the method to the `states` collection.
+                        this.states[ method ].apply( this.states, arguments );
+                        return this;
+                };
+        });
+
+        // wp.media.controller.Library
+        // ---------------------------
+        media.controller.Library = Backbone.Model.extend({
</ins><span class="cx">                 defaults: {
</span><del>-                        title: '',
-                        multiple: false,
-                        view: 'library',
-                        library: {},
-                        selection: []
</del><ins>+                        id: 'library',
+                        multiple: false,
+                        describe: false
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 initialize: function() {
</span><del>-                        this.createSelection();
</del><ins>+                        if ( ! this.get('selection') )
+                                this.set( 'selection', new Attachments() );
</ins><span class="cx">
</span><del>-                        // Initialize view storage.
-                        this._views = {};
-                        this._pendingViews = {};
</del><ins>+                        if ( ! this.get('library') )
+                                this.set( 'library', media.query() );
</ins><span class="cx">
</span><del>-                        // Initialize modal container view.
-                        this.modal = new media.view.Modal({ controller: this });
</del><ins>+                        this.on( 'activate', this.activate, this );
+                },
</ins><span class="cx">
</span><del>-                        // Add default views.
-                        //
-                        // Use the `library` property to initialize the corresponding view,
-                        // then unset the property.
-                        this.add( 'library', media.view.Workspace.Library, {
-                                collection: media.query( this.get('library') )
</del><ins>+                activate: function() {
+                        var frame = this.frame,
+                                toolbar;
+
+                        toolbar = this._postLibraryToolbar = new media.view.Toolbar.PostLibrary({
+                                controller: frame,
+                                selection: this.get('selection')
</ins><span class="cx">                         });
</span><del>-                        this.unset('library');
</del><span class="cx">
</span><del>-                        // Add the gallery view.
-                        this.add( 'gallery', media.view.Workspace.Gallery, {
-                                collection: this.selection
-                        });
-                        this.add( 'gallery-library', media.view.Workspace.Library.Gallery, {
-                                collection: media.query({ type: 'image' })
-                        });
</del><ins>+                        frame.toolbar( toolbar );
+                        this.get('selection').on( 'add remove', toolbar.visibility, toolbar );
+
+                        frame.content( new media.view.Attachments({
+                                directions: this.get('multiple') ? l10n.selectMediaMultiple : l10n.selectMediaSingular,
+                                controller: frame,
+                                collection: this.get('library'),
+                                // The single `Attachment` view to be used in the `Attachments` view.
+                                AttachmentView: media.view.Attachment.Library
+                        }).render() );
+
+                        if ( ! this.get('selection').length )
+                                frame.$el.addClass('hide-toolbar');
+
+                        // If we're in a workflow that supports multiple attachments,
+                        // automatically select any uploading attachments.
+                        if ( this.get('multiple') )
+                                wp.Uploader.queue.on( 'add', this.selectUpload, this );
</ins><span class="cx">                 },
</span><span class="cx">
</span><ins>+                deactivate: function() {
+                        var toolbar = this._postLibraryToolbar;
</ins><span class="cx">
</span><del>-                // Registers a view.
-                //
-                // `id` is a unique ID for the view relative to the workflow instance.
-                // `constructor` is a `Backbone.View` constructor. `options` are the
-                // options to be passed when the view is initialized.
-                //
-                // Triggers the `add` and `add:VIEW_ID` events.
-                add: function( id, constructor, options ) {
-                        this.remove( id );
-                        this._pendingViews[ id ] = {
-                                view: constructor,
-                                options: options
-                        };
-                        this.trigger( 'add add:' + id, constructor, options );
-                        return this;
</del><ins>+                        wp.Uploader.queue.off( 'add', this.selectUpload, this );
+                        this.get('selection').off( 'add remove', toolbar.visibility, toolbar );
</ins><span class="cx">                 },
</span><span class="cx">
</span><del>-                // Returns a registered view instance. If an `id` is not provided,
-                // it will return the active view.
-                //
-                // Lazily instantiates a registered view.
-                //
-                // Triggers the `init` and `init:VIEW_ID` events.
-                view: function( id ) {
-                        var pending;
</del><ins>+                selectUpload: function( attachment ) {
+                        this.get('selection').add( attachment );
+                }
+        });
</ins><span class="cx">
</span><del>-                        id = id || this.get('view');
-                        pending = this._pendingViews[ id ];
</del><ins>+        // wp.media.controller.Gallery
+        // ---------------------------
+        media.controller.Gallery = Backbone.Model.extend({
+                defaults: {
+                        id: 'gallery',
+                        multiple: true,
+                        describe: true
+                },
</ins><span class="cx">
</span><del>-                        if ( ! this._views[ id ] && pending ) {
-                                this._views[ id ] = new pending.view( _.extend({ controller: this }, pending.options || {} ) );
-                                delete this._pendingViews[ id ];
-                                this.trigger( 'init init:' + id, this._views[ id ] );
-                        }
</del><ins>+                initialize: function() {
+                        if ( ! this.get('selection') )
+                                this.set( 'selection', new Attachments() );
</ins><span class="cx">
</span><del>-                        return this._views[ id ];
</del><ins>+                        this.on( 'activate', this.activate, this );
</ins><span class="cx">                 },
</span><span class="cx">
</span><del>-                // Unregisters a view from the workflow.
-                //
-                // Triggers the `remove` and `remove:VIEW_ID` events.
-                remove: function( id ) {
-                        delete this._views[ id ];
-                        delete this._pendingViews[ id ];
-                        this.trigger( 'remove remove:' + id );
-                        return this;
</del><ins>+                activate: function() {
+                        var frame = this.frame;
+
+                        frame.toolbar( new media.view.Toolbar.Gallery({
+                                controller: frame,
+                                editing: this.get('editing'),
+                                selection: this.get('selection')
+                        }) );
+
+                        frame.content( new media.view.Attachments({
+                                directions: 'Gallery time!',
+                                controller: frame,
+                                collection: this.get('selection'),
+                                sortable: true,
+                                // The single `Attachment` view to be used in the `Attachments` view.
+                                AttachmentView: media.view.Attachment.Gallery
+                        }).render() );
+
+                        // Automatically select any uploading attachments.
+                        wp.Uploader.queue.on( 'add', this.selectUpload, this );
</ins><span class="cx">                 },
</span><span class="cx">
</span><del>-                // Renders a view and places it within the modal window.
-                // Automatically adds a view if `constructor` is provided.
-                render: function( id, constructor, options ) {
-                        var view;
-                        id = id || this.get('view');
</del><ins>+                deactivate: function() {
+                        wp.Uploader.queue.off( 'add', this.selectUpload, this );
+                },
</ins><span class="cx">
</span><del>-                        if ( constructor )
-                                this.add( id, constructor, options );
</del><ins>+                selectUpload: function( attachment ) {
+                        this.get('selection').add( attachment );
+                }
+        });
</ins><span class="cx">
</span><del>-                        view = this.view( id );
</del><ins>+        /**
+         * ========================================================================
+         * VIEWS
+         * ========================================================================
+         */
</ins><span class="cx">
</span><del>-                        if ( ! view )
-                                return this;
</del><ins>+        /**
+         * wp.media.view.Frame
+         */
+        media.view.Frame = Backbone.View.extend({
+                tagName: 'div',
+                className: 'media-frame',
+                template: media.template('media-frame'),
</ins><span class="cx">
</span><del>-                        view.render();
-                        this.modal.content( view );
-                        return this;
</del><ins>+                initialize: function() {
+                        _.defaults( this.options, {
+                                state: 'library',
+                                title: '',
+                                selection: [],
+                                library: {},
+                                modal: true,
+                                multiple: false,
+                                uploader: true
+                        });
+
+                        this.createSelection();
+                        this.createSubviews();
+                        this.createStates();
</ins><span class="cx">                 },
</span><span class="cx">
</span><del>-                update: function( event ) {
-                        this.close();
-                        this.trigger( 'update', this.selection, this );
-                        this.trigger( 'update:' + event, this.selection, this );
-                        this.selection.clear();
</del><ins>+                render: function() {
+                        var els = [ this.sidebar().el, this.toolbar().el, this.content().el ];
+
+                        if ( this.modal )
+                                this.modal.render();
+
+                        // Detach any views that will be rebound to maintain event bidnings.
+                        this.$el.children().filter( els ).detach();
+                        this.$el.empty().append( els );
+
+                        // Render the window uploader if it exists.
+                        if ( this.uploader )
+                                this.uploader.render().$el.appendTo( this.$el );
+
+                        return this;
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 createSelection: function() {
</span><del>-                        var controller = this;
</del><ins>+                        var controller = this,
+                                selection = this.options.selection;
</ins><span class="cx">
</span><del>-                        // Initialize workflow-specific models.
-                        // Use the `selection` property to initialize the Attachments
-                        // collection, then unset the property.
-                        this.selection = new Attachments( this.get('selection') );
-                        this.unset('selection');
</del><ins>+                        if ( ! (selection instanceof Attachments) )
+                                selection = this.options.selection = new Attachments( selection );
</ins><span class="cx">
</span><del>-                        _.extend( this.selection, {
</del><ins>+                        _.extend( selection, {
</ins><span class="cx">                                 // Override the selection's add method.
</span><span class="cx">                                 // If the workflow does not support multiple
</span><span class="cx">                                 // selected attachments, reset the selection.
</span><span class="cx">                                 add: function( models, options ) {
</span><del>-                                        if ( ! controller.get('multiple') ) {
</del><ins>+                                        if ( ! controller.state().get('multiple') ) {
</ins><span class="cx">                                                 models = _.isArray( models ) ? _.first( models ) : models;
</span><span class="cx">                                                 this.clear( options );
</span><span class="cx">                                         }
</span><span class="lines">@@ -170,24 +300,95 @@
</span><span class="cx">                                         return !! ( this.getByCid( attachment.cid ) || this.get( attachment.id ) );
</span><span class="cx">                                 }
</span><span class="cx">                         });
</span><ins>+                },
+
+                createStates: function() {
+                        var options = this.options;
+
+                        // Create the default `states` collection.
+                        this.states = new Backbone.Collection();
+
+                        // Ensure states have a reference to the frame.
+                        this.states.on( 'add', function( model ) {
+                                model.frame = this;
+                        }, this );
+
+                        // Add the default states.
+                        this.states.add([
+                                new media.controller.Library({
+                                        selection: options.selection,
+                                        collection: media.query( options.library ),
+                                        multiple: this.options.multiple
+                                }),
+                                new media.controller.Gallery({
+                                        selection: options.selection
+                                })
+                        ]);
+
+                        // Set the default state.
+                        this.state( options.state );
+                },
+
+                createSubviews: function() {
+                        // Initialize a stub view for each subview region.
+                        _.each(['toolbar','sidebar','content'], function( subview ) {
+                                this[ '_' + subview ] = new Backbone.View({
+                                        tagName: 'div',
+                                        className: 'media-' + subview
+                                });
+                        }, this );
+
+                        // Initialize modal container view.
+                        if ( this.options.modal ) {
+                                this.modal = new media.view.Modal({
+                                        controller: this,
+                                        $content: this.$el,
+                                        title: this.options.title
+                                });
+                        }
+
+                        // Initialize window-wide uploader.
+                        if ( this.options.uploader ) {
+                                this.uploader = new media.view.UploaderWindow({
+                                        uploader: {
+                                                dropzone: this.modal ? this.modal.$el : this.$el
+                                        }
+                                });
+                        }
</ins><span class="cx">                 }
</span><span class="cx">         });
</span><span class="cx">
</span><del>-        // Map modal methods to the workflow.
-        _.each(['attach','detach','open','close'], function( method ) {
-                media.controller.Workflow.prototype[ method ] = function() {
-                        this.modal[ method ].apply( this.modal, arguments );
</del><ins>+        // Make the `Frame` a `StateMachine`.
+        _.extend( media.view.Frame.prototype, media.controller.StateMachine.prototype );
+
+        // Create methods to fetch and replace individual subviews.
+        _.each(['toolbar','sidebar','content'], function( subview ) {
+                media.view.Frame.prototype[ subview ] = function( view ) {
+                        var previous = this[ '_' + subview ];
+
+                        if ( ! view )
+                                return previous;
+
+                        view.$el.addClass( 'media-' + subview );
+
+                        if ( previous.destroy )
+                                previous.destroy();
+                        previous.undelegateEvents();
+                        previous.$el.replaceWith( view.$el );
+                        this[ '_' + subview ] = view;
+                };
+        });
+
+        // Map some of the modal's methods to the frame.
+        _.each(['open','close','attach','detach'], function( method ) {
+                media.view.Frame.prototype[ method ] = function( view ) {
+                        if ( this.modal )
+                                this.modal[ method ].apply( this.modal, arguments );
</ins><span class="cx">                         return this;
</span><span class="cx">                 };
</span><span class="cx">         });
</span><span class="cx">
</span><span class="cx">         /**
</span><del>-         * ========================================================================
-         * VIEWS
-         * ========================================================================
-         */
-
-        /**
</del><span class="cx">          * wp.media.view.Modal
</span><span class="cx">          */
</span><span class="cx">         media.view.Modal = Backbone.View.extend({
</span><span class="lines">@@ -200,10 +401,10 @@
</span><span class="cx">
</span><span class="cx">                 initialize: function() {
</span><span class="cx">                         this.controller = this.options.controller;
</span><del>-                        this.controller.on( 'change:title', this.render, this );
</del><span class="cx">
</span><span class="cx">                         _.defaults( this.options, {
</span><del>-                                container: document.body
</del><ins>+                                container: document.body,
+                                title: ''
</ins><span class="cx">                         });
</span><span class="cx">                 },
</span><span class="cx">
</span><span class="lines">@@ -215,29 +416,37 @@
</span><span class="cx">                         // `this.$el.html()` from garbage collecting its events.
</span><span class="cx">                         this.options.$content.detach();
</span><span class="cx">
</span><del>-                        this.$el.html( this.template( this.controller.toJSON() ) );
-                        this.$('.media-modal-content').append( this.options.$content );
</del><ins>+                        this.$el.html( this.template({
+                                title: this.options.title
+                        }) );
+
+                        this.options.$content.addClass('media-modal-content');
+                        this.$('.media-modal').append( this.options.$content );
</ins><span class="cx">                         return this;
</span><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 attach: function() {
</span><span class="cx">                         this.$el.appendTo( this.options.container );
</span><span class="cx">                         this.controller.trigger( 'attach', this.controller );
</span><ins>+                        return this;
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 detach: function() {
</span><span class="cx">                         this.$el.detach();
</span><span class="cx">                         this.controller.trigger( 'detach', this.controller );
</span><ins>+                        return this;
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 open: function() {
</span><span class="cx">                         this.$el.show();
</span><span class="cx">                         this.controller.trigger( 'open', this.controller );
</span><ins>+                        return this;
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 close: function() {
</span><span class="cx">                         this.$el.hide();
</span><span class="cx">                         this.controller.trigger( 'close', this.controller );
</span><ins>+                        return this;
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 closeHandler: function( event ) {
</span><span class="lines">@@ -256,6 +465,103 @@
</span><span class="cx">                 }
</span><span class="cx">         });
</span><span class="cx">
</span><ins>+        // wp.media.view.UploaderWindow
+        // ----------------------------
+        media.view.UploaderWindow = Backbone.View.extend({
+                tagName: 'div',
+                className: 'uploader-window',
+                template: media.template('uploader-window'),
+
+                initialize: function() {
+                        var uploader;
+
+                        this.controller = this.options.controller;
+
+                        uploader = this.options.uploader = _.defaults( this.options.uploader || {}, {
+                                container: this.$el,
+                                dropzone: this.$el,
+                                browser: this.$('.upload-attachments a'),
+                                params: {}
+                        });
+
+                        // Track uploading attachments.
+                        wp.Uploader.queue.on( 'add remove reset change:percent', this.renderUploadProgress, this );
+
+                        if ( uploader.dropzone ) {
+                                // Ensure the dropzone is a jQuery collection.
+                                if ( ! (uploader.dropzone instanceof $) )
+                                        uploader.dropzone = $( uploader.dropzone );
+
+                                // Attempt to initialize the uploader whenever the dropzone is hovered.
+                                uploader.dropzone.one( 'mouseenter dragenter', _.bind( this.maybeInitUploader, this ) );
+                        }
+                },
+
+                render: function() {
+                        this.maybeInitUploader();
+                        this.renderUploadProgress();
+                        this.$el.html( this.template( this.options ) );
+                        this.$bar = this.$('.upload-attachments .media-progress-bar div');
+                        return this;
+                },
+
+                maybeInitUploader: function() {
+                        var $id, dropzone;
+
+                        // If the uploader already exists or the body isn't in the DOM, bail.
+                        if ( this.uploader || ! this.$el.closest('body').length )
+                                return;
+
+                        $id = $('#post_ID');
+                        if ( $id.length )
+                                this.options.uploader.params.post_id = $id.val();
+
+                        this.uploader = new wp.Uploader( this.options.uploader );
+
+                        dropzone = this.uploader.dropzone;
+                        dropzone.on( 'dropzone:enter', _.bind( this.show, this ) );
+                        dropzone.on( 'dropzone:leave', _.bind( this.hide, this ) );
+                },
+
+                show: function() {
+                        var $el = this.$el.show();
+
+                        // Ensure that the animation is triggered by waiting until
+                        // the transparent element is painted into the DOM.
+                        _.defer( function() {
+                                $el.css({ opacity: 1 });
+                        });
+                },
+
+                hide: function() {
+                        var $el = this.$el.css({ opacity: 0 });
+
+                        media.transition( $el ).done( function() {
+                                // Transition end events are subject to race conditions.
+                                // Make sure that the value is set as intended.
+                                if ( '0' === $el.css('opacity') )
+                                        $el.hide();
+                        });
+                },
+
+                renderUploadProgress: function() {
+                        var queue = wp.Uploader.queue;
+
+                        this.$el.toggleClass( 'uploading', !! queue.length );
+
+                        if ( ! this.$bar || ! queue.length )
+                                return;
+
+                        this.$bar.width( ( queue.reduce( function( memo, attachment ) {
+                                if ( attachment.get('uploading') )
+                                        return memo + ( attachment.get('percent') || 0 );
+                                else
+                                        return memo + 100;
+                        }, 0 ) / queue.length ) + '%' );
+                }
+        });
+
+
</ins><span class="cx">         /**
</span><span class="cx">          * wp.media.view.Toolbar
</span><span class="cx">          */
</span><span class="lines">@@ -264,16 +570,14 @@
</span><span class="cx">                 className: 'media-toolbar',
</span><span class="cx">
</span><span class="cx">                 initialize: function() {
</span><del>-                        this._views = {};
</del><ins>+                        this.controller = this.options.controller;
+
+                        this._views = {};
</ins><span class="cx">                         this.$primary = $('<div class="media-toolbar-primary" />').prependTo( this.$el );
</span><span class="cx">                         this.$secondary = $('<div class="media-toolbar-secondary" />').prependTo( this.$el );
</span><span class="cx">
</span><del>-                        if ( this.options.items ) {
-                                _.each( this.options.items, function( view, id ) {
-                                        this.add( id, view, { silent: true } );
-                                }, this );
-                                this.render();
-                        }
</del><ins>+                        if ( this.options.items )
+                                this.add( this.options.items, { silent: true }).render();
</ins><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 render: function() {
</span><span class="lines">@@ -293,11 +597,21 @@
</span><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 add: function( id, view, options ) {
</span><ins>+                        // Accept an object with an `id` : `view` mapping.
+                        if ( _.isObject( id ) ) {
+                                _.each( id, function( view, id ) {
+                                        this.add( id, view, options );
+                                }, this );
+                                return this;
+                        }
+
</ins><span class="cx">                         if ( ! ( view instanceof Backbone.View ) ) {
</span><span class="cx">                                 view.classes = [ id ].concat( view.classes || [] );
</span><span class="cx">                                 view = new media.view.Button( view ).render();
</span><span class="cx">                         }
</span><span class="cx">
</span><ins>+                        view.controller = view.controller || this.controller;
+
</ins><span class="cx">                         this._views[ id ] = view;
</span><span class="cx">                         if ( ! options || ! options.silent )
</span><span class="cx">                                 this.render();
</span><span class="lines">@@ -316,7 +630,132 @@
</span><span class="cx">                 }
</span><span class="cx">         });
</span><span class="cx">
</span><ins>+        // wp.media.view.Toolbar.PostLibrary
+        // ---------------------------------
+        media.view.Toolbar.PostLibrary = media.view.Toolbar.extend({
+                initialize: function() {
+                        var selection = this.options.selection,
+                                controller = this.options.controller;
</ins><span class="cx">
</span><ins>+                        this.options.items = {
+                                'selection-preview': new media.view.SelectionPreview({
+                                        controller: controller,
+                                        collection: selection,
+                                        priority: -40
+                                }),
+
+                                'create-new-gallery': {
+                                        style: 'primary',
+                                        text: l10n.createNewGallery,
+                                        priority: 40,
+
+                                        click: function() {
+                                                this.controller.state('gallery');
+                                        }
+                                },
+
+                                'insert-into-post': new media.view.ButtonGroup({
+                                        priority: 30,
+                                        classes: 'dropdown-flip-x',
+                                        buttons: [
+                                                {
+                                                        text: l10n.insertIntoPost,
+                                                        click: function() {
+                                                                controller.close();
+                                                                controller.state().trigger( 'insert', selection );
+                                                                selection.clear();
+                                                        }
+                                                },
+                                                {
+                                                        classes: ['down-arrow'],
+                                                        dropdown: new media.view.AttachmentDisplaySettings().render().$el,
+
+                                                        click: function( event ) {
+                                                                var $el = this.$el;
+
+                                                                if ( ! $( event.target ).closest('.dropdown').length )
+                                                                        $el.toggleClass('active');
+
+                                                                // Stop the event from propagating further so we can bind
+                                                                // a one-time event to the body (and ensure that a click
+                                                                // on the dropdown won't trigger said event).
+                                                                event.stopPropagation();
+
+                                                                if ( $el.is(':visible') ) {
+                                                                        $(document.body).one( 'click', function() {
+                                                                                $el.removeClass('active');
+                                                                        });
+                                                                }
+                                                        }
+                                                }
+                                        ]
+                                }).render(),
+
+                                'add-to-gallery': {
+                                        text: l10n.addToGallery,
+                                        priority: 20
+                                }
+                        };
+
+                        media.view.Toolbar.prototype.initialize.apply( this, arguments );
+                },
+
+                visibility: function() {
+                        var selection = this.options.selection,
+                                controller = this.options.controller,
+                                count = selection.length,
+                                showGallery;
+
+                        controller.$el.toggleClass( 'hide-toolbar', ! count );
+
+                        // Check if every attachment in the selection is an image.
+                        showGallery = count > 1 && selection.all( function( attachment ) {
+                                return 'image' === attachment.get('type');
+                        });
+
+                        this.get('create-new-gallery').$el.toggle( showGallery );
+                        insert = this.get('insert-into-post');
+                        _.each( insert.buttons, function( button ) {
+                                button.model.set( 'style', showGallery ? '' : 'primary' );
+                        });
+                }
+        });
+
+        // wp.media.view.Toolbar.Gallery
+        // -----------------------------
+        media.view.Toolbar.Gallery = media.view.Toolbar.extend({
+                initialize: function() {
+                        var editing = this.options.editing,
+                                selection = this.options.selection,
+                                controller = this.options.controller;
+
+                        this.options.items = {
+                                'update-gallery': {
+                                        style: 'primary',
+                                        text: editing ? l10n.updateGallery : l10n.insertGalleryIntoPost,
+                                        priority: 40,
+                                        click: function() {
+                                                controller.close();
+                                                controller.state().trigger( 'update', selection );
+                                                selection.clear();
+                                                controller.state('library');
+                                        }
+                                },
+
+                                'return-to-library': {
+                                        text: editing ? l10n.addImagesFromLibrary : l10n.returnToLibrary,
+                                        priority: -40,
+
+                                        click: function() {
+                                                this.controller.state('library');
+                                        }
+                                }
+                        };
+
+                        media.view.Toolbar.prototype.initialize.apply( this, arguments );
+                }
+        });
+
</ins><span class="cx">         /**
</span><span class="cx">          * wp.media.view.Button
</span><span class="cx">          */
</span><span class="lines">@@ -459,7 +898,7 @@
</span><span class="cx">                                 });
</span><span class="cx">
</span><span class="cx">                         options.buttons = this.buttons;
</span><del>-                        options.describe = this.controller.get('describe');
</del><ins>+                        options.describe = this.controller.state().get('describe');
</ins><span class="cx">
</span><span class="cx">                         if ( 'image' === options.type )
</span><span class="cx">                                 _.extend( options, this.crop() );
</span><span class="lines">@@ -472,7 +911,7 @@
</span><span class="cx">                                 delete this.$bar;
</span><span class="cx">
</span><span class="cx">                         // Check if the model is selected.
</span><del>-                        if ( this.controller.selection.has( this.model ) )
</del><ins>+                        if ( this.selected() )
</ins><span class="cx">                                 this.select();
</span><span class="cx">
</span><span class="cx">                         return this;
</span><span class="lines">@@ -484,23 +923,39 @@
</span><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 toggleSelection: function( event ) {
</span><del>-                        var selection = this.controller.selection;
</del><ins>+                        var selection = this.controller.state().get('selection');
+
+                        if ( ! selection )
+                                return;
+
</ins><span class="cx">                         selection[ selection.has( this.model ) ? 'remove' : 'add' ]( this.model );
</span><span class="cx">                 },
</span><span class="cx">
</span><ins>+                selected: function() {
+                        var selection = this.controller.state().get('selection');
+                        if ( selection )
+                                return selection.has( this.model );
+                },
+
</ins><span class="cx">                 select: function( model, collection ) {
</span><del>-                        // If a collection is provided, check if it's the selection.
-                        // If it's not, bail; we're in another selection's event loop.
-                        if ( collection && collection !== this.controller.selection )
</del><ins>+                        var selection = this.controller.state().get('selection');
+
+                        // Check if a selection exists and if it's the collection provided.
+                        // If they're not the same collection, bail; we're in another
+                        // selection's event loop.
+                        if ( ! selection || ( collection && collection !== selection ) )
</ins><span class="cx">                                 return;
</span><span class="cx">
</span><span class="cx">                         this.$el.addClass('selected');
</span><span class="cx">                 },
</span><span class="cx">
</span><span class="cx">                 deselect: function( model, collection ) {
</span><del>-                        // If a collection is provided, check if it's the selection.
-                        // If it's not, bail; we're in another selection's event loop.
-                        if ( collection && collection !== this.controller.selection )
</del><ins>+                        var selection = this.controller.state().get('selection');
+
+                        // Check if a selection exists and if it's the collection provided.
+                        // If they're not the same collection, bail; we're in another
+                        // selection's event loop.
+                        if ( ! selection || ( collection && collection !== selection ) )
</ins><span class="cx">                                 return;
</span><span class="cx">
</span><span class="cx">                         this.$el.removeClass('selected');
</span><span class="lines">@@ -610,298 +1065,6 @@
</span><span class="cx">         });
</span><span class="cx">
</span><span class="cx">         /**
</span><del>-         * wp.media.view.Workspace
-         */
-        media.view.Workspace = Backbone.View.extend({
-                tagName: 'div',
-                className: 'media-workspace',
-                template: media.template('media-workspace'),
-
-                // The `options` to be passed to `Attachments` view.
-                attachmentsView: {},
-
-                events: {
-                        'dragenter': 'maybeInitUploader',
-                        'mouseenter': 'maybeInitUploader'
-                },
-
-                initialize: function() {
-                        this.controller = this.options.controller;
-
-                        _.defaults( this.options, {
-                                selectOne: false,
-                                uploader: {},
-                                attachmentsView: {}
-                        });
-
-                        this.$content = $('<div class="existing-attachments" />');
-
-                        // Generate the `options` passed to the `Attachments` view.
-                        // Order of priority from lowest to highest: the provided defaults,
-                        // the prototypal `attachmentsView` property, the `attachmentsView`
-                        // option for the current instance, and then the `controller` and
-                        // `collection` keys, to ensure they're correctly set.
-                        this.attachmentsView = _.extend( {
-                                directions: this.controller.get('multiple') ? l10n.selectMediaMultiple : l10n.selectMediaSingular
-                        }, this.attachmentsView, this.options.attachmentsView, {
-                                controller: this.controller,
-                                collection: this.collection
-                        });
-
-                        // Initialize the `Attachments` view.
-                        this.attachmentsView = new media.view.Attachments( this.attachmentsView );
-                        this.$content.append( this.attachmentsView.$el );
-
-                        // Track uploading attachments.
-                        wp.Uploader.queue.on( 'add remove reset change:percent', this.renderUploadProgress, this );
-
-                        // If we're in a workflow that supports multiple attachments,
-                        // automatically select any uploading attachments.
-                        if ( this.controller.get('multiple') )
-                                wp.Uploader.queue.on( 'add', this.selectUpload, this );
-                },
-
-                render: function() {
-                        this.$content.detach();
-
-                        this.attachmentsView.render();
-                        this.renderUploadProgress();
-                        this.$el.html( this.template( this.options ) ).append( this.$content );
-                        this.$bar = this.$('.upload-attachments .media-progress-bar div');
-                        return this;
-                },
-
-                maybeInitUploader: function() {
-                        var workspace = this,
-                                params = {},
-                                $id;
-
-                        // If the uploader already exists or the body isn't in the DOM, bail.
-                        if ( this.uploader || ! this.$el.closest('body').length )
-                                return;
-
-                        $id = $('#post_ID');
-                        if ( $id.length )
-                                params.post_id = $id.val();
-
-                        this.uploader = new wp.Uploader( _.extend({
-                                container: this.$el,
-                                dropzone: this.$el,
-                                browser: this.$('.upload-attachments a'),
-                                params: params
-                        }, this.options.uploader ) );
-                },
-
-                selectUpload: function( attachment ) {
-                        this.controller.selection.add( attachment );
-                },
-
-                renderUploadProgress: function() {
-                        var queue = wp.Uploader.queue;
-
-                        this.$el.toggleClass( 'uploading', !! queue.length );
-
-                        if ( ! this.$bar || ! queue.length )
-                                return;
-
-                        this.$bar.width( ( queue.reduce( function( memo, attachment ) {
-                                if ( attachment.get('uploading') )
-                                        return memo + ( attachment.get('percent') || 0 );
-                                else
-                                        return memo + 100;
-                        }, 0 ) / queue.length ) + '%' );
-                }
-        });
-
-        /**
-         * wp.media.view.Workspace.Library
-         */
-        media.view.Workspace.Library = media.view.Workspace.extend({
-
-                attachmentsView: {
-                        // The single `Attachment` view to be used in the `Attachments` view.
-                        AttachmentView: media.view.Attachment.Library
-                },
-
-                initialize: function() {
-                        media.view.Workspace.prototype.initialize.apply( this, arguments );
-
-                        // If this supports multiple attachments, initialize the sample toolbar view.
-                        if ( this.controller.get('multiple') )
-                                this.initToolbarView();
-                },
-
-                // Initializes the toolbar view. Currently uses defaults set for
-                // inserting media into a post. This should be pulled out into the
-                // appropriate workflow when the time comes, but is currently here
-                // to test multiple selections.
-                initToolbarView: function() {
-                        var controller = this.controller;
-
-                        this.toolbarView = new media.view.Toolbar({
-                                items: {
-                                        'selection-preview': new media.view.SelectionPreview({
-                                                controller: this.controller,
-                                                collection: this.controller.selection,
-                                                priority: -40
-                                        }),
-
-                                        'create-new-gallery': {
-                                                style: 'primary',
-                                                text: l10n.createNewGallery,
-                                                priority: 40,
-
-                                                click: function() {
-                                                        controller.render('gallery');
-                                                }
-                                        },
-
-                                        'insert-into-post': new media.view.ButtonGroup({
-                                                priority: 30,
-                                                classes: 'dropdown-flip-x',
-                                                buttons: [
-                                                        {
-                                                                text: l10n.insertIntoPost,
-                                                                click: _.bind( controller.update, controller, 'insert' )
-                                                        },
-                                                        {
-                                                                classes: ['down-arrow'],
-                                                                dropdown: new media.view.AttachmentDisplaySettings().render().$el,
-
-                                                                click: function( event ) {
-                                                                        var $el = this.$el;
-
-                                                                        if ( ! $( event.target ).closest('.dropdown').length )
-                                                                                $el.toggleClass('active');
-
-                                                                        // Stop the event from propagating further so we can bind
-                                                                        // a one-time event to the body (and ensure that a click
-                                                                        // on the dropdown won't trigger said event).
-                                                                        event.stopPropagation();
-
-                                                                        if ( $el.is(':visible') ) {
-                                                                                $(document.body).one( 'click', function() {
-                                                                                        $el.removeClass('active');
-                                                                                });
-                                                                        }
-                                                                }
-                                                        }
-                                                ]
-                                        }).render(),
-
-                                        'add-to-gallery': {
-                                                text: l10n.addToGallery,
-                                                priority: 20
-                                        }
-                                }
-                        });
-
-                        this.controller.selection.on( 'add remove', function() {
-                                var count = this.controller.selection.length,
-                                        showGallery;
-
-                                this.$el.toggleClass( 'with-toolbar', !! count );
-
-                                // Check if every attachment in the selection is an image.
-                                showGallery = count > 1 && this.controller.selection.all( function( attachment ) {
-                                        return 'image' === attachment.get('type');
-                                });
-
-                                this.toolbarView.get('create-new-gallery').$el.toggle( showGallery );
-                                insert = this.toolbarView.get('insert-into-post');
-                                _.each( insert.buttons, function( button ) {
-                                        button.model.set( 'style', showGallery ? '' : 'primary' );
-                                });
-                        }, this );
-
-                        this.$content.append( this.toolbarView.$el );
-                }
-        });
-
-        media.view.Workspace.Library.Gallery = media.view.Workspace.Library.extend({
-                initToolbarView: function() {
-                        var controller = this.controller,
-                                editing = controller.get('editing'),
-                                items = {
-                                        'selection-preview': new media.view.SelectionPreview({
-                                                controller: this.controller,
-                                                collection: this.controller.selection,
-                                                priority: -40,
-                                                clearable: false
-                                        }),
-
-                                        'continue-editing-gallery': {
-                                                style: 'primary',
-                                                text: l10n.continueEditingGallery,
-                                                priority: 40,
-
-                                                click: function() {
-                                                        controller.render( 'gallery' );
-                                                }
-                                        }
-                                };
-
-                        this.toolbarView = new media.view.Toolbar({
-                                items: items
-                        });
-
-                        this.$el.addClass('with-toolbar');
-                        this.$content.append( this.toolbarView.$el );
-                }
-        });
-
-        /**
-         * wp.media.view.Workspace.Gallery
-         */
-        media.view.Workspace.Gallery = media.view.Workspace.extend({
-
-                attachmentsView: {
-                        // The single `Attachment` view to be used in the `Attachments` view.
-                        AttachmentView: media.view.Attachment.Gallery,
-                        sortable: true
-                },
-
-                initialize: function() {
-                        media.view.Workspace.prototype.initialize.apply( this, arguments );
-                        this.initToolbarView();
-                },
-
-                // Initializes the toolbar view. Currently uses defaults set for
-                // inserting media into a post. This should be pulled out into the
-                // appropriate workflow when the time comes, but is currently here
-                // to test multiple selections.
-                initToolbarView: function() {
-                        var controller = this.controller,
-                                editing = controller.get('editing'),
-                                items = {
-                                        'update-gallery': {
-                                                style: 'primary',
-                                                text: editing ? l10n.updateGallery : l10n.insertGalleryIntoPost,
-                                                priority: 40,
-                                                click: _.bind( controller.update, controller, 'gallery' )
-                                        },
-
-                                        'return-to-library': {
-                                                text: editing ? l10n.addImagesFromLibrary : l10n.returnToLibrary,
-                                                priority: -40,
-
-                                                click: function() {
-                                                        controller.render( editing ? 'gallery-library' : 'library' );
-                                                }
-                                        }
-                                };
-
-                        this.toolbarView = new media.view.Toolbar({
-                                items: items
-                        });
-
-                        this.$el.addClass('with-toolbar');
-                        this.$content.append( this.toolbarView.$el );
-                }
-        });
-
-
-        /**
</del><span class="cx">          * wp.media.view.Attachments
</span><span class="cx">          */
</span><span class="cx">         media.view.Attachments = Backbone.View.extend({
</span></span></pre></div>
<a id="trunkwpincludesjspluploadwppluploadjs"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/js/plupload/wp-plupload.js (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/js/plupload/wp-plupload.js        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-includes/js/plupload/wp-plupload.js        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -114,7 +114,7 @@
</span><span class="cx">                                 if ( active )
</span><span class="cx">                                         return;
</span><span class="cx">
</span><del>-                                dropzone.addClass('drag-over');
</del><ins>+                                dropzone.trigger('dropzone:enter').addClass('drag-over');
</ins><span class="cx">                                 active = true;
</span><span class="cx">                         });
</span><span class="cx">
</span><span class="lines">@@ -126,7 +126,7 @@
</span><span class="cx">                                 // See http://core.trac.wordpress.org/ticket/21705
</span><span class="cx">                                 timer = setTimeout( function() {
</span><span class="cx">                                         active = false;
</span><del>-                                        dropzone.removeClass('drag-over');
</del><ins>+                                        dropzone.trigger('dropzone:leave').removeClass('drag-over');
</ins><span class="cx">                                 }, 0 );
</span><span class="cx">                         });
</span><span class="cx">                 }( this.dropzone, this.supports.dragdrop ));
</span></span></pre></div>
<a id="trunkwpincludesmediaphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/media.php (22319 => 22320)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/media.php        2012-10-28 16:17:56 UTC (rev 22319)
+++ trunk/wp-includes/media.php        2012-10-28 23:29:17 UTC (rev 22320)
</span><span class="lines">@@ -1299,24 +1299,13 @@
</span><span class="cx">                 <div class="media-modal">
</span><span class="cx">                         <h3 class="media-modal-title"><%- title %></h3>
</span><span class="cx">                         <a class="media-modal-close" href="" title="<?php esc_attr_e('Close'); ?>">&times;</a>
</span><del>-                        <div class="media-modal-content"></div>
</del><span class="cx">                 </div>
</span><span class="cx">                 <div class="media-modal-backdrop"></div>
</span><span class="cx">         </script>
</span><span class="cx">
</span><del>-        <script type="text/html" id="tmpl-media-workspace">
-                <div class="upload-attachments">
-                        <% if ( selectOne ) { %>
-                                <h3><?php _e( 'Drop a file here' ); ?></h3>
-                                <span><?php _ex( 'or', 'Uploader: Drop a file here - or - Select a File' ); ?></span>
-                                <a href="#" class="button-secondary"><?php _e( 'Select a File' ); ?></a>
-                        <% } else { %>
-                                <h3><?php _e( 'Drop files here' ); ?></h3>
-                                <span><?php _ex( 'or', 'Uploader: Drop files here - or - Select Files' ); ?></span>
-                                <a href="#" class="button-secondary"><?php _e( 'Select Files' ); ?></a>
-                        <% } %>
-
-                        <div class="media-progress-bar"><div></div></div>
</del><ins>+        <script type="text/html" id="tmpl-uploader-window">
+                <div class="uploader-window-content">
+                        <h3><?php _e( 'Drop files here to upload' ); ?></h3>
</ins><span class="cx">                 </div>
</span><span class="cx">         </script>
</span><span class="cx">
</span></span></pre>
</div>
</div>
</body>
</html>