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

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

334 lines 12.5 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 * Marquee block (gutsliders/marquee + gutsliders/marquee-item): page output.
12 *
13 * - The items are printed as they were rendered (they are already safe); running
14 * them through wp_kses_post() removed SVG icons, embeds and forms.
15 * - The repeated copy is hidden from screen readers and keyboards (`inert`) and
16 * carries no duplicate ids; frontend.js adds copies until the strip fills the screen.
17 * - Speed can be set in pixels per second (the same on every screen); blocks saved
18 * before keep their "one loop every N seconds".
19 * - A linked item used to wrap its content in `<a>`, so buttons inside made invalid
20 * nested links; it becomes a `<div>` with a stretched link behind the content.
21 * - Item background images become real `<img>` elements (sizes, lazy loading).
22 * - Rows (every other one reversed), separators between items, designs, fade edges,
23 * item width / alignment, and the motion options read by engine.js.
24 *
25 * @package GutSlider\Blocks
26 * @since 3.1.1
27 */
28 final class Marquee {
29
30 const DIRECTIONS = array( 'left', 'right', 'up', 'down' );
31 const UNITS = array( 'px', 'em', 'rem', 'vh', 'vw', '%' );
32
33 /**
34 * Constructor.
35 */
36 public function __construct() {
37 add_filter( 'render_block_gutsliders/marquee-item', array( $this, 'item' ), 10, 2 );
38 }
39
40 /** Designs, separators and item alignments offered. */
41 const DESIGNS = array( 'logos', 'ticker', 'testimonials', 'gallery', 'tags', 'announcement' );
42 const SEPARATORS = array(
43 'dot' => '•',
44 'star' => '✦',
45 'slash' => '/',
46 'diamond' => '◆',
47 'dash' => '—',
48 );
49 const ALIGNS = array( 'start', 'center', 'end', 'stretch' );
50
51 /**
52 * The marquee.
53 *
54 * @param array $attributes Block attributes.
55 * @param string $content Rendered items (all of them).
56 * @param \WP_Block|null $block Block, to render the items one by one (rows, separators).
57 * @return string HTML.
58 */
59 public static function render( $attributes, $content, $block = null ) {
60 $unique_id = isset( $attributes['uniqueId'] ) ? sanitize_html_class( (string) $attributes['uniqueId'] ) : '';
61 $duration = isset( $attributes['speed'] ) ? max( 1, (int) $attributes['speed'] ) : 30;
62 $speed_px = isset( $attributes['speedPx'] ) ? max( 0, (float) $attributes['speedPx'] ) : 0;
63 $direction = isset( $attributes['direction'] ) && in_array( $attributes['direction'], self::DIRECTIONS, true ) ? $attributes['direction'] : 'left';
64 $vertical = 'vertical' === ( $attributes['orientation'] ?? 'horizontal' );
65 $orientation = $vertical ? 'vertical' : 'horizontal';
66 $pause_btn = ! isset( $attributes['pauseButton'] ) || ! empty( $attributes['pauseButton'] );
67 $rows = isset( $attributes['rows'] ) ? max( 1, min( 3, (int) $attributes['rows'] ) ) : 1;
68 $design = isset( $attributes['design'] ) && in_array( $attributes['design'], self::DESIGNS, true ) ? $attributes['design'] : '';
69 $separator = isset( $attributes['separator'] ) && isset( self::SEPARATORS[ $attributes['separator'] ] ) ? $attributes['separator'] : '';
70 $item_align = isset( $attributes['itemAlign'] ) && in_array( $attributes['itemAlign'], self::ALIGNS, true ) ? $attributes['itemAlign'] : '';
71 $item_width = isset( $attributes['itemWidth'] ) ? max( 0, (int) $attributes['itemWidth'] ) : 0;
72 $fade = ! empty( $attributes['fadeEdges'] );
73 $fade_size = isset( $attributes['fadeSize'] ) ? max( 0, min( 400, (int) $attributes['fadeSize'] ) ) : 80;
74
75 // Hover: pause (also the older "Pause on Hover") or slow down.
76 $hover = isset( $attributes['hoverEffect'] ) && in_array( $attributes['hoverEffect'], array( 'pause', 'slow' ), true ) ? $attributes['hoverEffect'] : '';
77 if ( ! isset( $attributes['hoverEffect'] ) && ! empty( $attributes['pauseOnHover'] ) ) {
78 $hover = 'pause';
79 }
80
81 // Responsive gap and height as custom properties (style.scss reads them).
82 $vars = array();
83 foreach ( array( 'desk', 'tab', 'mob' ) as $bp ) {
84 $gap = self::range( $attributes, 'columnsGap', $bp );
85 if ( $gap ) {
86 $vars[] = '--marquee-gap-' . $bp . ':' . $gap;
87 }
88 if ( $vertical ) {
89 $height = self::range( $attributes, 'slideHeight', $bp );
90 if ( $height ) {
91 $vars[] = '--marquee-height-' . $bp . ':' . $height;
92 }
93 }
94 }
95 if ( $item_width ) {
96 $vars[] = '--marquee-item-width:' . $item_width . 'px';
97 }
98 if ( $fade ) {
99 $vars[] = '--marquee-fade:' . $fade_size . 'px';
100 }
101
102 $classes = array( 'marquee-wrapper', $unique_id, 'marquee-' . $orientation );
103 if ( $design ) {
104 $classes[] = 'gs-mq-design-' . $design;
105 }
106 if ( $rows > 1 ) {
107 $classes[] = 'has-rows';
108 }
109 if ( $fade ) {
110 $classes[] = 'has-fade';
111 }
112 if ( $item_width ) {
113 $classes[] = 'has-item-width';
114 }
115 if ( $item_align ) {
116 $classes[] = 'gs-mq-align-' . $item_align;
117 }
118
119 $wrapper = array( 'class' => implode( ' ', array_filter( $classes ) ) );
120 if ( $vars ) {
121 $wrapper['style'] = implode( ';', $vars );
122 }
123
124 $data = sprintf( ' data-marquee="%s"', esc_attr( $orientation ) );
125 if ( $speed_px > 0 ) {
126 $data .= sprintf( ' data-speed-px="%s"', esc_attr( (string) $speed_px ) );
127 }
128 if ( $hover ) {
129 $data .= sprintf( ' data-hover="%s"', esc_attr( $hover ) );
130 }
131 if ( ! empty( $attributes['dragToScrub'] ) ) {
132 $data .= ' data-drag="1"';
133 }
134 if ( 'linked' === ( $attributes['scrollMode'] ?? '' ) ) {
135 $data .= ' data-scroll="linked"';
136 }
137 if ( ! empty( $attributes['reverseOnScroll'] ) ) {
138 $data .= ' data-reverse-scroll="1"';
139 }
140 if ( ! empty( $attributes['scrollBoost'] ) ) {
141 $data .= ' data-scroll-boost="1"';
142 }
143 if ( $pause_btn ) {
144 $data .= sprintf(
145 ' data-label-pause="%1$s" data-label-play="%2$s"',
146 esc_attr__( 'Pause the moving content', 'slider-blocks' ),
147 esc_attr__( 'Play the moving content', 'slider-blocks' )
148 );
149 }
150
151 // The items, one by one when possible (rows and separators need them apart).
152 $items = array();
153 if ( $block instanceof \WP_Block && ! empty( $block->inner_blocks ) ) {
154 foreach ( $block->inner_blocks as $inner ) {
155 $items[] = (string) $inner->render();
156 }
157 } else {
158 $items[] = (string) $content;
159 }
160 $items = array_values( array_filter( $items, static fn( $html ) => '' !== trim( $html ) ) );
161
162 // Rows: the items dealt out in turn when there are enough, else every row shows them all.
163 $row_items = array();
164 for ( $r = 0; $r < $rows; $r++ ) {
165 $row_items[ $r ] = array();
166 }
167 foreach ( $items as $index => $html ) {
168 if ( count( $items ) >= $rows * 2 ) {
169 $row_items[ $index % $rows ][] = $html;
170 } else {
171 for ( $r = 0; $r < $rows; $r++ ) {
172 $row_items[ $r ][] = $html;
173 }
174 }
175 }
176
177 $sep = $separator ? sprintf( '<span class="marquee-sep is-%1$s" aria-hidden="true">%2$s</span>', esc_attr( $separator ), esc_html( self::SEPARATORS[ $separator ] ) ) : '';
178
179 $html = '';
180 foreach ( $row_items as $r => $list ) {
181 // Every other row goes the other way.
182 $row_direction = $direction;
183 if ( 1 === $r % 2 && empty( $attributes['sameDirection'] ) ) {
184 $row_direction = array( 'left' => 'right', 'right' => 'left', 'up' => 'down', 'down' => 'up' )[ $direction ];
185 }
186 if ( $vertical ) {
187 $animation = in_array( $row_direction, array( 'up', 'left' ), true ) ? 'marquee-scroll-up' : 'marquee-scroll-down';
188 } else {
189 $animation = 'marquee-scroll-' . ( 'right' === $row_direction ? 'right' : 'left' );
190 }
191
192 $container_classes = array( 'marquee-container', 'marquee-' . $orientation );
193 if ( 'pause' === $hover ) {
194 $container_classes[] = 'pause-on-hover';
195 }
196
197 // The separator also follows the last item, so it sits between copies too.
198 $copy_html = $sep ? implode( $sep, $list ) . $sep : implode( '', $list );
199
200 $html .= sprintf(
201 '<div class="%1$s" data-direction="%2$s" style="--animation-duration:%3$ds;--animation-name:%4$s"><div class="marquee-content">%5$s</div><div class="marquee-content is-copy" aria-hidden="true" inert>%6$s</div></div>',
202 esc_attr( implode( ' ', $container_classes ) ),
203 esc_attr( $row_direction ),
204 $duration,
205 esc_attr( $animation ),
206 $copy_html,
207 self::copy( $copy_html )
208 );
209 }
210
211 return sprintf( '<div %1$s%2$s>%3$s</div>', get_block_wrapper_attributes( $wrapper ), $data, $html );
212 }
213
214 /**
215 * A repeat of the items: no ids (they must stay unique on the page).
216 *
217 * @param string $html Items HTML.
218 * @return string
219 */
220 public static function copy( $html ) {
221 return (string) preg_replace( '/(<[a-z][a-z0-9-]*\b[^>]*?)\s+id=("[^"]*"|\'[^\']*\')/i', '$1', $html );
222 }
223
224 /**
225 * A responsive range value with its unit, or ''.
226 *
227 * @param array $attributes Attributes.
228 * @param string $name Control name.
229 * @param string $bp desk|tab|mob.
230 * @return string
231 */
232 private static function range( $attributes, $name, $bp ) {
233 $ranges = isset( $attributes[ $name . 'Ranges' ] ) && is_array( $attributes[ $name . 'Ranges' ] ) ? $attributes[ $name . 'Ranges' ] : array();
234 $units = isset( $attributes[ $name . 'Units' ] ) && is_array( $attributes[ $name . 'Units' ] ) ? $attributes[ $name . 'Units' ] : array();
235 if ( ! isset( $ranges[ $bp ] ) || ! is_numeric( $ranges[ $bp ] ) ) {
236 return '';
237 }
238 $unit = isset( $units[ $bp ] ) && in_array( $units[ $bp ], self::UNITS, true ) ? $units[ $bp ] : 'px';
239 return (float) $ranges[ $bp ] . $unit;
240 }
241
242 /**
243 * An item: its link behind the content (no nested links), and no empty
244 * `background-image:url()` for items without an image.
245 *
246 * @param string $content Item HTML.
247 * @param array $block Parsed block.
248 * @return string
249 */
250 public function item( $content, $block ) {
251 $content = (string) $content;
252 $attrs = $block['attrs'] ?? array();
253
254 if ( ! preg_match( '/<(a|div)\b[^>]*\bclass="marquee-container-outer[^"]*"[^>]*>/', $content, $match, PREG_OFFSET_CAPTURE ) ) {
255 return $content;
256 }
257 $open_tag = $match[0][0];
258 $open_at = $match[0][1];
259 $is_link = 'a' === $match[1][0];
260 $has_img = ! empty( $attrs['image']['url'] );
261
262 $tags = new \WP_HTML_Tag_Processor( $open_tag );
263 $tags->next_tag();
264 $style = (string) $tags->get_attribute( 'style' );
265 $classic = 'classic' === ( $attrs['bgType'] ?? 'classic' );
266 $strip = static fn( $css ) => trim( (string) preg_replace( '/background-(image|size|position|repeat)\s*:\s*(url\([^)]*\)|[^;]*)\s*;?/', '', $css ), "; \t\n" );
267
268 // Background image as a real image: responsive sizes, lazy loading, focal point.
269 $img = '';
270 $image_id = isset( $attrs['image']['id'] ) ? absint( $attrs['image']['id'] ) : 0;
271 if ( $classic && empty( $attrs['enableResponsiveBg'] ) && $image_id && wp_attachment_is_image( $image_id ) ) {
272 $focus = isset( $attrs['focusPoint']['x'], $attrs['focusPoint']['y'] ) ? $attrs['focusPoint'] : array( 'x' => 0.5, 'y' => 0.5 );
273 $img = (string) wp_get_attachment_image(
274 $image_id,
275 'large',
276 false,
277 array(
278 'class' => 'gutslider-slide-bg',
279 'alt' => '',
280 'loading' => 'lazy',
281 'decoding' => 'async',
282 'style' => sprintf( 'object-position:%s%% %s%%', round( (float) $focus['x'] * 100, 2 ), round( (float) $focus['y'] * 100, 2 ) ),
283 )
284 );
285 if ( $img ) {
286 $style = $strip( $style );
287 }
288 }
289 if ( $classic && ! $has_img ) {
290 $style = $strip( $style );
291 }
292
293 if ( ! $is_link && ! $img && $style === (string) $tags->get_attribute( 'style' ) ) {
294 return $content;
295 }
296
297 $link = '';
298 if ( $is_link ) {
299 $link_attrs = '';
300 foreach ( (array) $tags->get_attribute_names_with_prefix( '' ) as $name ) {
301 if ( in_array( $name, array( 'class', 'style' ), true ) ) {
302 continue;
303 }
304 $value = $tags->get_attribute( $name );
305 $link_attrs .= true === $value ? ' ' . esc_attr( $name ) : sprintf( ' %s="%s"', esc_attr( $name ), esc_attr( (string) $value ) );
306 }
307 $label = '';
308 if ( preg_match( '/<(h[1-6]|p)\b[^>]*>(.*?)<\/\1>/is', $content, $text ) ) {
309 $label = trim( wp_strip_all_tags( $text[2] ) );
310 }
311 $link = sprintf(
312 '<a class="gutslider-slide-link"%1$s><span class="gutslider-sr-only">%2$s</span></a>',
313 $link_attrs,
314 esc_html( '' !== $label ? $label : __( 'Open link', 'slider-blocks' ) )
315 );
316 }
317
318 $new_open = sprintf(
319 '<div class="%1$s"%2$s>',
320 esc_attr( trim( (string) $tags->get_attribute( 'class' ) . ( $is_link ? ' has-slide-link' : '' ) ) ),
321 '' !== $style ? ' style="' . esc_attr( $style ) . '"' : ''
322 );
323
324 $html = substr_replace( $content, $new_open . $link . $img, $open_at, strlen( $open_tag ) );
325 if ( $is_link ) {
326 $close_at = strrpos( $html, '</a>' );
327 if ( false !== $close_at ) {
328 $html = substr_replace( $html, '</div>', $close_at, 4 );
329 }
330 }
331 return $html;
332 }
333 }
334