fix: FSN for guests via option filter, AJAX add-to-cart on single product pages

This commit is contained in:
2026-03-05 08:38:04 +01:00
parent 99e83931eb
commit f8081c6b65
2 changed files with 180 additions and 151 deletions

View File

@@ -2,25 +2,21 @@
* CommerceKit Floating Cart frontend script
*
* Responsibilities:
* 1. Open the CommerceKit / Shoptimizer minicart when the floating button is clicked.
* 2. Automatically open the minicart after a successful add-to-cart (AJAX or standard).
* 3. Play a small badge-bump animation when the item count changes.
*
* All selectors and feature flags come from the `cgkitFC` object localised by PHP.
* 1. Open the Shoptimizer minicart when the floating button is clicked.
* 2. Intercept single-product add-to-cart form submits, send them via AJAX,
* then open the minicart (WooCommerce's built-in AJAX only covers archives).
* 3. Auto-open the minicart after AJAX add-to-cart on archive/shop pages.
* 4. Animate the item-count badge when the cart changes.
*/
( function ( $ ) {
'use strict';
/* -----------------------------------------------------------------------
* Module
* --------------------------------------------------------------------- */
var FloatingCart = {
$btn: null,
$count: null,
$btn: null,
$count: null,
prevCount: 0,
/** Bootstrap */
init: function () {
if ( typeof cgkitFC === 'undefined' ) {
return;
@@ -35,87 +31,167 @@
this.prevCount = parseInt( this.$count.text(), 10 ) || 0;
this.bindEvents();
this.bindFloatingButton();
this.bindArchiveAddToCart();
this.bindSingleProductAddToCart();
this.bindFragmentRefresh();
},
/** Attach event listeners */
bindEvents: function () {
// -----------------------------------------------------------------
// 1. Floating button click → open minicart
// -----------------------------------------------------------------
bindFloatingButton: function () {
var self = this;
// --- Floating button click → open minicart -----------------------
this.$btn.on( 'click', function ( e ) {
e.preventDefault();
// Stop the click bubbling to document-level handlers.
// Shoptimizer closes the cart on any click outside .shoptimizer-cart,
// so without this the cart would open then instantly close again.
// Stop bubbling to Shoptimizer's document-level "click outside = close" handler.
e.stopPropagation();
self.openMinicart();
} );
// --- Auto-open after AJAX add-to-cart ----------------------------
if ( cgkitFC.autoOpen === 'yes' ) {
$( document.body ).on( 'added_to_cart', function () {
setTimeout( function () {
self.openMinicart();
self.pulseBtn();
}, parseInt( cgkitFC.autoOpenDelay, 10 ) || 400 );
} );
}
// --- Badge bump when WooCommerce refreshes fragments -------------
$( document.body ).on(
'wc_fragments_refreshed wc_fragments_loaded',
function () {
self.maybeBumpBadge();
}
);
},
// -----------------------------------------------------------------
// 2. Archive / shop pages WooCommerce fires `added_to_cart` natively
// after its own AJAX add-to-cart (enabled via PHP filter).
// -----------------------------------------------------------------
bindArchiveAddToCart: function () {
if ( cgkitFC.autoOpen !== 'yes' ) {
return;
}
var self = this;
$( document.body ).on( 'added_to_cart', function ( e, fragments, cartHash, $btn ) {
// Skip if this came from our own single-product handler
// (it will open the cart itself after the AJAX call).
if ( $btn && $btn.data( 'cgkit-fc-handled' ) ) {
return;
}
setTimeout( function () {
self.openMinicart();
self.pulseBtn();
}, parseInt( cgkitFC.autoOpenDelay, 10 ) || 400 );
} );
},
// -----------------------------------------------------------------
// 3. Single product page intercept form submit and use AJAX.
//
// WooCommerce's `woocommerce_enable_ajax_add_to_cart` option only
// covers archive loop buttons, NOT the single-product form. We handle
// it here so the page never reloads and the minicart opens instead.
// -----------------------------------------------------------------
bindSingleProductAddToCart: function () {
if ( cgkitFC.autoOpen !== 'yes' ) {
return;
}
var self = this;
var ajaxUrl = ( cgkitFC.wcAjaxUrl || '' ).replace( '%%endpoint%%', 'add_to_cart' );
if ( ! ajaxUrl ) {
return;
}
$( document ).on( 'submit', 'form.cart', function ( e ) {
var $form = $( this );
var $btn = $form.find( '.single_add_to_cart_button' );
// Skip disabled, loading, or variation-not-selected states.
if (
! $btn.length ||
$btn.hasClass( 'disabled' ) ||
$btn.hasClass( 'loading' ) ||
$btn.hasClass( 'wc-variation-selection-needed' )
) {
return;
}
// Skip grouped product forms (they POST to the cart page directly).
if ( $form.hasClass( 'grouped_form' ) ) {
return;
}
e.preventDefault();
$btn.addClass( 'loading' ).data( 'cgkit-fc-handled', true );
$.post( ajaxUrl, $form.serialize() )
.done( function ( response ) {
if ( ! response ) {
self.formFallback( $form, $btn );
return;
}
// WooCommerce signals a validation error by setting product_url.
if ( response.error && response.product_url ) {
window.location.href = response.product_url;
return;
}
// Let WooCommerce update all fragments (header count, mini-cart HTML, etc.)
$( document.body ).trigger( 'added_to_cart', [
response.fragments,
response.cart_hash,
$btn
] );
$btn.removeClass( 'loading' );
// Open the minicart after fragments have settled.
setTimeout( function () {
self.openMinicart();
self.pulseBtn();
}, parseInt( cgkitFC.autoOpenDelay, 10 ) || 400 );
} )
.fail( function () {
self.formFallback( $form, $btn );
} );
} );
},
// Fall back to a normal (non-AJAX) submit if the AJAX call fails.
formFallback: function ( $form, $btn ) {
$btn.removeClass( 'loading' ).removeData( 'cgkit-fc-handled' );
$( document ).off( 'submit', 'form.cart' );
$form.submit();
},
// -----------------------------------------------------------------
// 4. Badge animation when WooCommerce refreshes fragments
// -----------------------------------------------------------------
bindFragmentRefresh: function () {
var self = this;
$( document.body ).on( 'wc_fragments_refreshed wc_fragments_loaded', function () {
self.maybeBumpBadge();
} );
},
// -----------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------
/**
* Trigger the theme's minicart open mechanism.
*
* Strategy (stops at the first approach that succeeds):
* 1. Find the header cart button via the configured selector list and
* fire ONE native click on it. Using native click (not jQuery trigger
* + native) avoids the double-fire / toggle-closed bug.
* 2. If no button is found, try to show the Shoptimizer cart panel
* directly by toggling the body class the theme uses.
* 3. Last resort: navigate to the cart URL.
*
* NOTE: never call both $el.trigger('click') AND el.click() — that
* fires two events and the toggle cancels itself immediately.
* Open the Shoptimizer minicart by clicking its header button.
* Uses ONE native click only — never both jQuery trigger and native
* click, which would double-fire and toggle the cart back closed.
* Stops propagation so Shoptimizer's document "click outside" handler
* does not immediately close the cart again.
*/
openMinicart: function () {
// --- 1. Try clicking the configured header cart button ------------
var selectors = ( cgkitFC.minicartTrigger || '' ).split( ',' );
for ( var i = 0; i < selectors.length; i++ ) {
var $el = $( $.trim( selectors[ i ] ) ).first();
if ( $el.length ) {
// One native click reaches both jQuery and native listeners.
$el[ 0 ].click();
return;
}
}
// --- 2. Shoptimizer direct panel toggle --------------------------
// Shoptimizer's cart anchor is a.cart-contents inside .shoptimizer-cart.
// If the selector loop above somehow missed it, grab it directly.
var $directBtn = $( '.shoptimizer-cart a.cart-contents' );
if ( $directBtn.length ) {
$directBtn[ 0 ].click();
return;
}
// --- 3. Fall back to cart page -----------------------------------
// Absolute fallback: navigate to cart page.
if ( cgkitFC.cartUrl ) {
window.location.href = cgkitFC.cartUrl;
}
},
/**
* Add a pulse ring on the button after add-to-cart.
*/
pulseBtn: function () {
var self = this;
this.$btn.addClass( 'cgkit-fc--pulse' );
@@ -124,31 +200,23 @@
}, 600 );
},
/**
* If the item count increased, animate the badge.
*/
maybeBumpBadge: function () {
var $freshCount = $( '#cgkit-fc-count' );
// Re-query in case the fragment replaced the element.
if ( ! $freshCount.length ) {
var $fresh = $( '#cgkit-fc-count' );
if ( ! $fresh.length ) {
return;
}
this.$count = $freshCount;
this.$count = $fresh;
var newCount = parseInt( this.$count.text(), 10 ) || 0;
if ( newCount !== this.prevCount ) {
this.prevCount = newCount;
this.$count.removeClass( 'cgkit-floating-cart__count--bump' );
// Force reflow so removing + re-adding the class restarts the animation.
void this.$count[ 0 ].offsetWidth; // eslint-disable-line no-unused-expressions
void this.$count[ 0 ].offsetWidth; // force reflow to restart animation
this.$count.addClass( 'cgkit-floating-cart__count--bump' );
}
}
};
/* -----------------------------------------------------------------------
* Boot
* --------------------------------------------------------------------- */
$( document ).ready( function () {
FloatingCart.init();
} );