PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 15.0.3
Jetpack – WP Security, Backup, Speed, & Growth v15.0.3
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / extensions / blocks / tiled-gallery / tiled-gallery.php
jetpack / extensions / blocks / tiled-gallery Last commit date
tiled-gallery.php 11 months ago
tiled-gallery.php
940 lines
1 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Tiled Gallery block.
4 * Relies on Photon, but can be used even when the module is not active.
5 *
6 * @since 6.9.0
7 *
8 * @package automattic/jetpack
9 */
10
11 namespace Automattic\Jetpack\Extensions;
12
13 use Automattic\Jetpack\Blocks;
14 use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
15 use Automattic\Jetpack\Status;
16 use Automattic\Jetpack\Status\Host;
17 use Jetpack;
18 use Jetpack_Gutenberg;
19
20 /**
21 * Jetpack Tiled Gallery Block class
22 *
23 * @since 7.3
24 */
25 class Tiled_Gallery {
26 /* Values for building srcsets */
27 const IMG_SRCSET_WIDTH_MAX = 2000;
28 const IMG_SRCSET_WIDTH_MIN = 600;
29 const IMG_SRCSET_WIDTH_STEP = 300;
30
31 /**
32 * Register the block
33 */
34 public static function register() {
35 if (
36 ( defined( 'IS_WPCOM' ) && IS_WPCOM )
37 || Jetpack::is_connection_ready()
38 || ( new Status() )->is_offline_mode()
39 ) {
40 Blocks::jetpack_register_block(
41 __DIR__,
42 array(
43 'render_callback' => array( __CLASS__, 'render' ),
44 'render_email_callback' => array( __CLASS__, 'render_email' ),
45 )
46 );
47 }
48 }
49
50 /**
51 * Tiled gallery block registration
52 *
53 * @param array $attr Array containing the block attributes.
54 * @param string $content String containing the block content.
55 *
56 * @return string
57 */
58 public static function render( $attr, $content ) {
59 Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
60
61 $is_squareish_layout = self::is_squareish_layout( $attr );
62 // For backward compatibility (ensuring Tiled Galleries using now deprecated versions of the block are not affected).
63 // See isVIP() in utils/index.js.
64 $jetpack_plan = Jetpack_Plan::get();
65 wp_localize_script( 'jetpack-gallery-settings', 'jetpack_plan', array( 'data' => $jetpack_plan['product_slug'] ) );
66
67 if ( preg_match_all( '/<img [^>]+>/', $content, $images ) ) {
68 /**
69 * This block processes all of the images that are found and builds $find and $replace.
70 *
71 * The original img is added to the $find array and the replacement is made and added
72 * to the $replace array. This is so that the same find and replace operations can be
73 * made on the entire $content.
74 */
75 $find = array();
76 $replace = array();
77 $image_index = 0;
78 $number_images = count( $images[0] );
79
80 foreach ( $images[0] as $image_html ) {
81 if (
82 preg_match( '/data-width="([0-9]+)"/', $image_html, $img_width )
83 && preg_match( '/data-height="([0-9]+)"/', $image_html, $img_height )
84 && preg_match( '/src="([^"]+)"/', $image_html, $img_src )
85 ) {
86 ++$image_index;
87 // Drop img src query string so it can be used as a base to add photon params
88 // for the srcset.
89 $src_parts = explode( '?', $img_src[1], 2 );
90 $orig_src = $src_parts[0];
91 $orig_height = absint( $img_height[1] );
92 $orig_width = absint( $img_width[1] );
93
94 // Because URLs are already "photon", the photon function used short-circuits
95 // before ssl is added. Detect ssl and add is if necessary.
96 $is_ssl = ! empty( $src_parts[1] ) && str_contains( $src_parts[1], 'ssl=1' );
97
98 if ( ! $orig_width || ! $orig_height || ! $orig_src ) {
99 continue;
100 }
101
102 $srcset_parts = array();
103 if ( $is_squareish_layout ) {
104 $min_width = min( self::IMG_SRCSET_WIDTH_MIN, $orig_width, $orig_height );
105 $max_width = min( self::IMG_SRCSET_WIDTH_MAX, $orig_width, $orig_height );
106
107 for ( $w = $min_width; $w <= $max_width; $w = min( $max_width, $w + self::IMG_SRCSET_WIDTH_STEP ) ) {
108 $srcset_src = add_query_arg(
109 array(
110 'resize' => $w . ',' . $w,
111 'strip' => 'info',
112 ),
113 $orig_src
114 );
115 if ( $is_ssl ) {
116 $srcset_src = add_query_arg( 'ssl', '1', $srcset_src );
117 }
118 $srcset_parts[] = esc_url( $srcset_src ) . ' ' . $w . 'w';
119 if ( $w >= $max_width ) {
120 break;
121 }
122 }
123 } else {
124 $min_width = min( self::IMG_SRCSET_WIDTH_MIN, $orig_width );
125 $max_width = min( self::IMG_SRCSET_WIDTH_MAX, $orig_width );
126
127 for ( $w = $min_width; $w <= $max_width; $w = min( $max_width, $w + self::IMG_SRCSET_WIDTH_STEP ) ) {
128 $srcset_src = add_query_arg(
129 array(
130 'strip' => 'info',
131 'w' => $w,
132 ),
133 $orig_src
134 );
135 if ( $is_ssl ) {
136 $srcset_src = add_query_arg( 'ssl', '1', $srcset_src );
137 }
138 $srcset_parts[] = esc_url( $srcset_src ) . ' ' . $w . 'w';
139 if ( $w >= $max_width ) {
140 break;
141 }
142 }
143 }
144
145 $img_element = self::interactive_markup( $image_index, $number_images );
146
147 if ( ! empty( $srcset_parts ) ) {
148 $srcset = 'srcset="' . esc_attr( implode( ',', $srcset_parts ) ) . '"';
149
150 $find[] = $image_html;
151 $replace[] = str_replace( '<img', $img_element . $srcset, $image_html );
152 }
153 }
154 }
155
156 if ( ! empty( $find ) ) {
157 $content = str_replace( $find, $replace, $content );
158 }
159 }
160
161 /**
162 * Filter the output of the Tiled Galleries content.
163 *
164 * @module tiled-gallery
165 *
166 * @since 6.9.0
167 *
168 * @param string $content Tiled Gallery block content.
169 */
170 return apply_filters( 'jetpack_tiled_galleries_block_content', $content );
171 }
172
173 /**
174 * Adds tabindex, role and aria-label markup for images that should be interactive (front-end only).
175 *
176 * @param integer $image_index Integer The current image index.
177 * @param integer $number_images Integer The total number of images.
178 */
179 private static function interactive_markup( $image_index, $number_images ) {
180
181 $host = new Host();
182 $is_module_active = $host->is_wpcom_simple()
183 ? get_option( 'carousel_enable_it' )
184 : Jetpack::is_module_active( 'carousel' );
185
186 if ( $is_module_active ) {
187 $aria_label_content = sprintf(
188 /* Translators: %1$d is the current image index, %2$d is the total number of images. */
189 __( 'Open image %1$d of %2$d in full-screen', 'jetpack' ),
190 $image_index,
191 $number_images
192 );
193 $img_element = '<img role="button" tabindex="0" aria-label="' . esc_attr( $aria_label_content ) . '"';
194 } else {
195 $img_element = '<img ';
196 }
197 return $img_element;
198 }
199
200 /**
201 * Determines whether a Tiled Gallery block uses square or circle images (1:1 ratio)
202 *
203 * Layouts are block styles and will be available as `is-style-[LAYOUT]` in the className
204 * attribute. The default (rectangular) will be omitted.
205 *
206 * @param array $attr Attributes key/value array.
207 * @return boolean True if layout is squareish, otherwise false.
208 */
209 private static function is_squareish_layout( $attr ) {
210 return isset( $attr['className'] )
211 && (
212 'is-style-square' === $attr['className']
213 || 'is-style-circle' === $attr['className']
214 );
215 }
216
217 /**
218 * Render tiled gallery block for email.
219 *
220 * @since 15.0
221 *
222 * @param string $block_content The original block HTML content.
223 * @param array $parsed_block The parsed block data including attributes.
224 * @param object $rendering_context Email rendering context.
225 *
226 * @return string
227 */
228 public static function render_email( $block_content, array $parsed_block, $rendering_context ) {
229 // Validate input parameters and required dependencies
230 if ( ! isset( $parsed_block['attrs'] ) || ! is_array( $parsed_block['attrs'] ) ||
231 ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper' ) ||
232 ! class_exists( '\Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper' ) ) {
233 return '';
234 }
235
236 // Get spacing from email_attrs for better consistency with core blocks
237 $email_attrs = $parsed_block['email_attrs'] ?? array();
238 $table_margin_style = '';
239
240 if ( ! empty( $email_attrs ) && class_exists( '\WP_Style_Engine' ) ) {
241 // Get margin for table styling
242 $table_margin_style = \WP_Style_Engine::compile_css( array_intersect_key( $email_attrs, array_flip( array( 'margin' ) ) ), '' ) ?? '';
243 }
244
245 // Email cell padding
246 $email_cell_padding = 2; // Cell padding
247
248 $attr = $parsed_block['attrs'];
249
250 // Determine layout style and columns from attributes (needed for both image processing and layout building)
251 $layout_info = self::get_layout_style_from_attributes( $attr );
252
253 // Process images for email rendering
254 $images = self::process_tiled_gallery_images_for_email( $attr, $layout_info );
255
256 if ( empty( $images ) ) {
257 return '';
258 }
259
260 // Determine target width from the email layout if available
261 $target_width = self::get_email_target_width( $rendering_context );
262
263 // Build layout content based on style and columns
264 $grid_content = self::build_email_layout_content( $images, $layout_info, $email_cell_padding, $attr );
265
266 // Use Table_Wrapper_Helper for consistent email rendering
267 $table_style = sprintf( 'width: 100%%; max-width: %dpx; padding: 0; border-collapse: collapse;', $target_width );
268 if ( ! empty( $table_margin_style ) ) {
269 $table_style = $table_margin_style . '; ' . $table_style;
270 } else {
271 $table_style = 'margin: 16px 0; ' . $table_style;
272 }
273
274 $image_table_attrs = array(
275 'style' => $table_style,
276 );
277
278 $html = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper( $grid_content, $image_table_attrs );
279
280 return $html;
281 }
282
283 /**
284 * Get target width for email rendering.
285 *
286 * @param object $rendering_context Email rendering context.
287 * @return int Target width in pixels.
288 */
289 private static function get_email_target_width( $rendering_context ) {
290 $target_width = 600; // Default
291
292 if ( ! empty( $rendering_context ) && is_object( $rendering_context ) && method_exists( $rendering_context, 'get_layout_width_without_padding' ) ) {
293 $layout_width_px = $rendering_context->get_layout_width_without_padding();
294 if ( is_string( $layout_width_px ) ) {
295 $parsed_width = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper::parse_value( $layout_width_px );
296 if ( $parsed_width > 0 ) {
297 $target_width = $parsed_width;
298 }
299 }
300 }
301
302 return $target_width;
303 }
304
305 /**
306 * Process tiled gallery images for email rendering.
307 *
308 * @param array $attr Block attributes containing image data.
309 * @param array $layout_info Layout information from get_layout_style_from_attributes.
310 * @return array Processed image data for email rendering.
311 */
312 private static function process_tiled_gallery_images_for_email( $attr, $layout_info ) {
313 $images = array();
314
315 // Determine if this is a squareish layout
316 $is_squareish = in_array( $layout_info['style'], array( 'square', 'circle' ), true );
317
318 // Get images from IDs (primary data source)
319 if ( ! empty( $attr['ids'] ) && is_array( $attr['ids'] ) ) {
320 foreach ( $attr['ids'] as $id ) {
321 // Validate ID is a positive integer and attachment exists
322 $id = absint( $id );
323 if ( ! $id || ! wp_attachment_is_image( $id ) ) {
324 continue;
325 }
326
327 // For square/circle layouts, get a high-quality square crop
328 if ( $is_squareish ) {
329 // Start with full size image for better quality when resizing
330 $image_url = wp_get_attachment_image_url( $id, 'full' );
331
332 // If we have Photon/Jetpack image processing, request high-quality square crop
333 if ( function_exists( 'jetpack_photon_url' ) && $image_url ) {
334 $image_url = add_query_arg(
335 array(
336 'resize' => '1300,1300', // High-quality square crop for email
337 'crop' => '1',
338 ),
339 $image_url
340 );
341 }
342 } else {
343 $image_url = wp_get_attachment_image_url( $id, 'large' );
344 }
345
346 // Sanitize alt text from post meta
347 $alt_text = get_post_meta( $id, '_wp_attachment_image_alt', true );
348 $alt_text = sanitize_text_field( $alt_text );
349
350 if ( $image_url ) {
351 $images[] = array(
352 'url' => $image_url,
353 'alt' => $alt_text,
354 'id' => $id,
355 );
356 }
357 }
358 } elseif ( ! empty( $attr['images'] ) && is_array( $attr['images'] ) ) {
359 // Fall back to images array if IDs aren't available
360 foreach ( $attr['images'] as $image_data ) {
361 if ( ! empty( $image_data['url'] ) ) {
362 // Validate and sanitize URL
363 $url = esc_url_raw( $image_data['url'] );
364 if ( ! $url || ! wp_http_validate_url( $url ) ) {
365 continue;
366 }
367
368 // Sanitize alt text
369 $alt_text = ! empty( $image_data['alt'] ) ? sanitize_text_field( $image_data['alt'] ) : '';
370
371 // Validate ID if present
372 $id = ! empty( $image_data['id'] ) ? absint( $image_data['id'] ) : 0;
373
374 $images[] = array(
375 'url' => $url,
376 'alt' => $alt_text,
377 'id' => $id,
378 );
379 }
380 }
381 }
382
383 return $images;
384 }
385
386 /**
387 * Get layout style and columns from block attributes.
388 *
389 * @param array $attr Block attributes.
390 * @return array Array with 'style', 'columns', and 'border_radius' keys.
391 */
392 private static function get_layout_style_from_attributes( $attr ) {
393 $layout_info = array(
394 'style' => 'rectangular', // Default to rectangular/mosaic layout
395 'columns' => 3, // Default to 3 columns
396 'border_radius' => 0, // Default to no border radius
397 );
398
399 // Get number of columns from attributes with validation
400 if ( ! empty( $attr['columns'] ) && is_numeric( $attr['columns'] ) ) {
401 $columns = absint( $attr['columns'] );
402 // Clamp columns between 1 and 6 for reasonable layouts
403 $layout_info['columns'] = max( 1, min( 6, $columns ) );
404 }
405
406 // Get border radius from roundedCorners attribute (preferred method)
407 if ( ! empty( $attr['roundedCorners'] ) && is_numeric( $attr['roundedCorners'] ) ) {
408 $border_radius_value = absint( $attr['roundedCorners'] );
409 // Clamp value between 0 and 20
410 $layout_info['border_radius'] = max( 0, min( 20, $border_radius_value ) );
411 }
412
413 // Get layout style and border radius from className
414 if ( ! empty( $attr['className'] ) ) {
415 if ( str_contains( $attr['className'], 'is-style-square' ) ) {
416 $layout_info['style'] = 'square';
417 } elseif ( str_contains( $attr['className'], 'is-style-circle' ) ) {
418 $layout_info['style'] = 'circle';
419 } elseif ( str_contains( $attr['className'], 'is-style-columns' ) ) {
420 $layout_info['style'] = 'columns';
421 }
422
423 // Extract border radius from has-rounded-corners-{value} class (fallback method)
424 if ( $layout_info['border_radius'] === 0 && preg_match( '/has-rounded-corners-(\d+)/', $attr['className'], $matches ) ) {
425 $border_radius_value = absint( $matches[1] );
426 // Clamp value between 0 and 20
427 $layout_info['border_radius'] = max( 0, min( 20, $border_radius_value ) );
428 }
429 }
430
431 return $layout_info;
432 }
433
434 /**
435 * Build email layout content based on layout style and columns.
436 *
437 * @param array $images Array of image data.
438 * @param array $layout_info Array with 'style' and 'columns' keys.
439 * @param int $cell_padding Cell padding.
440 * @param array $attr Block attributes.
441 * @return string HTML content.
442 */
443 private static function build_email_layout_content( $images, $layout_info, $cell_padding, $attr ) {
444 $layout_style = $layout_info['style'];
445 $columns = $layout_info['columns'];
446 $border_radius = $layout_info['border_radius'];
447
448 switch ( $layout_style ) {
449 case 'square':
450 return self::build_square_layout_content( $images, $cell_padding, $columns, 'square', $border_radius, $attr );
451 case 'circle':
452 return self::build_square_layout_content( $images, $cell_padding, $columns, 'circle', $border_radius, $attr );
453 case 'columns':
454 return self::build_columns_layout_content( $images, $cell_padding, $columns, $border_radius, $attr );
455 case 'rectangular':
456 default:
457 return self::build_mosaic_layout_content( $images, $cell_padding, $border_radius, $attr );
458 }
459 }
460
461 /**
462 * Build square/circle layout content.
463 *
464 * @param array $images Array of image data.
465 * @param int $cell_padding Cell padding.
466 * @param int $columns Number of columns for the layout.
467 * @param string $style Layout style (square or circle).
468 * @param int $border_radius Border radius value (0-20).
469 * @param array $attr Block attributes.
470 * @return string HTML content.
471 */
472 private static function build_square_layout_content( $images, $cell_padding, $columns, $style = 'square', $border_radius = 0, $attr = array() ) {
473 $content_parts = array();
474
475 // Create rows of images with hierarchical chunks for square/circle layouts
476 $image_chunks = self::create_hierarchical_chunks( $images, $columns );
477
478 $border_radius_style = self::generate_border_radius_style( $style, $border_radius );
479
480 foreach ( $image_chunks as $row_images ) {
481 $images_in_row = count( $row_images );
482 $cell_width_percent = ( 100 / $images_in_row );
483
484 // Build table cells for this row
485 $row_cells = '';
486 foreach ( $row_images as $image ) {
487 // Calculate cell attributes with consistent padding
488 $cell_attrs = array(
489 'style' => sprintf(
490 'width: %s%%; padding: %dpx; vertical-align: top; text-align: center;',
491 $cell_width_percent,
492 $cell_padding
493 ),
494 );
495
496 $image_styles = self::generate_image_styles( false );
497
498 $cell_content = self::generate_image_html( $image, $image_styles, $border_radius_style, $attr );
499
500 $row_cells .= \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_cell(
501 $cell_content,
502 $cell_attrs
503 );
504 }
505
506 // Use Table_Wrapper_Helper for email-compatible table rendering
507 $table_attrs = array(
508 'style' => 'width: 100%; border-collapse: collapse;',
509 );
510
511 $content_parts[] = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper(
512 $row_cells,
513 $table_attrs
514 );
515 }
516
517 // Use Table_Wrapper_Helper for consistent email rendering
518 $wrapper_attrs = array(
519 'style' => 'width: 100%; border-collapse: collapse;',
520 );
521
522 return \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper(
523 implode( '', $content_parts ),
524 $wrapper_attrs
525 );
526 }
527
528 /**
529 * Build columns layout content using mosaic logic organized into columns.
530 *
531 * @param array $images Array of image data.
532 * @param int $cell_padding Cell padding.
533 * @param int $columns Number of columns for the layout.
534 * @param int $border_radius Border radius value (0-20).
535 * @param array $attr Block attributes.
536 * @return string HTML content.
537 */
538 private static function build_columns_layout_content( $images, $cell_padding, $columns, $border_radius = 0, $attr = array() ) {
539 $content_parts = array();
540 $border_radius_style = self::generate_border_radius_style( '', $border_radius );
541
542 // Distribute images across columns using round-robin approach for better balance
543 $column_arrays = array_fill( 0, $columns, array() );
544 foreach ( $images as $index => $image ) {
545 $column_index = $index % $columns;
546 $column_arrays[ $column_index ][] = $image;
547 }
548
549 // Build table cells for columns layout
550 $row_cells = '';
551 foreach ( $column_arrays as $column_images ) {
552 if ( empty( $column_images ) ) {
553 // Add empty cell for balance
554 $cell_attrs = array(
555 'style' => sprintf(
556 'width: %s%%; padding: %dpx; vertical-align: top;',
557 ( 100 / $columns ),
558 $cell_padding
559 ),
560 );
561 $row_cells .= \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_cell(
562 '',
563 $cell_attrs
564 );
565 continue;
566 }
567
568 // Calculate cell attributes
569 $cell_width_percent = ( 100 / $columns );
570 $cell_attrs = array(
571 'style' => sprintf(
572 'width: %s%%; padding: %dpx; vertical-align: top;',
573 $cell_width_percent,
574 $cell_padding
575 ),
576 );
577
578 // Generate mosaic-style groupings within this column
579 $column_rows = self::generate_column_mosaic_rows( $column_images );
580
581 $cell_content = '';
582 foreach ( $column_rows as $row_index => $row_images ) {
583 foreach ( $row_images as $image ) {
584 $image_styles = self::generate_image_styles( false );
585
586 // Add top margin to all images except the first one in the column
587 if ( $row_index > 0 || $cell_content !== '' ) {
588 $image_styles .= ' margin-top: ' . ( $cell_padding * 2 ) . 'px;';
589 }
590
591 $cell_content .= self::generate_image_html( $image, $image_styles, $border_radius_style, $attr );
592 }
593 }
594
595 $row_cells .= \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_cell(
596 $cell_content,
597 $cell_attrs
598 );
599 }
600
601 // Use Table_Wrapper_Helper for email-compatible table rendering
602 $table_attrs = array(
603 'style' => 'width: 100%; border-collapse: collapse;',
604 );
605
606 $content_parts[] = \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper(
607 $row_cells,
608 $table_attrs
609 );
610
611 // Use Table_Wrapper_Helper for consistent email rendering
612 $wrapper_attrs = array(
613 'style' => 'width: 100%; border-collapse: collapse;',
614 );
615
616 return \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper(
617 implode( '', $content_parts ),
618 $wrapper_attrs
619 );
620 }
621
622 /**
623 * Build mosaic layout content with flexible row/column structure.
624 *
625 * @param array $images Array of image data.
626 * @param int $cell_padding Cell padding.
627 * @param int $border_radius Border radius value (0-20).
628 * @param array $attr Block attributes.
629 * @return string HTML content.
630 */
631 private static function build_mosaic_layout_content( $images, $cell_padding, $border_radius = 0, $attr = array() ) {
632 $border_radius_style = self::generate_border_radius_style( '', $border_radius );
633
634 // Generate mosaic layout rows
635 $rows = self::generate_mosaic_rows( $images );
636
637 // Determine the maximum number of columns to ensure consistent layout
638 $max_columns = 0;
639 foreach ( $rows as $row ) {
640 $max_columns = max( $max_columns, count( $row ) );
641 }
642
643 // Build each row as a separate table to match flexbox behavior
644 $content_parts = array();
645 foreach ( $rows as $row ) {
646 $images_in_row = count( $row );
647
648 // Calculate width for each cell in this row (like flexbox)
649 $cell_width_percent = ( 100 / $images_in_row );
650
651 // Build table cells for this row
652 $row_cells = '';
653 foreach ( $row as $image ) {
654 $cell_style = sprintf(
655 'width: %s%%; padding: %dpx; vertical-align: top; text-align: center;',
656 $cell_width_percent,
657 $cell_padding
658 );
659
660 // Set consistent height for all images in this row to ensure alignment
661 // Use progressive enhancement: object-fit for supported clients, natural layout for others
662 $image_styles = self::generate_image_styles( true );
663
664 $cell_content = self::generate_image_html( $image, $image_styles, $border_radius_style, $attr );
665
666 $row_cells .= sprintf(
667 '<td style="%s">%s</td>',
668 esc_attr( $cell_style ),
669 $cell_content
670 );
671 }
672
673 // Create a separate table for each row with flexible height for alignment
674 $row_table = sprintf(
675 '<table role="presentation" style="width: 100%%; border-collapse: collapse; table-layout: fixed;"><tr>%s</tr></table>',
676 $row_cells
677 );
678
679 $content_parts[] = $row_table;
680 }
681
682 // Use Table_Wrapper_Helper for the main container
683 $table_attrs = array(
684 'style' => 'width: 100%; border-collapse: collapse;',
685 );
686
687 return \Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper::render_table_wrapper(
688 implode( '', $content_parts ),
689 $table_attrs
690 );
691 }
692
693 /**
694 * Generate mosaic layout rows based on image count.
695 *
696 * @param array $images Array of image data.
697 * @return array Array of rows, each containing images.
698 */
699 private static function generate_mosaic_rows( $images ) {
700 $rows = array();
701 $image_count = count( $images );
702
703 // More sophisticated mosaic algorithm based on image count
704 if ( $image_count <= 3 ) {
705 // For 3 or fewer images, use simple layout
706 $rows[] = $images;
707 } else {
708 // For more images, create varied row patterns
709 $patterns = array(
710 4 => array( 2, 2 ), // 4 images: 2 + 2
711 5 => array( 2, 3 ), // 5 images: 2 + 3
712 6 => array( 3, 3 ), // 6 images: 3 + 3
713 7 => array( 3, 2, 2 ), // 7 images: 3 + 2 + 2
714 8 => array( 3, 3, 2 ), // 8 images: 3 + 3 + 2
715 9 => array( 3, 3, 3 ), // 9 images: 3 + 3 + 3
716 );
717
718 if ( isset( $patterns[ $image_count ] ) ) {
719 // Use predefined pattern for 4-9 images
720 $pattern = $patterns[ $image_count ];
721 $image_index = 0;
722
723 foreach ( $pattern as $images_in_row ) {
724 $row = array();
725 for ( $i = 0; $i < $images_in_row; $i++ ) {
726 $row[] = $images[ $image_index ];
727 ++$image_index;
728 }
729 $rows[] = $row;
730 }
731 } else {
732 // For 10+ images, create rows of 3 with remainder handling
733 $full_rows = intval( $image_count / 3 );
734 $remainder = $image_count % 3;
735
736 $image_index = 0;
737
738 // Create full rows of 3
739 for ( $row = 0; $row < $full_rows; $row++ ) {
740 $rows[] = array(
741 $images[ $image_index ],
742 $images[ $image_index + 1 ],
743 $images[ $image_index + 2 ],
744 );
745 $image_index += 3;
746 }
747
748 // Handle remainder
749 if ( $remainder > 0 ) {
750 $remaining = array_slice( $images, $image_index );
751 $rows[] = $remaining;
752 }
753 }
754 }
755
756 return $rows;
757 }
758
759 /**
760 * Generate mosaic-style rows within a single column for columns layout.
761 *
762 * @param array $images Array of image data for this column.
763 * @return array Array of rows, each containing 1-2 images for variety.
764 */
765 private static function generate_column_mosaic_rows( $images ) {
766 $rows = array();
767 $image_count = count( $images );
768
769 if ( $image_count <= 2 ) {
770 // For 2 or fewer images, each gets its own row
771 foreach ( $images as $image ) {
772 $rows[] = array( $image );
773 }
774 } else {
775 // Create varied patterns: mix of single and paired images
776 $image_index = 0;
777
778 while ( $image_index < $image_count ) {
779 $remaining = $image_count - $image_index;
780
781 if ( $remaining === 1 ) {
782 // Last image - single row
783 $rows[] = array( $images[ $image_index ] );
784 ++$image_index;
785 } elseif ( $remaining === 3 ) {
786 // 3 remaining - do 1 + 2 for better balance
787 $rows[] = array( $images[ $image_index ] );
788 ++$image_index;
789 $rows[] = array( $images[ $image_index ], $images[ $image_index + 1 ] );
790 $image_index += 2;
791 } else {
792 // 2 or more remaining - alternate between single and pairs
793 $use_pair = ( count( $rows ) % 2 === 1 ); // Alternate pattern
794
795 if ( $use_pair && $remaining >= 2 ) {
796 // Create a pair
797 $rows[] = array( $images[ $image_index ], $images[ $image_index + 1 ] );
798 $image_index += 2;
799 } else {
800 // Single image
801 $rows[] = array( $images[ $image_index ] );
802 ++$image_index;
803 }
804 }
805 }
806 }
807
808 return $rows;
809 }
810
811 /**
812 * Generate border radius style based on layout style and border radius value.
813 *
814 * @param string $style Layout style (square, circle, etc.).
815 * @param int $border_radius Border radius value.
816 * @return string CSS border-radius style.
817 */
818 private static function generate_border_radius_style( $style, $border_radius ) {
819 if ( 'circle' === $style ) {
820 return 'border-radius:50%;';
821 } elseif ( $border_radius > 0 ) {
822 return 'border-radius:' . $border_radius . 'px;';
823 }
824 return '';
825 }
826
827 /**
828 * Generate image styles for email rendering.
829 *
830 * @param bool $use_fixed_height Whether to use fixed height with object-fit.
831 * @return string CSS style string.
832 */
833 private static function generate_image_styles( $use_fixed_height = false ) {
834 $base_styles = 'margin: 0; width: 100%; max-width: 100%; display: block;';
835
836 if ( $use_fixed_height ) {
837 return $base_styles . ' height: 200px; object-fit: cover; object-position: center;';
838 }
839
840 return $base_styles . ' height: auto;';
841 }
842
843 /**
844 * Generate image HTML with consistent styling.
845 *
846 * @param array $image Image data array.
847 * @param string $additional_styles Additional CSS styles.
848 * @param string $border_radius_style Border radius CSS.
849 * @param array $attr Block attributes (optional, for link processing).
850 * @return string Image HTML.
851 */
852 private static function generate_image_html( $image, $additional_styles = '', $border_radius_style = '', $attr = array() ) {
853 $base_styles = 'border:none;background-color:#0000001a;display:block;height:auto;max-width:100%;padding:0;';
854 $combined_styles = $base_styles . $additional_styles . $border_radius_style;
855
856 $img_html = sprintf(
857 '<img alt="%s" src="%s" style="%s" />',
858 esc_attr( $image['alt'] ),
859 esc_url( $image['url'] ),
860 $combined_styles
861 );
862
863 // Handle link settings for email
864 $link_to = ! empty( $attr['linkTo'] ) ? $attr['linkTo'] : 'none';
865 $href = self::get_image_link_href( $image, $attr, $link_to );
866
867 if ( ! empty( $href ) ) {
868 return sprintf( '<a href="%s">%s</a>', esc_url( $href ), $img_html );
869 }
870
871 return $img_html;
872 }
873
874 /**
875 * Get the href for an image based on link settings (used for email rendering).
876 * Excludes custom links which email clients will replace with the image.
877 *
878 * @since 15.0
879 *
880 * @param array $image Image data array.
881 * @param array $attr Block attributes.
882 * @param string $link_to Link setting.
883 * @return string The href URL or empty string.
884 */
885 private static function get_image_link_href( $image, $attr, $link_to ) {
886 switch ( $link_to ) {
887 case 'media':
888 return ! empty( $image['url'] ) ? $image['url'] : '';
889
890 case 'attachment':
891 // For email, we need to generate the attachment page URL from the image ID
892 if ( ! empty( $image['id'] ) ) {
893 $attachment_url = get_permalink( $image['id'] );
894 return $attachment_url ? $attachment_url : '';
895 }
896 return '';
897 default:
898 return '';
899 }
900 }
901
902 /**
903 * Create hierarchical chunks for square/circle layouts with larger items first.
904 *
905 * @param array $images Array of image data.
906 * @param int $columns Number of columns for the layout.
907 * @return array Array of rows with different sized chunks.
908 */
909 private static function create_hierarchical_chunks( $images, $columns ) {
910 $image_count = count( $images );
911 $chunks = array();
912
913 if ( $image_count <= $columns ) {
914 // For column count or fewer, single row
915 $chunks[] = $images;
916 } else {
917 // Calculate remainder when dividing by columns
918 $remainder = $image_count % $columns;
919 $start_index = 0;
920
921 // Handle all remainder cases to create proper hierarchy
922 if ( $remainder > 0 ) {
923 // Create a row with the remainder images (larger items first)
924 $chunks[] = array_slice( $images, 0, $remainder );
925 $start_index = $remainder;
926 }
927 // If remainder === 0, start_index stays 0
928
929 // Rest in groups of $columns
930 $remaining = array_slice( $images, $start_index );
931 $remaining_chunks = array_chunk( $remaining, $columns );
932 $chunks = array_merge( $chunks, $remaining_chunks );
933 }
934
935 return $chunks;
936 }
937 }
938
939 add_action( 'init', array( Tiled_Gallery::class, 'register' ) );
940