true, 'items' => array( array( 'text' => __( 'Free shipping on all orders over $50', 'runline' ), 'url' => '', 'new_tab' => false, 'icon' => '🚚', 'image' => '', 'author' => '', ), array( 'text' => __( 'New collection is live — shop now', 'runline' ), 'url' => '', 'new_tab' => false, 'icon' => '✨', 'image' => '', 'author' => '', ), array( 'text' => __( 'Subscribe and get 10% off your first order', 'runline' ), 'url' => '', 'new_tab' => false, 'icon' => '🎁', 'image' => '', 'author' => '', ), ), 'separator' => '•', 'label' => '', 'label_bg' => '#e11d48', 'label_color' => '#ffffff', 'speed' => 60, 'speed_mobile' => 45, 'direction' => 'left', 'pause_hover' => true, 'bg' => '#111827', 'color' => '#ffffff', 'link_color' => '', 'link_hover' => '', 'font' => 'inherit', 'font_size' => 15, 'font_size_mobile' => 14, 'font_weight' => '500', 'uppercase' => false, 'height' => 40, 'gap' => 40, 'bg2' => '', 'bg_angle' => 90, 'border_width' => 0, 'border_color' => '#000000', 'shadow' => 'none', 'ribbon' => 'none', 'tilt' => -2, 'ribbon2' => '#facc15', 'css_class' => '', 'signup' => array( 'on' => false, 'placeholder' => __( 'Your e-mail', 'runline' ), 'button' => __( 'Subscribe', 'runline' ), 'success' => __( 'Thank you! You are subscribed.', 'runline' ), 'consent' => '', 'hide_after' => true, 'service' => 'local', 'list' => '', ), 'ab' => array( 'on' => false, 'items' => array(), 'split' => 50, ), 'width' => 'full', 'max_width' => 1200, 'align' => 'center', 'radius' => 0, 'position' => 'top', 'sticky' => false, 'priority' => 10, 'mobile_layout' => 'stack', 'closable' => true, 'close_days' => 0, 'devices' => array( 'desktop' => true, 'tablet' => true, 'mobile' => true, ), 'rules' => array( 'show' => array(), 'hide' => array(), ), 'schedule' => array( 'on' => false, 'start' => '', 'end' => '', 'days' => array(), 'from' => '', 'to' => '', ), 'countdown' => array( 'on' => false, 'end' => '', 'place' => 'right', 'prefix' => __( 'Ends in', 'runline' ), 'after' => 'hide', 'after_text' => __( 'The offer has ended', 'runline' ), 'bg' => '#facc15', 'color' => '#111827', 'source' => 'date', 'product' => 0, ), 'mode' => 'marquee', 'interval' => 4, 'style' => 'inline', 'image_height' => 70, 'source' => array( 'type' => 'manual', 'count' => 5, 'term' => '', 'products' => 'newest', 'rss' => '', 'images' => false, 'prices' => true, 'template' => __( '{name} bought {product} {time}', 'runline' ), 'days' => 14, 'name' => 'initial', 'city' => false, 'ids' => array(), 'text' => '', 'mix' => false, ), 'button' => array( 'on' => false, 'text' => __( 'Shop now', 'runline' ), 'url' => '', 'new_tab' => false, 'bg' => '#ffffff', 'color' => '#111827', 'radius' => 4, ), 'coupon' => array( 'on' => false, 'code' => 'SALE10', 'prefix' => __( 'Code:', 'runline' ), 'copy' => __( 'Copy', 'runline' ), 'copied' => __( 'Copied!', 'runline' ), 'bg' => '#ffffff', 'color' => '#111827', ), 'shipping' => array( 'on' => false, 'threshold' => 0, 'before' => __( 'Add {free_shipping_left} more for free shipping', 'runline' ), 'after' => __( 'You get free shipping!', 'runline' ), 'progress' => true, 'color' => '#22c55e', ), ); } /** * Choices of the select fields: value => label. */ function runline_choices( $key ) { $choices = array( 'direction' => array( 'left' => __( 'Right to left', 'runline' ), 'right' => __( 'Left to right', 'runline' ), ), 'font' => array_merge( array( 'inherit' => __( 'Font of your theme', 'runline' ), 'system' => __( 'System sans-serif', 'runline' ), 'serif' => __( 'Serif', 'runline' ), 'mono' => __( 'Monospace', 'runline' ), ), runline_google_font_choices() ), 'shadow' => array( 'none' => __( 'None', 'runline' ), 'soft' => __( 'Soft', 'runline' ), 'strong' => __( 'Strong', 'runline' ), ), 'buyer_name' => array( 'initial' => __( 'First letter only ("A.")', 'runline' ), 'first' => __( 'First name ("Anna")', 'runline' ), 'none' => __( 'No name ("Someone")', 'runline' ), ), 'mobile_layout' => array( 'stack' => __( 'Form or button under the moving text', 'runline' ), 'hide' => __( 'Hide the moving text', 'runline' ), 'inline' => __( 'As on a computer', 'runline' ), ), 'ribbon' => array( 'none' => __( 'Straight', 'runline' ), 'tilted' => __( 'Tilted ribbon', 'runline' ), 'crossed' => __( 'Two crossed ribbons', 'runline' ), ), 'font_weight' => array( '400' => __( 'Normal', 'runline' ), '500' => __( 'Medium', 'runline' ), '600' => __( 'Semi-bold', 'runline' ), '700' => __( 'Bold', 'runline' ), '800' => __( 'Extra bold', 'runline' ), ), 'position' => array( 'top' => __( 'Top of the site', 'runline' ), 'bottom' => __( 'Bottom of the screen', 'runline' ), 'manual' => __( 'Only where I put the shortcode', 'runline' ), ), 'width' => array( 'full' => __( 'Full width', 'runline' ), 'boxed' => __( 'Limited width', 'runline' ), ), 'align' => array( 'left' => __( 'Left', 'runline' ), 'center' => __( 'Center', 'runline' ), 'right' => __( 'Right', 'runline' ), ), 'cd_place' => array( 'left' => __( 'Fixed on the left', 'runline' ), 'right' => __( 'Fixed on the right', 'runline' ), 'text' => __( 'Only inside the texts ({countdown})', 'runline' ), ), 'cd_after' => array( 'hide' => __( 'Hide the bar', 'runline' ), 'text' => __( 'Show a text instead of the timer', 'runline' ), ), 'cd_source' => array( 'date' => __( 'A date I choose', 'runline' ), 'product' => __( 'End of the sale of a product', 'runline' ), 'current_product' => __( 'End of the sale of the product on the page', 'runline' ), ), 'mode' => array( 'marquee' => __( 'Scrolling (endless)', 'runline' ), 'fade' => __( 'One by one: fade', 'runline' ), 'slide' => __( 'One by one: slide up', 'runline' ), 'typewriter' => __( 'One by one: typewriter', 'runline' ), 'vertical' => __( 'Vertical ticker', 'runline' ), 'static' => __( 'Still (no motion)', 'runline' ), ), 'style' => array( 'inline' => __( 'Text in a line', 'runline' ), 'cards' => __( 'Cards (reviews, logos with captions)', 'runline' ), ), 'source' => array( 'manual' => __( 'Messages I write', 'runline' ), 'posts' => __( 'Latest posts', 'runline' ), 'products' => __( 'WooCommerce products', 'runline' ), 'rss' => __( 'RSS feed', 'runline' ), 'orders' => __( 'Recent orders (social proof)', 'runline' ), ), 'service' => runline_signup_services(), 'products' => array( 'newest' => __( 'Newest', 'runline' ), 'on_sale' => __( 'On sale', 'runline' ), 'featured' => __( 'Featured', 'runline' ), 'best_selling' => __( 'Best selling', 'runline' ), 'chosen' => __( 'Products I choose', 'runline' ), ), ); return isset( $choices[ $key ] ) ? $choices[ $key ] : array(); } /** * CSS font stacks of the "font" choice. */ /** * Google Fonts as choices: "g:" => " (Google Fonts)". */ function runline_google_font_choices() { $choices = array(); foreach ( array_keys( runline_google_fonts() ) as $family ) { /* translators: %s: font name. */ $choices[ 'g:' . $family ] = sprintf( __( '%s (Google Fonts, from your site)', 'runline' ), $family ); } return $choices; } function runline_font_stack( $font ) { $family = runline_google_family( $font ); if ( $family ) { return '"' . $family . '", ' . runline_google_fonts()[ $family ]; } $stacks = array( 'system' => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 'serif' => 'Georgia, "Times New Roman", Times, serif', 'mono' => 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace', ); return isset( $stacks[ $font ] ) ? $stacks[ $font ] : 'inherit'; } /** * Font stacks of all "font" choices: key => CSS (for the previews in the admin app and the block). */ function runline_font_stacks() { $fonts = array(); foreach ( array_keys( runline_choices( 'font' ) ) as $font ) { $fonts[ $font ] = runline_font_stack( $font ); } return $fonts; } /** * HTML allowed inside an item text. */ function runline_allowed_html() { return array( 'strong' => array(), 'b' => array(), 'em' => array(), 'i' => array(), 'u' => array(), 's' => array(), 'br' => array(), 'span' => array( 'class' => true ), ); } /** * Cleans settings that come from the editor form (or an import) into a full, safe settings array. * Check boxes the form does not send are "off"; every other missing value takes the default. * * @param array $raw Unslashed form values. * @return array */ function runline_sanitize_settings( $raw ) { $raw = is_array( $raw ) ? $raw : array(); $defaults = runline_default_settings(); $out = array(); $out['enabled'] = ! empty( $raw['enabled'] ); $out['pause_hover'] = ! empty( $raw['pause_hover'] ); $out['uppercase'] = ! empty( $raw['uppercase'] ); $out['sticky'] = ! empty( $raw['sticky'] ); $out['closable'] = ! empty( $raw['closable'] ); $out['rules'] = runline_rules_sanitize( isset( $raw['rules'] ) ? $raw['rules'] : array() ); $out['schedule'] = runline_rules_sanitize_schedule( isset( $raw['schedule'] ) ? $raw['schedule'] : array() ); $out['countdown'] = runline_sanitize_countdown( isset( $raw['countdown'] ) ? $raw['countdown'] : array() ); $group = function ( $key, $schema ) use ( $raw, $defaults ) { return runline_sanitize_group( isset( $raw[ $key ] ) ? $raw[ $key ] : array(), $defaults[ $key ], $schema ); }; $out['source'] = $group( 'source', array( 'type' => 'choice:source', 'count' => 'int:1:30', 'term' => 'term', 'products' => 'choice:products', 'rss' => 'url', 'images' => 'bool', 'prices' => 'bool', 'template' => 'text:120', 'days' => 'int:1:90', 'name' => 'choice:buyer_name', 'city' => 'bool', 'ids' => 'ids:30', 'text' => 'text:120', 'mix' => 'bool', ) ); $out['button'] = $group( 'button', array( 'on' => 'bool', 'text' => 'text:40', 'url' => 'url', 'new_tab' => 'bool', 'bg' => 'color', 'color' => 'color', 'radius' => 'int:0:40', ) ); $out['coupon'] = $group( 'coupon', array( 'on' => 'bool', 'code' => 'text:40', 'prefix' => 'text:40', 'copy' => 'text:30', 'copied' => 'text:30', 'bg' => 'color', 'color' => 'color', ) ); $out['signup'] = $group( 'signup', array( 'on' => 'bool', 'placeholder' => 'text:60', 'button' => 'text:30', 'success' => 'text:120', 'consent' => 'text:200', 'hide_after' => 'bool', 'service' => 'choice:service', 'list' => 'text:60', ) ); $out['shipping'] = $group( 'shipping', array( 'on' => 'bool', 'threshold' => 'float', 'before' => 'text:160', 'after' => 'text:160', 'progress' => 'bool', 'color' => 'color', ) ); $out['items'] = runline_sanitize_items( isset( $raw['items'] ) ? $raw['items'] : array() ); $ab = isset( $raw['ab'] ) && is_array( $raw['ab'] ) ? $raw['ab'] : array(); $out['ab'] = array( 'on' => ! empty( $ab['on'] ), 'items' => runline_sanitize_items( isset( $ab['items'] ) ? $ab['items'] : array() ), 'split' => isset( $ab['split'] ) && is_numeric( $ab['split'] ) ? min( 90, max( 10, (int) $ab['split'] ) ) : 50, ); foreach ( array( 'separator', 'label' ) as $key ) { $value = isset( $raw[ $key ] ) ? sanitize_text_field( (string) $raw[ $key ] ) : ''; $out[ $key ] = mb_substr( $value, 0, 'label' === $key ? 40 : 8 ); } // Colours: the main ones fall back to the default, link colours may stay empty (= text colour). foreach ( array( 'bg', 'color', 'label_bg', 'label_color' ) as $key ) { $color = isset( $raw[ $key ] ) ? sanitize_hex_color( (string) $raw[ $key ] ) : ''; $out[ $key ] = $color ? $color : $defaults[ $key ]; } foreach ( array( 'border_color', 'ribbon2' ) as $key ) { $color = isset( $raw[ $key ] ) ? sanitize_hex_color( (string) $raw[ $key ] ) : ''; $out[ $key ] = $color ? $color : $defaults[ $key ]; } $out['css_class'] = isset( $raw['css_class'] ) ? implode( ' ', array_filter( array_map( 'sanitize_html_class', preg_split( '/\s+/', (string) $raw['css_class'] ) ) ) ) : ''; foreach ( array( 'link_color', 'link_hover', 'bg2' ) as $key ) { $color = isset( $raw[ $key ] ) ? sanitize_hex_color( (string) $raw[ $key ] ) : ''; $out[ $key ] = $color ? $color : ''; } $numbers = array( 'speed' => array( 10, 400 ), 'speed_mobile' => array( 10, 400 ), 'font_size' => array( 10, 48 ), 'font_size_mobile' => array( 10, 48 ), 'height' => array( 24, 120 ), 'gap' => array( 8, 200 ), 'max_width' => array( 120, 3000 ), 'radius' => array( 0, 60 ), 'priority' => array( 0, 100 ), 'close_days' => array( 0, 365 ), 'interval' => array( 2, 20 ), 'bg_angle' => array( 0, 360 ), 'border_width' => array( 0, 10 ), 'tilt' => array( -8, 8 ), 'image_height' => array( 20, 100 ), ); foreach ( $numbers as $key => $range ) { $value = isset( $raw[ $key ] ) && is_numeric( $raw[ $key ] ) ? (int) $raw[ $key ] : $defaults[ $key ]; $out[ $key ] = min( $range[1], max( $range[0], $value ) ); } foreach ( array( 'direction', 'font', 'font_weight', 'position', 'width', 'align', 'mode', 'style', 'shadow', 'ribbon', 'mobile_layout' ) as $key ) { $value = isset( $raw[ $key ] ) ? (string) $raw[ $key ] : ''; $out[ $key ] = array_key_exists( $value, runline_choices( $key ) ) ? $value : $defaults[ $key ]; } $out['devices'] = array(); foreach ( array_keys( $defaults['devices'] ) as $device ) { $out['devices'][ $device ] = ! empty( $raw['devices'][ $device ] ); } return apply_filters( 'runline_sanitize_settings', $out, $raw ); } /** * Cleans a list of messages (empty ones are dropped, at most runline_max_items()). * * @param mixed $raw Messages from the editor. * @return array */ function runline_sanitize_items( $raw ) { $items = array(); if ( ! is_array( $raw ) ) { return $items; } foreach ( $raw as $item ) { if ( ! is_array( $item ) ) { continue; } $text = isset( $item['text'] ) ? trim( wp_kses( (string) $item['text'], runline_allowed_html() ) ) : ''; $icon = isset( $item['icon'] ) ? trim( sanitize_text_field( (string) $item['icon'] ) ) : ''; $image = isset( $item['image'] ) ? esc_url_raw( trim( (string) $item['image'] ) ) : ''; if ( '' === $text && '' === $icon && '' === $image ) { continue; } $items[] = array( 'text' => $text, 'url' => isset( $item['url'] ) ? esc_url_raw( trim( (string) $item['url'] ) ) : '', 'new_tab' => ! empty( $item['new_tab'] ), 'icon' => mb_substr( $icon, 0, 8 ), 'image' => $image, 'author' => isset( $item['author'] ) ? mb_substr( trim( sanitize_text_field( (string) $item['author'] ) ), 0, 80 ) : '', ); if ( count( $items ) >= runline_max_items() ) { break; } } return $items; } /** * Cleans the countdown settings. "end" is "YYYY-MM-DDTHH:MM" in the site time zone. */ function runline_sanitize_countdown( $raw ) { $raw = is_array( $raw ) ? $raw : array(); $defaults = runline_default_settings()['countdown']; $end = isset( $raw['end'] ) ? (string) $raw['end'] : ''; $pick = function ( $key, $choices ) use ( $raw, $defaults ) { $value = isset( $raw[ $key ] ) ? (string) $raw[ $key ] : ''; return array_key_exists( $value, runline_choices( $choices ) ) ? $value : $defaults[ $key ]; }; $color = function ( $key ) use ( $raw, $defaults ) { $value = isset( $raw[ $key ] ) ? sanitize_hex_color( (string) $raw[ $key ] ) : ''; return $value ? $value : $defaults[ $key ]; }; return array( 'on' => ! empty( $raw['on'] ), 'end' => preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $end ) ? $end : '', 'place' => $pick( 'place', 'cd_place' ), 'prefix' => isset( $raw['prefix'] ) ? mb_substr( sanitize_text_field( (string) $raw['prefix'] ), 0, 60 ) : $defaults['prefix'], 'after' => $pick( 'after', 'cd_after' ), 'after_text' => isset( $raw['after_text'] ) ? mb_substr( sanitize_text_field( (string) $raw['after_text'] ), 0, 120 ) : $defaults['after_text'], 'bg' => $color( 'bg' ), 'color' => $color( 'color' ), 'source' => $pick( 'source', 'cd_source' ), 'product' => isset( $raw['product'] ) ? absint( $raw['product'] ) : 0, ); } /** * Cleans a group of settings by a schema: key => bool | text: | url | color | float | int:: | * choice: | term ("taxonomy:id" or "") | ids: (list of post IDs). Missing keys take the defaults (check boxes: off). * * @param mixed $raw Values from the editor. * @param array $defaults Defaults of the group. * @param array $schema Schema. * @return array */ function runline_sanitize_group( $raw, $defaults, $schema ) { $raw = is_array( $raw ) ? $raw : array(); $out = array(); foreach ( $schema as $key => $type ) { $parts = explode( ':', $type ); $value = isset( $raw[ $key ] ) ? $raw[ $key ] : null; switch ( $parts[0] ) { case 'bool': $out[ $key ] = ! empty( $value ); break; case 'text': $out[ $key ] = null === $value ? $defaults[ $key ] : mb_substr( sanitize_text_field( (string) $value ), 0, (int) $parts[1] ); break; case 'url': $out[ $key ] = null === $value ? $defaults[ $key ] : esc_url_raw( trim( (string) $value ) ); break; case 'color': $color = null === $value ? '' : sanitize_hex_color( (string) $value ); $out[ $key ] = $color ? $color : $defaults[ $key ]; break; case 'float': $out[ $key ] = is_numeric( $value ) ? max( 0, round( (float) $value, 2 ) ) : $defaults[ $key ]; break; case 'int': $out[ $key ] = is_numeric( $value ) ? min( (int) $parts[2], max( (int) $parts[1], (int) $value ) ) : $defaults[ $key ]; break; case 'choice': $out[ $key ] = array_key_exists( (string) $value, runline_choices( $parts[1] ) ) ? (string) $value : $defaults[ $key ]; break; case 'term': $out[ $key ] = preg_match( '/^[a-z0-9_-]+:\d+$/', (string) $value ) ? (string) $value : ''; break; case 'ids': $out[ $key ] = array_slice( array_values( array_unique( array_filter( array_map( 'absint', (array) $value ) ) ) ), 0, (int) $parts[1] ); break; } } return $out; } /** * End of the countdown as a Unix timestamp (0 = no countdown). */ function runline_countdown_end( $settings ) { $countdown = $settings['countdown']; if ( empty( $countdown['on'] ) ) { return 0; } if ( 'product' === $countdown['source'] ) { return runline_sale_end( $countdown['product'] ); } if ( 'current_product' === $countdown['source'] ) { return is_singular( 'product' ) ? runline_sale_end( get_queried_object_id() ) : 0; } if ( '' === $countdown['end'] ) { return 0; } $date = date_create_immutable( $countdown['end'], wp_timezone() ); return $date ? $date->getTimestamp() : 0; } /** * Settings of a bar, with defaults for anything a newer version added. * * @param int $post_id Bar ID. * @return array */ function runline_get_settings( $post_id ) { $json = get_post_meta( $post_id, RUNLINE_META, true ); $settings = is_string( $json ) && '' !== $json ? json_decode( $json, true ) : null; if ( ! is_array( $settings ) ) { return runline_default_settings(); } // Bars of the test builds before 1.0: "show_on" = all / home. if ( isset( $settings['show_on'] ) && ! isset( $settings['rules'] ) ) { $settings['rules'] = array( 'show' => 'home' === $settings['show_on'] ? array( array( 'type' => 'front', 'value' => null, ), ) : array(), 'hide' => array(), ); } unset( $settings['show_on'] ); $defaults = runline_default_settings(); $settings = array_merge( $defaults, $settings ); foreach ( array( 'devices', 'rules', 'schedule', 'countdown', 'source', 'button', 'coupon', 'shipping', 'ab', 'signup' ) as $key ) { $settings[ $key ] = array_merge( $defaults[ $key ], (array) $settings[ $key ] ); } return $settings; } /** * Saves cleaned settings of a bar. * * @param int $post_id Bar ID. * @param array $settings Output of runline_sanitize_settings(). */ function runline_save_settings( $post_id, $settings ) { update_post_meta( $post_id, RUNLINE_META, wp_slash( wp_json_encode( $settings, JSON_UNESCAPED_UNICODE ) ) ); update_post_meta( $post_id, RUNLINE_ACTIVE_META, $settings['enabled'] ? '1' : '0' ); do_action( 'runline_settings_saved', $post_id, $settings ); } /** * Switches a bar on or off. Any number of bars can be on. * * @param int $post_id Bar ID. * @param bool $on On or off. */ function runline_set_active( $post_id, $on ) { $settings = runline_get_settings( $post_id ); $settings['enabled'] = (bool) $on; runline_save_settings( $post_id, $settings ); }