` with a stretched link behind the content.
* - Item background images become real `
![]()
` elements (sizes, lazy loading).
* - Rows (every other one reversed), separators between items, designs, fade edges,
* item width / alignment, and the motion options read by engine.js.
*
* @package GutSlider\Blocks
* @since 3.1.1
*/
final class Marquee {
const DIRECTIONS = array( 'left', 'right', 'up', 'down' );
const UNITS = array( 'px', 'em', 'rem', 'vh', 'vw', '%' );
/**
* Constructor.
*/
public function __construct() {
add_filter( 'render_block_gutsliders/marquee-item', array( $this, 'item' ), 10, 2 );
}
/** Designs, separators and item alignments offered. */
const DESIGNS = array( 'logos', 'ticker', 'testimonials', 'gallery', 'tags', 'announcement' );
const SEPARATORS = array(
'dot' => '•',
'star' => '✦',
'slash' => '/',
'diamond' => '◆',
'dash' => '—',
);
const ALIGNS = array( 'start', 'center', 'end', 'stretch' );
/**
* The marquee.
*
* @param array $attributes Block attributes.
* @param string $content Rendered items (all of them).
* @param \WP_Block|null $block Block, to render the items one by one (rows, separators).
* @return string HTML.
*/
public static function render( $attributes, $content, $block = null ) {
$unique_id = isset( $attributes['uniqueId'] ) ? sanitize_html_class( (string) $attributes['uniqueId'] ) : '';
$duration = isset( $attributes['speed'] ) ? max( 1, (int) $attributes['speed'] ) : 30;
$speed_px = isset( $attributes['speedPx'] ) ? max( 0, (float) $attributes['speedPx'] ) : 0;
$direction = isset( $attributes['direction'] ) && in_array( $attributes['direction'], self::DIRECTIONS, true ) ? $attributes['direction'] : 'left';
$vertical = 'vertical' === ( $attributes['orientation'] ?? 'horizontal' );
$orientation = $vertical ? 'vertical' : 'horizontal';
$pause_btn = ! isset( $attributes['pauseButton'] ) || ! empty( $attributes['pauseButton'] );
$rows = isset( $attributes['rows'] ) ? max( 1, min( 3, (int) $attributes['rows'] ) ) : 1;
$design = isset( $attributes['design'] ) && in_array( $attributes['design'], self::DESIGNS, true ) ? $attributes['design'] : '';
$separator = isset( $attributes['separator'] ) && isset( self::SEPARATORS[ $attributes['separator'] ] ) ? $attributes['separator'] : '';
$item_align = isset( $attributes['itemAlign'] ) && in_array( $attributes['itemAlign'], self::ALIGNS, true ) ? $attributes['itemAlign'] : '';
$item_width = isset( $attributes['itemWidth'] ) ? max( 0, (int) $attributes['itemWidth'] ) : 0;
$fade = ! empty( $attributes['fadeEdges'] );
$fade_size = isset( $attributes['fadeSize'] ) ? max( 0, min( 400, (int) $attributes['fadeSize'] ) ) : 80;
// Hover: pause (also the older "Pause on Hover") or slow down.
$hover = isset( $attributes['hoverEffect'] ) && in_array( $attributes['hoverEffect'], array( 'pause', 'slow' ), true ) ? $attributes['hoverEffect'] : '';
if ( ! isset( $attributes['hoverEffect'] ) && ! empty( $attributes['pauseOnHover'] ) ) {
$hover = 'pause';
}
// Responsive gap and height as custom properties (style.scss reads them).
$vars = array();
foreach ( array( 'desk', 'tab', 'mob' ) as $bp ) {
$gap = self::range( $attributes, 'columnsGap', $bp );
if ( $gap ) {
$vars[] = '--marquee-gap-' . $bp . ':' . $gap;
}
if ( $vertical ) {
$height = self::range( $attributes, 'slideHeight', $bp );
if ( $height ) {
$vars[] = '--marquee-height-' . $bp . ':' . $height;
}
}
}
if ( $item_width ) {
$vars[] = '--marquee-item-width:' . $item_width . 'px';
}
if ( $fade ) {
$vars[] = '--marquee-fade:' . $fade_size . 'px';
}
$classes = array( 'marquee-wrapper', $unique_id, 'marquee-' . $orientation );
if ( $design ) {
$classes[] = 'gs-mq-design-' . $design;
}
if ( $rows > 1 ) {
$classes[] = 'has-rows';
}
if ( $fade ) {
$classes[] = 'has-fade';
}
if ( $item_width ) {
$classes[] = 'has-item-width';
}
if ( $item_align ) {
$classes[] = 'gs-mq-align-' . $item_align;
}
$wrapper = array( 'class' => implode( ' ', array_filter( $classes ) ) );
if ( $vars ) {
$wrapper['style'] = implode( ';', $vars );
}
$data = sprintf( ' data-marquee="%s"', esc_attr( $orientation ) );
if ( $speed_px > 0 ) {
$data .= sprintf( ' data-speed-px="%s"', esc_attr( (string) $speed_px ) );
}
if ( $hover ) {
$data .= sprintf( ' data-hover="%s"', esc_attr( $hover ) );
}
if ( ! empty( $attributes['dragToScrub'] ) ) {
$data .= ' data-drag="1"';
}
if ( 'linked' === ( $attributes['scrollMode'] ?? '' ) ) {
$data .= ' data-scroll="linked"';
}
if ( ! empty( $attributes['reverseOnScroll'] ) ) {
$data .= ' data-reverse-scroll="1"';
}
if ( ! empty( $attributes['scrollBoost'] ) ) {
$data .= ' data-scroll-boost="1"';
}
if ( $pause_btn ) {
$data .= sprintf(
' data-label-pause="%1$s" data-label-play="%2$s"',
esc_attr__( 'Pause the moving content', 'slider-blocks' ),
esc_attr__( 'Play the moving content', 'slider-blocks' )
);
}
// The items, one by one when possible (rows and separators need them apart).
$items = array();
if ( $block instanceof \WP_Block && ! empty( $block->inner_blocks ) ) {
foreach ( $block->inner_blocks as $inner ) {
$items[] = (string) $inner->render();
}
} else {
$items[] = (string) $content;
}
$items = array_values( array_filter( $items, static fn( $html ) => '' !== trim( $html ) ) );
// Rows: the items dealt out in turn when there are enough, else every row shows them all.
$row_items = array();
for ( $r = 0; $r < $rows; $r++ ) {
$row_items[ $r ] = array();
}
foreach ( $items as $index => $html ) {
if ( count( $items ) >= $rows * 2 ) {
$row_items[ $index % $rows ][] = $html;
} else {
for ( $r = 0; $r < $rows; $r++ ) {
$row_items[ $r ][] = $html;
}
}
}
$sep = $separator ? sprintf( '
%2$s', esc_attr( $separator ), esc_html( self::SEPARATORS[ $separator ] ) ) : '';
$html = '';
foreach ( $row_items as $r => $list ) {
// Every other row goes the other way.
$row_direction = $direction;
if ( 1 === $r % 2 && empty( $attributes['sameDirection'] ) ) {
$row_direction = array( 'left' => 'right', 'right' => 'left', 'up' => 'down', 'down' => 'up' )[ $direction ];
}
if ( $vertical ) {
$animation = in_array( $row_direction, array( 'up', 'left' ), true ) ? 'marquee-scroll-up' : 'marquee-scroll-down';
} else {
$animation = 'marquee-scroll-' . ( 'right' === $row_direction ? 'right' : 'left' );
}
$container_classes = array( 'marquee-container', 'marquee-' . $orientation );
if ( 'pause' === $hover ) {
$container_classes[] = 'pause-on-hover';
}
// The separator also follows the last item, so it sits between copies too.
$copy_html = $sep ? implode( $sep, $list ) . $sep : implode( '', $list );
$html .= sprintf(
'
',
esc_attr( implode( ' ', $container_classes ) ),
esc_attr( $row_direction ),
$duration,
esc_attr( $animation ),
$copy_html,
self::copy( $copy_html )
);
}
return sprintf( '
%3$s
', get_block_wrapper_attributes( $wrapper ), $data, $html );
}
/**
* A repeat of the items: no ids (they must stay unique on the page).
*
* @param string $html Items HTML.
* @return string
*/
public static function copy( $html ) {
return (string) preg_replace( '/(<[a-z][a-z0-9-]*\b[^>]*?)\s+id=("[^"]*"|\'[^\']*\')/i', '$1', $html );
}
/**
* A responsive range value with its unit, or ''.
*
* @param array $attributes Attributes.
* @param string $name Control name.
* @param string $bp desk|tab|mob.
* @return string
*/
private static function range( $attributes, $name, $bp ) {
$ranges = isset( $attributes[ $name . 'Ranges' ] ) && is_array( $attributes[ $name . 'Ranges' ] ) ? $attributes[ $name . 'Ranges' ] : array();
$units = isset( $attributes[ $name . 'Units' ] ) && is_array( $attributes[ $name . 'Units' ] ) ? $attributes[ $name . 'Units' ] : array();
if ( ! isset( $ranges[ $bp ] ) || ! is_numeric( $ranges[ $bp ] ) ) {
return '';
}
$unit = isset( $units[ $bp ] ) && in_array( $units[ $bp ], self::UNITS, true ) ? $units[ $bp ] : 'px';
return (float) $ranges[ $bp ] . $unit;
}
/**
* An item: its link behind the content (no nested links), and no empty
* `background-image:url()` for items without an image.
*
* @param string $content Item HTML.
* @param array $block Parsed block.
* @return string
*/
public function item( $content, $block ) {
$content = (string) $content;
$attrs = $block['attrs'] ?? array();
if ( ! preg_match( '/<(a|div)\b[^>]*\bclass="marquee-container-outer[^"]*"[^>]*>/', $content, $match, PREG_OFFSET_CAPTURE ) ) {
return $content;
}
$open_tag = $match[0][0];
$open_at = $match[0][1];
$is_link = 'a' === $match[1][0];
$has_img = ! empty( $attrs['image']['url'] );
$tags = new \WP_HTML_Tag_Processor( $open_tag );
$tags->next_tag();
$style = (string) $tags->get_attribute( 'style' );
$classic = 'classic' === ( $attrs['bgType'] ?? 'classic' );
$strip = static fn( $css ) => trim( (string) preg_replace( '/background-(image|size|position|repeat)\s*:\s*(url\([^)]*\)|[^;]*)\s*;?/', '', $css ), "; \t\n" );
// Background image as a real image: responsive sizes, lazy loading, focal point.
$img = '';
$image_id = isset( $attrs['image']['id'] ) ? absint( $attrs['image']['id'] ) : 0;
if ( $classic && empty( $attrs['enableResponsiveBg'] ) && $image_id && wp_attachment_is_image( $image_id ) ) {
$focus = isset( $attrs['focusPoint']['x'], $attrs['focusPoint']['y'] ) ? $attrs['focusPoint'] : array( 'x' => 0.5, 'y' => 0.5 );
$img = (string) wp_get_attachment_image(
$image_id,
'large',
false,
array(
'class' => 'gutslider-slide-bg',
'alt' => '',
'loading' => 'lazy',
'decoding' => 'async',
'style' => sprintf( 'object-position:%s%% %s%%', round( (float) $focus['x'] * 100, 2 ), round( (float) $focus['y'] * 100, 2 ) ),
)
);
if ( $img ) {
$style = $strip( $style );
}
}
if ( $classic && ! $has_img ) {
$style = $strip( $style );
}
if ( ! $is_link && ! $img && $style === (string) $tags->get_attribute( 'style' ) ) {
return $content;
}
$link = '';
if ( $is_link ) {
$link_attrs = '';
foreach ( (array) $tags->get_attribute_names_with_prefix( '' ) as $name ) {
if ( in_array( $name, array( 'class', 'style' ), true ) ) {
continue;
}
$value = $tags->get_attribute( $name );
$link_attrs .= true === $value ? ' ' . esc_attr( $name ) : sprintf( ' %s="%s"', esc_attr( $name ), esc_attr( (string) $value ) );
}
$label = '';
if ( preg_match( '/<(h[1-6]|p)\b[^>]*>(.*?)<\/\1>/is', $content, $text ) ) {
$label = trim( wp_strip_all_tags( $text[2] ) );
}
$link = sprintf(
'
%2$s',
$link_attrs,
esc_html( '' !== $label ? $label : __( 'Open link', 'slider-blocks' ) )
);
}
$new_open = sprintf(
'
',
esc_attr( trim( (string) $tags->get_attribute( 'class' ) . ( $is_link ? ' has-slide-link' : '' ) ) ),
'' !== $style ? ' style="' . esc_attr( $style ) . '"' : ''
);
$html = substr_replace( $content, $new_open . $link . $img, $open_at, strlen( $open_tag ) );
if ( $is_link ) {
$close_at = strrpos( $html, '' );
if ( false !== $close_at ) {
$html = substr_replace( $html, '
', $close_at, 4 );
}
}
return $html;
}
}