settings = $settings; $this->eligibility = $eligibility; }
public function register() {
add_action( 'wp', array( $this, 'bind_loop_placement' ) );
add_shortcode( 'artbees_qv', array( $this, 'shortcode' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_contextual_assets' ) );
}
/**
* Load assets where a product loop, the shortcode, or the block can render.
*
* A trigger printed later in the request still calls assets(), so loops outside
* these contexts keep working. The filter lets a theme force an early load.
*/
public function enqueue_contextual_assets() {
if ( is_admin() || ! $this->should_load_assets() ) {
return;
}
$this->assets();
}
private function should_load_assets() {
/**
* Filters whether quick-view assets load for this front-end request.
*
* @param bool $load Whether the shop, product, shortcode, or block checks already matched.
*/
return (bool) apply_filters( 'artbees_qv/load_frontend_assets', $this->request_can_render_quick_view() );
}
private function request_can_render_quick_view() {
if ( function_exists( 'is_shop' ) && is_shop() ) {
return true;
}
if ( function_exists( 'is_product_taxonomy' ) && is_product_taxonomy() ) {
return true;
}
if ( function_exists( 'is_post_type_archive' ) && is_post_type_archive( 'product' ) ) {
return true;
}
if ( function_exists( 'is_product' ) && is_product() ) {
return true;
}
if ( $this->is_product_search() ) {
return true;
}
return $this->singular_contains_quick_view();
}
private function is_product_search() {
if ( ! function_exists( 'is_search' ) || ! is_search() ) {
return false;
}
$post_type = get_query_var( 'post_type' );
return 'product' === $post_type || ( is_array( $post_type ) && in_array( 'product', $post_type, true ) );
}
private function singular_contains_quick_view() {
if ( ! is_singular() ) {
return false;
}
$post = get_post();
if ( ! $post instanceof \WP_Post ) {
return false;
}
if ( has_shortcode( $post->post_content, 'artbees_qv' ) ) {
return true;
}
return function_exists( 'has_block' ) && has_block( 'artbees/quick-view', $post );
}
public function bind_loop_placement() {
if ( is_admin() ) {
return;
}
$placement = $this->settings->get()['launch']['placement'] ?? 'after_add_to_cart';
if ( 'none' === $placement ) {
return;
}
if ( 'image_hover' === $placement ) {
add_action( 'woocommerce_before_shop_loop_item_title', array( $this, 'emit_image_hover' ), 15 );
return;
}
$priority = 'before_add_to_cart' === $placement ? 5 : 15;
add_action( 'woocommerce_after_shop_loop_item', array( $this, 'emit' ), $priority );
}
public function emit() {
global $product;
if ( ! $product instanceof \WC_Product ) {
return;
}
$surface = $this->current_surface();
if ( ! $this->surface_allowed( $surface ) ) {
return;
}
echo wp_kses_post( $this->trigger( $product->get_id(), $surface, false ) );
}
public function emit_image_hover() {
global $product;
if ( ! $product instanceof \WC_Product ) {
return;
}
$surface = $this->current_surface();
if ( ! $this->surface_allowed( $surface ) ) {
return;
}
echo wp_kses_post( $this->trigger( $product->get_id(), $surface, false, '', 'artbees-qv-trigger--hover' ) );
}
public function shortcode( $atts ) { $atts = shortcode_atts( array( 'id' => 0, 'label' => '', 'class' => '' ), $atts, 'artbees_qv' ); $id = absint( $atts['id'] ); if ( ! $id && function_exists( 'wc_get_product' ) ) { global $product; $id = $product instanceof \WC_Product ? $product->get_id() : 0; } return $this->trigger( $id, 'manual', true, sanitize_text_field( $atts['label'] ), sanitize_html_class( $atts['class'] ) ); }
public function trigger( $id, $surface, $manual = false, $label = '', $class = '' ) {
$settings = $this->settings->get();
if ( ! $id || ( ! $manual && 'none' === $settings['launch']['placement'] ) ) {
return '';
}
$decision = $this->eligibility->resolve( $id, new EligibilityContext( $surface ) );
$eligible = apply_filters( 'artbees_qv/trigger/is_eligible', $decision->eligible(), $id, array( 'surface' => $surface ), $decision );
if ( ! $eligible ) {
return '';
}
$key = $surface . ':' . $id;
if ( ! $manual && isset( $this->emitted[ $key ] ) ) {
return '';
}
$this->emitted[ $key ] = true;
$this->assets();
$label = $label ?: $settings['launch']['text'];
$contents = $settings['launch']['contents'];
$tag = 'link' === $settings['launch']['element'] ? 'a' : 'button';
// Include WooCommerce's `button` class so theme/builder button geometry applies.
$classes = array_filter(
array_merge(
array(
'artbees-qv-trigger',
'artbees-qv-trigger--' . str_replace( '_', '-', $contents ),
'artbees-qv-trigger--styled',
'button',
$class,
),
(array) ( $settings['launch']['css_classes'] ?? array() )
)
);
$classes = apply_filters( 'artbees_qv/trigger/classes', $classes, $id, array( 'surface' => $surface, 'manual' => $manual ) );
$attributes = ' class="' . esc_attr( implode( ' ', array_map( 'sanitize_html_class', (array) $classes ) ) ) . '" data-artbees-qv-product="' . (int) $id . '" data-artbees-qv-surface="' . esc_attr( $surface ) . '"';
if ( 'icon' === $contents ) {
$attributes .= ' aria-label="' . esc_attr( $this->accessible_label( $settings, $id ) ) . '"';
}
$inner = $this->trigger_contents( $contents, $label );
return 'a' === $tag
? '' . $inner . ''
: '';
}
private function accessible_label( array $settings, $id ) {
return str_replace( array( '{product}', '%s' ), get_the_title( $id ), $settings['launch']['accessible_label'] );
}
private function trigger_contents( $contents, $label ) {
$html = '';
if ( 'icon' === $contents || 'icon_text' === $contents ) {
$html .= '';
}
if ( 'text' === $contents || 'icon_text' === $contents ) {
$html .= '' . esc_html( $label ) . '';
}
return $html;
}
public function assets() {
if ( is_admin() ) {
return;
}
$asset = artbees_QV_PATH . 'build/frontend/index.asset.php';
$data = file_exists( $asset ) ? require $asset : array( 'dependencies' => array(), 'version' => artbees_QV_VERSION );
$style = artbees_QV_PATH . 'build/frontend/style-index.css';
$dependencies = array_unique( array_merge( (array) $data['dependencies'], array( 'jquery', 'wc-add-to-cart', 'wc-add-to-cart-variation' ) ) );
wp_enqueue_script( 'artbees-qv-frontend', artbees_QV_URL . 'build/frontend/index.js', $dependencies, $data['version'], true );
if ( file_exists( $style ) ) {
wp_enqueue_style( 'artbees-qv-frontend', artbees_QV_URL . 'build/frontend/style-index.css', array(), $data['version'] );
}
if ( $this->assets_enqueued ) {
return;
}
$this->assets_enqueued = true;
$settings = $this->settings->get();
wp_localize_script( 'artbees-qv-frontend', 'artbeesQv', $this->frontend_config( $settings ) );
$inline = $this->trigger_style_css( $settings ) . $this->modal_style_css( $settings );
if ( $inline && file_exists( $style ) ) {
wp_add_inline_style( 'artbees-qv-frontend', $inline );
}
}
private function frontend_config( array $settings ) {
$style = $settings['style'];
return array(
'behavior' => array(
'escape' => (bool) $settings['behavior']['escape'],
'overlay' => (bool) $settings['behavior']['overlay'],
'history' => (bool) $settings['behavior']['history'],
'navigation' => (bool) $settings['behavior']['navigation'],
'boundary' => (string) $settings['behavior']['boundary'],
'prefetch' => (bool) $settings['behavior']['prefetch'],
'timeout' => (int) $settings['behavior']['timeout'],
'retry_reads' => (bool) $settings['behavior']['retry_reads'],
'fragments' => (bool) $settings['behavior']['fragments'],
'simple_outcome' => (string) $settings['behavior']['simple_outcome'],
'variable_outcome' => (string) $settings['behavior']['variable_outcome'],
),
'labels' => array(
'loading' => (string) $settings['labels']['loading'],
'retry' => (string) $settings['labels']['retry'],
'details' => (string) $settings['labels']['details'],
'added' => (string) $settings['labels']['added'],
'previous' => (string) $settings['labels']['previous'],
'next' => (string) $settings['labels']['next'],
'close' => (string) $settings['labels']['close'],
),
'gallery' => array(
'thumbnails' => (bool) $settings['gallery']['thumbnails'],
'thumbnail_position' => (string) $settings['gallery']['thumbnail_position'],
'lightbox' => (bool) $settings['gallery']['lightbox'],
'zoom' => (bool) $settings['gallery']['zoom'],
'keyboard' => (bool) $settings['gallery']['keyboard'],
'transition' => (string) $settings['gallery']['transition'],
'source' => (string) $settings['gallery']['source'],
'fit' => (string) ( $settings['gallery']['fit'] ?? 'contain' ),
),
'style' => array(
'mode' => (string) $style['mode'],
'accent' => (string) $style['accent'],
'surface' => (string) $style['surface'],
'text' => (string) $style['text'],
'overlay' => (string) $style['overlay'],
'overlay_opacity' => (int) $style['overlay_opacity'],
'radius' => (int) $style['radius'],
'spacing' => (string) $style['spacing'],
'max_width' => (int) $style['max_width'],
'motion' => (string) $style['motion'],
'css_classes' => array_values( (array) ( $style['css_classes'] ?? array() ) ),
),
'launch' => array(
'title_click' => (bool) $settings['launch']['title_click'],
'image_click' => (bool) $settings['launch']['image_click'],
'placement' => (string) $settings['launch']['placement'],
),
'urls' => array(
'rest' => esc_url_raw( rest_url( 'artbees-quick-view/v1/' ) ),
'cart' => function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : '',
'checkout' => function_exists( 'wc_get_checkout_url' ) ? wc_get_checkout_url() : '',
'add_to_cart' => class_exists( '\\WC_AJAX' ) ?
\WC_AJAX::get_endpoint( 'add_to_cart' ) : '',
),
);
}
private function resolved_trigger_colors( array $settings ) {
$defaults = $this->settings->defaults()['style']['trigger_colors'];
$stored = $settings['style']['trigger_colors'] ?? array();
$resolved = array();
foreach ( $defaults as $key => $default ) {
$value = $stored[ $key ] ?? '';
$resolved[ $key ] = ( is_string( $value ) && '' !== $value ) ? $value : $default;
}
return $resolved;
}
private function trigger_style_css( array $settings ) {
$colors = $this->resolved_trigger_colors( $settings );
$selector = '.artbees-qv-trigger.artbees-qv-trigger--styled';
$base = array();
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $colors['background'] ) ) {
$base[] = 'background-color:' . $colors['background'] . '!important';
}
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $colors['border'] ) ) {
$base[] = 'border:1px solid ' . $colors['border'] . '!important';
}
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $colors['text'] ) ) {
$base[] = 'color:' . $colors['text'] . '!important';
}
if ( empty( $base ) ) {
return '';
}
$hover = array(
'background-color:#f6f7f7!important',
'border-color:#a7aaad!important',
'color:' . $colors['text'] . '!important',
);
return $selector . '{' . implode( ';', $base ) . '}' .
$selector . ':hover,' . $selector . ':focus-visible{' . implode( ';', $hover ) . '}';
}
private function modal_style_css( array $settings ) {
$style = $settings['style'];
if ( 'artbees' !== $style['mode'] && ! $this->modal_has_custom_tokens( $style ) ) {
return '';
}
$vars = array();
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $style['accent'] ) ) {
$vars[] = '--artbees-qv-accent:' . $style['accent'];
}
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $style['surface'] ) ) {
$vars[] = '--artbees-qv-surface:' . $style['surface'];
}
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $style['text'] ) ) {
$vars[] = '--artbees-qv-text:' . $style['text'];
}
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $style['overlay'] ) ) {
$opacity = max( 0, min( 90, (int) $style['overlay_opacity'] ) ) / 100;
$vars[] = '--artbees-qv-overlay:' . $this->hex_rgba( $style['overlay'], $opacity );
}
$vars[] = '--artbees-qv-radius:' . max( 0, (int) $style['radius'] ) . 'px';
$vars[] = '--artbees-qv-space:' . $this->spacing_px( $style['spacing'] ) . 'px';
$vars[] = '--artbees-qv-max-width:' . max( 480, (int) $style['max_width'] ) . 'px';
return '.artbees-qv-modal{' . implode( ';', $vars ) . '}';
}
private function modal_has_custom_tokens( array $style ) {
foreach ( array( 'accent', 'surface', 'text', 'overlay' ) as $key ) {
if ( preg_match( '/^#[a-fA-F0-9]{6}$/', $style[ $key ] ?? '' ) ) {
return true;
}
}
return false;
}
private function spacing_px( $spacing ) {
$map = array( 'compact' => 16, 'comfortable' => 24, 'spacious' => 32 );
return $map[ $spacing ] ?? 24;
}
private function hex_rgba( $hex, $alpha ) {
$hex = ltrim( $hex, '#' );
$r = hexdec( substr( $hex, 0, 2 ) );
$g = hexdec( substr( $hex, 2, 2 ) );
$b = hexdec( substr( $hex, 4, 2 ) );
return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $alpha . ')';
}
private function current_surface() {
$loop = function_exists( 'wc_get_loop_prop' ) ? (string) wc_get_loop_prop( 'name' ) : '';
if ( in_array( $loop, array( 'related', 'up-sells', 'upsells', 'cross-sells', 'crosssells' ), true ) ) {
return 'related';
}
if ( function_exists( 'is_shop' ) && is_shop() ) {
return 'shop';
}
if ( function_exists( 'is_product_category' ) && is_product_category() ) {
return 'category';
}
if ( function_exists( 'is_product_taxonomy' ) && is_product_taxonomy() ) {
return 'archive';
}
return 'archive';
}
private function surface_allowed( $surface ) {
$surfaces = $this->settings->get()['launch']['surfaces'] ?? array();
if ( ! is_array( $surfaces ) ) {
return true;
}
return in_array( $surface, $surfaces, true );
}
}