7 |
7 |
8 /** |
8 /** |
9 * Renders the `core/image` block on the server, |
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. |
10 * adding a data-id attribute to the element if core/gallery has added on pre-render. |
11 * |
11 * |
12 * @param array $attributes The block attributes. |
12 * @since 5.9.0 |
13 * @param string $content The block content. |
13 * |
14 * @return string Returns the block content with the data-id attribute added. |
14 * @param array $attributes The block attributes. |
15 */ |
15 * @param string $content The block content. |
16 function render_block_core_image( $attributes, $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 render_block_core_image( $attributes, $content, $block ) { |
|
21 if ( false === stripos( $content, '<img' ) ) { |
|
22 return ''; |
|
23 } |
|
24 |
|
25 $p = new WP_HTML_Tag_Processor( $content ); |
|
26 |
|
27 if ( ! $p->next_tag( 'img' ) || null === $p->get_attribute( 'src' ) ) { |
|
28 return ''; |
|
29 } |
|
30 |
|
31 $has_id_binding = isset( $attributes['metadata']['bindings']['id'] ) && isset( $attributes['id'] ); |
|
32 |
|
33 // Ensure the `wp-image-id` classname on the image block supports block bindings. |
|
34 if ( $has_id_binding ) { |
|
35 // If there's a mismatch with the 'wp-image-' class and the actual id, the id was |
|
36 // probably overridden by block bindings. Update it to the correct value. |
|
37 // See https://github.com/WordPress/gutenberg/issues/62886 for why this is needed. |
|
38 $id = $attributes['id']; |
|
39 $image_classnames = $p->get_attribute( 'class' ); |
|
40 $class_with_binding_value = "wp-image-$id"; |
|
41 if ( is_string( $image_classnames ) && ! str_contains( $image_classnames, $class_with_binding_value ) ) { |
|
42 $image_classnames = preg_replace( '/wp-image-(\d+)/', $class_with_binding_value, $image_classnames ); |
|
43 $p->set_attribute( 'class', $image_classnames ); |
|
44 } |
|
45 } |
|
46 |
|
47 // For backwards compatibility, the data-id html attribute is only set for |
|
48 // image blocks nested in a gallery. Detect if the image is in a gallery by |
|
49 // checking the data-id attribute. |
|
50 // See the `block_core_gallery_data_id_backcompatibility` function. |
17 if ( isset( $attributes['data-id'] ) ) { |
51 if ( isset( $attributes['data-id'] ) ) { |
18 // Add the data-id="$id" attribute to the img element |
52 // If there's a binding for the `id`, the `id` attribute is used for the |
19 // to provide backwards compatibility for the Gallery Block, |
53 // value, since `data-id` does not support block bindings. |
20 // which now wraps Image Blocks within innerBlocks. |
54 // Else the `data-id` is used for backwards compatibility, since |
21 // The data-id attribute is added in a core/gallery `render_block_data` hook. |
55 // third parties may be filtering its value. |
22 $data_id_attribute = 'data-id="' . esc_attr( $attributes['data-id'] ) . '"'; |
56 $data_id = $has_id_binding ? $attributes['id'] : $attributes['data-id']; |
23 if ( false === strpos( $content, $data_id_attribute ) ) { |
57 $p->set_attribute( 'data-id', $data_id ); |
24 $content = str_replace( '<img', '<img ' . $data_id_attribute . ' ', $content ); |
58 } |
25 } |
59 |
26 } |
60 $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; |
27 return $content; |
61 $lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block ); |
28 } |
62 |
29 |
63 /* |
|
64 * If the lightbox is enabled and the image is not linked, adds the filter and |
|
65 * the JavaScript view file. |
|
66 */ |
|
67 if ( |
|
68 isset( $lightbox_settings ) && |
|
69 'none' === $link_destination && |
|
70 isset( $lightbox_settings['enabled'] ) && |
|
71 true === $lightbox_settings['enabled'] |
|
72 ) { |
|
73 $suffix = wp_scripts_get_suffix(); |
|
74 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { |
|
75 $module_url = gutenberg_url( '/build/interactivity/image.min.js' ); |
|
76 } |
|
77 |
|
78 wp_register_script_module( |
|
79 '@wordpress/block-library/image', |
|
80 isset( $module_url ) ? $module_url : includes_url( "blocks/image/view{$suffix}.js" ), |
|
81 array( '@wordpress/interactivity' ), |
|
82 defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) |
|
83 ); |
|
84 |
|
85 wp_enqueue_script_module( '@wordpress/block-library/image' ); |
|
86 |
|
87 /* |
|
88 * This render needs to happen in a filter with priority 15 to ensure that |
|
89 * it runs after the duotone filter and that duotone styles are applied to |
|
90 * the image in the lightbox. Lightbox has to work with any plugins that |
|
91 * might use filters as well. Removing this can be considered in the future |
|
92 * if the way the blocks are rendered changes, or if a new kind of filter is |
|
93 * introduced. |
|
94 */ |
|
95 add_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15, 2 ); |
|
96 } else { |
|
97 /* |
|
98 * Remove the filter if previously added by other Image blocks. |
|
99 */ |
|
100 remove_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15 ); |
|
101 } |
|
102 |
|
103 return $p->get_updated_html(); |
|
104 } |
|
105 |
|
106 /** |
|
107 * Adds the lightboxEnabled flag to the block data. |
|
108 * |
|
109 * This is used to determine whether the lightbox should be rendered or not. |
|
110 * |
|
111 * @since 6.4.0 |
|
112 * |
|
113 * @param array $block Block data. |
|
114 * |
|
115 * @return array Filtered block data. |
|
116 */ |
|
117 function block_core_image_get_lightbox_settings( $block ) { |
|
118 // Gets the lightbox setting from the block attributes. |
|
119 if ( isset( $block['attrs']['lightbox'] ) ) { |
|
120 $lightbox_settings = $block['attrs']['lightbox']; |
|
121 } |
|
122 |
|
123 if ( ! isset( $lightbox_settings ) ) { |
|
124 $lightbox_settings = wp_get_global_settings( array( 'lightbox' ), array( 'block_name' => 'core/image' ) ); |
|
125 |
|
126 // If not present in global settings, check the top-level global settings. |
|
127 // |
|
128 // NOTE: If no block-level settings are found, the previous call to |
|
129 // `wp_get_global_settings` will return the whole `theme.json` structure in |
|
130 // which case we can check if the "lightbox" key is present at the top-level |
|
131 // of the global settings and use its value. |
|
132 if ( isset( $lightbox_settings['lightbox'] ) ) { |
|
133 $lightbox_settings = wp_get_global_settings( array( 'lightbox' ) ); |
|
134 } |
|
135 } |
|
136 |
|
137 return $lightbox_settings ?? null; |
|
138 } |
|
139 |
|
140 /** |
|
141 * Adds the directives and layout needed for the lightbox behavior. |
|
142 * |
|
143 * @since 6.4.0 |
|
144 * |
|
145 * @param string $block_content Rendered block content. |
|
146 * @param array $block Block object. |
|
147 * |
|
148 * @return string Filtered block content. |
|
149 */ |
|
150 function block_core_image_render_lightbox( $block_content, $block ) { |
|
151 /* |
|
152 * If there's no IMG tag in the block then return the given block content |
|
153 * as-is. There's nothing that this code can knowingly modify to add the |
|
154 * lightbox behavior. |
|
155 */ |
|
156 $p = new WP_HTML_Tag_Processor( $block_content ); |
|
157 if ( $p->next_tag( 'figure' ) ) { |
|
158 $p->set_bookmark( 'figure' ); |
|
159 } |
|
160 if ( ! $p->next_tag( 'img' ) ) { |
|
161 return $block_content; |
|
162 } |
|
163 |
|
164 $alt = $p->get_attribute( 'alt' ); |
|
165 $img_uploaded_src = $p->get_attribute( 'src' ); |
|
166 $img_class_names = $p->get_attribute( 'class' ); |
|
167 $img_styles = $p->get_attribute( 'style' ); |
|
168 $img_width = 'none'; |
|
169 $img_height = 'none'; |
|
170 $aria_label = __( 'Enlarge image' ); |
|
171 |
|
172 if ( $alt ) { |
|
173 /* translators: %s: Image alt text. */ |
|
174 $aria_label = sprintf( __( 'Enlarge image: %s' ), $alt ); |
|
175 } |
|
176 |
|
177 if ( isset( $block['attrs']['id'] ) ) { |
|
178 $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] ); |
|
179 $img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] ); |
|
180 $img_width = $img_metadata['width'] ?? 'none'; |
|
181 $img_height = $img_metadata['height'] ?? 'none'; |
|
182 } |
|
183 |
|
184 // Figure. |
|
185 $p->seek( 'figure' ); |
|
186 $figure_class_names = $p->get_attribute( 'class' ); |
|
187 $figure_styles = $p->get_attribute( 'style' ); |
|
188 $p->add_class( 'wp-lightbox-container' ); |
|
189 $p->set_attribute( 'data-wp-interactive', 'core/image' ); |
|
190 $p->set_attribute( |
|
191 'data-wp-context', |
|
192 wp_json_encode( |
|
193 array( |
|
194 'uploadedSrc' => $img_uploaded_src, |
|
195 'figureClassNames' => $figure_class_names, |
|
196 'figureStyles' => $figure_styles, |
|
197 'imgClassNames' => $img_class_names, |
|
198 'imgStyles' => $img_styles, |
|
199 'targetWidth' => $img_width, |
|
200 'targetHeight' => $img_height, |
|
201 'scaleAttr' => $block['attrs']['scale'] ?? false, |
|
202 'ariaLabel' => $aria_label, |
|
203 'alt' => $alt, |
|
204 ), |
|
205 JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP |
|
206 ) |
|
207 ); |
|
208 |
|
209 // Image. |
|
210 $p->next_tag( 'img' ); |
|
211 $p->set_attribute( 'data-wp-init', 'callbacks.setButtonStyles' ); |
|
212 $p->set_attribute( 'data-wp-on-async--load', 'callbacks.setButtonStyles' ); |
|
213 $p->set_attribute( 'data-wp-on-async-window--resize', 'callbacks.setButtonStyles' ); |
|
214 // Sets an event callback on the `img` because the `figure` element can also |
|
215 // contain a caption, and we don't want to trigger the lightbox when the |
|
216 // caption is clicked. |
|
217 $p->set_attribute( 'data-wp-on-async--click', 'actions.showLightbox' ); |
|
218 |
|
219 $body_content = $p->get_updated_html(); |
|
220 |
|
221 // Adds a button alongside image in the body content. |
|
222 $img = null; |
|
223 preg_match( '/<img[^>]+>/', $body_content, $img ); |
|
224 |
|
225 $button = |
|
226 $img[0] |
|
227 . '<button |
|
228 class="lightbox-trigger" |
|
229 type="button" |
|
230 aria-haspopup="dialog" |
|
231 aria-label="' . esc_attr( $aria_label ) . '" |
|
232 data-wp-init="callbacks.initTriggerButton" |
|
233 data-wp-on-async--click="actions.showLightbox" |
|
234 data-wp-style--right="context.imageButtonRight" |
|
235 data-wp-style--top="context.imageButtonTop" |
|
236 > |
|
237 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12"> |
|
238 <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" /> |
|
239 </svg> |
|
240 </button>'; |
|
241 |
|
242 $body_content = preg_replace( '/<img[^>]+>/', $button, $body_content ); |
|
243 |
|
244 add_action( 'wp_footer', 'block_core_image_print_lightbox_overlay' ); |
|
245 |
|
246 return $body_content; |
|
247 } |
|
248 |
|
249 /** |
|
250 * @since 6.5.0 |
|
251 */ |
|
252 function block_core_image_print_lightbox_overlay() { |
|
253 $close_button_label = esc_attr__( 'Close' ); |
|
254 |
|
255 // If the current theme does NOT have a `theme.json`, or the colors are not |
|
256 // defined, it needs to set the background color & close button color to some |
|
257 // default values because it can't get them from the Global Styles. |
|
258 $background_color = '#fff'; |
|
259 $close_button_color = '#000'; |
|
260 if ( wp_theme_has_theme_json() ) { |
|
261 $global_styles_color = wp_get_global_styles( array( 'color' ) ); |
|
262 if ( ! empty( $global_styles_color['background'] ) ) { |
|
263 $background_color = esc_attr( $global_styles_color['background'] ); |
|
264 } |
|
265 if ( ! empty( $global_styles_color['text'] ) ) { |
|
266 $close_button_color = esc_attr( $global_styles_color['text'] ); |
|
267 } |
|
268 } |
|
269 |
|
270 echo <<<HTML |
|
271 <div |
|
272 class="wp-lightbox-overlay zoom" |
|
273 data-wp-interactive="core/image" |
|
274 data-wp-context='{}' |
|
275 data-wp-bind--role="state.roleAttribute" |
|
276 data-wp-bind--aria-label="state.currentImage.ariaLabel" |
|
277 data-wp-bind--aria-modal="state.ariaModal" |
|
278 data-wp-class--active="state.overlayEnabled" |
|
279 data-wp-class--show-closing-animation="state.showClosingAnimation" |
|
280 data-wp-watch="callbacks.setOverlayFocus" |
|
281 data-wp-on--keydown="actions.handleKeydown" |
|
282 data-wp-on-async--touchstart="actions.handleTouchStart" |
|
283 data-wp-on--touchmove="actions.handleTouchMove" |
|
284 data-wp-on-async--touchend="actions.handleTouchEnd" |
|
285 data-wp-on-async--click="actions.hideLightbox" |
|
286 data-wp-on-async-window--resize="callbacks.setOverlayStyles" |
|
287 data-wp-on-async-window--scroll="actions.handleScroll" |
|
288 tabindex="-1" |
|
289 > |
|
290 <button type="button" aria-label="$close_button_label" style="fill: $close_button_color" class="close-button"> |
|
291 <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 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg> |
|
292 </button> |
|
293 <div class="lightbox-image-container"> |
|
294 <figure data-wp-bind--class="state.currentImage.figureClassNames" data-wp-bind--style="state.currentImage.figureStyles"> |
|
295 <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"> |
|
296 </figure> |
|
297 </div> |
|
298 <div class="lightbox-image-container"> |
|
299 <figure data-wp-bind--class="state.currentImage.figureClassNames" data-wp-bind--style="state.currentImage.figureStyles"> |
|
300 <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.enlargedSrc"> |
|
301 </figure> |
|
302 </div> |
|
303 <div class="scrim" style="background-color: $background_color" aria-hidden="true"></div> |
|
304 <style data-wp-text="state.overlayStyles"></style> |
|
305 </div> |
|
306 HTML; |
|
307 } |
30 |
308 |
31 /** |
309 /** |
32 * Registers the `core/image` block on server. |
310 * Registers the `core/image` block on server. |
|
311 * |
|
312 * @since 5.9.0 |
33 */ |
313 */ |
34 function register_block_core_image() { |
314 function register_block_core_image() { |
35 register_block_type_from_metadata( |
315 register_block_type_from_metadata( |
36 __DIR__ . '/image', |
316 __DIR__ . '/image', |
37 array( |
317 array( |