PluginProbe
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder / 3.1.4
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder v3.1.4
3.1.4 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.0.7 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.5.0 1.5.1 1.5.2 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 All 93 releases
ultimate-store-kit / src / js / modules / grid-variations.js

grid-variations.js in Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder 3.1.4, at src/js/modules/grid-variations.js

1,111 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Ultimate Store Kit - Grid Variations JS
3 *
4 * Handles the interactive behavior of variation swatches in product grids
5 */
6
7 class USKGridVariations {
8 constructor($container) {
9 this.$container = $container;
10 this.productId = $container.data("product-id");
11 this.$swatchWrappers = $container.find(".usk-variation-swatches__wrapper");
12 this.availableVariations = this.getAvailableVariations();
13 this.sequentialMode = $container.data("sequential") === true;
14 this.currentStep = 0;
15 this.totalSteps = 0;
16
17 // Initialize the container
18 this.initContainer();
19
20 // Bind methods to maintain context
21 this.bindMethods();
22
23 // Set up event handlers
24 this.setupEventListeners();
25
26 // Initialize variations
27 this.initActiveVariations();
28
29 // Initialize available attributes immediately to disable unavailable options
30 this.updateAvailableAttributes(true); // Passing true to indicate initial load
31
32 // Setup sequential mode if enabled
33 if (this.sequentialMode) {
34 this.setupSequentialMode();
35 }
36
37 // Trigger initialization event
38 jQuery(document.body).trigger("usk_grid_variations_init", this);
39 }
40
41 // Initialize container with required elements
42 initContainer() {
43 this.$container.off(".usk-grid-variations");
44
45 // Add classes
46 if (this.$swatchWrappers.length) {
47 this.$container.addClass("swatches-support");
48 }
49
50 if (this.sequentialMode) {
51 this.$container.addClass("usk-sequential-variations");
52 }
53
54 // Add required buttons
55 this.addResetButton();
56
57 if (this.sequentialMode) {
58 this.addBackButton();
59 }
60 }
61
62 // Bind methods to maintain 'this' context
63 bindMethods() {
64 this.getChosenAttributes = this.getChosenAttributes.bind(this);
65 this.onSwatchSelect = this.onSwatchSelect.bind(this);
66 this.onVariationButtonClick = this.onVariationButtonClick.bind(this);
67 this.onKeyPress = this.onKeyPress.bind(this);
68 this.onResetClick = this.onResetClick.bind(this);
69 this.onBackClick = this.onBackClick.bind(this);
70 }
71
72 // Set up event listeners
73 setupEventListeners() {
74 this.$container.on(
75 "click.usk-grid-variations",
76 ".usk-variation-swatches__item",
77 this.onSwatchSelect
78 );
79
80 this.$container.on(
81 "click.usk-grid-variations",
82 ".usk-variation-button",
83 this.onVariationButtonClick
84 );
85
86 this.$container.on(
87 "keydown.usk-grid-variations",
88 ".usk-variation-swatches__item, .usk-variation-button",
89 this.onKeyPress
90 );
91
92 this.$container.on(
93 "click.usk-grid-variations",
94 ".usk-reset-variations",
95 this.onResetClick
96 );
97
98 this.$container.on(
99 "click.usk-grid-variations",
100 ".usk-back-variation",
101 this.onBackClick
102 );
103 }
104
105 // Add reset button to the variations container
106 addResetButton() {
107 if (this.$container.find(".usk-reset-variations").length === 0) {
108 const $resetButton =
109 jQuery(`<button type="button" class="usk-reset-variations" aria-label="Reset">
110 <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
111 <path
112 stroke="currentColor"
113 stroke-linecap="round"
114 stroke-linejoin="round"
115 stroke-width="2"
116 d="M17.651 7.65a7.131 7.131 0 0 0-12.68 3.15M18.001 4v4h-4m-7.652 8.35a7.13 7.13 0 0 0 12.68-3.15M6 20v-4h4"
117 />
118 </svg>
119 </button>
120 `);
121 $resetButton.insertAfter(
122 this.$container.find(".usk-variation-group").last()
123 );
124 $resetButton.hide();
125 }
126 }
127
128 // Add back button for sequential variation selection
129 addBackButton() {
130 if (this.$container.find(".usk-back-variation").length === 0) {
131 const $backButton = jQuery(`
132 <button type="button" class="usk-back-variation" aria-label="Go Back">
133 <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
134 <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>
135 </svg>
136 </button>
137 `);
138
139 $backButton.insertAfter(
140 this.$container.find(".usk-variation-group").first()
141 );
142 $backButton.hide();
143 }
144 }
145
146 // Setup sequential variation selection mode
147 setupSequentialMode() {
148 this.totalSteps =
149 this.$container.find(".usk-variation-group").length ||
150 this.$swatchWrappers.length;
151
152 if (this.totalSteps > 1) {
153 this.$container.find(".usk-variation-group").each((index, el) => {
154 jQuery(el).toggle(index === 0);
155 });
156
157 this.$swatchWrappers.each((index, el) => {
158 jQuery(el).toggle(index === 0);
159 });
160 }
161
162 this.currentStep = 0;
163 }
164
165 // Go to the next variation step
166 goToNextStep() {
167 if (this.currentStep < this.totalSteps - 1) {
168 this.currentStep++;
169
170 this.$container.find(".usk-variation-group").each((index, el) => {
171 jQuery(el).toggle(index === this.currentStep);
172 });
173
174 this.$swatchWrappers.each((index, el) => {
175 jQuery(el).toggle(index === this.currentStep);
176 });
177
178 if (this.currentStep > 0) {
179 this.$container.find(".usk-back-variation").show();
180 }
181 }
182 }
183
184 // Go to the previous variation step
185 goToPreviousStep() {
186 if (this.currentStep > 0) {
187 this.currentStep--;
188
189 this.$container.find(".usk-variation-group").each((index, el) => {
190 jQuery(el).toggle(index === this.currentStep);
191 });
192
193 this.$swatchWrappers.each((index, el) => {
194 jQuery(el).toggle(index === this.currentStep);
195 });
196
197 if (this.currentStep === 0) {
198 this.$container.find(".usk-back-variation").hide();
199 }
200 }
201 }
202
203 // Handle back button click
204 onBackClick(event) {
205 event.preventDefault();
206 this.goToPreviousStep();
207 }
208
209 // Handle reset button click
210 onResetClick(event) {
211 event.preventDefault();
212
213 // Reset swatches and buttons
214 this.$container
215 .find(".usk-variation-swatches__item, .usk-variation-button")
216 .removeClass("selected active disabled")
217 .data("disabled", false)
218 .attr("aria-pressed", "false")
219 .attr("tabindex", 0);
220
221 // Reset data attributes
222 this.resetDataAttributes();
223
224 // Reset Add to Cart button
225 this.resetAddToCartButton();
226
227 // Reset UI elements
228 this.$container.find(".usk-reset-variations").hide();
229 this.removeVariationSummary();
230 this.$container.find(".usk-step-summary").remove();
231
232 // Reset sequential mode if enabled
233 if (this.sequentialMode) {
234 this.resetSequentialMode();
235 }
236
237 // Update available attributes
238 this.updateAvailableAttributes(true);
239 }
240
241 // Reset all data attributes
242 resetDataAttributes() {
243 // Reset variation group data
244 this.$container.find(".usk-variation-group").each((i, el) => {
245 const $group = jQuery(el);
246 const $button = $group.find(".usk-variation-button").first();
247
248 if ($button.length) {
249 const attrName = $button.data("attribute");
250 if (attrName) {
251 this.$container.removeData("selected-attribute_" + attrName);
252 }
253 }
254 });
255
256 // Reset swatch wrapper data
257 this.$swatchWrappers.each((i, el) => {
258 const attrName = jQuery(el).data("attribute_name");
259 if (attrName) {
260 this.$container.removeData("selected-" + attrName);
261 }
262 });
263
264 // Reset variation ID
265 this.$container.removeData("variation-id");
266 }
267
268 // Reset Add to Cart button to default state
269 resetAddToCartButton() {
270 const $productItem = this.$container.closest(".usk-item");
271 const $addToCartBtn = $productItem.find(".usk-button");
272
273 if (!$addToCartBtn.length) return;
274
275 $addToCartBtn
276 .removeClass("product_type_variation add_to_cart_button ajax_add_to_cart")
277 .addClass("product_type_variable")
278 .removeAttr("data-variation_id")
279 .attr("href", "javascript:void(0)");
280
281 // Remove attribute data
282 try {
283 jQuery.each($addToCartBtn[0].attributes, (i, attr) => {
284 if (attr && attr.name && attr.name.indexOf("data-attribute_") === 0) {
285 $addToCartBtn.removeAttr(attr.name);
286 }
287 });
288 } catch (e) {
289 console.log("Error clearing button attributes:", e);
290 }
291
292 // Update button text
293 const buttonText = $addToCartBtn.find(".usk-icon-arrow-right-8").length
294 ? 'Select options <i class="button-icon usk-icon-arrow-right-8"></i>'
295 : "Select options";
296
297 $addToCartBtn.html(buttonText);
298 }
299
300 // Reset sequential mode to first step
301 resetSequentialMode() {
302 this.$container.find(".usk-back-variation").hide();
303 this.currentStep = 0;
304
305 this.$container.find(".usk-variation-group").each((index, el) => {
306 jQuery(el).toggle(index === 0);
307 });
308
309 this.$swatchWrappers.each((index, el) => {
310 jQuery(el).toggle(index === 0);
311 });
312 }
313
314 // Get available variations from data attribute or AJAX
315 getAvailableVariations() {
316 let variations = this.$container.data("available_variations");
317
318 if (!variations && typeof usk_vars !== "undefined" && usk_vars.ajax_url) {
319 jQuery.ajax({
320 url: usk_vars.ajax_url,
321 type: "POST",
322 async: false,
323 data: {
324 action: "usk_get_available_variations",
325 product_id: this.productId,
326 },
327 success: (response) => {
328 if (response.success && response.data) {
329 variations = response.data;
330 this.$container.data("available_variations", variations);
331 }
332 },
333 });
334 }
335
336 return variations || [];
337 }
338
339 // Initialize active variation items
340 initActiveVariations() {
341 let selectedCount = 0;
342
343 // Activate pre-selected swatches
344 this.$container
345 .find(".usk-variation-swatches__item.selected")
346 .each((i, el) => {
347 jQuery(el).trigger("click.usk-grid-variations");
348 selectedCount++;
349 });
350
351 // Activate pre-selected buttons
352 this.$container.find(".usk-variation-button.active").each((i, el) => {
353 jQuery(el).trigger("click.usk-grid-variations");
354 selectedCount++;
355 });
356
357 // Select default attributes if nothing is selected
358 if (selectedCount === 0) {
359 this.selectDefaultAttributes();
360 }
361
362 // Toggle reset button
363 this.toggleResetButton();
364 }
365
366 // Select default attributes if provided
367 selectDefaultAttributes() {
368 const defaultAttributes = this.$container.data("default_attributes");
369 if (!defaultAttributes) return;
370
371 for (const attrName in defaultAttributes) {
372 if (defaultAttributes.hasOwnProperty(attrName)) {
373 const value = defaultAttributes[attrName];
374
375 // Try to find and select matching swatch
376 const $item = this.$container.find(
377 `.usk-variation-swatches__item[data-value="${value}"]`
378 );
379
380 if ($item.length) {
381 $item.trigger("click.usk-grid-variations");
382 } else {
383 // Try to find and select matching button
384 const $button = this.$container.find(
385 `.usk-variation-button[data-value="${value}"]`
386 );
387
388 if ($button.length) {
389 $button.trigger("click.usk-grid-variations");
390 }
391 }
392 }
393 }
394 }
395
396 // Toggle reset button visibility based on selections
397 toggleResetButton() {
398 const attributes = this.getChosenAttributes();
399 const $resetButton = this.$container.find(".usk-reset-variations");
400
401 $resetButton.toggle(attributes.chosenCount > 0);
402 }
403
404 // Handle click on a swatch
405 onSwatchSelect(event) {
406 event.preventDefault();
407
408 const $swatch = jQuery(event.currentTarget);
409
410 if ($swatch.hasClass("disabled") || $swatch.data("disabled")) {
411 return;
412 }
413
414 const $wrapper = $swatch.closest(".usk-variation-swatches__wrapper");
415 const attributeName = $wrapper.data("attribute_name");
416 const value = $swatch.data("value");
417
418 // Update UI
419 $wrapper
420 .find(".usk-variation-swatches__item")
421 .removeClass("selected")
422 .attr("aria-pressed", "false");
423
424 $swatch.addClass("selected").attr("aria-pressed", "true");
425
426 // Store selection
427 this.$container.data("selected-" + attributeName, value);
428
429 // Update state
430 this.updateAvailableAttributes();
431 this.toggleResetButton();
432 this.updateAddToCartButton();
433
434 // Handle sequential mode
435 if (this.sequentialMode && this.currentStep < this.totalSteps - 1) {
436 this.goToNextStep();
437 }
438 }
439
440 // Handle click on variation button
441 onVariationButtonClick(event) {
442 event.preventDefault();
443
444 const $button = jQuery(event.currentTarget);
445 const attribute = $button.data("attribute");
446 const value = $button.data("value");
447
448 if ($button.hasClass("disabled") || $button.data("disabled")) {
449 return;
450 }
451
452 // Update UI
453 $button.siblings(".usk-variation-button").removeClass("active");
454 $button.addClass("active");
455
456 // Store selection
457 this.$container.data("selected-attribute_" + attribute, value);
458
459 // Update state
460 this.updateAvailableAttributes();
461 this.toggleResetButton();
462 this.updateAddToCartButton();
463
464 // Handle sequential mode
465 if (this.sequentialMode && this.currentStep < this.totalSteps - 1) {
466 this.goToNextStep();
467 }
468 }
469
470 // Update available attributes based on current selection
471 updateAvailableAttributes(isInitialLoad = false) {
472 const attributes = this.getChosenAttributes();
473 const currentAttributes = attributes.data;
474 const variations = this.availableVariations;
475
476 // If no variations data, we can't determine availability
477 if (!variations || !variations.length) {
478 return;
479 }
480
481 // Reset all to enabled state
482 this.$container
483 .find(".usk-variation-swatches__item, .usk-variation-button")
484 .removeClass("disabled")
485 .data("disabled", false)
486 .attr("tabindex", 0);
487
488 // First pass: Mark all options that don't have any valid variations as disabled
489 // This should happen even if no attributes are chosen yet (on initial load)
490 if (isInitialLoad || !attributes.chosenCount) {
491 // Process swatch wrappers - mark all unavailable attributes as disabled immediately
492 this.$swatchWrappers.each((i, wrapper) => {
493 const $wrapper = jQuery(wrapper);
494 const attributeName = $wrapper.data("attribute_name");
495
496 $wrapper.find(".usk-variation-swatches__item").each((j, item) => {
497 const $item = jQuery(item);
498 const attributeValue = $item.data("value");
499
500 // Check if this value appears in any variation
501 let isAvailable = false;
502 for (let v = 0; v < variations.length; v++) {
503 const variation = variations[v];
504
505 if (!variation || !variation.attributes ||
506 !variation.is_in_stock || !variation.is_purchasable) {
507 continue;
508 }
509
510 const variationAttrs = variation.attributes;
511
512 // Check if this variation includes this attribute value
513 if (variationAttrs[attributeName] === "" ||
514 variationAttrs[attributeName] === attributeValue) {
515 isAvailable = true;
516 break;
517 }
518 }
519
520 // Disable if not available in any variation
521 if (!isAvailable) {
522 $item
523 .addClass("disabled")
524 .data("disabled", true)
525 .attr("tabindex", -1);
526 }
527 });
528 });
529
530 // Process variation buttons - mark all unavailable buttons as disabled immediately
531 this.$container.find(".usk-variation-group").each((i, group) => {
532 const $group = jQuery(group);
533 const firstButton = $group.find(".usk-variation-button").first();
534 const attributeName = "attribute_" + firstButton.data("attribute");
535
536 $group.find(".usk-variation-button").each((j, button) => {
537 const $button = jQuery(button);
538 const attributeValue = $button.data("value");
539
540 // Check if this value appears in any variation
541 let isAvailable = false;
542 for (let v = 0; v < variations.length; v++) {
543 const variation = variations[v];
544
545 if (!variation || !variation.attributes ||
546 !variation.is_in_stock || !variation.is_purchasable) {
547 continue;
548 }
549
550 const variationAttrs = variation.attributes;
551
552 // Check if this variation includes this attribute value
553 if (variationAttrs[attributeName] === "" ||
554 variationAttrs[attributeName] === attributeValue) {
555 isAvailable = true;
556 break;
557 }
558 }
559
560 // Disable if not available in any variation
561 if (!isAvailable) {
562 $button
563 .addClass("disabled")
564 .data("disabled", true)
565 .attr("tabindex", -1);
566 }
567 });
568 });
569
570 return;
571 }
572
573 // Second pass: If attributes are chosen, show only compatible options
574 // Process swatch wrappers
575 this.$swatchWrappers.each((i, wrapper) => {
576 const $wrapper = jQuery(wrapper);
577 const attributeName = $wrapper.data("attribute_name");
578
579 $wrapper.find(".usk-variation-swatches__item").each((j, item) => {
580 const $item = jQuery(item);
581 const attributeValue = $item.data("value");
582
583 // Disable if not available with current selections
584 if (
585 !this.isAttributeAvailable(
586 attributeName,
587 attributeValue,
588 currentAttributes
589 )
590 ) {
591 $item
592 .addClass("disabled")
593 .data("disabled", true)
594 .attr("tabindex", -1);
595 }
596 });
597 });
598
599 // Process variation buttons
600 this.$container.find(".usk-variation-group").each((i, group) => {
601 const $group = jQuery(group);
602 const firstButton = $group.find(".usk-variation-button").first();
603 const attributeName = "attribute_" + firstButton.data("attribute");
604
605 $group.find(".usk-variation-button").each((j, button) => {
606 const $button = jQuery(button);
607 const attributeValue = $button.data("value");
608
609 // Disable if not available with current selections
610 if (
611 !this.isAttributeAvailable(
612 attributeName,
613 attributeValue,
614 currentAttributes
615 )
616 ) {
617 $button
618 .addClass("disabled")
619 .data("disabled", true)
620 .attr("tabindex", -1);
621 }
622 });
623 });
624 }
625
626 // Check if a specific attribute value is available based on current selections
627 isAttributeAvailable(attributeName, attributeValue, currentAttributes) {
628 const variations = this.availableVariations;
629
630 // Basic validation
631 if (!attributeName || !attributeValue || !currentAttributes) {
632 return true;
633 }
634
635 if (!variations || !variations.length) {
636 return true;
637 }
638
639 // Create test attributes object
640 const testAttributes = {};
641 for (const key in currentAttributes) {
642 if (
643 currentAttributes.hasOwnProperty(key) &&
644 currentAttributes[key] !== ""
645 ) {
646 testAttributes[key] = currentAttributes[key];
647 }
648 }
649
650 // Add the attribute we're testing
651 testAttributes[attributeName] = attributeValue;
652
653 // Check for matching variations
654 for (let i = 0; i < variations.length; i++) {
655 const variation = variations[i];
656
657 if (!variation || !variation.attributes) {
658 continue;
659 }
660
661 const attributes = variation.attributes;
662 let match = true;
663
664 // Check if this variation matches test attributes
665 for (const testKey in testAttributes) {
666 if (testAttributes.hasOwnProperty(testKey)) {
667 const testValue = testAttributes[testKey];
668
669 // Skip if variation doesn't define this attribute
670 if (typeof attributes[testKey] === "undefined") {
671 continue;
672 }
673
674 // If variation attribute is empty, it matches any value
675 if (attributes[testKey] === "") {
676 continue;
677 }
678
679 // Check for exact match
680 if (attributes[testKey] !== testValue) {
681 match = false;
682 break;
683 }
684 }
685 }
686
687 // Return true if we found a matching variation
688 if (match && variation.is_in_stock && variation.is_purchasable) {
689 return true;
690 }
691 }
692
693 return false;
694 }
695
696 // Handle keypress events for accessibility
697 onKeyPress(event) {
698 const isSpace =
699 (event.keyCode && event.keyCode === 32) ||
700 (event.key && event.key === " ");
701 const isEnter =
702 (event.keyCode && event.keyCode === 13) ||
703 (event.key && event.key.toLowerCase() === "enter");
704
705 if (isSpace || isEnter) {
706 event.preventDefault();
707 jQuery(event.currentTarget).trigger("click.usk-grid-variations");
708 }
709 }
710
711 // Update product image with the provided URL
712 updateProductImage(imageUrl) {
713 if (!imageUrl) return;
714
715 const $productItem = this.$container.closest(".usk-item");
716 const $defaultImage = $productItem.find(".usk-image .img.image-default");
717 const $hoverImage = $productItem.find(".usk-image .img.image-hover");
718
719 // Update images with new src
720 if ($defaultImage.length) {
721 $defaultImage.attr("src", imageUrl);
722 }
723
724 if ($hoverImage.length) {
725 $hoverImage.attr("src", imageUrl);
726 }
727
728 // Also update srcset if it exists
729 if ($defaultImage.attr("srcset")) {
730 $defaultImage.attr("srcset", "");
731 }
732
733 if ($hoverImage.attr("srcset")) {
734 $hoverImage.attr("srcset", "");
735 }
736 }
737
738 // Update Add to Cart button based on selected variations
739 updateAddToCartButton() {
740 const $productItem = this.$container.closest(".usk-item");
741 const $addToCartBtn = $productItem.find(".usk-button");
742
743 if (!$addToCartBtn.length) {
744 return;
745 }
746
747 // Check if all attributes are selected
748 const attributes = this.getChosenAttributes();
749 if (!attributes || attributes.chosenCount !== attributes.count) {
750 this.resetAddToCartToSelectOptions($addToCartBtn);
751 return;
752 }
753
754 // Make sure we have all required variations selected
755 const requiredAttributeCount = this.getTotalRequiredAttributes();
756 if (attributes.chosenCount < requiredAttributeCount) {
757 this.resetAddToCartToSelectOptions($addToCartBtn);
758 return;
759 }
760
761 // Find matching variation
762 this.findMatchingVariation(
763 attributes.data,
764 (variationId, variationData) => {
765 // Store variation ID
766 this.$container.data("variation-id", variationId);
767
768 if (!variationId) {
769 this.setUnavailableButton($addToCartBtn);
770 return;
771 }
772
773 // Update the product image if variation has an image
774 if (variationData && variationData.image && variationData.image.src) {
775 this.updateProductImage(variationData.image.src);
776 }
777
778 this.setAddToCartButton($addToCartBtn, variationId, attributes);
779 }
780 );
781 }
782
783 // Reset button to "Select Options" state
784 resetAddToCartToSelectOptions($button) {
785 $button
786 .removeClass("product_type_variation add_to_cart_button ajax_add_to_cart")
787 .addClass("product_type_variable")
788 .removeAttr("data-variation_id")
789 .attr("href", "javascript:void(0)");
790
791 // Clear attributes
792 this.clearButtonAttributes($button);
793
794 // Update text
795 const buttonText = $button.find(".usk-icon-arrow-right-8").length
796 ? 'Select options <i class="button-icon usk-icon-arrow-right-8"></i>'
797 : "Select options";
798
799 $button.html(buttonText);
800
801 // Remove any summary
802 this.removeVariationSummary();
803 }
804
805 // Set button to "Unavailable" state
806 setUnavailableButton($button) {
807 $button
808 .removeClass("product_type_variation add_to_cart_button ajax_add_to_cart")
809 .addClass("product_type_variable")
810 .removeAttr("data-variation_id")
811 .attr("href", "javascript:void(0)");
812
813 // Update text
814 const buttonText = $button.find(".usk-icon-arrow-right-8").length
815 ? 'Unavailable <i class="button-icon usk-icon-arrow-right-8"></i>'
816 : "Unavailable";
817
818 $button.html(buttonText);
819
820 // Remove any summary
821 this.removeVariationSummary();
822 }
823
824 // Set button to "Add to Cart" state with variation data
825 setAddToCartButton($button, variationId, attributes) {
826 // Clear existing attributes
827 this.clearButtonAttributes($button);
828
829 // Set button properties
830 $button
831 .removeClass("product_type_variable")
832 .addClass("product_type_variation add_to_cart_button ajax_add_to_cart")
833 .attr("data-product_id", this.productId)
834 .attr("data-variation_id", variationId)
835 .attr("data-prevent_redirect", "true")
836 .attr("href", "javascript:void(0)");
837
838 // Add attribute data
839 if (attributes.data) {
840 jQuery.each(attributes.data, (name, value) => {
841 if (name && value) {
842 $button.attr("data-" + name, value);
843 }
844 });
845 }
846
847 // Update text
848 const buttonText = $button.find(".usk-icon-arrow-right-8").length
849 ? 'Add to cart <i class="button-icon usk-icon-arrow-right-8"></i>'
850 : "Add to cart";
851
852 $button.html(buttonText);
853 }
854
855 // Clear data-attribute_ properties from button
856 clearButtonAttributes($button) {
857 try {
858 jQuery.each($button[0].attributes, (i, attr) => {
859 if (attr && attr.name && attr.name.indexOf("data-attribute_") === 0) {
860 $button.removeAttr(attr.name);
861 }
862 });
863 } catch (e) {
864 console.log("Error clearing attributes:", e);
865 }
866 }
867
868 // Remove the variation summary display
869 removeVariationSummary() {
870 this.$container.find(".usk-variation-summary").remove();
871 }
872
873 // Find matching variation ID for the given attributes
874 findMatchingVariation(attributesData, callback) {
875 if (!attributesData) {
876 callback(null);
877 return;
878 }
879
880 // Try to find match locally first
881 const matchingVariation = this.findMatchingVariationLocally(attributesData);
882
883 if (matchingVariation) {
884 callback(matchingVariation.variation_id, matchingVariation);
885 return;
886 }
887
888 // If not found locally, try server
889 this.findMatchingVariationOnServer(attributesData, callback);
890 }
891
892 // Find matching variation locally
893 findMatchingVariationLocally(attributesData) {
894 const variations = this.availableVariations;
895
896 if (!variations || !variations.length) {
897 return null;
898 }
899
900 for (let i = 0; i < variations.length; i++) {
901 const variation = variations[i];
902
903 if (!variation || !variation.attributes) {
904 continue;
905 }
906
907 const attributes = variation.attributes;
908 let match = true;
909
910 // Check if variation matches all selected attributes
911 for (const attrName in attributesData) {
912 if (
913 attributesData.hasOwnProperty(attrName) &&
914 attributesData[attrName] !== ""
915 ) {
916 const attrValue = attributesData[attrName];
917
918 // Skip if variation doesn't define this attribute
919 if (typeof attributes[attrName] === "undefined") {
920 continue;
921 }
922
923 // If variation attribute is empty, it matches any value
924 if (attributes[attrName] === "") {
925 continue;
926 }
927
928 // Check for exact match
929 if (attributes[attrName] !== attrValue) {
930 match = false;
931 break;
932 }
933 }
934 }
935
936 // Return variation if we found a match
937 if (match && variation.is_in_stock && variation.is_purchasable) {
938 return variation;
939 }
940 }
941
942 return null;
943 }
944
945 // Find matching variation via AJAX
946 findMatchingVariationOnServer(attributesData, callback) {
947 if (typeof usk_vars === "undefined" || !usk_vars.ajax_url) {
948 callback(null);
949 return;
950 }
951
952 // Format attributes for AJAX
953 const formattedAttributes = {};
954 for (const key in attributesData) {
955 if (attributesData.hasOwnProperty(key) && attributesData[key]) {
956 const attrName = key.replace("attribute_", "");
957 formattedAttributes[attrName] = attributesData[key];
958 }
959 }
960
961 jQuery.ajax({
962 url: usk_vars.ajax_url,
963 type: "POST",
964 data: {
965 action: "usk_get_available_variations",
966 product_id: this.productId,
967 attributes: formattedAttributes,
968 security: usk_vars.nonce || "",
969 },
970 success: (response) => {
971 if (response && response.success && response.data) {
972 callback(response.data.variation_id, response.data);
973 } else {
974 callback(null);
975 }
976 },
977 error: () => {
978 callback(null);
979 },
980 });
981 }
982
983 // Get chosen attributes from container
984 getChosenAttributes() {
985 const data = {};
986 let count = 0;
987 let chosen = 0;
988
989 // Get attributes from variation swatches
990 if (this.$swatchWrappers && this.$swatchWrappers.length) {
991 this.$swatchWrappers.each((i, wrapper) => {
992 const $wrapper = jQuery(wrapper);
993 const attributeName = $wrapper.data("attribute_name");
994
995 if (!attributeName) {
996 return true; // Skip this iteration
997 }
998
999 const $selected = $wrapper.find(
1000 ".usk-variation-swatches__item.selected"
1001 );
1002 const value = $selected.length ? $selected.data("value") : "";
1003
1004 if (value) {
1005 chosen++;
1006 }
1007
1008 count++;
1009 data[attributeName] = value;
1010 });
1011 }
1012
1013 // Get attributes from variation buttons
1014 if (this.$container) {
1015 this.$container.find(".usk-variation-group").each((i, group) => {
1016 const $group = jQuery(group);
1017 const $activeBtn = $group.find(".usk-variation-button.active");
1018
1019 if (!$activeBtn.length) {
1020 return true;
1021 }
1022
1023 const attribute = $activeBtn.data("attribute");
1024 if (!attribute) {
1025 return true;
1026 }
1027
1028 const attributeName = "attribute_" + attribute;
1029 const value = $activeBtn.data("value") || "";
1030
1031 // Only add if not already set by swatches
1032 if (!data[attributeName]) {
1033 if (value) {
1034 chosen++;
1035 }
1036
1037 count++;
1038 data[attributeName] = value;
1039 }
1040 });
1041 }
1042
1043 return {
1044 count: count,
1045 chosenCount: chosen,
1046 data: data,
1047 };
1048 }
1049
1050 // Get total number of required attributes
1051 getTotalRequiredAttributes() {
1052 let count = 0;
1053
1054 // Count variation swatches
1055 if (this.$swatchWrappers && this.$swatchWrappers.length) {
1056 this.$swatchWrappers.each((i, wrapper) => {
1057 const $wrapper = jQuery(wrapper);
1058 if ($wrapper.data("attribute_name")) {
1059 count++;
1060 }
1061 });
1062 }
1063
1064 // Count variation buttons in groups that aren't already counted
1065 const countedAttributes = new Set();
1066 this.$swatchWrappers.each((i, wrapper) => {
1067 const attrName = jQuery(wrapper).data("attribute_name");
1068 if (attrName) {
1069 countedAttributes.add(attrName.replace('attribute_', ''));
1070 }
1071 });
1072
1073 this.$container.find(".usk-variation-group").each((i, group) => {
1074 const $group = jQuery(group);
1075 const $firstBtn = $group.find(".usk-variation-button").first();
1076 const attribute = $firstBtn.data("attribute");
1077
1078 if (attribute && !countedAttributes.has(attribute)) {
1079 count++;
1080 }
1081 });
1082
1083 return count;
1084 }
1085 }
1086
1087 // Initialize on document ready
1088 jQuery(function ($) {
1089 function initGridVariations() {
1090 $(".usk-variations-container:not(.swatches-support)").each(function () {
1091 new USKGridVariations($(this));
1092 });
1093 }
1094
1095 // Initialize on page load
1096 initGridVariations();
1097
1098 // Reinitialize after cart operations
1099 $(document.body).on(
1100 "wc_fragments_refreshed wc_fragments_loaded added_to_cart",
1101 initGridVariations
1102 );
1103
1104 // Initialize on AJAX content load
1105 $(document).ajaxComplete(function (event, xhr, settings) {
1106 setTimeout(function () {
1107 initGridVariations();
1108 }, 100);
1109 });
1110 });
1111