Bring Featured Images back into the main media dialog.

Most users don't realize that the Featured Image meta box exists; if they do, few use it.

Restores the old meta box UI, including the admin_post_thumbnail_html filter. If a plugin is using _wp_post_thumbnail_html() in conjunction with Thickbox elsewhere, it will also magically still work.

Specific underlying changes:
 * Converts the modal view to use the view manager, which means that a call to open() will automatically call render and attach if necessary.
 * Doesn't automatically set a state in wp.media, to allow code to customize the states to be added before activation.

props koopersmith.
fixes #21776.



git-svn-id: https://develop.svn.wordpress.org/trunk@22979 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Andrew Nacin 2012-12-03 02:38:10 +00:00
parent 8a6d1c5b89
commit f88b538001
10 changed files with 248 additions and 212 deletions

View File

@ -1674,23 +1674,31 @@ function wp_ajax_image_editor() {
} }
function wp_ajax_set_post_thumbnail() { function wp_ajax_set_post_thumbnail() {
$json = ! empty( $_REQUEST['json'] );
$post_ID = intval( $_POST['post_id'] ); $post_ID = intval( $_POST['post_id'] );
if ( !current_user_can( 'edit_post', $post_ID ) ) if ( !current_user_can( 'edit_post', $post_ID ) ) {
wp_die( -1 ); $json ? wp_send_json_error() : wp_die( -1 );
}
$thumbnail_id = intval( $_POST['thumbnail_id'] ); $thumbnail_id = intval( $_POST['thumbnail_id'] );
check_ajax_referer( "set_post_thumbnail-$post_ID" ); check_ajax_referer( "set_post_thumbnail-$post_ID" );
if ( $thumbnail_id == '-1' ) { if ( $thumbnail_id == '-1' ) {
if ( delete_post_thumbnail( $post_ID ) ) if ( delete_post_thumbnail( $post_ID ) ) {
wp_die( _wp_post_thumbnail_html( null, $post_ID ) ); $return = _wp_post_thumbnail_html( null, $post_ID );
else $json ? wp_send_json_success( $return ) : wp_die( $return );
wp_die( 0 ); } else {
$json ? wp_send_json_error() : wp_die( 0 );
}
} }
if ( set_post_thumbnail( $post_ID, $thumbnail_id ) ) if ( set_post_thumbnail( $post_ID, $thumbnail_id ) ) {
wp_die( _wp_post_thumbnail_html( $thumbnail_id, $post_ID ) ); $return = _wp_post_thumbnail_html( $thumbnail_id, $post_ID );
wp_die( 0 ); $json ? wp_send_json_success( $return ) : wp_die( $return );
}
$json ? wp_send_json_error() : wp_die( 0 );
} }
function wp_ajax_date_format() { function wp_ajax_date_format() {

View File

@ -1001,119 +1001,6 @@ function link_advanced_meta_box($link) {
* @since 2.9.0 * @since 2.9.0
*/ */
function post_thumbnail_meta_box( $post ) { function post_thumbnail_meta_box( $post ) {
global $_wp_additional_image_sizes; $thumbnail_id = get_post_meta( $post->ID, '_thumbnail_id', true );
echo _wp_post_thumbnail_html( $thumbnail_id, $post->ID );
?><script type="text/javascript">
jQuery( function($) {
var $element = $('#select-featured-image'),
$thumbnailId = $element.find('input[name="thumbnail_id"]'),
title = '<?php _e( "Choose a Featured Image" ); ?>',
update = '<?php _e( "Update Featured Image" ); ?>',
Attachment = wp.media.model.Attachment,
frame, setFeaturedImage;
setFeaturedImage = function( thumbnailId ) {
var selection;
$element.find('img').remove();
$element.toggleClass( 'has-featured-image', -1 != thumbnailId );
$thumbnailId.val( thumbnailId );
if ( frame ) {
selection = frame.state('library').get('selection');
if ( -1 === thumbnailId )
selection.clear();
else
selection.add( Attachment.get( thumbnailId ) );
}
};
$element.on( 'click', '.choose, img', function( event ) {
var options, thumbnailId, attachment;
event.preventDefault();
if ( frame ) {
frame.open();
return;
}
options = {
title: title,
library: {
type: 'image'
}
};
thumbnailId = $thumbnailId.val();
if ( '' !== thumbnailId && -1 !== thumbnailId ) {
attachment = Attachment.get( thumbnailId );
attachment.fetch();
options.selection = [ attachment ];
}
frame = wp.media( options );
frame.state('library').set( 'filterable', 'uploaded' );
frame.toolbar.on( 'activate:select', function() {
frame.toolbar.view().set({
select: {
style: 'primary',
text: update,
click: function() {
var selection = frame.state().get('selection'),
model = selection.first(),
sizes = model.get('sizes'),
size;
setFeaturedImage( model.id );
// @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();
frame.close();
$( '<img />', {
src: size.url,
width: size.width
}).prependTo( $element );
}
}
});
});
frame.toolbar.mode('select');
});
$element.on( 'click', '.remove', function( event ) {
event.preventDefault();
setFeaturedImage( -1 );
});
});
</script>
<?php
$thumbnail_id = get_post_meta( $post->ID, '_thumbnail_id', true );
$thumbnail_size = isset( $_wp_additional_image_sizes['post-thumbnail'] ) ? 'post-thumbnail' : 'medium';
$thumbnail_html = wp_get_attachment_image( $thumbnail_id, $thumbnail_size );
$classes = empty( $thumbnail_id ) ? '' : 'has-featured-image';
?><div id="select-featured-image"
class="<?php echo esc_attr( $classes ); ?>"
data-post-id="<?php echo esc_attr( $post->ID ); ?>">
<?php echo $thumbnail_html; ?>
<input type="hidden" name="thumbnail_id" value="<?php echo esc_attr( $thumbnail_id ); ?>" />
<a href="#" class="choose button-secondary"><?php _e( 'Choose a Featured Image' ); ?></a>
<a href="#" class="remove"><?php _e( 'Remove Featured Image' ); ?></a>
</div>
<?php
} }

View File

@ -199,14 +199,6 @@ function edit_post( $post_data = null ) {
set_post_format( $post_ID, false ); set_post_format( $post_ID, false );
} }
// Featured Images
if ( isset( $post_data['thumbnail_id'] ) ) {
if ( '-1' == $post_data['thumbnail_id'] )
delete_post_thumbnail( $post_ID );
else
set_post_thumbnail( $post_ID, $post_data['thumbnail_id'] );
}
// Meta Stuff // Meta Stuff
if ( isset($post_data['meta']) && $post_data['meta'] ) { if ( isset($post_data['meta']) && $post_data['meta'] ) {
foreach ( $post_data['meta'] as $key => $value ) { foreach ( $post_data['meta'] as $key => $value ) {

View File

@ -57,7 +57,7 @@
}); });
}); });
frame.setState('library'); frame.setState('library').open();
}); });
}); });
})(jQuery); })(jQuery);

