PluginProbe
Gutenify – Visual Site Builder Blocks & Starter Templates / 1.9.0
Gutenify – Visual Site Builder Blocks & Starter Templates v1.9.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 / inc / helpers / class-dynamic-styles.php

class-dynamic-styles.php in Gutenify – Visual Site Builder Blocks & Starter Templates 1.9.0, at core/inc/helpers/class-dynamic-styles.php

906 lines 29.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Dynamic_Styles class for generating responsive spacing styles with media queries.
4 *
5 * @package gutenify
6 * @since 1.0.0
7 */
8
9 namespace gutenify;
10
11 defined( 'ABSPATH' ) || exit;
12
13 /**
14 * Dynamic_Styles class for generating responsive spacing styles with media queries.
15 *
16 * @since 1.0.0
17 */
18 class Dynamic_Styles {
19
20 /**
21 * Generate spacing CSS with media queries for desktop, tablet, and mobile.
22 *
23 * @param string $selector The CSS selector to apply the styles to.
24 * @param array $styles Associative array containing 'desktop', 'tablet', 'mobile' keys with style values.
25 * @param string $prefix The CSS prefix (default is 'margin-').
26 * @return string The generated CSS string.
27 */
28 public static function get_spacing_with_media( $selector, $styles, $prefix = 'margin-' ) {
29 $style_chunk = '';
30
31 if ( ! empty( $styles['desktop'] ) ) {
32 $style_chunk .= $selector . '{';
33 $style_chunk .= \gutenify\Style_Helpers::box_control( $styles['desktop'], $prefix );
34 $style_chunk .= '}';
35 }
36
37 if ( ! empty( $styles['tablet'] ) ) {
38 $style_chunk .= '@media only screen and (max-width: ' . \gutenify\Style_Helpers::$tablet_max_width . ') {';
39 $style_chunk .= $selector . '{';
40 $style_chunk .= \gutenify\Style_Helpers::box_control( $styles['tablet'], $prefix );
41 $style_chunk .= '}';
42 $style_chunk .= '}';
43 }
44
45 if ( ! empty( $styles['mobile'] ) ) {
46 $style_chunk .= '@media only screen and (max-width: ' . \gutenify\Style_Helpers::$mobile_max_width . ') {';
47 $style_chunk .= $selector . '{';
48 $style_chunk .= \gutenify\Style_Helpers::box_control( $styles['mobile'], $prefix );
49 $style_chunk .= '}';
50 $style_chunk .= '}';
51 }
52 return $style_chunk;
53 }
54
55 /**
56 * Generate position CSS with media queries for desktop, tablet, mobile, and
57 * any block-level custom breakpoints (see AdvancedGroup's
58 * `customBlockBreakpoints` attribute — the only current caller of that).
59 *
60 * `$position` is either the flat (non-responsive) shape — {type, top, ...}
61 * directly — or the responsive shape — {desktop: {...}, tablet: {...},
62 * mobile: {...}, [customId]: {...}}. Detected by shape, not by caller, so a
63 * flat value (from a block not yet upgraded to responsive Position controls,
64 * or content saved before the upgrade) keeps rendering exactly as before.
65 * zIndex/overflow are per-breakpoint like everything else here — each
66 * breakpoint's own bucket carries its own zIndex/overflow if the editor
67 * wrote one there; Style_Helpers::position_control() reads both directly
68 * off whatever `$values` it's given, no special-casing needed here.
69 *
70 * @param string $selector The CSS selector to apply the styles to.
71 * @param array $position The stored `advancedStyling.position.normal` value (or a state bucket, when called from get_state_variants_css()).
72 * @param array $custom_breakpoints The block's `customBlockBreakpoints` attribute, if any.
73 * @return string The generated CSS string.
74 */
75 public static function get_position_with_media( $selector, $position, $custom_breakpoints = array() ) {
76 if ( empty( $position ) || ! is_array( $position ) ) {
77 return '';
78 }
79
80 $breakpoints = array(
81 array(
82 'id' => 'desktop',
83 'width' => null,
84 ),
85 array(
86 'id' => 'tablet',
87 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
88 ),
89 array(
90 'id' => 'mobile',
91 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
92 ),
93 );
94
95 $is_responsive = false;
96 foreach ( $breakpoints as $bp ) {
97 if ( ! empty( $position[ $bp['id'] ] ) && is_array( $position[ $bp['id'] ] ) ) {
98 $is_responsive = true;
99 break;
100 }
101 }
102
103 if ( ! $is_responsive ) {
104 $declarations = \gutenify\Style_Helpers::position_control( $position );
105 return $declarations ? $selector . '{' . $declarations . '}' : '';
106 }
107
108 if ( is_array( $custom_breakpoints ) ) {
109 foreach ( $custom_breakpoints as $custom_bp ) {
110 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
111 $breakpoints[] = array(
112 'id' => (string) $custom_bp['id'],
113 'width' => (int) $custom_bp['width'],
114 );
115 }
116 }
117 }
118
119 usort(
120 $breakpoints,
121 function ( $a, $b ) {
122 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
123 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
124 return $width_b <=> $width_a;
125 }
126 );
127
128 $css = '';
129
130 foreach ( $breakpoints as $bp ) {
131 $values = ! empty( $position[ $bp['id'] ] ) && is_array( $position[ $bp['id'] ] ) ? $position[ $bp['id'] ] : array();
132
133 $declarations = \gutenify\Style_Helpers::position_control( $values );
134 if ( ! $declarations ) {
135 continue;
136 }
137
138 if ( null === $bp['width'] ) {
139 $css .= $selector . '{' . $declarations . '}';
140 } else {
141 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
142 }
143 }
144
145 return $css;
146 }
147
148 /**
149 * Generate `transform`/`transform-origin` CSS with media queries for
150 * desktop, tablet, mobile, and any block-level custom breakpoints.
151 *
152 * Unlike get_position_with_media() above, this CANNOT just pass each
153 * breakpoint's own bucket straight to Style_Helpers::build_transform_value()
154 * — `transform` and `transform-origin` are each a single CSS property
155 * combining several logical sub-values (translateX, rotate, scaleX, ...).
156 * If tablet only overrides `rotate`, emitting just `transform: rotate(45deg)`
157 * at that media query would silently erase desktop's translateX/scaleX/etc,
158 * since CSS never merges partial transform functions across rules the way
159 * it does independent longhand properties (position/top/z-index/...).
160 *
161 * So each emitted breakpoint carries the full CUMULATIVE resolved value —
162 * desktop's own values, overridden field-by-field by every narrower
163 * breakpoint up to and including this one. Mirrored in the JS side,
164 * block-transform/inline-styles.js — keep both in sync.
165 *
166 * @param string $selector The CSS selector to apply the styles to.
167 * @param array $transform The stored `advancedStyling.transform.normal` value (or a state bucket, when called from get_state_variants_css()).
168 * @param array $custom_breakpoints The block's `customBlockBreakpoints` attribute, if any.
169 * @return string The generated CSS string.
170 */
171 public static function get_transform_with_media( $selector, $transform, $custom_breakpoints = array() ) {
172 if ( empty( $transform ) || ! is_array( $transform ) ) {
173 return '';
174 }
175
176 $breakpoints = array(
177 array(
178 'id' => 'desktop',
179 'width' => null,
180 ),
181 array(
182 'id' => 'tablet',
183 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
184 ),
185 array(
186 'id' => 'mobile',
187 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
188 ),
189 );
190
191 $is_responsive = false;
192 foreach ( $breakpoints as $bp ) {
193 if ( ! empty( $transform[ $bp['id'] ] ) && is_array( $transform[ $bp['id'] ] ) ) {
194 $is_responsive = true;
195 break;
196 }
197 }
198
199 if ( ! $is_responsive ) {
200 $declarations = \gutenify\Style_Helpers::transform_control( $transform );
201 return $declarations ? $selector . '{' . $declarations . '}' : '';
202 }
203
204 if ( is_array( $custom_breakpoints ) ) {
205 foreach ( $custom_breakpoints as $custom_bp ) {
206 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
207 $breakpoints[] = array(
208 'id' => (string) $custom_bp['id'],
209 'width' => (int) $custom_bp['width'],
210 );
211 }
212 }
213 }
214
215 usort(
216 $breakpoints,
217 function ( $a, $b ) {
218 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
219 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
220 return $width_b <=> $width_a;
221 }
222 );
223
224 $transform_fields = array( 'translateX', 'translateY', 'rotate', 'scaleX', 'scaleY', 'skewX', 'skewY' );
225 $origin_fields = array( 'originX', 'originY' );
226
227 $cumulative_transform = array();
228 $cumulative_origin = array();
229 $css = '';
230
231 foreach ( $breakpoints as $bp ) {
232 $bucket = ! empty( $transform[ $bp['id'] ] ) && is_array( $transform[ $bp['id'] ] ) ? $transform[ $bp['id'] ] : array();
233
234 $touched_transform = false;
235 foreach ( $transform_fields as $field ) {
236 if ( isset( $bucket[ $field ] ) && '' !== $bucket[ $field ] ) {
237 $cumulative_transform[ $field ] = $bucket[ $field ];
238 $touched_transform = true;
239 }
240 }
241
242 $touched_origin = false;
243 foreach ( $origin_fields as $field ) {
244 if ( isset( $bucket[ $field ] ) && '' !== $bucket[ $field ] ) {
245 $cumulative_origin[ $field ] = $bucket[ $field ];
246 $touched_origin = true;
247 }
248 }
249
250 $declarations = '';
251 if ( $touched_transform ) {
252 $value = \gutenify\Style_Helpers::build_transform_value( $cumulative_transform );
253 if ( $value ) {
254 $declarations .= 'transform:' . $value . ';';
255 }
256 }
257 if ( $touched_origin ) {
258 $value = \gutenify\Style_Helpers::build_transform_origin_value( $cumulative_origin );
259 if ( $value ) {
260 $declarations .= 'transform-origin:' . $value . ';';
261 }
262 }
263
264 if ( ! $declarations ) {
265 continue;
266 }
267
268 if ( null === $bp['width'] ) {
269 $css .= $selector . '{' . $declarations . '}';
270 } else {
271 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
272 }
273 }
274
275 return $css;
276 }
277
278 /**
279 * Generate `opacity`/`filter`/`backdrop-filter` CSS with media queries for
280 * desktop, tablet, mobile, and any block-level custom breakpoints.
281 *
282 * `filter` and `backdrop-filter` need the same cumulative-resolution
283 * treatment as get_transform_with_media() above (each is a single CSS
284 * property combining several functions — a narrower breakpoint that only
285 * sets one function would otherwise erase the others inherited from a
286 * wider breakpoint). `opacity` doesn't need this — it's an independent
287 * longhand, handled the same way as Position's simple fields. Mirrored in
288 * the JS side, block-effects/inline-styles.js — keep both in sync.
289 *
290 * @param string $selector The CSS selector to apply the styles to.
291 * @param array $effects The stored `advancedStyling.effects.normal` value (or a state bucket, when called from get_state_variants_css()).
292 * @param array $custom_breakpoints The block's `customBlockBreakpoints` attribute, if any.
293 * @return string The generated CSS string.
294 */
295 public static function get_effects_with_media( $selector, $effects, $custom_breakpoints = array() ) {
296 if ( empty( $effects ) || ! is_array( $effects ) ) {
297 return '';
298 }
299
300 $breakpoints = array(
301 array(
302 'id' => 'desktop',
303 'width' => null,
304 ),
305 array(
306 'id' => 'tablet',
307 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
308 ),
309 array(
310 'id' => 'mobile',
311 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
312 ),
313 );
314
315 $is_responsive = false;
316 foreach ( $breakpoints as $bp ) {
317 if ( ! empty( $effects[ $bp['id'] ] ) && is_array( $effects[ $bp['id'] ] ) ) {
318 $is_responsive = true;
319 break;
320 }
321 }
322
323 if ( ! $is_responsive ) {
324 $declarations = \gutenify\Style_Helpers::effects_control( $effects );
325 return $declarations ? $selector . '{' . $declarations . '}' : '';
326 }
327
328 if ( is_array( $custom_breakpoints ) ) {
329 foreach ( $custom_breakpoints as $custom_bp ) {
330 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
331 $breakpoints[] = array(
332 'id' => (string) $custom_bp['id'],
333 'width' => (int) $custom_bp['width'],
334 );
335 }
336 }
337 }
338
339 usort(
340 $breakpoints,
341 function ( $a, $b ) {
342 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
343 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
344 return $width_b <=> $width_a;
345 }
346 );
347
348 $filter_fields = array();
349 $backdrop_fields = array();
350 foreach ( \gutenify\Style_Helpers::filter_functions() as $fn ) {
351 $filter_fields[] = \gutenify\Style_Helpers::filter_field_key( $fn['name'], false );
352 $backdrop_fields[] = \gutenify\Style_Helpers::filter_field_key( $fn['name'], true );
353 }
354
355 $cumulative_filter = array();
356 $cumulative_backdrop = array();
357 $css = '';
358
359 foreach ( $breakpoints as $bp ) {
360 $bucket = ! empty( $effects[ $bp['id'] ] ) && is_array( $effects[ $bp['id'] ] ) ? $effects[ $bp['id'] ] : array();
361
362 $declarations = '';
363 $opacity_value = \gutenify\Style_Helpers::build_opacity_value( $bucket );
364 if ( '' !== $opacity_value ) {
365 $declarations .= 'opacity:' . esc_attr( $opacity_value ) . ';';
366 }
367
368 $declarations .= \gutenify\Style_Helpers::build_blend_clip_value( $bucket );
369
370 $touched_filter = false;
371 foreach ( $filter_fields as $field ) {
372 if ( isset( $bucket[ $field ] ) && '' !== $bucket[ $field ] ) {
373 $cumulative_filter[ $field ] = $bucket[ $field ];
374 $touched_filter = true;
375 }
376 }
377
378 $touched_backdrop = false;
379 foreach ( $backdrop_fields as $field ) {
380 if ( isset( $bucket[ $field ] ) && '' !== $bucket[ $field ] ) {
381 $cumulative_backdrop[ $field ] = $bucket[ $field ];
382 $touched_backdrop = true;
383 }
384 }
385
386 if ( $touched_filter ) {
387 $value = \gutenify\Style_Helpers::build_filter_functions_value( $cumulative_filter, false );
388 if ( $value ) {
389 $declarations .= 'filter:' . $value . ';';
390 }
391 }
392 if ( $touched_backdrop ) {
393 $value = \gutenify\Style_Helpers::build_filter_functions_value( $cumulative_backdrop, true );
394 if ( $value ) {
395 $declarations .= 'backdrop-filter:' . $value . ';-webkit-backdrop-filter:' . $value . ';';
396 }
397 }
398
399 if ( ! $declarations ) {
400 continue;
401 }
402
403 if ( null === $bp['width'] ) {
404 $css .= $selector . '{' . $declarations . '}';
405 } else {
406 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
407 }
408 }
409
410 return $css;
411 }
412
413 /**
414 * Generate `background-color`/`color` CSS with media queries for desktop,
415 * tablet, mobile, and any block-level custom breakpoints.
416 *
417 * Both are independent longhand properties, so — unlike
418 * get_transform_with_media()/get_effects_with_media() — this needs no
419 * cumulative-resolution handling; each breakpoint's own bucket is passed
420 * straight to Style_Helpers::colors_control(). Mirrored in the JS side,
421 * block-colors/inline-styles.js — keep both in sync.
422 *
423 * @param string $selector The CSS selector to apply the styles to.
424 * @param array $colors The stored `advancedStyling.colors.normal` value (or a state bucket, when called from get_state_variants_css()).
425 * @param array $custom_breakpoints The block's `customBlockBreakpoints` attribute, if any.
426 * @return string The generated CSS string.
427 */
428 public static function get_colors_with_media( $selector, $colors, $custom_breakpoints = array() ) {
429 if ( empty( $colors ) || ! is_array( $colors ) ) {
430 return '';
431 }
432
433 $breakpoints = array(
434 array(
435 'id' => 'desktop',
436 'width' => null,
437 ),
438 array(
439 'id' => 'tablet',
440 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
441 ),
442 array(
443 'id' => 'mobile',
444 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
445 ),
446 );
447
448 $is_responsive = false;
449 foreach ( $breakpoints as $bp ) {
450 if ( ! empty( $colors[ $bp['id'] ] ) && is_array( $colors[ $bp['id'] ] ) ) {
451 $is_responsive = true;
452 break;
453 }
454 }
455
456 if ( ! $is_responsive ) {
457 $declarations = \gutenify\Style_Helpers::colors_control( $colors );
458 return $declarations ? $selector . '{' . $declarations . '}' : '';
459 }
460
461 if ( is_array( $custom_breakpoints ) ) {
462 foreach ( $custom_breakpoints as $custom_bp ) {
463 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
464 $breakpoints[] = array(
465 'id' => (string) $custom_bp['id'],
466 'width' => (int) $custom_bp['width'],
467 );
468 }
469 }
470 }
471
472 usort(
473 $breakpoints,
474 function ( $a, $b ) {
475 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
476 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
477 return $width_b <=> $width_a;
478 }
479 );
480
481 $css = '';
482
483 foreach ( $breakpoints as $bp ) {
484 $values = ! empty( $colors[ $bp['id'] ] ) && is_array( $colors[ $bp['id'] ] ) ? $colors[ $bp['id'] ] : array();
485
486 $declarations = \gutenify\Style_Helpers::colors_control( $values );
487 if ( ! $declarations ) {
488 continue;
489 }
490
491 if ( null === $bp['width'] ) {
492 $css .= $selector . '{' . $declarations . '}';
493 } else {
494 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
495 }
496 }
497
498 return $css;
499 }
500
501 /**
502 * Generates dynamic `transition-duration`/`transition-timing-function`/
503 * `transition-delay` CSS for the block, including any block-level custom
504 * breakpoints. Normal-only — see block-transition/inline-styles.js's
505 * docblock for why Transition has no Hover/Active/Focus variant.
506 *
507 * @param string $selector CSS selector.
508 * @param array $transition `advancedStyling.transition.normal` bucket.
509 * @param array $custom_breakpoints Optional block-level custom breakpoints.
510 * @return string CSS styles.
511 */
512 public static function get_transition_with_media( $selector, $transition, $custom_breakpoints = array() ) {
513 if ( empty( $transition ) || ! is_array( $transition ) ) {
514 return '';
515 }
516
517 $breakpoints = array(
518 array(
519 'id' => 'desktop',
520 'width' => null,
521 ),
522 array(
523 'id' => 'tablet',
524 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
525 ),
526 array(
527 'id' => 'mobile',
528 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
529 ),
530 );
531
532 $is_responsive = false;
533 foreach ( $breakpoints as $bp ) {
534 if ( ! empty( $transition[ $bp['id'] ] ) && is_array( $transition[ $bp['id'] ] ) ) {
535 $is_responsive = true;
536 break;
537 }
538 }
539
540 if ( ! $is_responsive ) {
541 $declarations = \gutenify\Style_Helpers::transition_control( $transition );
542 return $declarations ? $selector . '{' . $declarations . '}' : '';
543 }
544
545 if ( is_array( $custom_breakpoints ) ) {
546 foreach ( $custom_breakpoints as $custom_bp ) {
547 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
548 $breakpoints[] = array(
549 'id' => (string) $custom_bp['id'],
550 'width' => (int) $custom_bp['width'],
551 );
552 }
553 }
554 }
555
556 usort(
557 $breakpoints,
558 function ( $a, $b ) {
559 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
560 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
561 return $width_b <=> $width_a;
562 }
563 );
564
565 $css = '';
566
567 foreach ( $breakpoints as $bp ) {
568 $values = ! empty( $transition[ $bp['id'] ] ) && is_array( $transition[ $bp['id'] ] ) ? $transition[ $bp['id'] ] : array();
569
570 $declarations = \gutenify\Style_Helpers::transition_control( $values );
571 if ( ! $declarations ) {
572 continue;
573 }
574
575 if ( null === $bp['width'] ) {
576 $css .= $selector . '{' . $declarations . '}';
577 } else {
578 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
579 }
580 }
581
582 return $css;
583 }
584
585 /**
586 * Generates dynamic `box-shadow` CSS for the block, including any
587 * block-level custom breakpoints. Unlike colors/transition's independent
588 * longhand fields, `box-shadow` is a single CSS shorthand, so this only
589 * calls `Style_Helpers::custom_box_shadow_control()` (which has no
590 * empty-guard of its own and always fills in defaults for whichever
591 * sub-fields are unset) when a given breakpoint's bucket has at least one
592 * sub-field set — matching the guard already used at every existing
593 * `custom_box_shadow_control()` call site (post-carousel, post-list).
594 *
595 * @param string $selector CSS selector.
596 * @param array $box_shadow `advancedStyling.boxShadow.<state>` bucket.
597 * @param array $custom_breakpoints Optional block-level custom breakpoints.
598 * @return string CSS styles.
599 */
600 public static function get_box_shadow_with_media( $selector, $box_shadow, $custom_breakpoints = array() ) {
601 if ( empty( $box_shadow ) || ! is_array( $box_shadow ) ) {
602 return '';
603 }
604
605 $breakpoints = array(
606 array(
607 'id' => 'desktop',
608 'width' => null,
609 ),
610 array(
611 'id' => 'tablet',
612 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
613 ),
614 array(
615 'id' => 'mobile',
616 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
617 ),
618 );
619
620 $is_responsive = false;
621 foreach ( $breakpoints as $bp ) {
622 if ( ! empty( $box_shadow[ $bp['id'] ] ) && is_array( $box_shadow[ $bp['id'] ] ) ) {
623 $is_responsive = true;
624 break;
625 }
626 }
627
628 if ( ! $is_responsive ) {
629 if ( empty( $box_shadow ) ) {
630 return '';
631 }
632 $declarations = \gutenify\Style_Helpers::custom_box_shadow_control( $box_shadow );
633 return $declarations ? $selector . '{' . $declarations . '}' : '';
634 }
635
636 if ( is_array( $custom_breakpoints ) ) {
637 foreach ( $custom_breakpoints as $custom_bp ) {
638 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
639 $breakpoints[] = array(
640 'id' => (string) $custom_bp['id'],
641 'width' => (int) $custom_bp['width'],
642 );
643 }
644 }
645 }
646
647 usort(
648 $breakpoints,
649 function ( $a, $b ) {
650 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
651 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
652 return $width_b <=> $width_a;
653 }
654 );
655
656 $css = '';
657
658 foreach ( $breakpoints as $bp ) {
659 $values = ! empty( $box_shadow[ $bp['id'] ] ) && is_array( $box_shadow[ $bp['id'] ] ) ? $box_shadow[ $bp['id'] ] : array();
660 if ( empty( $values ) ) {
661 continue;
662 }
663
664 $declarations = \gutenify\Style_Helpers::custom_box_shadow_control( $values );
665 if ( ! $declarations ) {
666 continue;
667 }
668
669 if ( null === $bp['width'] ) {
670 $css .= $selector . '{' . $declarations . '}';
671 } else {
672 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
673 }
674 }
675
676 return $css;
677 }
678
679 /**
680 * Generates dynamic `flex-grow`/`flex-shrink`/`flex-basis`/`align-self`/
681 * `order`/`cursor` CSS for the block, including any block-level custom
682 * breakpoints. Normal-only — these are static layout/interaction
683 * concerns, not state-driven effects, so there's no Hover/Active/Focus
684 * bucket to read (same reasoning as get_transition_with_media()).
685 *
686 * @param string $selector CSS selector.
687 * @param array $layout `advancedStyling.layout.normal` bucket.
688 * @param array $custom_breakpoints Optional block-level custom breakpoints.
689 * @return string CSS styles.
690 */
691 public static function get_layout_with_media( $selector, $layout, $custom_breakpoints = array() ) {
692 if ( empty( $layout ) || ! is_array( $layout ) ) {
693 return '';
694 }
695
696 $breakpoints = array(
697 array(
698 'id' => 'desktop',
699 'width' => null,
700 ),
701 array(
702 'id' => 'tablet',
703 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
704 ),
705 array(
706 'id' => 'mobile',
707 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
708 ),
709 );
710
711 $is_responsive = false;
712 foreach ( $breakpoints as $bp ) {
713 if ( ! empty( $layout[ $bp['id'] ] ) && is_array( $layout[ $bp['id'] ] ) ) {
714 $is_responsive = true;
715 break;
716 }
717 }
718
719 if ( ! $is_responsive ) {
720 $declarations = \gutenify\Style_Helpers::layout_control( $layout );
721 return $declarations ? $selector . '{' . $declarations . '}' : '';
722 }
723
724 if ( is_array( $custom_breakpoints ) ) {
725 foreach ( $custom_breakpoints as $custom_bp ) {
726 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
727 $breakpoints[] = array(
728 'id' => (string) $custom_bp['id'],
729 'width' => (int) $custom_bp['width'],
730 );
731 }
732 }
733 }
734
735 usort(
736 $breakpoints,
737 function ( $a, $b ) {
738 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
739 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
740 return $width_b <=> $width_a;
741 }
742 );
743
744 $css = '';
745
746 foreach ( $breakpoints as $bp ) {
747 $values = ! empty( $layout[ $bp['id'] ] ) && is_array( $layout[ $bp['id'] ] ) ? $layout[ $bp['id'] ] : array();
748
749 $declarations = \gutenify\Style_Helpers::layout_control( $values );
750 if ( ! $declarations ) {
751 continue;
752 }
753
754 if ( null === $bp['width'] ) {
755 $css .= $selector . '{' . $declarations . '}';
756 } else {
757 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
758 }
759 }
760
761 return $css;
762 }
763
764 /**
765 * Generates `flex-direction`/`flex-wrap`/`gap`/`align-items`/
766 * `justify-content` CSS with media queries for desktop, tablet, mobile,
767 * and any block-level custom breakpoints. Independent longhands, same
768 * simple (non-cumulative) treatment as get_position_with_media()/
769 * get_box_shadow_with_media() — state-crossed (called via
770 * get_state_variants_css() from container_layout_style() below), unlike
771 * get_layout_with_media() above which is Normal-only.
772 *
773 * @param string $selector The CSS selector to apply the styles to.
774 * @param array $container_layout The stored `advancedStyling.containerLayout.normal` value (or a state bucket, when called from get_state_variants_css()).
775 * @param array $custom_breakpoints The block's `customBlockBreakpoints` attribute, if any.
776 * @return string The generated CSS string.
777 */
778 public static function get_container_layout_with_media( $selector, $container_layout, $custom_breakpoints = array() ) {
779 if ( empty( $container_layout ) || ! is_array( $container_layout ) ) {
780 return '';
781 }
782
783 $breakpoints = array(
784 array(
785 'id' => 'desktop',
786 'width' => null,
787 ),
788 array(
789 'id' => 'tablet',
790 'width' => (int) \gutenify\Style_Helpers::$tablet_max_width,
791 ),
792 array(
793 'id' => 'mobile',
794 'width' => (int) \gutenify\Style_Helpers::$mobile_max_width,
795 ),
796 );
797
798 $is_responsive = false;
799 foreach ( $breakpoints as $bp ) {
800 if ( ! empty( $container_layout[ $bp['id'] ] ) && is_array( $container_layout[ $bp['id'] ] ) ) {
801 $is_responsive = true;
802 break;
803 }
804 }
805
806 if ( ! $is_responsive ) {
807 $declarations = \gutenify\Style_Helpers::container_layout_control( $container_layout );
808 return $declarations ? $selector . '{' . $declarations . '}' : '';
809 }
810
811 if ( is_array( $custom_breakpoints ) ) {
812 foreach ( $custom_breakpoints as $custom_bp ) {
813 if ( ! empty( $custom_bp['id'] ) && isset( $custom_bp['width'] ) ) {
814 $breakpoints[] = array(
815 'id' => (string) $custom_bp['id'],
816 'width' => (int) $custom_bp['width'],
817 );
818 }
819 }
820 }
821
822 usort(
823 $breakpoints,
824 function ( $a, $b ) {
825 $width_a = null === $a['width'] ? PHP_INT_MAX : $a['width'];
826 $width_b = null === $b['width'] ? PHP_INT_MAX : $b['width'];
827 return $width_b <=> $width_a;
828 }
829 );
830
831 $css = '';
832
833 foreach ( $breakpoints as $bp ) {
834 $values = ! empty( $container_layout[ $bp['id'] ] ) && is_array( $container_layout[ $bp['id'] ] ) ? $container_layout[ $bp['id'] ] : array();
835
836 $declarations = \gutenify\Style_Helpers::container_layout_control( $values );
837 if ( ! $declarations ) {
838 continue;
839 }
840
841 if ( null === $bp['width'] ) {
842 $css .= $selector . '{' . $declarations . '}';
843 } else {
844 $css .= '@media only screen and (max-width: ' . (int) $bp['width'] . 'px) {' . $selector . '{' . $declarations . '}}';
845 }
846 }
847
848 return $css;
849 }
850
851 /**
852 * Generates Hover/Active/Focus-within CSS for one feature (position,
853 * transform, or effects) — the frontend mirror of the JS side's state
854 * handling in block-position/index.js (see that file's docblock for the
855 * full reasoning). States are crossed WITH the breakpoint axis — each
856 * state bucket can itself be responsive (desktop/tablet/mobile) — and
857 * "Focus" maps to `:focus-within` rather than literal `:focus`.
858 *
859 * Each state bucket is passed straight to `$with_media_callback`
860 * (Dynamic_Styles::get_position_with_media()/get_transform_with_media()/
861 * get_effects_with_media()) — the SAME function used for Normal, since it
862 * already shape-detects flat vs. responsive and handles either
863 * correctly; a state bucket that happens to be flat (no per-breakpoint
864 * override set) takes that function's flat branch automatically.
865 *
866 * For Transform/Effects specifically, this emits each state's own
867 * `transform`/`filter`/`backdrop-filter` value literally — it does NOT
868 * compose with Normal's value the way independent Position fields do
869 * (a Hover-only Scale fully replaces a Normal Translate while hovering,
870 * rather than combining with it) — documented limitation, not a bug;
871 * see the JS-side comment for what fixing it properly would require.
872 *
873 * @param string $selector The block's base CSS selector (e.g. `.gtfy-XXXX`).
874 * @param array $block_attrs `$block['attrs']` from render_block.
875 * @param array $instance_attrs `$instance->attributes` from render_block.
876 * @param string $feature_key 'position', 'transform', or 'effects' — the base attribute key; state buckets are this plus 'Hover'/'Active'/'Focus'.
877 * @param array $custom_breakpoints The block's `customBlockBreakpoints` attribute, if any.
878 * @param callable $with_media_callback get_position_with_media()/get_transform_with_media()/get_effects_with_media(), called as ($selector, $bucket, $custom_breakpoints).
879 * @return string The generated CSS string.
880 */
881 public static function get_state_variants_css( $selector, $block_attrs, $instance_attrs, $feature_key, $custom_breakpoints, $with_media_callback ) {
882 $states = array(
883 'hover' => ':hover',
884 'active' => ':active',
885 'focusWithin' => ':focus-within',
886 );
887
888 $css = '';
889
890 foreach ( $states as $state_key => $pseudo ) {
891 $bucket = ! empty( $instance_attrs['advancedStyling'][ $feature_key ][ $state_key ] ) ? $instance_attrs['advancedStyling'][ $feature_key ][ $state_key ] : array();
892 if ( ! empty( $block_attrs['advancedStyling'][ $feature_key ][ $state_key ] ) ) {
893 $bucket = wp_parse_args( $block_attrs['advancedStyling'][ $feature_key ][ $state_key ], $bucket );
894 }
895
896 if ( empty( $bucket ) ) {
897 continue;
898 }
899
900 $css .= call_user_func( $with_media_callback, $selector . $pseudo, $bucket, $custom_breakpoints );
901 }
902
903 return $css;
904 }
905 }
906