feat: initial release of CommerceKit Floating Cart add-on v1.0.0
This commit is contained in:
124
assets/css/floating-cart.css
Normal file
124
assets/css/floating-cart.css
Normal 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
147
assets/js/floating-cart.js
Normal 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
183
cgkit-floating-cart.php
Normal 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();
|
||||
} );
|
||||
Reference in New Issue
Block a user