get_parent_id(); $attributes = $product->get_variation_attributes(); // e.g. { attribute_pa_color: 'black', ... } $extra_attrs = 'data-adnvg-parent-id="' . esc_attr( $parent_id ) . '" data-adnvg-attributes="' . esc_attr( wp_json_encode( $attributes ) ) . '" '; return preg_replace( '/^ "T-Shirt - Red" instead of listing every attribute. */ public function format_variation_title( $name, $product ) { if ( ! is_a( $product, 'WC_Product_Variation' ) ) { return $name; } if ( ! ( is_shop() || is_product_category() || is_product_tag() || is_search() ) ) { return $name; } $options = ADNVG_Options::get_all(); $attribute = $options['primary_attribute']; $attrs = $product->get_variation_attributes(); $key = 'attribute_' . $attribute; $value = isset( $attrs[ $key ] ) ? $attrs[ $key ] : ''; if ( $value === '' ) { return $name; } // get_variation_attributes() returns the raw term slug for taxonomy // attributes; convert to the readable term name. $term = get_term_by( 'slug', $value, $attribute ); $label = $term ? $term->name : $value; return $name . ' - ' . $label; } /** * Renders one button-group per secondary attribute (Size, Material, etc.) * inside each card. JS reads the currently-selected value from each group, * looks up the matching variation in the embedded map, and swaps price + * which variation ID the Add to Cart button targets. */ public function render_secondary_attribute_selector() { global $product; if ( ! is_a( $product, 'WC_Product_Variation' ) ) { return; } if ( ! ( is_shop() || is_product_category() || is_product_tag() || is_search() ) ) { return; } // Dedupe: if both hooks fire for the same product in this request, // only render once. if ( isset( $this->rendered_for[ $product->get_id() ] ) ) { return; } $this->rendered_for[ $product->get_id() ] = true; $options = ADNVG_Options::get_all(); $primary = $options['primary_attribute']; $primary_key = 'attribute_' . $primary; $parent_id = $product->get_parent_id(); // Compute this card's own primary-attribute value LIVE from the // variation's actual attributes, rather than trusting the // '_adnvg_group_value' meta written by the last sync — that meta can // go stale (e.g. right after the primary attribute setting changes, // before the next sync/cache cycle catches up), and a stale value // here would misidentify which attribute is "primary" and cause it // to leak into the secondary selector instead of being skipped. $current_attrs = $product->get_variation_attributes(); $group_val = isset( $current_attrs[ $primary_key ] ) ? $current_attrs[ $primary_key ] : ''; $parent = wc_get_product( $parent_id ); if ( ! $parent ) { return; } $siblings = array(); foreach ( $parent->get_children() as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( ! $variation ) { continue; } $variation_attrs = $variation->get_variation_attributes(); $variation_group = isset( $variation_attrs[ $primary_key ] ) ? $variation_attrs[ $primary_key ] : ''; if ( $variation_group === $group_val ) { $siblings[] = $variation; } } if ( count( $siblings ) <= 1 ) { return; // No other options to switch between within this color group. } // Build: list of secondary attribute taxonomies/names (in the order // they appear), the distinct values for each, and a variation map for // JS to look up "does this combination exist, and which variation ID // is it". $secondary_keys = array(); foreach ( $siblings as $sibling ) { foreach ( $sibling->get_variation_attributes() as $attr_key => $attr_val ) { if ( $attr_key === 'attribute_' . $primary ) { continue; } if ( ! in_array( $attr_key, $secondary_keys, true ) ) { $secondary_keys[] = $attr_key; } } } if ( empty( $secondary_keys ) ) { return; // Only the primary attribute exists — nothing to select inside the card. } $label_for = function ( $attr_key, $attr_val ) { $tax = str_replace( 'attribute_', '', $attr_key ); if ( $attr_val !== '' && taxonomy_exists( $tax ) ) { $term = get_term_by( 'slug', $attr_val, $tax ); return $term ? $term->name : $attr_val; } return $attr_val; }; // Distinct values per secondary attribute (preserve first-seen order). $values_by_attr = array(); foreach ( $secondary_keys as $attr_key ) { foreach ( $siblings as $sibling ) { $attrs = $sibling->get_variation_attributes(); $val = isset( $attrs[ $attr_key ] ) ? $attrs[ $attr_key ] : ''; if ( $val === '' ) { continue; } if ( ! isset( $values_by_attr[ $attr_key ] ) ) { $values_by_attr[ $attr_key ] = array(); } if ( ! isset( $values_by_attr[ $attr_key ][ $val ] ) ) { $values_by_attr[ $attr_key ][ $val ] = $label_for( $attr_key, $val ); } } } // Variation map: variation_id => { attr_key: value, ..., price_html, in_stock } $variation_map = array(); foreach ( $siblings as $sibling ) { $attrs = $sibling->get_variation_attributes(); $row = array( 'price_html' => wp_strip_all_tags( $sibling->get_price_html() ), 'in_stock' => $sibling->is_in_stock(), 'parent_id' => $parent_id, ); // Include the primary attribute's value too (constant across // this group) — WooCommerce's cart validation expects the full // attribute set, not just the secondary ones shown as buttons. $row[ $primary_key ] = $group_val; foreach ( $secondary_keys as $attr_key ) { $row[ $attr_key ] = isset( $attrs[ $attr_key ] ) ? $attrs[ $attr_key ] : ''; } /** * Extension point: premium modules (gallery, videos, etc.) add * extra fields here (e.g. 'image_url') without this file * needing to know anything about them. */ $row = apply_filters( 'adnvg_variation_map_row', $row, $sibling, $product ); $variation_map[ $sibling->get_id() ] = $row; } echo '
'; foreach ( $secondary_keys as $attr_key ) { if ( empty( $values_by_attr[ $attr_key ] ) ) { continue; } echo '
'; foreach ( $values_by_attr[ $attr_key ] as $val => $label ) { $is_current_val = ( isset( $current_attrs[ $attr_key ] ) && $current_attrs[ $attr_key ] === $val ); $classes = 'adnvg-attr-btn' . ( $is_current_val ? ' adnvg-active' : '' ); $default_html = sprintf( '', esc_attr( $classes ), esc_attr( $attr_key ), esc_attr( $val ), esc_html( $label ) ); /** * Premium swatches module hooks this to render color/image * swatches instead of plain buttons — free tier just returns * $default_html untouched. * * The filtered result is KSES-sanitized before output so a * hooked callback (present or future) cannot introduce * unsafe markup — only the button/span structure this * feature needs is allowed through. */ $button_html = apply_filters( 'adnvg_attribute_button_html', $default_html, $attr_key, $val, $label, $is_current_val, $product ); $allowed_html = array( 'button' => array( 'type' => true, 'class' => true, 'title' => true, 'style' => true, 'disabled' => true, 'data-attr-key' => true, 'data-attr-value' => true, ), 'span' => array( 'class' => true, 'style' => true, ), ); echo wp_kses( $button_html, $allowed_html ); } echo '
'; } echo '
'; } }