Open strarsis opened 4 years ago
I had need of this feature and did a quick and dirty plugin that filters the PostFeaturedImage
component. Posting here in case anyone else needs something similar.
First, I registered the featured_image_focal_point
post meta:
/**
* Register post meta for featured image focal point
*/
function featured_image_focal_point_post_meta() {
register_post_meta( '', 'featured_image_focal_point', array(
'type' => 'object',
'description' => 'Focal point of the featured image',
'single' => true,
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
'properties' => array(
'x' => array(
'type' => 'number',
),
'y' => array(
'type' => 'number',
),
),
),
),
) );
}
add_action( 'init', 'featured_image_focal_point_post_meta' );
Then I used wp.hooks.addFilter
to filter the PostFeaturedImage
component to append the FocalPointPicker
below. Ideally, I'd love for the focal point picker to replace the standard featured image, but, like I said, quick and dirty.
/**
* EDITOR: Featured Image with Focal Point
*/
const { FocalPointPicker } = wp.components;
const { compose } = wp.compose;
const { withDispatch, withSelect } = wp.data;
const { Fragment } = wp.element;
const { addFilter } = wp.hooks;
const { __ } = wp.i18n;
function wrapPostFeaturedImage( PostFeaturedImage ) {
return compose(
applyWithSelect,
applyWithDispatch,
)( ( props ) => {
const {
media,
featuredImageFocalPoint,
setFeaturedImageFocalPoint,
} = props;
if ( media && media.source_url ) {
const url = media.source_url;
const { width, height } = media;
return (
<Fragment>
<PostFeaturedImage { ...props } />
<FocalPointPicker
label={ __( 'Focal point picker' ) }
url={ url }
dimensions={ { width, height } }
value={ featuredImageFocalPoint }
onChange={ ( newFocalPoint ) =>
setFeaturedImageFocalPoint( newFocalPoint )
}
/>
</Fragment>
);
}
return (
<PostFeaturedImage { ...props } />
);
} );
}
const applyWithSelect = withSelect( ( select ) => {
const { getEditedPostAttribute } = select( 'core/editor' );
const featuredImageFocalPoint = getEditedPostAttribute( 'meta' )[ 'featured_image_focal_point' ];
return {
featuredImageFocalPoint,
};
} );
const applyWithDispatch = withDispatch( ( dispatch ) => {
const { editPost } = dispatch( 'core/editor' );
return {
setFeaturedImageFocalPoint( focalPoint ) {
editPost( { meta: { featured_image_focal_point: focalPoint } } );
},
};
} );
addFilter(
'editor.PostFeaturedImage',
'centralex/wrap-post-featured-image',
wrapPostFeaturedImage
);
EDIT: Using compose
with addFilter
got a bit hairy, let me know if I should have done that bit differently.
If this were to be a built-in feature, the theme would need to know how to use the focal point meta values, and it would perhaps be misleading to show the focal point picker by default if the theme doesn't reflect the setting. Perhaps this feature could be behind an add_theme_support
flag?
For me personally, in the future I'd rather just use the existing Cover Image block, but it would need to be aware of the post's featured image, and Gutenberg's template features aren't quite to the point where I feel I can utilize them yet. Also, the Cover Image block uses background-image
, but I prefer to use an actual img
tag to take advantage of srcset
. I output the featured image as an absolutely-positioned img
tag with object-fit: cover
and then use object-position
to set the focal point:
$image = get_post_thumbnail_id();
$focal_point = get_post_meta( get_the_ID(), 'featured_image_focal_point', true );
$focal_point_style = ( $focal_point && $focal_point['x'] && $focal_point['y'] ) ? sprintf('object-position: %d%% %d%%', $focal_point['x']*100, $focal_point['y']*100) : '';
echo wp_get_attachment_image( $image, 'banner-xl', false, array( 'style' => $focal_point_style ) );
Fun fact for anyone attempting to use the code I posted above with a custom post type: your custom post type MUST have 'custom-fields' in its supports
array when the post type is registered.
See https://github.com/WordPress/gutenberg/issues/17018#issuecomment-521483868
Not sure how to modify the code to not run on unsupported post types...
Not sure how to modify the code to not run on unsupported post types...
I'd like to know that as well.
did this a little (but ultimately not so) differently with HigherOrderComponents and it seems to work just as well.
const { __ } = wp.i18n;
const { addFilter } = wp.hooks;
const { Fragment } = wp.element;
const { createHigherOrderComponent } = wp.compose;
const { FocalPointPicker } = wp.components;
const { useEntityProp } = wp.coreData;
/**
* Add Focal Point Picker to Featured Image on posts.
*
* @param {function} PostFeaturedImage Featured Image component.
*
* @return {function} PostFeaturedImage Modified Featured Image component.
*/
const wrapPostFeaturedImage = createHigherOrderComponent(
(PostFeaturedImage) => {
return (props) => {
const { media } = props;
const [meta, setMeta] = useEntityProp('postType', 'post', 'meta');
const setFeaturedImageMeta = (val) => {
setMeta(
Object.assign({}, meta, {
featured_image_focal_point: val,
})
);
};
if (media && media.source_url) {
const url = media.source_url;
return (
<Fragment>
<PostFeaturedImage {...props} />
<FocalPointPicker
label={__('Focal point picker')}
url={url}
value={meta.featured_image_focal_point}
onChange={(newFocalPoint) => setFeaturedImageMeta(newFocalPoint)}
/>
</Fragment>
);
}
return <PostFeaturedImage {...props} />;
};
},
'wrapPostFeaturedImage'
);
addFilter(
'editor.PostFeaturedImage',
'abc/featured-image-control',
wrapPostFeaturedImage
);
couple things I noted:
media
object didn't have the width and height properties where expected, they're buried in the media_details property, and I opted not to use them since the FocalPointPicker
doesn't really need them.useEntityProp
saved a lot of trouble with fetching and saving the meta value. It could also probably be used to restrict the rendering of the FocalPointPicker
component to specific post types.FocalPointPicker
is barely usable in Safari (but that's for another issue)Highly recommend making the FocalPointPicker
component a part of the Featured Image, with the add_theme_support
flag suggestion @cr0ybot mentioned.
$focal_point_style = ( $focal_point && $focal_point['x'] && $focal_point['y'] ) ? sprintf('object-position: %d%% %d%%', $focal_point['x']*100, $focal_point['y']*100) : '';
"0" is a valid value for x and y, but returns false in the statement above, btw, which would prevent any style from being added to the wp_get_attachment_image
call. Maybe isset()
would work better here.
@cr0ybot or @ryanapsmith I'm interested in getting some of your code working, to see if I can push this along at all.
Edit: I've done some research into how this code might run in a Gutenberg context, and it's a little more clear now, but if you happen to have a plugin where this code is in context, it would still be very helpful!
@cr0ybot or @ryanapsmith I'm interested in getting some of your code working, to see if I can push this along at all.
Edit: I've done some research into how this code might run in a Gutenberg context, and it's a little more clear now, but if you happen to have a plugin where this code is in context, it would still be very helpful!
@NickGreen you got it right, you have to enqueue the JS file that contains the snippet I provided.
/**
* Register the JavaScript for the admin area.
*
* @since 1.0.0
*/
public function my_enqueue_scripts() {
wp_enqueue_script( 'admin-blocks', 'admin-blocks.js', array(
'jquery',
'wp-dom-ready',
'wp-i18n',
'wp-hooks',
'wp-element',
'wp-block-editor',
'wp-compose',
'wp-components',
'wp-core-data',
'wp-plugins',
'wp-edit-post',
), '1.0.0', false );
}
add_action('admin_enqueue_scripts', 'my_enqueue_scripts');
admin-blocks.js
would be the file that has the snippet in it, and the dependency array in wp_enqueue_script
function call just makes sure the stuff you need is available.
Insert this php into your theme's functions.php
file (easiest route), or generate a plugin and stick this in there, then activate the plugin (best way). Never generated a plugin before? Couple tools out there, like WP CLI (https://developer.wordpress.org/cli/commands/scaffold/plugin/) or the boilerplate generator found at https://wppb.me
Next time you load the editor, the focal point picker should show up in the sidebar for any post type with featured image support.
This is a really interesting topic, and something that deserves thought into the best way that it would actually work in practice.
First of all, the cover image focal point is a set of x and y coordinates that determine the offset of the image when loaded into a container. It does not re-crop the image. If this same approach were used for the featured image, then you're relying on the theme developer to output the featured image in the various locations using those coordinates.
This technique also isn't as performant as cropping would be; you're loading a larger image into a smaller container, and moving it around. This kind of defeats the whole purpose of having thumbnails of various sizes which are used in the appropriate context (Consider an archive page which could have tens or hundreds of featured image thumbnails. Would you want to load the full sized image in all of those cases?).
Consider how this 3rd party plugin does it: https://wecodepixels.com/shop/theia-smart-thumbnails-for-wordpress/
This is how, as a user, I would assume a focal point picker would work for a featured image. Similar to cropping an image from the Gutenberg image block, I would assume that setting a focal point would create a new version of the image, and re-crop all of the thumbnails based on the new focal point. This would not require theme developers to do anything to support it, since displaying various image sizes in various contexts is already standard practice.
@NickGreen Also the WordPress image sizes support a crop offset: https://developer.wordpress.org/reference/functions/add_image_size/#parameters:~:text=x_crop_position%20accepts%3A%20'left'%2C%20'center'%2C%20or%20'right'.,y_crop_position%20accepts%3A%20'top'%2C%20'center'%2C%20or%20'bottom'.
@NickGreen I'd personally prefer a lossless approach. Selecting a focal point allows the theme developer greater control over aspect ratios, with the content editor only having to worry about placement within an image container, while the developer dictates final sizing across different breakpoints with the container. Cropping would produce undesirable effects there (like trying to cram a square crop into a hero region with a 16:9 aspect ratio). The focal point picker suffices here – for image manipulation like cropping, that's more appropriate in the body of a post than a region predefined in a theme's template.
@ryanapsmith: Good point - but wouldn't <picture
alleviate this issue? It allows art direction.
@NickGreen
no this is just cropping, that is not preferable., we need focal point for responsive images that use object-fill.. so cropping is out of the question
@cr0ybot
I try to load the snippet in the admin but it seems I cannot load in React code.. It results in a Syntax error
had to rewrite it like this... probably am doing something wrong...
const {__} = wp.i18n;
const {addFilter} = wp.hooks;
const {Fragment} = wp.element;
const {createHigherOrderComponent} = wp.compose;
const {FocalPointPicker} = wp.components;
const useEntityProp = wp.coreData.useEntityProp;
const el = wp.element.createElement;
/**
* Add Focal Point Picker to Featured Image on posts.
*
* @param {function} PostFeaturedImage Featured Image component.
*
* @return {function} PostFeaturedImage Modified Featured Image component.
*/
const wrapPostFeaturedImage = createHigherOrderComponent(
(PostFeaturedImage) => {
return (props) => {
const {media} = props;
const [meta, setMeta] = useEntityProp('postType', 'project', 'meta');
const setFeaturedImageMeta = (val) => {
if (meta)
setMeta(
Object.assign({}, meta, {
featured_image_focal_point: val,
})
);
else {
setMeta({
featured_image_focal_point: val,
});
}
};
if (media && media.source_url) {
const url = media.source_url;
return el(
wp.element.Fragment,
{},
'Prepend above',
el(
PostFeaturedImage,
props
),
el(
wp.components.FocalPointPicker,
{
label: __('Focal point picker'),
value: meta?.featured_image_focal_point,
url: url,
onChange: (newFocalPoint) => setFeaturedImageMeta(newFocalPoint)
}
)
)
}
return el(
PostFeaturedImage,
props
)
};
},
'wrapPostFeaturedImage'
);
wp.hooks.addFilter(
'editor.PostFeaturedImage',
'koraysels/wrap-post-featured-image',
wrapPostFeaturedImage
);
If you want it to be displayed in wp-graphql i wrote this:
add_action('graphql_register_types', function () {
register_graphql_object_type('FocalPoint', [
'description' => __("FocalPoint of image", 'your-textdomain'),
'fields' => [
'x' => [
'type' => 'Number',
'description' => __('x focal point', 'your-textdomain'),
],
'y' => [
'type' => 'Number',
'description' => __('y focal point', 'your-textdomain'),
]
],
]);
});
add_action('graphql_register_types', function () {
register_graphql_field('NodeWithFeaturedImage', 'featuredImageFocalPoint', [
'type' => 'FocalPoint',
'description' => __('Focal Point of the featured Image', 'your-textdomain'),
'resolve' => function (\WPGraphQL\Model\Post $post, $args, $context, $info) {
return get_post_meta($post->ID, 'featured_image_focal_point', true);
}
]);
});
had to rewrite it like this... probably am doing something wrong...
You would need to be set up with a build step to use JSX syntax.
Also, sorry for being absent from this thread but @ryanapsmith did a great job condensing/simplifying the implementation via createHigherOrderComponent
and newer hooks like useEntityProp
.
Maybe I'll revisit this next time I have a need for it, though with the new Post Featured Image block (#19875) maybe this is not quite as relevant anymore, especially with the push towards full site editing instead of coded themes.
This is such a good idea. Setting the focal point would ideally be capable on all images in the Media Library. Most themes register a variety of different sizes, with different aspect ratios, and many plugins register their own as well. Displaying these images with a reasonable crop would be such a benefit to sites.
There have been various plugins attempting this over the years, but most seem abandoned or add way too much additional complexity beyond simply setting a focal point.
FWIW I like the way https://wordpress.org/plugins/better-image-sizes/ implements this (h/t @kubiqsk). The admin UI is really nice, providing previews of how the cropped image will look at different aspect ratios. It also includes face detection, which in my experience is the primary reason for wanting to set the focal point in the first place.
Is your feature request related to a problem? Please describe. It would be nice if the Featured Image field in Gutenberg editor also supports focal point + crop like in e.g. the cover block.
Describe the solution you'd like Allow/offer focal point + crop controls for featured image fields.
Describe alternatives you've considered Using a plugin for this, but this will introduce completely different, "classic" UI.
Edit: Similar plugin that does it for all media though: https://wordpress.org/plugins/wp-smartcrop/