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

View File

@ -72,9 +72,12 @@
* Toolbar * Toolbar
*/ */
.media-toolbar { .media-toolbar {
position: relative; position: absolute;
z-index: 50; top: 0;
height: 60px; left: 220px;
right: 0;
z-index: 100;
height: 50px;
padding: 0 10px; padding: 0 10px;
border-bottom: 1px solid #dfdfdf; border-bottom: 1px solid #dfdfdf;
} }
@ -91,22 +94,64 @@
.media-toolbar-primary > .media-button-group { .media-toolbar-primary > .media-button-group {
margin-left: 10px; margin-left: 10px;
float: left; float: left;
margin-top: 16px; margin-top: 10px;
} }
.media-toolbar-secondary > .media-button, .media-toolbar-secondary > .media-button,
.media-toolbar-secondary > .media-button-group { .media-toolbar-secondary > .media-button-group {
margin-right: 10px; margin-right: 10px;
float: left; 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 * Frame
*/ */
.media-frame .attachments, .media-frame .media-content,
.media-frame .media-toolbar { .media-frame .media-toolbar,
.media-frame .media-sidebar {
-webkit-transition-property: left, right, top, bottom, margin; -webkit-transition-property: left, right, top, bottom, margin;
-moz-transition-property: left, right, top, bottom, margin; -moz-transition-property: left, right, top, bottom, margin;
-ms-transition-property: left, right, top, bottom, margin; -ms-transition-property: left, right, top, bottom, margin;
@ -120,35 +165,41 @@
transition-duration: 0.2s; transition-duration: 0.2s;
} }
.media-frame .attachments { .media-frame .media-content {
position: absolute; position: absolute;
top: 61px; top: 51px;
left: 0; left: 220px;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: auto; height: auto;
width: auto; width: auto;
overflow: auto;
} }
.media-frame.hide-toolbar .attachments { .media-frame.hide-sidebar .media-content {
top: 0; left: 0;
}
.media-frame .media-toolbar {
margin-top: 0;
}
.media-frame.hide-toolbar .media-toolbar {
margin-top: -61px;
} }
.media-frame .media-toolbar .add-to-gallery { .media-frame .media-toolbar .add-to-gallery {
display: none; 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 { /*.attachments {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -173,16 +224,6 @@
font-weight: 200; 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 { .attachments ul {
position: absolute; position: absolute;
top: 50px; top: 50px;
@ -191,7 +232,7 @@
bottom: 0; bottom: 0;
overflow: auto; overflow: auto;
margin: 0 0 20px; margin: 0 0 20px;
} }*/
/** /**
* Attachment * Attachment
@ -401,7 +442,6 @@
bottom: 0; bottom: 0;
background: rgba( 0, 86, 132, 0.9 ); background: rgba( 0, 86, 132, 0.9 );
/*z-index: -200;*/
z-index: 250000; z-index: 250000;
display: none; display: none;
text-align: center; text-align: center;
@ -414,10 +454,6 @@
transition: opacity 250ms; transition: opacity 250ms;
} }
/*.drag-over .uploader-window {
z-index: 250000;
}*/
.uploader-window-content { .uploader-window-content {
position: absolute; position: absolute;
top: 30px; top: 30px;

View File

@ -113,7 +113,8 @@
defaults: { defaults: {
id: 'library', id: 'library',
multiple: false, multiple: false,
describe: false describe: false,
title: l10n.mediaLibrary
}, },
initialize: function() { initialize: function() {
@ -130,25 +131,41 @@
var frame = this.frame, var frame = this.frame,
toolbar; toolbar;
// Toolbar.
toolbar = this._postLibraryToolbar = new media.view.Toolbar.PostLibrary({ toolbar = this._postLibraryToolbar = new media.view.Toolbar.PostLibrary({
controller: frame, controller: frame,
selection: this.get('selection') state: this
}); });
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.
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({ frame.content( new media.view.Attachments({
directions: this.get('multiple') ? l10n.selectMediaMultiple : l10n.selectMediaSingular,
controller: frame, controller: frame,
collection: this.get('library'), collection: this.get('library'),
// 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 ( ! this.get('selection').length )
frame.$el.addClass('hide-toolbar');
// If we're in a workflow that supports multiple attachments, // If we're in a workflow that supports multiple attachments,
// automatically select any uploading attachments. // automatically select any uploading attachments.
if ( this.get('multiple') ) if ( this.get('multiple') )
@ -173,7 +190,8 @@
defaults: { defaults: {
id: 'gallery', id: 'gallery',
multiple: true, multiple: true,
describe: true describe: true,
title: l10n.createGallery
}, },
initialize: function() { initialize: function() {
@ -186,14 +204,19 @@
activate: function() { activate: function() {
var frame = this.frame; var frame = this.frame;
// Toolbar.
frame.toolbar( new media.view.Toolbar.Gallery({ frame.toolbar( new media.view.Toolbar.Gallery({
controller: frame, controller: frame,
editing: this.get('editing'), state: this
selection: this.get('selection')
}) ); }) );
// Sidebar.
frame.sidebar( new media.view.Sidebar({
controller: frame
}).render() );
// Content.
frame.content( new media.view.Attachments({ frame.content( new media.view.Attachments({
directions: 'Gallery time!',
controller: frame, controller: frame,
collection: this.get('selection'), collection: this.get('selection'),
sortable: true, sortable: true,
@ -245,7 +268,7 @@
}, },
render: function() { 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 ) if ( this.modal )
this.modal.render(); this.modal.render();
@ -634,16 +657,11 @@
// --------------------------------- // ---------------------------------
media.view.Toolbar.PostLibrary = media.view.Toolbar.extend({ media.view.Toolbar.PostLibrary = media.view.Toolbar.extend({
initialize: function() { initialize: function() {
var selection = this.options.selection, var state = this.options.state,
selection = state.get('selection'),
controller = this.options.controller; controller = this.options.controller;
this.options.items = { this.options.items = {
'selection-preview': new media.view.SelectionPreview({
controller: controller,
collection: selection,
priority: -40
}),
'create-new-gallery': { 'create-new-gallery': {
style: 'primary', style: 'primary',
text: l10n.createNewGallery, text: l10n.createNewGallery,
@ -662,7 +680,7 @@
text: l10n.insertIntoPost, text: l10n.insertIntoPost,
click: function() { click: function() {
controller.close(); controller.close();
controller.state().trigger( 'insert', selection ); state.trigger( 'insert', selection );
selection.clear(); selection.clear();
} }
}, },
@ -698,16 +716,16 @@
}; };
media.view.Toolbar.prototype.initialize.apply( this, arguments ); media.view.Toolbar.prototype.initialize.apply( this, arguments );
this.visibility();
}, },
visibility: function() { visibility: function() {
var selection = this.options.selection, var state = this.options.state,
selection = state.get('selection'),
controller = this.options.controller, controller = this.options.controller,
count = selection.length, count = selection.length,
showGallery; showGallery;
controller.$el.toggleClass( 'hide-toolbar', ! count );
// Check if every attachment in the selection is an image. // Check if every attachment in the selection is an image.
showGallery = count > 1 && selection.all( function( attachment ) { showGallery = count > 1 && selection.all( function( attachment ) {
return 'image' === attachment.get('type'); return 'image' === attachment.get('type');
@ -718,6 +736,8 @@
_.each( insert.buttons, function( button ) { _.each( insert.buttons, function( button ) {
button.model.set( 'style', showGallery ? '' : 'primary' ); 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({ media.view.Toolbar.Gallery = media.view.Toolbar.extend({
initialize: function() { initialize: function() {
var editing = this.options.editing, var state = this.options.state,
selection = this.options.selection, editing = state.get('editing'),
selection = state.get('selection'),
controller = this.options.controller; controller = this.options.controller;
this.options.items = { this.options.items = {
@ -736,7 +757,7 @@
priority: 40, priority: 40,
click: function() { click: function() {
controller.close(); controller.close();
controller.state().trigger( 'update', selection ); state.trigger( 'update', selection );
selection.clear(); selection.clear();
controller.state('library'); controller.state('library');
} }
@ -769,9 +790,10 @@
}, },
defaults: { defaults: {
text: '', text: '',
style: '', style: '',
size: 'large' size: 'large',
disabled: false
}, },
initialize: function() { initialize: function() {
@ -796,17 +818,19 @@
}, },
render: function() { render: function() {
var classes = [ 'button', this.className ]; var classes = [ 'button', this.className ],
model = this.model.toJSON();
if ( this.model.get('style') ) if ( model.style )
classes.push( 'button-' + this.model.get('style') ); classes.push( 'button-' + model.style );
if ( this.model.get('size') ) if ( model.size )
classes.push( 'button-' + this.model.get('size') ); classes.push( 'button-' + model.size );
classes = _.uniq( classes.concat( this.options.classes ) ); classes = _.uniq( classes.concat( this.options.classes ) );
this.el.className = classes.join(' '); this.el.className = classes.join(' ');
this.$el.attr( 'disabled', model.disabled );
// Detach the dropdown. // Detach the dropdown.
if ( this.options.dropdown ) if ( this.options.dropdown )
@ -822,7 +846,7 @@
click: function( event ) { click: function( event ) {
event.preventDefault(); event.preventDefault();
if ( this.options.click ) if ( this.options.click && ! this.model.get('disabled') )
this.options.click.apply( this, arguments ); 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 * wp.media.view.Attachment
*/ */
@ -1068,12 +1156,11 @@
* wp.media.view.Attachments * wp.media.view.Attachments
*/ */
media.view.Attachments = Backbone.View.extend({ media.view.Attachments = Backbone.View.extend({
tagName: 'div', tagName: 'ul',
className: 'attachments', className: 'attachments',
template: media.template('attachments'),
events: { events: {
'keyup .search': 'search' 'scroll': 'scroll'
}, },
initialize: function() { initialize: function() {
@ -1092,13 +1179,10 @@
}, this ); }, this );
}, this ); }, this );
this.collection.on( 'reset', this.refresh, this ); this.collection.on( 'reset', this.render, this );
this.$list = $('<ul />');
this.list = this.$list[0];
// Throttle the scroll handler.
this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value(); this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
this.$list.on( 'scroll.attachments', this.scroll );
this.initSortable(); this.initSortable();
}, },
@ -1110,13 +1194,13 @@
if ( ! this.options.sortable || ! $.fn.sortable ) if ( ! this.options.sortable || ! $.fn.sortable )
return; return;
this.$list.sortable({ this.$el.sortable({
// If the `collection` has a `comparator`, disable sorting. // If the `collection` has a `comparator`, disable sorting.
disabled: !! collection.comparator, disabled: !! collection.comparator,
// Prevent attachments from being dragged outside the bounding // Prevent attachments from being dragged outside the bounding
// box of the list. // box of the list.
containment: this.$list, containment: this.$el,
// Change the position of the attachment as soon as the // Change the position of the attachment as soon as the
// mouse pointer overlaps a thumbnail. // mouse pointer overlaps a thumbnail.
@ -1144,30 +1228,21 @@
// If the `orderby` property is changed on the `collection`, // If the `orderby` property is changed on the `collection`,
// check to see if we have a `comparator`. If so, disable sorting. // check to see if we have a `comparator`. If so, disable sorting.
collection.props.on( 'change:orderby', function() { collection.props.on( 'change:orderby', function() {
this.$list.sortable( 'option', 'disabled', !! collection.comparator ); this.$el.sortable( 'option', 'disabled', !! collection.comparator );
}, this ); }, this );
}, },
render: function() { 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 there are no elements, load some.
if ( ! this.collection.length ) { if ( ! this.collection.length ) {
this.collection.more(); this.collection.more();
this.$list.empty(); this.$el.empty();
return this; return this;
} }
// Otherwise, create all of the Attachment views, and replace // Otherwise, create all of the Attachment views, and replace
// the list in a single DOM operation. // 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({ return new this.options.AttachmentView({
controller: this.controller, controller: this.controller,
model: attachment model: attachment
@ -1188,37 +1263,57 @@
model: attachment model: attachment
}).render(); }).render();
children = this.$list.children(); children = this.$el.children();
if ( children.length > index ) if ( children.length > index )
children.eq( index ).before( view.$el ); children.eq( index ).before( view.$el );
else else
this.$list.append( view.$el ); this.$el.append( view.$el );
}, },
remove: function( attachment, index ) { remove: function( attachment, index ) {
var children = this.$list.children(); var children = this.$el.children();
if ( children.length ) if ( children.length )
children.eq( index ).detach(); children.eq( index ).detach();
}, },
scroll: function( event ) { scroll: function( event ) {
// @todo: is this still necessary? // @todo: is this still necessary?
if ( ! this.$list.is(':visible') ) if ( ! this.$el.is(':visible') )
return; 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(); 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 ) { search: function( event ) {
var props = this.collection.props;
if ( event.target.value ) if ( event.target.value )
props.set( 'search', event.target.value ); this.model.set( 'search', event.target.value );
else else
props.unset('search'); this.model.unset('search');
} }
}); });

View File

@ -1309,11 +1309,9 @@ function wp_print_media_templates( $attachment ) {
</div> </div>
</script> </script>
<script type="text/html" id="tmpl-attachments"> <script type="text/html" id="tmpl-sidebar">
<div class="attachments-header"> <h2 class="sidebar-title"><%- title %></h2>
<h3><%- directions %></h3> <div class="sidebar-content"></div>
<input class="search" type="text" placeholder="<?php esc_attr_e('Search'); ?>" />
</div>
</script> </script>
<script type="text/html" id="tmpl-attachment"> <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 ); $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( did_action( 'init' ) && $scripts->localize( 'media-views', '_wpMediaViewsL10n', array(
// Generic // Generic
'insertMedia' => __( 'Insert Media' ), 'insertMedia' => __( 'Insert Media' ),
'selectMediaSingular' => __( 'Select a media file:' ), 'search' => __( 'Search' ),
'selectMediaMultiple' => __( 'Select one or more media files:' ),
// Library // Library
'mediaLibrary' => __( 'Media Library' ),
'createNewGallery' => __( 'Create a new gallery' ), 'createNewGallery' => __( 'Create a new gallery' ),
'insertIntoPost' => __( 'Insert into post' ), 'insertIntoPost' => __( 'Insert into post' ),
'addToGallery' => __( 'Add to gallery' ), 'addToGallery' => __( 'Add to gallery' ),
// Gallery // Gallery
'createGallery' => __( 'Create Gallery' ),
'returnToLibrary' => __( 'Return to media library' ), 'returnToLibrary' => __( 'Return to media library' ),
'continueEditingGallery' => __( 'Continue editing gallery' ), 'continueEditingGallery' => __( 'Continue editing gallery' ),
'insertGalleryIntoPost' => __( 'Insert gallery into post' ), 'insertGalleryIntoPost' => __( 'Insert gallery into post' ),