View File

@ -40,7 +40,7 @@
}); });
}); });
frame.setState('library'); frame.setState('library').open();
}); });
}); });
}(jQuery)); }(jQuery));

View File

@ -403,6 +403,11 @@
*/ */
.media-frame { .media-frame {
overflow: hidden; overflow: hidden;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
} }
.media-frame .region-content { .media-frame .region-content {

View File

@ -375,6 +375,7 @@
workflow = workflows[ id ] = wp.media( _.defaults( options || {}, { workflow = workflows[ id ] = wp.media( _.defaults( options || {}, {
frame: 'post', frame: 'post',
state: 'upload',
title: wp.media.view.l10n.addMedia, title: wp.media.view.l10n.addMedia,
multiple: true multiple: true
} ) ); } ) );
@ -427,14 +428,52 @@
} }
}, this ); }, this );
workflow.state('featured-image').on( 'select', function() {
var settings = wp.media.view.settings,
featuredImage = settings.featuredImage,
selection = this.get('selection').single();
if ( ! featuredImage )
return;
featuredImage.id = selection ? selection.id : -1;
wp.media.post( 'set-post-thumbnail', {
json: true,
post_id: settings.postId,
thumbnail_id: featuredImage.id,
_wpnonce: featuredImage.nonce
}).done( function( html ) {
$( '.inside', '#postimagediv' ).html( html );
});
});
workflow.setState( workflow.options.state );
return workflow; return workflow;
}, },
id: function( id ) {
if ( id )
return id;
// If an empty `id` is provided, default to `wpActiveEditor`.
id = wpActiveEditor;
// If that doesn't work, fall back to `tinymce.activeEditor.id`.
if ( ! id && typeof tinymce !== 'undefined' && tinymce.activeEditor )
id = tinymce.activeEditor.id;
// Last but not least, fall back to the empty string.
id = id || '';
return id;
},
get: function( id ) { get: function( id ) {
id = this.id( id );
return workflows[ id ]; return workflows[ id ];
}, },
remove: function( id ) { remove: function( id ) {
id = this.id( id );
delete workflows[ id ]; delete workflows[ id ];
}, },
@ -497,6 +536,30 @@
} }
}, },
open: function( id ) {
var workflow, editor;
id = this.id( id );
// Save a bookmark of the caret position in IE.
if ( typeof tinymce !== 'undefined' ) {
editor = tinymce.get( id );
if ( tinymce.isIE && editor && ! editor.isHidden() ) {
editor.focus();
editor.windowManager.insertimagebookmark = editor.selection.getBookmark();
}
}
workflow = this.get( id );
// Initialize the editor's workflow if we haven't yet.
if ( ! workflow )
workflow = this.add( id );
return workflow.open();
},
init: function() { init: function() {
$(document.body).on( 'click', '.insert-media', function( event ) { $(document.body).on( 'click', '.insert-media', function( event ) {
var $this = $(this), var $this = $(this),
@ -513,45 +576,40 @@
wp.media.editor.open( editor ); wp.media.editor.open( editor );
}); });
},
open: function( id ) { // Open the content media manager to the 'featured image' tab when
var workflow, editor; // the post thumbnail is clicked.
$('#postimagediv').on( 'click', '#set-post-thumbnail', function( event ) {
event.preventDefault();
// Stop propagation to prevent thickbox from activating.
event.stopPropagation();
// If an empty `id` is provided, default to `wpActiveEditor`. // Always get the 'content' frame, since this is tailored to post.php.
id = id || wpActiveEditor; var frame = wp.media.editor.add('content'),
initialState = frame.state().id,
escape;
if ( typeof tinymce !== 'undefined' && tinymce.activeEditor ) { escape = function() {
// If that doesn't work, fall back to `tinymce.activeEditor`. // Only run this event once.
if ( ! id ) { this.off( 'escape', escape );
editor = tinymce.activeEditor;
id = id || editor.id;
} else {
editor = tinymce.get( id );
}
// Save a bookmark of the caret position, needed for IE // If we're still on the 'featured-image' state, restore
if ( tinymce.isIE && editor && ! editor.isHidden() ) { // the initial state.
editor.focus(); if ( 'featured-image' === this.state().id )
editor.windowManager.insertimagebookmark = editor.selection.getBookmark(); this.setState( initialState );
} };
}
// Last but not least, fall back to the empty string. frame.on( 'escape', escape, frame );
id = id || '';
workflow = wp.media.editor.get( id ); frame.setState('featured-image').open();
// If the workflow exists, open it. // Update the featured image id when the 'remove' link is clicked.
// Initialize the editor's workflow if we haven't yet. }).on( 'click', '#remove-post-thumbnail', function() {
if ( workflow ) wp.media.view.settings.featuredImage.id = -1;
workflow.open(); });
else
workflow = wp.media.editor.add( id );
return workflow;
} }
}; };
_.bindAll( wp.media.editor, 'open' );
$( wp.media.editor.init ); $( wp.media.editor.init );
}(jQuery)); }(jQuery));

