feat: initial release of CommerceKit Floating Cart add-on v1.0.0

This commit is contained in:
2026-03-05 08:01:33 +01:00
commit dbf48a692d
3 changed files with 454 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
/* ==========================================================================
CommerceKit Floating Cart
========================================================================== */
/* Container fixed to bottom-right, above most theme layers */
#cgkit-floating-cart {
position: fixed;
bottom: 28px;
right: 28px;
z-index: 99998; /* below most modals/overlays but above normal content */
}
/* The circle button */
.cgkit-floating-cart__btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
padding: 0;
border: none;
border-radius: 50%;
background-color: #2c2c2c;
color: #ffffff;
cursor: pointer;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
/* Prevent tap-highlight on mobile */
-webkit-tap-highlight-color: transparent;
}
.cgkit-floating-cart__btn:hover,
.cgkit-floating-cart__btn:focus-visible {
background-color: #444444;
transform: scale(1.08);
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.3);
outline: none;
}
.cgkit-floating-cart__btn:active {
transform: scale(0.96);
}
/* Cart icon SVG */
.cgkit-floating-cart__icon {
display: block;
width: 22px;
height: 22px;
pointer-events: none;
}
/* Item-count badge */
.cgkit-floating-cart__count {
position: absolute;
top: -4px;
right: -4px;
min-width: 20px;
height: 20px;
padding: 0 4px;
border-radius: 10px;
background-color: #e84040;
color: #ffffff;
font-size: 11px;
font-weight: 700;
line-height: 20px;
text-align: center;
pointer-events: none;
transition: transform 0.15s ease;
}
/* Hide badge when cart is empty */
.cgkit-floating-cart__count--hidden {
display: none;
}
/* "Bump" animation when count changes */
.cgkit-floating-cart__count--bump {
animation: cgkit-fc-bump 0.3s ease;
}
@keyframes cgkit-fc-bump {
0% { transform: scale(1); }
50% { transform: scale(1.4); }
100% { transform: scale(1); }
}
/* "Pulse" ring that briefly appears after add-to-cart */
.cgkit-floating-cart__btn::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid #2c2c2c;
opacity: 0;
transform: scale(1);
}
.cgkit-floating-cart__btn.cgkit-fc--pulse::after {
animation: cgkit-fc-pulse 0.5s ease-out forwards;
}
@keyframes cgkit-fc-pulse {
0% { opacity: 0.6; transform: scale(1); }
100% { opacity: 0; transform: scale(1.6); }
}
/* Responsive: shrink slightly on small screens */
@media (max-width: 480px) {
#cgkit-floating-cart {
bottom: 18px;
right: 18px;
}
.cgkit-floating-cart__btn {
width: 50px;
height: 50px;
}
.cgkit-floating-cart__icon {
width: 20px;
height: 20px;
}
}

147
assets/js/floating-cart.js Normal file
View File

@@ -0,0 +1,147 @@
/**
* 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.
*/
( function ( $ ) {
'use strict';
/* -----------------------------------------------------------------------
* Module
* --------------------------------------------------------------------- */
var FloatingCart = {
$btn: null,
$count: null,
prevCount: 0,
/** Bootstrap */
init: function () {
if ( typeof cgkitFC === 'undefined' ) {
return;
}
this.$btn = $( '#cgkit-floating-cart .cgkit-floating-cart__btn' );
this.$count = $( '#cgkit-fc-count' );
if ( ! this.$btn.length ) {
return;
}
this.prevCount = parseInt( this.$count.text(), 10 ) || 0;
this.bindEvents();
},
/** Attach event listeners */
bindEvents: function () {
var self = this;
// --- Floating button click → open minicart -----------------------
this.$btn.on( 'click', function ( e ) {
e.preventDefault();
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();
}
);
},
/**
* Trigger the theme's minicart open mechanism.
*
* Strategy:
* 1. Try clicking the first element matching the configured selector.
* 2. If that element has no jQuery click handlers registered, dispatch
* a native 'click' so themes that use addEventListener also fire.
* 3. Last resort: navigate to the cart URL.
*/
openMinicart: function () {
var selectors = ( cgkitFC.minicartTrigger || '' ).split( ',' );
var $trigger = null;
for ( var i = 0; i < selectors.length; i++ ) {
var $el = $( $.trim( selectors[ i ] ) ).first();
if ( $el.length ) {
$trigger = $el;
break;
}
}
if ( $trigger && $trigger.length ) {
// Fire both jQuery and native click so all listeners catch it.
$trigger.trigger( 'click' );
var nativeEl = $trigger.get( 0 );
if ( nativeEl && typeof nativeEl.click === 'function' ) {
nativeEl.click();
}
return;
}
// No trigger found → fall back 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' );
setTimeout( function () {
self.$btn.removeClass( 'cgkit-fc--pulse' );
}, 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 ) {
return;
}
this.$count = $freshCount;
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
this.$count.addClass( 'cgkit-floating-cart__count--bump' );
}
}
};
/* -----------------------------------------------------------------------
* Boot
* --------------------------------------------------------------------- */
$( document ).ready( function () {
FloatingCart.init();
} );
} )( jQuery );

