Add attachment details to the media sidebar.

* Also moves most of the `Frame` view's `createSelection` method to a real `Selection` model (which inherits from the `Attachments` model).
* Properly assigns the library within the `Gallery` state, allowing for the `Gallery` state to inherit from the `Library` state.

see #21390.


git-svn-id: http://core.svn.wordpress.org/trunk@22323 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Daryl Koopersmith 2012-10-29 15:13:02 +00:00
parent aa52a02dd1
commit 16c1011cdb
4 changed files with 276 additions and 123 deletions

View File

@ -307,8 +307,8 @@
font-weight: bold; font-weight: bold;
} }
.attachment .thumbnail, .attachment-preview .thumbnail,
.attachment .thumbnail img { .attachment-preview .thumbnail img {
-webkit-transition-property: width, height, top, left, right, bottom; -webkit-transition-property: width, height, top, left, right, bottom;
-moz-transition-property: width, height, top, left, right, bottom; -moz-transition-property: width, height, top, left, right, bottom;
-ms-transition-property: width, height, top, left, right, bottom; -ms-transition-property: width, height, top, left, right, bottom;
@ -326,12 +326,12 @@
transition-delay: 200ms; transition-delay: 200ms;
} }
.attachment .thumbnail { .attachment-preview .thumbnail {
width: 199px; width: 199px;
height: 199px; height: 199px;
} }
.attachment .thumbnail:after { .attachment-preview .thumbnail:after {
content: ''; content: '';
display: block; display: block;
position: absolute; position: absolute;
@ -388,7 +388,7 @@
display: block; display: block;
} }
.attachment .describe { .media-frame .describe {
position: relative; position: relative;
display: block; display: block;
width: 100%; width: 100%;
@ -592,6 +592,38 @@
line-height: 60px; line-height: 60px;
} }
/**
* Attachment Details
*/
.attachment-details {
padding-top: 20px;
}
.attachment-details-preview {
cursor: default;
}
.attachment-details-preview,
.attachment-details-preview .thumbnail {
width: auto;
height: auto;
float: left;
position: relative;
}
.attachment-details-preview .thumbnail img {
max-width: 120px;
max-height: 120px;
display: block;
margin: 0 auto;
}
.attachment-details .describe {
float: left;
margin: 10px 0 0;
}
/** /**
* Attachment Display Settings * Attachment Display Settings
*/ */

View File

@ -613,4 +613,47 @@ window.wp = window.wp || {};
}()) }())
}); });
/**
* wp.media.model.Selection
*
* Used to manage a selection of attachments in the views.
*/
media.model.Selection = Attachments.extend({
initialize: function( models, options ) {
Attachments.prototype.initialize.apply( this, arguments );
this.multiple = options && options.multiple;
},
// Override the selection's add method.
// If the workflow does not support multiple
// selected attachments, reset the selection.
add: function( models, options ) {
if ( ! this.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 ) );
}
});
}(jQuery)); }(jQuery));

View File