View File

@ -30,10 +30,8 @@ window.wp = window.wp || {};
frame = new MediaFrame.Post( attributes ); frame = new MediaFrame.Post( attributes );
delete attributes.frame; delete attributes.frame;
// Set the default state.
frame.setState( frame.options.state ); return frame;
// Render, attach, and open the frame.
return frame.render().attach().open();
}; };
_.extend( media, { model: {}, view: {}, controller: {} }); _.extend( media, { model: {}, view: {}, controller: {} });
@ -235,6 +233,9 @@ window.wp = window.wp || {};
// Overload the `update` request so properties can be saved. // Overload the `update` request so properties can be saved.
} else if ( 'update' === method ) { } else if ( 'update' === method ) {
if ( ! this.get('nonces') )
return $.Deferred().resolveWith( this ).promise();
options = options || {}; options = options || {};
options.context = this; options.context = this;

View File

@ -170,7 +170,7 @@
// created the `states` collection, or are trying to select a state // created the `states` collection, or are trying to select a state
// that does not exist. // that does not exist.
if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) )
return; return this;
if ( previous ) { if ( previous ) {
previous.trigger('deactivate'); previous.trigger('deactivate');
@ -179,6 +179,8 @@
this._state = id; this._state = id;
this.state().trigger('activate'); this.state().trigger('activate');
return this;
}, },
// Returns the previous active state. // Returns the previous active state.
@ -549,6 +551,40 @@
} }
}); });
// wp.media.controller.FeaturedImage
// ---------------------------------
media.controller.FeaturedImage = media.controller.Library.extend({
defaults: _.defaults({
id: 'featured-image',
filterable: 'uploaded',
multiple: false,
menu: 'main',
toolbar: 'featured-image'
}, media.controller.Library.prototype.defaults ),
initialize: function() {
// If we haven't been provided a `library`, create a `Selection`.
if ( ! this.get('library') )
this.set( 'library', media.query({ type: 'image' }) );
media.controller.Library.prototype.initialize.apply( this, arguments );
},
activate: function() {
var selection = this.get('selection'),
id = media.view.settings.featuredImage.id,
attachment;
if ( '' !== id && -1 !== id ) {
attachment = Attachment.get( id );
attachment.fetch();
}
selection.reset( attachment ? [ attachment ] : [] );
media.controller.Library.prototype.activate.apply( this, arguments );
}
});
// wp.media.controller.Embed // wp.media.controller.Embed
// ------------------------- // -------------------------
@ -605,7 +641,9 @@
}, this ); }, this );
this.set( 'url', '' ); this.set( 'url', '' );
this.frame.toolbar.view().refresh();
if ( this.id === this.frame.state().id )
this.frame.toolbar.view().refresh();
} }
}); });
@ -1077,9 +1115,10 @@
if ( this.options.modal ) { if ( this.options.modal ) {
this.modal = new media.view.Modal({ this.modal = new media.view.Modal({
controller: this, controller: this,
$content: this.$el,
title: this.options.title title: this.options.title
}); });
this.modal.content( this );
} }
// Force the uploader off if the upload limit has been exceeded or // Force the uploader off if the upload limit has been exceeded or
@ -1102,14 +1141,6 @@
this.on( 'attach', _.bind( this.views.ready, this.views ), this ); this.on( 'attach', _.bind( this.views.ready, this.views ), this );
}, },
render: function() {
if ( this.modal )
this.modal.render();
media.view.Frame.prototype.render.apply( this, arguments );
return this;
},
createIframeStates: function( options ) { createIframeStates: function( options ) {
var settings = media.view.settings, var settings = media.view.settings,
tabs = settings.tabs, tabs = settings.tabs,
@ -1186,7 +1217,7 @@
}); });
// Map some of the modal's methods to the frame. // Map some of the modal's methods to the frame.
_.each(['open','close','attach','detach'], function( method ) { _.each(['open','close','attach','detach','escape'], function( method ) {
media.view.MediaFrame.prototype[ method ] = function( view ) { media.view.MediaFrame.prototype[ method ] = function( view ) {
if ( this.modal ) if ( this.modal )
this.modal[ method ].apply( this.modal, arguments ); this.modal[ method ].apply( this.modal, arguments );
@ -1202,7 +1233,6 @@
media.view.MediaFrame.prototype.initialize.apply( this, arguments ); media.view.MediaFrame.prototype.initialize.apply( this, arguments );
_.defaults( this.options, { _.defaults( this.options, {
state: 'upload',
selection: [], selection: [],
library: {}, library: {},
multiple: false multiple: false
@ -1349,7 +1379,6 @@
media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({ media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({
initialize: function() { initialize: function() {
_.defaults( this.options, { _.defaults( this.options, {
state: 'upload',
multiple: true, multiple: true,
editing: false editing: false
}); });
@ -1409,6 +1438,14 @@
libraryState: 'gallery-edit' libraryState: 'gallery-edit'
}) })
]); ]);
if ( media.view.settings.featuredImage ) {
this.states.add( new media.controller.FeaturedImage({
controller: this,
menu: 'main'
}) );
}
}, },
bindHandlers: function() { bindHandlers: function() {
@ -1427,6 +1464,7 @@
toolbar: { toolbar: {
'main-attachments': 'mainAttachmentsToolbar', 'main-attachments': 'mainAttachmentsToolbar',
'main-embed': 'mainEmbedToolbar', 'main-embed': 'mainEmbedToolbar',
'featured-image': 'featuredImageToolbar',
'gallery-edit': 'galleryEditToolbar', 'gallery-edit': 'galleryEditToolbar',
'gallery-add': 'galleryAddToolbar' 'gallery-add': 'galleryAddToolbar'
} }
@ -1444,15 +1482,22 @@
media.view.MediaFrame.Select.prototype.mainMenu.call( this, { silent: true }); media.view.MediaFrame.Select.prototype.mainMenu.call( this, { silent: true });
this.menu.view().set({ this.menu.view().set({
separateLibrary: new media.View({ 'library-separator': new media.View({
className: 'separator', className: 'separator',
priority: 60 priority: 60
}), }),
embed: { 'embed': {
text: l10n.fromUrlTitle, text: l10n.fromUrlTitle,
priority: 80 priority: 80
} }
}); });
if ( media.view.settings.featuredImage ) {
this.menu.view().set( 'featured-image', {
text: l10n.featuredImageTitle,
priority: 100
});
}
}, },
galleryMenu: function() { galleryMenu: function() {
@ -1559,6 +1604,14 @@
}) ); }) );
}, },
featuredImageToolbar: function() {
this.toolbar.view( new media.view.Toolbar.Select({
controller: this,
text: l10n.setFeaturedImage,
state: this.options.state || 'upload'
}) );
},
mainEmbedToolbar: function() { mainEmbedToolbar: function() {
this.toolbar.view( new media.view.Toolbar.Embed({ this.toolbar.view( new media.view.Toolbar.Embed({
controller: this controller: this
@ -1629,7 +1682,7 @@
}, },
events: { events: {
'click .media-modal-backdrop, .media-modal-close': 'closeHandler', 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler',
'keydown': 'keydown' 'keydown': 'keydown'
}, },
@ -1643,56 +1696,73 @@
}); });
}, },
render: function() { prepare: function() {
// Ensure content div exists. return {
this.options.$content = this.options.$content || $('<div />');
// Detach the content element from the DOM to prevent
// `this.$el.html()` from garbage collecting its events.
this.options.$content.detach();
this.$el.html( this.template({
title: this.options.title title: this.options.title
}) ); };
this.options.$content.addClass('media-modal-content');
this.$('.media-modal').append( this.options.$content );
return this;
}, },
attach: function() { attach: function() {
if ( this.views.attached )
return this;
if ( ! this.views.rendered )
this.render();
this.$el.appendTo( this.options.container ); this.$el.appendTo( this.options.container );
// Manually mark the view as attached and trigger ready.
this.views.attached = true;
this.views.ready();
return this.propagate('attach'); return this.propagate('attach');
}, },
detach: function() { detach: function() {
if ( this.$el.is(':visible') )
this.close();
this.$el.detach(); this.$el.detach();
this.views.attached = false;
return this.propagate('detach'); return this.propagate('detach');
}, },
open: function() { open: function() {
if ( this.$el.is(':visible') )
return this;
if ( ! this.views.attached )
this.attach();
this.$el.show().focus(); this.$el.show().focus();
return this.propagate('open'); return this.propagate('open');
}, },
close: function() { close: function( options ) {
if ( ! this.views.attached || ! this.$el.is(':visible') )
return this;
this.$el.hide(); this.$el.hide();
return this.propagate('close'); this.propagate('close');
if ( options && options.escape )
this.propagate('escape');
return this;
}, },
closeHandler: function( event ) { escape: function() {
return this.close({ escape: true });
},
escapeHandler: function( event ) {
event.preventDefault(); event.preventDefault();
this.close(); this.escape();
}, },
content: function( $content ) { content: function( content ) {
// Detach any existing content to prevent events from being lost. this.views.set( '.media-modal-content', content );
if ( this.options.$content ) return this;
this.options.$content.detach();
// Set and render the content.
this.options.$content = ( $content instanceof Backbone.View ) ? $content.$el : $content;
return this.render();
}, },
// Triggers a modal event and if the `propagate` option is set, // Triggers a modal event and if the `propagate` option is set,
@ -1710,7 +1780,7 @@
// Close the modal when escape is pressed. // Close the modal when escape is pressed.
if ( 27 === event.which ) { if ( 27 === event.which ) {
event.preventDefault(); event.preventDefault();
this.close(); this.escape();
return; return;
} }
} }

