From 64308941d4d518f9409726876f47299e91967ad7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 3 Dec 2016 11:22:21 -0500 Subject: [PATCH 01/16] Bump stable tag to 0.8.4 --- readme.md | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 1196e9c..c9d915b 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ Edit posts and postmeta in the Customizer. Stop editing your posts/postmeta blin **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 +**Stable tag:** 0.8.4 **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) diff --git a/readme.txt b/readme.txt index a35e7b9..47dcdf1 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ 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 +Stable tag: 0.8.4 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html From ccf7d8940f5eedb078acf23c78780404c3255c99 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 12 Dec 2016 20:36:06 -0800 Subject: [PATCH 02/16] Clean up phpdoc comments --- php/class-wp-customize-posts-preview.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 5a82157..4aff45f 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -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 ) { From 8546e00c74c1a94c1a452ac6a21d451ea21bbfcc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 13 Dec 2016 17:23:23 -0800 Subject: [PATCH 03/16] Ensure clicking on child elements of edit post links works as if clicking link itself --- js/customize-preview-posts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index a16d4d1..8bf0a4f 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -144,7 +144,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 +156,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 ); From 834cb58039c9609a7a36e764096f261daaf342e0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 13 Dec 2016 17:57:36 -0800 Subject: [PATCH 04/16] Remove needless data-customize-post-id attribute on edit post links --- js/customize-preview-posts.js | 8 ++++++-- php/class-wp-customize-posts-preview.php | 14 -------------- .../php/test-class-wp-customize-posts-preview.php | 13 ------------- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index 8bf0a4f..f559fd1 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -192,8 +192,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/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 4aff45f..2cd7e69 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -94,7 +94,6 @@ 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' ) ); @@ -1239,19 +1238,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( '/(?<=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(). * From 81f8ea38669ff59d6fe8228099df1459bfec9f33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 14 Dec 2016 09:32:02 -0800 Subject: [PATCH 05/16] Add syncing of post setting to Backbone post models --- js/customize-preview-posts.js | 71 ++++++++++++++++++ php/class-wp-customize-posts-preview.php | 93 ++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index f559fd1..4466093 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -165,9 +165,80 @@ } )( 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, postModels = {}, 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, settingId; // eslint-disable-line consistent-this + originalInitialize.call( model, attributes, options ); + + // @todo Make sure that attributes.type is a registered post type. + // @todo We need a mapping of post type to schema type. + if ( ! attributes.type ) { + return; + } + + settingId = 'post[' + attributes.type + '][' + String( attributes.id ) + ']'; + postModels[ settingId ] = model; + + wp.customize( settingId, function( postSetting ) { + var updateModel = function( postData ) { + var modelAttributes = {}; + _.each( [ 'title', 'content', 'excerpt' ], function( field ) { + 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( '

' ) + '

'; + } + } + } ); + _.each( [ 'author', 'slug' ], function( field ) { + modelAttributes[ field ] = postData[ 'post_' + field ]; + } ); + modelAttributes.date = postData.post_date.replace( ' ', 'T' ); + model.set( modelAttributes ); + }; + if ( synced ) { + updateModel( postSetting.get() ); + } + postSetting.bind( updateModel ); + } ); + }; + + // Supply rendered data from server in the selective refresh response. + wp.customize.selectiveRefresh.bind( 'render-partials-response', function( data ) { + if ( ! data.rest_post_resources ) { + return; + } + _.each( data.rest_post_resources, function( postResource, settingId ) { + if ( postModels[ settingId ] ) { + postModels[ settingId ].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 ); diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 2cd7e69..a94c48e 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -98,6 +98,14 @@ public function customize_preview_init() { 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 ); + + // @todo There should be some more sophisticated logic for determining whether fallback_refresh is done. + add_filter( 'customize_posts_partial_schema', function( $schema ) { + $schema['post_title']['fallback_refresh'] = false; + $schema['post_excerpt']['fallback_refresh'] = false; + return $schema; + } ); remove_filter( 'get_edit_post_link', '__return_empty_string' ); // See . } @@ -1402,6 +1410,7 @@ 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' ), ); $data = sprintf( 'var _wpCustomizePreviewPostsData = %s;', wp_json_encode( $exported ) ); @@ -1421,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. * From d90c521201b39e4d5f2a5e79071228772dc7e09e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 14 Dec 2016 17:11:20 -0800 Subject: [PATCH 06/16] Fix PHP 5.2 compat --- js/customize-preview-posts.js | 2 ++ php/class-wp-customize-posts-preview.php | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index 4466093..df55b1b 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -194,6 +194,8 @@ wp.customize( settingId, function( postSetting ) { var updateModel = function( postData ) { var modelAttributes = {}; + + // @todo Make sure we only set attributes that exist in the model. _.each( [ 'title', 'content', 'excerpt' ], function( field ) { if ( ! model.get( field ).raw || model.get( field ).raw !== postData[ 'post_' + field ] ) { modelAttributes[ field ] = { diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index a94c48e..9056634 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -91,6 +91,7 @@ public function customize_preview_init() { add_action( 'parse_query', array( $this, 'ensure_page_for_posts_preview' ), 5 ); add_filter( 'customize_dynamic_partial_args', array( $this, 'filter_customize_dynamic_partial_args' ), 10, 2 ); add_filter( 'customize_dynamic_partial_class', array( $this, 'filter_customize_dynamic_partial_class' ), 10, 3 ); + add_filter( 'customize_posts_partial_schema', array( $this, 'filter_customize_posts_partial_schema' ) ); 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 ); @@ -99,13 +100,6 @@ public function customize_preview_init() { 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 ); - - // @todo There should be some more sophisticated logic for determining whether fallback_refresh is done. - add_filter( 'customize_posts_partial_schema', function( $schema ) { - $schema['post_title']['fallback_refresh'] = false; - $schema['post_excerpt']['fallback_refresh'] = false; - return $schema; - } ); remove_filter( 'get_edit_post_link', '__return_empty_string' ); // See . } @@ -1228,6 +1222,20 @@ function filter_customize_dynamic_partial_class( $partial_class, $partial_id, $p return $partial_class; } + /** + * Prevent fallback_refresh for select post fields. + * + * @todo There should be some more sophisticated logic for determining whether fallback_refresh is done. + * + * @param array $schema Schema. + * @return array Schema. + */ + function filter_customize_posts_partial_schema( $schema ) { + $schema['post_title']['fallback_refresh'] = false; + $schema['post_excerpt']['fallback_refresh'] = false; + return $schema; + } + /** * Filters get_edit_post_link to short-circuits if post cannot be edited in Customizer. * From aea31f172c63682bb802a1687437f9275e85d322 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 14 Dec 2016 17:22:54 -0800 Subject: [PATCH 07/16] Ensure multiple instances of a given post will get synced --- js/customize-preview-posts.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index df55b1b..eea291a 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.backbonePostModelInstances = {}; /** * Prevent shift-clicking from inadvertently causing text selection. @@ -171,7 +172,7 @@ * @returns {void} */ api.previewPosts.injectBackboneModelSync = function injectBackboneModelSync() { - var originalInitialize = wp.api.WPApiBaseModel.prototype.initialize, postModels = {}, synced = false; + var originalInitialize = wp.api.WPApiBaseModel.prototype.initialize, synced = false; wp.customize.bind( 'active', function() { synced = true; @@ -189,7 +190,13 @@ } settingId = 'post[' + attributes.type + '][' + String( attributes.id ) + ']'; - postModels[ settingId ] = model; + + if ( ! api.previewPosts.backbonePostModelInstances[ settingId ] ) { + api.previewPosts.backbonePostModelInstances[ settingId ] = []; + } + + // @todo Remove the model from this array when it is removed from a collection. + api.previewPosts.backbonePostModelInstances[ settingId ].push( model ); wp.customize( settingId, function( postSetting ) { var updateModel = function( postData ) { @@ -228,8 +235,10 @@ return; } _.each( data.rest_post_resources, function( postResource, settingId ) { - if ( postModels[ settingId ] ) { - postModels[ settingId ].set( postResource ); + if ( api.previewPosts.backbonePostModelInstances[ settingId ] ) { + _.each( api.previewPosts.backbonePostModelInstances[ settingId ], function( model ) { + model.set( postResource ); + } ); } } ); } ); From 47ba28cfe1a64635f872fc352f9038393e9f29ec Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 15 Dec 2016 20:42:03 -0800 Subject: [PATCH 08/16] Differentiate featured image partials on index templates for separate posts --- js/customize-preview-featured-image.js | 7 ++++--- ...ass-wp-customize-featured-image-controller.php | 15 +++------------ ...ass-wp-customize-featured-image-controller.php | 4 ++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/js/customize-preview-featured-image.js b/js/customize-preview-featured-image.js index 3d6fd5d..e5d41a1 100644 --- a/js/customize-preview-featured-image.js +++ b/js/customize-preview-featured-image.js @@ -7,7 +7,7 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { var component = { data: { - partialSelector: '', + partialSelectorAttribute: '', partialContainerInclusive: true } }; @@ -82,18 +82,19 @@ 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]$/ ); + var ensuredPartial, partialId, postId, matches = setting.id.match( /^postmeta\[.+?]\[(\d+)]\[_thumbnail_id]$/ ); if ( ! matches ) { return null; } partialId = setting.id; + postId = parseInt( matches[1], 10 ); ensuredPartial = api.selectiveRefresh.partial( partialId ); if ( ensuredPartial ) { return ensuredPartial; } ensuredPartial = new component.FeaturedImagePartial( partialId, { params: { - selector: component.data.partialSelector, + selector: '[' + component.data.partialSelectorAttribute + '=' + String( postId ) + ']', settings: [ setting.id ], primarySetting: setting.id, containerInclusive: component.data.partialContainerInclusive diff --git a/php/class-wp-customize-featured-image-controller.php b/php/class-wp-customize-featured-image-controller.php index bf47d2b..9d86251 100644 --- a/php/class-wp-customize-featured-image-controller.php +++ b/php/class-wp-customize-featured-image-controller.php @@ -62,13 +62,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 +86,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' ) ); @@ -127,7 +118,7 @@ public function enqueue_customize_preview_scripts() { $handle = 'customize-preview-featured-image'; wp_enqueue_script( $handle ); $exports = array( - 'partialSelector' => $this->partial_selector, + 'partialSelectorAttribute' => self::SELECTED_ATTRIBUTE, 'partialContainerInclusive' => self::PARTIAL_CONTAINER_INCLUSIVE, ); wp_add_inline_script( $handle, sprintf( 'CustomizePreviewFeaturedImage.init( %s )', wp_json_encode( $exports ) ) ); @@ -290,7 +281,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'] = '[' . self::SELECTED_ATTRIBUTE . '=' . $matches['post_id'] . ']'; $partial_args['container_inclusive'] = self::PARTIAL_CONTAINER_INCLUSIVE; } return $partial_args; @@ -319,7 +310,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/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' ) ); From 54fb89649ee2654dfdc461eb13e0a1c08d97d263 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 15 Dec 2016 21:40:01 -0800 Subject: [PATCH 09/16] Sync featured media changes into Post backbone model --- js/customize-preview-posts.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index eea291a..6178d27 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -180,7 +180,7 @@ // Inject into Post model creation to capture instances to sync with customize settings. wp.api.WPApiBaseModel.prototype.initialize = function( attributes, options ) { - var model = this, settingId; // eslint-disable-line consistent-this + var model = this, postSettingId, featuredImagePostMetaSettingId; // eslint-disable-line consistent-this originalInitialize.call( model, attributes, options ); // @todo Make sure that attributes.type is a registered post type. @@ -189,16 +189,17 @@ return; } - settingId = 'post[' + attributes.type + '][' + String( attributes.id ) + ']'; + postSettingId = 'post[' + attributes.type + '][' + String( attributes.id ) + ']'; + featuredImagePostMetaSettingId = 'postmeta[' + attributes.type + '][' + String( attributes.id ) + '][_thumbnail_id]'; - if ( ! api.previewPosts.backbonePostModelInstances[ settingId ] ) { - api.previewPosts.backbonePostModelInstances[ settingId ] = []; + if ( ! api.previewPosts.backbonePostModelInstances[ postSettingId ] ) { + api.previewPosts.backbonePostModelInstances[ postSettingId ] = []; } // @todo Remove the model from this array when it is removed from a collection. - api.previewPosts.backbonePostModelInstances[ settingId ].push( model ); + api.previewPosts.backbonePostModelInstances[ postSettingId ].push( model ); - wp.customize( settingId, function( postSetting ) { + wp.customize( postSettingId, function( postSetting ) { var updateModel = function( postData ) { var modelAttributes = {}; @@ -227,6 +228,16 @@ } postSetting.bind( updateModel ); } ); + + // Also handle syncing featured image. + // @todo This should be handled in the featured image controller. + if ( 'undefined' !== typeof model.get( 'featured_media' ) ) { + wp.customize( featuredImagePostMetaSettingId, function( postmetaSetting ) { + postmetaSetting.bind( function( featuredImageId ) { + model.set( 'featured_media', featuredImageId ); + } ); + } ); + } }; // Supply rendered data from server in the selective refresh response. From 60787ef043ca8bb78741be63ab6864d65cc9b323 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 15 Dec 2016 22:34:32 -0800 Subject: [PATCH 10/16] Move featured image syncing into featured-image controller --- js/customize-preview-featured-image.js | 21 +++++++++++ js/customize-preview-posts.js | 52 +++++++++++++------------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/js/customize-preview-featured-image.js b/js/customize-preview-featured-image.js index e5d41a1..2e334ee 100644 --- a/js/customize-preview-featured-image.js +++ b/js/customize-preview-featured-image.js @@ -23,6 +23,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 ( 'undefined' === typeof 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 ); + } ); + } ); }; /** diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index 6178d27..4ec9993 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -9,7 +9,7 @@ if ( ! api.previewPosts.data ) { api.previewPosts.data = {}; } - api.previewPosts.backbonePostModelInstances = {}; + api.previewPosts.wpApiModelInstances = _.extend( {}, api.Events ); /** * Prevent shift-clicking from inadvertently causing text selection. @@ -180,7 +180,7 @@ // 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, featuredImagePostMetaSettingId; // eslint-disable-line consistent-this + var model = this, postSettingId; // eslint-disable-line consistent-this originalInitialize.call( model, attributes, options ); // @todo Make sure that attributes.type is a registered post type. @@ -190,14 +190,14 @@ } postSettingId = 'post[' + attributes.type + '][' + String( attributes.id ) + ']'; - featuredImagePostMetaSettingId = 'postmeta[' + attributes.type + '][' + String( attributes.id ) + '][_thumbnail_id]'; - if ( ! api.previewPosts.backbonePostModelInstances[ postSettingId ] ) { - api.previewPosts.backbonePostModelInstances[ postSettingId ] = []; + 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.backbonePostModelInstances[ postSettingId ].push( model ); + api.previewPosts.wpApiModelInstances[ postSettingId ].push( model ); + api.previewPosts.wpApiModelInstances.trigger( 'add', model, postSettingId ); wp.customize( postSettingId, function( postSetting ) { var updateModel = function( postData ) { @@ -228,30 +228,28 @@ } postSetting.bind( updateModel ); } ); - - // Also handle syncing featured image. - // @todo This should be handled in the featured image controller. - if ( 'undefined' !== typeof model.get( 'featured_media' ) ) { - wp.customize( featuredImagePostMetaSettingId, function( postmetaSetting ) { - postmetaSetting.bind( function( featuredImageId ) { - model.set( 'featured_media', featuredImageId ); - } ); - } ); - } }; - // Supply rendered data from server in the selective refresh response. - wp.customize.selectiveRefresh.bind( 'render-partials-response', function( data ) { - if ( ! data.rest_post_resources ) { - return; + wp.customize.selectiveRefresh.bind( 'render-partials-response', api.previewPosts.handleRenderPartialsResponse ); + }; + + /** + * 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 ); + } ); } - _.each( data.rest_post_resources, function( postResource, settingId ) { - if ( api.previewPosts.backbonePostModelInstances[ settingId ] ) { - _.each( api.previewPosts.backbonePostModelInstances[ settingId ], function( model ) { - model.set( postResource ); - } ); - } - } ); } ); }; From b8e5c8f65802f10f7d2c6df86cd7b38d015d87d1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 15 Dec 2016 22:39:39 -0800 Subject: [PATCH 11/16] Only sync post models for recognized types --- js/customize-preview-featured-image.js | 2 +- js/customize-preview-posts.js | 66 +++++++++++++++--------- php/class-wp-customize-posts-preview.php | 1 + 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/js/customize-preview-featured-image.js b/js/customize-preview-featured-image.js index 2e334ee..44741c3 100644 --- a/js/customize-preview-featured-image.js +++ b/js/customize-preview-featured-image.js @@ -35,7 +35,7 @@ var CustomizePreviewFeaturedImage = (function( api, $ ) { */ component.handleWpApiBackboneModelAdd = function handleWpApiBackboneModelAdd( postModel ) { var settingId; - if ( 'undefined' === typeof postModel.get( 'featured_media' ) ) { + if ( _.isUndefined( postModel.get( 'featured_media' ) ) ) { return; } settingId = 'postmeta[' + postModel.get( 'type' ) + '][' + String( postModel.get( 'id' ) ) + '][_thumbnail_id]'; diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index 4ec9993..49f2b63 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -183,9 +183,8 @@ var model = this, postSettingId; // eslint-disable-line consistent-this originalInitialize.call( model, attributes, options ); - // @todo Make sure that attributes.type is a registered post type. - // @todo We need a mapping of post type to schema type. - if ( ! attributes.type ) { + // @todo The post type may not correspond directly to the schema type. + if ( -1 === api.previewPosts.data.postTypes.indexOf( attributes.type ) ) { return; } @@ -199,29 +198,9 @@ api.previewPosts.wpApiModelInstances[ postSettingId ].push( model ); api.previewPosts.wpApiModelInstances.trigger( 'add', model, postSettingId ); - wp.customize( postSettingId, function( postSetting ) { + api( postSettingId, function( postSetting ) { var updateModel = function( postData ) { - var modelAttributes = {}; - - // @todo Make sure we only set attributes that exist in the model. - _.each( [ 'title', 'content', 'excerpt' ], function( field ) { - 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( '

' ) + '

'; - } - } - } ); - _.each( [ 'author', 'slug' ], function( field ) { - modelAttributes[ field ] = postData[ 'post_' + field ]; - } ); - modelAttributes.date = postData.post_date.replace( ' ', 'T' ); - model.set( modelAttributes ); + api.previewPosts.handlePostSettingChangeForBackboneModel( model, postData ); }; if ( synced ) { updateModel( postSetting.get() ); @@ -233,6 +212,43 @@ wp.customize.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( '

' ) + '

'; + } + } + } ); + _.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. * diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 9056634..5de56c4 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -1419,6 +1419,7 @@ public function export_preview_data() { '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 ) ); From 6c92473009ec8743600b3986ca61f574dfffe5ef Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 15 Dec 2016 22:45:21 -0800 Subject: [PATCH 12/16] Handle line breaks in wpautop implementation --- js/customize-preview-posts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index 49f2b63..d05150d 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -235,6 +235,7 @@ // 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, '
' ); } } } ); From beabf5f00b301c718b34a8eb806b7a69b928bc60 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Dec 2016 23:55:40 -0800 Subject: [PATCH 13/16] Refactor body_selector, singular_only, and archive_only into fallback_dependent_selector * Allows partial selectors to have make use of post ID placeholders in the form of %d. * Selective refresh fallback behavior is changed to be prevented when changing a post that isn't referenced on a given template (via body_class or post_class). * Nevertheless, the selective refresh requests still are made so that the REST API Backbone models have the opportunity to update their rendered properties. * Adds initial support for Twenty Seventeen. --- js/customize-post-field-partial.js | 37 ++++++++++++++- js/customize-preview-posts.js | 37 ++++++++------- php/class-wp-customize-posts-preview.php | 31 +++++-------- ...-customize-posts-twenty-eleven-support.php | 2 +- ...customize-posts-twenty-fifteen-support.php | 2 +- ...stomize-posts-twenty-seventeen-support.php | 45 +++++++++++++++++++ ...customize-posts-twenty-sixteen-support.php | 2 +- ...ass-customize-posts-twenty-ten-support.php | 2 +- ...ustomize-posts-twenty-thirteen-support.php | 2 +- ...-customize-posts-twenty-twelve-support.php | 2 +- tests/data/themes/dummy/functions.php | 2 +- 11 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 php/theme-support/class-customize-posts-twenty-seventeen-support.php 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-posts.js b/js/customize-preview-posts.js index d05150d..d0140d0 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -36,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 ) { @@ -46,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 } ); @@ -79,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(); @@ -209,7 +212,7 @@ } ); }; - wp.customize.selectiveRefresh.bind( 'render-partials-response', api.previewPosts.handleRenderPartialsResponse ); + api.selectiveRefresh.bind( 'render-partials-response', api.previewPosts.handleRenderPartialsResponse ); }; /** diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 5de56c4..b6b6c5c 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -91,7 +91,6 @@ public function customize_preview_init() { add_action( 'parse_query', array( $this, 'ensure_page_for_posts_preview' ), 5 ); add_filter( 'customize_dynamic_partial_args', array( $this, 'filter_customize_dynamic_partial_args' ), 10, 2 ); add_filter( 'customize_dynamic_partial_class', array( $this, 'filter_customize_dynamic_partial_class' ), 10, 3 ); - add_filter( 'customize_posts_partial_schema', array( $this, 'filter_customize_posts_partial_schema' ) ); 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 ); @@ -1222,20 +1221,6 @@ function filter_customize_dynamic_partial_class( $partial_class, $partial_id, $p return $partial_class; } - /** - * Prevent fallback_refresh for select post fields. - * - * @todo There should be some more sophisticated logic for determining whether fallback_refresh is done. - * - * @param array $schema Schema. - * @return array Schema. - */ - function filter_customize_posts_partial_schema( $schema ) { - $schema['post_title']['fallback_refresh'] = false; - $schema['post_excerpt']['fallback_refresh'] = false; - return $schema; - } - /** * Filters get_edit_post_link to short-circuits if post cannot be edited in Customizer. * @@ -1321,22 +1306,19 @@ public function get_post_field_partial_schema( $field_id = '' ) { ), 'post_excerpt' => 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 ] ) ) { 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/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' ), ); From 576b8ce64d1cc3fc05533b04f830210a2a1177d7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Dec 2016 10:22:29 -0800 Subject: [PATCH 14/16] Extend the fallback dependent selector idea to featured image partials --- js/customize-preview-featured-image.js | 69 ++++++++++++++++--- ...wp-customize-featured-image-controller.php | 18 ++++- 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/js/customize-preview-featured-image.js b/js/customize-preview-featured-image.js index 44741c3..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: { - partialSelectorAttribute: '', - partialContainerInclusive: true + partialArgs: { + selector: '', + containerInclusive: true, + fallbackDependentSelector: '' + } } }; @@ -56,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. * @@ -93,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 ); } }); @@ -103,22 +158,18 @@ 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, postId, 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; - postId = parseInt( matches[1], 10 ); ensuredPartial = api.selectiveRefresh.partial( partialId ); if ( ensuredPartial ) { return ensuredPartial; } ensuredPartial = new component.FeaturedImagePartial( partialId, { params: { - selector: '[' + component.data.partialSelectorAttribute + '=' + String( postId ) + ']', - settings: [ setting.id ], - primarySetting: setting.id, - containerInclusive: component.data.partialContainerInclusive + settings: [ setting.id ] } } ); api.selectiveRefresh.partial.add( partialId, ensuredPartial ); diff --git a/php/class-wp-customize-featured-image-controller.php b/php/class-wp-customize-featured-image-controller.php index 9d86251..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. * @@ -117,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( - 'partialSelectorAttribute' => self::SELECTED_ATTRIBUTE, - '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 ) ) ); } @@ -281,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'] = '[' . self::SELECTED_ATTRIBUTE . '=' . $matches['post_id'] . ']'; + $partial_args['selector'] = sprintf( self::SELECTOR, $matches['post_id'] ); $partial_args['container_inclusive'] = self::PARTIAL_CONTAINER_INCLUSIVE; } return $partial_args; From 05b1c76837f1ece61e07597980e4d72859d10238 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 20 Dec 2016 23:53:21 -0800 Subject: [PATCH 15/16] Skip attempting model sync when no attributes passed --- js/customize-preview-posts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/customize-preview-posts.js b/js/customize-preview-posts.js index d0140d0..1e2c371 100644 --- a/js/customize-preview-posts.js +++ b/js/customize-preview-posts.js @@ -187,7 +187,7 @@ originalInitialize.call( model, attributes, options ); // @todo The post type may not correspond directly to the schema type. - if ( -1 === api.previewPosts.data.postTypes.indexOf( attributes.type ) ) { + if ( ! attributes || -1 === api.previewPosts.data.postTypes.indexOf( attributes.type ) ) { return; } From 645d8edc9a03bbf9b50dcb3ac97f355e3a22b063 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Jan 2017 11:03:06 -0800 Subject: [PATCH 16/16] Bump 0.8.5 --- composer.json | 2 +- customize-posts.php | 2 +- package.json | 2 +- readme.md | 18 +++++++++++++----- readme.txt | 19 ++++++++++++++----- 5 files changed, 30 insertions(+), 13 deletions(-) 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/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/readme.md b/readme.md index c9d915b..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.4 +**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 47dcdf1..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.4 +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.