diff --git a/composer.json b/composer.json index e2fc94b..5b9f2a6 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "xwp/wp-customize-posts", "description": "Manage posts and postmeta via the Customizer.", - "version": "0.8.4", + "version": "0.8.5", "type": "wordpress-plugin", "keywords": [ "customizer", "customize", "posts", "postmeta", "preview", "featured-image", "page-template" ], "homepage": "https://github.com/xwp/wp-customize-posts/", diff --git a/customize-posts.php b/customize-posts.php index 54172fa..01e4404 100644 --- a/customize-posts.php +++ b/customize-posts.php @@ -3,7 +3,7 @@ * Plugin Name: Customize Posts * Description: Manage posts and postmeta via the Customizer. Works best in conjunction with the Customize Setting Validation plugin. * Plugin URI: https://github.com/xwp/wp-customize-posts/ - * Version: 0.8.4 + * Version: 0.8.5 * Author: XWP * Author URI: https://make.xwp.co/ * License: GPLv2+ diff --git a/js/customize-post-field-partial.js b/js/customize-post-field-partial.js index 59a6abd..7b39d20 100644 --- a/js/customize-post-field-partial.js +++ b/js/customize-post-field-partial.js @@ -35,8 +35,6 @@ api.selectiveRefresh.partialConstructor.deferred.prototype.initialize.call( partial, id, args ); partial.addInstantPreviews(); - - // @todo If singular_only, and this is not the post singular post for this partial, then no refresh! }, /** @@ -123,6 +121,41 @@ return refreshPromise; }, + /** + * Handle fail to render partial. + * + * {@inheritdoc} + * + * @this {wp.customize.selectiveRefresh.partialConstructor.deferred} + * @returns {void} + */ + fallback: function postFieldPartialFallback() { + var partial = this, dependentSelector; + + /* + * Skip invoking fallback behavior for partials on documents that lack matches for + * the fallback dependent selector. The default fallback dependent selector is + * essentially checking to see if a body_class or post_class exists in the document + * which references the given post. If the dependent selector fails to match any + * elements, then the selector dependency fails and the partial should not be added. + * Note that the dependent selector could have been used as a determiner for whether + * the partial was added in the first place. However, this would have meant that no + * selective refresh requests would have been spawned by the change, and this would + * have meant that any Backbone models for the WP-API would not have had the chance + * to get the rendered updates from the server. + */ + dependentSelector = partial.params.fallbackDependentSelector; + if ( ! dependentSelector ) { + dependentSelector = '.hentry.post-%d, body.page-id-%d, body.postid-%d'; + } + dependentSelector = dependentSelector.replace( /%d/g, String( partial.params.post_id ) ); + if ( 0 === $( dependentSelector ).length ) { + return; + } + + api.selectiveRefresh.partialConstructor.deferred.prototype.fallback.call( partial ); + }, + /** * @inheritdoc */ diff --git a/js/customize-preview-featured-image.js b/js/customize-preview-featured-image.js index 3d6fd5d..a2bfca4 100644 --- a/js/customize-preview-featured-image.js +++ b/js/customize-preview-featured-image.js @@ -7,8 +7,11 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { var component = { data: { - partialSelector: '', - partialContainerInclusive: true + partialArgs: { + selector: '', + containerInclusive: true, + fallbackDependentSelector: '' + } } }; @@ -23,6 +26,27 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { _.extend( component.data, configData ); } component.registerPartials(); + + api.previewPosts.wpApiModelInstances.bind( 'add', component.handleWpApiBackboneModelAdd ); + }; + + /** + * Sync changes to featured image into Backbone models + * + * @param {wp.api.WPApiBaseModel|wp.api.models.Post} postModel Post model. + * @returns {void} + */ + component.handleWpApiBackboneModelAdd = function handleWpApiBackboneModelAdd( postModel ) { + var settingId; + if ( _.isUndefined( postModel.get( 'featured_media' ) ) ) { + return; + } + settingId = 'postmeta[' + postModel.get( 'type' ) + '][' + String( postModel.get( 'id' ) ) + '][_thumbnail_id]'; + api( settingId, function( postmetaSetting ) { + postmetaSetting.bind( function( featuredImageId ) { + postModel.set( 'featured_media', featuredImageId ); + } ); + } ); }; /** @@ -35,6 +59,37 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { */ component.FeaturedImagePartial = api.selectiveRefresh.partialConstructor.deferred.extend({ + idPattern: /^postmeta\[(.+?)]\[(\d+)]\[_thumbnail_id]$/, + + /** + * Initialize. + * + * @param {string} id Partial ID. + * @param {object} args Args. + * @param {object} args.params Params. + * @returns {void} + */ + initialize: function( id, args ) { + var partial = this, matches, postId, postType, params; + matches = id.match( partial.idPattern ); + postType = matches[1]; + postId = parseInt( matches[2], 10 ); + params = _.extend( + { + post_id: postId, + post_type: postType, + selector: component.data.partialArgs.selector.replace( /%d/g, String( postId ) ), + settings: [ id ], + primarySetting: id, + containerInclusive: component.data.partialArgs.containerInclusive, + fallbackDependentSelector: component.data.partialArgs.fallbackDependentSelector + }, + args ? args.params || {} : {} + ); + + api.selectiveRefresh.partialConstructor.deferred.prototype.initialize.call( partial, id, { params: params } ); + }, + /** * Force fallback (full page refresh) behavior when the featured image is removed. * @@ -72,6 +127,27 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { } else { return api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ); } + }, + + /** + * Handle fail to render partial. + * + * Skip performing fallback behavior if post does not appear on the current template. + * + * {@inheritdoc} + * + * @this {wp.customize.selectiveRefresh.partialConstructor.deferred} + * @returns {void} + */ + fallback: function postFieldPartialFallback() { + var partial = this, dependentSelector; + + dependentSelector = partial.params.fallbackDependentSelector.replace( /%d/g, String( partial.params.post_id ) ); + if ( 0 === $( dependentSelector ).length ) { + return; + } + + api.selectiveRefresh.partialConstructor.deferred.prototype.fallback.call( partial ); } }); @@ -82,8 +158,8 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { * @returns {component.FeaturedImagePartial|null} New or existing featured image partial, or null if not relevant setting. */ component.ensurePartialForSetting = function ensurePartialForSetting( setting ) { - var ensuredPartial, partialId, matches = setting.id.match( /^postmeta\[.+?]\[(\d+)]\[_thumbnail_id]$/ ); - if ( ! matches ) { + var ensuredPartial, partialId; + if ( ! component.FeaturedImagePartial.prototype.idPattern.test( setting.id ) ) { return null; } partialId = setting.id; @@ -93,10 +169,7 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { } ensuredPartial = new component.FeaturedImagePartial( partialId, { params: { - selector: component.data.partialSelector, - settings: [ setting.id ], - primarySetting: setting.id, - containerInclusive: component.data.partialContainerInclusive + settings: [ setting.id ] } } ); api.selectiveRefresh.partial.add( partialId, ensuredPartial ); diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index a16d4d1..1e2c371 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -9,6 +9,7 @@ if ( ! api.previewPosts.data ) { api.previewPosts.data = {}; } + api.previewPosts.wpApiModelInstances = _.extend( {}, api.Events ); /** * Prevent shift-clicking from inadvertently causing text selection. @@ -35,7 +36,7 @@ // Add the partials. _.each( api.previewPosts.partialSchema( setting.id ), function( schema ) { - var partial, addPartial, matches, baseSelector; + var partial, matches, postId, postType, selectorBases; matches = schema.id.match( idPattern ); if ( ! matches ) { @@ -45,25 +46,28 @@ if ( api.selectiveRefresh.partial.has( schema.id ) ) { return; } + postType = matches[1]; + postId = parseInt( matches[2], 10 ); if ( schema.params.selector ) { - if ( ! schema.params.bodySelector ) { - baseSelector = '.hentry.post-' + String( parseInt( matches[2], 10 ) ) + '.type-' + matches[1]; + + selectorBases = [ + '.hentry.post-' + String( postId ) + ]; + if ( 'page' === postType ) { + selectorBases.push( 'body.page.page-id-' + String( postId ) ); } else { - baseSelector = '.postid-' + String( parseInt( matches[2], 10 ) ) + '.single-' + matches[1]; + selectorBases.push( 'body.postid-' + String( postId ) ); } - schema.params.selector = baseSelector + ' ' + schema.params.selector; - - addPartial = - ! schema.params.singularOnly && ! schema.params.archiveOnly || - schema.params.singularOnly && api.previewPosts.data.isSingular || - schema.params.archiveOnly && ! api.previewPosts.data.isSingular; + schema.params.selector = _.map( selectorBases, function( selectorBase ) { + var selector = selectorBase + ' ' + schema.params.selector; + selector = selector.replace( /%d/g, String( postId ) ); + return selector; + } ).join( ', ' ); - if ( addPartial ) { - partial = new api.selectiveRefresh.partialConstructor.post_field( schema.id, { params: schema.params } ); - api.selectiveRefresh.partial.add( partial.id, partial ); - addedPartials.push( partial ); - } + partial = new api.selectiveRefresh.partialConstructor.post_field( schema.id, { params: schema.params } ); + api.selectiveRefresh.partial.add( partial.id, partial ); + addedPartials.push( partial ); } else { partial = new api.selectiveRefresh.partialConstructor.post_field( schema.id, { params: schema.params } ); @@ -78,7 +82,7 @@ partial.refresh = function refreshWithoutSelector() { var deferred = $.Deferred(); if ( this.params.fallbackRefresh ) { - api.selectiveRefresh.requestFullRefresh(); + api.selectiveRefresh.requestFullRefresh(); // @todo Do partial.fallback()? deferred.resolve(); } else { deferred.reject(); @@ -144,7 +148,7 @@ // Prevent not-allowed cursor on edit-post-links. api.isLinkPreviewable = ( function( originalIsLinkPreviewable ) { return function( element, options ) { - if ( $( element ).hasClass( 'post-edit-link' ) ) { + if ( $( element ).closest( 'a' ).hasClass( 'post-edit-link' ) ) { return true; } return originalIsLinkPreviewable.call( this, element, options ); @@ -156,7 +160,7 @@ if ( api.Preview.prototype.handleLinkClick ) { api.Preview.prototype.handleLinkClick = ( function( originalHandleLinkClick ) { return function( event ) { - if ( $( event.target ).hasClass( 'post-edit-link' ) ) { + if ( $( event.target ).closest( 'a' ).hasClass( 'post-edit-link' ) ) { event.preventDefault(); } else { originalHandleLinkClick.call( this, event ); @@ -165,9 +169,116 @@ } )( api.Preview.prototype.handleLinkClick ); } + /** + * Hook up post model in Backbone with post setting in customizer. + * + * @returns {void} + */ + api.previewPosts.injectBackboneModelSync = function injectBackboneModelSync() { + var originalInitialize = wp.api.WPApiBaseModel.prototype.initialize, synced = false; + + wp.customize.bind( 'active', function() { + synced = true; + } ); + + // Inject into Post model creation to capture instances to sync with customize settings. + wp.api.WPApiBaseModel.prototype.initialize = function( attributes, options ) { + var model = this, postSettingId; // eslint-disable-line consistent-this + originalInitialize.call( model, attributes, options ); + + // @todo The post type may not correspond directly to the schema type. + if ( ! attributes || -1 === api.previewPosts.data.postTypes.indexOf( attributes.type ) ) { + return; + } + + postSettingId = 'post[' + attributes.type + '][' + String( attributes.id ) + ']'; + + if ( ! api.previewPosts.wpApiModelInstances[ postSettingId ] ) { + api.previewPosts.wpApiModelInstances[ postSettingId ] = []; + } + + // @todo Remove the model from this array when it is removed from a collection. + api.previewPosts.wpApiModelInstances[ postSettingId ].push( model ); + api.previewPosts.wpApiModelInstances.trigger( 'add', model, postSettingId ); + + api( postSettingId, function( postSetting ) { + var updateModel = function( postData ) { + api.previewPosts.handlePostSettingChangeForBackboneModel( model, postData ); + }; + if ( synced ) { + updateModel( postSetting.get() ); + } + postSetting.bind( updateModel ); + } ); + }; + + api.selectiveRefresh.bind( 'render-partials-response', api.previewPosts.handleRenderPartialsResponse ); + }; + + /** + * Handle post setting change to sync into corresponding Backbone model. + * + * @param {wp.api.WPApiBaseModel|wp.api.models.Post} model Post model. + * @param {object} postData Data from the post setting. + * @returns {void} + */ + api.previewPosts.handlePostSettingChangeForBackboneModel = function handlePostSettingChangeForBackboneModel( model, postData ) { + var modelAttributes = {}; + + _.each( [ 'title', 'content', 'excerpt' ], function( field ) { + if ( ! _.isObject( model.get( field ) ) ) { + return; + } + if ( ! model.get( field ).raw || model.get( field ).raw !== postData[ 'post_' + field ] ) { + modelAttributes[ field ] = { + raw: postData[ 'post_' + field ], + rendered: postData[ 'post_' + field ] // Raw value used temporarily until new value fetched from server in selective refresh request. + }; + + // Apply rudimentary wpautop while waiting for selective refresh. + if ( modelAttributes[ field ].rendered && ( 'excerpt' === field || 'content' === field ) ) { + modelAttributes[ field ].rendered = '

