/** * Ultimate Store Kit - Grid Variations JS * * Handles the interactive behavior of variation swatches in product grids */ class USKGridVariations { constructor($container) { this.$container = $container; this.productId = $container.data("product-id"); this.$swatchWrappers = $container.find(".usk-variation-swatches__wrapper"); this.availableVariations = this.getAvailableVariations(); this.sequentialMode = $container.data("sequential") === true; this.currentStep = 0; this.totalSteps = 0; // Initialize the container this.initContainer(); // Bind methods to maintain context this.bindMethods(); // Set up event handlers this.setupEventListeners(); // Initialize variations this.initActiveVariations(); // Initialize available attributes immediately to disable unavailable options this.updateAvailableAttributes(true); // Passing true to indicate initial load // Setup sequential mode if enabled if (this.sequentialMode) { this.setupSequentialMode(); } // Trigger initialization event jQuery(document.body).trigger("usk_grid_variations_init", this); } // Initialize container with required elements initContainer() { this.$container.off(".usk-grid-variations"); // Add classes if (this.$swatchWrappers.length) { this.$container.addClass("swatches-support"); } if (this.sequentialMode) { this.$container.addClass("usk-sequential-variations"); } // Add required buttons this.addResetButton(); if (this.sequentialMode) { this.addBackButton(); } } // Bind methods to maintain 'this' context bindMethods() { this.getChosenAttributes = this.getChosenAttributes.bind(this); this.onSwatchSelect = this.onSwatchSelect.bind(this); this.onVariationButtonClick = this.onVariationButtonClick.bind(this); this.onKeyPress = this.onKeyPress.bind(this); this.onResetClick = this.onResetClick.bind(this); this.onBackClick = this.onBackClick.bind(this); } // Set up event listeners setupEventListeners() { this.$container.on( "click.usk-grid-variations", ".usk-variation-swatches__item", this.onSwatchSelect ); this.$container.on( "click.usk-grid-variations", ".usk-variation-button", this.onVariationButtonClick ); this.$container.on( "keydown.usk-grid-variations", ".usk-variation-swatches__item, .usk-variation-button", this.onKeyPress ); this.$container.on( "click.usk-grid-variations", ".usk-reset-variations", this.onResetClick ); this.$container.on( "click.usk-grid-variations", ".usk-back-variation", this.onBackClick ); } // Add reset button to the variations container addResetButton() { if (this.$container.find(".usk-reset-variations").length === 0) { const $resetButton = jQuery(` `); $resetButton.insertAfter( this.$container.find(".usk-variation-group").last() ); $resetButton.hide(); } } // Add back button for sequential variation selection addBackButton() { if (this.$container.find(".usk-back-variation").length === 0) { const $backButton = jQuery(` `); $backButton.insertAfter( this.$container.find(".usk-variation-group").first() ); $backButton.hide(); } } // Setup sequential variation selection mode setupSequentialMode() { this.totalSteps = this.$container.find(".usk-variation-group").length || this.$swatchWrappers.length; if (this.totalSteps > 1) { this.$container.find(".usk-variation-group").each((index, el) => { jQuery(el).toggle(index === 0); }); this.$swatchWrappers.each((index, el) => { jQuery(el).toggle(index === 0); }); } this.currentStep = 0; } // Go to the next variation step goToNextStep() { if (this.currentStep < this.totalSteps - 1) { this.currentStep++; this.$container.find(".usk-variation-group").each((index, el) => { jQuery(el).toggle(index === this.currentStep); }); this.$swatchWrappers.each((index, el) => { jQuery(el).toggle(index === this.currentStep); }); if (this.currentStep > 0) { this.$container.find(".usk-back-variation").show(); } } } // Go to the previous variation step goToPreviousStep() { if (this.currentStep > 0) { this.currentStep--; this.$container.find(".usk-variation-group").each((index, el) => { jQuery(el).toggle(index === this.currentStep); }); this.$swatchWrappers.each((index, el) => { jQuery(el).toggle(index === this.currentStep); }); if (this.currentStep === 0) { this.$container.find(".usk-back-variation").hide(); } } } // Handle back button click onBackClick(event) { event.preventDefault(); this.goToPreviousStep(); } // Handle reset button click onResetClick(event) { event.preventDefault(); // Reset swatches and buttons this.$container .find(".usk-variation-swatches__item, .usk-variation-button") .removeClass("selected active disabled") .data("disabled", false) .attr("aria-pressed", "false") .attr("tabindex", 0); // Reset data attributes this.resetDataAttributes(); // Reset Add to Cart button this.resetAddToCartButton(); // Reset UI elements this.$container.find(".usk-reset-variations").hide(); this.removeVariationSummary(); this.$container.find(".usk-step-summary").remove(); // Reset sequential mode if enabled if (this.sequentialMode) { this.resetSequentialMode(); } // Update available attributes this.updateAvailableAttributes(true); } // Reset all data attributes resetDataAttributes() { // Reset variation group data this.$container.find(".usk-variation-group").each((i, el) => { const $group = jQuery(el); const $button = $group.find(".usk-variation-button").first(); if ($button.length) { const attrName = $button.data("attribute"); if (attrName) { this.$container.removeData("selected-attribute_" + attrName); } } }); // Reset swatch wrapper data this.$swatchWrappers.each((i, el) => { const attrName = jQuery(el).data("attribute_name"); if (attrName) { this.$container.removeData("selected-" + attrName); } }); // Reset variation ID this.$container.removeData("variation-id"); } // Reset Add to Cart button to default state resetAddToCartButton() { const $productItem = this.$container.closest(".usk-item"); const $addToCartBtn = $productItem.find(".usk-button"); if (!$addToCartBtn.length) return; $addToCartBtn .removeClass("product_type_variation add_to_cart_button ajax_add_to_cart") .addClass("product_type_variable") .removeAttr("data-variation_id") .attr("href", "javascript:void(0)"); // Remove attribute data try { jQuery.each($addToCartBtn[0].attributes, (i, attr) => { if (attr && attr.name && attr.name.indexOf("data-attribute_") === 0) { $addToCartBtn.removeAttr(attr.name); } }); } catch (e) { console.log("Error clearing button attributes:", e); } // Update button text const buttonText = $addToCartBtn.find(".usk-icon-arrow-right-8").length ? 'Select options ' : "Select options"; $addToCartBtn.html(buttonText); } // Reset sequential mode to first step resetSequentialMode() { this.$container.find(".usk-back-variation").hide(); this.currentStep = 0; this.$container.find(".usk-variation-group").each((index, el) => { jQuery(el).toggle(index === 0); }); this.$swatchWrappers.each((index, el) => { jQuery(el).toggle(index === 0); }); } // Get available variations from data attribute or AJAX getAvailableVariations() { let variations = this.$container.data("available_variations"); if (!variations && typeof usk_vars !== "undefined" && usk_vars.ajax_url) { jQuery.ajax({ url: usk_vars.ajax_url, type: "POST", async: false, data: { action: "usk_get_available_variations", product_id: this.productId, }, success: (response) => { if (response.success && response.data) { variations = response.data; this.$container.data("available_variations", variations); } }, }); } return variations || []; } // Initialize active variation items initActiveVariations() { let selectedCount = 0; // Activate pre-selected swatches this.$container .find(".usk-variation-swatches__item.selected") .each((i, el) => { jQuery(el).trigger("click.usk-grid-variations"); selectedCount++; }); // Activate pre-selected buttons this.$container.find(".usk-variation-button.active").each((i, el) => { jQuery(el).trigger("click.usk-grid-variations"); selectedCount++; }); // Select default attributes if nothing is selected if (selectedCount === 0) { this.selectDefaultAttributes(); } // Toggle reset button this.toggleResetButton(); } // Select default attributes if provided selectDefaultAttributes() { const defaultAttributes = this.$container.data("default_attributes"); if (!defaultAttributes) return; for (const attrName in defaultAttributes) { if (defaultAttributes.hasOwnProperty(attrName)) { const value = defaultAttributes[attrName]; // Try to find and select matching swatch const $item = this.$container.find( `.usk-variation-swatches__item[data-value="${value}"]` ); if ($item.length) { $item.trigger("click.usk-grid-variations"); } else { // Try to find and select matching button const $button = this.$container.find( `.usk-variation-button[data-value="${value}"]` ); if ($button.length) { $button.trigger("click.usk-grid-variations"); } } } } } // Toggle reset button visibility based on selections toggleResetButton() { const attributes = this.getChosenAttributes(); const $resetButton = this.$container.find(".usk-reset-variations"); $resetButton.toggle(attributes.chosenCount > 0); } // Handle click on a swatch onSwatchSelect(event) { event.preventDefault(); const $swatch = jQuery(event.currentTarget); if ($swatch.hasClass("disabled") || $swatch.data("disabled")) { return; } const $wrapper = $swatch.closest(".usk-variation-swatches__wrapper"); const attributeName = $wrapper.data("attribute_name"); const value = $swatch.data("value"); // Update UI $wrapper .find(".usk-variation-swatches__item") .removeClass("selected") .attr("aria-pressed", "false"); $swatch.addClass("selected").attr("aria-pressed", "true"); // Store selection this.$container.data("selected-" + attributeName, value); // Update state this.updateAvailableAttributes(); this.toggleResetButton(); this.updateAddToCartButton(); // Handle sequential mode if (this.sequentialMode && this.currentStep < this.totalSteps - 1) { this.goToNextStep(); } } // Handle click on variation button onVariationButtonClick(event) { event.preventDefault(); const $button = jQuery(event.currentTarget); const attribute = $button.data("attribute"); const value = $button.data("value"); if ($button.hasClass("disabled") || $button.data("disabled")) { return; } // Update UI $button.siblings(".usk-variation-button").removeClass("active"); $button.addClass("active"); // Store selection this.$container.data("selected-attribute_" + attribute, value); // Update state this.updateAvailableAttributes(); this.toggleResetButton(); this.updateAddToCartButton(); // Handle sequential mode if (this.sequentialMode && this.currentStep < this.totalSteps - 1) { this.goToNextStep(); } } // Update available attributes based on current selection updateAvailableAttributes(isInitialLoad = false) { const attributes = this.getChosenAttributes(); const currentAttributes = attributes.data; const variations = this.availableVariations; // If no variations data, we can't determine availability if (!variations || !variations.length) { return; } // Reset all to enabled state this.$container .find(".usk-variation-swatches__item, .usk-variation-button") .removeClass("disabled") .data("disabled", false) .attr("tabindex", 0); // First pass: Mark all options that don't have any valid variations as disabled // This should happen even if no attributes are chosen yet (on initial load) if (isInitialLoad || !attributes.chosenCount) { // Process swatch wrappers - mark all unavailable attributes as disabled immediately this.$swatchWrappers.each((i, wrapper) => { const $wrapper = jQuery(wrapper); const attributeName = $wrapper.data("attribute_name"); $wrapper.find(".usk-variation-swatches__item").each((j, item) => { const $item = jQuery(item); const attributeValue = $item.data("value"); // Check if this value appears in any variation let isAvailable = false; for (let v = 0; v < variations.length; v++) { const variation = variations[v]; if (!variation || !variation.attributes || !variation.is_in_stock || !variation.is_purchasable) { continue; } const variationAttrs = variation.attributes; // Check if this variation includes this attribute value if (variationAttrs[attributeName] === "" || variationAttrs[attributeName] === attributeValue) { isAvailable = true; break; } } // Disable if not available in any variation if (!isAvailable) { $item .addClass("disabled") .data("disabled", true) .attr("tabindex", -1); } }); }); // Process variation buttons - mark all unavailable buttons as disabled immediately this.$container.find(".usk-variation-group").each((i, group) => { const $group = jQuery(group); const firstButton = $group.find(".usk-variation-button").first(); const attributeName = "attribute_" + firstButton.data("attribute"); $group.find(".usk-variation-button").each((j, button) => { const $button = jQuery(button); const attributeValue = $button.data("value"); // Check if this value appears in any variation let isAvailable = false; for (let v = 0; v < variations.length; v++) { const variation = variations[v]; if (!variation || !variation.attributes || !variation.is_in_stock || !variation.is_purchasable) { continue; } const variationAttrs = variation.attributes; // Check if this variation includes this attribute value if (variationAttrs[attributeName] === "" || variationAttrs[attributeName] === attributeValue) { isAvailable = true; break; } } // Disable if not available in any variation if (!isAvailable) { $button .addClass("disabled") .data("disabled", true) .attr("tabindex", -1); } }); }); return; } // Second pass: If attributes are chosen, show only compatible options // Process swatch wrappers this.$swatchWrappers.each((i, wrapper) => { const $wrapper = jQuery(wrapper); const attributeName = $wrapper.data("attribute_name"); $wrapper.find(".usk-variation-swatches__item").each((j, item) => { const $item = jQuery(item); const attributeValue = $item.data("value"); // Disable if not available with current selections if ( !this.isAttributeAvailable( attributeName, attributeValue, currentAttributes ) ) { $item .addClass("disabled") .data("disabled", true) .attr("tabindex", -1); } }); }); // Process variation buttons this.$container.find(".usk-variation-group").each((i, group) => { const $group = jQuery(group); const firstButton = $group.find(".usk-variation-button").first(); const attributeName = "attribute_" + firstButton.data("attribute"); $group.find(".usk-variation-button").each((j, button) => { const $button = jQuery(button); const attributeValue = $button.data("value"); // Disable if not available with current selections if ( !this.isAttributeAvailable( attributeName, attributeValue, currentAttributes ) ) { $button .addClass("disabled") .data("disabled", true) .attr("tabindex", -1); } }); }); } // Check if a specific attribute value is available based on current selections isAttributeAvailable(attributeName, attributeValue, currentAttributes) { const variations = this.availableVariations; // Basic validation if (!attributeName || !attributeValue || !currentAttributes) { return true; } if (!variations || !variations.length) { return true; } // Create test attributes object const testAttributes = {}; for (const key in currentAttributes) { if ( currentAttributes.hasOwnProperty(key) && currentAttributes[key] !== "" ) { testAttributes[key] = currentAttributes[key]; } } // Add the attribute we're testing testAttributes[attributeName] = attributeValue; // Check for matching variations for (let i = 0; i < variations.length; i++) { const variation = variations[i]; if (!variation || !variation.attributes) { continue; } const attributes = variation.attributes; let match = true; // Check if this variation matches test attributes for (const testKey in testAttributes) { if (testAttributes.hasOwnProperty(testKey)) { const testValue = testAttributes[testKey]; // Skip if variation doesn't define this attribute if (typeof attributes[testKey] === "undefined") { continue; } // If variation attribute is empty, it matches any value if (attributes[testKey] === "") { continue; } // Check for exact match if (attributes[testKey] !== testValue) { match = false; break; } } } // Return true if we found a matching variation if (match && variation.is_in_stock && variation.is_purchasable) { return true; } } return false; } // Handle keypress events for accessibility onKeyPress(event) { const isSpace = (event.keyCode && event.keyCode === 32) || (event.key && event.key === " "); const isEnter = (event.keyCode && event.keyCode === 13) || (event.key && event.key.toLowerCase() === "enter"); if (isSpace || isEnter) { event.preventDefault(); jQuery(event.currentTarget).trigger("click.usk-grid-variations"); } } // Update product image with the provided URL updateProductImage(imageUrl) { if (!imageUrl) return; const $productItem = this.$container.closest(".usk-item"); const $defaultImage = $productItem.find(".usk-image .img.image-default"); const $hoverImage = $productItem.find(".usk-image .img.image-hover"); // Update images with new src if ($defaultImage.length) { $defaultImage.attr("src", imageUrl); } if ($hoverImage.length) { $hoverImage.attr("src", imageUrl); } // Also update srcset if it exists if ($defaultImage.attr("srcset")) { $defaultImage.attr("srcset", ""); } if ($hoverImage.attr("srcset")) { $hoverImage.attr("srcset", ""); } } // Update Add to Cart button based on selected variations updateAddToCartButton() { const $productItem = this.$container.closest(".usk-item"); const $addToCartBtn = $productItem.find(".usk-button"); if (!$addToCartBtn.length) { return; } // Check if all attributes are selected const attributes = this.getChosenAttributes(); if (!attributes || attributes.chosenCount !== attributes.count) { this.resetAddToCartToSelectOptions($addToCartBtn); return; } // Make sure we have all required variations selected const requiredAttributeCount = this.getTotalRequiredAttributes(); if (attributes.chosenCount < requiredAttributeCount) { this.resetAddToCartToSelectOptions($addToCartBtn); return; } // Find matching variation this.findMatchingVariation( attributes.data, (variationId, variationData) => { // Store variation ID this.$container.data("variation-id", variationId); if (!variationId) { this.setUnavailableButton($addToCartBtn); return; } // Update the product image if variation has an image if (variationData && variationData.image && variationData.image.src) { this.updateProductImage(variationData.image.src); } this.setAddToCartButton($addToCartBtn, variationId, attributes); } ); } // Reset button to "Select Options" state resetAddToCartToSelectOptions($button) { $button .removeClass("product_type_variation add_to_cart_button ajax_add_to_cart") .addClass("product_type_variable") .removeAttr("data-variation_id") .attr("href", "javascript:void(0)"); // Clear attributes this.clearButtonAttributes($button); // Update text const buttonText = $button.find(".usk-icon-arrow-right-8").length ? 'Select options ' : "Select options"; $button.html(buttonText); // Remove any summary this.removeVariationSummary(); } // Set button to "Unavailable" state setUnavailableButton($button) { $button .removeClass("product_type_variation add_to_cart_button ajax_add_to_cart") .addClass("product_type_variable") .removeAttr("data-variation_id") .attr("href", "javascript:void(0)"); // Update text const buttonText = $button.find(".usk-icon-arrow-right-8").length ? 'Unavailable ' : "Unavailable"; $button.html(buttonText); // Remove any summary this.removeVariationSummary(); } // Set button to "Add to Cart" state with variation data setAddToCartButton($button, variationId, attributes) { // Clear existing attributes this.clearButtonAttributes($button); // Set button properties $button .removeClass("product_type_variable") .addClass("product_type_variation add_to_cart_button ajax_add_to_cart") .attr("data-product_id", this.productId) .attr("data-variation_id", variationId) .attr("data-prevent_redirect", "true") .attr("href", "javascript:void(0)"); // Add attribute data if (attributes.data) { jQuery.each(attributes.data, (name, value) => { if (name && value) { $button.attr("data-" + name, value); } }); } // Update text const buttonText = $button.find(".usk-icon-arrow-right-8").length ? 'Add to cart ' : "Add to cart"; $button.html(buttonText); } // Clear data-attribute_ properties from button clearButtonAttributes($button) { try { jQuery.each($button[0].attributes, (i, attr) => { if (attr && attr.name && attr.name.indexOf("data-attribute_") === 0) { $button.removeAttr(attr.name); } }); } catch (e) { console.log("Error clearing attributes:", e); } } // Remove the variation summary display removeVariationSummary() { this.$container.find(".usk-variation-summary").remove(); } // Find matching variation ID for the given attributes findMatchingVariation(attributesData, callback) { if (!attributesData) { callback(null); return; } // Try to find match locally first const matchingVariation = this.findMatchingVariationLocally(attributesData); if (matchingVariation) { callback(matchingVariation.variation_id, matchingVariation); return; } // If not found locally, try server this.findMatchingVariationOnServer(attributesData, callback); } // Find matching variation locally findMatchingVariationLocally(attributesData) { const variations = this.availableVariations; if (!variations || !variations.length) { return null; } for (let i = 0; i < variations.length; i++) { const variation = variations[i]; if (!variation || !variation.attributes) { continue; } const attributes = variation.attributes; let match = true; // Check if variation matches all selected attributes for (const attrName in attributesData) { if ( attributesData.hasOwnProperty(attrName) && attributesData[attrName] !== "" ) { const attrValue = attributesData[attrName]; // Skip if variation doesn't define this attribute if (typeof attributes[attrName] === "undefined") { continue; } // If variation attribute is empty, it matches any value if (attributes[attrName] === "") { continue; } // Check for exact match if (attributes[attrName] !== attrValue) { match = false; break; } } } // Return variation if we found a match if (match && variation.is_in_stock && variation.is_purchasable) { return variation; } } return null; } // Find matching variation via AJAX findMatchingVariationOnServer(attributesData, callback) { if (typeof usk_vars === "undefined" || !usk_vars.ajax_url) { callback(null); return; } // Format attributes for AJAX const formattedAttributes = {}; for (const key in attributesData) { if (attributesData.hasOwnProperty(key) && attributesData[key]) { const attrName = key.replace("attribute_", ""); formattedAttributes[attrName] = attributesData[key]; } } jQuery.ajax({ url: usk_vars.ajax_url, type: "POST", data: { action: "usk_get_available_variations", product_id: this.productId, attributes: formattedAttributes, security: usk_vars.nonce || "", }, success: (response) => { if (response && response.success && response.data) { callback(response.data.variation_id, response.data); } else { callback(null); } }, error: () => { callback(null); }, }); } // Get chosen attributes from container getChosenAttributes() { const data = {}; let count = 0; let chosen = 0; // Get attributes from variation swatches if (this.$swatchWrappers && this.$swatchWrappers.length) { this.$swatchWrappers.each((i, wrapper) => { const $wrapper = jQuery(wrapper); const attributeName = $wrapper.data("attribute_name"); if (!attributeName) { return true; // Skip this iteration } const $selected = $wrapper.find( ".usk-variation-swatches__item.selected" ); const value = $selected.length ? $selected.data("value") : ""; if (value) { chosen++; } count++; data[attributeName] = value; }); } // Get attributes from variation buttons if (this.$container) { this.$container.find(".usk-variation-group").each((i, group) => { const $group = jQuery(group); const $activeBtn = $group.find(".usk-variation-button.active"); if (!$activeBtn.length) { return true; } const attribute = $activeBtn.data("attribute"); if (!attribute) { return true; } const attributeName = "attribute_" + attribute; const value = $activeBtn.data("value") || ""; // Only add if not already set by swatches if (!data[attributeName]) { if (value) { chosen++; } count++; data[attributeName] = value; } }); } return { count: count, chosenCount: chosen, data: data, }; } // Get total number of required attributes getTotalRequiredAttributes() { let count = 0; // Count variation swatches if (this.$swatchWrappers && this.$swatchWrappers.length) { this.$swatchWrappers.each((i, wrapper) => { const $wrapper = jQuery(wrapper); if ($wrapper.data("attribute_name")) { count++; } }); } // Count variation buttons in groups that aren't already counted const countedAttributes = new Set(); this.$swatchWrappers.each((i, wrapper) => { const attrName = jQuery(wrapper).data("attribute_name"); if (attrName) { countedAttributes.add(attrName.replace('attribute_', '')); } }); this.$container.find(".usk-variation-group").each((i, group) => { const $group = jQuery(group); const $firstBtn = $group.find(".usk-variation-button").first(); const attribute = $firstBtn.data("attribute"); if (attribute && !countedAttributes.has(attribute)) { count++; } }); return count; } } // Initialize on document ready jQuery(function ($) { function initGridVariations() { $(".usk-variations-container:not(.swatches-support)").each(function () { new USKGridVariations($(this)); }); } // Initialize on page load initGridVariations(); // Reinitialize after cart operations $(document.body).on( "wc_fragments_refreshed wc_fragments_loaded added_to_cart", initGridVariations ); // Initialize on AJAX content load $(document).ajaxComplete(function (event, xhr, settings) { setTimeout(function () { initGridVariations(); }, 100); }); });