>
*/
private $card_stack = array();
/**
* How many chatboxes without a key, by the key they'd share, this request
* printed.
*
* @var array
*/
private $chatbox_keys = array();
/**
* Constructor.
*
* @param Frontend $frontend Front-end printer.
*/
public function __construct( Frontend $frontend ) {
$this->frontend = $frontend;
}
/**
* Hooks.
*/
public function register(): void {
add_shortcode( 'stand_button', array( $this, 'button' ) );
add_shortcode( 'stand_card', array( $this, 'card' ) );
add_shortcode( 'stand_chatbox', array( $this, 'chatbox' ) );
add_shortcode( 'stand_action', array( $this, 'card_child' ) );
add_shortcode( 'stand_visitor_message', array( $this, 'card_child' ) );
add_shortcode( 'stand_dismiss', array( $this, 'card_child' ) );
add_filter( 'the_content', array( self::class, 'unwrap_paragraphs' ), 12 );
add_filter( 'widget_text_content', array( self::class, 'unwrap_paragraphs' ), 12 );
}
/**
* [stand_button].
*
* @param array|string $atts Attributes.
* @param string|null $content Label.
*/
public function button( $atts, $content = null ): string {
$atts = shortcode_atts(
array(
'label' => '',
'show_when' => 'either',
'avatar' => 'yes',
'greeting' => '',
'prompt' => '',
'analytics_id' => '',
'class' => '',
),
is_array( $atts ) ? $atts : array(),
'stand_button'
);
$this->frontend->content_needs_widget();
$label = '' !== trim( (string) $content ) ? wp_strip_all_tags( (string) $content ) : $atts['label'];
return Markup::button(
array(
'label' => $label,
'showWhen' => $atts['show_when'],
'avatar' => self::flag( $atts['avatar'] ),
'greeting' => $atts['greeting'],
'prompt' => $atts['prompt'],
'analyticsId' => $atts['analytics_id'],
),
self::wrapper( 'wp-block-stand-chat-stand-button', $atts['class'] )
);
}
/**
* [stand_card].
*
* @param array|string $atts Attributes.
* @param string|null $content Body with optional child shortcodes.
*/
public function card( $atts, $content = null ): string {
$atts = shortcode_atts(
array(
'show_when' => 'either',
'greeting' => '',
'prompt' => '',
'analytics_id' => '',
'identity' => 'avatar name',
'theme' => 'outlined',
'spotlight' => 'no',
'action' => '',
'action_type' => 'button',
'message' => '',
'message_field' => 'no',
'placeholder' => '',
'footer' => '',
'image' => '',
'image_alt' => '',
'class' => '',
),
is_array( $atts ) ? $atts : array(),
'stand_card'
);
$this->frontend->content_needs_widget();
$this->card_stack[] = array();
$body = do_shortcode( (string) $content );
$children = array_pop( $this->card_stack );
$body = wp_kses_post( self::balance( $body ) );
$message_mode = 'none';
$message = '' !== ( $children['stand_visitor_message'] ?? '' ) ? $children['stand_visitor_message'] : $atts['message'];
if ( self::flag( $atts['message_field'] ) ) {
$message_mode = 'field';
} elseif ( '' !== $message ) {
$message_mode = 'fixed';
}
return Markup::card(
array(
'showWhen' => $atts['show_when'],
'greeting' => $atts['greeting'],
'prompt' => $atts['prompt'],
'analyticsId' => $atts['analytics_id'],
'identity' => $atts['identity'],
'theme' => $atts['theme'],
'spotlight' => self::flag( $atts['spotlight'] ),
'actionType' => $atts['action_type'],
'actionLabel' => '' !== ( $children['stand_action'] ?? '' ) ? $children['stand_action'] : $atts['action'],
'messageMode' => $message_mode,
'visitorMessage' => $message,
'fieldPlaceholder' => $atts['placeholder'],
'dismissText' => $children['stand_dismiss'] ?? '',
'footer' => $atts['footer'],
'mediaUrl' => $atts['image'],
'mediaAlt' => $atts['image_alt'],
),
$body,
self::wrapper( 'wp-block-stand-chat-stand-card', $atts['class'] )
);
}
/**
* [stand_action], [stand_visitor_message] and [stand_dismiss] inside a card.
*
* @param array|string $atts Unused.
* @param string|null $content Text.
* @param string $tag Shortcode name.
*/
public function card_child( $atts, $content = null, $tag = '' ): string {
unset( $atts );
if ( empty( $this->card_stack ) ) {
return '';
}
$last = count( $this->card_stack ) - 1;
$this->card_stack[ $last ][ $tag ] = trim( wp_strip_all_tags( (string) $content ) );
return '';
}
/**
* [stand_chatbox].
*
* @param array|string $atts Attributes.
* @param string|null $content Intro HTML.
*/
public function chatbox( $atts, $content = null ): string {
$atts = shortcode_atts(
array(
'greeting' => '',
'placeholder' => '',
'send_label' => '',
'floating' => 'no',
'show_header' => 'no',
'height' => '',
'show_when' => 'either',
'shared_session' => 'no',
'prompt' => '',
'analytics_id' => '',
'key' => '',
'label' => '',
'class' => '',
),
is_array( $atts ) ? $atts : array(),
'stand_chatbox'
);
$this->frontend->content_needs_widget();
$intro = wp_kses_post( self::balance( do_shortcode( (string) $content ) ) );
return Markup::chatbox(
array(
'greeting' => $atts['greeting'],
'placeholder' => $atts['placeholder'],
'sendLabel' => $atts['send_label'],
'floating' => self::flag( $atts['floating'] ),
'showHeader' => self::flag( $atts['show_header'] ),
'heightMode' => 'grow' === $atts['height'] ? 'grow' : 'capped',
'maxHeight' => is_numeric( $atts['height'] ) ? (int) $atts['height'] : null,
'showWhen' => $atts['show_when'],
'sharedSession' => self::flag( $atts['shared_session'] ),
'prompt' => $atts['prompt'],
'analyticsId' => $atts['analytics_id'],
'sessionKey' => '' !== $atts['key'] ? $atts['key'] : $this->chatbox_key( (string) $content ),
'label' => $atts['label'],
),
$intro,
self::wrapper( 'wp-block-stand-chat-stand-chatbox', $atts['class'] )
);
}
/**
* The conversation key of a [stand_chatbox] without one: from the page and
* the chatbox's intro, and, for another with the same intro on the page,
* its place among them, so the two don't share a visitor's conversation.
*
* @param string $content Intro HTML.
*/
private function chatbox_key( string $content ): string {
$key = 'wp-' . get_the_ID() . '-' . substr( md5( $content ), 0, 6 );
$this->chatbox_keys[ $key ] = ( $this->chatbox_keys[ $key ] ?? 0 ) + 1;
return 1 === $this->chatbox_keys[ $key ] ? $key : $key . '-' . $this->chatbox_keys[ $key ];
}
/**
* Removes the paragraph WordPress wraps around a card or chatbox.
*
* Automatic paragraphs run before shortcodes, so an enclosing shortcode on
* its own lines ends up as …
. Browsers close
* that paragraph at the card's first block element, which would end the
* card early.
*
* @param string $html Content after shortcodes ran.
*/
public static function unwrap_paragraphs( $html ) {
if ( ! is_string( $html ) || false === strpos( $html, '\s*()\s*(?:
\s*)?
#', '$1', (string) $html );
}
/**
* Tidies enclosed content: automatic paragraphs leave a dangling at the
* start and at the end when the shortcode spans several paragraphs.
*
* @param string $html Enclosed content.
*/
private static function balance( string $html ): string {
$html = trim( $html );
$html = preg_replace( '#^(?:
|
|\s)+#i', '', $html );
$html = preg_replace( '#(?:|
|\s)+$#i', '', (string) $html );
$html = preg_replace( '#
\s*
#i', '', (string) $html );
return force_balance_tags( (string) $html );
}
/**
* Truthy shortcode values: yes, true, 1, on.
*
* @param mixed $value Raw value.
*/
private static function flag( $value ): bool {
return in_array( strtolower( trim( (string) $value ) ), array( 'yes', 'true', '1', 'on' ), true );
}
/**
* Wrapper attributes with the block class and the author's classes.
*
* @param string $base Block class.
* @param string $extra Classes from the shortcode.
* @return array
*/
private static function wrapper( string $base, string $extra ): array {
$classes = array_filter( array_map( 'sanitize_html_class', preg_split( '/\s+/', $base . ' ' . $extra ) ) );
return array( 'class' => implode( ' ', $classes ) );
}
}