' + modelAttributes[ field ].rendered.split( /\n\n+/ ).join( '

' ) + '

'; + modelAttributes[ field ].rendered = modelAttributes[ field ].rendered.replace( /\n/g, '
' ); + } + } + } ); + _.each( [ 'author', 'slug' ], function( field ) { + if ( ! _.isUndefined( model.get( field ) ) ) { + modelAttributes[ field ] = postData[ 'post_' + field ]; + } + } ); + if ( ! _.isUndefined( model.get( 'date' ) ) ) { + modelAttributes.date = postData.post_date.replace( ' ', 'T' ); + } + model.set( modelAttributes ); + }; + + /** + * Supply rendered data from server in the selective refresh response. + * + * @param {object} data Response data. + * @param {object} data.rest_post_resources REST resources for the customized posts. + * @returns {void} + */ + api.previewPosts.handleRenderPartialsResponse = function handleRenderPartialsResponse( data ) { + if ( ! data.rest_post_resources ) { + return; + } + _.each( data.rest_post_resources, function( postResource, settingId ) { + if ( api.previewPosts.wpApiModelInstances[ settingId ] ) { + _.each( api.previewPosts.wpApiModelInstances[ settingId ], function( model ) { + model.set( postResource ); + } ); + } + } ); + }; + api.bind( 'preview-ready', function onPreviewReady() { _.extend( api.previewPosts.data, _wpCustomizePreviewPostsData ); + if ( api.previewPosts.data.hasRestApiBackboneClient ) { + api.previewPosts.injectBackboneModelSync(); + } api.each( api.previewPosts.ensurePartialsForPostSetting ); api.bind( 'add', api.previewPosts.ensurePartialsForPostSetting ); @@ -192,8 +303,12 @@ * Focus on the post section in the Customizer pane when clicking an edit-post-link. */ $( document.body ).on( 'click', '.post-edit-link', function( e ) { - var link = $( this ), postId; - postId = link.data( 'customize-post-id' ); + var link = $( this ), postId, matches; + matches = link.prop( 'search' ).match( /post=(\d+)/ ); + if ( ! matches ) { + return; + } + postId = parseInt( matches[1], 10 ); e.preventDefault(); if ( postId ) { api.preview.send( 'edit-post', postId ); diff --git a/package.json b/package.json index eee7bc0..d529a38 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "git", "url": "https://github.com/xwp/wp-customize-posts.git" }, - "version": "0.8.4", + "version": "0.8.5", "license": "GPL-2.0+", "private": true, "devDependencies": { diff --git a/php/class-wp-customize-featured-image-controller.php b/php/class-wp-customize-featured-image-controller.php index bf47d2b..0ec4ca8 100644 --- a/php/class-wp-customize-featured-image-controller.php +++ b/php/class-wp-customize-featured-image-controller.php @@ -25,6 +25,13 @@ class WP_Customize_Featured_Image_Controller extends WP_Customize_Postmeta_Contr */ const SELECTED_ATTRIBUTE = 'data-customize-featured-image-partial'; + /** + * Selector for finding featured images. + * + * @var string + */ + const SELECTOR = '[data-customize-featured-image-partial="%d"]'; + /** * The container_inclusive param for the partials. * @@ -62,13 +69,6 @@ class WP_Customize_Featured_Image_Controller extends WP_Customize_Postmeta_Contr */ public $setting_transport = 'postMessage'; - /** - * Selector for featured image partials. - * - * @var string - */ - public $partial_selector; - /** * Default value. * @@ -93,8 +93,6 @@ class WP_Customize_Featured_Image_Controller extends WP_Customize_Postmeta_Contr * @param array $args Args. */ public function __construct( array $args = array() ) { - $this->partial_selector = '[' . self::SELECTED_ATTRIBUTE . ']'; - parent::__construct( $args ); $this->override_default_edit_post_screen_functionality(); add_action( 'customize_register', array( $this, 'setup_selective_refresh' ) ); @@ -126,9 +124,14 @@ public function enqueue_customize_pane_scripts() { public function enqueue_customize_preview_scripts() { $handle = 'customize-preview-featured-image'; wp_enqueue_script( $handle ); + + // @todo These arguments should be configurable for featured image partials just as they are for post field partials. $exports = array( - 'partialSelector' => $this->partial_selector, - 'partialContainerInclusive' => self::PARTIAL_CONTAINER_INCLUSIVE, + 'partialArgs' => array( + 'selector' => self::SELECTOR, + 'fallbackDependentSelector' => '.hentry.post-%d, body.page-id-%d, body.postid-%d', + 'containerInclusive' => self::PARTIAL_CONTAINER_INCLUSIVE, + ), ); wp_add_inline_script( $handle, sprintf( 'CustomizePreviewFeaturedImage.init( %s )', wp_json_encode( $exports ) ) ); } @@ -290,7 +293,7 @@ public function filter_customize_dynamic_partial_args( $partial_args, $partial_i $partial_args['settings'] = array( $setting_id ); $partial_args['primary_setting'] = $setting_id; $partial_args['type'] = 'featured_image'; - $partial_args['selector'] = $this->partial_selector; + $partial_args['selector'] = sprintf( self::SELECTOR, $matches['post_id'] ); $partial_args['container_inclusive'] = self::PARTIAL_CONTAINER_INCLUSIVE; } return $partial_args; @@ -319,7 +322,7 @@ public function filter_post_thumbnail_html( $html, $post_id, $post_thumbnail_id, 'attr' => $attr, ); $replacement = '$1'; - $replacement .= sprintf( ' %s="1" ', self::SELECTED_ATTRIBUTE ); + $replacement .= sprintf( ' %s="%d" ', self::SELECTED_ATTRIBUTE, $post_id ); $replacement .= sprintf( ' data-customize-partial-placement-context="%s" ', esc_attr( wp_json_encode( $context ) ) ); $html = preg_replace( '#(<\w+)#', $replacement, $html, 1 ); return $html; diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 5a82157..b6b6c5c 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -94,11 +94,11 @@ public function customize_preview_init() { add_filter( 'the_posts', array( $this, 'filter_the_posts_to_tally_previewed_posts' ), 1000 ); add_filter( 'the_posts', array( $this, 'filter_the_posts_to_tally_orderby_keys' ), 10, 2 ); add_action( 'wp_footer', array( $this, 'export_preview_data' ), 10 ); - add_filter( 'edit_post_link', array( $this, 'filter_edit_post_link' ), 10, 2 ); add_filter( 'get_edit_post_link', array( $this, 'filter_get_edit_post_link' ), 10, 2 ); add_filter( 'get_avatar', array( $this, 'filter_get_avatar' ), 10, 6 ); add_filter( 'infinite_scroll_results', array( $this, 'amend_with_queried_post_ids' ) ); add_filter( 'customize_render_partials_response', array( $this, 'amend_with_queried_post_ids' ) ); + add_filter( 'customize_render_partials_response', array( $this, 'amend_partials_response_with_rest_resources' ), 10, 3 ); remove_filter( 'get_edit_post_link', '__return_empty_string' ); // See . } @@ -582,13 +582,11 @@ public function prepare_query_preview( WP_Query $query ) { } /** - * Filter post_fields to inject customized state. + * Filter posts_request to inject subselect UNIONs to include posts with the customized state. * - * This ensures that ordering will respect the customized post data. - * - * @param string $sql_select The SELECT clause of the query. + * @param string $sql_select A SQL SELECT query for posts. * @param WP_Query $query The WP_Query instance (passed by reference). - * @returns string Select fields. + * @return string SQL SELECT query. */ public function filter_posts_request_to_inject_customized_state( $sql_select, $query ) { global $wpdb; @@ -820,7 +818,7 @@ public function filter_get_meta_sql_to_inject_customized_state( $clauses, $queri * @access private * * @param array $matches Matches. - * @returns string SQL JOIN. + * @return string SQL JOIN. */ public function _inject_meta_sql_customized_derived_tables( $matches ) { global $wpdb; @@ -1006,7 +1004,7 @@ public function capture_sanitized_post_setting_values_for_nav_menu_items() { /** * Filter pristine nav menu item values early. * - * @param WP_Post $nav_menu_item Nav menu item. + * @param WP_Post|object $nav_menu_item Nav menu item. * @return WP_Post Nav menu item. */ function filter_pristine_early_nav_menu_item( $nav_menu_item ) { @@ -1034,7 +1032,7 @@ function remove_filter_pristine_early_nav_menu_item() { * @access public * @see WP_Customize_Nav_Menu_Item_Setting::value_as_wp_post_nav_menu_item() * - * @param WP_Post $nav_menu_item Nav menu item. + * @param WP_Post|object $nav_menu_item Nav menu item. * @return WP_Post Nav menu item. */ public function filter_nav_menu_item_to_set_post_dependent_props( $nav_menu_item ) { @@ -1241,19 +1239,6 @@ function filter_get_edit_post_link( $url, $post_id ) { return $url; } - /** - * Filter the post edit link so it can open the post in the Customizer. - * - * @param string $link Anchor tag for the edit link. - * @param int $post_id Post ID. - * @return string Edit link. - */ - function filter_edit_post_link( $link, $post_id ) { - $data_attributes = sprintf( ' data-customize-post-id="%d"', $post_id ); - $link = preg_replace( '/(?<= array( 'selector' => '.entry-summary', + 'fallback_refresh' => true, ), 'comment_status[comments-area]' => array( 'selector' => '.comments-area', - 'body_selector' => true, - 'singular_only' => true, 'container_inclusive' => true, ), 'comment_status[comments-link]' => array( 'selector' => '.comments-link', - 'archive_only' => true, + 'fallback_dependent_selector' => 'body.archive', // Only do fallback when on archives. 'container_inclusive' => true, ), 'ping_status' => array( 'selector' => '.comments-area', - 'body_selector' => true, - 'singular_only' => true, 'container_inclusive' => true, ), 'post_author[byline]' => array( @@ -1359,6 +1341,15 @@ public function get_post_field_partial_schema( $field_id = '' ) { */ $schema = apply_filters( 'customize_posts_partial_schema', $schema ); + $deprecated_keys = array( 'body_selector', 'archive_only', 'singular_only' ); + foreach ( $schema as $_field_id => $_partial_args ) { + foreach ( $deprecated_keys as $deprecated_key ) { + if ( array_key_exists( $deprecated_key, $_partial_args ) ) { + _deprecated_argument( __FUNCTION__, '0.8.5', sprintf( __( 'The %s param has been removed from the partial schema. Consider fallback_dependent_selector if needed.', 'customize-posts' ), $deprecated_key ) ); + } + } + } + // Return specific schema based on the field_id & placement. if ( ! empty( $field_id ) ) { if ( isset( $schema[ $field_id ] ) ) { @@ -1418,6 +1409,8 @@ public function export_preview_data() { 'postIds' => array_values( array_unique( $this->queried_post_ids ) ), 'partialSchema' => $exported_partial_schema, 'queriedOrderbyFields' => $queried_orderby_fields, + 'hasRestApiBackboneClient' => wp_script_is( 'wp-api', 'enqueued' ), + 'postTypes' => array_keys( $this->component->get_post_types() ), // Used to determine which REST API model types are for posts. ); $data = sprintf( 'var _wpCustomizePreviewPostsData = %s;', wp_json_encode( $exported ) ); @@ -1437,6 +1430,90 @@ public function amend_with_queried_post_ids( $results ) { return $results; } + /** + * Add the REST resources for the customized posts to the partial rendering response. + * + * @param array $response { + * Response. + * + * @type array $contents Associative array mapping a partial ID its corresponding array of contents + * for the containers requested. + * @type array $errors List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY` + * is enabled. + * } + * @param \WP_Customize_Selective_Refresh $selective_refresh Selective refresh component. + * @param array $partials Placements' context data for the partials rendered in the request. + * The array is keyed by partial ID, with each item being an array of + * the placements' context data. + * @return array Response. + */ + public function amend_partials_response_with_rest_resources( $response, $selective_refresh, $partials ) { + + // Abort if the partial render request isn't for a post field partial. + $requesting_post_field_partial = false; + foreach ( array_keys( $partials ) as $partial_id ) { + if ( $selective_refresh->get_partial( $partial_id ) instanceof \WP_Customize_Post_Field_Partial ) { + $requesting_post_field_partial = true; + break; + } + } + if ( ! $requesting_post_field_partial ) { + return $response; + } + + // Gather the customized posts by type. + $posts_by_type = array(); + foreach ( $selective_refresh->manager->settings() as $setting ) { + if ( $setting instanceof \WP_Customize_Post_Setting ) { + if ( ! isset( $posts_by_type[ $setting->post_type ] ) ) { + $posts_by_type[ $setting->post_type ] = array(); + } + $posts_by_type[ $setting->post_type ][] = get_post( $setting->post_id ); + } + } + + // Short-circuit if there are no customized posts. + if ( count( $posts_by_type ) === 0 ) { + return $response; + } + + // Amend partial render response with the rest resources for the given customized posts. + $response['rest_post_resources'] = array(); + $wp_rest_server = rest_get_server(); + foreach ( $posts_by_type as $type => $posts ) { + $post_type_object = get_post_type_object( $type ); + if ( ! $post_type_object || empty( $post_type_object->rest_base ) ) { + continue; + } + + // @todo Do a separate request for each post individually to improve performance? + $request = new \WP_REST_Request( 'GET', '/wp/v2/' . $post_type_object->rest_base ); + $request->set_query_params( array( + 'per_page' => 100, + 'include' => wp_list_pluck( $posts, 'ID' ), + ) ); + if ( current_user_can( $post_type_object->cap->edit_posts ) ) { + $request->set_query_params( array( + 'context' => 'edit', + ) ); + } + + $rest_response = $wp_rest_server->dispatch( $request ); + if ( ! $rest_response->is_error() ) { + + /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */ + $rest_response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $rest_response ), $wp_rest_server, $request ); + + foreach ( $wp_rest_server->response_to_data( $rest_response, true ) as $post_data ) { + $setting_id = WP_Customize_Post_Setting::get_post_setting_id( get_post( $post_data['id'] ) ); + $response['rest_post_resources'][ $setting_id ] = $post_data; + } + } + } + + return $response; + } + /** * Filter post_status to return customize-previewed value if available. * diff --git a/php/theme-support/class-customize-posts-twenty-eleven-support.php b/php/theme-support/class-customize-posts-twenty-eleven-support.php index f492a81..5a21de8 100644 --- a/php/theme-support/class-customize-posts-twenty-eleven-support.php +++ b/php/theme-support/class-customize-posts-twenty-eleven-support.php @@ -43,7 +43,7 @@ public function filter_partial_schema( $schema ) { $schema['post_author[biography]'] = array( 'selector' => '#author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/php/theme-support/class-customize-posts-twenty-fifteen-support.php b/php/theme-support/class-customize-posts-twenty-fifteen-support.php index 35c1e3b..304bc3c 100644 --- a/php/theme-support/class-customize-posts-twenty-fifteen-support.php +++ b/php/theme-support/class-customize-posts-twenty-fifteen-support.php @@ -41,7 +41,7 @@ public function add_support() { public function filter_partial_schema( $schema ) { $schema['post_author[biography]'] = array( 'selector' => '.author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/php/theme-support/class-customize-posts-twenty-seventeen-support.php b/php/theme-support/class-customize-posts-twenty-seventeen-support.php new file mode 100644 index 0000000..37bac3f --- /dev/null +++ b/php/theme-support/class-customize-posts-twenty-seventeen-support.php @@ -0,0 +1,45 @@ + '.author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/php/theme-support/class-customize-posts-twenty-ten-support.php b/php/theme-support/class-customize-posts-twenty-ten-support.php index 2d2d642..849f2ad 100644 --- a/php/theme-support/class-customize-posts-twenty-ten-support.php +++ b/php/theme-support/class-customize-posts-twenty-ten-support.php @@ -45,7 +45,7 @@ public function filter_partial_schema( $schema ) { $schema['post_author[biography]'] = array( 'selector' => '#entry-author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/php/theme-support/class-customize-posts-twenty-thirteen-support.php b/php/theme-support/class-customize-posts-twenty-thirteen-support.php index f3b97d1..457f34a 100644 --- a/php/theme-support/class-customize-posts-twenty-thirteen-support.php +++ b/php/theme-support/class-customize-posts-twenty-thirteen-support.php @@ -41,7 +41,7 @@ public function add_support() { public function filter_partial_schema( $schema ) { $schema['post_author[biography]'] = array( 'selector' => '.author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/php/theme-support/class-customize-posts-twenty-twelve-support.php b/php/theme-support/class-customize-posts-twenty-twelve-support.php index 94781c2..97cb80f 100644 --- a/php/theme-support/class-customize-posts-twenty-twelve-support.php +++ b/php/theme-support/class-customize-posts-twenty-twelve-support.php @@ -41,7 +41,7 @@ public function add_support() { public function filter_partial_schema( $schema ) { $schema['post_author[biography]'] = array( 'selector' => '.author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/readme.md b/readme.md index 1196e9c..65f2468 100644 --- a/readme.md +++ b/readme.md @@ -6,9 +6,9 @@ Edit posts and postmeta in the Customizer. Stop editing your posts/postmeta blin **Contributors:** [xwp](https://profiles.wordpress.org/xwp), [westonruter](https://profiles.wordpress.org/westonruter), [valendesigns](https://profiles.wordpress.org/valendesigns) **Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [customize](https://wordpress.org/plugins/tags/customize), [posts](https://wordpress.org/plugins/tags/posts), [postmeta](https://wordpress.org/plugins/tags/postmeta), [editor](https://wordpress.org/plugins/tags/editor), [preview](https://wordpress.org/plugins/tags/preview), [featured-image](https://wordpress.org/plugins/tags/featured-image), [page-template](https://wordpress.org/plugins/tags/page-template) -**Requires at least:** 4.5 -**Tested up to:** 4.7 -**Stable tag:** 0.8.3 +**Requires at least:** 4.5.0 +**Tested up to:** 4.7.0 +**Stable tag:** 0.8.5 **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) [![Build Status](https://travis-ci.org/xwp/wp-customize-posts.svg?branch=master)](https://travis-ci.org/xwp/wp-customize-posts) [![Coverage Status](https://coveralls.io/repos/xwp/wp-customize-posts/badge.svg?branch=master)](https://coveralls.io/github/xwp/wp-customize-posts) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com) [![devDependency Status](https://david-dm.org/xwp/wp-customize-posts/dev-status.svg)](https://david-dm.org/xwp/wp-customize-posts#info=devDependencies) @@ -90,9 +90,17 @@ The following are listed in reverse chronological order. The first, more recent ## Changelog ## +### [0.8.5] - 2017-01-03 ### +* Add syncing of customize post settings to Backbone post models; try this out with the Next Recent Posts Widget. Fixes #331, PR #333. +* Improve logic for determining when to do fallback refresh for post field partials, reducing the number of needless full page refreshes. PR #335. +* Differentiate featured image partials on index templates for separate posts, allowing changes to featured images to be previewed properly on the homepage and archive views. Fixes #297, PR #334. +* Handle edit post links that have child elements; remove need for data-customize-post-id attribute. PR #332. + +See issues and PRs in milestone and full release commit log. + ### [0.8.4] - 2016-12-03 ### -* Ensure auto-draft posts referenced in snapshot/changeset get transitioned to customize-draft, and that customize-draft nav_menus_created_posts get published (PR #326). -* Improve method for skipping attachments so no error in console appears (PR #325, Issue #32). +* Ensure auto-draft posts referenced in snapshot/changeset get transitioned to customize-draft, and that customize-draft nav_menus_created_posts get published (PR #326). +* Improve method for skipping attachments so no error in console appears (PR #325, Issue #32). See issues and PRs in milestone and full release commit log. diff --git a/readme.txt b/readme.txt index a35e7b9..a64361e 100644 --- a/readme.txt +++ b/readme.txt @@ -1,9 +1,9 @@ === Customize Posts === Contributors: xwp, westonruter, valendesigns Tags: customizer, customize, posts, postmeta, editor, preview, featured-image, page-template -Requires at least: 4.5 -Tested up to: 4.7 -Stable tag: 0.8.3 +Requires at least: 4.5.0 +Tested up to: 4.7.0 +Stable tag: 0.8.5 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -67,10 +67,19 @@ The following are listed in reverse chronological order. The first, more recent == Changelog == += [0.8.5] - 2017-01-03 = + +* Add syncing of customize post settings to Backbone post models; try this out with the Next Recent Posts Widget. Fixes #331, PR #333. +* Improve logic for determining when to do fallback refresh for post field partials, reducing the number of needless full page refreshes. PR #335. +* Differentiate featured image partials on index templates for separate posts, allowing changes to featured images to be previewed properly on the homepage and archive views. Fixes #297, PR #334. +* Handle edit post links that have child elements; remove need for data-customize-post-id attribute. PR #332. + +See issues and PRs in milestone and full release commit log. + = [0.8.4] - 2016-12-03 = -* Ensure auto-draft posts referenced in snapshot/changeset get transitioned to customize-draft, and that customize-draft nav_menus_created_posts get published (PR #326). -* Improve method for skipping attachments so no error in console appears (PR #325, Issue #32). +* Ensure auto-draft posts referenced in snapshot/changeset get transitioned to customize-draft, and that customize-draft nav_menus_created_posts get published (PR #326). +* Improve method for skipping attachments so no error in console appears (PR #325, Issue #32). See issues and PRs in milestone and full release commit log. diff --git a/tests/data/themes/dummy/functions.php b/tests/data/themes/dummy/functions.php index 2578469..ec13260 100644 --- a/tests/data/themes/dummy/functions.php +++ b/tests/data/themes/dummy/functions.php @@ -58,7 +58,7 @@ public function add_support() { public function filter_partial_schema( $schema ) { $schema['post_author[biography]'] = array( 'selector' => '.author-info', - 'singular_only' => true, + 'fallback_dependent_selector' => 'body.singular', 'container_inclusive' => true, 'render_callback' => array( $this, 'biography_render_callback' ), ); diff --git a/tests/php/test-class-wp-customize-featured-image-controller.php b/tests/php/test-class-wp-customize-featured-image-controller.php index 342fbc2..413061f 100644 --- a/tests/php/test-class-wp-customize-featured-image-controller.php +++ b/tests/php/test-class-wp-customize-featured-image-controller.php @@ -195,12 +195,12 @@ public function test_filter_post_thumbnail_html() { $controller = new WP_Customize_Featured_Image_Controller(); $html = get_the_post_thumbnail( $post_id ); - $this->assertNotContains( 'data-customize-featured-image-partial="1"', $html ); + $this->assertNotContains( 'data-customize-featured-image-partial="' . $post_id . '"', $html ); $this->assertNotContains( 'data-customize-partial-placement-context', $html ); $controller->setup_selective_refresh(); $html = get_the_post_thumbnail( $post_id ); - $this->assertContains( 'data-customize-featured-image-partial="1"', $html ); + $this->assertContains( 'data-customize-featured-image-partial="' . $post_id . '"', $html ); $this->assertContains( 'data-customize-partial-placement-context', $html ); $html = get_the_post_thumbnail( $post_id, 'large', array( 'data-foo' => 'bar' ) ); diff --git a/tests/php/test-class-wp-customize-posts-preview.php b/tests/php/test-class-wp-customize-posts-preview.php index ef5ca5f..928ef0b 100644 --- a/tests/php/test-class-wp-customize-posts-preview.php +++ b/tests/php/test-class-wp-customize-posts-preview.php @@ -127,7 +127,6 @@ public function test_customize_preview_init() { $this->assertEquals( 1000, has_filter( 'the_posts', array( $preview, 'filter_the_posts_to_tally_previewed_posts' ) ) ); $this->assertEquals( 10, has_filter( 'the_posts', array( $preview, 'filter_the_posts_to_tally_orderby_keys' ) ) ); $this->assertEquals( 10, has_action( 'wp_footer', array( $preview, 'export_preview_data' ) ) ); - $this->assertEquals( 10, has_filter( 'edit_post_link', array( $preview, 'filter_edit_post_link' ) ) ); $this->assertEquals( 10, has_filter( 'get_edit_post_link', array( $preview, 'filter_get_edit_post_link' ) ) ); $this->assertEquals( 10, has_filter( 'get_avatar', array( $preview, 'filter_get_avatar' ) ) ); $this->assertEquals( 10, has_filter( 'infinite_scroll_results', array( $preview, 'amend_with_queried_post_ids' ) ) ); @@ -1155,18 +1154,6 @@ public function test_filter_get_edit_post_link() { $this->assertEquals( $edit_post_link, $preview->filter_get_edit_post_link( $edit_post_link, $this->post_id ) ); } - /** - * Test filter_edit_post_link(). - * - * @see WP_Customize_Posts_Preview::filter_edit_post_link() - */ - public function test_filter_edit_post_link() { - $preview = new WP_Customize_Posts_Preview( $this->posts_component ); - $link = 'Edit'; - $contained = sprintf( ' data-customize-post-id="%d"', $this->post_id ); - $this->assertContains( $contained, $preview->filter_edit_post_link( $link, $this->post_id ) ); - } - /** * Test filter_get_avatar(). *