View File

@ -1434,6 +1434,16 @@ function wp_enqueue_media( $args = array() ) {
$post = get_post( $args['post'] ); $post = get_post( $args['post'] );
$settings['postId'] = $post->ID; $settings['postId'] = $post->ID;
$settings['nonce']['updatePost'] = wp_create_nonce( 'update-post_' . $post->ID ); $settings['nonce']['updatePost'] = wp_create_nonce( 'update-post_' . $post->ID );
if ( current_theme_supports( 'post-thumbnails', $post->post_type ) && post_type_supports( $post->post_type, 'thumbnail' ) ) {
$featuredImageId = get_post_meta( $post->ID, '_thumbnail_id', true );
$settings['featuredImage'] = array(
'id' => $featuredImageId ? $featuredImageId : -1,
'nonce' => wp_create_nonce( 'set_post_thumbnail-' . $post->ID ),
);
}
} }
$hier = $post && is_post_type_hierarchical( $post->post_type ); $hier = $post && is_post_type_hierarchical( $post->post_type );
@ -1467,6 +1477,10 @@ function wp_enqueue_media( $args = array() ) {
// From URL // From URL
'fromUrlTitle' => __( 'From URL' ), 'fromUrlTitle' => __( 'From URL' ),
// Featured Images
'featuredImageTitle' => __( 'Featured Image' ),
'setFeaturedImage' => __( 'Set featured image' ),
// Gallery // Gallery
'createGalleryTitle' => __( 'Create Gallery' ), 'createGalleryTitle' => __( 'Create Gallery' ),
'editGalleryTitle' => __( 'Edit Gallery' ), 'editGalleryTitle' => __( 'Edit Gallery' ),
@ -1511,6 +1525,7 @@ function wp_print_media_templates() {
<div class="media-modal wp-core-ui"> <div class="media-modal wp-core-ui">
<h3 class="media-modal-title">{{ data.title }}</h3> <h3 class="media-modal-title">{{ data.title }}</h3>
<a class="media-modal-close media-modal-icon" href="#" title="<?php esc_attr_e('Close'); ?>"></a> <a class="media-modal-close media-modal-icon" href="#" title="<?php esc_attr_e('Close'); ?>"></a>
<div class="media-modal-content"></div>
</div> </div>
<div class="media-modal-backdrop"> <div class="media-modal-backdrop">
<div></div> <div></div>