PluginProbe
GutSlider – All in One Slider and Carousel Blocks for Gutenberg / 3.2.0
GutSlider – All in One Slider and Carousel Blocks for Gutenberg v3.2.0
3.2.0 3.1.0 3.0.0 2.13.2 2.13.1 2.13.0 trunk 1.0.0 2.1.0 2.10.0 2.10.1 2.11.0 2.11.1 2.11.2 2.11.3 2.11.4 2.12.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.1 2.5.3 2.5.4 2.5.5 All 57 releases
slider-blocks / includes / Blocks / FlexibleSlider.php

FlexibleSlider.php in GutSlider – All in One Slider and Carousel Blocks for Gutenberg 3.2.0, at includes/Blocks/FlexibleSlider.php

371 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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