Files
CommerceKit-FloatingCart/cgkit-floating-cart.php

200 lines
6.8 KiB
PHP
Raw Normal View History

<?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.1.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.1.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 );
}
} );
/**
* 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.
*/
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' ] );
// Disable WooCommerce's "redirect to cart page after add" so the page
// 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 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' ] );
}
/**
* Return 'no' so WooCommerce never redirects to /cart after adding a product.
*/
public function disable_cart_redirect( string $value ): string {
return 'no';
}
/**
* Return 'yes' so WooCommerce uses AJAX add-to-cart on archive/shop pages.
*/
public function enable_ajax_add_to_cart( string $value ): string {
return ( 'yes' === $value ) ? $value : 'yes';
}
/**
* 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.
*/
$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 false to disable auto-open after add. */
$auto_open = (bool) apply_filters( 'cgkit_fc_auto_open', true );
/** 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', [
'minicartTrigger' => $trigger,
'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%%' ),
] );
}
/**
* 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
}
/**
* 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 );
return $fragments;
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
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 : '' )
);
}
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();
} );