fix: FSN for guests via option filter, AJAX add-to-cart on single product pages
This commit is contained in:
@@ -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();
|
||||
} );
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: CommerceKit Floating Cart
|
||||
* Plugin URI: https://www.commercegurus.com
|
||||
* Description: Adds a floating cart icon (bottom-right) and auto-opens the CommerceKit minicart after add to cart. Requires CommerceGurus CommerceKit and WooCommerce.
|
||||
* Version: 1.0.4
|
||||
* Version: 1.1.0
|
||||
* Author: CommerceGurus
|
||||
* Author URI: https://www.commercegurus.com
|
||||
* License: GPLv3
|
||||
@@ -19,7 +19,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'CGKIT_FC_VERSION', '1.0.4' );
|
||||
define( 'CGKIT_FC_VERSION', '1.1.0' );
|
||||
define( 'CGKIT_FC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'CGKIT_FC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
@@ -32,6 +32,30 @@ add_action( 'before_woocommerce_init', function () {
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* Force CommerceKit's "show FSN before shipping address is entered" flag on.
|
||||
*
|
||||
* Registered early (plugins_loaded priority 5) so it is in place before
|
||||
* CommerceKit reads the option at plugins_loaded priority 10+.
|
||||
*
|
||||
* Why here rather than in the class: this filter must be attached BEFORE
|
||||
* WooCommerce and CommerceKit finish loading so the very first option read
|
||||
* during any request already sees fsn_before_ship = 1.
|
||||
*
|
||||
* What fsn_before_ship does inside CommerceKit:
|
||||
* if ( ! $fsn_before_ship ) {
|
||||
* if ( ! $show_shipping ) { return ''; } // bails for guests
|
||||
* }
|
||||
* Setting it to 1 skips that entire block, so the notification renders
|
||||
* regardless of whether WooCommerce has calculated shipping yet.
|
||||
*/
|
||||
add_filter( 'option_commercekit', function ( $options ) {
|
||||
if ( is_array( $options ) ) {
|
||||
$options['fsn_before_ship'] = 1;
|
||||
}
|
||||
return $options;
|
||||
}, 1 );
|
||||
|
||||
/**
|
||||
* Main plugin class.
|
||||
*/
|
||||
@@ -55,40 +79,9 @@ class CommerceKit_Floating_Cart {
|
||||
// stays put and we can open the minicart instead.
|
||||
add_filter( 'pre_option_woocommerce_cart_redirect_after_add', [ $this, 'disable_cart_redirect' ] );
|
||||
|
||||
// Ensure WooCommerce's AJAX add-to-cart is active so the body receives
|
||||
// the `added_to_cart` JS event that our script listens for.
|
||||
// Ensure WooCommerce's AJAX add-to-cart is active so archive/shop page
|
||||
// buttons fire the `added_to_cart` JS event without a page reload.
|
||||
add_filter( 'pre_option_woocommerce_enable_ajax_add_to_cart', [ $this, 'enable_ajax_add_to_cart' ] );
|
||||
|
||||
// Show the CommerceKit free shipping notification for guests.
|
||||
// WC's show_shipping() returns false for logged-out users when the store
|
||||
// hides shipping costs until an address is entered, which causes CommerceKit
|
||||
// to skip rendering the bar entirely. We force shipping => true for any
|
||||
// non-empty cart so the notification always renders.
|
||||
add_filter( 'commercekit_fsn_get_cart_options', [ $this, 'fsn_show_for_guests' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the CommerceKit free shipping notification to render for logged-out
|
||||
* users. WC_Cart::show_shipping() returns false for guests when the store
|
||||
* option "Hide shipping costs until an address is entered" is active, which
|
||||
* makes CommerceKit bail before building the bar HTML. Overriding the
|
||||
* `shipping` key here only affects the FSN display logic — WooCommerce's
|
||||
* actual shipping calculation is untouched.
|
||||
*
|
||||
* @param array $options Result from commercekit_fsn_get_cart_options().
|
||||
* @return array
|
||||
*/
|
||||
public function fsn_show_for_guests( array $options ): array {
|
||||
if ( ! isset( $options['shipping'] ) || $options['shipping'] ) {
|
||||
return $options; // already showing, nothing to do.
|
||||
}
|
||||
|
||||
$cart = WC()->cart;
|
||||
if ( $cart && $cart->get_cart_contents_count() > 0 && $options['amount'] > 0 ) {
|
||||
$options['shipping'] = true;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,10 +93,8 @@ class CommerceKit_Floating_Cart {
|
||||
|
||||
/**
|
||||
* Return 'yes' so WooCommerce uses AJAX add-to-cart on archive/shop pages.
|
||||
* This ensures `added_to_cart` JS event fires without a page reload.
|
||||
*/
|
||||
public function enable_ajax_add_to_cart( string $value ): string {
|
||||
// Only override when it's not already enabled.
|
||||
return ( 'yes' === $value ) ? $value : 'yes';
|
||||
}
|
||||
|
||||
@@ -128,39 +119,17 @@ class CommerceKit_Floating_Cart {
|
||||
|
||||
/**
|
||||
* Filter: cgkit_fc_minicart_trigger
|
||||
*
|
||||
* CSS selector(s) for the theme's minicart open button. The floating
|
||||
* cart icon and the add-to-cart auto-open both programmatically click
|
||||
* the first matching element.
|
||||
*
|
||||
* For Shoptimizer / CommerceKit the default covers the most common
|
||||
* header cart selectors. Override this filter in your child theme if
|
||||
* needed.
|
||||
*
|
||||
* @param string $selector A comma-separated CSS selector string.
|
||||
* CSS selector(s) for the theme's minicart open button.
|
||||
*/
|
||||
$trigger = apply_filters(
|
||||
'cgkit_fc_minicart_trigger',
|
||||
'.shoptimizer-cart .cart-contents, .site-header-cart .cart-contents, .wcmenucart, .header-cart-link'
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter: cgkit_fc_auto_open
|
||||
*
|
||||
* Set to false to disable auto-opening the minicart after add to cart.
|
||||
*
|
||||
* @param bool $auto_open
|
||||
*/
|
||||
/** Filter: cgkit_fc_auto_open — set false to disable auto-open after add. */
|
||||
$auto_open = (bool) apply_filters( 'cgkit_fc_auto_open', true );
|
||||
|
||||
/**
|
||||
* Filter: cgkit_fc_auto_open_delay
|
||||
*
|
||||
* Milliseconds to wait before opening the minicart after add to cart,
|
||||
* to allow WooCommerce fragments to refresh first.
|
||||
*
|
||||
* @param int $delay
|
||||
*/
|
||||
/** Filter: cgkit_fc_auto_open_delay — ms to wait before opening (default 400). */
|
||||
$delay = (int) apply_filters( 'cgkit_fc_auto_open_delay', 400 );
|
||||
|
||||
wp_localize_script( 'cgkit-floating-cart', 'cgkitFC', [
|
||||
@@ -168,6 +137,8 @@ class CommerceKit_Floating_Cart {
|
||||
'autoOpen' => $auto_open ? 'yes' : 'no',
|
||||
'autoOpenDelay' => $delay,
|
||||
'cartUrl' => wc_get_cart_url(),
|
||||
// WooCommerce AJAX endpoint used for single-product AJAX add-to-cart.
|
||||
'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
|
||||
] );
|
||||
}
|
||||
|
||||
@@ -187,15 +158,11 @@ class CommerceKit_Floating_Cart {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a WooCommerce fragment so the badge count updates via AJAX
|
||||
* (e.g. after mini-cart quantity changes or AJAX add-to-cart).
|
||||
*
|
||||
* @param array $fragments
|
||||
* @return array
|
||||
* WooCommerce fragment — keeps the badge count live after AJAX operations.
|
||||
*/
|
||||
public function cart_count_fragment( array $fragments ): array {
|
||||
$count = WC()->cart ? WC()->cart->get_cart_contents_count() : 0;
|
||||
$fragments['#cgkit-fc-count'] = $this->count_badge_html( $count );
|
||||
$count = WC()->cart ? WC()->cart->get_cart_contents_count() : 0;
|
||||
$fragments['#cgkit-fc-count'] = $this->count_badge_html( $count );
|
||||
return $fragments;
|
||||
}
|
||||
|
||||
@@ -203,9 +170,6 @@ class CommerceKit_Floating_Cart {
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the count badge HTML (targeted by fragments).
|
||||
*/
|
||||
private function count_badge_html( int $count ): string {
|
||||
$hidden = 0 === $count ? ' cgkit-floating-cart__count--hidden' : '';
|
||||
return sprintf(
|
||||
@@ -215,9 +179,6 @@ class CommerceKit_Floating_Cart {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shopping-cart SVG icon (Feather icons style, matches CommerceKit aesthetic).
|
||||
*/
|
||||
private function cart_icon_svg(): string {
|
||||
return '<svg class="cgkit-floating-cart__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
width="24" height="24" aria-hidden="true" focusable="false"
|
||||
|
||||
Reference in New Issue
Block a user