183
cgkit-floating-cart.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
/**
* 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.0
* Author: CommerceGurus
* Author URI: https://www.commercegurus.com
* License: GPLv3
* License URI: http://www.gnu.org/licenses/gpl.html
* Text Domain: cgkit-floating-cart
* Requires at least: 5.6
* Requires PHP: 7.4
* WC requires at least: 5.0
* WC tested up to: 9.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'CGKIT_FC_VERSION', '1.0.0' );
define( 'CGKIT_FC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'CGKIT_FC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
/**
* Declare WooCommerce HPOS compatibility.
*/
add_action( 'before_woocommerce_init', function () {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
}
} );
/**
* Main plugin class.
*/
class CommerceKit_Floating_Cart {
private static $instance = null;
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'wp_footer', [ $this, 'render_floating_cart' ] );
add_filter( 'woocommerce_add_to_cart_fragments', [ $this, 'cart_count_fragment' ] );
}
/**
* Enqueue frontend CSS and JS.
*/
public function enqueue_assets(): void {
wp_enqueue_style(
'cgkit-floating-cart',
CGKIT_FC_PLUGIN_URL . 'assets/css/floating-cart.css',
[],
CGKIT_FC_VERSION
);
wp_enqueue_script(
'cgkit-floating-cart',
CGKIT_FC_PLUGIN_URL . 'assets/js/floating-cart.js',
[ 'jquery' ],
CGKIT_FC_VERSION,
true
);
/**
* 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.
*/
$trigger = apply_filters(
'cgkit_fc_minicart_trigger',
'.site-header-cart .cart-click, .wcmenucart, a.cart-contents, .header-cart-link, .shoptimizer-cart-link'
);
/**
* Filter: cgkit_fc_auto_open
*
* Set to false to disable auto-opening the minicart after add to cart.
*
* @param bool $auto_open
*/
$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
*/
$delay = (int) apply_filters( 'cgkit_fc_auto_open_delay', 400 );
wp_localize_script( 'cgkit-floating-cart', 'cgkitFC', [
'minicartTrigger' => $trigger,
'autoOpen' => $auto_open ? 'yes' : 'no',
'autoOpenDelay' => $delay,
'cartUrl' => wc_get_cart_url(),
] );
}
/**
* Output the floating cart button HTML in the footer.
*/
public function render_floating_cart(): void {
$count = WC()->cart ? WC()->cart->get_cart_contents_count() : 0;
?>
<div id="cgkit-floating-cart" class="cgkit-floating-cart" role="complementary" aria-label="<?php esc_attr_e( 'Floating cart', 'cgkit-floating-cart' ); ?>">
<button class="cgkit-floating-cart__btn" type="button" aria-label="<?php esc_attr_e( 'Open mini cart', 'cgkit-floating-cart' ); ?>">
<?php echo $this->cart_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput ?>
<?php echo $this->count_badge_html( $count ); // phpcs:ignore WordPress.Security.EscapeOutput ?>
</button>
</div>
<?php
}
/**
* 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
*/
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 );
return $fragments;
}
// -------------------------------------------------------------------------
// 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(
'<span id="cgkit-fc-count" class="cgkit-floating-cart__count%s" aria-hidden="true">%s</span>',
esc_attr( $hidden ),
esc_html( $count > 0 ? $count : '' )
);
}
/**
* 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"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>';
}
}
/**
* Bootstrap after WooCommerce is loaded so WC() is available.
*/
add_action( 'woocommerce_loaded', function () {
CommerceKit_Floating_Cart::get_instance();
} );