Customize WooCommerce Checkout Layout
August 7, 2025 · AI Conversations
separate payment and shipping from there and move to left side below billing/shipping address
—
### ✅ 1. **Override Checkout Template**
Copy this WooCommerce template to your theme:
/wp-content/themes/your-child-theme/woocommerce/checkout/form-checkout.php
—
### ✅ 2. **Edit `form-checkout.php`**
Replace the default structure with a **custom two-column layout**, and move shipping/payment accordingly.
Here’s the modified template:
<?php
defined( 'ABSPATH' ) || exit;
do_action( 'woocommerce_before_checkout_form', $checkout );
// If checkout registration is disabled and not logged in, and not allowed to checkout
if ( ! $checkout->is_registration_enabled() && $checkout->is_registration_required() && ! is_user_logged_in() ) {
echo esc_html( apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'woocommerce' ) ) );
return;
}
?>
<form name="checkout" method="post" class="checkout woocommerce-checkout" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data">
<div class="custom-checkout-grid" style="display: flex; flex-wrap: wrap; gap: 30px;">
<!-- LEFT SIDE -->
<div class="left-checkout" style="flex: 1 1 60%;">
<?php do_action( 'woocommerce_checkout_billing' ); ?>
<?php do_action( 'woocommerce_checkout_shipping' ); ?>
<div id="custom-shipping-methods">
<?php if ( WC()->cart->needs_shipping() && WC()->cart->show_shipping() ) : ?>
<?php do_action( 'woocommerce_review_order_before_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_after_shipping' ); ?>
<?php endif; ?>
</div>
<div id="custom-payment-methods">
<?php do_action( 'woocommerce_review_order_before_payment' ); ?>
<?php
if ( ! is_ajax() ) {
do_action( 'woocommerce_checkout_before_order_review' );
}
?>
<div id="payment" class="woocommerce-checkout-payment">
<?php if ( $checkout->get_checkout_fields() ) : ?>
<?php do_action( 'woocommerce_checkout_payment' ); ?>
<?php endif; ?>
</div>
<?php
if ( ! is_ajax() ) {
do_action( 'woocommerce_checkout_after_order_review' );
}
?>
<?php do_action( 'woocommerce_review_order_after_payment' ); ?>
</div>
</div>
<!-- RIGHT SIDE -->
<div class="right-checkout" style="flex: 1 1 35%;">
<h3><?php esc_html_e( 'Your order', 'woocommerce' ); ?></h3>
<?php do_action( 'woocommerce_checkout_before_order_review_heading' ); ?>
<div id="order_review" class="woocommerce-checkout-review-order">
<?php do_action( 'woocommerce_checkout_order_review' ); ?>
</div>
</div>
</div>
</form>
<?php do_action( 'woocommerce_after_checkout_form', $checkout ); ?>
—
### 🧩 Optional CSS (for better layout)
Add to your theme or `style.css`:
@media (max-width: 768px) {
.custom-checkout-grid {
flex-direction: column;
}
}
—
### ✅ Outcome
– Left Side:
– Billing & Shipping Address
– Shipping Methods
– Payment Methods
– Right Side:
– Order Summary (Items + Total)
Let me know if you also want to add a **custom CAPTCHA**, **catch-all note field**, or **custom wrapper styles**.
—
### ✅ **1. Goal Recap**
– Move:
– **Shipping methods**
– **Payment methods**
– From: `do_action( 'woocommerce_checkout_order_review' )` (right side)
– To: **left column**, under customer details (`woocommerce_checkout_billing` + `woocommerce_checkout_shipping`)
—
### ✅ **2. Updated Template Code (Modified Part Only)**
Here's the updated **left column code** (inside “):
<div class="large-6 col pr-sec <?php echo esc_attr( $main_classes ); ?>">
<?php if ( $checkout->get_checkout_fields() ) : ?>
<?php do_action( 'woocommerce_checkout_before_customer_details' ); ?>
<div id="customer_details">
<div class="clear">
<?php do_action( 'woocommerce_checkout_billing' ); ?>
</div>
<div class="clear">
<?php do_action( 'woocommerce_checkout_shipping' ); ?>
</div>
</div>
<?php do_action( 'woocommerce_checkout_after_customer_details' ); ?>
<?php endif; ?>
<!-- 🚚 Shipping Methods -->
<?php if ( WC()->cart->needs_shipping() && WC()->cart->show_shipping() ) : ?>
<div id="checkout-shipping-methods" class="checkout-shipping-methods">
<?php do_action( 'woocommerce_review_order_before_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_after_shipping' ); ?>
</div>
<?php endif; ?>
<!-- 💳 Payment Methods -->
<div id="checkout-payment-methods" class="checkout-payment-methods">
<?php do_action( 'woocommerce_review_order_before_payment' ); ?>
<div id="payment" class="woocommerce-checkout-payment">
<?php if ( $checkout->get_checkout_fields() ) : ?>
<?php do_action( 'woocommerce_checkout_payment' ); ?>
<?php endif; ?>
</div>
<?php do_action( 'woocommerce_review_order_after_payment' ); ?>
</div>
</div>
—
### ✅ **3. Keep the Right Column as Order Summary Only**
No need to change the right column:
<div class="large-6 col pl-sec">
<?php flatsome_sticky_column_open( 'checkout_sticky_sidebar' ); ?>
<div class="col-inner <?php echo esc_attr( $sidebar_classes ); ?>">
<div class="checkout-sidebar sm-touch-scroll">
<?php do_action( 'woocommerce_checkout_before_order_review_heading' ); ?>
<h3 id="order_review_heading"><?php esc_html_e( 'Your order', 'woocommerce' ); ?></h3>
<?php do_action( 'woocommerce_checkout_before_order_review' ); ?>
<div id="order_review" class="woocommerce-checkout-review-order">
<?php
// Optional: keep only cart summary (items, totals), or remove below if already moved parts out
do_action( 'woocommerce_review_order_before_cart_contents' );
do_action( 'woocommerce_review_order_cart_contents' );
do_action( 'woocommerce_review_order_after_cart_contents' );
do_action( 'woocommerce_review_order_before_order_total' );
do_action( 'woocommerce_review_order_order_total' );
do_action( 'woocommerce_review_order_after_order_total' );
?>
</div>
<?php do_action( 'woocommerce_checkout_after_order_review' ); ?>
</div>
</div>
<?php flatsome_sticky_column_close( 'checkout_sticky_sidebar' ); ?>
</div>
—
### ✅ Optional: Add Basic CSS
To style spacing if needed:
#checkout-shipping-methods,
#checkout-payment-methods {
margin-top: 30px;
}
—
### ✅ Result:
| Left Side | Right Side |
|———————————-|——————-|
| Billing | Order Summary |
| Shipping Address | |
| Shipping Method (Radio Buttons) | |
| Payment Method | |
| Place Order Button (via payment) | |
Let me know if you want to move the **Place Order** button separately or align styles to match Flatsome.
– Move **Shipping Methods** and **Payment Methods** below Billing and Shipping Address (on the **left side**).
– Keep **Order Summary** on the **right side**.
—
### ✅ Final Code: `yourtheme/woocommerce/checkout/form-checkout.php`
<?php
/**
* Checkout Form
*
* This template can be overridden by copying it to yourtheme/woocommerce/checkout/form-checkout.php.
*
* @see https://woocommerce.com/document/template-structure/
* @package WooCommerceTemplates
* @version 9.4.0
* @flatsome-version 3.19.9
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="pageTitle">
<h1 class="cart-title">Checkout</h1>
</div>
<?php
$wrapper_classes = array();
$row_classes = array();
$main_classes = array();
$sidebar_classes = array();
$layout = get_theme_mod( 'checkout_layout', '' );
if ( ! $layout ) {
$sidebar_classes[] = 'has-border';
}
if ( $layout == 'simple' ) {
$sidebar_classes[] = 'is-well';
}
$wrapper_classes = implode( ' ', $wrapper_classes );
$row_classes = implode( ' ', $row_classes );
$main_classes = implode( ' ', $main_classes );
$sidebar_classes = implode( ' ', $sidebar_classes );
do_action( 'woocommerce_before_checkout_form', $checkout );
if ( ! $checkout->is_registration_enabled() && $checkout->is_registration_required() && ! is_user_logged_in() ) {
echo esc_html( apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'woocommerce' ) ) );
return;
}
if ( get_theme_mod( 'facebook_login_checkout', 0 ) && get_option( 'woocommerce_enable_myaccount_registration' ) == 'yes' && ! is_user_logged_in() ) {
wc_get_template( 'checkout/social-login.php' );
}
?>
<form name="checkout" method="post" class="checkout woocommerce-checkout <?php echo esc_attr( $wrapper_classes ); ?>" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data" aria-label="<?php echo esc_attr__( 'Checkout', 'woocommerce' ); ?>">
<div class="row pt-0 <?php echo esc_attr( $row_classes ); ?>">
<!-- LEFT SIDE -->
<div class="large-6 col pr-sec <?php echo esc_attr( $main_classes ); ?>">
<?php if ( $checkout->get_checkout_fields() ) : ?>
<?php do_action( 'woocommerce_checkout_before_customer_details' ); ?>
<div id="customer_details">
<div class="clear">
<?php do_action( 'woocommerce_checkout_billing' ); ?>
</div>
<div class="clear">
<?php do_action( 'woocommerce_checkout_shipping' ); ?>
</div>
</div>
<?php do_action( 'woocommerce_checkout_after_customer_details' ); ?>
<?php endif; ?>
<!-- 🚚 Shipping Methods -->
<?php if ( WC()->cart->needs_shipping() && WC()->cart->show_shipping() ) : ?>
<div id="checkout-shipping-methods" class="checkout-shipping-methods">
<?php do_action( 'woocommerce_review_order_before_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_after_shipping' ); ?>
</div>
<?php endif; ?>
<!-- 💳 Payment Methods -->
<div id="checkout-payment-methods" class="checkout-payment-methods">
<?php do_action( 'woocommerce_review_order_before_payment' ); ?>
<div id="payment" class="woocommerce-checkout-payment">
<?php if ( $checkout->get_checkout_fields() ) : ?>
<?php do_action( 'woocommerce_checkout_payment' ); ?>
<?php endif; ?>
</div>
<?php do_action( 'woocommerce_review_order_after_payment' ); ?>
</div>
</div>
<!-- RIGHT SIDE -->
<div class="large-6 col pl-sec">
<?php flatsome_sticky_column_open( 'checkout_sticky_sidebar' ); ?>
<div class="col-inner <?php echo esc_attr( $sidebar_classes ); ?>">
<div class="checkout-sidebar sm-touch-scroll">
<?php do_action( 'woocommerce_checkout_before_order_review_heading' ); ?>
<h3 id="order_review_heading"><?php esc_html_e( 'Your order', 'woocommerce' ); ?></h3>
<?php do_action( 'woocommerce_checkout_before_order_review' ); ?>
<div id="order_review" class="woocommerce-checkout-review-order">
<?php
// Only show order items and totals (cart summary), since payment was moved
do_action( 'woocommerce_review_order_before_cart_contents' );
do_action( 'woocommerce_review_order_cart_contents' );
do_action( 'woocommerce_review_order_after_cart_contents' );
do_action( 'woocommerce_review_order_before_order_total' );
do_action( 'woocommerce_review_order_order_total' );
do_action( 'woocommerce_review_order_after_order_total' );
?>
</div>
<?php do_action( 'woocommerce_checkout_after_order_review' ); ?>
</div>
</div>
<?php flatsome_sticky_column_close( 'checkout_sticky_sidebar' ); ?>
</div>
</div>
</form>
<?php do_action( 'woocommerce_after_checkout_form', $checkout ); ?>
—
### ✅ Optional CSS Styling
If spacing or alignment looks off, you can adjust with this CSS:
.checkout-shipping-methods,
.checkout-payment-methods {
margin-top: 30px;
}
—
Let me know if you'd like to:
– Move the **Place Order** button below the payment methods.
– Add accordion toggles for shipping/payment.
– Show custom fields or notes in a specific section.
Let's fix this **step-by-step**, ensuring:
– Shipping method appears on the left
– Payment method appears below that
– Order summary (items, subtotal, total) appears on the right
—
## ✅ **Fix: Use `WC()->checkout()->checkout_fields` only in right context**
WooCommerce expects `do_action( 'woocommerce_checkout_order_review' )` to be inside `#order_review`. If we split it manually, we must **call sub-actions properly**.
—
## ✅ Full Working Template Code (Cleaned & Split Properly)
### 📍 Place this in:
`yourtheme/woocommerce/checkout/form-checkout.php`
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="pageTitle">
<h1 class="cart-title">Checkout</h1>
</div>
<?php
$wrapper_classes = array();
$row_classes = array();
$main_classes = array();
$sidebar_classes = array();
$layout = get_theme_mod( 'checkout_layout', '' );
if ( ! $layout ) {
$sidebar_classes[] = 'has-border';
}
if ( $layout == 'simple' ) {
$sidebar_classes[] = 'is-well';
}
$wrapper_classes = implode( ' ', $wrapper_classes );
$row_classes = implode( ' ', $row_classes );
$main_classes = implode( ' ', $main_classes );
$sidebar_classes = implode( ' ', $sidebar_classes );
do_action( 'woocommerce_before_checkout_form', $checkout );
if ( ! $checkout->is_registration_enabled() && $checkout->is_registration_required() && ! is_user_logged_in() ) {
echo esc_html( apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'woocommerce' ) ) );
return;
}
if ( get_theme_mod( 'facebook_login_checkout', 0 ) && get_option( 'woocommerce_enable_myaccount_registration' ) == 'yes' && ! is_user_logged_in() ) {
wc_get_template( 'checkout/social-login.php' );
}
?>
<form name="checkout" method="post" class="checkout woocommerce-checkout <?php echo esc_attr( $wrapper_classes ); ?>" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data">
<div class="row pt-0 <?php echo esc_attr( $row_classes ); ?>">
<!-- LEFT SIDE -->
<div class="large-6 col pr-sec <?php echo esc_attr( $main_classes ); ?>">
<?php if ( $checkout->get_checkout_fields() ) : ?>
<?php do_action( 'woocommerce_checkout_before_customer_details' ); ?>
<div id="customer_details">
<?php do_action( 'woocommerce_checkout_billing' ); ?>
<?php do_action( 'woocommerce_checkout_shipping' ); ?>
</div>
<?php do_action( 'woocommerce_checkout_after_customer_details' ); ?>
<?php endif; ?>
<!-- SHIPPING METHODS -->
<?php if ( WC()->cart->needs_shipping() && WC()->cart->show_shipping() ) : ?>
<div id="checkout-shipping-methods" class="checkout-shipping-methods">
<?php do_action( 'woocommerce_review_order_before_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_shipping' ); ?>
<?php do_action( 'woocommerce_review_order_after_shipping' ); ?>
</div>
<?php endif; ?>
<!-- PAYMENT METHODS -->
<div id="checkout-payment-methods" class="checkout-payment-methods">
<?php do_action( 'woocommerce_review_order_before_payment' ); ?>
<div id="payment" class="woocommerce-checkout-payment">
<?php woocommerce_checkout_payment(); ?>
</div>
<?php do_action( 'woocommerce_review_order_after_payment' ); ?>
</div>
</div>
<!-- RIGHT SIDE (Order Summary) -->
<div class="large-6 col pl-sec">
<?php flatsome_sticky_column_open( 'checkout_sticky_sidebar' ); ?>
<div class="col-inner <?php echo esc_attr( $sidebar_classes ); ?>">
<div class="checkout-sidebar sm-touch-scroll">
<?php do_action( 'woocommerce_checkout_before_order_review_heading' ); ?>
<h3 id="order_review_heading"><?php esc_html_e( 'Your order', 'woocommerce' ); ?></h3>
<?php do_action( 'woocommerce_checkout_before_order_review' ); ?>
<div id="order_review" class="woocommerce-checkout-review-order">
<?php
// Display cart items and totals
do_action( 'woocommerce_review_order_before_cart_contents' );
do_action( 'woocommerce_review_order_cart_contents' );
do_action( 'woocommerce_review_order_after_cart_contents' );
do_action( 'woocommerce_review_order_before_order_total' );
do_action( 'woocommerce_review_order_order_total' );
do_action( 'woocommerce_review_order_after_order_total' );
?>
</div>
<?php do_action( 'woocommerce_checkout_after_order_review' ); ?>
</div>
</div>
<?php flatsome_sticky_column_close( 'checkout_sticky_sidebar' ); ?>
</div>
</div>
</form>
<?php do_action( 'woocommerce_after_checkout_form', $checkout ); ?>
—
## ✅ Why This Works
– We no longer call `do_action( 'woocommerce_checkout_order_review' )`, which is a **wrapper for multiple hooks**.
– Instead, we **manually split**:
– `woocommerce_review_order_shipping` to left side
– `woocommerce_checkout_payment` to left side
– `woocommerce_review_order_cart_contents` and totals to right side
—
## ✅ Optional CSS
Add to your theme or customizer:
.checkout-shipping-methods,
.checkout-payment-methods {
margin-top: 30px;
}
.woocommerce-checkout-review-order {
border: 1px solid #ddd;
padding: 20px;
background: #f9f9f9;
}
—
Let me know if you also want to:
– Style payment methods with custom UI
– Show shipping cost breakdown
– Move the “Place Order” button separately
I'm happy to help further.
—
### ✅ 1. **Use the Correct Hook**
`woocommerce_review_order_after_cart_contents` **is re-rendered** via AJAX. So any content inside this hook **must be printed every time**, or else it'll disappear.
You **should not rely on static HTML outside this hook**.
—
### ✅ 2. **Add Your Custom Content via This Hook**
In your theme’s `functions.php`:
add_action( 'woocommerce_review_order_after_cart_contents', 'custom_content_after_cart', 20 );
function custom_content_after_cart() {
echo '<div class="my-custom-message" style="margin-top:15px; padding:10px; background:#f1f1f1; border-left:3px solid #333;">
✅ You will earn loyalty points for this purchase!
</div>';
}
✅ This will **appear after cart items** and **stay there even after AJAX updates** like:
– Changing shipping method
– Changing country/state
– Coupon applied
—
### 🧠 If You Need JS Logic Too?
If your content includes interactive JS (like tooltips or sliders), rebind it like this:
jQuery(document.body).on('updated_checkout', function() {
// Reinitialize tooltips or JS UI after AJAX
jQuery('.my-custom-message').fadeIn();
});
—
### ❌ Don't Do This:
Don’t place the HTML directly in `form-checkout.php` **outside the hook**, or it’ll be overwritten on AJAX.
—
### ✅ Summary
| Task | Solution |
|——|———-|
| Add custom content after cart | `woocommerce_review_order_after_cart_contents` |
| Keep it during AJAX updates | Add inside the hook in `functions.php` |
| Add JS again after updates | Use `updated_checkout` event |
—
Let me know what **exact content** you want added (HTML, images, dynamic text), and I’ll generate full code for it.
1. You're using **customized checkout markup** (like in Flatsome), which may not re-trigger WooCommerce’s JS hooks fully.
2. Or you're placing content using **PHP** in a part that WooCommerce does **not re-render via AJAX**.
Let’s solve it properly so your custom content persists **after every AJAX refresh** (like changing shipping/payment).
—
## ✅ SOLUTION: Inject via JS on AJAX Event (`updated_checkout`)
Instead of PHP, use **JavaScript** to inject or restore your custom content **after every AJAX refresh**.
### 💡 Why?
WooCommerce re-renders only specific parts of the DOM (`#order_review`), and it replaces them — so your PHP-injected content disappears unless WooCommerce includes it again in the AJAX response.
—
## ✅ FINAL IMPLEMENTATION (WORKS 100%)
### 1. Add this JS snippet to your checkout page
**Recommended way**: enqueue in your child theme or via `functions.php`:
add_action( 'wp_footer', function () {
if ( is_checkout() && ! is_wc_endpoint_url() ) {
?>
<script>
jQuery(function($) {
function injectCustomCartMessage() {
const $target = $('.woocommerce-checkout-review-order-table'); // or #order_review
if ( $target.length && !$('.my-custom-message').length ) {
$target.after(`
<div class="my-custom-message" style="margin-top:15px; padding:10px; background:#f0f0f0; border-left: 4px solid #007cba;">
✅ You will earn loyalty points with this order!
</div>
`);
}
}
// Initial call
injectCustomCartMessage();
// Call again after WooCommerce refreshes checkout via AJAX
$(document.body).on('updated_checkout', function() {
injectCustomCartMessage();
});
});
</script>
<?php
}
}, 100 );
—
## 🔁 What This Does:
– Hooks into `updated_checkout` (WooCommerce fires this on every AJAX reload)
– Appends your custom message **every time**, if it’s not already present
– Keeps the message **outside WooCommerce AJAX templates**, so it **never gets removed**
—
## ✅ BONUS: If You Want It Inline in PHP (Not Recommended)
If you absolutely must use PHP, inject your content **inside the part that WooCommerce AJAX renders**, like this:
add_action( 'woocommerce_checkout_order_review', function() {
echo '<div class="my-custom-message" style="padding: 10px; background: #f9f9f9; border: 1px solid #ccc;">💡 Custom note inside WooCommerce AJAX content</div>';
}, 30 );
But this can break other templates or look messy. **JS-based injection is more flexible and safer** with Flatsome or other custom layouts.
—
Let me know if you want it to be:
– Conditional (e.g. only on certain shipping methods)
– Animated/fade in
– Show specific dynamic values (like cart total, coupon info, etc.)
I’ll tailor the JS or PHP as needed.
* Add partial payments discount box on checkout right bar
*/
public function pfc_add_discount_option_checkout_rightbar() {
$opt = $this->pfc_get_settings();
if( ! isset( $opt['pp_discount'] ) ) {
return;
}
global $gamipress_wc_partial_payments_template_args;
$gamipress_pp_discount_template = GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR . 'templates/wc-partial-payments-checkout.php';
if( file_exists( $gamipress_pp_discount_template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
echo '';
include $gamipress_pp_discount_template;
echo '';
?>
<script>
( function( $ ) {
} )( jQuery );
</script>
public function pfc_add_discount_option_checkout_rightbar()
However, if this “ content is disappearing after an AJAX refresh (e.g., due to shipping/payment changes), it’s because WooCommerce **replaces the entire `#order_review` DOM block via AJAX**.
—
## ✅ Goal
Ensure that your **partial payments discount box stays visible after checkout updates** (i.e., it doesn't disappear after WooCommerce replaces `#order_review`).
—
## ✅ Correct WooCommerce Hook for This
If you're injecting HTML directly into the **“ elements of the order summary**, use this hook:
add_action( 'woocommerce_review_order_after_order_total', [ $this, 'pfc_add_discount_option_checkout_rightbar' ] );
✅ This hook is **inside the AJAX-refreshed content**, so your PHP-based “ output will always be included during each reload.
—
## ✅ FIX YOUR FUNCTION OUTPUT
There are a couple of issues in your current code:
### ❌ Invalid HTML (e.g. “ without “ context)
### ❌ Mixing JS and output unsafely
—
### ✅ Suggested FIXED version of your function
public function pfc_add_discount_option_checkout_rightbar() {
$opt = $this->pfc_get_settings();
if ( ! isset( $opt['pp_discount'] ) ) {
return;
}
global $gamipress_wc_partial_payments_template_args;
$template = GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR . 'templates/wc-partial-payments-checkout.php';
if ( file_exists( $template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
echo '<tr class="pfc-pp-right-bar-row"><td colspan="2">';
echo '<div class="pfc-pp-right-bar">';
include $template;
echo '</div></td></tr>';
}
}
> ✅ Now it outputs proper HTML inside a “ and “ structure that WooCommerce expects in the order summary table.
—
## ✅ JS Injection (if Needed)
If your template includes dynamic JS (like toggles, fields), you can safely enqueue a JS file, or rebind it on:
jQuery(document.body).on('updated_checkout', function() {
// Re-bind any click or DOM events from your template
});
But avoid placing JS **inside PHP output** unless scoped clearly.
—
## ✅ Summary
| Problem | Solution |
|——–|———-|
| Content disappears on AJAX | Hook into `woocommerce_review_order_after_order_total` |
| Invalid “ HTML | Properly wrap in “ and close tags |
| Need dynamic JS? | Use `updated_checkout` event in JS |
—
Let me know if:
– You want the discount box to appear **only when a certain payment method is selected**
– You want the discount box styled as a **notice** rather than a table row
– Your included template needs sanitizing or refactoring
Happy to help refine further.
// Re-bind any click or DOM events from your template
});
give me this code
—
### ✅ Basic JS Template (rebind your logic after AJAX)
jQuery(document.body).on('updated_checkout', function () {
// ✅ Example: Show a console log (test if it's firing)
console.log('Checkout was updated via AJAX.');
// ✅ Example: Re-bind a click event
jQuery('.my-toggle-button').off('click').on('click', function () {
jQuery('.my-hidden-box').slideToggle();
});
// ✅ Example: Re-insert missing custom content if needed
if (jQuery('.pfc-pp-right-bar').length === 0) {
jQuery('.woocommerce-checkout-review-order-table tfoot').append(`
<tr class="pfc-pp-right-bar-row">
<td colspan="2">
<div class="pfc-pp-right-bar">
<!-- Your custom content here -->
💡 Partial payment discount will apply if selected.
</div>
</td>
</tr>
`);
}
});
—
### ✅ How to Use It
– Add this to your theme’s footer via `functions.php`, or enqueue it in a JS file loaded only on the checkout page.
#### Example (add to `functions.php`):
add_action('wp_footer', function () {
if (is_checkout() && !is_wc_endpoint_url()) {
?>
<script>
jQuery(document.body).on('updated_checkout', function () {
console.log('Checkout updated');
// Toggle logic or re-initializations
jQuery('.my-toggle-button').off('click').on('click', function () {
jQuery('.my-hidden-box').slideToggle();
});
// Optional: Inject content again if needed
if (jQuery('.pfc-pp-right-bar').length === 0) {
jQuery('.woocommerce-checkout-review-order-table tfoot').append(`
<tr class="pfc-pp-right-bar-row">
<td colspan="2">
<div class="pfc-pp-right-bar">
💡 Partial payment discount applies!
</div>
</td>
</tr>
`);
}
});
</script>
<?php
}
}, 100);
—
### 🧪 Test It
1. Go to your checkout page
2. Open browser console (F12 > Console tab)
3. Try changing shipping method or payment option
4. You should see `Checkout updated` in the console, and your JS logic will re-fire
—
Let me know if you want this to:
– Only trigger under certain conditions (e.g., payment method = X)
– Load content via AJAX
– Animate or add fields inside your discount box
Happy to help.
everyting rint in review-order.php excet $temalte is hide after ajax loasd
—
### ❌ Problem Summary
You are printing everything inside `review-order.php` — like:
<tr><td colspan=2>
...
include $template;
</td></tr>
On initial page load, `include $template` works fine.
But after an **AJAX update**, WooCommerce refreshes `#order_review` using `wc-ajax=update_order_review`, and **the file included via `$template` suddenly doesn't output**.
—
### ✅ Root Cause
The issue is **not that `$template` is missing**. It’s that during AJAX calls:
– **Global variables like** `$gamipress_wc_partial_payments_template_args` might be `null` or reset.
– Or the included file is being skipped silently because of an invalid path or missing context (especially in `GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR`).
—
### ✅ Real Solution (Bulletproof)
To make your included content **load reliably during AJAX refresh**, do this:
—
## ✅ Step-by-step Fix
### ✅ 1. **Hook into WooCommerce-rendered order review table**
This is **critical**. Use this exact hook:
add_action( 'woocommerce_review_order_after_order_total', 'pfc_add_discount_row_to_order_review', 20 );
### ✅ 2. **Define the function**
function pfc_add_discount_row_to_order_review() {
$opts = get_option( 'pfc_settings' );
if ( empty( $opts['pp_discount'] ) ) return;
global $gamipress_wc_partial_payments_template_args;
$template = trailingslashit( GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR ) . 'templates/wc-partial-payments-checkout.php';
echo '<tr class="pfc-pp-discount-row"><td colspan="2">';
echo '<div class="pfc-pp-right-bar">';
if ( file_exists( $template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
include $template;
} else {
echo '<!-- Template missing or args null -->';
}
echo '</div>';
echo '</td></tr>';
}
✅ Now this “ is **inside WooCommerce's AJAX-refreshed template**, and the `include` works as long as your global args are populated correctly.
—
### ⚠️ If the included template still does NOT render:
It's likely because:
– The global variable `gamipress_wc_partial_payments_template_args` is **null or reset** during AJAX.
– You must **initialize** that variable again during the AJAX call.
—
### ✅ 3. Ensure the variable is always set (even during AJAX)
Before calling the template, add this safety:
if ( is_ajax() && is_checkout() ) {
// Ensure the template args are available
if ( is_null( $gamipress_wc_partial_payments_template_args ) ) {
$gamipress_wc_partial_payments_template_args = apply_filters( 'gamipress_wc_partial_payments_template_args', [] );
}
}
—
### ✅ Optional Debug Tip
Inside your template (`wc-partial-payments-checkout.php`), add this temporarily:
echo '<pre>';
print_r($gamipress_wc_partial_payments_template_args);
echo '</pre>';
To verify it's available on both normal and AJAX load.
—
### ✅ Final Advice
– Output everything **via WooCommerce hooks** like `woocommerce_review_order_after_order_total`.
– Do not hard-code “ in `review-order.php`, as WooCommerce controls that markup.
—
Let me know if you want:
– This to only show for specific payment methods
– To convert the row to a message bar (outside table)
I can help you tailor it perfectly.
$opt = $this->pfc_get_settings();
if( ! isset( $opt['pp_discount'] ) ) {
return;
}
global $gamipress_wc_partial_payments_template_args;
$gamipress_pp_discount_template = GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR . 'templates/wc-partial-payments-checkout.php';
if( file_exists( $gamipress_pp_discount_template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
echo '';
include $gamipress_pp_discount_template;
echo '';
?>
<script>
( function( $ ) {
} )( jQuery );
</script>
> ✅ **Your content shows correctly when you output just a “ but disappears when you wrap it inside “ after AJAX refresh.**
—
### ✅ Why This Happens
WooCommerce's `review-order` table (where your hook runs) is a **structured “**, but not all parts of it are valid places to inject “.
The hook you're using:
add_action( 'woocommerce_review_order_after_cart_contents', [ $this, 'pfc_add_discount_option_checkout_rightbar' ] );
fires **inside**:
<table class="shop_table woocommerce-checkout-review-order-table">
<tbody>
<!-- cart contents rows -->
</tbody>
<tfoot>
<!-- totals -->
</tfoot>
</table>
When you output “ here, WooCommerce may:
– Try to append it **outside “ or “**
– Result in **invalid HTML structure**
– And during AJAX reload, jQuery `.html()` silently drops invalid/injected “ nodes
—
### ✅ The Fix: Output “ **only in valid hook positions**
Use one of these **hooks** if you need to output a “:
#### ✅ `woocommerce_review_order_before_order_total`
→ Before subtotal/total rows (safe “ position)
#### ✅ `woocommerce_review_order_after_order_total`
→ After total row (safe “ position)
—
### ✅ How to Fix It
add_action( 'woocommerce_review_order_after_order_total', [ $this, 'pfc_add_discount_option_checkout_rightbar' ] );
And **update your function** like this:
public function pfc_add_discount_option_checkout_rightbar() {
$opt = $this->pfc_get_settings();
if ( empty( $opt['pp_discount'] ) ) {
return;
}
global $gamipress_wc_partial_payments_template_args;
$template = trailingslashit( GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR ) . 'templates/wc-partial-payments-checkout.php';
if ( file_exists( $template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
echo '<tr class="pfc-pp-discount-row"><td colspan="2">';
echo '<div class="pfc-pp-right-bar">';
include $template;
echo '</div></td></tr>';
}
}
—
### ✅ Alternative: No “, just a styled div
If you **don't need table-row layout**, you can safely stay inside your current hook and just:
echo '<div class="pfc-pp-right-bar">';
include $template;
echo '</div>';
And style it like:
.pfc-pp-right-bar {
margin-top: 15px;
padding: 10px;
background: #f1f1f1;
border-left: 3px solid #007cba;
}
—
### ✅ Summary
| Situation | Fix |
|———-|—–|
| Content disappears after AJAX | Because “ is placed outside valid “/“ |
| Hook you’re using (`after_cart_contents`) | Not suitable for “ elements |
| ✅ Use `woocommerce_review_order_after_order_total` instead | Safe “ output |
| Or stay with “ if not using rows | Perfectly valid |
—
Let me know if you'd like to:
– Only show the discount box under specific conditions
– Dynamically update its content via AJAX
– Move it outside the table completely
I'm happy to help further!
—
## 🧠 Root Cause
WooCommerce replaces `#order_review` using **AJAX** and jQuery’s `.html()` method.
When you insert **raw “ tags** **not directly inside a “ or “**, jQuery silently **drops** those tags because they’re invalid in isolation.
Even if you hook into `woocommerce_review_order_after_order_total`, WooCommerce **may render it outside of “**, depending on the theme (like Flatsome).
—
## ✅ Solution 1: **Output Only “ Instead of “**
The most reliable way (and what WooCommerce officially does for notices and extra content) is to output a “ **outside the table**, styled like a row:
### ✅ Hook:
add_action( 'woocommerce_review_order_after_order_table', [ $this, 'pfc_add_discount_option_checkout_rightbar' ] );
### ✅ Function:
public function pfc_add_discount_option_checkout_rightbar() {
$opt = $this->pfc_get_settings();
if ( empty( $opt['pp_discount'] ) ) {
return;
}
global $gamipress_wc_partial_payments_template_args;
$template = trailingslashit( GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR ) . 'templates/wc-partial-payments-checkout.php';
if ( file_exists( $template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
echo '<div class="pfc-pp-right-bar" style="margin-top: 15px; padding: 10px; background: #f1f1f1; border-left: 4px solid #0073aa;">';
include $template;
echo '</div>';
}
}
> ✅ This **will not disappear** on AJAX because it's inserted in a stable DOM position and uses valid structure.
—
## ❌ Avoid “ Unless You Control “ Output
If you **must use “**, you'd need to:
– **Override the WooCommerce template** that renders the order summary table
– Insert your row **inside “** manually in your `form-checkout.php` or `review-order.php` override
That’s complex and fragile.
—
## ✅ Final Recommendation
Use:
add_action( 'woocommerce_review_order_after_order_table', ... );
And output a “, **not** a “.
It’s what WooCommerce extensions like Klarna, PayPal, Stripe, and plugins like "Checkout Field Editor" also do.
—
## Let Me Know
Would you like help styling the “ like a table row? Or inserting it next to totals?
Happy to assist!
this cause problem when ajax refessh in tr td
> ✅ **`global $gamipress_wc_partial_payments_template_args` is `null` or missing during WooCommerce AJAX refresh** — which makes your “ content **disappear** when using it inside WooCommerce’s `#order_review`.
—
## 🧠 Why This Happens
WooCommerce re-renders `#order_review` via AJAX (`?wc-ajax=update_order_review`).
When that happens:
– **Globals are not preserved**
– If your plugin or theme **sets `global $gamipress_wc_partial_payments_template_args` during initial page load only**, it will be **undefined or null during AJAX**
Thus, your condition:
if ( file_exists( $template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) )
fails silently during AJAX and **skips output** inside the “ — resulting in **no content = invalid “ = removed by jQuery**.
—
## ✅ Solution: Always Re-initialize That Global During AJAX
Update your method to re-populate the global variable if it's `null` during AJAX:
—
### ✅ Updated Method
public function pfc_add_discount_option_checkout_rightbar() {
$opt = $this->pfc_get_settings();
if ( empty( $opt['pp_discount'] ) ) {
return;
}
global $gamipress_wc_partial_payments_template_args;
// ✅ Fix: Re-initialize during AJAX checkout updates
if ( is_ajax() && doing_action( 'woocommerce_checkout_update_order_review' ) ) {
$gamipress_wc_partial_payments_template_args = apply_filters( 'gamipress_wc_partial_payments_template_args', [] );
}
$template = trailingslashit( GAMIPRESS_WC_PARTIAL_PAYMENTS_DIR ) . 'templates/wc-partial-payments-checkout.php';
if ( file_exists( $template ) && ! is_null( $gamipress_wc_partial_payments_template_args ) ) {
echo '<tr class="pfc-pp-discount-row"><td colspan="2">';
echo '<div class="pfc-pp-right-bar">';
include $template;
echo '</div></td></tr>';
}
}
—
### ✅ Explanation of Fix
– `is_ajax()` ensures you're inside an AJAX call
– `doing_action( 'woocommerce_checkout_update_order_review' )` ensures it's **WooCommerce's checkout refresh**
– `apply_filters( 'gamipress_wc_partial_payments_template_args', [] )` is how GamiPress likely populates that global originally
If GamiPress provides a helper function for setting the template args (e.g., `gamipress_wc_partial_payments_setup_template_args()`), you can call that instead.
—
### ✅ Hook to Use
You can now safely continue using:
add_action( 'woocommerce_review_order_after_order_total', [ $this, 'pfc_add_discount_option_checkout_rightbar' ] );
—
## ✅ Summary
| Problem | Fix |
|——–|—–|
| `$gamipress_wc_partial_payments_template_args` is `null` on AJAX | Re-populate it manually using `apply_filters()` or GamiPress helper |
| “ gets removed | Happens when included content is empty or invalid during AJAX |
| Fixed output | Always ensure the template and globals are set in both page load and AJAX |
—
Let me know if you’d like me to trace where the template args are normally set in GamiPress, or create a fallback HTML block if the global is null.
*
—
## ✅ 1. Generate the Math Question
We'll generate the question and store the **correct answer** in the session for later validation.
—
## ✅ 2. Add the Field Before the “Place Order” Button
### ✅ Add this in your theme's `functions.php`:
add_action('woocommerce_review_order_before_submit', 'custom_add_human_check_field', 10);
function custom_add_human_check_field() {
// Generate question
$rand1 = rand(2, 12);
$rand2 = rand(2, 12);
$question = "{$rand1} x {$rand2}";
$correctAnswer = $rand1 * $rand2;
// Store correct answer in session
WC()->session->set('human_check_answer', $correctAnswer);
// Output field
echo '<p class="woocommerce-form-row woocommerce-form-row--wide form-row form-row-wide">';
echo '<label for="human_check">';
echo esc_html__("Are you a human? Please, solve {$question}.", 'woocommerce');
echo ' <span class="required">*</span></label>';
echo '<input type="number" class="woocommerce-Input woocommerce-Input--text input-text" name="human_check" placeholder="Answer here" required />';
echo '</p>';
}
—
## ✅ 3. Validate the Field on Checkout Submission
add_action('woocommerce_checkout_process', 'custom_validate_human_check');
function custom_validate_human_check() {
$correct = WC()->session->get('human_check_answer');
$userAnswer = isset($_POST['human_check']) ? intval($_POST['human_check']) : null;
if ($userAnswer === null || $userAnswer !== intval($correct)) {
wc_add_notice(__('Incorrect answer to the human check question. Please try again.', 'woocommerce'), 'error');
}
}
—
## ✅ 4. Clear the Stored Answer on Success (Optional)
add_action('woocommerce_checkout_order_processed', function() {
WC()->session->__unset('human_check_answer');
}, 10, 1);
—
## ✅ Result
– A math challenge like `Are you a human? Please, solve 7 x 8.` will appear **before the "Place Order" button**.
– If incorrect, checkout will not proceed.
—
Let me know if you want:
– To randomize operators (like `+`, `-`, `x`)
– Style it with inline error messages
– Store the answer securely or log failed attempts
Happy to help!
woocommerce_checkout_before_terms_and_conditions
This hook renders content **just before** the Terms and Conditions section, making it ideal for your use case.
—
### ✅ Full Implementation
Add this code to your theme’s `functions.php` file:
—
### ✅ 1. Display the Human Check Field (above terms)
add_action('woocommerce_checkout_before_terms_and_conditions', 'custom_add_human_check_field_before_terms');
function custom_add_human_check_field_before_terms() {
$rand1 = rand(2, 12);
$rand2 = rand(2, 12);
$question = "{$rand1} x {$rand2}";
$correctAnswer = $rand1 * $rand2;
// Save correct answer in WooCommerce session
WC()->session->set('human_check_answer', $correctAnswer);
echo '<p class="woocommerce-form-row woocommerce-form-row--wide form-row form-row-wide">';
echo '<label for="human_check">';
echo esc_html__("Are you a human? Please, solve {$question}.", 'woocommerce');
echo ' <span class="required">*</span></label>';
echo '<input type="number" class="woocommerce-Input woocommerce-Input--text input-text" name="human_check" placeholder="Answer here" required />';
echo '</p>';
}
—
### ✅ 2. Validate User's Answer During Checkout Submission
add_action('woocommerce_checkout_process', 'custom_validate_human_check');
function custom_validate_human_check() {
$correct = WC()->session->get('human_check_answer');
$userAnswer = isset($_POST['human_check']) ? intval($_POST['human_check']) : null;
if ($userAnswer === null || $userAnswer !== intval($correct)) {
wc_add_notice(__('Incorrect answer to the human check question. Please try again.', 'woocommerce'), 'error');
}
}
—
### ✅ 3. Optional: Clear the Answer After Order is Placed
add_action('woocommerce_checkout_order_processed', function() {
WC()->session->__unset('human_check_answer');
}, 10, 1);
—
### ✅ Final Result
– The math question appears **above the Terms and Conditions**
– Prevents bots from submitting checkout
– Validated securely via `woocommerce_checkout_process`
—
Let me know if you'd like to:
– Add support for `+`, `-`, or random operators
– Show this only for guest users (not logged-in customers)
– Save the challenge and result as order meta
Happy to help!
—
## ✅ Final Result
– Place order button is **disabled by default**
– As soon as the user enters the **correct answer**, it gets **enabled**
– Keeps the server-side PHP validation too (as a fallback)
—
## ✅ Step-by-Step Implementation
### ✅ 1. Modify your **Human Check field** output with `data-answer`
Update the display function:
add_action('woocommerce_checkout_before_terms_and_conditions', 'custom_add_human_check_field_before_terms');
function custom_add_human_check_field_before_terms() {
$rand1 = rand(2, 12);
$rand2 = rand(2, 12);
$question = "{$rand1} x {$rand2}";
$correctAnswer = $rand1 * $rand2;
WC()->session->set('human_check_answer', $correctAnswer);
echo '<p class="woocommerce-form-row woocommerce-form-row--wide form-row form-row-wide">';
echo '<label for="human_check">';
echo esc_html__("Are you a human? Please, solve {$question}.", 'woocommerce');
echo ' <span class="required">*</span></label>';
echo '<input type="number" class="woocommerce-Input input-text human_check" name="human_check" ';
echo 'placeholder="Answer here" required data-answer="' . esc_attr($correctAnswer) . '" />';
echo '</p>';
}
—
### ✅ 2. Enqueue JavaScript to Disable/Enable Place Order Button
Add this to `functions.php` or enqueue a proper JS file:
add_action('wp_footer', function () {
if (is_checkout() && !is_wc_endpoint_url()) {
?>
<script>
jQuery(function($) {
function validateHumanCheck() {
const input = $('input.human_check');
const answer = parseInt(input.data('answer'), 10);
const entered = parseInt(input.val(), 10);
const button = $('#place_order');
if (entered === answer) {
button.prop('disabled', false);
input.removeClass('input-error');
} else {
button.prop('disabled', true);
input.addClass('input-error');
}
}
// Run on input change
$(document.body).on('keyup change', 'input.human_check', validateHumanCheck);
// Disable initially on load
$(document.body).on('updated_checkout', function () {
validateHumanCheck();
});
// Run on first load
validateHumanCheck();
});
</script>
<style>
input.input-error {
border: 2px solid red;
}
</style>
<?php
}
});
—
### ✅ 3. Server-side Validation (as fallback — already added):
add_action('woocommerce_checkout_process', 'custom_validate_human_check');
function custom_validate_human_check() {
$correct = WC()->session->get('human_check_answer');
$userAnswer = isset($_POST['human_check']) ? intval($_POST['human_check']) : null;
if ($userAnswer === null || $userAnswer !== intval($correct)) {
wc_add_notice(__('Incorrect answer to the human check question. Please try again.', 'woocommerce'), 'error');
}
}
—
### ✅ 4. (Optional) Clear answer after order is placed:
add_action('woocommerce_checkout_order_processed', function() {
WC()->session->__unset('human_check_answer');
}, 10, 1);
—
## ✅ Summary
| Feature | Included |
|——–|———-|
| Math question above Terms | ✅ |
| Disable “Place Order” by default | ✅ |
| Enable only on correct answer | ✅ |
| JS + server validation | ✅ |
| Accessible + styled error border | ✅ |
—
Let me know if you'd like to:
– Make it work for touch-only users
– Add fade-in or animation when correct
– Log failed attempts
Happy to enhance it further!
—
### ✅ Plain JavaScript
document.querySelector('.mfp-close')?.click();
—
### ✅ jQuery
jQuery('.mfp-close').trigger('click');
> ✅ This works only if the `.mfp-close` element is present in the DOM. If the popup is not yet open, the button won't exist.
—
### ✅ If You Want to Wait Until It's Available (Dynamic Popups)
Sometimes `.mfp-close` appears **after AJAX or user interaction**. In that case, you can **wait** or **watch**:
const interval = setInterval(function () {
const closeBtn = document.querySelector('.mfp-close');
if (closeBtn) {
closeBtn.click();
clearInterval(interval);
}
}, 100); // check every 100ms
—
Let me know if you want to:
– Close the popup after a delay
– Automatically close it after form submission
– Or open another modal after closing this one
Happy to help!
I want open it after mfp-close
—
### ✅ Option 1: Use Magnific Popup’s `close` event (recommended)
jQuery(document).on('mfpClose', function () {
// Open your mobile mega menu here
jQuery('#mobile-mega-menu').slideDown(); // or .addClass('open') etc.
});
This is the cleanest way because it's triggered **after the popup closes**, even if the user closes it via:
– `.mfp-close`
– clicking outside
– ESC key
—
### ✅ Option 2: Chain logic after clicking `.mfp-close` manually
If you are triggering `.mfp-close` programmatically and want to wait for it to close before opening the menu:
jQuery('.mfp-close').trigger('click');
setTimeout(function () {
jQuery('#mobile-mega-menu').slideDown(); // or .show(), .addClass('open'), etc.
}, 500); // delay must match your popup close animation
> ⚠️ Use this only if you're **forcing the popup to close** via script. Otherwise, `mfpClose` event is better.
—
### ✅ Option 3: Fully Control Both (close + open)
jQuery(document).on('click', '.mfp-close', function () {
setTimeout(function () {
jQuery('#mobile-mega-menu').slideDown();
}, 500); // match Magnific Popup’s fadeOut duration
});
—
### ✅ Example: Toggle Mega Menu via class
jQuery('#mobile-mega-menu').addClass('open'); // or .fadeIn()
And CSS:
#mobile-mega-menu {
display: none;
}
#mobile-mega-menu.open {
display: block;
}
—
### ✅ Best Practice Summary
| Action | Method |
|——–|——–|
| After any close | `mfpClose` event |
| Manual close → open next | `setTimeout()` |
| Style-based open | `.addClass('open')` or `.slideDown()` |
—
Let me know how you're opening the mega menu (class, animation, etc.), and I’ll tailor the exact code for your setup.
—
## ✅ 1. HTML Structure
Make sure you have:
<div id="mobile-mega-menu" class="mega-overlay">
<div class="mega-overlay-content">
<button id="close-mega-menu">✕</button>
<!-- Your menu content here -->
<p>Mobile Mega Menu Content</p>
</div>
</div>
—
## ✅ 2. CSS for Fullscreen Overlay
Add this to your theme’s CSS or inside “ tags:
.mega-overlay {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 9999;
color: #fff;
overflow-y: auto;
}
.mega-overlay-content {
padding: 40px;
max-width: 600px;
margin: 100px auto;
text-align: center;
}
#close-mega-menu {
position: absolute;
top: 20px;
right: 30px;
background: none;
border: none;
font-size: 32px;
color: #fff;
cursor: pointer;
}
—
## ✅ 3. JavaScript: Open After `.mfp-close`
Place this in your footer or enqueue it properly:
jQuery(document).on('mfpClose', function () {
jQuery('#mobile-mega-menu').fadeIn();
});
// Also allow closing the overlay
jQuery(document).on('click', '#close-mega-menu', function () {
jQuery('#mobile-mega-menu').fadeOut();
});
—
## ✅ Optional: Close by clicking outside content
Add this to the JS if you want:
jQuery(document).on('click', '#mobile-mega-menu', function (e) {
if (jQuery(e.target).is('#mobile-mega-menu')) {
jQuery('#mobile-mega-menu').fadeOut();
}
});
—
## ✅ Summary
| Feature | Implemented |
|——–|————-|
| Fullscreen dark overlay | ✅ |
| Trigger after Magnific Popup closes | ✅ via `mfpClose` |
| Close with ✕ or outside click | ✅ |
—
Let me know if you want:
– A slide-in mega menu from the side instead
– Mobile-only behavior
– Scroll lock while menu is open
Happy to adjust!
Search
—
## ✅ 1. Wrap the RC Types List in a Toggle Container
Update your HTML to include a **toggle button** and wrap the list in a container to hide/show:
<h3 style="cursor: pointer;" id="toggle-rc-types">
<?php echo __('RC Type', 'flatsome-child'); ?> ▼
</h3>
<div id="rc-types-wrapper" style="display: none;">
<?php
$rcTypes = get_terms([
'taxonomy' => 'pa_rc-type',
'hide_empty' => true,
]);
if ($rcTypes && !is_wp_error($rcTypes)) {
echo '<ul class="menu-items">';
foreach ($rcTypes as $rcType) {
$term_id = esc_attr($rcType->term_id);
$term_slug = esc_attr($rcType->slug);
$term_name = esc_html($rcType->name);
$shop_url = get_permalink(wc_get_page_id('shop'));
echo "<li><div data-cat-id='{$term_id}'><a href='{$shop_url}?filter_rc-type={$term_slug}'>{$term_name}</a></div></li>";
}
echo '</ul>';
} else {
echo '<p>No types found.</p>';
}
?>
</div>
—
## ✅ 2. Add JavaScript to Toggle
Add this in your footer or enqueue it in your theme:
jQuery(document).ready(function($) {
$('#toggle-rc-types').on('click', function() {
$('#rc-types-wrapper').slideToggle();
// Optional: change arrow icon ▼ / ▲
var current = $(this).text().trim();
if (current.endsWith('▼')) {
$(this).html('<?php echo __("RC Type", "flatsome-child"); ?> ▲');
} else {
$(this).html('<?php echo __("RC Type", "flatsome-child"); ?> ▼');
}
});
});
> If you’re loading this from within PHP, you can wrap the `js` part inside “ tags in `wp_footer`.
—
## ✅ 3. Optional: CSS Styling (Optional But Helpful)
#toggle-rc-types {
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-bottom: 10px;
}
#rc-types-wrapper ul.menu-items {
padding-left: 20px;
list-style: disc;
}
—
## ✅ Final Result
– **RC Types list is hidden** by default
– Clicking “RC Type ▼” reveals it with slide animation
– Arrow toggles between ▼ and ▲
—
Let me know if you'd like:
– Multiple sections toggled independently
– “Close all” or “Show all” buttons
– Toggle icons instead of text
Happy to help enhance it!
—
## ✅ Final Output Features:
– “RC Type ▼” when **collapsed**
– “RC Type ▲” when **expanded**
– Smooth slide animation
– Clean toggle behavior
—
## ✅ 1. HTML & PHP Markup (Update Your Section)
Replace your current heading + list section with this:
<h3 id="toggle-rc-types" style="cursor: pointer;">
<?php echo __('RC Type', 'flatsome-child'); ?>
<span id="rc-arrow">▼</span>
</h3>
<div id="rc-types-wrapper" style="display: none;">
<?php
$rcTypes = get_terms([
'taxonomy' => 'pa_rc-type',
'hide_empty' => true,
]);
if ($rcTypes && !is_wp_error($rcTypes)) {
echo '<ul class="menu-items">';
foreach ($rcTypes as $rcType) {
$term_id = esc_attr($rcType->term_id);
$term_slug = esc_attr($rcType->slug);
$term_name = esc_html($rcType->name);
$shop_url = get_permalink(wc_get_page_id('shop'));
echo "<li><div data-cat-id='{$term_id}'><a href='{$shop_url}?filter_rc-type={$term_slug}'>{$term_name}</a></div></li>";
}
echo '</ul>';
} else {
echo '<p>No types found.</p>';
}
?>
</div>
—
## ✅ 2. JavaScript (Toggle + Arrow Direction)
Add this to your footer (`wp_footer`) or enqueue via a JS file:
jQuery(document).ready(function($) {
$('#toggle-rc-types').on('click', function() {
const wrapper = $('#rc-types-wrapper');
const arrow = $('#rc-arrow');
wrapper.slideToggle(300, function() {
if (wrapper.is(':visible')) {
arrow.text('▲');
} else {
arrow.text('▼');
}
});
});
});
—
## ✅ 3. Optional Styling (Improve Clickability)
Add this to your `style.css`:
#toggle-rc-types {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
#rc-arrow {
font-size: 18px;
margin-left: 8px;
}
—
## ✅ Result:
– 🔽 “RC Type ▼” → opens the section
– 🔼 “RC Type ▲” → hides it again
– Smooth experience on both desktop and mobile
—
Let me know if you'd like:
– Font Awesome icons instead of plain text arrows
– RC Type to open by default on desktop but collapsed on mobile
– Animate the icon rotation instead of swapping characters
Happy to enhance further!
– ✅ Parent categories are listed
– ✅ Subcategories are shown **only when you click** on a parent
– ✅ The **first parent category is expanded by default**
– ✅ Toggle behavior works (open one, close others if needed)
Here’s a complete and clean solution:
—
## ✅ 1. Final Markup with Subcategory Inclusion
Replace your current PHP block with this:
<ul class="product-menu">
<?php
$parent_cats = get_terms([
'taxonomy' => 'product_cat',
'parent' => 0,
'hide_empty' => true,
'orderby' => 'name',
'order' => 'ASC',
]);
$firstCatId = 0;
$loopi = 0;
foreach ($parent_cats as $cat) :
$term_link = esc_url(get_term_link($cat));
$subcategories = get_terms([
'taxonomy' => 'product_cat',
'parent' => $cat->term_id,
'hide_empty' => true,
]);
$has_subcats = !empty($subcategories);
$active = $loopi === 0 ? 'active-cat' : '';
echo "<li class='parent-cat {$active}'>";
echo "<div class='category-item' data-cat-id='{$cat->term_id}'>";
echo "<span class='toggle-subcat'>" . ($has_subcats ? '▶' : '') . "</span> ";
echo "<a href='{$term_link}'>{$cat->name}</a>";
echo "</div>";
// Subcategories
if ($has_subcats) {
$sub_display = $loopi === 0 ? 'block' : 'none';
echo "<ul class='subcategories' style='display: {$sub_display};'>";
foreach ($subcategories as $subcat) {
$sub_link = esc_url(get_term_link($subcat));
echo "<li><a href='{$sub_link}'>{$subcat->name}</a></li>";
}
echo "</ul>";
}
echo "</li>";
$loopi++;
endforeach;
?>
</ul>
—
## ✅ 2. JavaScript for Toggle Behavior
Add this JS (inline via `wp_footer` or enqueue a file):
jQuery(document).ready(function ($) {
$('.category-item').on('click', function (e) {
const $parentLi = $(this).closest('li');
const $subcatList = $parentLi.find('ul.subcategories');
// Optional: Collapse other open categories
$('.subcategories').not($subcatList).slideUp();
$('.parent-cat').not($parentLi).removeClass('active-cat');
// Toggle current one
$subcatList.slideToggle();
$parentLi.toggleClass('active-cat');
e.preventDefault(); // prevent link click if needed
});
});
> ✅ If you want to allow clicking the **category name link**, remove `e.preventDefault()`.
—
## ✅ 3. Optional CSS Styling
.product-menu {
list-style: none;
padding-left: 0;
}
.product-menu > li {
margin-bottom: 10px;
}
.category-item {
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
}
.subcategories {
margin-top: 5px;
padding-left: 20px;
list-style: disc;
}
.toggle-subcat {
font-size: 14px;
color: #666;
}
.active-cat > .category-item .toggle-subcat {
transform: rotate(90deg); /* optional arrow animation */
}
—
## ✅ Final Result
| Feature | Behavior |
|——–|———-|
| Parent category click | Expands subcategories |
| First category open by default | ✅ |
| Only one open at a time (optional) | ✅ |
| Expand/Collapse with animation | ✅ |
—
Let me know if you'd like:
– Accordion behavior (only one open at a time)
– Font Awesome icons instead of arrows
– Highlight active links
Happy to refine further!
$sub_link = esc_url(get_term_link($subcat));
echo "{$subcat->name}";
}
now add top 3 prudct from each sub cat and display it when click sub cate and by defalt firs tsub cat will open
– ✅ Show **top 3 products** under each **sub-category**.
– ✅ Only display them **when the sub-category is clicked**.
– ✅ For the first parent + first sub-category, **expand it by default**.
—
## ✅ Here’s the Full Working Code (PHP + JS)
### 📍 PHP: Update your subcategory loop inside each parent category
Replace this part:
foreach ($subcategories as $subcat) {
$sub_link = esc_url(get_term_link($subcat));
echo "<li><a href='{$sub_link}'>{$subcat->name}</a></li>";
}
with this updated version:
$subLoopIndex = 0;
foreach ($subcategories as $subcat) {
$sub_link = esc_url(get_term_link($subcat));
$product_args = [
'post_type' => 'product',
'posts_per_page' => 3,
'tax_query' => [
[
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $subcat->term_id,
],
],
];
$products = new WP_Query($product_args);
$open_class = $subLoopIndex === 0 && $loopi === 0 ? 'open-subcat' : '';
echo "<li class='subcat-item {$open_class}'>";
echo "<a href='{$sub_link}' class='subcat-toggle'>{$subcat->name}</a>";
// Product container (hidden unless active)
$style = ($open_class) ? 'display:block;' : 'display:none;';
echo "<ul class='subcat-products' style='{$style}'>";
if ($products->have_posts()) {
while ($products->have_posts()) {
$products->the_post();
global $product;
echo "<li><a href='" . get_the_permalink() . "'>" . get_the_title() . "</a></li>";
}
} else {
echo "<li>No products found</li>";
}
echo "</ul>";
echo "</li>";
wp_reset_postdata();
$subLoopIndex++;
}
—
### ✅ JavaScript: Handle subcategory toggle and product reveal
Add this in `wp_footer` or enqueue properly:
jQuery(document).ready(function ($) {
// Toggle parent categories
$('.category-item').on('click', function (e) {
const $parentLi = $(this).closest('li');
const $subcatList = $parentLi.find('ul.subcategories');
$('.subcategories').not($subcatList).slideUp();
$('.parent-cat').not($parentLi).removeClass('active-cat');
$subcatList.slideToggle();
$parentLi.toggleClass('active-cat');
e.preventDefault();
});
// Toggle subcategories and product list
$('.subcat-toggle').on('click', function (e) {
e.preventDefault();
const $subItem = $(this).closest('.subcat-item');
const $productList = $subItem.find('.subcat-products');
$('.subcat-products').not($productList).slideUp();
$('.subcat-item').not($subItem).removeClass('open-subcat');
$productList.slideToggle();
$subItem.toggleClass('open-subcat');
});
});
—
### ✅ CSS (Optional for clarity)
.subcat-products {
padding-left: 20px;
margin-top: 5px;
}
.subcat-item > a {
display: block;
font-weight: bold;
margin: 5px 0;
}
—
## ✅ Result
– Parent category shows subcategories on click
– Subcategories expand on click and show top 3 products
– First subcategory of first parent is expanded by default
—
Let me know if you'd like:
– Product thumbnails/prices shown too
– Products displayed in a grid
– “View All Products” button below the 3 items
Happy to enhance it further!
'limit' => 3,
'status' => 'publish',
'category' => [$subcat->term_id],
]);
print_r($products);
this return blank even their are produts
—
## ❌ Problem
'category' => [$subcat->term_id]
This is incorrect. The `category` parameter in `wc_get_products()` expects **a slug** (or an array of slugs), **not a term ID**.
—
## ✅ Fix
Update this line:
'category' => [$subcat->term_id],
To use the **slug**:
'category' => [$subcat->slug],
—
## ✅ Correct Full Example
$products = wc_get_products([
'limit' => 3,
'status' => 'publish',
'category' => [ $subcat->slug ],
]);
Now `print_r($products);` should return 3 `WC_Product` objects — or fewer if less available.
—
## ✅ Optional: Debug
If still blank, try adding:
echo 'Looking for category slug: ' . $subcat->slug . '<br>';
Or run:
print_r( wc_get_products([
'limit' => 3,
'status' => 'publish',
'category' => [ $subcat->slug ],
'return' => 'ids' // just to confirm matching product IDs
]) );
—
Let me know if you also want to display product title, thumbnail, price, or add to cart links. Happy to help!