03:04:05.
*/
function runline_countdown_span( $settings, $end ) {
$countdown = $settings['countdown'];
$left = $end - time();
$after = 'text' === $countdown['after'] ? $countdown['after_text'] : '';
$text = $left > 0 || '' === $after ? runline_countdown_text( $left ) : $after;
return sprintf(
'%4$s ',
$end * 1000,
esc_attr( runline_countdown_days_format() ),
esc_attr( $after ),
esc_html( $text )
);
}
/**
* HTML of a bar.
*
* @param int $post_id Bar ID.
* @param array $settings Bar settings.
* @param string $place "top", "bottom", "manual" (shortcode, block, widget) or "preview".
* @param array|null $gate What the browser still checks (runline_rules_decide()), plus "group" for one bar per place.
* @return string
*/
function runline_render_bar( $post_id, $settings, $place, $gate = null ) {
static $count = array();
$items = runline_items( $settings );
if ( ! $items ) {
return '';
}
$count[ $post_id ] = isset( $count[ $post_id ] ) ? $count[ $post_id ] + 1 : 1;
$id = 'runline-' . $post_id . ( $count[ $post_id ] > 1 ? '-' . $count[ $post_id ] : '' );
$key = $post_id . '-' . substr( md5( wp_json_encode( $items ) ), 0, 8 );
$preview = 'preview' === $place;
$end = runline_countdown_end( $settings );
$classes = array( 'runline', 'runline--' . $place, 'runline--' . $settings['direction'], 'runline--mode-' . $settings['mode'] );
if ( 'cards' === $settings['style'] ) {
$classes[] = 'runline--cards';
}
if ( 'none' !== $settings['shadow'] ) {
$classes[] = 'runline--shadow-' . $settings['shadow'];
}
if ( '' !== $settings['css_class'] ) {
$classes = array_merge( $classes, explode( ' ', $settings['css_class'] ) );
}
if ( $settings['pause_hover'] ) {
$classes[] = 'runline--pause-hover';
}
if ( $settings['uppercase'] ) {
$classes[] = 'runline--upper';
}
if ( 'boxed' === $settings['width'] ) {
$classes[] = 'runline--boxed';
$classes[] = 'runline--align-' . $settings['align'];
}
$hide = array();
if ( ! $preview ) {
foreach ( $settings['devices'] as $device => $on ) {
if ( ! $on ) {
$hide[] = 'runline--hide-' . $device;
}
}
}
$classes = array_merge( $classes, $hide );
// Phones: is there room (about 120 px) for the moving text next to the form, the button and the coupon?
$stack_parts = $settings['signup']['on'] || ( $settings['button']['on'] && '' !== $settings['button']['text'] && '' !== $settings['button']['url'] ) || ( $settings['coupon']['on'] && '' !== $settings['coupon']['code'] );
$crowded = $stack_parts && 'inline' !== $settings['mobile_layout'] && runline_mobile_crowded( $settings, $end );
if ( $crowded ) {
$classes[] = 'runline--m-' . $settings['mobile_layout'];
}
$vars = array(
'--rl-bg' => $settings['bg'],
'--rl-color' => $settings['color'],
'--rl-link' => $settings['link_color'] ? $settings['link_color'] : $settings['color'],
'--rl-link-hover' => $settings['link_hover'] ? $settings['link_hover'] : ( $settings['link_color'] ? $settings['link_color'] : $settings['color'] ),
'--rl-label-bg' => $settings['label_bg'],
'--rl-label-color' => $settings['label_color'],
'--rl-font' => runline_font_stack( $settings['font'] ),
'--rl-fs' => $settings['font_size'] . 'px',
'--rl-fs-m' => $settings['font_size_mobile'] . 'px',
'--rl-weight' => $settings['font_weight'],
'--rl-h' => $settings['height'] . 'px',
'--rl-gap' => $settings['gap'] . 'px',
// First guess of the duration for the time before runline.js measures the strip.
'--rl-dur' => max( 8, round( count( $items ) * 300 / $settings['speed'] ) ) . 's',
);
if ( 'boxed' === $settings['width'] ) {
$vars['--rl-max'] = $settings['max_width'] . 'px';
}
if ( $settings['radius'] ) {
$vars['--rl-radius'] = $settings['radius'] . 'px';
}
if ( '' !== $settings['bg2'] ) {
$vars['--rl-bg-image'] = 'linear-gradient(' . $settings['bg_angle'] . 'deg,' . $settings['bg'] . ',' . $settings['bg2'] . ')';
}
if ( $settings['border_width'] ) {
$vars['--rl-border'] = $settings['border_width'] . 'px solid ' . $settings['border_color'];
}
if ( $end ) {
$vars['--rl-cd-bg'] = $settings['countdown']['bg'];
$vars['--rl-cd-color'] = $settings['countdown']['color'];
}
$has_image = false;
foreach ( $items as $item ) {
$has_image = $has_image || ! empty( $item['image'] );
}
if ( $has_image ) {
$vars['--rl-img-h'] = round( $settings['height'] * $settings['image_height'] / 100 ) . 'px';
}
if ( $settings['button']['on'] ) {
$vars['--rl-btn-bg'] = $settings['button']['bg'];
$vars['--rl-btn-color'] = $settings['button']['color'];
$vars['--rl-btn-radius'] = $settings['button']['radius'] . 'px';
}
if ( $settings['coupon']['on'] ) {
$vars['--rl-cp-bg'] = $settings['coupon']['bg'];
$vars['--rl-cp-color'] = $settings['coupon']['color'];
}
$ship = runline_shipping_parts( $settings, $preview );
if ( $ship ) {
$vars['--rl-ship'] = $settings['shipping']['color'];
}
$style = '';
foreach ( $vars as $name => $value ) {
$style .= $name . ':' . $value . ';';
}
$cd = $end ? runline_countdown_span( $settings, $end ) : '';
$text_vars = array_merge(
runline_text_vars(),
array(
'{countdown}' => $cd,
'{coupon}' => $settings['coupon']['on'] ? '' . esc_html( $settings['coupon']['code'] ) . ' ' : '',
'{free_shipping_left}' => $ship ? $ship['left'] : '',
)
);
$group = runline_render_group( $items, $settings['separator'], false, $text_vars );
$copy = runline_render_group( $items, $settings['separator'], true, $text_vars );
$track_b = '';
if ( runline_ab_active( $settings ) ) {
// Variant B of the A/B test: the checker (gate.js) keeps one of the two strips for each visitor.
$track_b = '
'
. runline_render_group( $settings['ab']['items'], $settings['separator'], false, $text_vars )
. runline_render_group( $settings['ab']['items'], $settings['separator'], true, $text_vars ) . '
';
}
$gate_config = $preview ? null : runline_gate_config( $key, $settings, (array) $gate, $end );
$html = '';
if ( 'bottom' === $place ) {
// Keeps the end of the page visible above the fixed bar.
$spacer = array_merge( array( 'runline-spacer' ), $hide, $crowded && 'stack' === $settings['mobile_layout'] ? array( 'runline-spacer--stack' ) : array() );
$html .= '
';
}
$html .= sprintf(
'',
esc_attr( $id ),
esc_attr( implode( ' ', $classes ) ),
esc_attr( $style ),
(int) $settings['speed'],
(int) $settings['speed_mobile'],
esc_attr( $key ),
esc_attr( $settings['label'] ? $settings['label'] : __( 'Announcements', 'runline' ) ),
( ! empty( $gate['group'] ) ? ' data-group="' . esc_attr( $gate['group'] ) . '"' : '' )
. ( ! $preview && $gate_config ? ' data-gate="' . esc_attr( wp_json_encode( $gate_config ) ) . '"' : '' )
. ( $ship && ! $preview ? ' data-ship="' . esc_attr( wp_json_encode( $ship['data'] ) ) . '"' : '' )
. ( ! $preview && $post_id && runline_stats_on() ? ' data-bar="' . (int) $post_id . '" data-track="' . esc_url( rest_url( RUNLINE_REST_NS . '/track' ) ) . '"' : '' ),
(int) $settings['interval']
);
if ( '' !== $settings['label'] ) {
$html .= '
' . esc_html( $settings['label'] ) . '
';
}
$fixed_cd = '';
if ( $end && 'text' !== $settings['countdown']['place'] ) {
$fixed_cd = '
'
. ( '' !== $settings['countdown']['prefix'] ? '' . esc_html( $settings['countdown']['prefix'] ) . ' ' : '' )
. $cd . '
';
}
if ( 'left' === $settings['countdown']['place'] ) {
$html .= $fixed_cd;
}
if ( $ship ) {
$html .= $ship['block'];
}
$html .= '
' . $group . $copy . '
' . $track_b . '
';
if ( $crowded && 'stack' === $settings['mobile_layout'] ) {
// On phones the form, the button and the coupon go to a second row after this break.
$html .= '
';
}
if ( 'right' === $settings['countdown']['place'] ) {
$html .= $fixed_cd;
}
$coupon = $settings['coupon'];
if ( $coupon['on'] && '' !== $coupon['code'] ) {
$html .= '
'
. ( '' !== $coupon['prefix'] ? '' . esc_html( $coupon['prefix'] ) . ' ' : '' )
. '' . esc_html( $coupon['code'] ) . ' '
. '' . esc_html( $coupon['copy'] ) . '
';
}
$html .= runline_signup_form( $post_id, $settings, $preview );
$button = $settings['button'];
if ( $button['on'] && '' !== $button['text'] && '' !== $button['url'] ) {
$html .= '
' . esc_html( $button['text'] ) . ' ';
}
if ( $ship && $settings['shipping']['progress'] ) {
$html .= '
';
}
if ( $settings['closable'] ) {
$html .= '
× ';
}
$html .= '
';
if ( 'none' !== $settings['ribbon'] ) {
// A tilted ribbon (and a second one crossing it) in a box that hides what sticks out.
$html = sprintf(
'%3$s%4$s
',
esc_attr( $settings['ribbon'] ),
esc_attr( '--rl-h:' . $settings['height'] . 'px;--rl-tilt:' . $settings['tilt'] . 'deg;--rl-ribbon2:' . $settings['ribbon2'] . ';' ),
'crossed' === $settings['ribbon'] ? ' ' : '',
$html
);
}
if ( ! $preview ) {
$html .= runline_gate_script( $id, $key, $settings, (array) $gate, $end );
runline_enqueue_font( $settings );
}
return apply_filters( 'runline_bar_html', $html, $post_id, $settings, $place );
}
/**
* The inline call that decides, before the first paint, whether the visitor sees the bar (closed by the visitor,
* visitor rules, schedule, ended countdown, one bar per place). The checker itself (gate.js) is printed once per page.
*/
function runline_gate_script( $id, $key, $settings, $gate, $end ) {
$config = runline_gate_config( $key, $settings, $gate, $end );
if ( ! $config ) {
return '';
}
// The checker is printed in on pages known to have bars; a bar found later brings it to the footer
// (before runline.js, which then checks such bars). Never inline in the content: content filters change "&".
if ( ! did_action( 'runline_gate_printed' ) && ! wp_scripts()->get_data( 'runline', 'runline_gate' ) ) {
wp_add_inline_script( 'runline', runline_gate_runtime(), 'before' );
wp_script_add_data( 'runline', 'runline_gate', true );
}
return wp_get_inline_script_tag( 'if(window.runlineGate)runlineGate(' . wp_json_encode( $id ) . ');' );
}
/**
* What the browser checks for a bar (see gate.js), or null when there is nothing to check.
*/
function runline_gate_config( $key, $settings, $gate, $end ) {
$config = array( 'key' => $key );
if ( $settings['closable'] ) {
$config['closable'] = 1;
$config['closeDays'] = (int) $settings['close_days'];
}
foreach ( array( 'show', 'hide', 'sched', 'group', 'cartUrl', 'countryUrl' ) as $part ) {
if ( ! empty( $gate[ $part ] ) ) {
$config[ $part ] = $gate[ $part ];
}
}
if ( $end && 'hide' === $settings['countdown']['after'] ) {
$config['cdEnd'] = $end * 1000;
}
if ( runline_ab_active( $settings ) ) {
$config['ab'] = (int) $settings['ab']['split'];
}
if ( $settings['signup']['on'] && $settings['signup']['hide_after'] ) {
$config['signup'] = 1;
}
return 1 === count( $config ) ? null : $config;
}
/**
* Whether a bar runs an A/B test (written messages and a second set of them).
*/
function runline_ab_active( $settings ) {
return ! empty( $settings['ab']['on'] ) && ! empty( $settings['ab']['items'] ) && 'manual' === $settings['source']['type'];
}
/**
* The checker (assets/js/gate.js, minified when available).
*/
function runline_gate_runtime() {
$file = RUNLINE_DIR . 'assets/js/gate.min.js';
$file = file_exists( $file ) ? $file : RUNLINE_DIR . 'assets/js/gate.js';
return trim( (string) file_get_contents( $file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- a file of this plugin.
}
/**
* Whether the fixed parts of a bar leave less than about 120 px for the moving text on a phone (390 px).
* A rough measure from the texts, the same in render.js — so the layout is decided before the page shows.
*
* @param array $settings Bar settings.
* @param int $end End of the countdown (0 = none).
*/
function runline_mobile_crowded( $settings, $end ) {
if ( $settings['signup']['on'] ) {
return true;
}
$char = $settings['font_size_mobile'] * ( $settings['uppercase'] ? 0.72 : 0.6 );
$width = 0;
$timer = $end && 'text' !== $settings['countdown']['place'];
if ( '' !== $settings['label'] && ! $timer ) {
$width += mb_strlen( $settings['label'] ) * $char + 32;
}
if ( $timer ) {
$width += 11 * $char + 20;
}
if ( $settings['coupon']['on'] && '' !== $settings['coupon']['code'] ) {
$width += ( mb_strlen( $settings['coupon']['code'] ) + mb_strlen( $settings['coupon']['copy'] ) ) * $char + 56;
}
if ( $settings['button']['on'] && '' !== $settings['button']['text'] && '' !== $settings['button']['url'] ) {
$width += mb_strlen( $settings['button']['text'] ) * $char + 32;
}
if ( $settings['closable'] ) {
$width += 40;
}
return $width > 390 - 120;
}
/**
* One copy of the items. Copies are hidden from screen readers and from the Tab key.
*
* @param array $items Items.
* @param string $separator Separator between items.
* @param bool $copy A copy for the loop.
* @param array $vars Text variables: "{name}" => HTML.
* @return string
*/
function runline_render_group( $items, $separator, $copy, $vars = array() ) {
$html = $copy ? '' : '';
foreach ( $items as $item ) {
$inner = '';
if ( '' !== $item['icon'] ) {
$inner .= '' . esc_html( $item['icon'] ) . ' ';
}
$text = strtr( wp_kses( $item['text'], runline_allowed_html() ), $vars );
if ( ! empty( $item['image'] ) ) {
$alt = trim( wp_strip_all_tags( '' !== $item['text'] ? $item['text'] : ( isset( $item['author'] ) ? $item['author'] : '' ) ) );
$text = ' ' . $text;
}
if ( '' !== $item['url'] ) {
$attrs = ' href="' . esc_url( $item['url'] ) . '"';
if ( $item['new_tab'] ) {
$attrs .= ' target="_blank" rel="noopener"';
}
if ( $copy ) {
$attrs .= ' tabindex="-1"';
}
$text = '' . $text . ' ';
}
$text = '' . $text . ' ';
if ( ! empty( $item['author'] ) ) {
$text = '' . $text . '' . esc_html( $item['author'] ) . ' ';
}
$html .= '' . $inner . $text . ' ';
if ( '' !== $separator ) {
$html .= '' . esc_html( $separator ) . ' ';
}
}
return $html . ' ';
}
/**
* Free shipping parts of a bar: the text block, data for the browser, the amount left. Null when off or unknown.
* Before the browser knows the cart, the whole threshold is "left".
*
* @param array $settings Bar settings.
* @param bool $preview Editor preview (example: a third of the threshold is in the cart).
*/
function runline_shipping_parts( $settings, $preview ) {
$ship = $settings['shipping'];
if ( ! $ship['on'] ) {
return null;
}
$threshold = runline_free_shipping_threshold( $settings );
if ( $threshold <= 0 ) {
return null;
}
$in_cart = $preview ? round( $threshold / 3, 2 ) : 0;
$left = runline_shipping_price( $threshold - $in_cart );
$span = '' . esc_html( $left ) . ' ';
return array(
'left' => $span,
'percent' => round( 100 * $in_cart / $threshold ),
'block' => '' . str_replace( '{free_shipping_left}', $span, esc_html( $ship['before'] ) ) . '
',
'data' => array(
't' => $threshold,
'before' => $ship['before'],
'after' => $ship['after'],
'fmt' => runline_price_format(),
'cart' => esc_url_raw( rest_url( 'wc/store/v1/cart' ) ),
),
);
}
/**
* A price as plain text in the format of the shop ("66,67 zł"), or a plain number without WooCommerce.
*/
function runline_shipping_price( $amount ) {
if ( function_exists( 'wc_price' ) ) {
return html_entity_decode( wp_strip_all_tags( wc_price( $amount ) ), ENT_QUOTES, 'UTF-8' );
}
return number_format_i18n( $amount, 2 );
}