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, '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().
*