PluginProbe
Gutenify – Visual Site Builder Blocks & Starter Templates / 1.8.0
Gutenify – Visual Site Builder Blocks & Starter Templates v1.8.0
1.9.0 1.8.0 1.7.0 1.6.6 1.6.5 1.6.4 trunk 0.0.1 0.0.2 0.0.3 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 74 releases
gutenify / core / dist / blocks / term-thumbnail / render.php

render.php in Gutenify – Visual Site Builder Blocks & Starter Templates 1.8.0, at core/dist/blocks/term-thumbnail/render.php

335 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Term Thumbnail render template.
4 *
5 * @package Gutenify
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 if ( ! function_exists( 'gutenify_term_thumbnail_get_border_attributes' ) ) {
13 /**
14 * Builds border classnames/styles for this block from its own `style`
15 * attribute, mirroring core's own (Gutenberg-plugin)
16 * `gutenberg_get_block_core_post_featured_image_border_attributes()`.
17 *
18 * block.json's `__experimentalBorder` support declares
19 * `__experimentalSkipSerialization: true`, so WordPress never
20 * auto-applies the border to `get_block_wrapper_attributes()`'s outer
21 * wrapper `<div>` — without that flag the rounded corners/width/color
22 * only ever showed on the invisible wrapper, never on the `<img>` (or
23 * the dim-overlay `<span>`) sitting inside it. This re-applies border
24 * support manually to the actual image/overlay elements below, using
25 * the stable core `wp_style_engine_get_styles()` (not the
26 * Gutenberg-plugin-only `gutenberg_style_engine_get_styles()`, which
27 * only exists here because the Gutenberg plugin happens to be checked
28 * out for reference on this dev site).
29 *
30 * Declared as a plain guarded function (not a class method/namespaced
31 * function) because this file is a render template, not a class, and
32 * `render.php` can be `include`d more than once per request — a Terms
33 * Query Loop renders this block once per term, so a second `include`
34 * of this same file within the same request is the normal case here,
35 * not an edge case.
36 *
37 * @param array $attributes Block attributes.
38 * @return array{class?: string, style?: string} Border classnames/styles, if any were set.
39 */
40 function gutenify_term_thumbnail_get_border_attributes( $attributes ) {
41 $border_styles = array();
42 $sides = array( 'top', 'right', 'bottom', 'left' );
43
44 if ( isset( $attributes['style']['border']['radius'] ) ) {
45 $border_styles['radius'] = $attributes['style']['border']['radius'];
46 }
47
48 if ( isset( $attributes['style']['border']['style'] ) ) {
49 $border_styles['style'] = $attributes['style']['border']['style'];
50 }
51
52 if ( isset( $attributes['style']['border']['width'] ) ) {
53 $border_styles['width'] = $attributes['style']['border']['width'];
54 }
55
56 $preset_color = ! empty( $attributes['borderColor'] ) ? 'var:preset|color|' . $attributes['borderColor'] : null;
57 $custom_color = $attributes['style']['border']['color'] ?? null;
58 $border_styles['color'] = $preset_color ? $preset_color : $custom_color;
59
60 // Individual per-side overrides (top/right/bottom/left), same shape
61 // core's own border block-support stores them in.
62 foreach ( $sides as $side ) {
63 $side_border = $attributes['style']['border'][ $side ] ?? null;
64 $border_styles[ $side ] = array(
65 'color' => $side_border['color'] ?? null,
66 'style' => $side_border['style'] ?? null,
67 'width' => $side_border['width'] ?? null,
68 );
69 }
70
71 $styles = wp_style_engine_get_styles( array( 'border' => $border_styles ) );
72
73 $border_attributes = array();
74 if ( ! empty( $styles['classnames'] ) ) {
75 $border_attributes['class'] = $styles['classnames'];
76 }
77 if ( ! empty( $styles['css'] ) ) {
78 $border_attributes['style'] = $styles['css'];
79 }
80 return $border_attributes;
81 }
82 }
83
84 // Both the classic-editor context (is_admin()) and ServerSideRender's own
85 // block-renderer REST call (wp_is_json_request()) render this template for
86 // editor-canvas preview purposes, never as real frontend output. Every
87 // editor-only branch below (missing-term messages, and the link-suppression
88 // in Task 1) keys off this single flag rather than repeating the same
89 // condition three times.
90 $is_editor_context = is_admin() || wp_is_json_request();
91
92 // core/term-template injects `termId`/`taxonomy` into block context for
93 // every child block rendered inside its loop (same contract core's own
94 // core/term-name and core/term-count consume).
95 $term_id = isset( $block->context['termId'] ) ? absint( $block->context['termId'] ) : 0;
96 $taxonomy = isset( $block->context['taxonomy'] ) ? sanitize_key( $block->context['taxonomy'] ) : '';
97
98 // The editor's ServerSideRender preview calls the block-renderer REST
99 // endpoint, which does not forward block context — only attributes. When
100 // this block sits inside a real Term Template in the editor canvas,
101 // Gutenberg's own BlockContextProvider still supplies `context` to this
102 // file's caller (edit.js), which mirrors it into these preview attributes
103 // so the SSR request can reconstruct the same term. Mirrors
104 // single-product-gallery's previewProductId pattern.
105 if ( ! $term_id && ! empty( $attributes['previewTermId'] ) ) {
106 $term_id = absint( $attributes['previewTermId'] );
107 $taxonomy = ! empty( $attributes['previewTaxonomy'] ) ? sanitize_key( $attributes['previewTaxonomy'] ) : $taxonomy;
108 }
109
110 if ( ! $term_id ) {
111 if ( $is_editor_context ) {
112 echo '<p class="gutenify--term-thumbnail__message">' . esc_html__( 'This block must be placed inside a Terms Query Loop (Term Template) so it can receive a term.', 'gutenify' ) . '</p>';
113 }
114 return;
115 }
116
117 $term = get_term( $term_id, $taxonomy );
118
119 if ( ! $term || is_wp_error( $term ) ) {
120 if ( $is_editor_context ) {
121 echo '<p class="gutenify--term-thumbnail__message">' . esc_html__( 'This term could not be found.', 'gutenify' ) . '</p>';
122 }
123 return;
124 }
125
126 // get_term() already resolves the real taxonomy from the term object even
127 // when the `$taxonomy` arg above was empty/wrong — use its value from here
128 // on for the WooCommerce fallback check and the term link below.
129 $taxonomy = $term->taxonomy;
130
131 $allowed_sizes = array_values(
132 array_unique(
133 array_merge( array( 'thumbnail', 'medium', 'medium_large', 'large', 'full' ), get_intermediate_image_sizes() )
134 )
135 );
136 $image_size = isset( $attributes['imageSize'] ) && in_array( $attributes['imageSize'], $allowed_sizes, true ) ? $attributes['imageSize'] : 'thumbnail';
137
138 // Same three values as CSS's own `object-fit` keywords, matching core's
139 // Image block "Scale" control (Cover/Contain/Fill) 1:1.
140 $allowed_scales = array( 'cover', 'contain', 'fill' );
141 $scale = isset( $attributes['scale'] ) && in_array( $attributes['scale'], $allowed_scales, true ) ? $attributes['scale'] : 'cover';
142
143 // Same preset values as core's Image block "Aspect ratio" control —
144 // whitelisted rather than passed through raw since this becomes a CSS
145 // custom property value below.
146 //
147 // Default is "Square" (1), not "Original" (auto) — this block is meant
148 // for a Terms Query Loop grid, where each term's own thumbnail almost
149 // certainly has a different natural size/orientation (different source
150 // photos per category). "Original" produced a visibly broken grid out
151 // of the box; a uniform square crop is the standard choice for exactly
152 // this reason.
153 $allowed_aspect_ratios = array( 'auto', '1', '4/3', '3/2', '16/9', '3/4', '2/3', '9/16' );
154 $aspect_ratio = isset( $attributes['aspectRatio'] ) ? (string) $attributes['aspectRatio'] : '1';
155 if ( ! in_array( $aspect_ratio, $allowed_aspect_ratios, true ) ) {
156 $aspect_ratio = '1';
157 }
158
159 // `focalPoint` mirrors core's Image block attribute shape ({x, y}, each
160 // 0-1) — clamped here since it becomes a CSS percentage below.
161 $focal_point = isset( $attributes['focalPoint'] ) && is_array( $attributes['focalPoint'] ) ? $attributes['focalPoint'] : array();
162 $focal_point_x = isset( $focal_point['x'] ) ? min( 1, max( 0, (float) $focal_point['x'] ) ) : 0.5;
163 $focal_point_y = isset( $focal_point['y'] ) ? min( 1, max( 0, (float) $focal_point['y'] ) ) : 0.5;
164
165 $link_to_term = ! isset( $attributes['linkToTerm'] ) || ! empty( $attributes['linkToTerm'] );
166
167 // Border support is skip-serialized in block.json (see the helper above),
168 // so it never lands on get_block_wrapper_attributes()'s wrapper <div>
169 // below — apply it directly to the <img>/placeholder <img> (and, further
170 // down, the dim-overlay <span>) instead, same as core's own Post Featured
171 // Image block does for its <img>/overlay pair.
172 $border_attributes = gutenify_term_thumbnail_get_border_attributes( $attributes );
173 $border_class = ! empty( $border_attributes['class'] ) ? (string) $border_attributes['class'] : '';
174 $border_style = ! empty( $border_attributes['style'] ) ? (string) $border_attributes['style'] : '';
175
176 // Same WooCommerce term-thumbnail meta convention category-list already
177 // reads.
178 $image_id = absint( get_term_meta( $term_id, 'thumbnail_id', true ) );
179 $image_html = '';
180
181 if ( $image_id ) {
182 $attachment_title = get_the_title( $image_id );
183 $image_attr = array(
184 'loading' => 'lazy',
185 'alt' => $attachment_title ? $attachment_title : $term->name,
186 );
187 // wp_get_attachment_image() esc_attr()'s every $attr value itself
188 // before echoing, so class/style don't need re-escaping here — only
189 // merged in when actually set, so the default attachment/size classes
190 // are left untouched when there's no border to apply.
191 if ( $border_class ) {
192 $image_attr['class'] = $border_class;
193 }
194 if ( $border_style ) {
195 $image_attr['style'] = $border_style;
196 }
197 $image_html = wp_get_attachment_image( $image_id, $image_size, false, $image_attr );
198 } elseif ( function_exists( 'wc_placeholder_img' ) && 'product_cat' === $taxonomy ) {
199 // Generic, not WooCommerce-gated: this fallback only fires for the one
200 // case category-list already handles the same way — a product_cat term
201 // with no thumbnail_id meta, while WooCommerce is active. Any other
202 // taxonomy with no thumbnail simply renders nothing below, matching
203 // core's own Post Featured Image block when a post has no featured
204 // image.
205 //
206 // wc_placeholder_img()'s own default $attr (`wp_parse_args( $attr,
207 // $default_attr )`) lets a passed-in `class` key win outright rather
208 // than merging strings, so its default
209 // `woocommerce-placeholder wp-post-image` class has to be concatenated
210 // in here explicitly or it's silently lost the moment a border class
211 // exists. wc_placeholder_img() also esc_attr()'s every attribute
212 // itself before echoing, same as wp_get_attachment_image() above.
213 $placeholder_attr = array(
214 'class' => trim( 'woocommerce-placeholder wp-post-image' . ( $border_class ? ' ' . $border_class : '' ) ),
215 );
216 if ( $border_style ) {
217 $placeholder_attr['style'] = $border_style;
218 }
219 $image_html = wc_placeholder_img( $image_size, $placeholder_attr );
220 }
221
222 if ( '' === $image_html ) {
223 if ( $is_editor_context ) {
224 echo '<p class="gutenify--term-thumbnail__message">' . esc_html__( 'This term does not have a thumbnail image.', 'gutenify' ) . '</p>';
225 }
226 return;
227 }
228
229 $has_aspect_ratio = 'auto' !== $aspect_ratio;
230 $style = '--term-thumbnail-scale:' . esc_attr( $scale ) . ';';
231 if ( $has_aspect_ratio ) {
232 $style .= sprintf(
233 '--term-thumbnail-aspect-ratio:%1$s;--term-thumbnail-focal-x:%2$s%%;--term-thumbnail-focal-y:%3$s%%;',
234 esc_attr( $aspect_ratio ),
235 esc_attr( $focal_point_x * 100 ),
236 esc_attr( $focal_point_y * 100 )
237 );
238 }
239
240 $wrapper_attributes = get_block_wrapper_attributes(
241 array(
242 'class' => 'gutenify--term-thumbnail' . ( $has_aspect_ratio ? ' has-aspect-ratio' : '' ),
243 'style' => $style,
244 )
245 );
246
247 // A link is never rendered for an editor-context render (classic-editor
248 // is_admin(), or ServerSideRender's block-renderer REST call) regardless of
249 // the `linkToTerm` attribute's value — clicking a link inside the editor
250 // canvas is bad UX (nested-link/click-target problems), and core's own
251 // Post Featured Image block never renders its link in the editor either,
252 // only on the real frontend. get_term_link() is also skipped entirely in
253 // that case since its result would never be used.
254 $term_link = ( ! $is_editor_context && $link_to_term ) ? get_term_link( $term ) : '';
255 $has_link = ! $is_editor_context && $link_to_term && $term_link && ! is_wp_error( $term_link );
256
257 $link_target = isset( $attributes['linkTarget'] ) && '_blank' === $attributes['linkTarget'] ? '_blank' : '_self';
258 $link_rel = isset( $attributes['rel'] ) ? (string) $attributes['rel'] : '';
259
260 // Overlay: dim/color-or-gradient layer on top of the image, matching core's
261 // own Post Featured Image "Overlay" panel (dimRatio/overlayColor/
262 // customOverlayColor/gradient/customGradient). Snapped to the nearest 10 to
263 // match the discrete has-background-dim-{n} opacity classes shipped in
264 // style.scss (also guards against an out-of-step value ever reaching here
265 // via a raw REST/attributes payload, since RangeControl's step is only a
266 // display hint).
267 $dim_ratio = isset( $attributes['dimRatio'] ) ? absint( $attributes['dimRatio'] ) : 0;
268 $dim_ratio = (int) ( round( $dim_ratio / 10 ) * 10 );
269 $dim_ratio = max( 0, min( 100, $dim_ratio ) );
270
271 $overlay_color = isset( $attributes['overlayColor'] ) ? sanitize_html_class( $attributes['overlayColor'] ) : '';
272 $custom_overlay_color = isset( $attributes['customOverlayColor'] ) ? (string) $attributes['customOverlayColor'] : '';
273 $gradient_slug = isset( $attributes['gradient'] ) ? sanitize_html_class( $attributes['gradient'] ) : '';
274 $custom_gradient = isset( $attributes['customGradient'] ) ? (string) $attributes['customGradient'] : '';
275
276 $overlay_html = '';
277
278 if ( $dim_ratio > 0 ) {
279 $overlay_classes = array( 'gutenify--term-thumbnail__overlay', 'has-background-dim', 'has-background-dim-' . $dim_ratio );
280 $overlay_styles = array();
281
282 if ( $overlay_color ) {
283 $overlay_classes[] = 'has-' . $overlay_color . '-background-color';
284 }
285
286 if ( $gradient_slug || $custom_gradient ) {
287 $overlay_classes[] = 'has-background-gradient';
288 }
289
290 if ( $gradient_slug ) {
291 $overlay_classes[] = 'has-' . $gradient_slug . '-gradient-background';
292 }
293
294 // Give the overlay the same border radius/width/color as the image
295 // beneath it — otherwise a square overlay pokes out past the image's
296 // rounded corners, same fix core's own Post Featured Image block
297 // applies via its overlay markup builder.
298 if ( $border_class ) {
299 $overlay_classes[] = $border_class;
300 }
301 if ( $border_style ) {
302 $overlay_styles[] = $border_style;
303 }
304
305 // Mirrors core's own overlay markup builder: build the raw declarations
306 // first, then run the combined string through safecss_filter_attr()
307 // (rejects anything that isn't a safe CSS value, e.g. a javascript: URI
308 // smuggled into a gradient) before esc_attr()'ing the whole style
309 // attribute — the same two-layer defense already used above for
310 // aspect-ratio/height/width/object-fit.
311 if ( $custom_gradient ) {
312 $overlay_styles[] = 'background-image:' . $custom_gradient . ';';
313 } elseif ( $custom_overlay_color ) {
314 $overlay_styles[] = 'background-color:' . $custom_overlay_color . ';';
315 }
316
317 $overlay_html = sprintf(
318 '<span class="%1$s" style="%2$s" aria-hidden="true"></span>',
319 esc_attr( implode( ' ', $overlay_classes ) ),
320 esc_attr( safecss_filter_attr( implode( '', $overlay_styles ) ) )
321 );
322 }
323
324 $image_html .= $overlay_html;
325 ?>
326 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
327 <?php if ( $has_link ) : ?>
328 <a class="gutenify--term-thumbnail__link" href="<?php echo esc_url( $term_link ); ?>"<?php echo '_blank' === $link_target ? ' target="_blank" rel="' . esc_attr( $link_rel ) . '"' : ''; ?>>
329 <span class="gutenify--term-thumbnail__image"><?php echo $image_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
330 </a>
331 <?php else : ?>
332 <span class="gutenify--term-thumbnail__image"><?php echo $image_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></span>
333 <?php endif; ?>
334 </div>
335