Surface Type:Honed
Coverage Area (sq ft/box):--
Length:--
Width:--
Height:--
Color Shade:
Pcs Per box:--
Price UOM:--
// ============================================
// ISOLATED COVERAGE CALCULATOR
// Badge positioned above Add to Cart button
// ============================================
(function() {
'use strict';
// Create unique namespace
window.ShopifyCoverageCalculator = window.ShopifyCoverageCalculator || {};
// Private variables - completely isolated
const PRIVATE = {
initialized: false,
currentVariant: null,
currentQuantity: 1,
productData: null,
coverageData: null,
elements: {},
addToCartButton: null,
config: {
debug: false,
coveragePerBox: 10.64,
defaultPrice: 0
}
};
// ===== HELPER FUNCTIONS =====
const Helpers = {
// Safe console log
log: function() {
if (PRIVATE.config.debug && console && console.log) {
console.log('[CoverageCalc]', ...arguments);
}
},
// Parse price from text
parsePrice: function(priceText) {
if (!priceText || typeof priceText !== 'string') return 0;
const match = priceText.match(/[\d,]+(?:\.\d{2})?/);
if (match) {
return parseFloat(match[0].replace(/,/g, ''));
}
return 0;
},
// Format price
formatPrice: function(price) {
return price.toFixed(2);
},
// Safe event handler wrapper
safeHandler: function(fn) {
return function() {
try {
return fn.apply(this, arguments);
} catch (e) {
Helpers.log('Error in handler:', e);
}
};
},
// Find Add to Cart button
findAddToCartButton: function() {
// Common selectors for Add to Cart button
const selectors = [
'button[type="submit"][name="add"]',
'button[name="add"]',
'.product-form__submit',
'.shopify-payment-button__button',
'button.add-to-cart',
'.add-to-cart-button',
'form[action*="/cart/add"] button[type="submit"]',
'#add-to-cart-button',
'.product-form__add-to-cart'
];
for (let selector of selectors) {
const button = document.querySelector(selector);
if (button && button.offsetParent !== null) { // Check if visible
return button;
}
}
// Fallback: try to find any submit button in cart form
const cartForm = document.querySelector('form[action*="/cart/add"]');
if (cartForm) {
const submitButton = cartForm.querySelector('button[type="submit"], input[type="submit"]');
if (submitButton) return submitButton;
}
return null;
},
// Insert badge above Add to Cart button
insertBadgeAboveAddToCart: function(badgeElement) {
const addToCartBtn = PRIVATE.addToCartButton || Helpers.findAddToCartButton();
if (addToCartBtn && addToCartBtn.parentNode) {
// Insert badge before the Add to Cart button
addToCartBtn.parentNode.insertBefore(badgeElement, addToCartBtn);
Helpers.log('Badge inserted above Add to Cart button');
return true;
}
// Fallback: try to find product form
const productForm = document.querySelector('form[action*="/cart/add"]');
if (productForm) {
productForm.insertBefore(badgeElement, productForm.firstChild);
Helpers.log('Badge inserted at top of product form');
return true;
}
// Last resort: append to price info
const priceInfo = document.querySelector('.price-info');
if (priceInfo) {
priceInfo.appendChild(badgeElement);
Helpers.log('Badge appended to price-info');
return true;
}
return false;
}
};
// ===== DOM ELEMENT CACHE =====
const Elements = {
cache: function() {
PRIVATE.elements = {
area: document.getElementById('area'),
boxQty: document.getElementById('box-quantity'),
totalPrice: document.getElementById('total-price'),
dynamicTotal: document.querySelector('.total-price-dynamic'),
pricePerBox: document.getElementById('productPrice'),
pricePerSqFt: document.getElementById('discountedProductPrice'),
savedAmount: document.querySelector('.saved_amount span'),
coverageCalculator: document.querySelector('.coverage-calculator'),
productPriceText: document.getElementById('product-price')
};
// Find Add to Cart button
PRIVATE.addToCartButton = Helpers.findAddToCartButton();
Helpers.log('Elements cached:', Object.keys(PRIVATE.elements).filter(k => PRIVATE.elements[k]));
Helpers.log('Add to Cart button found:', !!PRIVATE.addToCartButton);
},
update: function(selector, content) {
const element = PRIVATE.elements[selector];
if (element && element !== document) {
element.textContent = content;
return true;
}
return false;
},
html: function(selector, html) {
const element = PRIVATE.elements[selector];
if (element) {
element.innerHTML = html;
return true;
}
return false;
},
val: function(selector, value) {
const element = PRIVATE.elements[selector];
if (element) {
if (value !== undefined) {
element.value = value;
return true;
}
return element.value;
}
return null;
}
};
// ===== DATA MANAGEMENT =====
const DataManager = {
loadProductData: function() {
try {
const productJson = document.getElementById('ProductJson');
const coverageJson = document.getElementById('VariantCoverageData');
if (productJson && productJson.textContent) {
PRIVATE.productData = JSON.parse(productJson.textContent);
Helpers.log('Product data loaded:', PRIVATE.productData.title);
}
if (coverageJson && coverageJson.textContent) {
PRIVATE.coverageData = JSON.parse(coverageJson.textContent);
Helpers.log('Coverage data loaded');
}
if (PRIVATE.productData && PRIVATE.productData.variants && PRIVATE.productData.variants.length > 0) {
PRIVATE.currentVariant = PRIVATE.productData.variants[0];
PRIVATE.config.defaultPrice = PRIVATE.currentVariant.price / 100;
// Update coverage per box from data attribute
if (PRIVATE.elements.coverageCalculator && PRIVATE.elements.coverageCalculator.dataset.coveragePerBox) {
PRIVATE.config.coveragePerBox = parseFloat(PRIVATE.elements.coverageCalculator.dataset.coveragePerBox);
}
// Or get from coverage data
if (PRIVATE.coverageData && PRIVATE.coverageData[PRIVATE.currentVariant.id]) {
PRIVATE.config.coveragePerBox = parseFloat(PRIVATE.coverageData[PRIVATE.currentVariant.id]);
if (PRIVATE.elements.coverageCalculator) {
PRIVATE.elements.coverageCalculator.dataset.coveragePerBox = PRIVATE.config.coveragePerBox;
}
}
return true;
}
} catch (e) {
Helpers.log('Error loading product data:', e);
}
return false;
},
getCurrentPrice: function() {
if (PRIVATE.currentVariant && PRIVATE.currentVariant.price) {
return PRIVATE.currentVariant.price / 100;
}
// Try to get from DOM
if (PRIVATE.elements.pricePerBox) {
return Helpers.parsePrice(PRIVATE.elements.pricePerBox.textContent);
}
return PRIVATE.config.defaultPrice;
},
getCoveragePerBox: function() {
if (PRIVATE.elements.coverageCalculator && PRIVATE.elements.coverageCalculator.dataset.coveragePerBox) {
return parseFloat(PRIVATE.elements.coverageCalculator.dataset.coveragePerBox);
}
return PRIVATE.config.coveragePerBox;
},
getQuantity: function() {
const qty = Elements.val('boxQty');
if (qty && !isNaN(qty)) {
return parseInt(qty) > 0 ? parseInt(qty) : 1;
}
return 1;
}
};
// ===== CALCULATION ENGINE =====
const Calculator = {
// Calculate boxes from area
calculateBoxesFromArea: function(area) {
const coverage = DataManager.getCoveragePerBox();
if (coverage <= 0) return 1;
return Math.ceil(area / coverage);
},
// Calculate area from boxes
calculateAreaFromBoxes: function(boxes) {
const coverage = DataManager.getCoveragePerBox();
return boxes * coverage;
},
// Calculate total price
calculateTotal: function(price, quantity) {
return price * quantity;
},
// Main calculation and update
update: function() {
try {
const price = DataManager.getCurrentPrice();
const quantity = DataManager.getQuantity();
const total = this.calculateTotal(price, quantity);
const coverage = DataManager.getCoveragePerBox();
const area = this.calculateAreaFromBoxes(quantity);
// Update all displays
this.updateDisplays(price, quantity, total, area, coverage);
// Update dynamic badge
this.updateDynamicBadge(price, quantity, total, area);
Helpers.log('Calculation updated:', { price, quantity, total, area });
return total;
} catch (e) {
Helpers.log('Error in calculation:', e);
return 0;
}
},
updateDisplays: function(price, quantity, total, area, coverage) {
// Update total price span
Elements.update('totalPrice', Helpers.formatPrice(total));
// Update dynamic total div
if (PRIVATE.elements.dynamicTotal) {
PRIVATE.elements.dynamicTotal.innerHTML = `Total Price: $${Helpers.formatPrice(total)}`;
}
// Update area input if needed
const currentArea = parseFloat(Elements.val('area'));
if (isNaN(currentArea) || Math.abs(currentArea - area) > 0.01) {
Elements.val('area', area.toFixed(2));
}
// Update price per sq ft
const pricePerSqFt = price / coverage;
Elements.update('pricePerSqFt', `$${Helpers.formatPrice(pricePerSqFt)}`);
},
updateDynamicBadge: function(price, quantity, total, area) {
let badge = document.getElementById('coverage-calc-badge');
let badgeContainer = document.getElementById('coverage-calc-badge-container');
// Create container if it doesn't exist
if (!badgeContainer) {
badgeContainer = document.createElement('div');
badgeContainer.id = 'coverage-calc-badge-container';
badgeContainer.style.marginBottom = '15px';
// Insert above Add to Cart button
Helpers.insertBadgeAboveAddToCart(badgeContainer);
}
// Create or update badge
if (!badge) {
badge = document.createElement('div');
badge.id = 'coverage-calc-badge';
badgeContainer.appendChild(badge);
}
// const pricePerSqFt = price / DataManager.getCoveragePerBox();
badge.innerHTML = `
Order Summary
$${Helpers.formatPrice(total)}
${quantity} box${quantity !== 1 ? 'es' : ''} × $${Helpers.formatPrice(price)}/box
Covers ${Helpers.formatPrice(area)} sq ft @ $${Helpers.formatPrice(pricing.unit_price)}/sq ft
`;
// Add animation styles if not already added
if (!document.getElementById('coverage-calc-styles')) {
const styles = document.createElement('style');
styles.id = 'coverage-calc-styles';
styles.textContent = `
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
document.head.appendChild(styles);
}
},
// Update from area input
updateFromArea: function(areaValue) {
if (!areaValue || areaValue <= 0) {
Elements.val('boxQty', '1');
this.update();
return;
}
const boxes = this.calculateBoxesFromArea(areaValue);
const actualArea = this.calculateAreaFromBoxes(boxes);
// Update box quantity
Elements.val('boxQty', boxes.toString());
// Update area to actual coverage
if (Math.abs(areaValue - actualArea) > 0.01) {
Elements.val('area', actualArea.toFixed(2));
}
this.update();
},
// Update from box quantity
updateFromBoxes: function(boxesValue) {
if (!boxesValue || boxesValue <= 0) {
Elements.val('boxQty', '1');
boxesValue = 1;
}
const area = this.calculateAreaFromBoxes(boxesValue);
Elements.val('area', area.toFixed(2));
this.update();
}
};
// ===== VARIANT MANAGEMENT =====
const VariantManager = {
updateVariant: function(variantId) {
if (!PRIVATE.productData || !PRIVATE.productData.variants) return false;
const variant = PRIVATE.productData.variants.find(v => v.id == variantId);
if (!variant) return false;
PRIVATE.currentVariant = variant;
// Update coverage per box
if (PRIVATE.coverageData && PRIVATE.coverageData[variantId]) {
PRIVATE.config.coveragePerBox = parseFloat(PRIVATE.coverageData[variantId]);
if (PRIVATE.elements.coverageCalculator) {
PRIVATE.elements.coverageCalculator.dataset.coveragePerBox = PRIVATE.config.coveragePerBox;
}
}
// Update price displays
const pricePerBox = variant.price / 100;
const coverage = DataManager.getCoveragePerBox();
const pricePerSqFt = pricing.unit_price;
Elements.update('pricePerBox', `$${Helpers.formatPrice(pricePerBox)}`);
Elements.update('pricePerSqFt', `$${Helpers.formatPrice(pricePerSqFt)}`);
// Update savings
if (variant.compare_at_price) {
const comparePrice = variant.compare_at_price / 100;
const save = comparePrice - pricePerBox;
if (save > 0 && PRIVATE.elements.savedAmount) {
PRIVATE.elements.savedAmount.textContent = `Save $${Helpers.formatPrice(save)} per box`;
}
}
// Reset calculator
Elements.val('boxQty', '1');
Elements.val('area', coverage.toFixed(2));
Calculator.update();
Helpers.log('Variant updated:', variant.title, pricePerBox);
return true;
},
getSelectedVariant: function() {
// Try to get from form
const variantInput = document.querySelector('input[name="id"]:checked, select[name="id"]');
if (variantInput && variantInput.value) {
const variant = PRIVATE.productData?.variants?.find(v => v.id == variantInput.value);
if (variant) return variant;
}
// Try from swatches
const selectedSwatch = document.querySelector('.swatch input[type="radio"]:checked, .product-form__input input[type="radio"]:checked');
if (selectedSwatch) {
const variantId = selectedSwatch.dataset.variantId || selectedSwatch.value;
const variant = PRIVATE.productData?.variants?.find(v => v.id == variantId);
if (variant) return variant;
}
return PRIVATE.currentVariant;
}
};
// ===== EVENT HANDLERS =====
const EventManager = {
setup: function() {
// Area input events
const areaEl = PRIVATE.elements.area;
if (areaEl) {
areaEl.addEventListener('input', Helpers.safeHandler(function(e) {
const value = parseFloat(e.target.value);
if (!isNaN(value) && value > 0) {
Calculator.updateFromArea(value);
}
}));
areaEl.addEventListener('blur', Helpers.safeHandler(function(e) {
let value = parseFloat(e.target.value);
if (isNaN(value) || value <= 0) {
value = DataManager.getCoveragePerBox();
Elements.val('area', value.toFixed(2));
}
Calculator.updateFromArea(value);
}));
}
// Box quantity events
const boxEl = PRIVATE.elements.boxQty;
if (boxEl) {
boxEl.addEventListener('input', Helpers.safeHandler(function(e) {
const value = parseInt(e.target.value);
if (!isNaN(value) && value > 0) {
Calculator.updateFromBoxes(value);
}
}));
boxEl.addEventListener('blur', Helpers.safeHandler(function(e) {
let value = parseInt(e.target.value);
if (isNaN(value) || value <= 0) {
value = 1;
Elements.val('boxQty', '1');
}
Calculator.updateFromBoxes(value);
}));
}
// Listen to variant changes
this.setupVariantListeners();
// Watch for DOM changes to reposition badge if needed
this.watchForAddToCartButton();
},
setupVariantListeners: function() {
// Watch for swatch clicks
document.addEventListener('click', Helpers.safeHandler(function(e) {
const target = e.target.closest('.swatch input[type="radio"], .product-form__input input[type="radio"], [data-variant-id]');
if (target) {
setTimeout(() => {
const variant = VariantManager.getSelectedVariant();
if (variant && variant.id !== PRIVATE.currentVariant?.id) {
VariantManager.updateVariant(variant.id);
}
}, 50);
}
}));
// Listen to select changes
document.addEventListener('change', Helpers.safeHandler(function(e) {
const target = e.target;
if (target.matches('select[name="id"], select.single-option-selector')) {
setTimeout(() => {
const variant = VariantManager.getSelectedVariant();
if (variant) {
VariantManager.updateVariant(variant.id);
}
}, 50);
}
}));
// Listen to Shopify events
if (typeof $ !== 'undefined') {
$(document).on('variant:change', Helpers.safeHandler(function(e, variant) {
if (variant && variant.id) {
VariantManager.updateVariant(variant.id);
}
}));
}
},
watchForAddToCartButton: function() {
// Watch for Add to Cart button if it's loaded dynamically
const observer = new MutationObserver(function(mutations) {
const button = Helpers.findAddToCartButton();
if (button && button !== PRIVATE.addToCartButton) {
PRIVATE.addToCartButton = button;
Helpers.log('Add to Cart button found, repositioning badge...');
// Reposition badge above button
const badgeContainer = document.getElementById('coverage-calc-badge-container');
if (badgeContainer && button.parentNode) {
button.parentNode.insertBefore(badgeContainer, button);
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
};
// ===== PUBLIC API =====
const PublicAPI = {
// Update total
update: function() {
return Calculator.update();
},
// Update variant
setVariant: function(variantId) {
return VariantManager.updateVariant(variantId);
},
// Get current state
getState: function() {
return {
variant: PRIVATE.currentVariant,
pricePerBox: DataManager.getCurrentPrice(),
quantity: DataManager.getQuantity(),
total: DataManager.getCurrentPrice() * DataManager.getQuantity(),
coveragePerBox: DataManager.getCoveragePerBox()
};
},
// Enable debug
debug: function(enabled) {
PRIVATE.config.debug = enabled === true;
Helpers.log('Debug mode:', PRIVATE.config.debug);
},
// Reset calculator
reset: function() {
Elements.val('boxQty', '1');
Elements.val('area', DataManager.getCoveragePerBox().toFixed(2));
Calculator.update();
},
// Force reposition badge
repositionBadge: function() {
const badgeContainer = document.getElementById('coverage-calc-badge-container');
if (badgeContainer) {
Helpers.insertBadgeAboveAddToCart(badgeContainer);
}
}
};
// ===== INITIALIZATION =====
function init() {
if (PRIVATE.initialized) {
Helpers.log('Already initialized');
return;
}
Helpers.log('Initializing...');
// Cache elements
Elements.cache();
// Load data
if (!DataManager.loadProductData()) {
Helpers.log('Waiting for product data...');
setTimeout(init, 500);
return;
}
// Setup event handlers
EventManager.setup();
// Initial calculation
setTimeout(() => {
Calculator.update();
}, 100);
PRIVATE.initialized = true;
Helpers.log('✅ Calculator initialized successfully');
Helpers.log('Badge positioned above Add to Cart button');
Helpers.log('State:', PublicAPI.getState());
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose public API
window.ShopifyCoverageCalculator = PublicAPI;
})();
// ============================================
// VARIANT HANDLER - SKU, Name & Image Updates
// Completely separate from calculator
// ============================================
(function() {
'use strict';
// Create unique namespace
window.ShopifyVariantHandler = window.ShopifyVariantHandler || {};
// Private variables
const PRIVATE = {
initialized: false,
productData: null,
currentVariant: null,
elements: {
sku: null,
variantName: null,
productImage: null,
variantTitle: null
},
config: {
debug: false,
imageSelector: '.product__media img, .product-single__photo img, .product-featured-img, .product-image-main img, [data-product-image] img',
skuSelector: '.sku, .product-sku, [data-sku], .variant-sku',
variantNameSelector: '.variant-name, .product-variant, .selected-variant, [data-variant-name]',
variantTitleSelector: '.variant-title, .product-title-variant'
}
};
// ===== HELPER FUNCTIONS =====
const Helpers = {
log: function() {
if (PRIVATE.config.debug && console) {
console.log('[VariantHandler]', ...arguments);
}
},
// Find all variant selectors on page
findVariantSelectors: function() {
const selectors = [
'input[name="id"]:checked',
'select[name="id"]',
'.swatch input[type="radio"]:checked',
'.product-form__input input[type="radio"]:checked',
'[data-variant-id]'
];
for (let selector of selectors) {
const element = document.querySelector(selector);
if (element && element.value) {
return { element, value: element.value };
}
}
return null;
},
// Find variant by ID
findVariantById: function(variantId) {
if (!PRIVATE.productData || !PRIVATE.productData.variants) return null;
return PRIVATE.productData.variants.find(v => v.id == variantId);
},
// Format price
formatPrice: function(price) {
return price.toFixed(2);
},
// Safe event handler
safeHandler: function(fn) {
return function() {
try {
return fn.apply(this, arguments);
} catch (e) {
Helpers.log('Error in handler:', e);
}
};
}
};
// ===== ELEMENT MANAGER =====
const ElementManager = {
// Find and cache all relevant elements
cacheElements: function() {
// Find SKU element
PRIVATE.elements.sku = document.querySelector(PRIVATE.config.skuSelector);
if (!PRIVATE.elements.sku) {
// Try to find any element containing SKU text
const allElements = document.querySelectorAll('[class*="sku"], [id*="sku"]');
PRIVATE.elements.sku = allElements[0] || null;
}
// Find variant name elements
PRIVATE.elements.variantName = document.querySelector(PRIVATE.config.variantNameSelector);
PRIVATE.elements.variantTitle = document.querySelector(PRIVATE.config.variantTitleSelector);
// Find product image
PRIVATE.elements.productImage = document.querySelector(PRIVATE.config.imageSelector);
// If no image found, try common Shopify image selectors
if (!PRIVATE.elements.productImage) {
const imageSelectors = [
'.product__media img',
'.product-single__photo img',
'.product-featured-img',
'.product-image-main img',
'[data-product-image] img',
'.product-single__image img',
'.product-image img',
'#ProductPhotoImg',
'.product-photo-container img'
];
for (let selector of imageSelectors) {
const img = document.querySelector(selector);
if (img) {
PRIVATE.elements.productImage = img;
break;
}
}
}
Helpers.log('Elements found:', {
sku: !!PRIVATE.elements.sku,
variantName: !!PRIVATE.elements.variantName,
variantTitle: !!PRIVATE.elements.variantTitle,
productImage: !!PRIVATE.elements.productImage
});
},
// Update SKU
updateSku: function(sku) {
if (!PRIVATE.elements.sku) return false;
const skuText = sku || 'N/A';
// Handle different SKU element types
if (PRIVATE.elements.sku.tagName === 'INPUT' || PRIVATE.elements.sku.tagName === 'SELECT') {
PRIVATE.elements.sku.value = skuText;
} else {
PRIVATE.elements.sku.textContent = skuText;
}
Helpers.log('SKU updated:', skuText);
return true;
},
// Update variant name
updateVariantName: function(variant) {
if (!PRIVATE.elements.variantName && !PRIVATE.elements.variantTitle) return false;
// Get variant name
let variantName = variant.title || '';
// If title is like "Color / Size", format nicely
if (variantName.includes('/')) {
const parts = variantName.split('/').map(p => p.trim());
variantName = parts.join(' • ');
}
// Update variant name element
if (PRIVATE.elements.variantName) {
if (PRIVATE.elements.variantName.tagName === 'INPUT') {
PRIVATE.elements.variantName.value = variantName;
} else {
PRIVATE.elements.variantName.textContent = variantName;
}
}
// Update variant title element
if (PRIVATE.elements.variantTitle) {
if (PRIVATE.elements.variantTitle.tagName === 'INPUT') {
PRIVATE.elements.variantTitle.value = variantName;
} else {
PRIVATE.elements.variantTitle.textContent = variantName;
}
}
Helpers.log('Variant name updated:', variantName);
return true;
},
// Update product image
updateImage: function(variant) {
if (!PRIVATE.elements.productImage) return false;
// Get variant image
let imageUrl = null;
if (variant.featured_image && variant.featured_image.src) {
imageUrl = variant.featured_image.src;
} else if (variant.image && variant.image.src) {
imageUrl = variant.image.src;
} else if (variant.featured_media && variant.featured_media.preview_image) {
imageUrl = variant.featured_media.preview_image.src;
}
if (!imageUrl) {
Helpers.log('No variant image found');
return false;
}
// Update image
const oldSrc = PRIVATE.elements.productImage.src;
if (oldSrc !== imageUrl) {
// Add fade effect
const container = PRIVATE.elements.productImage.closest('.product__media, .product-single__photo, .product-image-container');
if (container) {
container.style.opacity = '0.5';
container.style.transition = 'opacity 0.2s ease';
}
PRIVATE.elements.productImage.src = imageUrl;
PRIVATE.elements.productImage.alt = variant.title || PRIVATE.productData?.title || 'Product image';
// Also update any thumbnail images if they exist
ElementManager.updateThumbnails(variant);
setTimeout(() => {
if (container) {
container.style.opacity = '1';
}
}, 100);
Helpers.log('Image updated:', imageUrl);
return true;
}
return false;
},
// Update thumbnail images
updateThumbnails: function(variant) {
const thumbnails = document.querySelectorAll('.thumbnail img, .product-thumbnail img, [data-thumbnail] img');
if (thumbnails.length > 0 && variant.featured_image && variant.featured_image.src) {
thumbnails.forEach(thumb => {
const thumbSrc = thumb.src;
if (thumbSrc && thumbSrc.includes(variant.featured_image.src)) {
thumb.classList.add('active');
} else {
thumb.classList.remove('active');
}
});
}
},
// Update all variant info
updateAll: function(variant) {
if (!variant) return false;
this.updateSku(variant.sku);
this.updateVariantName(variant);
this.updateImage(variant);
// Also update any meta data
this.updateMetaData(variant);
return true;
},
// Update meta data (like price, compare price)
updateMetaData: function(variant) {
// Update any price comparison elements
const comparePriceElements = document.querySelectorAll('.compare-price, .compare-at-price, [data-compare-price]');
if (comparePriceElements.length > 0 && variant.compare_at_price) {
const comparePrice = variant.compare_at_price / 100;
comparePriceElements.forEach(el => {
el.textContent = `$${Helpers.formatPrice(comparePrice)}`;
});
}
// Update availability
const availabilityElements = document.querySelectorAll('.product-availability, .stock-status, [data-availability]');
if (availabilityElements.length > 0) {
const isAvailable = variant.available;
const statusText = isAvailable ? 'In Stock' : 'Out of Stock';
const statusColor = isAvailable ? '#27ae60' : '#e74c3c';
availabilityElements.forEach(el => {
el.textContent = statusText;
el.style.color = statusColor;
});
}
}
};
// ===== IMAGE PRELOADER =====
const ImagePreloader = {
preload: function(imageUrl) {
if (!imageUrl) return;
const img = new Image();
img.src = imageUrl;
}
};
// ===== VARIANT MANAGER =====
const VariantManager = {
// Load product data
loadProductData: function() {
try {
const productJson = document.getElementById('ProductJson');
if (productJson && productJson.textContent) {
PRIVATE.productData = JSON.parse(productJson.textContent);
Helpers.log('Product data loaded:', PRIVATE.productData.title);
return true;
}
} catch (e) {
Helpers.log('Error loading product data:', e);
}
return false;
},
// Update variant
updateVariant: function(variantId) {
if (!PRIVATE.productData) return false;
const variant = Helpers.findVariantById(variantId);
if (!variant) {
Helpers.log('Variant not found:', variantId);
return false;
}
// Check if it's the same variant
if (PRIVATE.currentVariant && PRIVATE.currentVariant.id === variant.id) {
Helpers.log('Same variant, skipping update');
return false;
}
PRIVATE.currentVariant = variant;
// Preload image
if (variant.featured_image && variant.featured_image.src) {
ImagePreloader.preload(variant.featured_image.src);
}
// Update all elements
ElementManager.updateAll(variant);
// Dispatch custom event for other scripts
const event = new CustomEvent('variant:updated', {
detail: { variant: variant },
bubbles: true
});
document.dispatchEvent(event);
Helpers.log('Variant updated:', {
id: variant.id,
title: variant.title,
sku: variant.sku,
hasImage: !!variant.featured_image
});
return true;
},
// Get current selected variant
getSelectedVariant: function() {
const selected = Helpers.findVariantSelectors();
if (selected && selected.value) {
const variant = Helpers.findVariantById(selected.value);
if (variant) return variant;
}
return PRIVATE.currentVariant;
},
// Force update from DOM
forceUpdate: function() {
const selectedVariant = this.getSelectedVariant();
if (selectedVariant) {
this.updateVariant(selectedVariant.id);
}
}
};
// ===== EVENT HANDLERS =====
const EventManager = {
setup: function() {
// Listen to swatch clicks
this.setupSwatchListeners();
// Listen to select changes
this.setupSelectListeners();
// Listen to Shopify variant:change event
this.setupShopifyEvents();
// Watch for dynamically added elements
this.watchForDynamicElements();
},
setupSwatchListeners: function() {
// Click handler for swatches
document.addEventListener('click', Helpers.safeHandler(function(e) {
// Check if clicked on a swatch radio or its label
const target = e.target.closest('.swatch input, .product-form__input input, [data-variant-id]');
if (target) {
setTimeout(() => {
const variant = VariantManager.getSelectedVariant();
if (variant) {
VariantManager.updateVariant(variant.id);
}
}, 50);
}
}));
// Change handler for radio inputs
document.addEventListener('change', Helpers.safeHandler(function(e) {
if (e.target.matches('.swatch input[type="radio"], .product-form__input input[type="radio"]')) {
const variantId = e.target.dataset.variantId || e.target.value;
if (variantId) {
VariantManager.updateVariant(variantId);
}
}
}));
},
setupSelectListeners: function() {
document.addEventListener('change', Helpers.safeHandler(function(e) {
if (e.target.matches('select[name="id"], select.single-option-selector')) {
setTimeout(() => {
const variant = VariantManager.getSelectedVariant();
if (variant) {
VariantManager.updateVariant(variant.id);
}
}, 50);
}
}));
},
setupShopifyEvents: function() {
// Listen to Shopify's variant:change event
if (typeof $ !== 'undefined') {
$(document).on('variant:change', Helpers.safeHandler(function(e, variant) {
if (variant && variant.id) {
VariantManager.updateVariant(variant.id);
}
}));
}
// Listen to section reload events
document.addEventListener('shopify:section:load', Helpers.safeHandler(function() {
setTimeout(() => {
ElementManager.cacheElements();
VariantManager.forceUpdate();
}, 100);
}));
},
watchForDynamicElements: function() {
// Watch for dynamically added variant selectors
const observer = new MutationObserver(function(mutations) {
let shouldUpdate = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
// Check if new swatches were added
const newSwatches = document.querySelectorAll('.swatch input:not([data-handler-added])');
if (newSwatches.length > 0) {
newSwatches.forEach(swatch => {
swatch.setAttribute('data-handler-added', 'true');
shouldUpdate = true;
});
}
// Check if new images were added
const newImages = document.querySelectorAll(PRIVATE.config.imageSelector);
if (newImages.length > 0 && !PRIVATE.elements.productImage) {
ElementManager.cacheElements();
shouldUpdate = true;
}
}
});
if (shouldUpdate) {
setTimeout(() => {
VariantManager.forceUpdate();
}, 100);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
};
// ===== PUBLIC API =====
const PublicAPI = {
// Update variant by ID
setVariant: function(variantId) {
return VariantManager.updateVariant(variantId);
},
// Get current variant
getCurrentVariant: function() {
return PRIVATE.currentVariant;
},
// Force update from current selection
refresh: function() {
VariantManager.forceUpdate();
},
// Enable debug mode
debug: function(enabled) {
PRIVATE.config.debug = enabled === true;
Helpers.log('Debug mode:', PRIVATE.config.debug);
},
// Get product data
getProductData: function() {
return PRIVATE.productData;
},
// Get all variants
getVariants: function() {
return PRIVATE.productData?.variants || [];
}
};
// ===== INITIALIZATION =====
function init() {
if (PRIVATE.initialized) {
Helpers.log('Already initialized');
return;
}
Helpers.log('Initializing Variant Handler...');
// Cache elements
ElementManager.cacheElements();
// Load product data
if (!VariantManager.loadProductData()) {
Helpers.log('Waiting for product data...');
setTimeout(init, 500);
return;
}
// Setup event handlers
EventManager.setup();
// Initial variant update
setTimeout(() => {
VariantManager.forceUpdate();
}, 100);
PRIVATE.initialized = true;
Helpers.log('✅ Variant Handler initialized successfully');
Helpers.log('Initial variant:', PRIVATE.currentVariant?.title);
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose public API
window.ShopifyVariantHandler = PublicAPI;
})();
(function() {
'use strict';
function updateUIFromVariant(variant) {
if (!variant) return;
// ✅ Update custom color name places
let name = variant.title || '';
if (name.includes('/')) {
name = name.split('/').map(v => v.trim()).join(' • ');
}
document.querySelectorAll('#selectedValue, #otherPlace').forEach(el => {
el.textContent = name;
});
// ✅ Optional: Thumbnail → slider sync (ONLY if your theme needs it)
const thumbnails = document.querySelectorAll('.productView-thumbnail');
thumbnails.forEach(thumb => {
thumb.classList.remove('active');
thumb.classList.forEach(cls => {
if (cls.includes(variant.id)) {
thumb.classList.add('active');
const link = thumb.querySelector('.productView-thumbnail-link');
if (link) link.click();
}
});
});
}
// 🔥 Listen to YOUR VariantHandler (BEST WAY)
document.addEventListener('variant:updated', function(e) {
updateUIFromVariant(e.detail.variant);
});
// 🔥 Backup: Shopify event
if (typeof jQuery !== 'undefined') {
jQuery(document).on('variant:change', function(e, variant) {
updateUIFromVariant(variant);
});
}
})();
// ============================================
// VARIANT HANDLER (FIXED FOR SLICK SLIDER)
// ============================================
(function() {
'use strict';
window.ShopifyVariantHandler = {};
const PRIVATE = {
productData: null,
currentVariant: null,
config: {
debug: false,
slickMain: '.product__media, .product-gallery, .product-images',
slickSlides: 'img'
}
};
// ===== HELPERS =====
const Helpers = {
log: function(...args) {
if (PRIVATE.config.debug) {
console.log('[VariantHandler]', ...args);
}
},
getProductData: function() {
const el = document.getElementById('ProductJson');
if (el) {
PRIVATE.productData = JSON.parse(el.textContent);
}
},
findVariant: function(id) {
return PRIVATE.productData?.variants?.find(v => v.id == id);
},
getSelectedVariantId: function() {
const el =
document.querySelector('input[name="id"]:checked') ||
document.querySelector('select[name="id"]');
return el ? el.value : null;
}
};
// ===== SLICK IMAGE HANDLER =====
const SlickHandler = {
goToVariantImage: function(variant) {
if (!variant || !variant.featured_image) return;
const imageSrc = variant.featured_image.src;
const slider = $(PRIVATE.config.slickMain);
if (!slider.length || !slider.hasClass('slick-initialized')) {
Helpers.log('Slick not initialized');
return;
}
let targetIndex = -1;
slider.find('.slick-slide').each(function(index) {
const img = $(this).find('img');
if (img.length && img.attr('src')?.includes(imageSrc)) {
targetIndex = $(this).data('slick-index');
}
});
if (targetIndex !== -1) {
slider.slick('slickGoTo', targetIndex);
Helpers.log('Moved to slide:', targetIndex);
} else {
Helpers.log('Image not found in slider');
}
}
};
// ===== UI UPDATES =====
const UI = {
updateSKU: function(variant) {
const el = document.querySelector('.sku, [data-sku]');
if (el) el.textContent = variant.sku || 'N/A';
},
updateVariantName: function(variant) {
const el = document.querySelector('.variant-name, [data-variant-name]');
if (el) el.textContent = variant.title;
}
};
// ===== MAIN UPDATE =====
function updateVariant() {
const id = Helpers.getSelectedVariantId();
if (!id) return;
const variant = Helpers.findVariant(id);
if (!variant) return;
if (PRIVATE.currentVariant?.id === variant.id) return;
PRIVATE.currentVariant = variant;
UI.updateSKU(variant);
UI.updateVariantName(variant);
// 🔥 Slick Image Fix
setTimeout(() => {
SlickHandler.goToVariantImage(variant);
}, 100);
}
// ===== EVENTS =====
function bindEvents() {
document.addEventListener('change', function(e) {
if (
e.target.matches('select[name="id"]') ||
e.target.matches('input[type="radio"]')
) {
setTimeout(updateVariant, 50);
}
});
document.addEventListener('click', function(e) {
if (e.target.closest('[data-variant-id], .swatch')) {
setTimeout(updateVariant, 50);
}
});
if (typeof $ !== 'undefined') {
$(document).on('variant:change', function(e, variant) {
if (variant) {
PRIVATE.currentVariant = variant;
UI.updateSKU(variant);
UI.updateVariantName(variant);
SlickHandler.goToVariantImage(variant);
}
});
}
}
// ===== INIT =====
function init() {
Helpers.getProductData();
bindEvents();
setTimeout(updateVariant, 200);
Helpers.log('✅ Variant Handler Ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// Simple and direct - updates image on swatch click
$(document).ready(function() {
// When any color swatch is clicked
$('input[name="Color"]').on('change', function() {
var selectedColor = $(this).val();
// Update the variation name
$('#selectedValue').text(selectedColor);
$('#otherPlace').text(selectedColor);
// Find the clicked swatch label to get the image
var $selectedLabel = $('label[for="' + $(this).attr('id') + '"]');
var $expandImg = $selectedLabel.find('.expand img');
var newImgSrc = $expandImg.attr('srcset') || $expandImg.attr('src');
if (newImgSrc) {
// Convert to large image size
newImgSrc = newImgSrc.replace('_compact', '_720x').replace('_75x', '_720x').replace('_130x', '_720x');
// Update the main product image
var $mainImage = $('.productView-nav .slick-current .productView-img-container img');
if ($mainImage.length) {
$mainImage.attr('src', newImgSrc);
$mainImage.attr('srcset', newImgSrc);
}
// Also update the zoom image
$('.productView-nav .slick-current .media').attr('href', newImgSrc);
}
});
// Trigger on page load for initial state
$('input[name="Color"]:checked').trigger('change');
});