diff --git a/wp-admin/includes/ajax-actions.php b/wp-admin/includes/ajax-actions.php index e8814e82c3..4e72d1ec3e 100644 --- a/wp-admin/includes/ajax-actions.php +++ b/wp-admin/includes/ajax-actions.php @@ -1674,23 +1674,31 @@ function wp_ajax_image_editor() { } function wp_ajax_set_post_thumbnail() { + $json = ! empty( $_REQUEST['json'] ); + $post_ID = intval( $_POST['post_id'] ); - if ( !current_user_can( 'edit_post', $post_ID ) ) - wp_die( -1 ); + if ( !current_user_can( 'edit_post', $post_ID ) ) { + $json ? wp_send_json_error() : wp_die( -1 ); + } $thumbnail_id = intval( $_POST['thumbnail_id'] ); check_ajax_referer( "set_post_thumbnail-$post_ID" ); if ( $thumbnail_id == '-1' ) { - if ( delete_post_thumbnail( $post_ID ) ) - wp_die( _wp_post_thumbnail_html( null, $post_ID ) ); - else - wp_die( 0 ); + if ( delete_post_thumbnail( $post_ID ) ) { + $return = _wp_post_thumbnail_html( null, $post_ID ); + $json ? wp_send_json_success( $return ) : wp_die( $return ); + } else { + $json ? wp_send_json_error() : wp_die( 0 ); + } } - if ( set_post_thumbnail( $post_ID, $thumbnail_id ) ) - wp_die( _wp_post_thumbnail_html( $thumbnail_id, $post_ID ) ); - wp_die( 0 ); + if ( set_post_thumbnail( $post_ID, $thumbnail_id ) ) { + $return = _wp_post_thumbnail_html( $thumbnail_id, $post_ID ); + $json ? wp_send_json_success( $return ) : wp_die( $return ); + } + + $json ? wp_send_json_error() : wp_die( 0 ); } function wp_ajax_date_format() { diff --git a/wp-admin/includes/meta-boxes.php b/wp-admin/includes/meta-boxes.php index cf71d3064b..4669a7db44 100644 --- a/wp-admin/includes/meta-boxes.php +++ b/wp-admin/includes/meta-boxes.php @@ -1001,119 +1001,6 @@ function link_advanced_meta_box($link) { * @since 2.9.0 */ function post_thumbnail_meta_box( $post ) { - global $_wp_additional_image_sizes; - - ?> - - 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'; - - ?>
- ID, '_thumbnail_id', true ); + echo _wp_post_thumbnail_html( $thumbnail_id, $post->ID ); } \ No newline at end of file diff --git a/wp-admin/includes/post.php b/wp-admin/includes/post.php index 5a4d966258..f0b7b4cded 100644 --- a/wp-admin/includes/post.php +++ b/wp-admin/includes/post.php @@ -199,14 +199,6 @@ function edit_post( $post_data = null ) { 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 if ( isset($post_data['meta']) && $post_data['meta'] ) { foreach ( $post_data['meta'] as $key => $value ) { diff --git a/wp-admin/js/custom-background.js b/wp-admin/js/custom-background.js index 5409b64991..7656ce455e 100644 --- a/wp-admin/js/custom-background.js +++ b/wp-admin/js/custom-background.js @@ -57,7 +57,7 @@ }); }); - frame.setState('library'); + frame.setState('library').open(); }); }); })(jQuery); \ No newline at end of file diff --git a/wp-admin/js/custom-header.js b/wp-admin/js/custom-header.js index 9a806141eb..40c0128249 100644 --- a/wp-admin/js/custom-header.js +++ b/wp-admin/js/custom-header.js @@ -40,7 +40,7 @@ }); }); - frame.setState('library'); + frame.setState('library').open(); }); }); }(jQuery)); diff --git a/wp-includes/css/media-views.css b/wp-includes/css/media-views.css index cdf1af7d64..42a2aa3857 100644 --- a/wp-includes/css/media-views.css +++ b/wp-includes/css/media-views.css @@ -403,6 +403,11 @@ */ .media-frame { overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; } .media-frame .region-content { diff --git a/wp-includes/js/media-editor.js b/wp-includes/js/media-editor.js index 6d3046420a..7078b8cc35 100644 --- a/wp-includes/js/media-editor.js +++ b/wp-includes/js/media-editor.js @@ -375,6 +375,7 @@ workflow = workflows[ id ] = wp.media( _.defaults( options || {}, { frame: 'post', + state: 'upload', title: wp.media.view.l10n.addMedia, multiple: true } ) ); @@ -427,14 +428,52 @@ } }, 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; }, + 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 ) { + id = this.id( id ); return workflows[ id ]; }, remove: function( id ) { + id = this.id( 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() { $(document.body).on( 'click', '.insert-media', function( event ) { var $this = $(this), @@ -513,45 +576,40 @@ wp.media.editor.open( editor ); }); - }, - open: function( id ) { - var workflow, editor; + // Open the content media manager to the 'featured image' tab when + // 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`. - id = id || wpActiveEditor; + // Always get the 'content' frame, since this is tailored to post.php. + var frame = wp.media.editor.add('content'), + initialState = frame.state().id, + escape; - if ( typeof tinymce !== 'undefined' && tinymce.activeEditor ) { - // If that doesn't work, fall back to `tinymce.activeEditor`. - if ( ! id ) { - editor = tinymce.activeEditor; - id = id || editor.id; - } else { - editor = tinymce.get( id ); - } + escape = function() { + // Only run this event once. + this.off( 'escape', escape ); - // Save a bookmark of the caret position, needed for IE - if ( tinymce.isIE && editor && ! editor.isHidden() ) { - editor.focus(); - editor.windowManager.insertimagebookmark = editor.selection.getBookmark(); - } - } + // If we're still on the 'featured-image' state, restore + // the initial state. + if ( 'featured-image' === this.state().id ) + this.setState( initialState ); + }; - // Last but not least, fall back to the empty string. - id = id || ''; + frame.on( 'escape', escape, frame ); - workflow = wp.media.editor.get( id ); + frame.setState('featured-image').open(); - // If the workflow exists, open it. - // Initialize the editor's workflow if we haven't yet. - if ( workflow ) - workflow.open(); - else - workflow = wp.media.editor.add( id ); - - return workflow; + // Update the featured image id when the 'remove' link is clicked. + }).on( 'click', '#remove-post-thumbnail', function() { + wp.media.view.settings.featuredImage.id = -1; + }); } }; + _.bindAll( wp.media.editor, 'open' ); $( wp.media.editor.init ); }(jQuery)); diff --git a/wp-includes/js/media-models.js b/wp-includes/js/media-models.js index a6c8c01912..cb4b6b6ecd 100644 --- a/wp-includes/js/media-models.js +++ b/wp-includes/js/media-models.js @@ -30,10 +30,8 @@ window.wp = window.wp || {}; frame = new MediaFrame.Post( attributes ); delete attributes.frame; - // Set the default state. - frame.setState( frame.options.state ); - // Render, attach, and open the frame. - return frame.render().attach().open(); + + return frame; }; _.extend( media, { model: {}, view: {}, controller: {} }); @@ -235,6 +233,9 @@ window.wp = window.wp || {}; // Overload the `update` request so properties can be saved. } else if ( 'update' === method ) { + if ( ! this.get('nonces') ) + return $.Deferred().resolveWith( this ).promise(); + options = options || {}; options.context = this; diff --git a/wp-includes/js/media-views.js b/wp-includes/js/media-views.js index f995972444..405d23a620 100644 --- a/wp-includes/js/media-views.js +++ b/wp-includes/js/media-views.js @@ -170,7 +170,7 @@ // created the `states` collection, or are trying to select a state // that does not exist. if ( ( previous && id === previous.id ) || ! this.states || ! this.states.get( id ) ) - return; + return this; if ( previous ) { previous.trigger('deactivate'); @@ -179,6 +179,8 @@ this._state = id; this.state().trigger('activate'); + + return this; }, // 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 // ------------------------- @@ -605,7 +641,9 @@ }, this ); 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 ) { this.modal = new media.view.Modal({ controller: this, - $content: this.$el, title: this.options.title }); + + this.modal.content( this ); } // 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 ); }, - render: function() { - if ( this.modal ) - this.modal.render(); - - media.view.Frame.prototype.render.apply( this, arguments ); - return this; - }, - createIframeStates: function( options ) { var settings = media.view.settings, tabs = settings.tabs, @@ -1186,7 +1217,7 @@ }); // 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 ) { if ( this.modal ) this.modal[ method ].apply( this.modal, arguments ); @@ -1202,7 +1233,6 @@ media.view.MediaFrame.prototype.initialize.apply( this, arguments ); _.defaults( this.options, { - state: 'upload', selection: [], library: {}, multiple: false @@ -1349,7 +1379,6 @@ media.view.MediaFrame.Post = media.view.MediaFrame.Select.extend({ initialize: function() { _.defaults( this.options, { - state: 'upload', multiple: true, editing: false }); @@ -1409,6 +1438,14 @@ libraryState: 'gallery-edit' }) ]); + + + if ( media.view.settings.featuredImage ) { + this.states.add( new media.controller.FeaturedImage({ + controller: this, + menu: 'main' + }) ); + } }, bindHandlers: function() { @@ -1427,6 +1464,7 @@ toolbar: { 'main-attachments': 'mainAttachmentsToolbar', 'main-embed': 'mainEmbedToolbar', + 'featured-image': 'featuredImageToolbar', 'gallery-edit': 'galleryEditToolbar', 'gallery-add': 'galleryAddToolbar' } @@ -1444,15 +1482,22 @@ media.view.MediaFrame.Select.prototype.mainMenu.call( this, { silent: true }); this.menu.view().set({ - separateLibrary: new media.View({ + 'library-separator': new media.View({ className: 'separator', priority: 60 }), - embed: { + 'embed': { text: l10n.fromUrlTitle, priority: 80 } }); + + if ( media.view.settings.featuredImage ) { + this.menu.view().set( 'featured-image', { + text: l10n.featuredImageTitle, + priority: 100 + }); + } }, 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() { this.toolbar.view( new media.view.Toolbar.Embed({ controller: this @@ -1629,7 +1682,7 @@ }, events: { - 'click .media-modal-backdrop, .media-modal-close': 'closeHandler', + 'click .media-modal-backdrop, .media-modal-close': 'escapeHandler', 'keydown': 'keydown' }, @@ -1643,56 +1696,73 @@ }); }, - render: function() { - // Ensure content div exists. - this.options.$content = this.options.$content || $(''); - - // 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({ + prepare: function() { + return { title: this.options.title - }) ); - - this.options.$content.addClass('media-modal-content'); - this.$('.media-modal').append( this.options.$content ); - return this; + }; }, attach: function() { + if ( this.views.attached ) + return this; + + if ( ! this.views.rendered ) + this.render(); + 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'); }, detach: function() { + if ( this.$el.is(':visible') ) + this.close(); + this.$el.detach(); + this.views.attached = false; return this.propagate('detach'); }, open: function() { + if ( this.$el.is(':visible') ) + return this; + + if ( ! this.views.attached ) + this.attach(); + this.$el.show().focus(); return this.propagate('open'); }, - close: function() { + close: function( options ) { + if ( ! this.views.attached || ! this.$el.is(':visible') ) + return this; + 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(); - this.close(); + this.escape(); }, - content: function( $content ) { - // Detach any existing content to prevent events from being lost. - if ( this.options.$content ) - this.options.$content.detach(); - - // Set and render the content. - this.options.$content = ( $content instanceof Backbone.View ) ? $content.$el : $content; - return this.render(); + content: function( content ) { + this.views.set( '.media-modal-content', content ); + return this; }, // Triggers a modal event and if the `propagate` option is set, @@ -1710,7 +1780,7 @@ // Close the modal when escape is pressed. if ( 27 === event.which ) { event.preventDefault(); - this.close(); + this.escape(); return; } } diff --git a/wp-includes/media.php b/wp-includes/media.php index 66e50428d8..a2281e2a92 100644 --- a/wp-includes/media.php +++ b/wp-includes/media.php @@ -1434,6 +1434,16 @@ function wp_enqueue_media( $args = array() ) { $post = get_post( $args['post'] ); $settings['postId'] = $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 ); @@ -1467,6 +1477,10 @@ function wp_enqueue_media( $args = array() ) { // From URL 'fromUrlTitle' => __( 'From URL' ), + // Featured Images + 'featuredImageTitle' => __( 'Featured Image' ), + 'setFeaturedImage' => __( 'Set featured image' ), + // Gallery 'createGalleryTitle' => __( 'Create Gallery' ), 'editGalleryTitle' => __( 'Edit Gallery' ), @@ -1511,6 +1525,7 @@ function wp_print_media_templates() {