/** * CommerceKit Floating Cart – frontend script * * Responsibilities: * 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'; var FloatingCart = { $btn: null, $count: null, prevCount: 0, 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.bindFloatingButton(); this.bindArchiveAddToCart(); this.bindSingleProductAddToCart(); this.bindFragmentRefresh(); }, // ----------------------------------------------------------------- // 1. Floating button click → open minicart // ----------------------------------------------------------------- bindFloatingButton: function () { var self = this; this.$btn.on( 'click', function ( e ) { e.preventDefault(); // Stop bubbling to Shoptimizer's document-level "click outside = close" handler. e.stopPropagation(); self.openMinicart(); } ); }, // ----------------------------------------------------------------- // 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 ); // WC's wc-ajax=add_to_cart reads $_POST['product_id'], but the // single-product form submits the ID as field name 'add-to-cart'. // Append the correctly-named field so the endpoint finds it. var serialized = $form.serialize(); var productId = $form.find( '[name="add-to-cart"]' ).val() || $form.find( '[name="product_id"]' ).val() || ''; if ( productId && serialized.indexOf( 'product_id=' ) === -1 ) { serialized += '&product_id=' + encodeURIComponent( productId ); } $.post( ajaxUrl, serialized ) .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 // ----------------------------------------------------------------- /** * 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 () { var selectors = ( cgkitFC.minicartTrigger || '' ).split( ',' ); for ( var i = 0; i < selectors.length; i++ ) { var $el = $( $.trim( selectors[ i ] ) ).first(); if ( $el.length ) { $el[ 0 ].click(); return; } } // Absolute fallback: navigate to cart page. if ( cgkitFC.cartUrl ) { window.location.href = cgkitFC.cartUrl; } }, pulseBtn: function () { var self = this; this.$btn.addClass( 'cgkit-fc--pulse' ); setTimeout( function () { self.$btn.removeClass( 'cgkit-fc--pulse' ); }, 600 ); }, maybeBumpBadge: function () { var $fresh = $( '#cgkit-fc-count' ); if ( ! $fresh.length ) { return; } 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' ); void this.$count[ 0 ].offsetWidth; // force reflow to restart animation this.$count.addClass( 'cgkit-floating-cart__count--bump' ); } } }; $( document ).ready( function () { FloatingCart.init(); } ); } )( jQuery );