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 ); } }