PluginProbe
Gutenberg / 22.9.0
Gutenberg v22.9.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / build / scripts / block-library / image.php

image.php in Gutenberg 22.9.0, at build/scripts/block-library/image.php

422 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server-side rendering of the `core/image` block.
4 *
5 * @package WordPress
6 */
7
8 /**
9 * Renders the `core/image` block on the server,
10 * adding a data-id attribute to the element if core/gallery has added on pre-render.
11 *
12 * @since 5.9.0
13 *
14 * @param array $attributes The block attributes.
15 * @param string $content The block content.
16 * @param WP_Block $block The block object.
17 *
18 * @return string The block content with the data-id attribute added.
19 */
20 function gutenberg_render_block_core_image( $attributes, $content, $block ) {
21 if ( false === stripos( $content, '<img' ) ) {
22 return '';
23 }
24
25 $processor = new class( $content ) extends WP_HTML_Tag_Processor {
26 /**
27 * Return input span for an empty FIGCAPTION element.
28 *
29 * Returns span of input for an empty FIGCAPTION, if currently matched on a
30 * FIGCAPTION opening tag and if the element is properly closed and empty.
31 *
32 * @since 6.9.0
33 *
34 * @return WP_HTML_Span|false Span of input if the element is empty; otherwise false.
35 */
36 public function block_core_image_extract_empty_figcaption_element() {
37 $this->set_bookmark( 'here' );
38 $opener = $this->bookmarks['here'];
39
40 // Allow comments within the definition of “empty.”
41 while ( $this->next_token() && '#comment' === $this->get_token_name() ) {
42 continue;
43 }
44
45 if ( 'FIGCAPTION' !== $this->get_tag() || ! $this->is_tag_closer() ) {
46 return false;
47 }
48
49 $this->set_bookmark( 'here' );
50 $closer = $this->bookmarks['here'];
51
52 return new WP_HTML_Span( $opener->start, $closer->start + $closer->length - $opener->start );
53 }
54 };
55
56 if ( ! $processor->next_tag( 'img' ) || ! $processor->get_attribute( 'src' ) ) {
57 return '';
58 }
59
60 $has_id_binding = isset( $attributes['metadata']['bindings']['id'] ) && isset( $attributes['id'] );
61
62 // Ensure the `wp-image-id` classname on the image block supports block bindings.
63 if ( $has_id_binding ) {
64 // If there's a mismatch with the 'wp-image-' class and the actual id, the id was
65 // probably overridden by block bindings. Update it to the correct value.
66 // See https://github.com/WordPress/gutenberg/issues/62886 for why this is needed.
67 $id = $attributes['id'];
68 $image_classnames = $processor->get_attribute( 'class' );
69 $class_with_binding_value = "wp-image-$id";
70 if ( is_string( $image_classnames ) && ! str_contains( $image_classnames, $class_with_binding_value ) ) {
71 $image_classnames = preg_replace( '/wp-image-(\d+)/', $class_with_binding_value, $image_classnames );
72 $processor->set_attribute( 'class', $image_classnames );
73 }
74 }
75
76 // For backwards compatibility, the data-id html attribute is only set for
77 // image blocks nested in a gallery. Detect if the image is in a gallery by
78 // checking the data-id attribute.
79 // See the `block_core_gallery_data_id_backcompatibility` function.
80 if ( isset( $attributes['data-id'] ) ) {
81 // If there's a binding for the `id`, the `id` attribute is used for the
82 // value, since `data-id` does not support block bindings.
83 // Else the `data-id` is used for backwards compatibility, since
84 // third parties may be filtering its value.
85 $data_id = $has_id_binding ? $attributes['id'] : $attributes['data-id'];
86 $processor->set_attribute( 'data-id', $data_id );
87 }
88
89 /*
90 * If the `caption` attribute is empty and we encounter a `<figcaption>` element,
91 * we take note of its span so we can remove it later.
92 */
93 if ( $processor->next_tag( 'FIGCAPTION' ) && empty( $attributes['caption'] ) ) {
94 $figcaption_span = $processor->block_core_image_extract_empty_figcaption_element();
95 }
96
97 $link_destination = $attributes['linkDestination'] ?? 'none';
98 $lightbox_settings = gutenberg_block_core_image_get_lightbox_settings( $block->parsed_block );
99
100 /*
101 * If the lightbox is enabled and the image is not linked, adds the filter and
102 * the JavaScript view file.
103 */
104 if (
105 isset( $lightbox_settings ) &&
106 'none' === $link_destination &&
107 isset( $lightbox_settings['enabled'] ) &&
108 true === $lightbox_settings['enabled']
109 ) {
110 wp_enqueue_script_module( '@wordpress/block-library/image/view' );
111
112 /*
113 * This render needs to happen in a filter with priority 15 to ensure that
114 * it runs after the duotone filter and that duotone styles are applied to
115 * the image in the lightbox. Lightbox has to work with any plugins that
116 * might use filters as well. Removing this can be considered in the future
117 * if the way the blocks are rendered changes, or if a new kind of filter is
118 * introduced.
119 */
120 add_filter( 'render_block_core/image', 'gutenberg_block_core_image_render_lightbox', 15, 3 );
121 } else {
122 /*
123 * Remove the filter if previously added by other Image blocks.
124 */
125 remove_filter( 'render_block_core/image', 'gutenberg_block_core_image_render_lightbox', 15 );
126 }
127
128 $output = $processor->get_updated_html();
129 if ( ! empty( $figcaption_span ) ) {
130 return substr( $output, 0, $figcaption_span->start ) . substr( $output, $figcaption_span->start + $figcaption_span->length );
131 }
132 return $output;
133 }
134
135 /**
136 * Adds the lightboxEnabled flag to the block data.
137 *
138 * This is used to determine whether the lightbox should be rendered or not.
139 *
140 * @since 6.4.0
141 *
142 * @param array $block Block data.
143 *
144 * @return array|null Filtered block data.
145 */
146 function gutenberg_block_core_image_get_lightbox_settings( $block ) {
147 // Gets the lightbox setting from the block attributes.
148 if ( isset( $block['attrs']['lightbox'] ) ) {
149 $lightbox_settings = $block['attrs']['lightbox'];
150 }
151
152 if ( ! isset( $lightbox_settings ) ) {
153 $lightbox_settings = gutenberg_get_global_settings( array( 'lightbox' ), array( 'block_name' => 'core/image' ) );
154
155 // If not present in global settings, check the top-level global settings.
156 //
157 // NOTE: If no block-level settings are found, the previous call to
158 // `gutenberg_get_global_settings` will return the whole `theme.json` structure in
159 // which case we can check if the "lightbox" key is present at the top-level
160 // of the global settings and use its value.
161 if ( isset( $lightbox_settings['lightbox'] ) ) {
162 $lightbox_settings = gutenberg_get_global_settings( array( 'lightbox' ) );
163 }
164 }
165
166 return $lightbox_settings ?? null;
167 }
168
169 /**
170 * Adds the directives and layout needed for the lightbox behavior.
171 *
172 * @since 6.4.0
173 *
174 * @param string $block_content Rendered block content.
175 * @param array $block Block object.
176 * @param array $block_instance Block instance.
177 *
178 * @return string Filtered block content.
179 */
180 function gutenberg_block_core_image_render_lightbox( $block_content, $block, $block_instance ) {
181 /*
182 * If there's no IMG tag in the block then return the given block content
183 * as-is. There's nothing that this code can knowingly modify to add the
184 * lightbox behavior.
185 */
186 $processor = new WP_HTML_Tag_Processor( $block_content );
187
188 if ( $processor->next_tag( 'figure' ) ) {
189 $processor->set_bookmark( 'figure' );
190 }
191 if ( ! $processor->next_tag( 'img' ) ) {
192 return $block_content;
193 }
194
195 $alt = $processor->get_attribute( 'alt' );
196 $img_uploaded_src = $processor->get_attribute( 'src' );
197 $img_class_names = $processor->get_attribute( 'class' );
198 $img_styles = $processor->get_attribute( 'style' );
199 $img_width = 'none';
200 $img_height = 'none';
201 $img_srcset = false;
202
203 wp_interactivity_config(
204 'core/image',
205 array(
206 'defaultAriaLabel' => __( 'Enlarged image' ),
207 'closeButtonText' => esc_html__( 'Close' ),
208 'prevButtonText' => esc_html__( 'Previous' ),
209 'nextButtonText' => esc_html__( 'Next' ),
210 )
211 );
212
213 if ( $alt ) {
214 /* translators: %s: Image alt text. */
215 $custom_aria_label = sprintf( __( 'Enlarged image: %s' ), $alt );
216 }
217
218 if ( isset( $block['attrs']['id'] ) ) {
219 $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
220 $img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
221 $has_dimensions = ( $img_metadata['width'] ?? '' ) && ( $img_metadata['height'] ?? '' );
222 $srcset_size = $has_dimensions ? array( $img_metadata['width'], $img_metadata['height'] ) : 'large';
223 $img_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'], $srcset_size );
224 $img_width = $img_metadata['width'] ?? 'none';
225 $img_height = $img_metadata['height'] ?? 'none';
226 }
227
228 // Figure.
229 $processor->seek( 'figure' );
230 $figure_class_names = $processor->get_attribute( 'class' );
231 $figure_styles = $processor->get_attribute( 'style' );
232
233 // Create unique id and set the image metadata in the state.
234 $unique_image_id = uniqid();
235 wp_interactivity_state(
236 'core/image',
237 array(
238 'metadata' => array(
239 $unique_image_id => array(
240 'uploadedSrc' => $img_uploaded_src,
241 'lightboxSrcset' => $img_srcset,
242 'figureClassNames' => $figure_class_names,
243 'figureStyles' => $figure_styles,
244 'imgClassNames' => $img_class_names,
245 'imgStyles' => $img_styles,
246 'targetWidth' => $img_width,
247 'targetHeight' => $img_height,
248 'scaleAttr' => $block['attrs']['scale'] ?? false,
249 'alt' => $alt,
250 'galleryId' => $block_instance->context['galleryId'] ?? null,
251 'customAriaLabel' => $custom_aria_label ?? null,
252 'navigationButtonType' => $block_instance->context['navigationButtonType'] ?? 'icon',
253 'triggerButtonAriaLabel' => null,
254 ),
255 ),
256 )
257 );
258
259 $processor->add_class( 'wp-lightbox-container' );
260 $processor->set_attribute( 'data-wp-interactive', 'core/image' );
261 $processor->set_attribute(
262 'data-wp-context',
263 wp_json_encode(
264 array( 'imageId' => $unique_image_id ),
265 JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
266 )
267 );
268 $processor->set_attribute( 'data-wp-key', $unique_image_id );
269
270 // Image.
271 $processor->next_tag( 'img' );
272 $processor->set_attribute( 'data-wp-init', 'callbacks.setButtonStyles' );
273 $processor->set_attribute( 'data-wp-on--load', 'callbacks.setButtonStyles' );
274 $processor->set_attribute( 'data-wp-on-window--resize', 'callbacks.setButtonStyles' );
275
276 // Set an event to preload the image on pointerenter and pointerdown(mobile).
277 // Pointerleave is used to cancel the preload if the user hovers away from the image
278 // before the predefined delay.
279 $processor->set_attribute( 'data-wp-on--pointerenter', 'actions.preloadImageWithDelay' );
280 $processor->set_attribute( 'data-wp-on--pointerdown', 'actions.preloadImage' );
281 $processor->set_attribute( 'data-wp-on--pointerleave', 'actions.cancelPreload' );
282
283 // Sets an event callback on the `img` because the `figure` element can also
284 // contain a caption, and we don't want to trigger the lightbox when the
285 // caption is clicked.
286 $processor->set_attribute( 'data-wp-on--click', 'actions.showLightbox' );
287 $processor->set_attribute( 'data-wp-class--hide', 'state.isContentHidden' );
288 $processor->set_attribute( 'data-wp-class--show', 'state.isContentVisible' );
289
290 $body_content = $processor->get_updated_html();
291
292 // Adds a button alongside image in the body content.
293 $img = null;
294 preg_match( '/<img[^>]+>/', $body_content, $img );
295
296 $button =
297 $img[0]
298 . '<button
299 class="lightbox-trigger"
300 type="button"
301 aria-haspopup="dialog"
302 data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
303 data-wp-init="callbacks.initTriggerButton"
304 data-wp-on--click="actions.showLightbox"
305 data-wp-style--right="state.thisImage.buttonRight"
306 data-wp-style--top="state.thisImage.buttonTop"
307 >
308 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
309 <path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
310 </svg>
311 </button>';
312
313 $body_content = preg_replace( '/<img[^>]+>/', $button, $body_content );
314
315 add_action( 'wp_footer', 'gutenberg_block_core_image_print_lightbox_overlay' );
316
317 return $body_content;
318 }
319
320 /**
321 * @since 6.5.0
322 */
323 function gutenberg_block_core_image_print_lightbox_overlay() {
324 $dialog_label = esc_attr__( 'Enlarged images' );
325 $close_button_text = esc_attr__( 'Close' );
326 $prev_button_text = esc_attr__( 'Previous' );
327 $next_button_text = esc_attr__( 'Next' );
328 $close_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="m13.06 12 6.47-6.47-1.06-1.06L12 10.94 5.53 4.47 4.47 5.53 10.94 12l-6.47 6.47 1.06 1.06L12 13.06l6.47 6.47 1.06-1.06L13.06 12Z"></path></svg>';
329 $prev_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="28" aria-hidden="true" focusable="false"><path d="M14.6 7l-1.2-1L8 12l5.4 6 1.2-1-4.6-5z"></path></svg>';
330 $next_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="28" aria-hidden="true" focusable="false"><path d="M10.6 6L9.4 7l4.6 5-4.6 5 1.2 1 5.4-6z"></path></svg>';
331
332 // If the current theme does NOT have a `theme.json`, or the colors are not
333 // defined, it needs to set the background color & close button color to some
334 // default values because it can't get them from the Global Styles.
335 $background_color = '#fff';
336 $close_button_color = '#000';
337 if ( wp_theme_has_theme_json() ) {
338 $global_styles_color = wp_get_global_styles( array( 'color' ) );
339 if ( ! empty( $global_styles_color['background'] ) ) {
340 $background_color = esc_attr( $global_styles_color['background'] );
341 }
342 if ( ! empty( $global_styles_color['text'] ) ) {
343 $close_button_color = esc_attr( $global_styles_color['text'] );
344 }
345 }
346
347 echo <<<HTML
348 <div
349 class="wp-lightbox-overlay zoom"
350 aria-label="{$dialog_label}"
351 data-wp-interactive="core/image"
352 data-wp-router-region='{ "id": "core/image-overlay", "attachTo": "body" }'
353 data-wp-key="wp-lightbox-overlay"
354 data-wp-context='{}'
355 data-wp-bind--role="state.roleAttribute"
356 data-wp-bind--aria-label="state.ariaLabel"
357 data-wp-bind--aria-modal="state.ariaModal"
358 data-wp-class--active="state.overlayEnabled"
359 data-wp-class--show-closing-animation="state.overlayOpened"
360 data-wp-watch---focus="callbacks.setOverlayFocus"
361 data-wp-watch---inert="callbacks.setInertElements"
362 data-wp-on--keydown="actions.handleKeydown"
363 data-wp-on--touchstart="actions.handleTouchStart"
364 data-wp-on--touchmove="actions.handleTouchMove"
365 data-wp-on--touchend="actions.handleTouchEnd"
366 data-wp-on--click="actions.hideLightbox"
367 data-wp-on-window--resize="callbacks.setOverlayStyles"
368 data-wp-on-window--scroll="actions.handleScroll"
369 data-wp-bind--style="state.overlayStyles"
370 tabindex="-1"
371 >
372 <button type="button" style="fill:{$close_button_color}" class="wp-lightbox-close-button" data-wp-bind--aria-label="state.closeButtonAriaLabel">
373 <span class="wp-lightbox-close-icon" data-wp-bind--hidden="!state.hasNavigationIcon">{$close_button_icon}</span>
374 <span class="wp-lightbox-close-text" data-wp-bind--hidden="!state.hasNavigationText">{$close_button_text}</span>
375 </button>
376 <button type="button" style="fill:{$close_button_color}" class="wp-lightbox-navigation-button wp-lightbox-navigation-button-prev" data-wp-bind--hidden="!state.hasNavigation" data-wp-on--click="actions.showPreviousImage" data-wp-bind--aria-label="state.prevButtonAriaLabel">
377 <span class="wp-lightbox-navigation-icon" data-wp-bind--hidden="!state.hasNavigationIcon">{$prev_button_icon}</span>
378 <span class="wp-lightbox-navigation-text" data-wp-bind--hidden="!state.hasNavigationText">{$prev_button_text}</span>
379 </button>
380 <div class="lightbox-image-container">
381 <figure data-wp-bind--class="state.selectedImage.figureClassNames" data-wp-bind--style="state.figureStyles">
382 <img data-wp-bind--alt="state.selectedImage.alt" data-wp-bind--class="state.selectedImage.imgClassNames" data-wp-bind--style="state.imgStyles" data-wp-bind--src="state.selectedImage.currentSrc">
383 </figure>
384 </div>
385 <div class="lightbox-image-container">
386 <figure data-wp-bind--class="state.selectedImage.figureClassNames" data-wp-bind--style="state.figureStyles">
387 <img
388 data-wp-bind--alt="state.selectedImage.alt"
389 data-wp-bind--class="state.selectedImage.imgClassNames"
390 data-wp-bind--style="state.imgStyles"
391 data-wp-bind--src="state.enlargedSrc"
392 data-wp-bind--srcset="state.enlargedSrcset"
393 data-wp-bind--srcset="state.enlargedSrcset"
394 sizes="100vw"
395 >
396 </figure>
397 </div>
398 <button type="button" style="fill:{$close_button_color}" class="wp-lightbox-navigation-button wp-lightbox-navigation-button-next" data-wp-bind--hidden="!state.hasNavigation" data-wp-on--click="actions.showNextImage" data-wp-bind--aria-label="state.nextButtonAriaLabel">
399 <span class="wp-lightbox-navigation-text" data-wp-bind--hidden="!state.hasNavigationText">{$next_button_text}</span>
400 <span class="wp-lightbox-navigation-icon" data-wp-bind--hidden="!state.hasNavigationIcon">{$next_button_icon}</span>
401 </button>
402 <div data-wp-text="state.ariaLabel" aria-live="polite" aria-atomic="true" class="screen-reader-text"></div>
403 <div class="scrim" style="background-color: {$background_color}" aria-hidden="true"></div>
404 </div>
405 HTML;
406 }
407
408 /**
409 * Registers the `core/image` block on server.
410 *
411 * @since 5.9.0
412 */
413 function gutenberg_register_block_core_image() {
414 register_block_type_from_metadata(
415 __DIR__ . '/image',
416 array(
417 'render_callback' => 'gutenberg_render_block_core_image',
418 )
419 );
420 }
421 add_action( 'init', 'gutenberg_register_block_core_image', 20 );
422