You've already forked wc-tier-and-package-prices
This major feature release adds full support for WooCommerce variable products with variation-level pricing configuration. ## Key Features - Each product variation can have independent tier and package pricing - AJAX-based dynamic pricing table loading on variation selection - Admin UI integrated into WooCommerce variation panels - Full backward compatibility with existing simple product functionality - WooCommerce Blocks compatibility maintained ## Implementation Highlights - Effective ID pattern throughout codebase for variation handling - Variation-specific meta boxes with field prefix support - Template system updated to support both simple and variation products - JavaScript enhancements for variation selector integration - Cart logic updated to handle variation pricing correctly ## Files Changed - Core: wc-tier-and-package-prices.php (version 1.2.0), composer.json - Cart: includes/class-wc-tpp-cart.php (effective ID logic) - Frontend: includes/class-wc-tpp-frontend.php (AJAX endpoint, variation detection) - Admin: includes/class-wc-tpp-product-meta.php (variation hooks and methods) - Templates: templates/admin/*.twig (field prefix support, table structure) - JavaScript: assets/js/*.js (variation support) - Documentation: CHANGELOG.md, README.md, CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
358 lines
14 KiB
JavaScript
358 lines
14 KiB
JavaScript
/**
|
|
* Frontend JavaScript for WooCommerce Tier and Package Prices
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
$(document).ready(function() {
|
|
const $quantityInput = $('input.qty');
|
|
const $priceDisplay = $('.woocommerce-Price-amount.amount').first();
|
|
const $addToCartButton = $('.single_add_to_cart_button');
|
|
const isRestrictedMode = $('.wc-tpp-package-pricing-table').hasClass('wc-tpp-restricted-mode');
|
|
|
|
if ($quantityInput.length === 0 && !isRestrictedMode) {
|
|
return;
|
|
}
|
|
|
|
// Get tiers and packages data
|
|
const tiers = [];
|
|
const packages = [];
|
|
|
|
$('.wc-tpp-tier-pricing-table tbody tr').each(function() {
|
|
tiers.push({
|
|
minQty: parseInt($(this).data('min-qty')),
|
|
price: parseFloat($(this).data('price'))
|
|
});
|
|
});
|
|
|
|
$('.wc-tpp-package').each(function() {
|
|
packages.push({
|
|
qty: parseInt($(this).data('qty')),
|
|
price: parseFloat($(this).data('price'))
|
|
});
|
|
});
|
|
|
|
// Update price display based on quantity
|
|
function updatePriceDisplay() {
|
|
const quantity = parseInt($quantityInput.val()) || 1;
|
|
|
|
// Check for exact package match
|
|
let matchedPackage = null;
|
|
packages.forEach(function(pkg) {
|
|
if (pkg.qty === quantity) {
|
|
matchedPackage = pkg;
|
|
}
|
|
});
|
|
|
|
if (matchedPackage) {
|
|
const totalPrice = matchedPackage.price;
|
|
const unitPrice = totalPrice / quantity;
|
|
updatePrice(unitPrice, 'Package price: ' + formatPrice(totalPrice) + ' total');
|
|
highlightPackage(quantity);
|
|
return;
|
|
}
|
|
|
|
// Check for tier pricing
|
|
let applicableTier = null;
|
|
tiers.forEach(function(tier) {
|
|
if (quantity >= tier.minQty) {
|
|
applicableTier = tier;
|
|
}
|
|
});
|
|
|
|
if (applicableTier) {
|
|
updatePrice(applicableTier.price, 'Volume discount applied');
|
|
highlightTier(quantity);
|
|
removePackageHighlight();
|
|
} else {
|
|
removeAllHighlights();
|
|
}
|
|
}
|
|
|
|
// Update price in the product page
|
|
function updatePrice(price, message) {
|
|
if ($priceDisplay.length) {
|
|
const formattedPrice = formatPrice(price);
|
|
$priceDisplay.html(formattedPrice);
|
|
|
|
// Add message if provided
|
|
if (message) {
|
|
if (!$('.wc-tpp-price-message').length) {
|
|
$priceDisplay.after('<div class="wc-tpp-price-message"></div>');
|
|
}
|
|
$('.wc-tpp-price-message').html('<small>' + message + '</small>');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format price according to WooCommerce settings
|
|
function formatPrice(price) {
|
|
const decimals = wcTppData.price_decimals || 2;
|
|
const decimalSep = wcTppData.price_decimal_separator || '.';
|
|
const thousandSep = wcTppData.price_thousand_separator || ',';
|
|
const symbol = wcTppData.currency_symbol || '$';
|
|
const position = wcTppData.currency_position || 'left';
|
|
|
|
let priceStr = price.toFixed(decimals);
|
|
const parts = priceStr.split('.');
|
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
|
|
priceStr = parts.join(decimalSep);
|
|
|
|
switch (position) {
|
|
case 'left':
|
|
return symbol + priceStr;
|
|
case 'right':
|
|
return priceStr + symbol;
|
|
case 'left_space':
|
|
return symbol + ' ' + priceStr;
|
|
case 'right_space':
|
|
return priceStr + ' ' + symbol;
|
|
default:
|
|
return symbol + priceStr;
|
|
}
|
|
}
|
|
|
|
// Highlight active tier
|
|
function highlightTier(quantity) {
|
|
$('.wc-tpp-table tbody tr').removeClass('wc-tpp-active-tier');
|
|
let activeRow = null;
|
|
|
|
$('.wc-tpp-table tbody tr').each(function() {
|
|
const minQty = parseInt($(this).data('min-qty'));
|
|
if (quantity >= minQty) {
|
|
activeRow = $(this);
|
|
}
|
|
});
|
|
|
|
if (activeRow) {
|
|
activeRow.addClass('wc-tpp-active-tier');
|
|
}
|
|
}
|
|
|
|
// Highlight selected package
|
|
function highlightPackage(quantity) {
|
|
$('.wc-tpp-package').removeClass('wc-tpp-selected');
|
|
$('.wc-tpp-table tbody tr').removeClass('wc-tpp-active-tier');
|
|
|
|
$('.wc-tpp-package').each(function() {
|
|
const pkgQty = parseInt($(this).data('qty'));
|
|
if (pkgQty === quantity) {
|
|
$(this).addClass('wc-tpp-selected');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Remove package highlight
|
|
function removePackageHighlight() {
|
|
$('.wc-tpp-package').removeClass('wc-tpp-selected');
|
|
}
|
|
|
|
// Remove all highlights
|
|
function removeAllHighlights() {
|
|
$('.wc-tpp-package').removeClass('wc-tpp-selected');
|
|
$('.wc-tpp-table tbody tr').removeClass('wc-tpp-active-tier');
|
|
$('.wc-tpp-price-message').remove();
|
|
}
|
|
|
|
// Toggle add to cart button state based on quantity
|
|
function updateAddToCartButton() {
|
|
const quantity = parseInt($quantityInput.val()) || 0;
|
|
|
|
if (quantity <= 0) {
|
|
$addToCartButton.prop('disabled', true).addClass('disabled');
|
|
} else {
|
|
$addToCartButton.prop('disabled', false).removeClass('disabled');
|
|
}
|
|
}
|
|
|
|
// Handle quantity input changes
|
|
$quantityInput.on('input change', function() {
|
|
updatePriceDisplay();
|
|
updateAddToCartButton();
|
|
});
|
|
|
|
// Handle tier pricing row clicks
|
|
$('.wc-tpp-tier-pricing-table tbody tr').on('click', function() {
|
|
const minQty = parseInt($(this).data('min-qty'));
|
|
|
|
if ($quantityInput.length > 0 && !isRestrictedMode) {
|
|
$quantityInput.val(minQty).trigger('change');
|
|
|
|
// Scroll to quantity input for better UX
|
|
$('html, body').animate({
|
|
scrollTop: $quantityInput.offset().top - 100
|
|
}, 300);
|
|
}
|
|
});
|
|
|
|
// Handle package selection
|
|
$('.wc-tpp-select-package').on('click', function(e) {
|
|
e.preventDefault();
|
|
const $package = $(this).closest('.wc-tpp-package');
|
|
const qty = parseInt($package.data('qty'));
|
|
|
|
if (isRestrictedMode) {
|
|
// In restricted mode, we need to set a hidden input or use data attribute
|
|
// since the quantity field is hidden
|
|
if ($quantityInput.length === 0) {
|
|
// Create a hidden quantity input if it doesn't exist
|
|
if ($('.qty-hidden-input').length === 0) {
|
|
$('.single_add_to_cart_button').before('<input type="hidden" name="quantity" class="qty qty-hidden-input" value="1" />');
|
|
}
|
|
$('.qty-hidden-input').val(qty);
|
|
} else {
|
|
$quantityInput.val(qty);
|
|
}
|
|
|
|
// Highlight selected package
|
|
$('.wc-tpp-package').removeClass('wc-tpp-selected');
|
|
$package.addClass('wc-tpp-selected');
|
|
|
|
// Update price display
|
|
const price = parseFloat($package.data('price'));
|
|
const unitPrice = price / qty;
|
|
updatePrice(unitPrice, 'Package price: ' + formatPrice(price) + ' total');
|
|
} else {
|
|
$quantityInput.val(qty).trigger('change');
|
|
}
|
|
|
|
// Scroll to add to cart button
|
|
$('html, body').animate({
|
|
scrollTop: $('.single_add_to_cart_button').offset().top - 100
|
|
}, 500);
|
|
});
|
|
|
|
// In restricted mode, prevent form submission if no package is selected
|
|
if (isRestrictedMode) {
|
|
$('form.cart').on('submit', function(e) {
|
|
const hasSelection = $('.wc-tpp-package.wc-tpp-selected').length > 0;
|
|
if (!hasSelection) {
|
|
e.preventDefault();
|
|
alert('Please select a package size before adding to cart.');
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initial update
|
|
if (!isRestrictedMode) {
|
|
updatePriceDisplay();
|
|
}
|
|
|
|
// Initial button state check
|
|
if ($quantityInput.length > 0 && $addToCartButton.length > 0) {
|
|
updateAddToCartButton();
|
|
}
|
|
|
|
// ========================================
|
|
// Variable Product Support
|
|
// ========================================
|
|
|
|
const $variationsForm = $('.variations_form');
|
|
const $pricingTableContainer = $('.wc-tpp-pricing-table-container');
|
|
|
|
if ($variationsForm.length && $pricingTableContainer.length) {
|
|
// Handle variation selection
|
|
$variationsForm.on('found_variation', function(event, variation) {
|
|
if (!variation.variation_id) {
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
$pricingTableContainer.html('<div class="wc-tpp-loading">Loading pricing...</div>').show();
|
|
|
|
// Fetch variation pricing via AJAX
|
|
$.ajax({
|
|
url: wcTppData.ajax_url,
|
|
type: 'POST',
|
|
data: {
|
|
action: 'wc_tpp_get_variation_pricing',
|
|
nonce: wcTppData.nonce,
|
|
variation_id: variation.variation_id
|
|
},
|
|
success: function(response) {
|
|
if (response.success && response.data.has_pricing) {
|
|
// Display the pricing table HTML
|
|
$pricingTableContainer.html(response.data.html).show();
|
|
|
|
// Re-initialize event handlers for the new content
|
|
initializePricingHandlers();
|
|
|
|
// Handle quantity restrictions
|
|
if (response.data.restrict_to_packages) {
|
|
$('input.qty').hide().closest('.quantity').hide();
|
|
$('<style>.quantity { display: none !important; }</style>').appendTo('head');
|
|
} else {
|
|
$('input.qty').show().closest('.quantity').show();
|
|
$('style:contains(".quantity { display: none")').remove();
|
|
}
|
|
} else {
|
|
// No pricing for this variation
|
|
$pricingTableContainer.html('').hide();
|
|
$('input.qty').show().closest('.quantity').show();
|
|
$('style:contains(".quantity { display: none")').remove();
|
|
}
|
|
},
|
|
error: function() {
|
|
$pricingTableContainer.html('').hide();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Handle variation reset
|
|
$variationsForm.on('reset_data', function() {
|
|
$pricingTableContainer.html('').hide();
|
|
$('input.qty').show().closest('.quantity').show();
|
|
$('style:contains(".quantity { display: none")').remove();
|
|
});
|
|
|
|
// Initialize pricing handlers for dynamically loaded content
|
|
function initializePricingHandlers() {
|
|
// Re-attach package selection handlers
|
|
$('.wc-tpp-select-package').off('click').on('click', function(e) {
|
|
e.preventDefault();
|
|
const $package = $(this).closest('.wc-tpp-package');
|
|
const qty = parseInt($package.data('qty'));
|
|
const $qtyInput = $('input.qty');
|
|
|
|
if ($qtyInput.length === 0 || $qtyInput.is(':hidden')) {
|
|
// Create hidden input for restricted products
|
|
if ($('.qty-hidden-input').length === 0) {
|
|
$('.single_add_to_cart_button').before('<input type="hidden" name="quantity" class="qty qty-hidden-input" value="1" />');
|
|
}
|
|
$('.qty-hidden-input').val(qty);
|
|
} else {
|
|
$qtyInput.val(qty).trigger('change');
|
|
}
|
|
|
|
// Highlight selected package
|
|
$('.wc-tpp-package').removeClass('wc-tpp-selected');
|
|
$package.addClass('wc-tpp-selected');
|
|
|
|
// Scroll to add to cart button
|
|
$('html, body').animate({
|
|
scrollTop: $('.single_add_to_cart_button').offset().top - 100
|
|
}, 500);
|
|
});
|
|
|
|
// Re-attach tier row click handlers
|
|
$('.wc-tpp-tier-pricing-table tbody tr').off('click').on('click', function() {
|
|
const minQty = parseInt($(this).data('min-qty'));
|
|
const $qtyInput = $('input.qty');
|
|
|
|
if ($qtyInput.length > 0 && $qtyInput.is(':visible')) {
|
|
$qtyInput.val(minQty).trigger('change');
|
|
|
|
// Scroll to quantity input
|
|
$('html, body').animate({
|
|
scrollTop: $qtyInput.offset().top - 100
|
|
}, 300);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|