| 1 |
<?php |
| 2 |
declare( strict_types=1 ); |
| 3 |
|
| 4 |
namespace GutSlider\Blocks; |
| 5 |
|
| 6 |
if ( ! defined( 'ABSPATH' ) ) { |
| 7 |
exit; |
| 8 |
} |
| 9 |
|
| 10 |
/** |
| 11 |
* Flexible slider (gutsliders/any-content + gutsliders/slide): page output fixes. |
| 12 |
* |
| 13 |
* The blocks save static markup and many are published, so nothing here changes what |
| 14 |
* is saved: the saved HTML of each slide is adjusted as it is printed. |
| 15 |
* |
| 16 |
* - A slide link wraps the whole slide in `<a>`; buttons and links inside it made |
| 17 |
* invalid nested links, which browsers re-arrange (content went missing). The slide |
| 18 |
* becomes a `<div>` with a stretched link behind its content. |
| 19 |
* - Background images become real `<img>` elements: responsive sizes, lazy loading for |
| 20 |
* slides after the first, the focal point kept. |
| 21 |
* - Slides without an image no longer print `background-image:url()`. |
| 22 |
* - A slide's fixed height becomes a minimum, so long content is never cut off. |
| 23 |
* - Background videos get the slide image as their poster; autoplay sliders get the |
| 24 |
* labels for their pause button (frontend.js). |
| 25 |
* - The newer settings (design, height mode, animations, background effect, tab |
| 26 |
* pagination, progress bar, deep links; per slide: position, text colour, width, |
| 27 |
* visibility, schedule) become classes and data attributes here. |
| 28 |
* |
| 29 |
* @package GutSlider\Blocks |
| 30 |
* @since 3.1.1 |
| 31 |
*/ |
| 32 |
final class FlexibleSlider { |
| 33 |
|
| 34 |
/** Sliders rendered so far in this request. */ |
| 35 |
private static $sliders = 0; |
| 36 |
|
| 37 |
/** Slides rendered so far in the current slider. */ |
| 38 |
private static $slides = 0; |
| 39 |
|
| 40 |
/** Settings of the slider being rendered, for its slides. */ |
| 41 |
private static $context = array(); |
| 42 |
|
| 43 |
/** Slide content positions. */ |
| 44 |
const POSITIONS = array( 'top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right' ); |
| 45 |
|
| 46 |
/** Values the new slider settings may take. */ |
| 47 |
const DESIGNS = array( 'hero', 'split', 'cards', 'fullscreen', 'stacked', 'ticker' ); |
| 48 |
const ANIMATIONS = array( 'fade-up', 'fade', 'zoom', 'slide-left' ); |
| 49 |
const BG_EFFECTS = array( 'kenburns', 'parallax' ); |
| 50 |
|
| 51 |
/** |
| 52 |
* Constructor. |
| 53 |
*/ |
| 54 |
public function __construct() { |
| 55 |
add_filter( 'render_block_data', array( $this, 'prepare' ), 10, 1 ); |
| 56 |
add_filter( 'render_block', array( $this, 'render' ), 20, 2 ); |
| 57 |
} |
| 58 |
|
| 59 |
/** |
| 60 |
* Before a slider renders: count it, and turn its slide height into a minimum. |
| 61 |
* |
| 62 |
* @param array $block Parsed block. |
| 63 |
* @return array |
| 64 |
*/ |
| 65 |
public function prepare( $block ) { |
| 66 |
if ( 'gutsliders/any-content' !== ( $block['blockName'] ?? '' ) ) { |
| 67 |
return $block; |
| 68 |
} |
| 69 |
|
| 70 |
++self::$sliders; |
| 71 |
self::$slides = 0; |
| 72 |
self::$context = $block['attrs'] ?? array(); |
| 73 |
|
| 74 |
$mode = $block['attrs']['heightMode'] ?? 'min'; |
| 75 |
$vertical = 'vertical' === ( $block['attrs']['direction'] ?? '' ); |
| 76 |
// Blocks saved before height modes wrote a fixed `height`: a minimum now (see below). |
| 77 |
if ( 'min' === $mode && ! $vertical && ! empty( $block['attrs']['blockStyle'] ) && is_string( $block['attrs']['blockStyle'] ) ) { |
| 78 |
$block['attrs']['blockStyle'] = self::min_height( $block['attrs']['blockStyle'] ); |
| 79 |
} |
| 80 |
|
| 81 |
return $block; |
| 82 |
} |
| 83 |
|
| 84 |
/** |
| 85 |
* `height` in the slide rules of a saved block style becomes `min-height` |
| 86 |
* (same as dynamic-style.js now writes it). |
| 87 |
* |
| 88 |
* @param string $css Block style. |
| 89 |
* @return string |
| 90 |
*/ |
| 91 |
public static function min_height( $css ) { |
| 92 |
return (string) preg_replace_callback( |
| 93 |
'/(\.gutslider-[\w-]+\s+\.swiper-slide\s*\{)([^}]*)\}/', |
| 94 |
static function ( $match ) { |
| 95 |
return $match[1] . preg_replace( '/(?<![\w-])height\s*:/', 'min-height:', $match[2] ) . '}'; |
| 96 |
}, |
| 97 |
$css |
| 98 |
); |
| 99 |
} |
| 100 |
|
| 101 |
/** |
| 102 |
* Adjust a slide's or a slider's output. |
| 103 |
* |
| 104 |
* @param string $content Block HTML. |
| 105 |
* @param array $block Parsed block. |
| 106 |
* @return string |
| 107 |
*/ |
| 108 |
public function render( $content, $block ) { |
| 109 |
$name = $block['blockName'] ?? ''; |
| 110 |
|
| 111 |
if ( 'gutsliders/slide' === $name ) { |
| 112 |
$attrs = $block['attrs'] ?? array(); |
| 113 |
// Scheduled slides are left out outside their dates. |
| 114 |
if ( self::out_of_schedule( $attrs ) ) { |
| 115 |
return ''; |
| 116 |
} |
| 117 |
++self::$slides; |
| 118 |
return self::slide( (string) $content, $attrs ); |
| 119 |
} |
| 120 |
|
| 121 |
if ( 'gutsliders/any-content' === $name ) { |
| 122 |
return self::slider( (string) $content, $block['attrs'] ?? array() ); |
| 123 |
} |
| 124 |
|
| 125 |
return $content; |
| 126 |
} |
| 127 |
|
| 128 |
/** |
| 129 |
* The slider: classes for its design and settings, screen reader wording, pause |
| 130 |
* button labels (autoplay) and its deep link id. |
| 131 |
* |
| 132 |
* @param string $content Slider HTML. |
| 133 |
* @param array $attrs Attributes. |
| 134 |
* @return string |
| 135 |
*/ |
| 136 |
private static function slider( $content, $attrs ) { |
| 137 |
$tags = new \WP_HTML_Tag_Processor( $content ); |
| 138 |
if ( ! $tags->next_tag( array( 'class_name' => 'wp-block-gutsliders-any-content' ) ) ) { |
| 139 |
return $content; |
| 140 |
} |
| 141 |
|
| 142 |
// Same classes as flexClasses() in edit.js. |
| 143 |
$design = in_array( $attrs['design'] ?? '', self::DESIGNS, true ) ? $attrs['design'] : ''; |
| 144 |
$mode = $attrs['heightMode'] ?? 'min'; |
| 145 |
$animation = in_array( $attrs['contentAnimation'] ?? '', self::ANIMATIONS, true ) ? $attrs['contentAnimation'] : ''; |
| 146 |
$bg_effect = in_array( $attrs['bgEffect'] ?? '', self::BG_EFFECTS, true ) ? $attrs['bgEffect'] : ''; |
| 147 |
$autoplay = ! empty( $attrs['sliderOptions']['autoplay'] ); |
| 148 |
$classes = array_filter( |
| 149 |
array( |
| 150 |
$design ? 'gs-flex-design-' . $design : '', |
| 151 |
in_array( $mode, array( 'fixed', 'auto' ), true ) ? 'gs-height-' . $mode : '', |
| 152 |
'vertical' === ( $attrs['direction'] ?? '' ) ? 'gs-vertical' : '', |
| 153 |
$animation ? 'gs-anim-' . $animation : '', |
| 154 |
$bg_effect ? 'gs-bg-' . $bg_effect : '', |
| 155 |
// showPagination is on by default (not saved when on). |
| 156 |
! empty( $attrs['paginationTabs'] ) && ( $attrs['showPagination'] ?? true ) ? 'has-tab-pagination' : '', |
| 157 |
! empty( $attrs['showProgress'] ) && $autoplay ? 'has-progress' : '', |
| 158 |
) |
| 159 |
); |
| 160 |
foreach ( $classes as $class ) { |
| 161 |
$tags->add_class( $class ); |
| 162 |
} |
| 163 |
|
| 164 |
// Screen reader wording for Swiper's accessibility module. |
| 165 |
$tags->set_attribute( 'data-label-carousel', __( 'carousel', 'slider-blocks' ) ); |
| 166 |
$tags->set_attribute( 'data-label-slide', __( 'slide', 'slider-blocks' ) ); |
| 167 |
/* translators: 1: slide number, 2: number of slides. */ |
| 168 |
$tags->set_attribute( 'data-label-position', __( '%1$s of %2$s', 'slider-blocks' ) ); |
| 169 |
|
| 170 |
if ( $autoplay ) { |
| 171 |
$tags->set_attribute( 'data-label-pause', __( 'Pause slideshow', 'slider-blocks' ) ); |
| 172 |
$tags->set_attribute( 'data-label-play', __( 'Play slideshow', 'slider-blocks' ) ); |
| 173 |
} |
| 174 |
|
| 175 |
// Deep links: #{anchor or id}-{slide number}. |
| 176 |
if ( ! empty( $attrs['deepLink'] ) ) { |
| 177 |
$id = $tags->get_attribute( 'id' ); |
| 178 |
$tags->set_attribute( 'data-deep-link', is_string( $id ) && '' !== $id ? $id : (string) ( $attrs['uniqueId'] ?? '' ) ); |
| 179 |
} |
| 180 |
|
| 181 |
return $tags->get_updated_html(); |
| 182 |
} |
| 183 |
|
| 184 |
/** |
| 185 |
* Whether a slide is outside its schedule (site time zone). |
| 186 |
* |
| 187 |
* @param array $attrs Slide attributes. |
| 188 |
* @return bool |
| 189 |
*/ |
| 190 |
private static function out_of_schedule( $attrs ) { |
| 191 |
$start = isset( $attrs['scheduleStart'] ) ? (string) $attrs['scheduleStart'] : ''; |
| 192 |
$end = isset( $attrs['scheduleEnd'] ) ? (string) $attrs['scheduleEnd'] : ''; |
| 193 |
if ( '' === $start && '' === $end ) { |
| 194 |
return false; |
| 195 |
} |
| 196 |
try { |
| 197 |
$zone = wp_timezone(); |
| 198 |
$now = new \DateTimeImmutable( 'now', $zone ); |
| 199 |
if ( '' !== $start && $now < new \DateTimeImmutable( $start, $zone ) ) { |
| 200 |
return true; |
| 201 |
} |
| 202 |
if ( '' !== $end && $now > new \DateTimeImmutable( $end, $zone ) ) { |
| 203 |
return true; |
| 204 |
} |
| 205 |
} catch ( \Exception $e ) { |
| 206 |
return false; |
| 207 |
} |
| 208 |
return false; |
| 209 |
} |
| 210 |
|
| 211 |
/** |
| 212 |
* One slide. |
| 213 |
* |
| 214 |
* @param string $content Slide HTML. |
| 215 |
* @param array $attrs Attributes (saved, non-default ones). |
| 216 |
* @return string |
| 217 |
*/ |
| 218 |
private static function slide( $content, $attrs ) { |
| 219 |
// The slide's content box: <a> (linked slide) or <div>. |
| 220 |
if ( ! preg_match( '/<(a|div)\b[^>]*\bclass="swiper-container-outer[^"]*"[^>]*>/', $content, $match, PREG_OFFSET_CAPTURE ) ) { |
| 221 |
return $content; |
| 222 |
} |
| 223 |
$open_tag = $match[0][0]; |
| 224 |
$open_at = $match[0][1]; |
| 225 |
$is_link = 'a' === $match[1][0]; |
| 226 |
$bg_type = $attrs['bgType'] ?? 'classic'; |
| 227 |
$responsive = ! empty( $attrs['enableResponsiveBg'] ); |
| 228 |
$image = isset( $attrs['image'] ) && is_array( $attrs['image'] ) ? $attrs['image'] : array(); |
| 229 |
$image_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; |
| 230 |
$image_url = isset( $image['url'] ) ? (string) $image['url'] : ''; |
| 231 |
|
| 232 |
$tags = new \WP_HTML_Tag_Processor( $open_tag ); |
| 233 |
$tags->next_tag(); |
| 234 |
$style = (string) $tags->get_attribute( 'style' ); |
| 235 |
|
| 236 |
$before = ''; |
| 237 |
// Background image as a real image (not for responsive or gradient backgrounds). |
| 238 |
if ( 'classic' === $bg_type && ! $responsive && $image_id && wp_attachment_is_image( $image_id ) ) { |
| 239 |
$focus = isset( $attrs['focusPoint']['x'], $attrs['focusPoint']['y'] ) ? $attrs['focusPoint'] : array( 'x' => 0.5, 'y' => 0.5 ); |
| 240 |
$first = 1 === self::$slides; |
| 241 |
$img_html = wp_get_attachment_image( |
| 242 |
$image_id, |
| 243 |
'full', |
| 244 |
false, |
| 245 |
array( |
| 246 |
'class' => 'gutslider-slide-bg', |
| 247 |
// Decorative: the slide's own content says what it is about. |
| 248 |
'alt' => '', |
| 249 |
'loading' => $first ? 'eager' : 'lazy', |
| 250 |
'fetchpriority' => $first && 1 === self::$sliders ? 'high' : 'auto', |
| 251 |
'decoding' => 'async', |
| 252 |
'style' => sprintf( 'object-position:%s%% %s%%', round( (float) $focus['x'] * 100, 2 ), round( (float) $focus['y'] * 100, 2 ) ), |
| 253 |
) |
| 254 |
); |
| 255 |
// Parallax: the image moves slower than the slide. |
| 256 |
if ( $img_html && 'parallax' === ( self::$context['bgEffect'] ?? '' ) ) { |
| 257 |
$img_html = str_replace( '<img ', '<img data-swiper-parallax="35%" ', $img_html ); |
| 258 |
} |
| 259 |
if ( $img_html ) { |
| 260 |
$before .= $img_html; |
| 261 |
$style = self::strip_background_image( $style ); |
| 262 |
} |
| 263 |
} |
| 264 |
|
| 265 |
// No image at all: drop the empty `url()` and what goes with it. |
| 266 |
if ( 'classic' === $bg_type && '' === $image_url ) { |
| 267 |
$style = self::strip_background_image( $style ); |
| 268 |
} |
| 269 |
|
| 270 |
// A linked slide: a <div>, with the link stretched behind its content. |
| 271 |
$link = ''; |
| 272 |
if ( $is_link ) { |
| 273 |
$link_attrs = ''; |
| 274 |
foreach ( (array) $tags->get_attribute_names_with_prefix( '' ) as $name ) { |
| 275 |
if ( in_array( $name, array( 'class', 'style' ), true ) ) { |
| 276 |
continue; |
| 277 |
} |
| 278 |
$value = $tags->get_attribute( $name ); |
| 279 |
$link_attrs .= true === $value ? ' ' . esc_attr( $name ) : sprintf( ' %s="%s"', esc_attr( $name ), esc_attr( (string) $value ) ); |
| 280 |
} |
| 281 |
$label = self::slide_label( $content ); |
| 282 |
$link = sprintf( '<a class="gutslider-slide-link"%1$s><span class="gutslider-sr-only">%2$s</span></a>', $link_attrs, esc_html( $label ) ); |
| 283 |
} |
| 284 |
|
| 285 |
// Per-slide settings (same as slideClasses() / slideStyle() in slide.js). |
| 286 |
$classes = (string) $tags->get_attribute( 'class' ); |
| 287 |
$position = $attrs['contentPosition'] ?? ''; |
| 288 |
if ( in_array( $position, self::POSITIONS, true ) ) { |
| 289 |
$classes .= ' gs-pos-' . $position; |
| 290 |
} |
| 291 |
if ( in_array( $attrs['textColor'] ?? '', array( 'light', 'dark' ), true ) ) { |
| 292 |
$classes .= ' gs-text-' . $attrs['textColor']; |
| 293 |
} |
| 294 |
$width = isset( $attrs['contentMaxWidth'] ) ? (int) $attrs['contentMaxWidth'] : 0; |
| 295 |
if ( $width > 0 ) { |
| 296 |
$classes .= ' gs-has-width'; |
| 297 |
$style = trim( $style . ';--gs-content-width:' . $width . 'px', ';' ); |
| 298 |
} |
| 299 |
if ( $is_link ) { |
| 300 |
$classes .= ' has-slide-link'; |
| 301 |
} |
| 302 |
|
| 303 |
$new_open = sprintf( |
| 304 |
'<div class="%1$s"%2$s>', |
| 305 |
esc_attr( trim( $classes ) ), |
| 306 |
'' !== trim( $style ) ? ' style="' . esc_attr( $style ) . '"' : '' |
| 307 |
); |
| 308 |
|
| 309 |
$html = substr_replace( $content, $new_open . $link . $before, $open_at, strlen( $open_tag ) ); |
| 310 |
|
| 311 |
if ( $is_link ) { |
| 312 |
// The box's closing </a> is the last one in the slide. |
| 313 |
$close_at = strrpos( $html, '</a>' ); |
| 314 |
if ( false !== $close_at ) { |
| 315 |
$html = substr_replace( $html, '</div>', $close_at, 4 ); |
| 316 |
} |
| 317 |
} |
| 318 |
|
| 319 |
// The slide itself: its name (tab pagination) and where it is hidden. |
| 320 |
$slide_tag = new \WP_HTML_Tag_Processor( $html ); |
| 321 |
if ( $slide_tag->next_tag( array( 'class_name' => 'swiper-slide' ) ) ) { |
| 322 |
$title = isset( $attrs['slideTitle'] ) && '' !== $attrs['slideTitle'] ? (string) $attrs['slideTitle'] : self::slide_label( $content, '' ); |
| 323 |
if ( '' !== $title ) { |
| 324 |
$slide_tag->set_attribute( 'data-title', $title ); |
| 325 |
} |
| 326 |
$hide = array_intersect( (array) ( $attrs['hideOn'] ?? array() ), array( 'desktop', 'tablet', 'mobile' ) ); |
| 327 |
if ( $hide ) { |
| 328 |
$slide_tag->set_attribute( 'data-hide', implode( ' ', $hide ) ); |
| 329 |
} |
| 330 |
if ( 'parallax' === ( self::$context['bgEffect'] ?? '' ) ) { |
| 331 |
$slide_tag->add_class( 'has-parallax' ); |
| 332 |
} |
| 333 |
$html = $slide_tag->get_updated_html(); |
| 334 |
} |
| 335 |
|
| 336 |
// Background video: the slide image as its poster. |
| 337 |
if ( 'video' === $bg_type && '' !== $image_url && false === strpos( $html, 'poster=' ) ) { |
| 338 |
$html = preg_replace( '/<video\b(?=[^>]*class="gutslider-video")/', '<video poster="' . esc_url( $image_url ) . '"', $html, 1 ); |
| 339 |
} |
| 340 |
|
| 341 |
return $html; |
| 342 |
} |
| 343 |
|
| 344 |
/** |
| 345 |
* Remove the background image declarations from an inline style. |
| 346 |
* |
| 347 |
* @param string $style Inline style. |
| 348 |
* @return string |
| 349 |
*/ |
| 350 |
private static function strip_background_image( $style ) { |
| 351 |
$style = preg_replace( '/background-(image|size|position|repeat)\s*:\s*(url\([^)]*\)|[^;]*)\s*;?/', '', $style ); |
| 352 |
return trim( (string) $style, "; \t\n" ); |
| 353 |
} |
| 354 |
|
| 355 |
/** |
| 356 |
* A name for a slide link: the slide's first heading, else a generic label. |
| 357 |
* |
| 358 |
* @param string $content Slide HTML. |
| 359 |
* @return string |
| 360 |
*/ |
| 361 |
private static function slide_label( $content, $fallback = null ) { |
| 362 |
if ( preg_match( '/<h[1-6]\b[^>]*>(.*?)<\/h[1-6]>/is', $content, $heading ) ) { |
| 363 |
$text = trim( wp_strip_all_tags( $heading[1] ) ); |
| 364 |
if ( '' !== $text ) { |
| 365 |
return $text; |
| 366 |
} |
| 367 |
} |
| 368 |
return null === $fallback ? __( 'Open this slide', 'slider-blocks' ) : $fallback; |
| 369 |
} |
| 370 |
} |
| 371 |
|