@ -118,16 +118,43 @@
}, },
initialize: function() { initialize: function() {
if ( ! this.get('selection') ) if ( ! this.get('selection') ) {
this.set( 'selection', new Attachments() ); this.set( 'selection', new media.model.Selection( null, {
multiple: this.get('multiple')
}) );
}
if ( ! this.get('library') ) if ( ! this.get('library') )
this.set( 'library', media.query() ); this.set( 'library', media.query() );
this.on( 'activate', this.activate, this ); this.on( 'activate', this.activate, this );
this.on( 'deactivate', this.deactivate, this );
this.on( 'change:details', this.details, this );
}, },
activate: function() { activate: function() {
this.toolbar();
this.sidebar();
this.content();
// 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 );
this.get('selection').on( 'add remove', this.toggleDetails, this );
},
deactivate: function() {
var toolbar = this._postLibraryToolbar;
if ( toolbar )
this.get('selection').off( 'add remove', toolbar.visibility, toolbar );
wp.Uploader.queue.off( 'add', this.selectUpload, this );
this.get('selection').off( 'add remove', this.toggleDetails, this );
},
toolbar: function() {
var frame = this.frame, var frame = this.frame,
toolbar; toolbar;
@ -139,25 +166,35 @@
frame.toolbar( toolbar ); frame.toolbar( toolbar );
this.get('selection').on( 'add remove', toolbar.visibility, toolbar ); this.get('selection').on( 'add remove', toolbar.visibility, toolbar );
},
sidebar: function() {
var frame = this.frame;
// Sidebar. // Sidebar.
frame.sidebar( new media.view.Sidebar({ frame.sidebar( new media.view.Sidebar({
controller: frame, controller: frame
views: {
search: new media.view.Search({
controller: frame,
model: this.get('library').props,
priority: 20
}),
selection: new media.view.SelectionPreview({
controller: frame,
collection: this.get('selection'),
priority: 40
})
}
}) ); }) );
this.details({ silent: true });
frame.sidebar().add({
search: new media.view.Search({
controller: frame,
model: this.get('library').props,
priority: 20
}),
selection: new media.view.SelectionPreview({
controller: frame,
collection: this.get('selection'),
priority: 40
})
});
},
content: function() {
var frame = this.frame;
// Content. // Content.
frame.content( new media.view.Attachments({ frame.content( new media.view.Attachments({
controller: frame, controller: frame,
@ -165,75 +202,81 @@
// The single `Attachment` view to be used in the `Attachments` view. // The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: media.view.Attachment.Library AttachmentView: media.view.Attachment.Library
}).render() ); }).render() );
// 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 );
},
deactivate: function() {
var toolbar = this._postLibraryToolbar;
wp.Uploader.queue.off( 'add', this.selectUpload, this );
this.get('selection').off( 'add remove', toolbar.visibility, toolbar );
}, },
selectUpload: function( attachment ) { selectUpload: function( attachment ) {
this.get('selection').add( attachment ); this.get('selection').add( attachment );
},
details: function( options ) {
var model = this.get('details'),
view;
if ( model ) {
view = new media.view.Attachment.Details({
controller: this.frame,
model: model,
priority: 80
});
} else {
view = new Backbone.View();
}
if ( ! options || ! options.silent )
view.render();
this.frame.sidebar().add( 'details', view, options );
},
toggleDetails: function( model ) {
var details = this.get('details'),
selection = this.get('selection');
if ( selection.has( model ) )
this.set( 'details', model );
else if ( selection.length )
this.set( 'details', selection.last() );
else
this.unset('details');
} }
}); });
// wp.media.controller.Gallery // wp.media.controller.Gallery
// --------------------------- // ---------------------------
media.controller.Gallery = Backbone.Model.extend({ media.controller.Gallery = media.controller.Library.extend({
defaults: { defaults: {
id: 'gallery', id: 'gallery',
multiple: true, multiple: false,
describe: true, describe: true,
title: l10n.createGallery title: l10n.createGallery
}, },
initialize: function() { toolbar: function() {
if ( ! this.get('selection') ) this.frame.toolbar( new media.view.Toolbar.Gallery({
this.set( 'selection', new Attachments() ); controller: this.frame,
this.on( 'activate', this.activate, this );
},
activate: function() {
var frame = this.frame;
// Toolbar.
frame.toolbar( new media.view.Toolbar.Gallery({
controller: frame,
state: this state: this
}) ); }) );
},
sidebar: function() {
var frame = this.frame;
// Sidebar. // Sidebar.
frame.sidebar( new media.view.Sidebar({ frame.sidebar( new media.view.Sidebar({
controller: frame controller: frame
}).render() ); }) );
// Content. this.details();
frame.content( new media.view.Attachments({ },
controller: frame,
collection: this.get('selection'), content: function() {
this.frame.content( new media.view.Attachments({
controller: this.frame,
collection: this.get('library'),
sortable: true, sortable: true,
// The single `Attachment` view to be used in the `Attachments` view. // The single `Attachment` view to be used in the `Attachments` view.
AttachmentView: media.view.Attachment.Gallery AttachmentView: media.view.Attachment.Gallery
}).render() ); }).render() );
// Automatically select any uploading attachments.
wp.Uploader.queue.on( 'add', this.selectUpload, this );
},
deactivate: function() {
wp.Uploader.queue.off( 'add', this.selectUpload, this );
},
selectUpload: function( attachment ) {
this.get('selection').add( attachment );
} }
}); });
@ -288,41 +331,11 @@
var controller = this, var controller = this,
selection = this.options.selection; selection = this.options.selection;
if ( ! (selection instanceof Attachments) ) if ( ! (selection instanceof media.model.Selection) ) {
selection = this.options.selection = new Attachments( selection ); selection = this.options.selection = new media.model.Selection( selection, {
multiple: this.options.multiple
_.extend( 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.state().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 ) );
}
});
}, },
createStates: function() { createStates: function() {
@ -339,12 +352,12 @@
// Add the default states. // Add the default states.
this.states.add([ this.states.add([
new media.controller.Library({ new media.controller.Library({
selection: options.selection, selection: options.selection,
collection: media.query( options.library ), library: media.query( options.library ),
multiple: this.options.multiple multiple: this.options.multiple
}), }),
new media.controller.Gallery({ new media.controller.Gallery({
selection: options.selection library: options.selection
}) })
]); ]);
@ -630,7 +643,10 @@
this.$secondary = $('<div class="media-toolbar-secondary" />').prependTo( this.$el ); this.$secondary = $('<div class="media-toolbar-secondary" />').prependTo( this.$el );
if ( this.options.items ) if ( this.options.items )
this.add( this.options.items, { silent: true }).render(); this.add( this.options.items, { silent: true });
if ( ! this.options.silent )
this.render();
}, },
render: function() { render: function() {
@ -650,11 +666,16 @@
}, },
add: function( id, view, options ) { add: function( id, view, options ) {
options = options || {};
// Accept an object with an `id` : `view` mapping. // Accept an object with an `id` : `view` mapping.
if ( _.isObject( id ) ) { if ( _.isObject( id ) ) {
_.each( id, function( view, id ) { _.each( id, function( view, id ) {
this.add( id, view, options ); this.add( id, view, { silent: true });
}, this ); }, this );
if ( ! options.silent )
this.render();
return this; return this;
} }
@ -666,7 +687,7 @@
view.controller = view.controller || this.controller; view.controller = view.controller || this.controller;
this._views[ id ] = view; this._views[ id ] = view;
if ( ! options || ! options.silent ) if ( ! options.silent )
this.render(); this.render();
return this; return this;
}, },
@ -777,7 +798,7 @@
initialize: function() { initialize: function() {
var state = this.options.state, var state = this.options.state,
editing = state.get('editing'), editing = state.get('editing'),
selection = state.get('selection'), library = state.get('library'),
controller = this.options.controller; controller = this.options.controller;
this.options.items = { this.options.items = {
@ -787,8 +808,8 @@
priority: 40, priority: 40,
click: function() { click: function() {
controller.close(); controller.close();
state.trigger( 'update', selection ); state.trigger( 'update', library );
selection.clear(); library.clear();
controller.state('library'); controller.state('library');
} }
}, },
@ -921,7 +942,10 @@
this._views = {}; this._views = {};
if ( this.options.views ) if ( this.options.views )
this.add( this.options.views, { silent: true }).render(); this.add( this.options.views, { silent: true });
if ( ! this.options.silent )
this.render();
}, },
render: function() { render: function() {
@ -949,18 +973,23 @@
}, },
add: function( id, view, options ) { add: function( id, view, options ) {
options = options || {};
// Accept an object with an `id` : `view` mapping. // Accept an object with an `id` : `view` mapping.
if ( _.isObject( id ) ) { if ( _.isObject( id ) ) {
_.each( id, function( view, id ) { _.each( id, function( view, id ) {
this.add( id, view, options ); this.add( id, view, { silent: true });
}, this ); }, this );
if ( ! options.silent )
this.render();
return this; return this;
} }
view.controller = view.controller || this.controller; view.controller = view.controller || this.controller;
this._views[ id ] = view; this._views[ id ] = view;
if ( ! options || ! options.silent ) if ( ! options.silent )
this.render(); this.render();
return this; return this;
}, },
@ -989,8 +1018,7 @@
'click .attachment-preview': 'toggleSelection', 'click .attachment-preview': 'toggleSelection',
'mouseenter .attachment-preview': 'shrink', 'mouseenter .attachment-preview': 'shrink',
'mouseleave .attachment-preview': 'expand', 'mouseleave .attachment-preview': 'expand',
'change .describe': 'describe', 'change .describe': 'describe'
'click .close': 'toggleSelection'
}, },
buttons: {}, buttons: {},
@ -1182,9 +1210,13 @@
events: (function() { events: (function() {
var events = _.clone( media.view.Attachment.prototype.events ); var events = _.clone( media.view.Attachment.prototype.events );
delete events['click .attachment-preview']; events['click .close'] = 'removeFromGallery';
return events; return events;
}()) }()),
removeFromGallery: function() {
this.controller.state().get('library').remove( this.model );
}
}); });
/** /**
@ -1376,7 +1408,7 @@
render: function() { render: function() {
var options = _.clone( this.options ), var options = _.clone( this.options ),
first, sizes, amount; last, sizes, amount;
// If nothing is selected, display nothing. // If nothing is selected, display nothing.
if ( ! this.collection.length ) { if ( ! this.collection.length ) {
@ -1385,13 +1417,13 @@
} }
options.count = this.collection.length; options.count = this.collection.length;
first = this.collection.first(); last = this.collection.last();
sizes = first.get('sizes'); sizes = last.get('sizes');
if ( 'image' === first.get('type') ) if ( 'image' === last.get('type') )
options.thumbnail = ( sizes && sizes.thumbnail ) ? sizes.thumbnail.url : first.get('url'); options.thumbnail = ( sizes && sizes.thumbnail ) ? sizes.thumbnail.url : last.get('url');
else else
options.thumbnail = first.get('icon'); options.thumbnail = last.get('icon');
this.$el.html( this.template( options ) ); this.$el.html( this.template( options ) );
return this; return this;
@ -1490,4 +1522,17 @@
_( options.changes ).chain().keys().each( this.update, this ); _( options.changes ).chain().keys().each( this.update, this );
} }
}); });
/**
* wp.media.view.Attachment.Details
*/
media.view.Attachment.Details = media.view.Attachment.extend({
tagName: 'div',
className: 'attachment-details',
template: media.template('attachment-details'),
events: {
'change .describe': 'describe'
}
});
}(jQuery)); }(jQuery));

