PluginProbe
Gutenberg / 22.3.0
Gutenberg v22.3.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.3.0, at build/scripts/block-library/image.php

386 lines 14.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 = isset( $attributes['linkDestination'] ) ? $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, 2 );
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 *
177 * @return string Filtered block content.
178 */
179 function gutenberg_block_core_image_render_lightbox( $block_content, $block ) {
180 /*
181 * If there's no IMG tag in the block then return the given block content
182 * as-is. There's nothing that this code can knowingly modify to add the
183 * lightbox behavior.
184 */
185 $processor = new WP_HTML_Tag_Processor( $block_content );
186 if ( $processor->next_tag( 'figure' ) ) {
187 $processor->set_bookmark( 'figure' );
188 }
189 if ( ! $processor->next_tag( 'img' ) ) {
190 return $block_content;
191 }
192
193 $alt = $processor->get_attribute( 'alt' );
194 $img_uploaded_src = $processor->get_attribute( 'src' );
195 $img_class_names = $processor->get_attribute( 'class' );
196 $img_styles = $processor->get_attribute( 'style' );
197 $img_width = 'none';
198 $img_height = 'none';
199 $img_srcset = false;
200 $aria_label = __( 'Enlarge' );
201 $dialog_aria_label = __( 'Enlarged image' );
202
203 if ( isset( $block['attrs']['id'] ) ) {
204 $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
205 $img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
206 $img_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'] );
207 $img_width = $img_metadata['width'] ?? 'none';
208 $img_height = $img_metadata['height'] ?? 'none';
209 }
210
211 // Figure.
212 $processor->seek( 'figure' );
213 $figure_class_names = $processor->get_attribute( 'class' );
214 $figure_styles = $processor->get_attribute( 'style' );
215
216 // Create unique id and set the image metadata in the state.
217 $unique_image_id = uniqid();
218
219 wp_interactivity_state(
220 'core/image',
221 array(
222 'metadata' => array(
223 $unique_image_id => array(
224 'uploadedSrc' => $img_uploaded_src,
225 'lightboxSrcset' => $img_srcset,
226 'figureClassNames' => $figure_class_names,
227 'figureStyles' => $figure_styles,
228 'imgClassNames' => $img_class_names,
229 'imgStyles' => $img_styles,
230 'targetWidth' => $img_width,
231 'targetHeight' => $img_height,
232 'scaleAttr' => $block['attrs']['scale'] ?? false,
233 'ariaLabel' => $dialog_aria_label,
234 'alt' => $alt,
235 ),
236 ),
237 )
238 );
239
240 $processor->add_class( 'wp-lightbox-container' );
241 $processor->set_attribute( 'data-wp-interactive', 'core/image' );
242 $processor->set_attribute(
243 'data-wp-context',
244 wp_json_encode(
245 array(
246 'imageId' => $unique_image_id,
247 ),
248 JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
249 )
250 );
251 $processor->set_attribute( 'data-wp-key', $unique_image_id );
252
253 // Image.
254 $processor->next_tag( 'img' );
255 $processor->set_attribute( 'data-wp-init', 'callbacks.setButtonStyles' );
256 $processor->set_attribute( 'data-wp-on--load', 'callbacks.setButtonStyles' );
257 $processor->set_attribute( 'data-wp-on-window--resize', 'callbacks.setButtonStyles' );
258
259 // Set an event to preload the image on pointerenter and pointerdown(mobile).
260 // Pointerleave is used to cancel the preload if the user hovers away from the image
261 // before the predefined delay.
262 $processor->set_attribute( 'data-wp-on--pointerenter', 'actions.preloadImageWithDelay' );
263 $processor->set_attribute( 'data-wp-on--pointerdown', 'actions.preloadImage' );
264 $processor->set_attribute( 'data-wp-on--pointerleave', 'actions.cancelPreload' );
265
266 // Sets an event callback on the `img` because the `figure` element can also
267 // contain a caption, and we don't want to trigger the lightbox when the
268 // caption is clicked.
269 $processor->set_attribute( 'data-wp-on--click', 'actions.showLightbox' );
270 $processor->set_attribute( 'data-wp-class--hide', 'state.isContentHidden' );
271 $processor->set_attribute( 'data-wp-class--show', 'state.isContentVisible' );
272
273 $body_content = $processor->get_updated_html();
274
275 // Adds a button alongside image in the body content.
276 $img = null;
277 preg_match( '/<img[^>]+>/', $body_content, $img );
278
279 $button =
280 $img[0]
281 . '<button
282 class="lightbox-trigger"
283 type="button"
284 aria-haspopup="dialog"
285 aria-label="' . esc_attr( $aria_label ) . '"
286 data-wp-init="callbacks.initTriggerButton"
287 data-wp-on--click="actions.showLightbox"
288 data-wp-style--right="state.imageButtonRight"
289 data-wp-style--top="state.imageButtonTop"
290 >
291 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
292 <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" />
293 </svg>
294 </button>';
295
296 $body_content = preg_replace( '/<img[^>]+>/', $button, $body_content );
297
298 add_action( 'wp_footer', 'gutenberg_block_core_image_print_lightbox_overlay' );
299
300 return $body_content;
301 }
302
303 /**
304 * @since 6.5.0
305 */
306 function gutenberg_block_core_image_print_lightbox_overlay() {
307 $close_button_label = esc_attr__( 'Close' );
308
309 // If the current theme does NOT have a `theme.json`, or the colors are not
310 // defined, it needs to set the background color & close button color to some
311 // default values because it can't get them from the Global Styles.
312 $background_color = '#fff';
313 $close_button_color = '#000';
314 if ( wp_theme_has_theme_json() ) {
315 $global_styles_color = wp_get_global_styles( array( 'color' ) );
316 if ( ! empty( $global_styles_color['background'] ) ) {
317 $background_color = esc_attr( $global_styles_color['background'] );
318 }
319 if ( ! empty( $global_styles_color['text'] ) ) {
320 $close_button_color = esc_attr( $global_styles_color['text'] );
321 }
322 }
323
324 echo <<<HTML
325 <div
326 class="wp-lightbox-overlay zoom"
327 data-wp-interactive="core/image"
328 data-wp-router-region='{ "id": "core/image-overlay", "attachTo": "body" }'
329 data-wp-key="wp-lightbox-overlay"
330 data-wp-context='{}'
331 data-wp-bind--role="state.roleAttribute"
332 data-wp-bind--aria-label="state.currentImage.ariaLabel"
333 data-wp-bind--aria-modal="state.ariaModal"
334 data-wp-class--active="state.overlayEnabled"
335 data-wp-class--show-closing-animation="state.overlayOpened"
336 data-wp-watch="callbacks.setOverlayFocus"
337 data-wp-on--keydown="actions.handleKeydown"
338 data-wp-on--touchstart="actions.handleTouchStart"
339 data-wp-on--touchmove="actions.handleTouchMove"
340 data-wp-on--touchend="actions.handleTouchEnd"
341 data-wp-on--click="actions.hideLightbox"
342 data-wp-on-window--resize="callbacks.setOverlayStyles"
343 data-wp-on-window--scroll="actions.handleScroll"
344 data-wp-bind--style="state.overlayStyles"
345 tabindex="-1"
346 >
347 <button type="button" aria-label="$close_button_label" style="fill: $close_button_color" class="close-button">
348 <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>
349 </button>
350 <div class="lightbox-image-container">
351 <figure data-wp-bind--class="state.currentImage.figureClassNames" data-wp-bind--style="state.figureStyles">
352 <img data-wp-bind--alt="state.currentImage.alt" data-wp-bind--class="state.currentImage.imgClassNames" data-wp-bind--style="state.imgStyles" data-wp-bind--src="state.currentImage.currentSrc">
353 </figure>
354 </div>
355 <div class="lightbox-image-container">
356 <figure data-wp-bind--class="state.currentImage.figureClassNames" data-wp-bind--style="state.figureStyles">
357 <img
358 data-wp-bind--alt="state.currentImage.alt"
359 data-wp-bind--class="state.currentImage.imgClassNames"
360 data-wp-bind--style="state.imgStyles"
361 data-wp-bind--src="state.enlargedSrc"
362 data-wp-bind--srcset="state.enlargedSrcset"
363 sizes="100vw"
364 >
365 </figure>
366 </div>
367 <div class="scrim" style="background-color: $background_color" aria-hidden="true"></div>
368 </div>
369 HTML;
370 }
371
372 /**
373 * Registers the `core/image` block on server.
374 *
375 * @since 5.9.0
376 */
377 function gutenberg_register_block_core_image() {
378 register_block_type_from_metadata(
379 __DIR__ . '/image',
380 array(
381 'render_callback' => 'gutenberg_render_block_core_image',
382 )
383 );
384 }
385 add_action( 'init', 'gutenberg_register_block_core_image', 20 );
386