-
Notifications
You must be signed in to change notification settings - Fork 4.8k
LinkControl: Add support for images in attachment search results #58458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
b402cfe
1c1eaaf
2e6226b
37137e9
52ae180
7f1a659
58d35ba
59f980f
eb93565
fc302a1
f8eb929
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| <?php | ||
| /** | ||
| * REST API: WP_REST_Media_Search_Handler class | ||
| * | ||
| * @package WordPress | ||
| * @subpackage REST_API | ||
| * @since 6.6.0 | ||
| * */ | ||
|
|
||
| if ( class_exists( 'WP_REST_Media_Search_Handler' ) ) { | ||
| return; | ||
| } | ||
|
|
||
| /** | ||
| * Core class representing a search handler for attachments in the REST API. | ||
| * | ||
| * @since 6.6.0 | ||
| * | ||
| * @see WP_REST_Search_Handler | ||
| */ | ||
| class WP_REST_Media_Search_Handler_Gutenberg_6_6 extends WP_REST_Post_Search_Handler { | ||
|
|
||
| /** | ||
| * Constructor. | ||
| * | ||
| * @since 6.6.0 | ||
| */ | ||
| public function __construct() { | ||
| parent::__construct(); | ||
| $this->type = 'media'; | ||
| $this->subtypes = array(); | ||
| } | ||
|
|
||
| /** | ||
| * Searches the object type content for a given search request. | ||
| * | ||
| * @since 6.6.0 | ||
| * | ||
| * @param WP_REST_Request $request Full REST request. | ||
| * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing | ||
| * an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the | ||
| * total count for the matching search results. | ||
| */ | ||
| public function search_items( WP_REST_Request $request ) { | ||
|
|
||
| $query_args = array( | ||
| 'post_type' => 'attachment', | ||
| 'post_status' => 'inherit', | ||
| 'paged' => (int) $request['page'], | ||
| 'posts_per_page' => (int) $request['per_page'], | ||
| ); | ||
|
|
||
| if ( ! empty( $request['search'] ) ) { | ||
| $query_args['s'] = $request['search']; | ||
|
|
||
| // Filter query clauses to include filenames. | ||
| add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); | ||
| } | ||
|
|
||
| if ( ! empty( $request['exclude'] ) ) { | ||
| $query_args['post__not_in'] = $request['exclude']; | ||
| } | ||
|
|
||
| if ( ! empty( $request['include'] ) ) { | ||
| $query_args['post__in'] = $request['include']; | ||
| } | ||
|
|
||
| /** | ||
| * Filters the query arguments for a REST API search request. | ||
| * | ||
| * Enables adding extra arguments or setting defaults for a media search request. | ||
| * | ||
| * @since 6.6.0 | ||
| * | ||
| * @param array $query_args Key value array of query var to query value. | ||
| * @param WP_REST_Request $request The request used. | ||
| */ | ||
| $query_args = apply_filters( 'rest_media_search_query', $query_args, $request ); | ||
|
|
||
| $query = new WP_Query(); | ||
| $posts = $query->query( $query_args ); | ||
| // Querying the whole post object will warm the object cache, avoiding an extra query per result. | ||
| $found_ids = wp_list_pluck( $posts, 'ID' ); | ||
| $total = $query->found_posts; | ||
|
|
||
| return array( | ||
| self::RESULT_IDS => $found_ids, | ||
| self::RESULT_TOTAL => $total, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Prepares the search result for a given ID. | ||
| * | ||
| * @since 6.6.0 | ||
| * | ||
| * @param int $id Item ID. | ||
| * @param array $fields Fields to include for the item. | ||
| * @return array Associative array containing all fields for the item. | ||
| */ | ||
| public function prepare_item( $id, array $fields ) { | ||
| $data = parent::prepare_item( $id, $fields ); | ||
|
|
||
| if ( isset( $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] ) ) { | ||
| unset( $data[ WP_REST_Search_Controller::PROP_SUBTYPE ] ); | ||
| } | ||
|
|
||
| return $data; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| <?php | ||
| /** | ||
| * PHP and WordPress configuration compatibility functions for the Gutenberg | ||
| * editor plugin changes related to REST API. | ||
| * | ||
| * @package gutenberg | ||
| */ | ||
|
|
||
| if ( ! defined( 'ABSPATH' ) ) { | ||
| die( 'Silence is golden.' ); | ||
| } | ||
|
|
||
| /** | ||
| * Registers additional fields for search result rest api. | ||
| * | ||
| * @access private* @internal | ||
| * | ||
| * @param array $result_object Search result object. | ||
| * @param string $field_name Current additional field's name (unused). | ||
| * @param \WP_REST_Request $request Rest request object. | ||
| * @return string Thumbnail for the result object. | ||
| */ | ||
| function _gutenberg_get_search_result_thumbnail_field( $result_object ) { | ||
|
|
||
| $object_id = $result_object['id']; | ||
| if ( empty( $object_id ) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| $thumbnail = wp_get_attachment_image_src( $object_id, 'thumbnail' ); | ||
|
|
||
| return $thumbnail[0] ? $thumbnail[0] : ''; | ||
| } | ||
|
|
||
| /** | ||
| * Registers additional fields for search result rest api. | ||
| * | ||
| * @access private* @internal | ||
| * | ||
| * @param array $result_object Search result object. | ||
| * @param string $field_name Current additional field's name (unused). | ||
| * @param \WP_REST_Request $request Rest request object. | ||
| * @return string Alt text for the result object. | ||
| */ | ||
| function _gutenberg_get_search_result_alt_text_field( $result_object ) { | ||
|
|
||
| $object_id = $result_object['id']; | ||
| if ( empty( $object_id ) ) { | ||
| return ''; | ||
| } | ||
|
|
||
| $alt_text = get_post_meta( $object_id, '_wp_attachment_image_alt', true ); | ||
|
|
||
| return $alt_text; | ||
| } | ||
|
|
||
| /** | ||
| * Registers additional fields for search result rest api. | ||
| * | ||
| * @access private | ||
| * @internal | ||
| */ | ||
| function _gutenberg_register_search_result_additional_fields() { | ||
| global $wp_rest_additional_fields; | ||
|
|
||
| if ( isset( $wp_rest_additional_fields['search-result']['thumbnail'] ) ) { | ||
| return; | ||
| } | ||
|
|
||
| register_rest_field( | ||
| 'search-result', | ||
| 'thumbnail', | ||
| array( | ||
| 'get_callback' => '_gutenberg_get_search_result_thumbnail_field', | ||
| 'update_callback' => null, | ||
| 'schema' => array( | ||
| 'description' => __( 'Object human readable subtype.', 'gutenberg' ), | ||
| 'type' => 'string', | ||
| 'readonly' => true, | ||
| 'context' => array( 'view', 'embed' ), | ||
| ), | ||
| ) | ||
| ); | ||
|
|
||
| register_rest_field( | ||
| 'search-result', | ||
| 'alt_text', | ||
| array( | ||
| 'get_callback' => '_gutenberg_get_search_result_alt_text_field', | ||
| 'update_callback' => null, | ||
| 'schema' => array( | ||
| 'description' => __( 'Object human readable subtype.', 'gutenberg' ), | ||
| 'type' => 'string', | ||
| 'readonly' => true, | ||
| 'context' => array( 'view', 'embed' ), | ||
| ), | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| add_action( 'rest_api_init', '_gutenberg_register_search_result_additional_fields' ); | ||
|
|
||
| /** | ||
| * Register custom rest media search handler. | ||
| * | ||
| * @param array $handlers | ||
| * | ||
| * @return array | ||
| */ | ||
| function _gutenberg_register_media_search_handler( $handlers ) { | ||
| if ( class_exists( 'WP_REST_Media_Search_Handler_Gutenberg_6_6' ) ) { | ||
| $handlers[] = new WP_REST_Media_Search_Handler_Gutenberg_6_6(); | ||
| } | ||
|
|
||
| return $handlers; | ||
| } | ||
|
|
||
| add_action( 'wp_rest_search_handlers', '_gutenberg_register_media_search_handler' ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /** | ||
| * Checks if the url is an image. | ||
| * @param {string} url the url we are checking. | ||
| * @return {boolean} true if the url is an image url. | ||
| */ | ||
| export default function isImageUrl( url ) { | ||
| const pattern = | ||
| /^(https?:\/\/)([\w-]+(?:\.[\w-]+)*\/)*[\w-]+\.(?:jpg|JPG|jpeg|gif|png|webp)$/i; | ||
| return pattern.test( url ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,11 @@ | |
| */ | ||
| import classnames from 'classnames'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import isImageUrl from './is-url-image'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
|
|
@@ -56,8 +61,22 @@ export default function LinkPreview( { | |
|
|
||
| let icon; | ||
|
|
||
| const attachmentImg = | ||
| value.type === 'attachment' && isImageUrl( value?.url ) | ||
| ? value.url | ||
| : null; | ||
|
|
||
| if ( richData?.icon ) { | ||
| icon = <img src={ richData?.icon } alt="" />; | ||
| } else if ( attachmentImg ) { | ||
| //TODO we don't have an alt text for this image | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An empty |
||
| icon = ( | ||
| <img | ||
| className="block-editor-link-control__search-item-media-thumbnail" | ||
| src={ attachmentImg } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this loads the original sized image, it feels a little overkill
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both this and a thumbnail for the image are on coreStore, but the component shouldn't have to know about core-data. Should we pass that extra info to all instances of the link preview, or should we leave this as is?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How big is the image? If it's huge then I don't think this is acceptable. I did have a plan to use the This selector would need to be passed in via blockEditorSettings because - as you noted - we cannot consume Core Store directly here. If we do this I would consider whether it's better to do that bit as a separate PR.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #50998 this one perhaps? |
||
| alt="" | ||
| /> | ||
| ); | ||
| } else if ( isEmptyURL ) { | ||
| icon = <Icon icon={ info } size={ 32 } />; | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import isImageUrl from '../is-url-image'; | ||
|
|
||
| describe( 'isImageUrl', () => { | ||
| // use .each to test multiple cases | ||
| it.each( [ | ||
| [ true, 'https://example.com/image.jpg' ], | ||
| [ true, 'https://example.com/image.gif' ], | ||
| [ true, 'https://example.com/image.png' ], | ||
| [ true, 'https://example.com/image.webp' ], | ||
| [ true, 'https://example.com/image.jpeg' ], | ||
| [ true, 'https://example.com/image.JPG' ], | ||
| [ false, 'https://example.com/image.txt' ], | ||
| [ false, 'https://example.com/image' ], | ||
| [ false, 'https://example.com/image.jpg?query=123' ], | ||
| [ false, '' ], | ||
| [ false, null ], | ||
| [ false, undefined ], | ||
| [ false, 123 ], | ||
| ] )( | ||
| 'returns %s when testing against URL "%s" for a valid image', | ||
| ( expected, testString ) => { | ||
| expect( isImageUrl( testString ) ).toBe( expected ); | ||
| } | ||
| ); | ||
| } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I replicated the intention of this PR and gave credit to the author in case this one gets merged first