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 / BeforeAfter.php

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

487 lines 19.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 * Before After block: server output.
12 *
13 * The block still saves its original markup (so every saved block stays valid), but
14 * the page is built here: real, responsive images that show without JavaScript, and
15 * only the settings the slider needs. The saved image data is never printed.
16 *
17 * Class names match the ones the block's style settings target (labels, caption,
18 * `__rcs-handle-*`), so saved styles keep working. compare.js makes it interactive.
19 *
20 * @package GutSlider\Blocks
21 * @since 3.1.1
22 */
23 final class BeforeAfter {
24
25 /** Labels positions and animations the block offers. */
26 const V_POSITIONS = array( 'icgbv__top', 'icgbv__center', 'icgbv__bottom' );
27 const H_POSITIONS = array( 'icgbh__left', 'icgbh__center', 'icgbh__right' );
28 const ANIMATIONS = array( 'icgb__fade', 'icgb__zoom_in', 'icgb__zoom_out', 'icgb__slide_down', 'icgb__slide_up', 'icgb__slide_left', 'icgb__slide_right' );
29 const CAPTION_TAGS = array( 'span', 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );
30
31 const DESIGNS = array( 'minimal', 'glass', 'card', 'split', 'dark' );
32 const RATIOS = array( '16/9', '3/2', '4/3', '1/1', '3/4', '9/16', '21/9' );
33 const HANDLE_STYLES = array( 'circle', 'arrows', 'line', 'pill', 'icon' );
34 const MODES = array( 'slide', 'fade', 'toggle' );
35 const CTA_POSITIONS = array( 'bottom-left', 'bottom-center', 'bottom-right', 'top-left' );
36
37 /** Comparisons printed so far in this request (the first one may be above the fold). */
38 private static $count = 0;
39
40 /**
41 * Settings with the block's defaults filled in (unchanged settings aren't saved).
42 *
43 * @param array $attributes Block attributes.
44 * @return array
45 */
46 public static function normalize( $attributes ) {
47 $slider = wp_parse_args(
48 isset( $attributes['sliderOptions'] ) && is_array( $attributes['sliderOptions'] ) ? $attributes['sliderOptions'] : array(),
49 array(
50 'showLabels' => true,
51 'labelsVisibility' => 'always',
52 'hoverAnimation' => 'icgb__fade',
53 'labelVPosition' => 'icgbv__center',
54 'labelHPosition' => 'icgbh__center',
55 'beforeLabel' => 'Before',
56 'afterLabel' => 'After',
57 )
58 );
59 $compare = wp_parse_args(
60 isset( $attributes['comparisonOptions'] ) && is_array( $attributes['comparisonOptions'] ) ? $attributes['comparisonOptions'] : array(),
61 array(
62 'disableSliding' => false,
63 'onlyHandleDraggable' => false,
64 'slideMode' => 'horizontal',
65 'swipeMode' => 'drag',
66 'initialPosition' => 50,
67 )
68 );
69
70 $pick = static function ( $key, $allowed, $fallback ) use ( $attributes ) {
71 return isset( $attributes[ $key ] ) && in_array( $attributes[ $key ], $allowed, true ) ? $attributes[ $key ] : $fallback;
72 };
73
74 // More comparisons (carousel).
75 $pairs = array();
76 if ( ! empty( $attributes['pairs'] ) && is_array( $attributes['pairs'] ) ) {
77 foreach ( $attributes['pairs'] as $pair ) {
78 $pair = array(
79 'before' => self::image( $pair['beforeImage'] ?? array() ),
80 'after' => self::image( $pair['afterImage'] ?? array() ),
81 'caption' => isset( $pair['caption'] ) ? (string) $pair['caption'] : '',
82 );
83 if ( '' !== $pair['before']['url'] && '' !== $pair['after']['url'] ) {
84 $pairs[] = $pair;
85 }
86 }
87 }
88
89 return array(
90 'uniqueId' => isset( $attributes['uniqueId'] ) ? (string) $attributes['uniqueId'] : '',
91 'before' => self::image( $attributes['beforeImage'] ?? array() ),
92 'after' => self::image( $attributes['afterImage'] ?? array() ),
93 'slider' => $slider,
94 'compare' => $compare,
95 'showCaption' => ! empty( $attributes['showCaption'] ),
96 'caption' => isset( $attributes['sliderCaption'] ) ? (string) $attributes['sliderCaption'] : '',
97 'captionTag' => $pick( 'captionTag', self::CAPTION_TAGS, 'span' ),
98 'design' => $pick( 'design', self::DESIGNS, '' ),
99 'ratio' => $pick( 'aspectRatio', self::RATIOS, '' ),
100 'imageSize' => $pick( 'imageSize', array( 'large', 'full' ), 'full' ),
101 'handleStyle' => $pick( 'handleStyle', self::HANDLE_STYLES, 'circle' ),
102 'handleText' => isset( $attributes['handleText'] ) ? (string) $attributes['handleText'] : '',
103 'handleIcon' => isset( $attributes['handleIcon'] ) ? (string) $attributes['handleIcon'] : '',
104 'mode' => $pick( 'compareMode', self::MODES, 'slide' ),
105 'introHint' => ! empty( $attributes['introHint'] ),
106 'autoPlay' => ! empty( $attributes['autoPlay'] ),
107 'fullscreen' => ! empty( $attributes['fullscreen'] ),
108 'ctaText' => isset( $attributes['ctaText'] ) ? (string) $attributes['ctaText'] : '',
109 'ctaUrl' => isset( $attributes['ctaUrl'] ) ? (string) $attributes['ctaUrl'] : '',
110 'ctaNewTab' => ! empty( $attributes['ctaNewTab'] ),
111 'ctaPosition' => $pick( 'ctaPosition', self::CTA_POSITIONS, 'bottom-center' ),
112 'video' => 'video' === ( $attributes['mediaType'] ?? 'image' ),
113 'beforeVideo' => self::image( $attributes['beforeVideo'] ?? array() ),
114 'afterVideo' => self::image( $attributes['afterVideo'] ?? array() ),
115 'pairs' => $pairs,
116 'pairThumbs' => ! isset( $attributes['pairThumbs'] ) || ! empty( $attributes['pairThumbs'] ),
117 );
118 }
119
120 /**
121 * The parts of a saved image (or video) the page needs: id, url, alt.
122 *
123 * @param mixed $image Saved image.
124 * @return array{id:int,url:string,alt:string}
125 */
126 private static function image( $image ) {
127 $image = is_array( $image ) ? $image : array();
128 return array(
129 'id' => isset( $image['id'] ) ? absint( $image['id'] ) : 0,
130 'url' => isset( $image['url'] ) ? (string) $image['url'] : '',
131 'alt' => isset( $image['alt'] ) ? (string) $image['alt'] : '',
132 );
133 }
134
135 /**
136 * A label as shown: the untouched English default is translated.
137 *
138 * @param string $label Saved label.
139 * @param string $which before|after.
140 * @return string
141 */
142 public static function label( $label, $which ) {
143 if ( 'Before' === $label && 'before' === $which ) {
144 return __( 'Before', 'slider-blocks' );
145 }
146 if ( 'After' === $label && 'after' === $which ) {
147 return __( 'After', 'slider-blocks' );
148 }
149 return (string) $label;
150 }
151
152 /**
153 * Block output.
154 *
155 * @param array $attributes Block attributes.
156 * @param string $content Saved markup (only its HTML anchor is used).
157 * @return string
158 */
159 public static function render( $attributes, $content = '' ) {
160 $atts = self::normalize( $attributes );
161
162 // The first comparison (images or videos) must be complete.
163 $first = $atts['video']
164 ? array( 'before' => $atts['beforeVideo'], 'after' => $atts['afterVideo'], 'caption' => '' )
165 : array( 'before' => $atts['before'], 'after' => $atts['after'], 'caption' => '' );
166 if ( '' === $first['before']['url'] || '' === $first['after']['url'] ) {
167 return '';
168 }
169
170 // Several comparisons make a carousel (images only).
171 $pairs = $atts['video'] ? array() : $atts['pairs'];
172
173 $classes = array( $atts['uniqueId'] );
174 if ( $atts['design'] ) {
175 $classes[] = 'gs-ba-design-' . $atts['design'];
176 }
177 if ( $pairs ) {
178 $classes[] = 'has-pairs';
179 }
180 $wrapper = array( 'class' => implode( ' ', array_filter( $classes ) ) );
181 // The HTML anchor is saved in the markup, not in the attributes.
182 if ( preg_match( '/^\s*<div\b[^>]*\bid="([^"]+)"/', (string) $content, $match ) ) {
183 $wrapper['id'] = $match[1];
184 }
185
186 $caption = $atts['showCaption'] ? $atts['caption'] : '';
187
188 if ( ! $pairs ) {
189 return sprintf(
190 '<div %1$s>%2$s</div>',
191 get_block_wrapper_attributes( $wrapper ),
192 self::figure( $first, $atts, $caption, true )
193 );
194 }
195
196 // Carousel: Swiper only loads for blocks that need it.
197 wp_enqueue_script( 'gutslider-swiper-script' );
198 wp_enqueue_style( 'gutslider-swiper-style' );
199
200 $all = array_merge( array( array_merge( $first, array( 'caption' => $caption ) ) ), $pairs );
201 $slides = '';
202 foreach ( $all as $index => $pair ) {
203 $slides .= sprintf(
204 '<div class="swiper-slide" role="group" aria-label="%1$s">%2$s</div>',
205 /* translators: 1: comparison number, 2: number of comparisons. */
206 esc_attr( sprintf( __( '%1$d of %2$d', 'slider-blocks' ), $index + 1, count( $all ) ) ),
207 self::figure( $pair, $atts, $pair['caption'], 0 === $index )
208 );
209 }
210
211 $thumbs = '';
212 if ( $atts['pairThumbs'] ) {
213 foreach ( $all as $index => $pair ) {
214 $thumb = $pair['after']['id'] ? wp_get_attachment_image_url( $pair['after']['id'], 'thumbnail' ) : '';
215 $thumbs .= sprintf(
216 '<button type="button" class="gs-ba-thumbs__item%1$s" aria-label="%2$s"%3$s><img src="%4$s" alt="" loading="lazy" decoding="async" /></button>',
217 0 === $index ? ' is-active' : '',
218 /* translators: %d: comparison number. */
219 esc_attr( sprintf( __( 'Show comparison %d', 'slider-blocks' ), $index + 1 ) ),
220 0 === $index ? ' aria-current="true"' : '',
221 esc_url( $thumb ? $thumb : $pair['after']['url'] )
222 );
223 }
224 $thumbs = '<div class="gs-ba-thumbs">' . $thumbs . '</div>';
225 }
226
227 return sprintf(
228 '<div %1$s><div class="gs-ba-carousel swiper" aria-roledescription="%2$s"><div class="swiper-wrapper">%3$s</div><button type="button" class="gs-ba-carousel__nav is-prev" aria-label="%4$s"></button><button type="button" class="gs-ba-carousel__nav is-next" aria-label="%5$s"></button></div>%6$s</div>',
229 get_block_wrapper_attributes( $wrapper ),
230 esc_attr__( 'carousel', 'slider-blocks' ),
231 $slides,
232 esc_attr__( 'Previous comparison', 'slider-blocks' ),
233 esc_attr__( 'Next comparison', 'slider-blocks' ),
234 $thumbs
235 );
236 }
237
238 /**
239 * One comparison with its caption.
240 *
241 * @param array $pair before, after (id, url, alt).
242 * @param array $atts Normalised settings.
243 * @param string $caption Caption (HTML).
244 * @param bool $lead First comparison of the block.
245 * @return string HTML.
246 */
247 private static function figure( $pair, $atts, $caption, $lead ) {
248 $slider = $atts['slider'];
249 $compare = $atts['compare'];
250 $mode = $atts['mode'];
251 $portrait = 'slide' === $mode && 'vertical' === $compare['slideMode'];
252 $position = max( 0, min( 100, (float) $compare['initialPosition'] ) );
253 if ( 'toggle' === $mode ) {
254 $position = 100;
255 }
256
257 // Only the page's first comparison may hold its main image.
258 if ( $lead ) {
259 ++self::$count;
260 }
261 $eager = $lead && 1 === self::$count;
262
263 $root_classes = array(
264 'gutslider-compare-block',
265 'gs-ba',
266 $portrait ? 'is-vertical' : 'is-horizontal',
267 'is-' . $mode,
268 'has-handle-' . $atts['handleStyle'],
269 );
270 if ( $compare['disableSliding'] && 'toggle' !== $mode ) {
271 $root_classes[] = 'is-disabled';
272 }
273 if ( $compare['onlyHandleDraggable'] ) {
274 $root_classes[] = 'is-handle-only';
275 }
276 if ( $atts['ratio'] ) {
277 $root_classes[] = 'has-ratio';
278 }
279 if ( $atts['video'] ) {
280 $root_classes[] = 'is-video';
281 }
282
283 // Opacity of "after": fade follows the knob (right = after), toggle starts on "before".
284 $style = sprintf( '--gs-ba-pos:%1$s%%;--gs-ba-o:%2$s;', $position, 'fade' === $mode ? $position / 100 : 1 - $position / 100 );
285 if ( $atts['ratio'] ) {
286 $style .= 'aspect-ratio:' . $atts['ratio'] . ';';
287 }
288
289 $before_label = self::label( $slider['beforeLabel'], 'before' );
290 $after_label = self::label( $slider['afterLabel'], 'after' );
291
292 ob_start();
293 ?>
294 <figure class="gs-ba-figure">
295 <div
296 class="<?php echo esc_attr( implode( ' ', $root_classes ) ); ?>"
297 style="<?php echo esc_attr( $style ); ?>"
298 data-orientation="<?php echo $portrait ? 'vertical' : 'horizontal'; ?>"
299 data-position="<?php echo esc_attr( (string) $position ); ?>"
300 data-compare="<?php echo esc_attr( $mode ); ?>"
301 data-mode="<?php echo 'hover' === $compare['swipeMode'] ? 'hover' : 'drag'; ?>"
302 data-handle-only="<?php echo $compare['onlyHandleDraggable'] ? '1' : '0'; ?>"
303 data-disabled="<?php echo $compare['disableSliding'] ? '1' : '0'; ?>"
304 data-hint="<?php echo $atts['introHint'] ? '1' : '0'; ?>"
305 data-autoplay="<?php echo $atts['autoPlay'] ? '1' : '0'; ?>"
306 >
307 <?php
308 echo self::item( $pair['before'], 'before', $before_label, $atts, $eager, $eager ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
309 ?>
310 <div class="gs-ba__clip">
311 <?php echo self::item( $pair['after'], 'after', $after_label, $atts, $eager, false ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
312 </div>
313 <?php echo self::handle( $atts, $portrait, $position ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
314 <?php echo self::ui( $atts, $before_label, $after_label ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
315 </div>
316 <?php if ( '' !== trim( wp_strip_all_tags( $caption ) ) ) : ?>
317 <figcaption class="gutslider-caption">
318 <?php
319 printf(
320 '<%1$s class="gutslider-caption-text">%2$s</%1$s>',
321 tag_escape( $atts['captionTag'] ),
322 wp_kses_post( $caption )
323 );
324 ?>
325 </figcaption>
326 <?php endif; ?>
327 </figure>
328 <?php
329 return (string) ob_get_clean();
330 }
331
332 /**
333 * The divider handle (slide) or knob (fade).
334 *
335 * @param array $atts Normalised settings.
336 * @param bool $portrait Vertical slider.
337 * @param float $position Start position.
338 * @return string HTML.
339 */
340 private static function handle( $atts, $portrait, $position ) {
341 $disabled = $atts['compare']['disableSliding'] || 'toggle' === $atts['mode'];
342
343 if ( 'pill' === $atts['handleStyle'] ) {
344 $inner = '<div class="__rcs-handle-arrow"></div><span class="gs-ba__handle-text">' . esc_html( '' !== $atts['handleText'] ? $atts['handleText'] : __( 'Drag', 'slider-blocks' ) ) . '</span><div class="__rcs-handle-arrow"></div>';
345 } elseif ( 'icon' === $atts['handleStyle'] && '' !== $atts['handleIcon'] && function_exists( 'gutslider_display_icon' ) ) {
346 $inner = '<span class="gs-ba__handle-icon">' . wp_kses( gutslider_display_icon( $atts['handleIcon'] ), gutslider_allowed_svg_tags() ) . '</span>';
347 } else {
348 $inner = '<div class="__rcs-handle-arrow"></div><div class="__rcs-handle-arrow"></div>';
349 }
350
351 return sprintf(
352 '<div class="gs-ba__handle" role="slider" tabindex="%1$s" aria-label="%2$s" aria-orientation="%3$s" aria-valuemin="0" aria-valuemax="100" aria-valuenow="%4$s"%5$s><div class="__rcs-handle-root"><div class="__rcs-handle-line"></div><div class="__rcs-handle-button">%6$s</div><div class="__rcs-handle-line"></div></div></div>',
353 $disabled ? '-1' : '0',
354 'fade' === $atts['mode'] ? esc_attr__( 'Fade between the before and after images', 'slider-blocks' ) : esc_attr__( 'Drag to compare the before and after images', 'slider-blocks' ),
355 $portrait ? 'vertical' : 'horizontal',
356 esc_attr( (string) round( $position ) ),
357 $disabled ? ' aria-disabled="true"' : '',
358 $inner
359 );
360 }
361
362 /**
363 * Buttons over the comparison: toggle, play/pause, fullscreen, call to action.
364 *
365 * @param array $atts Normalised settings.
366 * @param string $before_label Before label.
367 * @param string $after_label After label.
368 * @return string HTML.
369 */
370 private static function ui( $atts, $before_label, $after_label ) {
371 $html = '';
372
373 if ( 'toggle' === $atts['mode'] ) {
374 $html .= sprintf(
375 '<div class="gs-ba__ui gs-ba__toggle" role="group" aria-label="%1$s"><button type="button" data-show="before" aria-pressed="true">%2$s</button><button type="button" data-show="after" aria-pressed="false">%3$s</button></div>',
376 esc_attr__( 'Show', 'slider-blocks' ),
377 esc_html( '' !== $before_label ? $before_label : __( 'Before', 'slider-blocks' ) ),
378 esc_html( '' !== $after_label ? $after_label : __( 'After', 'slider-blocks' ) )
379 );
380 }
381
382 if ( $atts['video'] ) {
383 $html .= sprintf(
384 '<button type="button" class="gs-ba__ui gs-ba__play" aria-pressed="true" aria-label="%1$s" data-label-play="%2$s" data-label-pause="%1$s"><span aria-hidden="true"></span></button>',
385 esc_attr__( 'Pause videos', 'slider-blocks' ),
386 esc_attr__( 'Play videos', 'slider-blocks' )
387 );
388 }
389
390 if ( $atts['fullscreen'] ) {
391 $html .= sprintf(
392 '<button type="button" class="gs-ba__ui gs-ba__fs" aria-pressed="false" aria-label="%1$s" data-label-open="%1$s" data-label-close="%2$s"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>',
393 esc_attr__( 'View fullscreen', 'slider-blocks' ),
394 esc_attr__( 'Exit fullscreen', 'slider-blocks' )
395 );
396 }
397
398 if ( '' !== $atts['ctaText'] && '' !== $atts['ctaUrl'] ) {
399 $html .= sprintf(
400 '<a class="gs-ba__ui gs-ba__cta is-%1$s" href="%2$s"%3$s>%4$s</a>',
401 esc_attr( $atts['ctaPosition'] ),
402 esc_url( $atts['ctaUrl'] ),
403 $atts['ctaNewTab'] ? ' target="_blank" rel="noopener noreferrer"' : '',
404 esc_html( $atts['ctaText'] )
405 );
406 }
407
408 return $html;
409 }
410
411 /**
412 * One side: its label and image (or video).
413 *
414 * @param array $media id, url, alt.
415 * @param string $which before|after.
416 * @param string $label Label text.
417 * @param array $atts Normalised settings.
418 * @param bool $eager Load right away.
419 * @param bool $priority May be fetched with high priority.
420 * @return string HTML.
421 */
422 private static function item( $media, $which, $label, $atts, $eager, $priority ) {
423 $slider = $atts['slider'];
424 $visibility = 'hover' === $slider['labelsVisibility'] ? 'hover' : 'always';
425 $portrait = 'slide' === $atts['mode'] && 'vertical' === $atts['compare']['slideMode'];
426
427 $label_classes = array( 'gutslider-label', $which . '-label', $portrait ? 'vertical' : 'horizontal' );
428 if ( 'hover' === $visibility && in_array( $slider['hoverAnimation'], self::ANIMATIONS, true ) ) {
429 $label_classes[] = $slider['hoverAnimation'];
430 }
431 $place = $portrait ? $slider['labelHPosition'] : $slider['labelVPosition'];
432 if ( in_array( $place, $portrait ? self::H_POSITIONS : self::V_POSITIONS, true ) ) {
433 $label_classes[] = $place;
434 }
435
436 if ( $atts['video'] ) {
437 $body = sprintf(
438 '<video class="gs-ba__img gs-ba__video" src="%1$s" muted loop playsinline autoplay preload="metadata" aria-label="%2$s"></video>',
439 esc_url( $media['url'] ),
440 esc_attr( wp_strip_all_tags( $label ) )
441 );
442 } else {
443 // The attachment's current alt text, else the saved one, else the label.
444 $alt = $media['id'] ? (string) get_post_meta( $media['id'], '_wp_attachment_image_alt', true ) : '';
445 if ( '' === $alt ) {
446 $alt = '' !== $media['alt'] ? $media['alt'] : wp_strip_all_tags( $label );
447 }
448 $loading = $eager ? 'eager' : 'lazy';
449
450 $body = $media['id'] && wp_attachment_is_image( $media['id'] )
451 ? wp_get_attachment_image(
452 $media['id'],
453 $atts['imageSize'],
454 false,
455 array(
456 'class' => 'gs-ba__img wp-image-' . $media['id'],
457 'alt' => $alt,
458 'draggable' => 'false',
459 'decoding' => 'async',
460 'loading' => $loading,
461 'fetchpriority' => $priority ? 'high' : 'auto',
462 )
463 )
464 : '';
465 if ( '' === $body ) {
466 $body = sprintf(
467 '<img class="gs-ba__img" src="%1$s" alt="%2$s" draggable="false" decoding="async" loading="%3$s" />',
468 esc_url( $media['url'] ),
469 esc_attr( $alt ),
470 esc_attr( $loading )
471 );
472 }
473 }
474
475 ob_start();
476 ?>
477 <div class="<?php echo esc_attr( 'gutslider-' . $which . ' gutslider-ba-item ' . $visibility ); ?>">
478 <?php if ( $slider['showLabels'] && '' !== $label && 'toggle' !== $atts['mode'] ) : ?>
479 <div class="<?php echo esc_attr( implode( ' ', $label_classes ) ); ?>" aria-hidden="true"><?php echo esc_html( $label ); ?></div>
480 <?php endif; ?>
481 <?php echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
482 </div>
483 <?php
484 return (string) ob_get_clean();
485 }
486 }
487