View File

@ -1305,7 +1305,7 @@ function wp_print_media_templates( $attachment ) {
<script type="text/html" id="tmpl-uploader-window"> <script type="text/html" id="tmpl-uploader-window">
<div class="uploader-window-content"> <div class="uploader-window-content">
<h3><?php _e( 'Drop files here to upload' ); ?></h3> <h3><?php _e( 'Drop files to upload' ); ?></h3>
</div> </div>
</script> </script>
@ -1358,6 +1358,39 @@ function wp_print_media_templates( $attachment ) {
<% } %> <% } %>
</script> </script>
<script type="text/html" id="tmpl-attachment-details">
<div class="attachment-preview attachment-details-preview type-<%- type %> subtype-<%- subtype %> <%- orientation %>">
<% if ( uploading ) { %>
<div class="media-progress-bar"><div></div></div>
<% } else if ( 'image' === type ) { %>
<div class="thumbnail">
<img src="<%- url %>" draggable="false" />
</div>
<% } else { %>
<div class="icon-thumbnail">
<img src="<%- icon %>" class="icon" draggable="false" />
<div class="filename"><%- filename %></div>
</div>
<% } %>
</div>
<% if ( 'image' === type ) { %>
<textarea class="describe"
placeholder="<?php esc_attr_e('Describe this image&hellip;'); ?>"
><%- caption %></textarea>
<% } else { %>
<textarea class="describe"
<% if ( 'video' === type ) { %>
placeholder="<?php esc_attr_e('Describe this video&hellip;'); ?>"
<% } else if ( 'audio' === type ) { %>
placeholder="<?php esc_attr_e('Describe this audio file&hellip;'); ?>"
<% } else { %>
placeholder="<?php esc_attr_e('Describe this media file&hellip;'); ?>"
<% } %>
><%- title %></textarea>
<% } %>
</script>
<script type="text/html" id="tmpl-media-selection-preview"> <script type="text/html" id="tmpl-media-selection-preview">
<div class="selected-img selected-count-<%- count %>"> <div class="selected-img selected-count-<%- count %>">
<% if ( thumbnail ) { %> <% if ( thumbnail ) { %>