woocommerce.js

I’ll build this as a strong inc/js/woocommerce.js front-end file for RX Theme: cart UX, quantity controls, AJAX events, checkout helpers, product gallery, accessibility, and safe fallbacks.

My dear friend, yes — you can create a powerful inc/js/woocommerce.js file for your RX Theme. This file will improve WooCommerce product page, shop page, cart page, checkout page, mini-cart, quantity buttons, AJAX cart updates, accessibility, sticky add-to-cart, coupon toggle, checkout validation UI, and more.

Create or replace this file:

rx-theme/inc/js/woocommerce.js

Then paste this full code:

/*!
 * RX Theme WooCommerce Frontend
 * File: inc/js/woocommerce.js
 * Theme: RX Theme
 * Author: RxHarun
 *
 * Purpose:
 * Advanced WooCommerce frontend helper for product page, shop page,
 * cart page, checkout page, mini-cart, accessibility, UX, and AJAX events.
 */

(function ($, window, document) {
	'use strict';

	var RXWoo = {
		config: {
			body: $('body'),
			window: $(window),
			document: $(document),
			htmlBody: $('html, body'),
			ajaxDelay: 350,
			noticeDelay: 6000,
			stickyOffset: 220,
			activeClass: 'is-active',
			loadingClass: 'is-loading',
			openClass: 'is-open',
			disabledClass: 'is-disabled',
			hiddenClass: 'rx-hidden',
			initializedClass: 'rx-woo-initialized'
		},

		init: function () {
			this.cacheSelectors();
			this.addBodyClasses();
			this.wrapWooElements();
			this.quantityButtons();
			this.autoUpdateCart();
			this.miniCartPanel();
			this.productGallery();
			this.productTabs();
			this.variableProductUX();
			this.stickyAddToCart();
			this.shopFilters();
			this.orderingEnhancement();
			this.couponToggle();
			this.checkoutEnhancement();
			this.formEnhancement();
			this.noticeEnhancement();
			this.accountPageEnhancement();
			this.productArchiveCards();
			this.wishlistCompareSafeButtons();
			this.backToShopButton();
			this.accessibilityFixes();
			this.bindWooEvents();
			this.scrollFixes();
			this.performanceSafeLazyActions();

			this.config.body.addClass(this.config.initializedClass);
		},

		cacheSelectors: function () {
			this.$body = this.config.body;
			this.$window = this.config.window;
			this.$document = this.config.document;

			this.$cartForm = $('.woocommerce-cart-form');
			this.$checkoutForm = $('form.checkout');
			this.$productForm = $('form.cart');
			this.$miniCart = $('.widget_shopping_cart, .woocommerce-mini-cart');
			this.$noticesWrapper = $('.woocommerce-notices-wrapper');
			this.$singleProduct = $('.single-product');
			this.$productSummary = $('.single-product .summary');
			this.$productGallery = $('.woocommerce-product-gallery');
			this.$archiveProducts = $('.woocommerce ul.products');
		},

		addBodyClasses: function () {
			if ($('.woocommerce').length) {
				this.$body.addClass('rx-woocommerce-page');
			}

			if ($('.woocommerce-cart').length) {
				this.$body.addClass('rx-woo-cart-page');
			}

			if ($('.woocommerce-checkout').length) {
				this.$body.addClass('rx-woo-checkout-page');
			}

			if ($('.single-product').length) {
				this.$body.addClass('rx-woo-single-product-page');
			}

			if ($('.woocommerce-account').length) {
				this.$body.addClass('rx-woo-account-page');
			}
		},

		wrapWooElements: function () {
			$('.woocommerce-notices-wrapper').each(function () {
				var $this = $(this);

				if (!$this.parent().hasClass('rx-woo-notice-area')) {
					$this.wrap('<div class="rx-woo-notice-area" aria-live="polite"></div>');
				}
			});

			$('.woocommerce table.shop_table').each(function () {
				var $table = $(this);

				if (!$table.parent().hasClass('rx-woo-table-scroll')) {
					$table.wrap('<div class="rx-woo-table-scroll"></div>');
				}
			});

			$('.woocommerce-result-count, .woocommerce-ordering').each(function () {
				var $item = $(this);

				if (!$item.parent().hasClass('rx-woo-toolbar-item')) {
					$item.wrap('<div class="rx-woo-toolbar-item"></div>');
				}
			});

			if ($('.woocommerce-result-count').length && $('.woocommerce-ordering').length) {
				var $count = $('.woocommerce-result-count').first();
				var $ordering = $('.woocommerce-ordering').first();

				if (!$count.closest('.rx-woo-shop-toolbar').length) {
					$count.parent('.rx-woo-toolbar-item')
						.add($ordering.parent('.rx-woo-toolbar-item'))
						.wrapAll('<div class="rx-woo-shop-toolbar"></div>');
				}
			}
		},

		quantityButtons: function () {
			var self = this;

			$('.quantity').each(function () {
				var $quantity = $(this);
				var $input = $quantity.find('input.qty');

				if (!$input.length || $quantity.hasClass('rx-quantity-ready')) {
					return;
				}

				$quantity.addClass('rx-quantity-ready');

				var minusText = $input.attr('aria-label')
					? 'Decrease ' + $input.attr('aria-label')
					: 'Decrease quantity';

				var plusText = $input.attr('aria-label')
					? 'Increase ' + $input.attr('aria-label')
					: 'Increase quantity';

				$input.before(
					'<button type="button" class="rx-qty-btn rx-qty-minus" aria-label="' + self.escapeAttr(minusText) + '">−</button>'
				);

				$input.after(
					'<button type="button" class="rx-qty-btn rx-qty-plus" aria-label="' + self.escapeAttr(plusText) + '">+</button>'
				);
			});

			this.$document.off('click.rxQty').on('click.rxQty', '.rx-qty-btn', function (e) {
				e.preventDefault();

				var $button = $(this);
				var $quantity = $button.closest('.quantity');
				var $input = $quantity.find('input.qty');
				var current = parseFloat($input.val());
				var max = parseFloat($input.attr('max'));
				var min = parseFloat($input.attr('min'));
				var step = parseFloat($input.attr('step'));

				current = isNaN(current) ? 0 : current;
				max = isNaN(max) ? false : max;
				min = isNaN(min) ? 0 : min;
				step = isNaN(step) || step <= 0 ? 1 : step;

				if ($button.hasClass('rx-qty-plus')) {
					if (max && current >= max) {
						return;
					}

					current = current + step;

					if (max && current > max) {
						current = max;
					}
				}

				if ($button.hasClass('rx-qty-minus')) {
					if (current <= min) {
						return;
					}

					current = current - step;

					if (current < min) {
						current = min;
					}
				}

				$input.val(self.cleanNumber(current)).trigger('change');
			});

			this.updateQtyButtonStates();

			this.$document.off('change.rxQtyState').on('change.rxQtyState', '.quantity input.qty', function () {
				self.updateQtyButtonStates();
			});
		},

		updateQtyButtonStates: function () {
			$('.quantity').each(function () {
				var $quantity = $(this);
				var $input = $quantity.find('input.qty');
				var current = parseFloat($input.val());
				var max = parseFloat($input.attr('max'));
				var min = parseFloat($input.attr('min'));

				current = isNaN(current) ? 0 : current;
				max = isNaN(max) ? false : max;
				min = isNaN(min) ? 0 : min;

				$quantity.find('.rx-qty-minus').prop('disabled', current <= min);

				if (max) {
					$quantity.find('.rx-qty-plus').prop('disabled', current >= max);
				} else {
					$quantity.find('.rx-qty-plus').prop('disabled', false);
				}
			});
		},

		autoUpdateCart: function () {
			var self = this;
			var timer = null;

			this.$document.off('change.rxCartQty').on('change.rxCartQty', '.woocommerce-cart-form input.qty', function () {
				var $form = $('.woocommerce-cart-form');
				var $button = $form.find('button[name="update_cart"]');

				if (!$button.length) {
					return;
				}

				$button.prop('disabled', false).removeAttr('disabled');

				clearTimeout(timer);

				timer = setTimeout(function () {
					$form.addClass(self.config.loadingClass);
					$button.trigger('click');
				}, self.config.ajaxDelay);
			});
		},

		miniCartPanel: function () {
			var self = this;

			if (!$('.rx-mini-cart-toggle').length && $('.cart-contents, .site-header-cart, .header-cart').length) {
				$('.cart-contents, .site-header-cart > a, .header-cart > a').first().addClass('rx-mini-cart-toggle');
			}

			this.$document.off('click.rxMiniCart').on('click.rxMiniCart', '.rx-mini-cart-toggle', function (e) {
				var $panel = $('.rx-mini-cart-panel, .widget_shopping_cart').first();

				if (!$panel.length) {
					return;
				}

				e.preventDefault();

				self.$body.toggleClass('rx-mini-cart-open');
				$panel.toggleClass(self.config.openClass);

				if ($panel.hasClass(self.config.openClass)) {
					$panel.attr('aria-hidden', 'false');
				} else {
					$panel.attr('aria-hidden', 'true');
				}
			});

			this.$document.off('click.rxMiniCartClose').on('click.rxMiniCartClose', '.rx-mini-cart-close, .rx-mini-cart-overlay', function (e) {
				e.preventDefault();
				self.closeMiniCart();
			});

			this.$document.off('keyup.rxMiniCartEsc').on('keyup.rxMiniCartEsc', function (e) {
				if (e.key === 'Escape') {
					self.closeMiniCart();
				}
			});
		},

		closeMiniCart: function () {
			this.$body.removeClass('rx-mini-cart-open');
			$('.rx-mini-cart-panel, .widget_shopping_cart')
				.removeClass(this.config.openClass)
				.attr('aria-hidden', 'true');
		},

		productGallery: function () {
			var self = this;

			$('.woocommerce-product-gallery').each(function () {
				var $gallery = $(this);

				if ($gallery.hasClass('rx-gallery-ready')) {
					return;
				}

				$gallery.addClass('rx-gallery-ready');

				$gallery.find('.woocommerce-product-gallery__image a').attr({
					'aria-label': 'View product image',
					'data-rx-gallery-link': 'true'
				});

				if (!$gallery.find('.rx-gallery-counter').length) {
					var total = $gallery.find('.woocommerce-product-gallery__image').length;

					if (total > 1) {
						$gallery.append('<div class="rx-gallery-counter" aria-live="polite">1 / ' + total + '</div>');
					}
				}
			});

			this.$document.off('click.rxGalleryThumb').on('click.rxGalleryThumb', '.flex-control-thumbs li img', function () {
				var index = $(this).closest('li').index() + 1;
				var total = $('.woocommerce-product-gallery__image').length;

				$('.rx-gallery-counter').text(index + ' / ' + total);
			});

			this.$document.off('keydown.rxGallery').on('keydown.rxGallery', '.woocommerce-product-gallery', function (e) {
				var $thumbs = $(this).find('.flex-control-thumbs li img');

				if (!$thumbs.length) {
					return;
				}

				var $active = $thumbs.filter('.flex-active');
				var index = $thumbs.index($active);

				if (e.key === 'ArrowRight') {
					e.preventDefault();
					$thumbs.eq(Math.min(index + 1, $thumbs.length - 1)).trigger('click');
				}

				if (e.key === 'ArrowLeft') {
					e.preventDefault();
					$thumbs.eq(Math.max(index - 1, 0)).trigger('click');
				}
			});

			setTimeout(function () {
				self.productImageZoomHint();
			}, 700);
		},

		productImageZoomHint: function () {
			var $gallery = $('.woocommerce-product-gallery');

			if (!$gallery.length || $gallery.find('.rx-product-zoom-hint').length) {
				return;
			}

			if ($gallery.find('.woocommerce-product-gallery__trigger').length) {
				$gallery.append('<span class="rx-product-zoom-hint">Click image to zoom</span>');
			}
		},

		productTabs: function () {
			var self = this;

			$('.woocommerce-tabs').each(function () {
				var $tabs = $(this);

				if ($tabs.hasClass('rx-tabs-ready')) {
					return;
				}

				$tabs.addClass('rx-tabs-ready');

				$tabs.find('.tabs li a').each(function () {
					var $link = $(this);
					var target = $link.attr('href');

					$link.attr({
						role: 'tab',
						'aria-selected': $link.parent().hasClass('active') ? 'true' : 'false'
					});

					if (target && target.charAt(0) === '#') {
						$(target).attr({
							role: 'tabpanel',
							tabindex: '0'
						});
					}
				});
			});

			this.$document.off('click.rxTabs').on('click.rxTabs', '.woocommerce-tabs .tabs li a', function () {
				var $link = $(this);
				var $tabs = $link.closest('.woocommerce-tabs');

				setTimeout(function () {
					$tabs.find('.tabs li a').attr('aria-selected', 'false');
					$tabs.find('.tabs li.active a').attr('aria-selected', 'true');

					if (window.location.hash && $(window.location.hash).length) {
						self.smoothScrollTo($(window.location.hash), 80);
					}
				}, 60);
			});
		},

		variableProductUX: function () {
			var self = this;

			this.$document.off('found_variation.rxVariation').on('found_variation.rxVariation', 'form.variations_form', function (event, variation) {
				var $form = $(this);

				$form.addClass('rx-variation-found');
				$form.removeClass('rx-variation-empty');

				if (variation && variation.price_html) {
					self.updateVariationPrice($form, variation.price_html);
				}

				if (variation && variation.availability_html) {
					self.updateVariationStock($form, variation.availability_html);
				}
			});

			this.$document.off('reset_data.rxVariation').on('reset_data.rxVariation', 'form.variations_form', function () {
				var $form = $(this);

				$form.removeClass('rx-variation-found');
				$form.addClass('rx-variation-empty');

				$form.find('.rx-selected-variation-price').remove();
				$form.find('.rx-selected-variation-stock').remove();
			});

			this.$document.off('change.rxVariationSelect').on('change.rxVariationSelect', '.variations select', function () {
				var $select = $(this);
				var label = $select.closest('tr').find('label').text();
				var value = $select.find('option:selected').text();

				if (value) {
					$select.attr('aria-label', label + ': ' + value);
				}
			});

			this.$document.off('click.rxResetVariations').on('click.rxResetVariations', '.reset_variations', function () {
				$('.rx-selected-variation-price, .rx-selected-variation-stock').remove();
			});
		},

		updateVariationPrice: function ($form, priceHtml) {
			var $summary = $form.closest('.summary');

			if (!$summary.find('.rx-selected-variation-price').length) {
				$form.before('<div class="rx-selected-variation-price" aria-live="polite"></div>');
			}

			$summary.find('.rx-selected-variation-price').html(priceHtml);
		},

		updateVariationStock: function ($form, stockHtml) {
			var $summary = $form.closest('.summary');

			if (!$summary.find('.rx-selected-variation-stock').length) {
				$form.before('<div class="rx-selected-variation-stock" aria-live="polite"></div>');
			}

			$summary.find('.rx-selected-variation-stock').html(stockHtml);
		},

		stickyAddToCart: function () {
			var self = this;

			if (!$('.single-product').length || !$('form.cart').length) {
				return;
			}

			if ($('.rx-sticky-add-to-cart').length) {
				return;
			}

			var title = $('.product_title').first().text();
			var price = $('.summary .price').first().html();
			var image = $('.woocommerce-product-gallery__image img, .wp-post-image').first().attr('src');

			var html = '';
			html += '<div class="rx-sticky-add-to-cart" aria-hidden="true">';
			html += '<div class="rx-sticky-product-info">';

			if (image) {
				html += '<img src="' + self.escapeAttr(image) + '" alt="' + self.escapeAttr(title) + '" class="rx-sticky-product-image">';
			}

			html += '<div class="rx-sticky-product-text">';
			html += '<strong>' + self.escapeHtml(title) + '</strong>';

			if (price) {
				html += '<span class="rx-sticky-product-price">' + price + '</span>';
			}

			html += '</div>';
			html += '</div>';
			html += '<button type="button" class="rx-sticky-cart-button">Add to cart</button>';
			html += '</div>';

			this.$body.append(html);

			this.$window.off('scroll.rxStickyCart').on('scroll.rxStickyCart', function () {
				var scrollTop = self.$window.scrollTop();
				var $sticky = $('.rx-sticky-add-to-cart');
				var $cart = $('form.cart').first();

				if (!$cart.length) {
					return;
				}

				if (scrollTop > self.config.stickyOffset && !$cart.isInViewport()) {
					$sticky.addClass(self.config.activeClass).attr('aria-hidden', 'false');
				} else {
					$sticky.removeClass(self.config.activeClass).attr('aria-hidden', 'true');
				}
			});

			this.$document.off('click.rxStickyCart').on('click.rxStickyCart', '.rx-sticky-cart-button', function (e) {
				e.preventDefault();

				var $cartButton = $('form.cart .single_add_to_cart_button').first();

				if ($cartButton.length) {
					$cartButton.trigger('click');
				} else {
					self.smoothScrollTo($('form.cart').first(), 80);
				}
			});
		},

		shopFilters: function () {
			var self = this;

			if (!$('.woocommerce-shop, .post-type-archive-product, .tax-product_cat, .tax-product_tag').length) {
				return;
			}

			if (!$('.rx-shop-filter-toggle').length && $('.widget_price_filter, .woocommerce-widget-layered-nav, .widget_layered_nav_filters').length) {
				$('.woocommerce-products-header').after(
					'<button type="button" class="rx-shop-filter-toggle" aria-expanded="false">Filter products</button>'
				);
			}

			this.$document.off('click.rxFilterToggle').on('click.rxFilterToggle', '.rx-shop-filter-toggle', function (e) {
				e.preventDefault();

				var $button = $(this);

				self.$body.toggleClass('rx-shop-filter-open');

				var expanded = self.$body.hasClass('rx-shop-filter-open') ? 'true' : 'false';
				$button.attr('aria-expanded', expanded);
			});

			this.$document.off('click.rxFilterClose').on('click.rxFilterClose', '.rx-shop-filter-close, .rx-filter-overlay', function (e) {
				e.preventDefault();

				self.$body.removeClass('rx-shop-filter-open');
				$('.rx-shop-filter-toggle').attr('aria-expanded', 'false');
			});

			$('.widget_price_filter form').each(function () {
				var $form = $(this);

				if (!$form.find('.rx-price-filter-note').length) {
					$form.append('<p class="rx-price-filter-note">Use the price slider and press Filter.</p>');
				}
			});
		},

		orderingEnhancement: function () {
			$('.woocommerce-ordering select').each(function () {
				var $select = $(this);

				if (!$select.attr('aria-label')) {
					$select.attr('aria-label', 'Product sorting');
				}
			});

			this.$document.off('change.rxOrdering').on('change.rxOrdering', '.woocommerce-ordering select', function () {
				$(this).closest('form').addClass('is-submitting');
			});
		},

		couponToggle: function () {
			var self = this;

			$('.woocommerce-form-coupon-toggle .showcoupon').attr({
				role: 'button',
				'aria-expanded': 'false'
			});

			this.$document.off('click.rxCouponToggle').on('click.rxCouponToggle', '.showcoupon', function () {
				var $button = $(this);

				setTimeout(function () {
					var isVisible = $('.checkout_coupon, .woocommerce-form-coupon').is(':visible');
					$button.attr('aria-expanded', isVisible ? 'true' : 'false');

					if (isVisible) {
						self.smoothScrollTo($('.checkout_coupon, .woocommerce-form-coupon').first(), 100);
					}
				}, 100);
			});

			$('.coupon input.input-text').attr('placeholder', 'Enter coupon code');
		},

		checkoutEnhancement: function () {
			var self = this;

			if (!$('form.checkout').length) {
				return;
			}

			$('form.checkout').attr('novalidate', 'novalidate');

			$('.woocommerce-checkout-review-order-table').each(function () {
				var $table = $(this);

				if (!$table.closest('.rx-checkout-order-box').length) {
					$table.wrap('<div class="rx-checkout-order-box"></div>');
				}
			});

			$('#ship-to-different-address-checkbox').on('change.rxShippingAddress', function () {
				var checked = $(this).is(':checked');
				$('.shipping_address').attr('aria-hidden', checked ? 'false' : 'true');
			}).trigger('change.rxShippingAddress');

			this.$document.off('checkout_error.rxCheckout').on('checkout_error.rxCheckout', function () {
				var $error = $('.woocommerce-error').first();

				if ($error.length) {
					self.smoothScrollTo($error, 120);
					$error.attr('tabindex', '-1').focus();
				}
			});

			this.$document.off('updated_checkout.rxCheckout').on('updated_checkout.rxCheckout', function () {
				self.refreshCheckoutUI();
			});

			this.$document.off('change.rxPaymentMethod').on('change.rxPaymentMethod', 'input[name="payment_method"]', function () {
				$('.wc_payment_method').removeClass('rx-payment-selected');
				$(this).closest('.wc_payment_method').addClass('rx-payment-selected');
			});

			$('input[name="payment_method"]:checked').closest('.wc_payment_method').addClass('rx-payment-selected');
		},

		refreshCheckoutUI: function () {
			$('.woocommerce-checkout-review-order-table').closest('.rx-checkout-order-box').addClass('rx-checkout-updated');

			setTimeout(function () {
				$('.rx-checkout-order-box').removeClass('rx-checkout-updated');
			}, 600);

			$('input[name="payment_method"]:checked').closest('.wc_payment_method').addClass('rx-payment-selected');
		},

		formEnhancement: function () {
			var self = this;

			$('.woocommerce form input, .woocommerce form textarea, .woocommerce form select').each(function () {
				var $field = $(this);

				if ($field.attr('type') === 'hidden') {
					return;
				}

				var id = $field.attr('id');
				var $label = id ? $('label[for="' + id + '"]') : $();

				if ($label.length && !$field.attr('aria-label')) {
					$field.attr('aria-label', $.trim($label.text().replace('*', '')));
				}
			});

			this.$document.off('focus.rxField').on('focus.rxField', '.woocommerce form input, .woocommerce form textarea, .woocommerce form select', function () {
				$(this).closest('.form-row, p').addClass('rx-field-focused');
			});

			this.$document.off('blur.rxField').on('blur.rxField', '.woocommerce form input, .woocommerce form textarea, .woocommerce form select', function () {
				var $field = $(this);
				var $row = $field.closest('.form-row, p');

				$row.removeClass('rx-field-focused');

				if ($field.val()) {
					$row.addClass('rx-field-has-value');
				} else {
					$row.removeClass('rx-field-has-value');
				}
			});

			this.$document.off('submit.rxWooForms').on('submit.rxWooForms', '.woocommerce form', function () {
				$(this).addClass(self.config.loadingClass);
			});
		},

		noticeEnhancement: function () {
			var self = this;

			$('.woocommerce-message, .woocommerce-info, .woocommerce-error').each(function () {
				var $notice = $(this);

				if ($notice.hasClass('rx-notice-ready')) {
					return;
				}

				$notice.addClass('rx-notice-ready');
				$notice.attr('role', $notice.hasClass('woocommerce-error') ? 'alert' : 'status');

				if (!$notice.find('.rx-notice-close').length) {
					$notice.append('<button type="button" class="rx-notice-close" aria-label="Close notice">×</button>');
				}
			});

			this.$document.off('click.rxNoticeClose').on('click.rxNoticeClose', '.rx-notice-close', function (e) {
				e.preventDefault();

				$(this).closest('.woocommerce-message, .woocommerce-info, .woocommerce-error').slideUp(180, function () {
					$(this).remove();
				});
			});

			clearTimeout(window.rxWooNoticeTimer);

			window.rxWooNoticeTimer = setTimeout(function () {
				$('.woocommerce-message.rx-notice-ready, .woocommerce-info.rx-notice-ready').fadeOut(300);
			}, self.config.noticeDelay);
		},

		accountPageEnhancement: function () {
			if (!$('.woocommerce-account').length) {
				return;
			}

			$('.woocommerce-MyAccount-navigation').each(function () {
				var $nav = $(this);

				if (!$nav.attr('aria-label')) {
					$nav.attr('aria-label', 'My account navigation');
				}
			});

			$('.woocommerce-MyAccount-navigation-link.is-active a').attr('aria-current', 'page');

			if (!$('.rx-account-mobile-toggle').length && $('.woocommerce-MyAccount-navigation').length) {
				$('.woocommerce-MyAccount-navigation').before(
					'<button type="button" class="rx-account-mobile-toggle" aria-expanded="false">Account menu</button>'
				);
			}

			this.$document.off('click.rxAccountToggle').on('click.rxAccountToggle', '.rx-account-mobile-toggle', function (e) {
				e.preventDefault();

				var $button = $(this);
				var $nav = $('.woocommerce-MyAccount-navigation');

				$nav.toggleClass('is-open');

				$button.attr('aria-expanded', $nav.hasClass('is-open') ? 'true' : 'false');
			});
		},

		productArchiveCards: function () {
			var self = this;

			$('.woocommerce ul.products li.product').each(function () {
				var $product = $(this);

				if ($product.hasClass('rx-product-card-ready')) {
					return;
				}

				$product.addClass('rx-product-card-ready');

				var $link = $product.find('a.woocommerce-LoopProduct-link').first();
				var title = $.trim($product.find('.woocommerce-loop-product__title').text());

				if ($link.length && title) {
					$link.attr('aria-label', 'View product: ' + title);
				}

				$product.find('.button').each(function () {
					var $button = $(this);

					if (!$button.attr('aria-label') && title) {
						$button.attr('aria-label', $button.text() + ': ' + title);
					}
				});
			});

			this.$document.off('mouseenter.rxProductCard focusin.rxProductCard').on(
				'mouseenter.rxProductCard focusin.rxProductCard',
				'.woocommerce ul.products li.product',
				function () {
					$(this).addClass('rx-card-hover');
				}
			);

			this.$document.off('mouseleave.rxProductCard focusout.rxProductCard').on(
				'mouseleave.rxProductCard focusout.rxProductCard',
				'.woocommerce ul.products li.product',
				function () {
					$(this).removeClass('rx-card-hover');
				}
			);

			this.$document.off('added_to_cart.rxProductCard').on('added_to_cart.rxProductCard', function () {
				self.noticeEnhancement();
			});
		},

		wishlistCompareSafeButtons: function () {
			$('.yith-wcwl-add-to-wishlist a, .compare.button, .woosc-btn, .woosw-btn').each(function () {
				var $button = $(this);

				if (!$button.attr('aria-label')) {
					$button.attr('aria-label', $.trim($button.text()) || 'Product action');
				}
			});
		},

		backToShopButton: function () {
			if (!$('.woocommerce-cart').length) {
				return;
			}

			if (!$('.rx-back-to-shop').length && $('.cart-empty.woocommerce-info').length) {
				$('.return-to-shop').addClass('rx-back-to-shop');
			}
		},

		accessibilityFixes: function () {
			$('.star-rating').each(function () {
				var $rating = $(this);

				if (!$rating.attr('aria-label')) {
					var text = $.trim($rating.text());

					if (text) {
						$rating.attr('aria-label', text);
					}
				}
			});

			$('.woocommerce-pagination a, .woocommerce-pagination span').each(function () {
				var $item = $(this);

				if ($item.hasClass('current')) {
					$item.attr('aria-current', 'page');
				}
			});

			$('.remove').each(function () {
				var $remove = $(this);

				if (!$remove.attr('aria-label')) {
					$remove.attr('aria-label', 'Remove item');
				}
			});

			$('.woocommerce-breadcrumb').attr('aria-label', 'Product breadcrumb');
		},

		bindWooEvents: function () {
			var self = this;

			this.$document.on('added_to_cart removed_from_cart wc_fragments_refreshed', function () {
				self.closeMiniCart();
				self.noticeEnhancement();
				self.quantityButtons();
				self.accessibilityFixes();
			});

			this.$document.on('updated_cart_totals updated_wc_div', function () {
				$('.woocommerce-cart-form').removeClass(self.config.loadingClass);
				self.quantityButtons();
				self.noticeEnhancement();
				self.accessibilityFixes();
			});

			this.$document.on('wc_fragments_loaded wc_fragments_refreshed', function () {
				self.miniCartPanel();
				self.noticeEnhancement();
			});

			this.$document.on('updated_checkout', function () {
				self.refreshCheckoutUI();
				self.formEnhancement();
				self.noticeEnhancement();
			});

			this.$document.on('found_variation reset_data', function () {
				self.updateQtyButtonStates();
			});
		},

		scrollFixes: function () {
			var self = this;

			this.$document.off('click.rxScrollReviews').on('click.rxScrollReviews', '.woocommerce-review-link', function (e) {
				var target = $('#reviews');

				if (target.length) {
					e.preventDefault();
					self.smoothScrollTo(target, 90);
					target.attr('tabindex', '-1').focus();
				}
			});

			this.$document.off('click.rxTabHash').on('click.rxTabHash', 'a[href^="#tab-"]', function () {
				var hash = $(this).attr('href');

				if (hash && $(hash).length) {
					setTimeout(function () {
						self.smoothScrollTo($(hash), 90);
					}, 80);
				}
			});
		},

		performanceSafeLazyActions: function () {
			var self = this;

			if ('requestIdleCallback' in window) {
				window.requestIdleCallback(function () {
					self.lazyProductImages();
					self.markExternalProductLinks();
				});
			} else {
				setTimeout(function () {
					self.lazyProductImages();
					self.markExternalProductLinks();
				}, 900);
			}
		},

		lazyProductImages: function () {
			$('.woocommerce img').each(function () {
				var $img = $(this);

				if (!$img.attr('loading')) {
					$img.attr('loading', 'lazy');
				}

				if (!$img.attr('decoding')) {
					$img.attr('decoding', 'async');
				}
			});

			$('.single-product .woocommerce-product-gallery img').first().attr('loading', 'eager');
		},

		markExternalProductLinks: function () {
			var siteHost = window.location.hostname;

			$('.woocommerce a[href^="http"]').each(function () {
				var $link = $(this);
				var linkHost = this.hostname;

				if (linkHost && linkHost !== siteHost) {
					$link.attr({
						target: '_blank',
						rel: 'noopener noreferrer'
					});
				}
			});
		},

		smoothScrollTo: function ($target, offset) {
			if (!$target || !$target.length) {
				return;
			}

			offset = offset || 0;

			$('html, body').stop().animate({
				scrollTop: $target.offset().top - offset
			}, 450);
		},

		cleanNumber: function (number) {
			if (number % 1 === 0) {
				return parseInt(number, 10);
			}

			return parseFloat(number.toFixed(2));
		},

		escapeHtml: function (text) {
			if (!text) {
				return '';
			}

			return String(text)
				.replace(/&/g, '&amp;')
				.replace(/</g, '&lt;')
				.replace(/>/g, '&gt;')
				.replace(/"/g, '&quot;')
				.replace(/'/g, '&#039;');
		},

		escapeAttr: function (text) {
			return this.escapeHtml(text);
		}
	};

	$.fn.isInViewport = function () {
		if (!this.length) {
			return false;
		}

		var elementTop = this.offset().top;
		var elementBottom = elementTop + this.outerHeight();

		var viewportTop = $(window).scrollTop();
		var viewportBottom = viewportTop + $(window).height();

		return elementBottom > viewportTop && elementTop < viewportBottom;
	};

	$(document).ready(function () {
		RXWoo.init();
	});

	window.RXWoo = RXWoo;

})(jQuery, window, document);

Now enqueue it in your functions.php like this:

/**
 * RX Theme WooCommerce frontend script.
 */
function rx_theme_enqueue_woocommerce_script() {
	if ( ! class_exists( 'WooCommerce' ) ) {
		return;
	}

	wp_enqueue_script(
		'rx-theme-woocommerce',
		get_template_directory_uri() . '/inc/js/woocommerce.js',
		array( 'jquery', 'wc-add-to-cart', 'wc-cart-fragments' ),
		wp_get_theme()->get( 'Version' ),
		true
	);
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_woocommerce_script' );

For better styling, you can later add CSS classes such as:

.rx-qty-btn {
	cursor: pointer;
}

.rx-sticky-add-to-cart {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 9999;
	display: none;
	background: #fff;
	box-shadow: 0 -5px 25px rgba(0,0,0,.12);
	padding: 12px 20px;
}

.rx-sticky-add-to-cart.is-active {
	display: flex;
	align-items: center;
	justify-content: space-between;
}

.rx-sticky-product-info {
	display: flex;
	align-items: center;
	gap: 12px;
}

.rx-sticky-product-image {
	width: 50px;
	height: 50px;
	object-fit: cover;
	border-radius: 8px;
}

.rx-notice-close {
	float: right;
	border: 0;
	background: transparent;
	font-size: 20px;
	cursor: pointer;
}

.rx-woo-table-scroll {
	overflow-x: auto;
}

.rx-shop-filter-open .widget_price_filter,
.rx-shop-filter-open .woocommerce-widget-layered-nav {
	display: block;
}

Important note: this JavaScript is safe for a normal WooCommerce theme, but some features depend on your theme HTML structure. If your header cart or filter sidebar has different class names, we can adjust those selectors later.

Leave a Reply

Your email address will not be published. Required fields are marked *