<!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>[21683] trunk: Add new media workflow scripts, styles, and templates.</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/21683">21683</a></dd>
<dt>Author</dt> <dd>koopersmith</dd>
<dt>Date</dt> <dd>2012-08-31 04:54:23 +0000 (Fri, 31 Aug 2012)</dd>
</dl>

<h3>Log Message</h3>
<pre>Add new media workflow scripts, styles, and templates.


Please note that this commit does not integrate media into the existing UI. If you would like to see the new UI, navigate to the post editor and run the following in your browser's Javascript console:

        new wp.media.controller.Workflow().render().modal.open();


The Javascript is broken up into two files, with the slugs media-models and media-views.

* media-models: The models are UI agnostic, and can be used independent of the views. If you'd like to create custom UIs, this is the script for you.

* media-views: This is the Media Experience. The views (and controllers) depend on the models (which are listed as a dependency and will automatically be included thanks to wp_enqueue_script). The views also require the media templates, media-view styles, and the plupload bridge settings. Perhaps we should create a function to include the whole shebang, but in the meantime...

To include media-views in the admin, run the following PHP in or after 'admin_enqueue_scripts':

        wp_enqueue_script( 'media-views' );
        wp_enqueue_style( 'media-views' );
        wp_plupload_default_settings();
        add_action( 'admin_footer', 'wp_print_media_templates' );

