| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Services\Renderer; |
| 4 |
|
| 5 |
use FluentCart\App\Models\AttributeGroup; |
| 6 |
use FluentCart\App\Models\AttributeTerm; |
| 7 |
use FluentCart\Framework\Support\Arr; |
| 8 |
|
| 9 |
class AdvancedVariationRenderer |
| 10 |
{ |
| 11 |
protected $product; |
| 12 |
|
| 13 |
public function __construct($product) |
| 14 |
{ |
| 15 |
$this->product = $product; |
| 16 |
} |
| 17 |
|
| 18 |
/** |
| 19 |
* Whitelist a term's stored color before it lands in an inline |
| 20 |
* `style="background-color: ..."` attribute. esc_attr escapes HTML |
| 21 |
* attribute chars but NOT CSS syntax — a stored value saved through |
| 22 |
* sanitize_text_field (which never validated color format) like |
| 23 |
* "red;position:fixed;inset:0;z-index:9999" would otherwise inject |
| 24 |
* extra CSS declarations and deface the product page. Accept only |
| 25 |
* hex (#rgb/#rgba/#rrggbb/#rrggbbaa), rgb()/rgba() with numeric |
| 26 |
* content, or a bare CSS named color; anything else returns '' and |
| 27 |
* the caller drops the style attribute entirely. |
| 28 |
*/ |
| 29 |
protected function safeCssColor($color): string |
| 30 |
{ |
| 31 |
$color = trim((string) $color); |
| 32 |
if ($color === '') { |
| 33 |
return ''; |
| 34 |
} |
| 35 |
if (preg_match('/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color)) { |
| 36 |
return $color; |
| 37 |
} |
| 38 |
if (preg_match('/^rgba?\(\s*[0-9.,%\s]+\)$/i', $color)) { |
| 39 |
return $color; |
| 40 |
} |
| 41 |
if (preg_match('/^[a-zA-Z]{1,40}$/', $color)) { |
| 42 |
return $color; |
| 43 |
} |
| 44 |
return ''; |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Emits the storefront selector markup. Returns true only when markup |
| 49 |
* was actually echoed — false on every early bail. The caller writes |
| 50 |
* this into the filter's 'rendered' flag so free core can fall through |
| 51 |
* to its simple-variation rendering when this renderer no-ops (e.g. a |
| 52 |
* product switched to advanced_variations before attributes are built). |
| 53 |
*/ |
| 54 |
public function render($selectorStyle = 'auto'): bool |
| 55 |
{ |
| 56 |
$variants = $this->product->variants; |
| 57 |
if (!$variants || $variants->isEmpty()) { |
| 58 |
return false; |
| 59 |
} |
| 60 |
|
| 61 |
$variants->load(['attrMap', 'media']); |
| 62 |
|
| 63 |
$otherInfo = (array)Arr::get($this->product->detail, 'other_info'); |
| 64 |
$attributeConfig = Arr::get($otherInfo, 'attribute_config', []); |
| 65 |
|
| 66 |
if (empty($attributeConfig)) { |
| 67 |
return false; |
| 68 |
} |
| 69 |
|
| 70 |
$allTermIds = []; |
| 71 |
foreach ($attributeConfig as $attrGroup) { |
| 72 |
// (array) cast — other_info is an independent read path; if a |
| 73 |
// corrupt or non-syncVariantOption writer ever stored a scalar |
| 74 |
// under 'variants', a bare array_merge would TypeError and fatal |
| 75 |
// the whole product page. |
| 76 |
$termIds = (array) Arr::get($attrGroup, 'variants', []); |
| 77 |
$allTermIds = array_merge($allTermIds, $termIds); |
| 78 |
} |
| 79 |
$allTermIds = array_unique(array_map('intval', $allTermIds)); |
| 80 |
|
| 81 |
$terms = AttributeTerm::query()->whereIn('id', $allTermIds)->get()->keyBy('id'); |
| 82 |
|
| 83 |
$groupIds = array_column($attributeConfig, 'group_id'); |
| 84 |
$groups = AttributeGroup::query()->whereIn('id', array_map('intval', $groupIds))->get()->keyBy('id'); |
| 85 |
|
| 86 |
$variationMap = []; |
| 87 |
$variantData = []; |
| 88 |
|
| 89 |
foreach ($variants as $variant) { |
| 90 |
if ($variant->item_status !== 'active') { |
| 91 |
continue; |
| 92 |
} |
| 93 |
|
| 94 |
$termIds = []; |
| 95 |
foreach ($variant->attrMap as $rel) { |
| 96 |
$termIds[] = (int)$rel->term_id; |
| 97 |
} |
| 98 |
sort($termIds, SORT_NUMERIC); |
| 99 |
$identifier = implode('_', $termIds); |
| 100 |
|
| 101 |
$variationMap[$identifier] = (int)$variant->id; |
| 102 |
|
| 103 |
$variantData[$variant->id] = [ |
| 104 |
'stock' => ($variant->manage_stock ? ($variant->stock_status ?: 'in-stock') : 'in-stock'), |
| 105 |
// Raw — esc_attr on the JSON below escapes the HTML-attribute |
| 106 |
// context, and the JS consumer must render via textContent |
| 107 |
// (NOT innerHTML). Pre-escaping here would double-encode |
| 108 |
// titles like "S&L" into "S&L" by the time JS reads |
| 109 |
// them, showing literal "&" to the shopper. |
| 110 |
'title' => $variant->variation_title, |
| 111 |
// Authoritative payment_type column so the selector can hide |
| 112 |
// Add to Cart for subscription combinations (subscriptions are |
| 113 |
// Buy Now only) — mirrors the simple-variation flow. |
| 114 |
'payment_type' => $variant->payment_type, |
| 115 |
// serial_index so the selector can fall back to the first |
| 116 |
// combination by order (not first in-stock) when the product has |
| 117 |
// no default_variation_id — matching the server-side default rule. |
| 118 |
// Keep NULL as null (don't cast to 0) so the selector's |
| 119 |
// `?? Infinity` guard sorts an unordered legacy row LAST, matching |
| 120 |
// the server fallback — casting null->0 would make it sort first. |
| 121 |
'serial_index' => is_null($variant->serial_index) ? null : (int) $variant->serial_index, |
| 122 |
]; |
| 123 |
} |
| 124 |
|
| 125 |
// If every variant was filtered out (all inactive), there's nothing |
| 126 |
// for the selector to map to — bail before emitting markup that |
| 127 |
// would just be dead controls. |
| 128 |
if (empty($variationMap)) { |
| 129 |
return false; |
| 130 |
} |
| 131 |
|
| 132 |
// Authoritative group→terms ownership map — used by JS to avoid heuristic narrowing |
| 133 |
$attrConfigForJs = []; |
| 134 |
foreach ($attributeConfig as $attrGroup) { |
| 135 |
$groupId = absint(Arr::get($attrGroup, 'group_id')); |
| 136 |
$termIds = array_values(array_map('absint', Arr::get($attrGroup, 'variants', []))); |
| 137 |
$attrConfigForJs[$groupId] = $termIds; |
| 138 |
} |
| 139 |
|
| 140 |
// Auto-detect the primary visual group (color or image type) — its terms |
| 141 |
// drive the main gallery image swap on the storefront. First match wins. |
| 142 |
$primaryGroupId = 0; |
| 143 |
foreach ($attributeConfig as $attrGroup) { |
| 144 |
$gid = absint(Arr::get($attrGroup, 'group_id')); |
| 145 |
$group = $groups->get($gid); |
| 146 |
$type = $group ? Arr::get((array) $group->settings, 'type', '') : ''; |
| 147 |
if (in_array($type, ['color', 'image'], true)) { |
| 148 |
$primaryGroupId = $gid; |
| 149 |
break; |
| 150 |
} |
| 151 |
} |
| 152 |
|
| 153 |
// Build term→image map for the primary group: first non-empty thumbnail |
| 154 |
// found among variants that belong to each term wins. |
| 155 |
$termImageMap = []; |
| 156 |
if ($primaryGroupId) { |
| 157 |
foreach ($variants as $variant) { |
| 158 |
$thumb = $variant->thumbnail ?: ''; |
| 159 |
if (!$thumb) { |
| 160 |
continue; |
| 161 |
} |
| 162 |
foreach ($variant->attrMap as $rel) { |
| 163 |
$tid = (int) $rel->term_id; |
| 164 |
if ((int) $rel->group_id === $primaryGroupId && !isset($termImageMap[$tid])) { |
| 165 |
$termImageMap[$tid] = $thumb; |
| 166 |
} |
| 167 |
} |
| 168 |
} |
| 169 |
} |
| 170 |
|
| 171 |
// wp_json_encode returns false on encoding failure (e.g. invalid UTF-8 |
| 172 |
// in a term title); fall back to a parseable empty object so the JS |
| 173 |
// doesn't crash on JSON.parse(""). Default to {} (not []) because |
| 174 |
// the JS consumers expect object lookups by string key. |
| 175 |
$variationMapJson = wp_json_encode($variationMap) ?: '{}'; |
| 176 |
$variantDataJson = wp_json_encode($variantData) ?: '{}'; |
| 177 |
$attrConfigJson = wp_json_encode($attrConfigForJs) ?: '{}'; |
| 178 |
$termImageMapJson = wp_json_encode($termImageMap) ?: '{}'; |
| 179 |
|
| 180 |
// Surface the merchant's saved default variant id so the JS |
| 181 |
// selector can pre-select that combination on first paint. When |
| 182 |
// the column is empty (legacy products, or the merchant cleared |
| 183 |
// it), the JS falls back to the first combination by serial_index. Read |
| 184 |
// straight off the model — (array) cast on a WPFluent model |
| 185 |
// returns its public protected/$attributes shape inconsistently |
| 186 |
// depending on lazy-load state, so `default_variation_id` came |
| 187 |
// back null on first paint even when the column was set. |
| 188 |
$defaultVariationId = $this->product->detail |
| 189 |
? (int) $this->product->detail->default_variation_id |
| 190 |
: 0; |
| 191 |
|
| 192 |
?> |
| 193 |
<div class="fct-advanced-variation-wrap" |
| 194 |
data-variation-map="<?php echo esc_attr($variationMapJson); ?>" |
| 195 |
data-variant-data="<?php echo esc_attr($variantDataJson); ?>" |
| 196 |
data-attribute-config="<?php echo esc_attr($attrConfigJson); ?>" |
| 197 |
data-primary-group-id="<?php echo esc_attr($primaryGroupId); ?>" |
| 198 |
data-term-image-map="<?php echo esc_attr($termImageMapJson); ?>" |
| 199 |
data-product-id="<?php echo esc_attr($this->product->ID); ?>" |
| 200 |
data-default-variation-id="<?php echo esc_attr($defaultVariationId); ?>" |
| 201 |
data-selector-style="<?php echo esc_attr($selectorStyle); ?>"> |
| 202 |
<?php |
| 203 |
foreach ($attributeConfig as $attrConfig) { |
| 204 |
$groupId = absint(Arr::get($attrConfig, 'group_id')); |
| 205 |
$group = isset($groups[$groupId]) ? $groups[$groupId] : null; |
| 206 |
$termIds = Arr::get($attrConfig, 'variants', []); |
| 207 |
|
| 208 |
if (!$group || empty($termIds)) { |
| 209 |
continue; |
| 210 |
} |
| 211 |
|
| 212 |
$groupTerms = []; |
| 213 |
foreach ($termIds as $termId) { |
| 214 |
$termId = absint($termId); |
| 215 |
if (isset($terms[$termId])) { |
| 216 |
$groupTerms[] = $terms[$termId]; |
| 217 |
} |
| 218 |
} |
| 219 |
|
| 220 |
// After term resolution, every claimed term ID could be a |
| 221 |
// term that no longer exists (deleted after the editor save). |
| 222 |
// Rendering an empty dropdown / swatch row would show a |
| 223 |
// ghost control with no options — skip the whole group. |
| 224 |
if (empty($groupTerms)) { |
| 225 |
continue; |
| 226 |
} |
| 227 |
|
| 228 |
$groupSettings = is_array($group->settings) ? $group->settings : []; |
| 229 |
$groupType = Arr::get($groupSettings, 'type', 'options'); |
| 230 |
$groupStyling = Arr::get($groupSettings, 'styling', ''); |
| 231 |
|
| 232 |
do_action('fluent_cart/product/single/before_variant_item', [ |
| 233 |
'product' => $this->product, |
| 234 |
'group' => $group, |
| 235 |
'scope' => 'attribute_selector_group', |
| 236 |
]); |
| 237 |
|
| 238 |
if ($groupType === 'color' && $groupStyling === 'dropdown') { |
| 239 |
$this->renderColorDropdown($group, $groupTerms); |
| 240 |
} elseif ($groupType === 'image' && $groupStyling === 'dropdown') { |
| 241 |
$this->renderImageDropdown($group, $groupTerms); |
| 242 |
} elseif ($groupStyling === 'dropdown') { |
| 243 |
$this->renderAttributeDropdown($group, $groupTerms); |
| 244 |
} else { |
| 245 |
$this->renderAttributeSwatches($group, $groupTerms); |
| 246 |
} |
| 247 |
|
| 248 |
do_action('fluent_cart/product/single/after_variant_item', [ |
| 249 |
'product' => $this->product, |
| 250 |
'group' => $group, |
| 251 |
'scope' => 'attribute_selector_group', |
| 252 |
]); |
| 253 |
} |
| 254 |
?> |
| 255 |
|
| 256 |
</div> |
| 257 |
<?php |
| 258 |
|
| 259 |
return true; |
| 260 |
} |
| 261 |
|
| 262 |
protected function renderAttributeDropdown($group, $terms) |
| 263 |
{ |
| 264 |
?> |
| 265 |
<?php $listboxId = 'fct-listbox-' . (int) $group->id; ?> |
| 266 |
<div class="fct-attribute-selector fct-custom-dropdown fct-options-dropdown" |
| 267 |
data-group-id="<?php echo esc_attr($group->id); ?>" |
| 268 |
data-attribute-group="<?php echo esc_attr($group->id); ?>"> |
| 269 |
<label class="fct-attribute-label"> |
| 270 |
<?php echo esc_html($group->title); ?> |
| 271 |
</label> |
| 272 |
|
| 273 |
<div class="fct-custom-select" data-fct-custom-select tabindex="0" |
| 274 |
role="combobox" |
| 275 |
aria-haspopup="listbox" |
| 276 |
aria-expanded="false" |
| 277 |
aria-controls="<?php echo esc_attr($listboxId); ?>" |
| 278 |
aria-label="<?php echo esc_attr($group->title); ?>"> |
| 279 |
<div class="fct-custom-select__trigger" data-fct-select-trigger> |
| 280 |
<span class="fct-custom-select__value" data-fct-select-value> |
| 281 |
<?php |
| 282 |
/* translators: %1$s is the attribute group title */ |
| 283 |
printf(esc_html__('Choose %1$s', 'fluent-cart'), esc_html($group->title)); |
| 284 |
?> |
| 285 |
</span> |
| 286 |
<span class="fct-custom-select__arrow" aria-hidden="true"></span> |
| 287 |
</div> |
| 288 |
<div class="fct-custom-select__options" role="listbox" id="<?php echo esc_attr($listboxId); ?>"> |
| 289 |
<?php foreach ($terms as $term) : ?> |
| 290 |
<div class="fct-custom-select__option" |
| 291 |
data-fct-select-option |
| 292 |
role="option" |
| 293 |
aria-selected="false" |
| 294 |
data-value="<?php echo esc_attr($term->id); ?>" |
| 295 |
data-term-slug="<?php echo esc_attr($term->slug); ?>"> |
| 296 |
<span class="fct-option-label"> |
| 297 |
<?php echo esc_html($term->title); ?> |
| 298 |
</span> |
| 299 |
</div> |
| 300 |
<?php endforeach; ?> |
| 301 |
</div> |
| 302 |
</div> |
| 303 |
</div> |
| 304 |
<?php |
| 305 |
} |
| 306 |
|
| 307 |
protected function renderColorDropdown($group, $terms) |
| 308 |
{ |
| 309 |
?> |
| 310 |
<?php $listboxId = 'fct-listbox-' . (int) $group->id; ?> |
| 311 |
<div class="fct-attribute-selector fct-custom-dropdown fct-color-dropdown" |
| 312 |
data-group-id="<?php echo esc_attr($group->id); ?>" |
| 313 |
data-attribute-group="<?php echo esc_attr($group->id); ?>"> |
| 314 |
<label class="fct-attribute-label"> |
| 315 |
<?php echo esc_html($group->title); ?> |
| 316 |
</label> |
| 317 |
|
| 318 |
<div class="fct-custom-select" data-fct-custom-select tabindex="0" |
| 319 |
role="combobox" |
| 320 |
aria-haspopup="listbox" |
| 321 |
aria-expanded="false" |
| 322 |
aria-controls="<?php echo esc_attr($listboxId); ?>" |
| 323 |
aria-label="<?php echo esc_attr($group->title); ?>"> |
| 324 |
<div class="fct-custom-select__trigger" data-fct-select-trigger> |
| 325 |
<span class="fct-custom-select__value" data-fct-select-value> |
| 326 |
<?php |
| 327 |
/* translators: %1$s is the attribute group title */ |
| 328 |
printf(esc_html__('Choose %1$s', 'fluent-cart'), esc_html($group->title)); |
| 329 |
?> |
| 330 |
</span> |
| 331 |
<span class="fct-custom-select__arrow" aria-hidden="true"></span> |
| 332 |
</div> |
| 333 |
<div class="fct-custom-select__options" role="listbox" id="<?php echo esc_attr($listboxId); ?>"> |
| 334 |
<?php foreach ($terms as $term) : |
| 335 |
$settings = is_array($term->settings) ? $term->settings : []; |
| 336 |
$color = $this->safeCssColor(Arr::get($settings, 'color', '')); |
| 337 |
?> |
| 338 |
<div class="fct-custom-select__option" |
| 339 |
data-fct-select-option |
| 340 |
role="option" |
| 341 |
aria-selected="false" |
| 342 |
data-value="<?php echo esc_attr($term->id); ?>" |
| 343 |
data-term-slug="<?php echo esc_attr($term->slug); ?>"> |
| 344 |
<div class="fct-color-option"> |
| 345 |
<?php if ($color) : ?> |
| 346 |
<span class="fct-color-swatch" style="background-color: <?php echo esc_attr($color); ?>"></span> |
| 347 |
<?php endif; ?> |
| 348 |
<span class="fct-color-label"> |
| 349 |
<?php echo esc_html($term->title); ?> |
| 350 |
</span> |
| 351 |
</div> |
| 352 |
</div> |
| 353 |
<?php endforeach; ?> |
| 354 |
</div> |
| 355 |
</div> |
| 356 |
</div> |
| 357 |
<?php |
| 358 |
} |
| 359 |
|
| 360 |
protected function renderImageDropdown($group, $terms) |
| 361 |
{ |
| 362 |
?> |
| 363 |
<?php $listboxId = 'fct-listbox-' . (int) $group->id; ?> |
| 364 |
<div class="fct-attribute-selector fct-custom-dropdown fct-image-dropdown" |
| 365 |
data-group-id="<?php echo esc_attr($group->id); ?>" |
| 366 |
data-attribute-group="<?php echo esc_attr($group->id); ?>"> |
| 367 |
<label class="fct-attribute-label"> |
| 368 |
<?php echo esc_html($group->title); ?> |
| 369 |
</label> |
| 370 |
|
| 371 |
<div class="fct-custom-select" data-fct-custom-select tabindex="0" |
| 372 |
role="combobox" |
| 373 |
aria-haspopup="listbox" |
| 374 |
aria-expanded="false" |
| 375 |
aria-controls="<?php echo esc_attr($listboxId); ?>" |
| 376 |
aria-label="<?php echo esc_attr($group->title); ?>"> |
| 377 |
<div class="fct-custom-select__trigger" data-fct-select-trigger> |
| 378 |
<span class="fct-custom-select__value" data-fct-select-value> |
| 379 |
<?php |
| 380 |
/* translators: %1$s is the attribute group title */ |
| 381 |
printf(esc_html__('Choose %1$s', 'fluent-cart'), esc_html($group->title)); |
| 382 |
?> |
| 383 |
</span> |
| 384 |
<span class="fct-custom-select__arrow" aria-hidden="true"></span> |
| 385 |
</div> |
| 386 |
<div class="fct-custom-select__options" role="listbox" id="<?php echo esc_attr($listboxId); ?>"> |
| 387 |
<?php foreach ($terms as $term) : |
| 388 |
$settings = is_array($term->settings) ? $term->settings : []; |
| 389 |
$image = Arr::get($settings, 'image', ''); |
| 390 |
?> |
| 391 |
<div class="fct-custom-select__option" |
| 392 |
data-fct-select-option |
| 393 |
role="option" |
| 394 |
aria-selected="false" |
| 395 |
data-value="<?php echo esc_attr($term->id); ?>" |
| 396 |
data-term-slug="<?php echo esc_attr($term->slug); ?>"> |
| 397 |
<div class="fct-image-option"> |
| 398 |
<?php if ($image) : ?> |
| 399 |
<img |
| 400 |
class="fct-image-src" |
| 401 |
src="<?php echo esc_url($image); ?>" |
| 402 |
alt="<?php echo esc_attr($term->title); ?>" |
| 403 |
/> |
| 404 |
<?php endif; ?> |
| 405 |
<span class="fct-image-label"> |
| 406 |
<?php echo esc_html($term->title); ?> |
| 407 |
</span> |
| 408 |
</div> |
| 409 |
</div> |
| 410 |
<?php endforeach; ?> |
| 411 |
</div> |
| 412 |
</div> |
| 413 |
</div> |
| 414 |
<?php |
| 415 |
} |
| 416 |
|
| 417 |
protected function renderAttributeSwatches($group, $terms) |
| 418 |
{ |
| 419 |
$groupSettings = is_array($group->settings) ? $group->settings : []; |
| 420 |
// Image groups render thumbnail-only pills (no caption under each |
| 421 |
// image), so the currently selected term's title is surfaced next to |
| 422 |
// the group label instead — e.g. "Image: Design 2". |
| 423 |
$isImageGroup = Arr::get($groupSettings, 'type') === 'image'; |
| 424 |
?> |
| 425 |
<div class="fct-attribute-selector fct-swatch-selector<?php echo $isImageGroup ? ' fct-image-group' : ''; ?>" data-group-id="<?php echo esc_attr($group->id); ?>"> |
| 426 |
<label class="fct-attribute-label"> |
| 427 |
<?php echo esc_html($group->title); ?> |
| 428 |
<?php if ($isImageGroup) : ?> |
| 429 |
<span class="fct-selected-term-title" aria-live="polite"></span> |
| 430 |
<?php endif; ?> |
| 431 |
</label> |
| 432 |
<div class="fct-swatch-options" role="radiogroup" aria-label="<?php echo esc_attr($group->title); ?>"> |
| 433 |
<?php foreach ($terms as $term) : |
| 434 |
$settings = is_array($term->settings) ? $term->settings : []; |
| 435 |
$color = $this->safeCssColor(Arr::get($settings, 'color', '')); |
| 436 |
$image = Arr::get($settings, 'image', ''); |
| 437 |
?> |
| 438 |
<?php if (!empty($color)) : ?> |
| 439 |
<button class="fct-swatch-item fct-swatch-color" |
| 440 |
data-term-id="<?php echo esc_attr($term->id); ?>" |
| 441 |
data-group-id="<?php echo esc_attr($group->id); ?>" |
| 442 |
data-term-title="<?php echo esc_attr($term->title); ?>" |
| 443 |
title="<?php echo esc_attr($term->title); ?>" |
| 444 |
role="radio" |
| 445 |
aria-checked="false" |
| 446 |
tabindex="0" |
| 447 |
type="button"> |
| 448 |
<span class="fct-swatch-dot" style="background-color: <?php echo esc_attr($color); ?>"></span> |
| 449 |
<span class="fct-swatch-text"><?php echo esc_html($term->title); ?></span> |
| 450 |
</button> |
| 451 |
<?php elseif (!empty($image)) : ?> |
| 452 |
<button class="fct-swatch-item fct-swatch-image<?php echo $isImageGroup ? ' fct-swatch-image-only' : ''; ?>" |
| 453 |
data-term-id="<?php echo esc_attr($term->id); ?>" |
| 454 |
data-group-id="<?php echo esc_attr($group->id); ?>" |
| 455 |
data-term-title="<?php echo esc_attr($term->title); ?>" |
| 456 |
title="<?php echo esc_attr($term->title); ?>" |
| 457 |
role="radio" |
| 458 |
aria-checked="false" |
| 459 |
tabindex="0" |
| 460 |
type="button"> |
| 461 |
<span class="fct-swatch-thumb"> |
| 462 |
<img src="<?php echo esc_url($image); ?>" alt="<?php echo esc_attr($term->title); ?>" /> |
| 463 |
</span> |
| 464 |
<?php if (!$isImageGroup) : ?> |
| 465 |
<span class="fct-swatch-text"><?php echo esc_html($term->title); ?></span> |
| 466 |
<?php endif; ?> |
| 467 |
</button> |
| 468 |
<?php else : ?> |
| 469 |
<button class="fct-swatch-item fct-swatch-label" |
| 470 |
data-term-id="<?php echo esc_attr($term->id); ?>" |
| 471 |
data-group-id="<?php echo esc_attr($group->id); ?>" |
| 472 |
data-term-title="<?php echo esc_attr($term->title); ?>" |
| 473 |
role="radio" |
| 474 |
aria-checked="false" |
| 475 |
tabindex="0" |
| 476 |
type="button"> |
| 477 |
<?php echo esc_html($term->title); ?> |
| 478 |
</button> |
| 479 |
<?php endif; ?> |
| 480 |
<?php endforeach; ?> |
| 481 |
</div> |
| 482 |
</div> |
| 483 |
<?php |
| 484 |
} |
| 485 |
} |
| 486 |
|