Add a sidebar to the media modal.

* Adds `wp.media.view.Sidebar`, to aid in rendering the sidebar.
* Removes the `directions` from the `Attachments` view and shifts search into a separate view (`wp.mce.view.Search`) that can be relocated at will. This also serves to simplify the `Attachments` view by removing the nested `list` and `$list` parameters.
* Show the toolbar on the featured image workflow, effectively requiring confirmation before closing the dialog.

see #21390, #21776, #21808.


git-svn-id: http://core.svn.wordpress.org/trunk@22321 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Daryl Koopersmith 2012-10-29 06:56:23 +00:00
parent 53ccb09bce
commit f7c1aaf26d
5 changed files with 274 additions and 132 deletions

View File

@ -1018,7 +1018,8 @@ function post_thumbnail_meta_box( $post ) {
var $element = $('#select-featured-image'),
$thumbnailId = $element.find('input[name="thumbnail_id"]'),
title = '<?php _e( "Choose a Featured Image" ); ?>',
workflow, selection, setFeaturedImage;
update = '<?php _e( "Update Featured Image" ); ?>',
frame, selection, setFeaturedImage;
setFeaturedImage = function( thumbnailId ) {
$element.find('img').remove();
@ -1029,41 +1030,52 @@ function post_thumbnail_meta_box( $post ) {
$element.on( 'click', '.choose, img', function( event ) {
event.preventDefault();
if ( ! workflow ) {
workflow = wp.media({
if ( ! frame ) {
frame = wp.media({
title: title,
library: {
type: 'image'
}
});
selection = workflow.state().get('selection');
frame.toolbar( new wp.media.view.Toolbar({
controller: frame,
items: {
update: {
style: 'primary',
text: update,
priority: 40,
selection.on( 'add', function( model ) {
var sizes = model.get('sizes'),
size;
click: function() {
var selection = frame.state().get('selection'),
model = selection.first(),
sizes = model.get('sizes'),
size;
setFeaturedImage( model.id );
setFeaturedImage( model.id );
// @todo: might need a size hierarchy equivalent.
if ( sizes )
size = sizes['post-thumbnail'] || sizes.medium;
// @todo: might need a size hierarchy equivalent.
if ( sizes )
size = sizes['post-thumbnail'] || sizes.medium;
// @todo: Need a better way of accessing full size
// data besides just calling toJSON().
size = size || model.toJSON();
// @todo: Need a better way of accessing full size
// data besides just calling toJSON().
size = size || model.toJSON();
workflow.close();
selection.clear();
frame.close();
selection.clear();
$( '<img />', {
src: size.url,
width: size.width
}).prependTo( $element );
});
$( '<img />', {
src: size.url,
width: size.width
}).prependTo( $element );
}
}
}
}) );
}
workflow.open();
frame.open();
});
$element.on( 'click', '.remove', function( event ) {

View File

@ -72,9 +72,12 @@
* Toolbar
*/
.media-toolbar {
position: relative;
z-index: 50;
height: 60px;
position: absolute;
top: 0;
left: 220px;
right: 0;
z-index: 100;
height: 50px;
padding: 0 10px;
border-bottom: 1px solid #dfdfdf;
}
@ -91,22 +94,64 @@
.media-toolbar-primary > .media-button-group {
margin-left: 10px;
float: left;
margin-top: 16px;
margin-top: 10px;
}
.media-toolbar-secondary > .media-button,
.media-toolbar-secondary > .media-button-group {
margin-right: 10px;
float: left;
margin-top: 16px;
margin-top: 10px;
}
/**
* Sidebar
*/
.media-sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 219px;
z-index: 50;
background: #f5f5f5;
border-right: 1px solid #dfdfdf;
}
.hide-sidebar .media-sidebar {
display: none;
}
.media-sidebar .sidebar-title {
font-weight: 200;
font-size: 20px;
margin: 0;
padding: 12px 10px 10px;
line-height: 28px;
/*border-bottom: 1px solid #dfdfdf;*/
}
.media-sidebar .sidebar-content {
padding: 0 10px;
}
.media-sidebar .search {
display: block;
width: 100%;
}
.media-sidebar .selection-preview {
display: block;
padding-top: 5px;
}
/**
* Frame
*/
.media-frame .attachments,
.media-frame .media-toolbar {
.media-frame .media-content,
.media-frame .media-toolbar,
.media-frame .media-sidebar {
-webkit-transition-property: left, right, top, bottom, margin;
-moz-transition-property: left, right, top, bottom, margin;
-ms-transition-property: left, right, top, bottom, margin;
@ -120,35 +165,41 @@
transition-duration: 0.2s;
}
.media-frame .attachments {
.media-frame .media-content {
position: absolute;
top: 61px;
left: 0;
top: 51px;
left: 220px;
right: 0;
bottom: 0;
height: auto;
width: auto;
overflow: auto;
}
.media-frame.hide-toolbar .attachments {
top: 0;
}
.media-frame .media-toolbar {
margin-top: 0;
}
.media-frame.hide-toolbar .media-toolbar {
margin-top: -61px;
.media-frame.hide-sidebar .media-content {
left: 0;
}
.media-frame .media-toolbar .add-to-gallery {
display: none;
}
/**
* Search
*/
.media-frame .search {
margin-top: 11px;
padding: 4px;
line-height: 18px;
font-size: 13px;
color: #464646;
font-family: sans-serif;
}
/**
* Attachments
*/
.attachments {
/*.attachments {
position: relative;
width: 100%;
height: 100%;
@ -173,16 +224,6 @@
font-weight: 200;
}
.attachments-header .search {
float: right;
margin-top: 11px;
padding: 4px;
line-height: 18px;
font-size: 13px;
color: #464646;
font-family: sans-serif;
}
.attachments ul {
position: absolute;
top: 50px;
@ -191,7 +232,7 @@
bottom: 0;
overflow: auto;
margin: 0 0 20px;
}
}*/
/**
* Attachment
@ -401,7 +442,6 @@
bottom: 0;
background: rgba( 0, 86, 132, 0.9 );
/*z-index: -200;*/
z-index: 250000;
display: none;
text-align: center;
@ -414,10 +454,6 @@
transition: opacity 250ms;
}
/*.drag-over .uploader-window {
z-index: 250000;
}*/
.uploader-window-content {
position: absolute;
top: 30px;

View File

@ -113,7 +113,8 @@
defaults: {
id: 'library',
multiple: false,
describe: false
describe: false,
title: l10n.mediaLibrary
},
initialize: function() {
@ -130,25 +131,41 @@
var frame = this.frame,
toolbar;
// Toolbar.
toolbar = this._postLibraryToolbar = new media.view.Toolbar.PostLibrary({
controller: frame,
selection: this.get('selection')
state: this
});
frame.toolbar( toolbar );
this.get('selection').on( 'add remove', toolbar.visibility, toolbar );
// Sidebar.
frame.sidebar( new media.view.Sidebar({
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
})
}
}) );
// Content.
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') )
@ -173,7 +190,8 @@
defaults: {
id: 'gallery',
multiple: true,
describe: true
describe: true,
title: l10n.createGallery
},
initialize: function() {
@ -186,14 +204,19 @@
activate: function() {
var frame = this.frame;
// Toolbar.
frame.toolbar( new media.view.Toolbar.Gallery({
controller: frame,
editing: this.get('editing'),
selection: this.get('selection')
state: this
}) );
// Sidebar.
frame.sidebar( new media.view.Sidebar({
controller: frame
}).render() );
// Content.
frame.content( new media.view.Attachments({
directions: 'Gallery time!',
controller: frame,
collection: this.get('selection'),
sortable: true,
@ -245,7 +268,7 @@
},
render: function() {
var els = [ this.sidebar().el, this.toolbar().el, this.content().el ];
var els = [ this.toolbar().el, this.sidebar().el, this.content().el ];
if ( this.modal )
this.modal.render();
@ -634,16 +657,11 @@
// ---------------------------------
media.view.Toolbar.PostLibrary = media.view.Toolbar.extend({
initialize: function() {
var selection = this.options.selection,
var state = this.options.state,
selection = state.get('selection'),
controller = this.options.controller;
this.options.items = {
'selection-preview': new media.view.SelectionPreview({
controller: controller,
collection: selection,
priority: -40
}),
'create-new-gallery': {
style: 'primary',
text: l10n.createNewGallery,
@ -662,7 +680,7 @@
text: l10n.insertIntoPost,
click: function() {
controller.close();
controller.state().trigger( 'insert', selection );
state.trigger( 'insert', selection );
selection.clear();
}
},
@ -698,16 +716,16 @@
};
media.view.Toolbar.prototype.initialize.apply( this, arguments );
this.visibility();
},
visibility: function() {
var selection = this.options.selection,
var state = this.options.state,
selection = state.get('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');
@ -718,6 +736,8 @@
_.each( insert.buttons, function( button ) {
button.model.set( 'style', showGallery ? '' : 'primary' );
});
_.first( insert.buttons ).model.set( 'disabled', ! count );
}
});
@ -725,8 +745,9 @@
// -----------------------------
media.view.Toolbar.Gallery = media.view.Toolbar.extend({
initialize: function() {
var editing = this.options.editing,
selection = this.options.selection,
var state = this.options.state,
editing = state.get('editing'),
selection = state.get('selection'),
controller = this.options.controller;
this.options.items = {
@ -736,7 +757,7 @@
priority: 40,
click: function() {
controller.close();
controller.state().trigger( 'update', selection );
state.trigger( 'update', selection );
selection.clear();
controller.state('library');
}
@ -769,9 +790,10 @@
},
defaults: {
text: '',
style: '',
size: 'large'
text: '',
style: '',
size: 'large',
disabled: false
},
initialize: function() {
@ -796,17 +818,19 @@
},
render: function() {
var classes = [ 'button', this.className ];
var classes = [ 'button', this.className ],
model = this.model.toJSON();
if ( this.model.get('style') )
classes.push( 'button-' + this.model.get('style') );
if ( model.style )
classes.push( 'button-' + model.style );
if ( this.model.get('size') )
classes.push( 'button-' + this.model.get('size') );
if ( model.size )
classes.push( 'button-' + model.size );
classes = _.uniq( classes.concat( this.options.classes ) );
this.el.className = classes.join(' ');
this.$el.attr( 'disabled', model.disabled );
// Detach the dropdown.
if ( this.options.dropdown )
@ -822,7 +846,7 @@
click: function( event ) {
event.preventDefault();
if ( this.options.click )
if ( this.options.click && ! this.model.get('disabled') )
this.options.click.apply( this, arguments );
}
});
@ -854,6 +878,70 @@
}
});
/**
* wp.media.view.Sidebar
*/
media.view.Sidebar = Backbone.View.extend({
tagName: 'div',
className: 'media-sidebar',
template: media.template('sidebar'),
initialize: function() {
this.controller = this.options.controller;
this._views = {};
if ( this.options.views )
this.add( this.options.views, { silent: true }).render();
},
render: function() {
var els = _( this._views ).chain().sortBy( function( view ) {
return view.options.priority || 10;
}).pluck('el').value();
// Make sure to detach the elements we want to reuse.
// Otherwise, `jQuery.html()` will unbind their events.
$( els ).detach();
this.$el.html( this.template({
title: this.controller.state().get('title') || '',
uploader: this.controller.options.uploader
}) );
this.$('.sidebar-content').html( els );
return this;
},
add: function( id, view, options ) {
// Accept an object with an `id` : `view` mapping.
if ( _.isObject( id ) ) {
_.each( id, function( view, id ) {
this.add( id, view, options );
}, this );
return this;
}
view.controller = view.controller || this.controller;
this._views[ id ] = view;
if ( ! options || ! options.silent )
this.render();
return this;
},
get: function( id ) {
return this._views[ id ];
},
remove: function( id, options ) {
delete this._views[ id ];
if ( ! options || ! options.silent )
this.render();
return this;
}
});
/**
* wp.media.view.Attachment
*/
@ -1068,12 +1156,11 @@
* wp.media.view.Attachments
*/
media.view.Attachments = Backbone.View.extend({
tagName: 'div',
tagName: 'ul',
className: 'attachments',
template: media.template('attachments'),
events: {
'keyup .search': 'search'
'scroll': 'scroll'
},
initialize: function() {
@ -1092,13 +1179,10 @@
}, this );
}, this );
this.collection.on( 'reset', this.refresh, this );
this.$list = $('<ul />');
this.list = this.$list[0];
this.collection.on( 'reset', this.render, this );
// Throttle the scroll handler.
this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
this.$list.on( 'scroll.attachments', this.scroll );
this.initSortable();
},
@ -1110,13 +1194,13 @@
if ( ! this.options.sortable || ! $.fn.sortable )
return;
this.$list.sortable({
this.$el.sortable({
// If the `collection` has a `comparator`, disable sorting.
disabled: !! collection.comparator,
// Prevent attachments from being dragged outside the bounding
// box of the list.
containment: this.$list,
containment: this.$el,
// Change the position of the attachment as soon as the
// mouse pointer overlaps a thumbnail.
@ -1144,30 +1228,21 @@
// If the `orderby` property is changed on the `collection`,
// check to see if we have a `comparator`. If so, disable sorting.
collection.props.on( 'change:orderby', function() {
this.$list.sortable( 'option', 'disabled', !! collection.comparator );
this.$el.sortable( 'option', 'disabled', !! collection.comparator );
}, this );
},
render: function() {
// Detach the list from the DOM to prevent event removal.
this.$list.detach();
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();
this.$el.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 ) {
this.$el.html( this.collection.map( function( attachment ) {
return new this.options.AttachmentView({
controller: this.controller,
model: attachment
@ -1188,37 +1263,57 @@
model: attachment
}).render();
children = this.$list.children();
children = this.$el.children();
if ( children.length > index )
children.eq( index ).before( view.$el );
else
this.$list.append( view.$el );
this.$el.append( view.$el );
},
remove: function( attachment, index ) {
var children = this.$list.children();
var children = this.$el.children();
if ( children.length )
children.eq( index ).detach();
},
scroll: function( event ) {
// @todo: is this still necessary?
if ( ! this.$list.is(':visible') )
if ( ! this.$el.is(':visible') )
return;
if ( this.list.scrollHeight < this.list.scrollTop + ( this.list.clientHeight * this.options.refreshThreshold ) ) {
if ( this.el.scrollHeight < this.el.scrollTop + ( this.el.clientHeight * this.options.refreshThreshold ) ) {
this.collection.more();
}
}
});
/**
* wp.media.view.Search
*/
media.view.Search = Backbone.View.extend({
tagName: 'input',
className: 'search',
attributes: {
type: 'text',
placeholder: l10n.search
},
events: {
'keyup': 'search'
},
render: function() {
this.el.value = this.model.escape('search');
return this;
},
search: function( event ) {
var props = this.collection.props;
if ( event.target.value )
props.set( 'search', event.target.value );
this.model.set( 'search', event.target.value );
else
props.unset('search');
this.model.unset('search');
}
});

View File

@ -1309,11 +1309,9 @@ function wp_print_media_templates( $attachment ) {
</div>
</script>
<script type="text/html" id="tmpl-attachments">
<div class="attachments-header">
<h3><%- directions %></h3>
<input class="search" type="text" placeholder="<?php esc_attr_e('Search'); ?>" />
</div>
<script type="text/html" id="tmpl-sidebar">
<h2 class="sidebar-title"><%- title %></h2>
<div class="sidebar-content"></div>
</script>
<script type="text/html" id="tmpl-attachment">

View File

@ -323,16 +323,17 @@ function wp_default_scripts( &$scripts ) {
$scripts->add( 'media-views', "/wp-includes/js/media-views$suffix.js", array( 'media-models', 'wp-plupload' ), false, 1 );
did_action( 'init' ) && $scripts->localize( 'media-views', '_wpMediaViewsL10n', array(
// Generic
'insertMedia' => __( 'Insert Media' ),
'selectMediaSingular' => __( 'Select a media file:' ),
'selectMediaMultiple' => __( 'Select one or more media files:' ),
'insertMedia' => __( 'Insert Media' ),
'search' => __( 'Search' ),
// Library
'mediaLibrary' => __( 'Media Library' ),
'createNewGallery' => __( 'Create a new gallery' ),
'insertIntoPost' => __( 'Insert into post' ),
'addToGallery' => __( 'Add to gallery' ),
// Gallery
'createGallery' => __( 'Create Gallery' ),
'returnToLibrary' => __( 'Return to media library' ),
'continueEditingGallery' => __( 'Continue editing gallery' ),
'insertGalleryIntoPost' => __( 'Insert gallery into post' ),