see <a href="http://core.trac.wordpress.org/ticket/21390">#21390</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkwpadmineditformadvancedphp">trunk/wp-admin/edit-form-advanced.php</a></li>
<li><a href="#trunkwpincludesmediaphp">trunk/wp-includes/media.php</a></li>
<li><a href="#trunkwpincludesscriptloaderphp">trunk/wp-includes/script-loader.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkwpincludescssmediaviewscss">trunk/wp-includes/css/media-views.css</a></li>
<li><a href="#trunkwpincludescssmediaviewsmincss">trunk/wp-includes/css/media-views.min.css</a></li>
<li><a href="#trunkwpincludesjsmediamodelsjs">trunk/wp-includes/js/media-models.js</a></li>
<li><a href="#trunkwpincludesjsmediamodelsminjs">trunk/wp-includes/js/media-models.min.js</a></li>
<li><a href="#trunkwpincludesjsmediaviewsjs">trunk/wp-includes/js/media-views.js</a></li>
<li><a href="#trunkwpincludesjsmediaviewsminjs">trunk/wp-includes/js/media-views.min.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkwpadmineditformadvancedphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-admin/edit-form-advanced.php (21682 => 21683)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-admin/edit-form-advanced.php        2012-08-31 04:32:25 UTC (rev 21682)
+++ trunk/wp-admin/edit-form-advanced.php        2012-08-31 04:54:23 UTC (rev 21683)
</span><span class="lines">@@ -18,6 +18,10 @@
</span><span class="cx"> if ( post_type_supports($post_type, 'editor') || post_type_supports($post_type, 'thumbnail') ) {
</span><span class="cx">         add_thickbox();
</span><span class="cx">         wp_enqueue_script('media-upload');
</span><ins>+        wp_enqueue_script( 'media-views' );
+        wp_enqueue_style( 'media-views' );
+        wp_plupload_default_settings();
+        add_action( 'admin_footer', 'wp_print_media_templates' );
</ins><span class="cx"> }
</span><span class="cx"> 
</span><span class="cx"> /**
</span></span></pre></div>
<a id="trunkwpincludescssmediaviewscss"></a>
<div class="addfile"><h4>Added: trunk/wp-includes/css/media-views.css (0 => 21683)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/css/media-views.css                                (rev 0)
+++ trunk/wp-includes/css/media-views.css        2012-08-31 04:54:23 UTC (rev 21683)
</span><span class="lines">@@ -0,0 +1,278 @@
</span><ins>+/**
+ * Modal
+ */
+.media-modal {
+        position: fixed;
+        top: 60px;
+        left: 60px;
+        right: 60px;
+        bottom: 60px;
+        background: #fff;
+        z-index: 125000;
+}
+
+.media-modal-backdrop {
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: #000;
+        opacity: 0.8;
+        z-index: 120000;
+}
+
+.media-modal-header {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        height: 28px;
+        background: #f1f1f1;
+}
+
+.media-modal-header h3 {
+        float: left;
+        padding: 0 0 0 10px;
+        margin: 0;
+        line-height: 28px;
+}
+
+.media-modal-close {
+        float: right;
+        padding-right: 10px;
+        line-height: 28px;
+}
+
+.media-modal-content {
+        position: absolute;
+        top: 28px;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        overflow: auto;
+}
+
+/**
+ * Workspace
+ */
+.media-workspace {
+        position: relative;
+        width: 100%;
+        height: 100%;
+}
+
+.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;
+}
+
+.existing-attachments {
+        position: absolute;
+        top: 0;
+        left: 200px;
+        right: 0;
+        bottom: 0;
+}
+
+/**
+ * Attachments
+ */
+.attachments {
+        width: 100%;
+        height: 100%;
+}
+
+.attachments-header {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        height: 50px;
+        background: #fff;
+}
+
+.attachments-header h3 {
+        float: left;
+        margin: 0;
+        padding: 0 0 0 10px;
+        line-height: 50px;
+        font-size: 18px;
+        font-weight: 200;
+}
+.attachments-header input {
+        float: right;
+        margin-top: 10px;
+        margin-right: 10px;
+}
+
+.attachments ul {
+        position: absolute;
+        top: 50px;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        overflow: auto;
+        margin: 0 10px 20px;
+}
+
+/**
+ * Attachment
+ */
+.attachment {
+        position: relative;
+        float: left;
+        width: 200px;
+        height: 200px;
+
+        padding: 0;
+        margin: 0 10px 20px;
+        border: 1px solid #dfdfdf;
+}
+
+.attachment:hover {
+        border-color: #d54e21;
+}
+
+.attachment.selected {
+        border-color: #21759b;
+}
+
+.attachment-thumbnail {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        overflow: hidden;
+}
+
+.attachment img {
+        display: block;
+        max-height: 200px;
+        max-width: 200px;
+
+        /* Vertically center the thumbnails. */
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        -webkit-transform: translate( -50%, -50% );
+        -moz-transform:    translate( -50%, -50% );
+        -ms-transform:     translate( -50%, -50% );
+        -o-transform:      translate( -50%, -50% );
+        transform:         translate( -50%, -50% );
+
+        margin: 0 auto;
+        box-shadow: inset 0 0 0 1px rgba( 0, 0, 0, 0.4 );
+}
+
+
+/* Square crop with overflow visible on hover. */
+/*
+.attachment .portrait img {
+        width: 200px;
+}
+.attachment .landscape img {
+        height: 200px;
+}
+.attachment .attachment-thumbnail:hover {
+        overflow: visible;
+        z-index: 1000;
+}
+.attachment .attachment-thumbnail:hover img {
+        border: 10px solid #fff;
+        box-shadow: 0 0 10px rgba( 0, 0, 0, 0.4 );
+}*/
+
+
+/* Square crop with resized image on hover. */
+/*
+.attachment .portrait img {
+        width: 200px;
+}
+.attachment .landscape img {
+        height: 200px;
+}
+.attachment .attachment-thumbnail:hover img {
+        height: auto;
+        width: auto;
+        max-height: 200px;
+        max-width: 200px;
+}*/
+
+
+/**
+ * Progress Bar
+ */
+.media-progress-bar {
+        position: relative;
+        height: 8px;
+        width: 70%;
+        margin: 10px auto;
+        padding: 2px;
+        border: 2px solid #dfdfdf;
+        border-radius: 8px;
+}
+
+.media-progress-bar div {
+        height: 8px;
+        min-width: 8px;
+        width: 0;
+        background: #dfdfdf;
+        border-radius: 10px;
+        -webkit-transition: width 200ms;
+        -moz-transition:    width 200ms;
+        -ms-transition:     width 200ms;
+        -o-transition:      width 200ms;
+        transition:         width 200ms;
+}
+
+.attachment-thumbnail .media-progress-bar {
+        position: absolute;
+        top: 50%;
+        left: 15%;
+        width: 70%;
+        margin: -8px 0 0 -4px;
+}
+
+.upload-attachments .media-progress-bar {
+        margin-top: 80px;
+        display: none;
+}
+
+.uploading .upload-attachments .media-progress-bar {
+        display: block;
+}
</ins><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkwpincludesjsmediamodelsjs"></a>
<div class="addfile"><h4>Added: trunk/wp-includes/js/media-models.js (0 => 21683)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/js/media-models.js                                (rev 0)
+++ trunk/wp-includes/js/media-models.js        2012-08-31 04:54:23 UTC (rev 21683)
</span><span class="lines">@@ -0,0 +1,410 @@
</span><ins>+if ( typeof wp === 'undefined' )
+        var wp = {};
+
+(function($){
+        var media = wp.media = { model: {}, view: {}, controller: {} },
+                Attachment, Attachments, Query;
+
+        /**
+         * ========================================================================
+         * UTILITIES
+         * ========================================================================
+         */
+
+        _.extend( media, {
+                /**
+                 * media.template( id )
+                 *
+                 * Fetches a template by id.
+                 *
+                 * @param  {string} id   A string that corresponds to a DOM element with an id prefixed with &quot;tmpl-&quot;.
+                 *                       For example, &quot;attachment&quot; maps to &quot;tmpl-attachment&quot;.
+                 * @return {function}    A function that lazily-compiles the template requested.
+                 */
+                template: _.memoize( function( id ) {
+                        var compiled;
+                        return function( data ) {
+                                compiled = compiled || _.template( $( '#tmpl-' + id ).html() );
+                                return compiled( data );
+                        };
+                }),
+
+                /**
+                 * media.post( [action], [data] )
+                 *
+                 * Sends a POST request to WordPress.
+                 *
+                 * @param  {string} action The slug of the action to fire in WordPress.
+                 * @param  {object} data   The data to populate $_POST with.
+                 * @return {$.promise}     A jQuery promise that represents the request.
+                 */
+                post: function( action, data ) {
+                        return media.ajax({
+                                data: _.isObject( action ) ? action : _.extend( data || {}, { action: action })
+                        });
+                },
+
+                /**
+                 * media.ajax( [action], [options] )
+                 *
+                 * Sends a POST request to WordPress.
+                 *
+                 * @param  {string} action  The slug of the action to fire in WordPress.
+                 * @param  {object} options The options passed to jQuery.ajax.
+                 * @return {$.promise}      A jQuery promise that represents the request.
+                 */
+                ajax: function( action, options ) {
+                        if ( _.isObject( action ) ) {
+                                options = action;
+                        } else {
+                                options = options || {};
+                                options.data = _.extend( options.data || {}, { action: action });
+                        }
+
+                        options = _.defaults( options || {}, {
+                                type:    'POST',
+                                url:     ajaxurl,
+                                context: this
+                        });
+
+                        return $.Deferred( function( deferred ) {
+                                // Transfer success/error callbacks.
+                                if ( options.success )
+                                        deferred.done( options.success );
+                                if ( options.error )
+                                        deferred.fail( options.error );
+
+                                delete options.success;
+                                delete options.error;
+
+                                // Use with PHP's wp_send_json_success() and wp_send_json_error()
+                                $.ajax( options ).done( function( response ) {
+                                        if ( _.isObject( response ) &amp;&amp; ! _.isUndefined( response.success ) )
+                                                deferred[ response.success ? 'resolveWith' : 'rejectWith' ]( this, [response.data] );
+                                        else
+                                                deferred.rejectWith( this, [response] );
+                                }).fail( function() {
+                                        deferred.rejectWith( this, arguments );
+                                });
+                        }).promise();
+                }
+        });
+
+
+        /**
+         * ========================================================================
+         * MODELS
+         * ========================================================================
+         */
+
+        /**
+         * wp.media.model.Attachment
+         */
+        Attachment = media.model.Attachment = Backbone.Model.extend({
+                sync: function( method, model, options ) {
+                        // Overload the read method so Attachment.fetch() functions correctly.
+                        if ( 'read' === method ) {
+                                options = options || {};
+                                options.context = this;
+                                options.data = _.extend( options.data || {}, {
+                                        action: 'get-attachment',
+                                        id: this.id
+                                });
+                                return media.ajax( options );
+
+                        // Otherwise, fall back to Backbone.sync()
+                        } else {
+                                return Backbone.sync.apply( this, arguments );
+                        }
+                },
+
+                parse: function( resp, xhr ) {
+                        // Convert date strings into Date objects.
+                        resp.date = new Date( resp.date );
+                        resp.modified = new Date( resp.modified );
+                        return resp;
+                }
+        }, {
+                create: function( attrs ) {
+                        return Attachments.all.push( attrs );
+                },
+
+                get: _.memoize( function( id, attachment ) {
+                        return Attachments.all.push( attachment || { id: id } );
+                })
+        });
+
+        /**
+         * wp.media.model.Attachments
+         */
+        Attachments = media.model.Attachments = Backbone.Collection.extend({
+                model: Attachment,
+
+                initialize: function( models, options ) {
+                        options = options || {};
+
+                        this.filters = options.filters || {};
+
+                        if ( options.watch )
+                                this.watch( options.watch );
+
+                        if ( options.mirror )
+                                this.mirror( options.mirror );
+                },
+
+                validate: function( attachment ) {
+                        return _.all( this.filters, function( filter ) {
+                                return !! filter.call( this, attachment );
+                        }, this );
+                },
+
+                changed: function( attachment, options ) {
+
+                        if ( this.validate( attachment ) )
+                                this.add( attachment );
+                        else
+                                this.remove( attachment );
+                        return this;
+                },
+
+                watch: function( attachments ) {
+                        attachments.on( 'add change', this.changed, this );
+                },
+
+                unwatch: function( attachments ) {
+                        attachments.off( 'add change', this.changed, this );
+                },
+
+                mirror: function( attachments ) {
+                        if ( this.mirroring &amp;&amp; this.mirroring === attachments )
+                                return;
+
+                        this.unmirror();
+                        this.mirroring = attachments;
+                        this.reset( attachments.models );
+                        attachments.on( 'add',    this._mirrorAdd,    this );
+                        attachments.on( 'remove', this._mirrorRemove, this );
+                        attachments.on( 'reset',  this._mirrorReset,  this );
+                },
+
+                unmirror: function() {
+                        if ( ! this.mirroring )
+                                return;
+
+                        this.mirroring.off( 'add',    this._mirrorAdd,    this );
+                        this.mirroring.off( 'remove', this._mirrorRemove, this );
+                        this.mirroring.off( 'reset',  this._mirrorReset,  this );
+                        delete this.mirroring;
+                },
+
+                _mirrorAdd: function( attachment, attachments, options ) {
+                        this.add( attachment, { at: options.index });
+                },
+
+                _mirrorRemove: function( attachment ) {
+                        this.remove( attachment );
+                },
+
+                _mirrorReset: function( attachments ) {
+                        this.reset( attachments.models );
+                },
+
+                more: function( options ) {
+                        if ( this.mirroring &amp;&amp; this.mirroring.more )
+                                return this.mirroring.more( options );
+                },
+
+                parse: function( resp, xhr ) {
+                        return _.map( resp, function( attrs ) {
+                                var attachment = Attachment.get( attrs.id );
+                                return attachment.set( attachment.parse( attrs, xhr ) );
+                        });
+                }
+        });
+
+        Attachments.all = new Attachments();
+
+        /**
+         * wp.media.query
+         */
+        media.query = (function(){
+                var queries = [];
+
+                return function( args, options ) {
+                        args = _.defaults( args || {}, Query.defaultArgs );
+
+                        var query = _.find( queries, function( query ) {
+                                return _.isEqual( query.args, args );
+                        });
+
+                        if ( ! query ) {
+                                query = new Query( [], _.extend( options || {}, { args: args } ) );
+                                queries.push( query );
+                        }
+
+                        return query;
+                };
+        }());
+
+        /**
+         * wp.media.model.Query
+         *
+         * A set of attachments that corresponds to a set of consecutively paged
+         * queries on the server.
+         *
+         * Note: Do NOT change this.args after the query has been initialized.
+         *       Things will break.
+         */
+        Query = media.model.Query = Attachments.extend({
+                initialize: function( models, options ) {
+                        var orderby,
+                                defaultArgs = Query.defaultArgs;
+
+                        options = options || {};
+                        Attachments.prototype.initialize.apply( this, arguments );
+
+                        // Generate this.args. Don't mess with them.
+                        this.args = _.defaults( options.args || {}, defaultArgs );
+
+                        // Normalize the order.
+                        this.args.order = this.args.order.toUpperCase();
+                        if ( 'DESC' !== this.args.order &amp;&amp; 'ASC' !== this.args.order )
+                                this.args.order = defaultArgs.order.toUpperCase();
+
+                        // Set allowed orderby values.
+                        // These map directly to attachment keys in most scenarios.
+                        // Exceptions are specified in orderby.keymap.
+                        orderby = {
+                                allowed: [ 'name', 'author', 'date', 'title', 'modified', 'parent', 'ID' ],
+                                keymap:  {
+                                        'ID':     'id',
+                                        'parent': 'uploadedTo'
+                                }
+                        };
+
+                        if ( ! _.contains( orderby.allowed, this.args.orderby ) )
+                                this.args.orderby = defaultArgs.orderby;
+                        this.orderkey = orderby.keymap[ this.args.orderby ] || this.args.orderby;
+
+                        this.hasMore = true;
+                        this.created = new Date();
+
+                        this.filters.order = function( attachment ) {
+                                // We want any items that can be placed before the last
+                                // item in the set. If we add any items after the last
+                                // item, then we can't guarantee the set is complete.
+                                if ( this.length ) {
+                                        return 1 !== this.comparator( attachment, this.last() );
+
+                                // Handle the case where there are no items yet and
+                                // we're sorting for recent items. In that case, we want
+                                // changes that occurred after we created the query.
+                                } else if ( 'DESC' === this.args.order &amp;&amp; ( 'date' === this.orderkey || 'modified' === this.orderkey ) ) {
+                                        return attachment.get( this.orderkey ) &gt;= this.created;
+                                }
+
+                                // Otherwise, we don't want any items yet.
+                                return false;
+                        };
+
+                        if ( this.args.s ) {
+                                // Note that this client-side searching is *not* equivalent
+                                // to our server-side searching.
+                                this.filters.search = function( attachment ) {
+                                        return _.any(['title','filename','description','caption','name'], function( key ) {
+                                                var value = attachment.get( key );
+                                                return value &amp;&amp; -1 !== value.search( this.args.s );
+                                        }, this );
+                                };
+                        }
+
+                        this.watch( Attachments.all );
+                },
+
+                more: function( options ) {
+                        var query = this;
+
+                        if ( ! this.hasMore )
+                                return;
+
+                        options = options || {};
+                        options.add = true;
+
+                        return this.fetch( options ).done( function( resp ) {
+                                if ( _.isEmpty( resp ) || resp.length &lt; this.args.posts_per_page )
+                                        query.hasMore = false;
+                        });
+                },
+
+                sync: function( method, model, options ) {
+                        var fallback;
+
+                        // Overload the read method so Attachment.fetch() functions correctly.
+                        if ( 'read' === method ) {
+                                options = options || {};
+                                options.context = this;
+                                options.data = _.extend( options.data || {}, {
+                                        action: 'query-attachments'
+                                });
+
+                                // Clone the args so manipulation is non-destructive.
+                                args = _.clone( this.args );
+
+                                // Determine which page to query.
+                                args.paged = Math.floor( this.length / args.posts_per_page ) + 1;
+
+                                options.data.query = args;
+                                return media.ajax( options );
+
+                        // Otherwise, fall back to Backbone.sync()
+                        } else {
+                                fallback = Attachments.prototype.sync ? Attachments.prototype : Backbone;
+                                return fallback.sync.apply( this, arguments );
+                        }
+                },
+
+                comparator: (function(){
+                        /**
+                         * A basic comparator.
+                         *
+                         * @param  {mixed}  a  The primary parameter to compare.
+                         * @param  {mixed}  b  The primary parameter to compare.
+                         * @param  {string} ac The fallback parameter to compare, a's cid.
+                         * @param  {string} bc The fallback parameter to compare, b's cid.
+                         * @return {number}    -1: a should come before b.
+                         *                      0: a and b are of the same rank.
+                         *                      1: b should come before a.
+                         */
+                        var compare = function( a, b, ac, bc ) {
+                                if ( _.isEqual( a, b ) )
+                                        return ac === bc ? 0 : (ac &gt; bc ? -1 : 1);
+                                else
+                                        return a &gt; b ? -1 : 1;
+                        };
+
+                        return function( a, b ) {
+                                var key   = this.orderkey,
+                                        order = this.args.order,
+                                        ac    = a.cid,
+                                        bc    = b.cid;
+
+                                a = a.get( key );
+                                b = b.get( key );
+
+                                if ( 'date' === key || 'modified' === key ) {
+                                        a = a || new Date();
+                                        b = b || new Date();
+                                }
+
+                                return ( 'DESC' === order ) ? compare( a, b, ac, bc ) : compare( b, a, bc, ac );
+                        };
+                }())
+        }, {
+                defaultArgs: {
+                        posts_per_page: 40,
+                        orderby:       'date',
+                        order:         'DESC'
+                }
+        });
+
+}(jQuery));
</ins><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkwpincludesjsmediaviewsjs"></a>
<div class="addfile"><h4>Added: trunk/wp-includes/js/media-views.js (0 => 21683)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/js/media-views.js                                (rev 0)
+++ trunk/wp-includes/js/media-views.js        2012-08-31 04:54:23 UTC (rev 21683)
</span><span class="lines">@@ -0,0 +1,429 @@
</span><ins>+(function($){
+        var media       = wp.media,
+                Attachment  = media.model.Attachment,
+                Attachments = media.model.Attachments,
+                Query       = media.model.Query;
+
+
+        /**
+         * ========================================================================
+         * CONTROLLERS
+         * ========================================================================
+         */
+
+        /**
+         * wp.media.controller.Workflow
+         */
+        media.controller.Workflow = Backbone.Model.extend({
+                defaults: {
+                        multiple: false
+                },
+
+                initialize: function() {
+                        this.createSelection();
+
+                        // Initialize views.
+                        this.modal     = new media.view.Modal({ controller: this });
+                        this.workspace = new media.view.Workspace({ controller: this });
+                },
+
+                createSelection: function() {
+                        var controller = this;
+
+                        // Initialize workflow-specific models.
+                        this.selection = new Attachments();
+
+                        _.extend( this.selection, {
+                                // Override the selection's add method.
+                                // If the workflow does not support multiple
+                                // selected attachments, reset the selection.
+                                add: function( models, options ) {
+                                        if ( ! controller.get('multiple') ) {
+                                                models = _.isArray( models ) ? _.first( models ) : models;
+                                                this.clear( options );
+                                        }
+
+                                        return Attachments.prototype.add.call( this, models, options );
+                                },
+
+                                // Removes all models from the selection.
+                                clear: function( options ) {
+                                        return this.remove( this.models, options );
+                                },
+
+                                // Override the selection's reset method.
+                                // Always direct items through add and remove,
+                                // as we need them to fire.
+                                reset: function( models, options ) {
+                                        return this.clear( options ).add( models, options );
+                                },
+
+                                // Create selection.has, which determines if a model
+                                // exists in the collection based on cid and id,
+                                // instead of direct comparison.
+                                has: function( attachment ) {
+                                        return !! ( this.getByCid( attachment.cid ) || this.get( attachment.id ) );
+                                }
+                        });
+                },
+
+                render: function() {
+                        this.workspace.render();
+                        this.modal.content( this.workspace ).attach();
+                        return this;
+                }
+        });
+
+        /**
+         * ========================================================================
+         * VIEWS
+         * ========================================================================
+         */
+
+        /**
+         * wp.media.view.Modal
+         */
+        media.view.Modal = Backbone.View.extend({
+                tagName:  'div',
+                template: media.template('media-modal'),
+
+                events: {
+                        'click .media-modal-backdrop, .media-modal-close' : 'closeHandler'
+                },
+
+                initialize: function() {
+                        this.controller = this.options.controller;
+
+                        _.defaults( this.options, {
+                                title: '',
+                                container: document.body
+                        });
+                },
+
+                render: function() {
+                        this.$el.html( this.template( this.options ) );
+                        this.$('.media-modal-content').append( this.options.$content );
+                        return this;
+                },
+
+                attach: function() {
+                        this.$el.appendTo( this.options.container );
+                },
+
+                detach: function() {
+                        this.$el.detach();
+                },
+
+                open: function() {
+                        this.$el.show();
+                },
+
+                close: function() {
+                        this.$el.hide();
+                },
+
+                closeHandler: function( event ) {
+                        event.preventDefault();
+                        this.close();
+                },
+
+                content: function( $content ) {
+                        this.options.$content = ( $content instanceof Backbone.View ) ? $content.$el : $content;
+                        return this.render();
+                },
+
+                title: function( title ) {
+                        this.options.title = title;
+                        return this.render();
+                }
+        });
+
+        /**
+         * wp.media.view.Workspace
+         */
+        media.view.Workspace = Backbone.View.extend({
+                tagName:   'div',
+                className: 'media-workspace',
+                template:  media.template('media-workspace'),
+
+                events: {
+                        'dragenter':  'maybeInitUploader',
+                        'mouseenter': 'maybeInitUploader'
+                },
+
+                initialize: function() {
+                        this.controller = this.options.controller;
+
+                        _.defaults( this.options, {
+                                selectOne: false,
+                                uploader:  {}
+                        });
+
+                        this.attachmentsView = new media.view.Attachments({
+                                controller: this.controller,
+                                directions: 'Select stuff.',
+                                collection: new Attachments( null, {
+                                        mirror: media.query()
+                                })
+                        });
+
+                        this.$content = $('&lt;div class=&quot;existing-attachments&quot; /&gt;');
+                        this.$content.append( this.attachmentsView.$el );
+
+                        // Track uploading attachments.
+                        this.pending = new Attachments( [], { query: false });
+                        this.pending.on( 'add remove reset change:percent', function() {
+                                this.$el.toggleClass( 'uploading', !! this.pending.length );
+
+                                if ( ! this.$bar || ! this.pending.length )
+                                        return;
+
+                                this.$bar.width( ( this.pending.reduce( function( memo, attachment ) {
+                                        if ( attachment.get('uploading') )
+                                                return memo + ( attachment.get('percent') || 0 );
+                                        else
+                                                return memo + 100;
+                                }, 0 ) / this.pending.length ) + '%' );
+                        }, this );
+                },
+
+                render: function() {
+                        this.attachmentsView.render();
+                        this.$el.html( this.template( this.options ) ).append( this.$content );
+                        this.$bar = this.$('.media-progress-bar div');
+                        return this;
+                },
+
+                maybeInitUploader: function() {
+                        var workspace = this;
+
+                        // If the uploader already exists or the body isn't in the DOM, bail.
+                        if ( this.uploader || ! this.$el.closest('body').length )
+                                return;
+
+                        this.uploader = new wp.Uploader( _.extend({
+                                container: this.$el,
+                                dropzone:  this.$el,
+                                browser:   this.$('.upload-attachments a'),
+
+                                added: function( file ) {
+                                        file.attachment = Attachment.create( _.extend({
+                                                file: file,
+                                                uploading: true,
+                                                date: new Date()
+                                        }, _.pick( file, 'loaded', 'size', 'percent' ) ) );
+
+                                        workspace.pending.add( file.attachment );
+                                },
+
+                                progress: function( file ) {
+                                        file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
+                                },
+
+                                success: function( resp, file ) {
+                                        var complete;
+
+                                        _.each(['file','loaded','size','uploading','percent'], function( key ) {
+                                                file.attachment.unset( key );
+                                        });
+
+                                        file.attachment.set( 'id', resp.id );
+                                        Attachment.get( resp.id, file.attachment ).fetch();
+
+                                        complete = workspace.pending.all( function( attachment ) {
+                                                return ! attachment.get('uploading');
+                                        });
+
+                                        if ( complete )
+                                                workspace.pending.reset();
+                                },
+
+                                error: function( message, error, file ) {
+                                        file.attachment.destroy();
+                                }
+                        }, this.options.uploader ) );
+                }
+        });
+
+
+        /**
+         * wp.media.view.Attachments
+         */
+        media.view.Attachments = Backbone.View.extend({
+                tagName:   'div',
+                className: 'attachments',
+                template:  media.template('attachments'),
+
+                events: {
+                        'keyup input': 'search'
+                },
+
+                initialize: function() {
+                        this.controller = this.options.controller;
+
+                        _.defaults( this.options, {
+                                refreshSensitivity: 200,
+                                refreshThreshold:   3
+                        });
+
+                        _.each(['add','remove'], function( method ) {
+                                this.collection.on( method, function( attachment, attachments, options ) {
+                                        this[ method ]( attachment, options.index );
+                                }, this );
+                        }, this );
+
+                        this.collection.on( 'reset', this.refresh, this );
+
+                        this.$list = $('&lt;ul /&gt;');
+                        this.list  = this.$list[0];
+
+                        this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
+                        this.$list.on( 'scroll.attachments', this.scroll );
+                },
+
+                render: function() {
+                        this.$el.html( this.template( this.options ) ).append( this.$list );
+                        this.refresh();
+                        return this;
+                },
+
+                refresh: function() {
+                        // If there are no elements, load some.
+                        if ( ! this.collection.length ) {
+                                this.collection.more();
+                                this.$list.empty();
+                                return this;
+                        }
+
+                        // Otherwise, create all of the Attachment views, and replace
+                        // the list in a single DOM operation.
+                        this.$list.html( this.collection.map( function( attachment ) {
+                                return new media.view.Attachment({
+                                        controller: this.controller,
+                                        model:      attachment
+                                }).render().$el;
+                        }, this ) );
+
+                        // Then, trigger the scroll event to check if we're within the
+                        // threshold to query for additional attachments.
+                        this.scroll();
+                        return this;
+                },
+
+                add: function( attachment, index ) {
+                        var view, children;
+
+                        view = new media.view.Attachment({
+                                controller: this.controller,
+                                model:      attachment
+                        }).render();
+
+                        children = this.$list.children();
+
+                        if ( children.length &gt; index )
+                                children.eq( index ).before( view.$el );
+                        else
+                                this.$list.append( view.$el );
+                },
+
+                remove: function( attachment, index ) {
+                        var children = this.$list.children();
+                        if ( children.length )
+                                children.eq( index ).detach();
+                },
+
+                scroll: function( event ) {
+                        // @todo: is this still necessary?
+                        if ( ! this.$list.is(':visible') )
+                                return;
+
+                        if ( this.list.scrollHeight &lt; this.list.scrollTop + ( this.list.clientHeight * this.options.refreshThreshold ) ) {
+                                this.collection.more();
+                        }
+                },
+
+                search: function( event ) {
+                        var args = _.clone( this.collection.mirroring.args );
+
+                        // Bail if we're currently searching for the same string.
+                        if ( args.s === event.target.value )
+                                return;
+
+                        if ( event.target.value )
+                                args.s = event.target.value;
+                        else
+                                delete args.s;
+
+                        this.collection.mirror( media.query( args ) );
+                }
+        });
+
+
+        /**
+         * wp.media.view.Attachment
+         */
+        media.view.Attachment = Backbone.View.extend({
+                tagName:   'li',
+                className: 'attachment',
+                template:  media.template('attachment'),
+
+                events: {
+                        'click': 'toggleSelection'
+                },
+
+                initialize: function() {
+                        this.controller = this.options.controller;
+
+                        this.model.on( 'change:sizes change:uploading', this.render, this );
+                        this.model.on( 'change:percent', this.progress, this );
+                        this.model.on( 'add', this.select, this );
+                        this.model.on( 'remove', this.deselect, this );
+                },
+
+                render: function() {
+                        var attachment = this.model.toJSON(),
+                                options = {
+                                        orientation: attachment.orientation || 'landscape',
+                                        thumbnail:   attachment.url || '',
+                                        uploading:   attachment.uploading
+                                };
+
+                        // Use the medium size if possible. If the medium size
+                        // doesn't exist, then the attachment is too small.
+                        // In that case, use the attachment itself.
+                        if ( attachment.sizes &amp;&amp; attachment.sizes.medium ) {
+                                options.orientation = attachment.sizes.medium.orientation;
+                                options.thumbnail   = attachment.sizes.medium.url;
+                        }
+
+                        this.$el.html( this.template( options ) );
+
+                        if ( attachment.uploading )
+                                this.$bar = this.$('.media-progress-bar div');
+                        else
+                                delete this.$bar;
+
+                        return this;
+                },
+
+                progress: function() {
+                        if ( this.$bar &amp;&amp; this.$bar.length )
+                                this.$bar.width( this.model.get('percent') + '%' );
+                },
+
+                toggleSelection: function( event ) {
+                        var selection = this.controller.selection;
+                        selection[ selection.has( this.model ) ? 'remove' : 'add' ]( this.model );
+                },
+
+                select: function( model, collection ) {
+                        if ( collection === this.controller.selection )
+                                this.$el.addClass('selected');
+                },
+
+                deselect: function( model, collection ) {
+                        if ( collection === this.controller.selection )
+                                this.$el.removeClass('selected');
+                }
+        });
+}(jQuery));
</ins><span class="cx">\ No newline at end of file
</span></span></pre></div>
<a id="trunkwpincludesmediaphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/media.php (21682 => 21683)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/media.php        2012-08-31 04:32:25 UTC (rev 21682)
+++ trunk/wp-includes/media.php        2012-08-31 04:54:23 UTC (rev 21683)
</span><span class="lines">@@ -1595,3 +1595,60 @@
</span><span class="cx"> 
</span><span class="cx">         return apply_filters( 'wp_prepare_attachment_for_js', $response, $attachment, $meta );
</span><span class="cx"> }
</span><ins>+
+/**
+ * Prints the templates used in the media manager.
+ *
+ * @since 3.5.0
+ */
+function wp_print_media_templates( $attachment ) {
+        ?&gt;
+        &lt;script type=&quot;text/html&quot; id=&quot;tmpl-media-modal&quot;&gt;
+                &lt;div class=&quot;media-modal&quot;&gt;
+                        &lt;div class=&quot;media-modal-header&quot;&gt;
+                                &lt;h3&gt;&lt;%- title %&gt;&lt;/h3&gt;
+                                &lt;a class=&quot;media-modal-close&quot; href=&quot;&quot; title=&quot;&lt;?php esc_attr_e('Close'); ?&gt;&quot;&gt;&lt;?php echo 'Close'; ?&gt;&lt;/a&gt;
+                        &lt;/div&gt;
+                        &lt;div class=&quot;media-modal-content&quot;&gt;&lt;/div&gt;
+                &lt;/div&gt;
+                &lt;div class=&quot;media-modal-backdrop&quot;&gt;&lt;/div&gt;
+        &lt;/script&gt;
+
+        &lt;script type=&quot;text/html&quot; id=&quot;tmpl-media-workspace&quot;&gt;
+                &lt;div class=&quot;upload-attachments&quot;&gt;
+                        &lt;% if ( selectOne ) { %&gt;
+                                &lt;h3&gt;&lt;?php _e( 'Drop a file here' ); ?&gt;&lt;/h3&gt;
+                                &lt;span&gt;&lt;?php _ex( 'or', 'Uploader: Drop a file here - or - Select a File' ); ?&gt;&lt;/span&gt;
+                                &lt;a href=&quot;#&quot; class=&quot;button-secondary&quot;&gt;&lt;?php _e( 'Select a File' ); ?&gt;&lt;/a&gt;
+                        &lt;% } else { %&gt;
+                                &lt;h3&gt;&lt;?php _e( 'Drop files here' ); ?&gt;&lt;/h3&gt;
+                                &lt;span&gt;&lt;?php _ex( 'or', 'Uploader: Drop files here - or - Select Files' ); ?&gt;&lt;/span&gt;
+                                &lt;a href=&quot;#&quot; class=&quot;button-secondary&quot;&gt;&lt;?php _e( 'Select Files' ); ?&gt;&lt;/a&gt;
+                        &lt;% } %&gt;
+
+                        &lt;div class=&quot;media-progress-bar&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;
+                &lt;/div&gt;
+        &lt;/script&gt;
+
+        &lt;script type=&quot;text/html&quot; id=&quot;tmpl-attachments&quot;&gt;
+                &lt;div class=&quot;attachments-header&quot;&gt;
+                        &lt;h3&gt;&lt;%- directions %&gt;&lt;/h3&gt;
+                        &lt;input class=&quot;search&quot; type=&quot;text&quot; placeholder=&quot;&lt;?php esc_attr_e('Search'); ?&gt;&quot; /&gt;
+                &lt;/div&gt;
+        &lt;/script&gt;
+
+        &lt;script type=&quot;text/html&quot; id=&quot;tmpl-attachment&quot;&gt;
+                &lt;div class=&quot;attachment-thumbnail &lt;%- orientation %&gt;&quot;&gt;
+                        &lt;% if ( thumbnail ) { %&gt;
+                                &lt;img src=&quot;&lt;%- thumbnail %&gt;&quot; /&gt;
+                        &lt;% } %&gt;
+
+                        &lt;% if ( uploading ) { %&gt;
+                                &lt;div class=&quot;media-progress-bar&quot;&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;
+                        &lt;% } %&gt;
+                        &lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;
+                &lt;/div&gt;
+                &lt;div class=&quot;describe&quot;&gt;&lt;/div&gt;
+        &lt;/script&gt;
+        &lt;?php
+}
</ins></span></pre></div>
<a id="trunkwpincludesscriptloaderphp"></a>
<div class="modfile"><h4>Modified: trunk/wp-includes/script-loader.php (21682 => 21683)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/wp-includes/script-loader.php        2012-08-31 04:32:25 UTC (rev 21682)
+++ trunk/wp-includes/script-loader.php        2012-08-31 04:54:23 UTC (rev 21683)
</span><span class="lines">@@ -314,6 +314,15 @@
</span><span class="cx">                 'cheatin'   =&gt; __( 'Cheatin&amp;#8217; uh?' ),
</span><span class="cx">         ) );
</span><span class="cx"> 
</span><ins>+        $scripts-&gt;add( 'media-models', &quot;/wp-includes/js/media-models$suffix.js&quot;, array( 'backbone', 'jquery' ), false, 1 );
+        $scripts-&gt;add( 'media-views',  &quot;/wp-includes/js/media-views$suffix.js&quot;,  array( 'media-models', 'wp-plupload' ), false, 1 );
+        did_action( 'init' ) &amp;&amp; $scripts-&gt;localize( 'media-views', '_wpMediaViewsL10n', array(
+                'insertMedia'         =&gt; __( 'Insert Media' ),
+                'chooseFeatured'      =&gt; __( 'Choose a Featured Image' ),
+                'selectMediaSingular' =&gt; __( 'Select a media file:' ),
+                'selectMediaMultiple' =&gt; __( 'Select one or more media files:' ),
+        ) );
+
</ins><span class="cx">         if ( is_admin() ) {
</span><span class="cx">                 $scripts-&gt;add( 'ajaxcat', &quot;/wp-admin/js/cat$suffix.js&quot;, array( 'wp-lists' ) );
</span><span class="cx">                 $scripts-&gt;add_data( 'ajaxcat', 'group', 1 );
</span><span class="lines">@@ -492,6 +501,7 @@
</span><span class="cx">         $styles-&gt;add( 'editor-buttons', &quot;/wp-includes/css/editor$suffix.css&quot; );
</span><span class="cx">         $styles-&gt;add( 'wp-pointer', &quot;/wp-includes/css/wp-pointer$suffix.css&quot; );
</span><span class="cx">         $styles-&gt;add( 'customize-controls', &quot;/wp-admin/css/customize-controls$suffix.css&quot;, array( 'wp-admin', 'colors', 'ie' ) );
</span><ins>+        $styles-&gt;add( 'media-views', &quot;/wp-includes/css/media-views$suffix.css&quot; );
</ins><span class="cx"> 
</span><span class="cx">         foreach ( $rtl_styles as $rtl_style ) {
</span><span class="cx">                 $styles-&gt;add_data( $rtl_style, 'rtl', true );
</span></span></pre>
</div>
</div>

</body>
</html>