Jump to content

Edit History

Hola, pues tengo el carrito de prestashop, lo estuve modificando un poco, pero necesito que tenga una respuesta inmediata.

Suma, resta, muestra cantidad, elimina producto; pero eso, lo hace con un pequeño delay cuando uno apreta varias veces el botón de "+" o "-".

¿Alguna idea?

Ya minimizé gran parte del código .js, .css, y .tpl

PRESTASHOP 8.2.0

Html:

<button type="button" class="close-cart-btn" onclick="window.cartClosedManually=true;document.querySelectorAll('.tvcart-product-list').forEach(el=>el.style.display='none');" style="font-size:xx-large;position:absolute;width:30px;height:30px;top:-35px;right:auto;align-items:center;align-content:center;align-self:center;cursor:pointer;display:flex;z-index:9999;justify-content:center;" aria-label="Cerrar carrito" title="Cerrar">&times;</button>
<a href="{$product.url}"><div class="tvcart-product-list-img"><img class='tvimage-lazy' src="{$product.cover.bySize.cart_default.url}"></div><div class="tvcart-product-content"><div class="tvcart-product-list-quentity"><div class="tvcart-dropdown-title"><span class="product-quentity">{$product.quantity}</span><span class="tvshopping-cart-quentity">&nbsp;x&nbsp;</span><span class="product-name">{$product.name}</span></div><div class="tvcart-product-remove">{$url='controller=cart&delete='|cat:$product.id_product}<a class="remove-from-cart tvcmsremove-from-cart" rel="nofollow" href="{$product.remove_from_cart_url}" data-link-action="delete-from-cart" data-id-product="{$product.id_product|escape:'javascript'}" data-id-product-attribute="{$product.id_product_attribute|escape:'javascript'}" data-id-customization="{$product.id_customization|escape:'javascript'}" title="{l s='remove from cart' d='Shop.Theme.Actions'}"><i class="material-icons">&#xe5cd;</i></a></div></div><div class="tvcart-product-list-price"><span class="product-price" data-unit-price="{$product.unit_price|floatval}" data-price-format="{$product.price}">{$product.total}</span><span class="product-price" style="color:#fff;font-size:xx-small;">&nbsp;{$product.unit_price_full}</span>{if isset($product.unit_price_full)&&!empty($product.unit_price_full)}{if isset($cart)&&isset($cart.products)&&count($cart.products)>0}{assign var="unit_text" value=$product.unit_price_full|regex_replace:'/^[^ ]* /':''}{else}<span class="product-price" style="color:#fff;font-size:xx-small;">{$product.unit_price_full}</span>{/if}{else}{hook h='displayProductPriceBlock' product=$product type="before_price"}<span class="product-price" style="color:#fff;font-size:xx-small;">PRECIO DESDE UNA UNID</span>{/if}{hook h='displayProductPriceBlock' product=$product type="unit_price"}{foreach from=$product.attributes item="property_value" key="property"}<span style="display:none;"><strong>{$property}</strong>: {$property_value}</span><br>{/foreach}<p style="color:#fff;font-weight:700;">{l s='Quantity:' d='Shop.Theme.Checkout'}&nbsp;{$product.cart_quantity}<br></p>{if isset($product.is_gift)&&$product.is_gift}<span class="gift-quantity product-cart-qty-{$product.id_product}-{$product.id_product_attribute}" style="display:none!important;">{$product.quantity}</span>{else}<div class="cart-line" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}"><div class="cart-line" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}" data-id-customization="{$product.id_customization}"><div class="quantity-wrapper"><button class="qty-btn down" aria-label="Restar cantidad"></button><input class="js-cart-line-product-quantity" type="text" value="{$product.quantity}" min="{$product.minimal_quantity}" readonly data-product-id="{$product.id_product}" data-product-attribute-id="{$product.id_product_attribute}" data-update-url="{$product.update_quantity_url}" data-up-url="{$product.up_quantity_url}" data-down-url="{$product.down_quantity_url}"><button class="qty-btn up" aria-label="Sumar cantidad">+</button></div></div></div>{/if}<a class="remove-from-cart" rel="nofollow" href="{$product.remove_from_cart_url}" data-link-action="delete-from-cart" data-id-product="{$product.id_product|escape:'javascript'}" data-id-product-attribute="{$product.id_product_attribute|escape:'javascript'}" data-id-customization="{$product.id_customization|escape:'javascript'}"></a>{block name='hook_cart_extra_product_actions'}{hook h='displayCartExtraProductActions' product=$product}{/block}<div class="tvcart-product-list-attribute">{foreach $product.attributes as $prod_attb=>$prod_val}<div class="tvcart-product-attr"><span>{$prod_attb}:</span> {$prod_val}</div>{/foreach}</div>{if $product.customizations|count}<div class="customizations"><ul>{foreach from=$product.customizations item='customization'}<li><span class="product-quantity">{$customization.quantity}</span><a href="{$customization.remove_from_cart_url}" title="{l s='remove from cart' d='Shop.Theme.Actions'}" class="remove-from-cart" rel="nofollow">{l s='Remove' d='Shop.Theme.Actions'}</a><ul>{foreach from=$customization.fields item='field'}<li><span>{$field.label}</span>{if $field.type=='text'}<span>{$field.text nofilter}</span>{else if $field.type=='image'}<img class='tvimage-lazy' src="{$field.image.small.url}">{/if}</li>{/foreach}</ul></li>{/foreach}</ul></div>{/if}</div></a>

Style:

span.input-group-btn-vertical {
    width: 12% !important;
    display: flex !important;
    flex-direction: column;
}
input.js-cart-line-product-quantity.form-control {
    width: 50px !important;
}
input.js-cart-line-product-quantity {
    width: 50px !important;
    height: 30px !important;
    margin: -5px;
}
.quantity-wrapper {
  display: flex;
  align-items: center;
  gap: 5px;
}
.qty-btn {
  width: 30px;
  height: 30px;
  font-size: 18px;
  cursor: pointer;
}
@keyframes bounceQty {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.2); }
  60%  { transform: scale(0.95); }
  100% { transform: scale(1); }
}
.qty-animated {
  animation: bounceQty 0.4s ease;
}

Script:

document.addEventListener("DOMContentLoaded", () => {
  console.log("🛒 Script de cantidad actualizado y funcionando");

  function animateQty(input) {
    input.classList.add("qty-animated");
    setTimeout(() => input.classList.remove("qty-animated"), 400);
  }

  // 🔒 Previene que se agregue el event listener más de una vez
  if (!window.qtyBtnHandlerAdded) {
    document.body.addEventListener("click", (e) => {
      const btn = e.target.closest(".qty-btn");
      if (!btn) return;

      const wrapper = btn.closest(".quantity-wrapper");
      const input = wrapper.querySelector(".js-cart-line-product-quantity");
      if (!input) return;

      const min = parseInt(input.getAttribute("min")) || 1;
      const downUrl = input.dataset.downUrl;
      const upUrl = input.dataset.upUrl;
      let currentQty = parseInt(input.value) || min;
      let newQty = currentQty;

      let targetUrl = "";

      if (btn.classList.contains("up")) {
        newQty = currentQty + 1;
        targetUrl = upUrl;
      } else if (btn.classList.contains("down")) {
        newQty = currentQty - 1;

        if (newQty < 1) {
          // Buscar y usar la URL de eliminar producto
          const removeBtn = wrapper.closest('.tvcart-product-content')?.querySelector('.remove-from-cart');
          if (removeBtn && removeBtn.href) {
            fetch(removeBtn.href, {
              method: "POST",
              credentials: "same-origin"
            })
            .then(res => res.json())
            .then(data => {
              prestashop.emit("updateCart", data);
              reloadCustomCart();
            })
            .catch(err => console.error(" Error al eliminar producto:", err));
          }
          return;
        }

        targetUrl = downUrl;
      }

      input.value = newQty;
      animateQty(input);

// Actualiza el total del producto (si existe)
const totalPriceEl = wrapper.closest('.tvcart-product-content')?.querySelector('.product-price[data-unit-price]');
if (totalPriceEl) {
  const unitPrice = parseFloat(totalPriceEl.dataset.unitPrice);
  const priceFormat = totalPriceEl.dataset.priceFormat;
  const total = (unitPrice * newQty).toFixed(0);
  const formattedTotal = priceFormat.replace('%s', total.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'));
  totalPriceEl.textContent = formattedTotal;
}

      fetch(targetUrl, {
        method: "POST",
        credentials: "same-origin"
      })
      .then(res => {
        const contentType = res.headers.get("Content-Type");
        if (contentType && contentType.includes("application/json")) {
          return res.json();
        } else {
          return res.text();
        }
      })
      .then(data => {
        if (data && typeof data === "object" && data.cart) {
          prestashop.emit("updateCart", data);
        } else {
          console.log("ℹ️ Cambio aplicado sin emitir updateCart (respuesta no JSON)");
        }

        reloadCustomCart();
      })
      .catch(err => console.error(" Error al actualizar cantidad:", err));
    });

    window.qtyBtnHandlerAdded = true; // Marcamos que ya fue agregado
  }
});

// 🔁 Recarga y actualiza el contenido del carrito
function reloadCustomCart() {
  const cartWrapper = document.querySelector('.tvcart-product-list');
  if (cartWrapper) {
    cartWrapper.style.display = 'block';
  }

  fetch(window.location.href, {
    method: "GET",
    headers: {
      "X-Requested-With": "XMLHttpRequest"
    }
  })
  .then(res => res.text())
  .then(html => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');

    // 🔁 Reemplazo del listado de productos del carrito
    const newCartList = doc.querySelector('.tvcart-product-list');
    const currentCartList = document.querySelector('.tvcart-product-list');
    if (newCartList && currentCartList) {
      currentCartList.innerHTML = newCartList.innerHTML;
    }

    // 🔁 Reemplazo del resumen del carrito
    const newSummary = doc.querySelector('.tvcart-summary');
    const currentSummary = document.querySelector('.tvcart-summary');
    if (newSummary && currentSummary) {
      currentSummary.innerHTML = newSummary.innerHTML;
    }

    // 🔁 Reemplazo del minicart
    const newMiniCart = doc.querySelector('.blockcart');
    const currentMiniCart = document.querySelector('.blockcart');
    if (newMiniCart && currentMiniCart) {
      currentMiniCart.innerHTML = newMiniCart.innerHTML;
    }

    // 🔁 Reemplazo de la cantidad de productos en el checkout
    const newSummaryProducts = doc.querySelector('.cart-summary-products');
    const currentSummaryProducts = document.querySelector('.cart-summary-products');
    if (newSummaryProducts && currentSummaryProducts) {
      currentSummaryProducts.innerHTML = newSummaryProducts.innerHTML;
    }

    console.log(" Carrito, totales, minicart y resumen de checkout actualizados visualmente.");
  })
  .catch(err => console.error(" Error al recargar el carrito:", err));
}

// 🔔 Siempre mantener el carrito visible al recibir updateCart
prestashop.on('updateCart', () => {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList && !window.cartClosedManually) {
    cartList.style.display = 'block';
  }
});

document.addEventListener('DOMContentLoaded', () => {
  const toggleBtn = document.querySelector('.tvshopping-cart-price');

  if (toggleBtn) {
    toggleBtn.addEventListener('click', () => {
      const cartList = document.querySelector('.tvcart-product-list');
      if (cartList) {
        cartList.style.display = 'block';
        window.cartClosedManually = false;
      }
    });
  }
});

// Bandera global para saber si el usuario cerró el carrito
window.cartClosedManually = false;

// Función para abrir el carrito
function openCart() {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList) {
    cartList.style.display = 'block';
  }
  window.cartClosedManually = false;
}

// Función para cerrar el carrito manualmente (con la X)
function closeCart() {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList) {
    cartList.style.display = 'none';
  }
  window.cartClosedManually = true;
}

// Reaplicar comportamiento luego de que el carrito se actualice con AJAX
prestashop.on('updateCart', () => {
  setTimeout(() => {
    const cartList = document.querySelector('.tvcart-product-list');
    const closeButton = document.querySelector('.close-cart-btn');

    // Solo mostrar el carrito si el usuario no lo cerró manualmente
    if (cartList && !window.cartClosedManually) {
      cartList.style.display = 'block';
    }

    // Reasignar el listener al botón de cerrar
    if (closeButton) {
      closeButton.removeEventListener('click', closeCart); // Evitar duplicados
      closeButton.addEventListener('click', closeCart);
    }
  }); // Espera para asegurar que el DOM esté actualizado
});

// Evento al cargar la página
document.addEventListener('DOMContentLoaded', () => {
  // Mostrar el carrito al hacer clic en el total del carrito
  const toggleBtn = document.querySelector('.tvshopping-cart-price');
  if (toggleBtn) {
    toggleBtn.addEventListener('click', () => {
      openCart();
    });
  }
});

// 🔄 Forzar visibilidad del carrito mientras se detectan cambios
(function forceCartVisibleDuringUpdates() {
  const cartList = document.querySelector('.tvcart-product-list');

  if (!cartList) return;

  // Marcar como "actualizando carrito"
  const setCartUpdating = (updating) => {
    cartList.setAttribute('data-updating', updating ? 'true' : 'false');

    if (updating && !window.cartClosedManually) {
      cartList.style.display = 'block';
    }
  };

  // Hook de PrestaShop para detectar eventos relacionados al carrito
  prestashop.on('updateCart', () => {
    setCartUpdating(true);

    // Esperamos un momento para "desmarcar" la actualización
    setTimeout(() => {
      setCartUpdating(false);

      if (!window.cartClosedManually) {
        cartList.style.display = 'block'; // Nos aseguramos de que siga visible
      }
    }); // puedes ajustar este delay según rendimiento del sitio
  });

  // Observa si hay cambios visibles en el carrito
  const observer = new MutationObserver(() => {
    if (!window.cartClosedManually) {
      cartList.style.display = 'block';
    }
  });

  observer.observe(cartList, { childList: true, subtree: true });
})();

SCRIPT DEL HEADER:

//CANTIDADES
  function updateCartQuantity(productId, productAttributeId, newQty) {
    const action = newQty === 0 ? 'delete' : 'update';
    const requestData = {
      ajax: true,
      action: action,
      id_product: productId,
      id_product_attribute: productAttributeId,
    };

    if (action === 'update') {
      requestData.qty = newQty;
    }

    $.post(prestashop.urls.pages.cart, requestData)
      .done(function (resp) {
        prestashop.emit('updateCart', {
          reason: {
            idProduct: productId,
            idProductAttribute: productAttributeId,
            quantity: newQty
          },
          resp: resp
        });
      });
  }

  function bindTouchSpinEvents() {
    $('.js-cart-line-product-quantity').off('change').on('change', function () {
      const $input = $(this);
      const newQty = parseInt($input.val());
      const $line = $input.closest('[data-id-product]');

      const idProduct = $line.data('id-product');
      const idProductAttribute = $line.data('id-product-attribute') || 0;

      if (!isNaN(newQty)) {
        updateCartQuantity(idProduct, idProductAttribute, newQty);
      }
    });
  }

  document.addEventListener('DOMContentLoaded', function () {
    bindTouchSpinEvents();
    prestashop.on('updateCart', function () {
      bindTouchSpinEvents();
    });
  });

//ELIMINAR PRODUCTO
document.addEventListener('DOMContentLoaded', function () {
  document.body.addEventListener('click', function (e) {
    const removeBtn = e.target.closest('.remove-from-cart');

    if (removeBtn && removeBtn.dataset.linkAction === 'delete-from-cart') {
      e.preventDefault(); // Evita la recarga

      const url = removeBtn.getAttribute('href');

      $.post(url, { ajax: true })
        .done(function (resp) {
          prestashop.emit('updateCart', {
            reason: { linkAction: 'delete-from-cart' },
            resp: resp
          });
        })
        .fail(function (err) {
          console.error('Error al eliminar del carrito:', err);
        });
    }
  });
});

//MOSTRAR BLOQUE CARRITO DE COMPRAS AL DARLE CLIC AL CONTADOR DE ITEMS
  document.addEventListener('DOMContentLoaded', function () {
    prestashop.on('updateCart', function () {
      const blockCart = document.querySelector('.blockcart');
      if (blockCart) {
        blockCart.classList.add('show-cart-panel');
      }
    });

  // Evita cerrar el carrito si se hace clic en un botón de cantidad o dentro del bloque del carrito
  const clickedQtyBtn = event.target.closest('.qty-btn, .quantity-wrapper, .js-cart-line-product-quantity');

  if (
    blockCart &&
    !blockCart.contains(event.target) &&
    !clickedQtyBtn && // 👈 Añadido para ignorar clicks sobre botones o inputs de cantidad
    blockCart.classList.contains('show-cart-panel')
  ) {
    blockCart.classList.remove('show-cart-panel');
  };

  });

//ALTERNAR MOSTRAR-OCULTAR BLOQUE DE CARRITO DE COMPRAS AL DARLE CLIC AL CONTADOR DE ITEMS
document.addEventListener('DOMContentLoaded', function () {
  document.body.addEventListener('click', function (e) {
    const cartTrigger = e.target.closest('.tvshopping-cart-containt-box, .tvshopping-cart-inner, .tvshopping-cart-price');

    if (cartTrigger) {
      const cartList = document.querySelector('.tvcart-product-list');
      if (cartList) {
        const currentDisplay = window.getComputedStyle(cartList).display;
        cartList.style.display = (currentDisplay === 'block') ? 'block' : 'block';
      }
    }
  });
});

//ELIMINA UN PRODUCTO DEL CARRITO SI ÉSTE BAJA DE CANTIDAD 1
document.addEventListener('DOMContentLoaded', function () {
  // 1. Si el usuario baja desde 1 con el botón de restar (↓)
  document.body.addEventListener('click', function (e) {
    const downBtn = e.target.closest('.bootstrap-touchspin-down, .touchspin-down');
    if (!downBtn) return;

    const input = downBtn.closest('.cart-line')?.querySelector('.js-cart-line-product-quantity');
    if (!input) return;

    const currentQty = parseInt(input.value);
    const updateUrl = input.dataset.updateUrl;

    if (currentQty === 1 && updateUrl) {
      e.preventDefault();

      $.post(updateUrl, { ajax: true, qty: 0 })
        .done(function (resp) {
          prestashop.emit('updateCart', {
            reason: {
              idProduct: input.dataset.productId,
              linkAction: 'delete-from-cart',
              quantity: 0
            },
            resp: resp
          });
        })
        .fail(function (err) {
          console.error('Error al eliminar con botón ↓:', err);
        });
    }
  });

  // 2. Si el usuario escribe 0 manualmente
  document.body.addEventListener('change', function (e) {
    const input = e.target.closest('.js-cart-line-product-quantity');
    if (!input) return;

    const newQty = parseInt(input.value);
    const updateUrl = input.dataset.updateUrl;

    if (newQty <= 0 && updateUrl) {
      $.post(updateUrl, { ajax: true, qty: 0 })
        .done(function (resp) {
          prestashop.emit('updateCart', {
            reason: {
              idProduct: input.dataset.productId,
              linkAction: 'delete-from-cart',
              quantity: 0
            },
            resp: resp
          });
        })
        .fail(function (err) {
          console.error('Error al eliminar al escribir 0:', err);
        });
    }
  });
});

//OCULTA LA OPCIÓN "CREAR CUENTA" SI EL USUARIO ESTÁ LOGUEADO
  document.addEventListener('DOMContentLoaded', function () {
    if (document.body.classList.contains('page-customer-logged-in')) {
      const registerLink = document.querySelector('.register-link');
      if (registerLink) {
        registerLink.style.display = 'none';
      }
    }
  });

  document.addEventListener('DOMContentLoaded', function () {
    var isLogged = document.body.classList.contains('page-customer-logged-in');
    if (isLogged) {
      var registerLink = document.querySelector('li.register-link');
      if (registerLink) {
        registerLink.style.display = 'none';
      }
    }
  });

  document.addEventListener('DOMContentLoaded', function () {
    var isLoggedIn = document.querySelector('.user-info a[href*="my-account"]');
    if (isLoggedIn) {
      var regLink = document.querySelector('.register-link');
      if (regLink) regLink.style.display = 'none';
    }
  });

  (function () {
    const CART_SELECTOR = '.tvcart-product-list';
    const TOGGLE_BTN_SEL = '.tvshopping-cart-containt-box, .tvshopping-cart-inner, .tvshopping-cart-price';
    const CLOSE_BTN_SEL = '.close-cart-btn';
    const STORAGE_KEY = 'alishop_cart_closed';

    // Devuelve true si estaba marcado como cerrado
    function isClosed() {
      return localStorage.getItem(STORAGE_KEY) === 'true';
    }

    function applyState() {
      const cart = document.querySelector(CART_SELECTOR);
      if (!cart) { console.warn('Cart selector no encontrado:', CART_SELECTOR); return; }
      if (isClosed()) {
        console.log('[Cart] Estado: CERRADO (hide)');
        cart.style.display = 'none';
      } else {
        console.log('[Cart] Estado: ABIERTO (show)');
        cart.style.display = 'block';
      }
    }

    function openCart() {
      console.log('[Cart] openCart()');
      localStorage.setItem(STORAGE_KEY, 'false');
      applyState();
    }

    function closeCart() {
      console.log('[Cart] closeCart()');
      localStorage.setItem(STORAGE_KEY, 'true');
      applyState();
    }

    // Inicializamos al cargar la página
    document.addEventListener('DOMContentLoaded', function () {
      console.log('[Cart] DOMContentLoaded → aplicando estado guardado');
      applyState();

      // Toggle al clic en el ícono de items
      document.body.addEventListener('click', function (e) {
        if (e.target.closest(TOGGLE_BTN_SEL)) {
          e.preventDefault();
          console.log('[Cart] Click en TOGGLE_BTN_SEL');
          openCart();
        }
      });

      // Cerrar al clic en la X
      document.body.addEventListener('click', function (e) {
        if (e.target.closest(CLOSE_BTN_SEL)) {
          e.preventDefault();
          console.log('[Cart] Click en CLOSE_BTN_SEL');
          closeCart();
        }
      });

      // Cada vez que PrestaShop emita updateCart tras AJAX
      if (window.prestashop) {
        prestashop.on('updateCart', function () {
          console.log('[Cart] evento updateCart recibido');
          applyState();
        });
      } else {
        console.warn('PrestaShop JS no encontrado');
      }
    });
  })();

function forceReflow() {
  document.body.style.display = 'none';
  // lectura forzada:
  void document.body.offsetHeight;
  document.body.style.display = '';
}

window.addEventListener('load', function () {
    document.querySelector('ul#top-menu').style.display = 'flex';
});

document.addEventListener('DOMContentLoaded', function () {
    const currentUrl = window.location.href.split('#')[0].split('?')[0];

    document.querySelectorAll('a.dropdown-item.d-flex').forEach(function (link) {
      const linkUrl = link.href.split('#')[0].split('?')[0];

      if (linkUrl === currentUrl) {
        link.classList.add('active');
      }
    });
  });

 

Hola, pues tengo el carrito de prestashop, lo estuve modificando un poco, pero necesito que tenga una respuesta inmediata.

Suma, resta, muestra cantidad, elimina producto; pero eso, lo hace con un pequeño delay cuando uno apreta varias veces el botón de "+" o "-".

¿Alguna idea?

Ya minimizé gran parte del código .js, .css, y .tpl

PRESTASHOP 8.2.0

Html:

<button type="button" class="close-cart-btn" onclick="window.cartClosedManually=true;document.querySelectorAll('.tvcart-product-list').forEach(el=>el.style.display='none');" aria-label="Cerrar carrito" title="Cerrar">&times;</button><a href="{$product.url}"><div class="tvcart-product-list-img"><img class='tvimage-lazy' src="{$product.cover.bySize.cart_default.url}"></div><div class="tvcart-product-content"><div class="tvcart-product-list-quentity"><div class="tvcart-dropdown-title"><span class="product-quentity">{$product.quantity}</span><span class="tvshopping-cart-quentity">&nbsp;x&nbsp;</span><span class="product-name">{$product.name}</span></div><div class="tvcart-product-remove">{$url='controller=cart&delete='|cat:$product.id_product}<a class="remove-from-cart tvcmsremove-from-cart" rel="nofollow" href="{$product.remove_from_cart_url}" data-link-action="delete-from-cart" data-id-product="{$product.id_product|escape:'javascript'}" data-id-product-attribute="{$product.id_product_attribute|escape:'javascript'}" data-id-customization="{$product.id_customization|escape:'javascript'}" title="{l s='remove from cart' d='Shop.Theme.Actions'}"><i class="material-icons">&#xe5cd;</i></a></div></div><div class="tvcart-product-list-price"><span class="product-price" data-unit-price="{$product.unit_price|floatval}" data-price-format="{$product.price}" data-total-price-el>{$product.total}</span><span class="product-price" style="color:#fff;font-size:xx-small;">&nbsp;{$product.unit_price_full}</span>{if isset($product.unit_price_full)&&!empty($product.unit_price_full)}{if isset($cart)&&isset($cart.products)&&count($cart.products)>0}{assign var="unit_text" value=$product.unit_price_full|regex_replace:'/^[^ ]* /':''}{else}<span class="product-price" style="color:#fff;font-size:xx-small;">{$product.unit_price_full}</span>{/if}{else}{hook h='displayProductPriceBlock' product=$product type="before_price"}<span class="product-price" style="color:#fff;font-size:xx-small;">PRECIO DESDE UNA UNID</span>{/if}{hook h='displayProductPriceBlock' product=$product type="unit_price"}{foreach from=$product.attributes item="property_value" key="property"}<span style="display:none;"><strong>{$property}</strong>: {$property_value}</span><br>{/foreach}<p style="color:#fff;font-weight:700;">{l s='Quantity:' d='Shop.Theme.Checkout'}&nbsp;{$product.cart_quantity}<br></p>{if isset($product.is_gift)&&$product.is_gift}<span class="gift-quantity product-cart-qty-{$product.id_product}-{$product.id_product_attribute}" style="display:none!important;">{$product.quantity}</span>{else}<div class="cart-line" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}"><div class="cart-line" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}" data-id-customization="{$product.id_customization}"><div class="quantity-wrapper"><button class="qty-btn down" aria-label="Restar cantidad"></button><input class="js-cart-line-product-quantity" type="text" value="{$product.quantity}" min="{$product.minimal_quantity}" readonly data-product-id="{$product.id_product}" data-product-attribute-id="{$product.id_product_attribute}" data-update-url="{$product.update_quantity_url}" data-up-url="{$product.up_quantity_url}" data-down-url="{$product.down_quantity_url}"><button class="qty-btn up" aria-label="Sumar cantidad">+</button></div></div></div>{/if}<a class="remove-from-cart" rel="nofollow" href="{$product.remove_from_cart_url}" data-link-action="delete-from-cart" data-id-product="{$product.id_product|escape:'javascript'}" data-id-product-attribute="{$product.id_product_attribute|escape:'javascript'}" data-id-customization="{$product.id_customization|escape:'javascript'}"></a>{block name='hook_cart_extra_product_actions'}{hook h='displayCartExtraProductActions' product=$product}{/block}<div class="tvcart-product-list-attribute">{foreach $product.attributes as $prod_attb=>$prod_val}<div class="tvcart-product-attr"><span>{$prod_attb}:</span> {$prod_val}</div>{/foreach}</div>{if $product.customizations|count}<div class="customizations"><ul>{foreach from=$product.customizations item='customization'}<li><span class="product-quantity">{$customization.quantity}</span><a href="{$customization.remove_from_cart_url}" title="{l s='remove from cart' d='Shop.Theme.Actions'}" class="remove-from-cart" rel="nofollow">{l s='Remove' d='Shop.Theme.Actions'}</a><ul>{foreach from=$customization.fields item='field'}<li><span>{$field.label}</span>{if $field.type=='text'}<span>{$field.text nofilter}</span>{else if $field.type=='image'}<img class='tvimage-lazy' src="{$field.image.small.url}">{/if}</li>{/foreach}</ul></li>{/foreach}</ul></div>{/if}</div></a>

Style:

.close-cart-btn {
  font-size: xx-large;
  position: absolute;
  width: 30px;
  height: 30px;
  top: -35px;
  right: auto;
  align-items: center;
  align-content: center;
  align-self: center;
  cursor: pointer;
  display: flex;
  z-index: 9999;
  justify-content: center;
}
span.input-group-btn-vertical {
    width: 12% !important;
    display: flex !important;
    flex-direction: column;
}
input.js-cart-line-product-quantity.form-control {
    width: 50px !important;
}
input.js-cart-line-product-quantity {
    width: 50px !important;
    height: 30px !important;
    margin: -5px;
}
.quantity-wrapper {
  display: flex;
  align-items: center;
  gap: 5px;
}
.qty-btn {
  width: 30px;
  height: 30px;
  font-size: 18px;
  cursor: pointer;
}
@keyframes bounceQty {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.2); }
  60%  { transform: scale(0.95); }
  100% { transform: scale(1); }
}
.qty-animated {
  animation: bounceQty 0.4s ease;
}

Script:

document.addEventListener('DOMContentLoaded', () => {
  console.log("🛒 Script de cantidad actualizado y funcionando");

  function animateQty(input) {
    input.classList.add("qty-animated");
    setTimeout(() => input.classList.remove("qty-animated"), 400);
  }

  const toggleBtn = document.querySelector('.tvshopping-cart-price');

  if (toggleBtn) {
    toggleBtn.addEventListener('click', () => {
    openCart();
      const cartList = document.querySelector('.tvcart-product-list');
      if (cartList) {
        cartList.style.display = 'block';
        window.cartClosedManually = false;
      }
    });
  }

  // 🔒 Previene que se agregue el event listener más de una vez
  if (!window.qtyBtnHandlerAdded) {
    document.body.addEventListener("click", (e) => {
      const btn = e.target.closest(".qty-btn");
      if (!btn) return;

      const wrapper = btn.closest(".quantity-wrapper");
      const input = wrapper.querySelector(".js-cart-line-product-quantity");
      if (!input) return;

      const min = parseInt(input.getAttribute("min")) || 1;
      const downUrl = input.dataset.downUrl;
      const upUrl = input.dataset.upUrl;
      let currentQty = parseInt(input.value) || min;
      let newQty = currentQty;

      let targetUrl = "";

      if (btn.classList.contains("up")) {
        newQty = currentQty + 1;
        targetUrl = upUrl;
      } else if (btn.classList.contains("down")) {
        newQty = currentQty - 1;

        if (newQty < 1) {
          // Buscar y usar la URL de eliminar producto
          const removeBtn = wrapper.closest('.tvcart-product-content')?.querySelector('.remove-from-cart');
          if (removeBtn && removeBtn.href) {
            fetch(removeBtn.href, {
              method: "POST",
              credentials: "same-origin"
            })
            .then(res => res.json())
            .then(data => {
              prestashop.emit("updateCart", data);
              delayedReloadCart();
            })
            .catch(err => console.error(" Error al eliminar producto:", err));
          }
          return;
        }

        targetUrl = downUrl;
      }

      input.value = newQty;
      animateQty(input);

document.addEventListener('DOMContentLoaded', () => {
  // 1) Animación rápida de pulso
  function animatePulse(el) {
    el.classList.add('qty-animated');
    setTimeout(() => el.classList.remove('qty-animated'), 400);
  }

  // 2) Debounce para agrupar requests
  const debounce = (fn, wait = 500) => {
    let timer;
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => fn(...args), wait);
    };
  };

  // 3) Envío “silencioso” al servidor
  const remoteUpdate = debounce((url) => {
    fetch(url, { method: 'POST', credentials: 'same-origin' })
      .then(r => r.json().catch(() => null))
      .then(data => {
        if (data && data.cart) prestashop.emit('updateCart', data);
      })
      .catch(console.error);
  }, 500);

  // 4) Handler global de "+" y "−"
  document.body.addEventListener('click', e => {
    const btn = e.target.closest('.qty-btn');
    if (!btn) return;
    const line = btn.closest('.cart-line');
    const input = line.querySelector('.js-cart-line-product-quantity');
    const totalEl= line.querySelector('.line-total');
    let qty = parseInt(input.value, 10);
    const min = parseInt(input.min, 10) || 1;

    // Determinar nueva cantidad y URL
    let url;
    if (btn.classList.contains('up')) {
      qty++;
      url = input.dataset.upUrl;
    } else {
      qty = Math.max(min, qty - 1);
      url = input.dataset.downUrl;
    }

    // 5) Actualizo **inmediato** en el DOM
    input.value = qty;
    const unit = parseFloat(input.dataset.unitPrice);
    const fmt  = input.dataset.priceFormat;
    const total= (unit * qty).toFixed(0);
    const formatted = fmt.replace('%s', total.replace(/\B(?=(\d{3})+(?!\d))/g, '.'));
    totalEl.textContent = formatted;
    animatePulse(totalEl);

    // 6) Disparo la petición en segundo plano
    remoteUpdate(url);
  });
});


      fetch(targetUrl, {
        method: "POST",
        credentials: "same-origin"
      })
      .then(res => {
        const contentType = res.headers.get("Content-Type");
        if (contentType && contentType.includes("application/json")) {
          return res.json();
        } else {
          return res.text();
        }
      })
      .then(data => {
        if (data && typeof data === "object" && data.cart) {
          prestashop.emit("updateCart", data);
        } else {
          console.log("ℹ️ Cambio aplicado sin emitir updateCart (respuesta no JSON)");
        }

        delayedReloadCart();
      })
      .catch(err => console.error(" Error al actualizar cantidad:", err));
    });

    window.qtyBtnHandlerAdded = true; // Marcamos que ya fue agregado
  }
});

// 🔁 Recarga y actualiza el contenido del carrito
function delayedReloadCart() {
  const cartWrapper = document.querySelector('.tvcart-product-list');
  if (cartWrapper) {
    cartWrapper.style.display = 'block';
  }

  fetch(window.location.href, {
    method: "GET",
    headers: {
      "X-Requested-With": "XMLHttpRequest"
    }
  })
  .then(res => res.text())
  .then(html => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');

    // 🔁 Reemplazo del listado de productos del carrito
    const newCartList = doc.querySelector('.tvcart-product-list');
    const currentCartList = document.querySelector('.tvcart-product-list');
    if (newCartList && currentCartList) {
      currentCartList.innerHTML = newCartList.innerHTML;
    }

    // 🔁 Reemplazo del resumen del carrito
    const newSummary = doc.querySelector('.tvcart-summary');
    const currentSummary = document.querySelector('.tvcart-summary');
    if (newSummary && currentSummary) {
      currentSummary.innerHTML = newSummary.innerHTML;
    }

    // 🔁 Reemplazo del minicart
    const newMiniCart = doc.querySelector('.blockcart');
    const currentMiniCart = document.querySelector('.blockcart');
    if (newMiniCart && currentMiniCart) {
      currentMiniCart.innerHTML = newMiniCart.innerHTML;
    }

    // 🔁 Reemplazo de la cantidad de productos en el checkout
    const newSummaryProducts = doc.querySelector('.cart-summary-products');
    const currentSummaryProducts = document.querySelector('.cart-summary-products');
    if (newSummaryProducts && currentSummaryProducts) {
      currentSummaryProducts.innerHTML = newSummaryProducts.innerHTML;
    }

    console.log(" Carrito, totales, minicart y resumen de checkout actualizados visualmente.");
  })
  .catch(err => console.error(" Error al recargar el carrito:", err));
}

// 🔔 Siempre mantener el carrito visible al recibir updateCart
prestashop.on('updateCart', () => {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList && !window.cartClosedManually) {
    cartList.style.display = 'block';
  }
});

// Bandera global para saber si el usuario cerró el carrito
window.cartClosedManually = false;

// Función para abrir el carrito
function openCart() {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList) {
    cartList.style.display = 'block';
  }
  window.cartClosedManually = false;
}

// Función para cerrar el carrito manualmente (con la X)
function closeCart() {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList) {
    cartList.style.display = 'none';
  }
  window.cartClosedManually = true;
}

// Reaplicar comportamiento luego de que el carrito se actualice con AJAX
prestashop.on('updateCart', () => {
  setTimeout(() => {
    const cartList = document.querySelector('.tvcart-product-list');
    const closeButton = document.querySelector('.close-cart-btn');

    // Solo mostrar el carrito si el usuario no lo cerró manualmente
    if (cartList && !window.cartClosedManually) {
      cartList.style.display = 'block';
    }

    // Reasignar el listener al botón de cerrar
    if (closeButton) {
      closeButton.removeEventListener('click', closeCart); // Evitar duplicados
      closeButton.addEventListener('click', closeCart);
    }
  }); // Espera para asegurar que el DOM esté actualizado
});

// 🔄 Forzar visibilidad del carrito mientras se detectan cambios
(function forceCartVisibleDuringUpdates() {
  const cartList = document.querySelector('.tvcart-product-list');

  if (!cartList) return;

  // Marcar como "actualizando carrito"
  const setCartUpdating = (updating) => {
    cartList.setAttribute('data-updating', updating ? 'true' : 'false');

    if (updating && !window.cartClosedManually) {
      cartList.style.display = 'block';
    }
  };

  // Hook de PrestaShop para detectar eventos relacionados al carrito
  prestashop.on('updateCart', () => {
    setCartUpdating(true);

    // Esperamos un momento para "desmarcar" la actualización
    setTimeout(() => {
      setCartUpdating(false);

      if (!window.cartClosedManually) {
        cartList.style.display = 'block'; // Nos aseguramos de que siga visible
      }
    }); // puedes ajustar este delay según rendimiento del sitio
  });

  // Observa si hay cambios visibles en el carrito
  const observer = new MutationObserver(() => {
    if (!window.cartClosedManually) {
      cartList.style.display = 'block';
    }
  });

  observer.observe(cartList, { childList: true, subtree: true });
})();

SCRIPT DEL HEADER:

//CANTIDADES
  function updateCartQuantity(productId, productAttributeId, newQty) {
    const action = newQty === 0 ? 'delete' : 'update';
    const requestData = {
      ajax: true,
      action: action,
      id_product: productId,
      id_product_attribute: productAttributeId,
    };

    if (action === 'update') {
      requestData.qty = newQty;
    }

    $.post(prestashop.urls.pages.cart, requestData)
      .done(function (resp) {
        prestashop.emit('updateCart', {
          reason: {
            idProduct: productId,
            idProductAttribute: productAttributeId,
            quantity: newQty
          },
          resp: resp
        });
      });
  }

  function bindTouchSpinEvents() {
    $('.js-cart-line-product-quantity').off('change').on('change', function () {
      const $input = $(this);
      const newQty = parseInt($input.val());
      const $line = $input.closest('[data-id-product]');

      const idProduct = $line.data('id-product');
      const idProductAttribute = $line.data('id-product-attribute') || 0;

      if (!isNaN(newQty)) {
        updateCartQuantity(idProduct, idProductAttribute, newQty);
      }
    });
  }

  document.addEventListener('DOMContentLoaded', function () {
    bindTouchSpinEvents();
    prestashop.on('updateCart', function () {
      bindTouchSpinEvents();
    });
  });

//ELIMINAR PRODUCTO
document.addEventListener('DOMContentLoaded', function () {
  document.body.addEventListener('click', function (e) {
    const removeBtn = e.target.closest('.remove-from-cart');

    if (removeBtn && removeBtn.dataset.linkAction === 'delete-from-cart') {
      e.preventDefault(); // Evita la recarga

      const url = removeBtn.getAttribute('href');

      $.post(url, { ajax: true })
        .done(function (resp) {
          prestashop.emit('updateCart', {
            reason: { linkAction: 'delete-from-cart' },
            resp: resp
          });
        })
        .fail(function (err) {
          console.error('Error al eliminar del carrito:', err);
        });
    }
  });
});

//MOSTRAR BLOQUE CARRITO DE COMPRAS AL DARLE CLIC AL CONTADOR DE ITEMS
  document.addEventListener('DOMContentLoaded', function () {
    prestashop.on('updateCart', function () {
      const blockCart = document.querySelector('.blockcart');
      if (blockCart) {
        blockCart.classList.add('show-cart-panel');
      }
    });

  // Evita cerrar el carrito si se hace clic en un botón de cantidad o dentro del bloque del carrito
  const clickedQtyBtn = event.target.closest('.qty-btn, .quantity-wrapper, .js-cart-line-product-quantity');

  if (
    blockCart &&
    !blockCart.contains(event.target) &&
    !clickedQtyBtn && // 👈 Añadido para ignorar clicks sobre botones o inputs de cantidad
    blockCart.classList.contains('show-cart-panel')
  ) {
    blockCart.classList.remove('show-cart-panel');
  };

  });

//ALTERNAR MOSTRAR-OCULTAR BLOQUE DE CARRITO DE COMPRAS AL DARLE CLIC AL CONTADOR DE ITEMS
document.addEventListener('DOMContentLoaded', function () {
  document.body.addEventListener('click', function (e) {
    const cartTrigger = e.target.closest('.tvshopping-cart-containt-box, .tvshopping-cart-inner, .tvshopping-cart-price');

    if (cartTrigger) {
      const cartList = document.querySelector('.tvcart-product-list');
      if (cartList) {
        const currentDisplay = window.getComputedStyle(cartList).display;
        cartList.style.display = (currentDisplay === 'block') ? 'none' : 'block';
      }
    }
  });
});

//ELIMINA UN PRODUCTO DEL CARRITO SI ÉSTE BAJA DE CANTIDAD 1
document.addEventListener('DOMContentLoaded', function () {
  // 1. Si el usuario baja desde 1 con el botón de restar (↓)
  document.body.addEventListener('click', function (e) {
    const downBtn = e.target.closest('.bootstrap-touchspin-down, .touchspin-down');
    if (!downBtn) return;

    const input = downBtn.closest('.cart-line')?.querySelector('.js-cart-line-product-quantity');
    if (!input) return;

    const currentQty = parseInt(input.value);
    const updateUrl = input.dataset.updateUrl;

    if (currentQty === 1 && updateUrl) {
      e.preventDefault();

      $.post(updateUrl, { ajax: true, qty: 0 })
        .done(function (resp) {
          prestashop.emit('updateCart', {
            reason: {
              idProduct: input.dataset.productId,
              linkAction: 'delete-from-cart',
              quantity: 0
            },
            resp: resp
          });
        })
        .fail(function (err) {
          console.error('Error al eliminar con botón ↓:', err);
        });
    }
  });

  // 2. Si el usuario escribe 0 manualmente
  document.body.addEventListener('change', function (e) {
    const input = e.target.closest('.js-cart-line-product-quantity');
    if (!input) return;

    const newQty = parseInt(input.value);
    const updateUrl = input.dataset.updateUrl;

    if (newQty <= 0 && updateUrl) {
      $.post(updateUrl, { ajax: true, qty: 0 })
        .done(function (resp) {
          prestashop.emit('updateCart', {
            reason: {
              idProduct: input.dataset.productId,
              linkAction: 'delete-from-cart',
              quantity: 0
            },
            resp: resp
          });
        })
        .fail(function (err) {
          console.error('Error al eliminar al escribir 0:', err);
        });
    }
  });
});

//OCULTA LA OPCIÓN "CREAR CUENTA" SI EL USUARIO ESTÁ LOGUEADO
  document.addEventListener('DOMContentLoaded', function () {
    if (document.body.classList.contains('page-customer-logged-in')) {
      const registerLink = document.querySelector('.register-link');
      if (registerLink) {
        registerLink.style.display = 'none';
      }
    }
  });

  document.addEventListener('DOMContentLoaded', function () {
    var isLogged = document.body.classList.contains('page-customer-logged-in');
    if (isLogged) {
      var registerLink = document.querySelector('li.register-link');
      if (registerLink) {
        registerLink.style.display = 'none';
      }
    }
  });

  document.addEventListener('DOMContentLoaded', function () {
    var isLoggedIn = document.querySelector('.user-info a[href*="my-account"]');
    if (isLoggedIn) {
      var regLink = document.querySelector('.register-link');
      if (regLink) regLink.style.display = 'none';
    }
  });

  (function () {
    const CART_SELECTOR = '.tvcart-product-list';
    const TOGGLE_BTN_SEL = '.tvshopping-cart-containt-box, .tvshopping-cart-inner, .tvshopping-cart-price';
    const CLOSE_BTN_SEL = '.close-cart-btn';
    const STORAGE_KEY = 'alishop_cart_closed';

    // Devuelve true si estaba marcado como cerrado
    function isClosed() {
      return localStorage.getItem(STORAGE_KEY) === 'true';
    }

    function applyState() {
      const cart = document.querySelector(CART_SELECTOR);
      if (!cart) { console.warn('Cart selector no encontrado:', CART_SELECTOR); return; }
      if (isClosed()) {
        console.log('[Cart] Estado: CERRADO (hide)');
        cart.style.display = 'none';
      } else {
        console.log('[Cart] Estado: ABIERTO (show)');
        cart.style.display = 'block';
      }
    }

    function openCart() {
      console.log('[Cart] openCart()');
      localStorage.setItem(STORAGE_KEY, 'false');
      applyState();
    }

    function closeCart() {
      console.log('[Cart] closeCart()');
      localStorage.setItem(STORAGE_KEY, 'true');
      applyState();
    }

    // Inicializamos al cargar la página
    document.addEventListener('DOMContentLoaded', function () {
      console.log('[Cart] DOMContentLoaded → aplicando estado guardado');
      applyState();

      // Toggle al clic en el ícono de items
      document.body.addEventListener('click', function (e) {
        if (e.target.closest(TOGGLE_BTN_SEL)) {
          e.preventDefault();
          console.log('[Cart] Click en TOGGLE_BTN_SEL');
          openCart();
        }
      });

      // Cerrar al clic en la X
      document.body.addEventListener('click', function (e) {
        if (e.target.closest(CLOSE_BTN_SEL)) {
          e.preventDefault();
          console.log('[Cart] Click en CLOSE_BTN_SEL');
          closeCart();
        }
      });

      // Cada vez que PrestaShop emita updateCart tras AJAX
      if (window.prestashop) {
        prestashop.on('updateCart', function () {
          console.log('[Cart] evento updateCart recibido');
          applyState();
        });
      } else {
        console.warn('PrestaShop JS no encontrado');
      }
    });
  })();

function forceReflow() {
  document.body.style.display = 'none';
  // lectura forzada:
  void document.body.offsetHeight;
  document.body.style.display = '';
}

window.addEventListener('load', function () {
    document.querySelector('ul#top-menu').style.display = 'flex';
});

document.addEventListener('DOMContentLoaded', function () {
    const currentUrl = window.location.href.split('#')[0].split('?')[0];

    document.querySelectorAll('a.dropdown-item.d-flex').forEach(function (link) {
      const linkUrl = link.href.split('#')[0].split('?')[0];

      if (linkUrl === currentUrl) {
        link.classList.add('active');
      }
    });
  });

 

Hola, pues tengo el carrito de prestashop, lo estuve modificando un poco, pero necesito que tenga una respuesta inmediata.

Suma, resta, muestra cantidad, elimina producto; pero eso, lo hace con un pequeño delay cuando uno apreta varias veces el botón de "+" o "-".

¿Alguna idea?

Ya minimizé gran parte del código .js, .css, y .tpl

PRESTASHOP 8.2.0

Html:

<button type="button" class="close-cart-btn" onclick="window.cartClosedManually=true;document.querySelectorAll('.tvcart-product-list').forEach(el=>el.style.display='none');" aria-label="Cerrar carrito" title="Cerrar">&times;</button><a href="{$product.url}"><div class="tvcart-product-list-img"><img class='tvimage-lazy' src="{$product.cover.bySize.cart_default.url}"></div><div class="tvcart-product-content"><div class="tvcart-product-list-quentity"><div class="tvcart-dropdown-title"><span class="product-quentity">{$product.quantity}</span><span class="tvshopping-cart-quentity">&nbsp;x&nbsp;</span><span class="product-name">{$product.name}</span></div><div class="tvcart-product-remove">{$url='controller=cart&delete='|cat:$product.id_product}<a class="remove-from-cart tvcmsremove-from-cart" rel="nofollow" href="{$product.remove_from_cart_url}" data-link-action="delete-from-cart" data-id-product="{$product.id_product|escape:'javascript'}" data-id-product-attribute="{$product.id_product_attribute|escape:'javascript'}" data-id-customization="{$product.id_customization|escape:'javascript'}" title="{l s='remove from cart' d='Shop.Theme.Actions'}"><i class="material-icons">&#xe5cd;</i></a></div></div><div class="tvcart-product-list-price"><span class="product-price" data-unit-price="{$product.unit_price|floatval}" data-price-format="{$product.price}" data-total-price-el>{$product.total}</span><span class="product-price" style="color:#fff;font-size:xx-small;">&nbsp;{$product.unit_price_full}</span>{if isset($product.unit_price_full)&&!empty($product.unit_price_full)}{if isset($cart)&&isset($cart.products)&&count($cart.products)>0}{assign var="unit_text" value=$product.unit_price_full|regex_replace:'/^[^ ]* /':''}{else}<span class="product-price" style="color:#fff;font-size:xx-small;">{$product.unit_price_full}</span>{/if}{else}{hook h='displayProductPriceBlock' product=$product type="before_price"}<span class="product-price" style="color:#fff;font-size:xx-small;">PRECIO DESDE UNA UNID</span>{/if}{hook h='displayProductPriceBlock' product=$product type="unit_price"}{foreach from=$product.attributes item="property_value" key="property"}<span style="display:none;"><strong>{$property}</strong>: {$property_value}</span><br>{/foreach}<p style="color:#fff;font-weight:700;">{l s='Quantity:' d='Shop.Theme.Checkout'}&nbsp;{$product.cart_quantity}<br></p>{if isset($product.is_gift)&&$product.is_gift}<span class="gift-quantity product-cart-qty-{$product.id_product}-{$product.id_product_attribute}" style="display:none!important;">{$product.quantity}</span>{else}<div class="cart-line" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}"><div class="cart-line" data-id-product="{$product.id_product}" data-id-product-attribute="{$product.id_product_attribute}" data-id-customization="{$product.id_customization}"><div class="quantity-wrapper"><button class="qty-btn down" aria-label="Restar cantidad"></button><input class="js-cart-line-product-quantity" type="text" value="{$product.quantity}" min="{$product.minimal_quantity}" readonly data-product-id="{$product.id_product}" data-product-attribute-id="{$product.id_product_attribute}" data-update-url="{$product.update_quantity_url}" data-up-url="{$product.up_quantity_url}" data-down-url="{$product.down_quantity_url}"><button class="qty-btn up" aria-label="Sumar cantidad">+</button></div></div></div>{/if}<a class="remove-from-cart" rel="nofollow" href="{$product.remove_from_cart_url}" data-link-action="delete-from-cart" data-id-product="{$product.id_product|escape:'javascript'}" data-id-product-attribute="{$product.id_product_attribute|escape:'javascript'}" data-id-customization="{$product.id_customization|escape:'javascript'}"></a>{block name='hook_cart_extra_product_actions'}{hook h='displayCartExtraProductActions' product=$product}{/block}<div class="tvcart-product-list-attribute">{foreach $product.attributes as $prod_attb=>$prod_val}<div class="tvcart-product-attr"><span>{$prod_attb}:</span> {$prod_val}</div>{/foreach}</div>{if $product.customizations|count}<div class="customizations"><ul>{foreach from=$product.customizations item='customization'}<li><span class="product-quantity">{$customization.quantity}</span><a href="{$customization.remove_from_cart_url}" title="{l s='remove from cart' d='Shop.Theme.Actions'}" class="remove-from-cart" rel="nofollow">{l s='Remove' d='Shop.Theme.Actions'}</a><ul>{foreach from=$customization.fields item='field'}<li><span>{$field.label}</span>{if $field.type=='text'}<span>{$field.text nofilter}</span>{else if $field.type=='image'}<img class='tvimage-lazy' src="{$field.image.small.url}">{/if}</li>{/foreach}</ul></li>{/foreach}</ul></div>{/if}</div></a>

Style:

.close-cart-btn {
  font-size: xx-large;
  position: absolute;
  width: 30px;
  height: 30px;
  top: -35px;
  right: auto;
  align-items: center;
  align-content: center;
  align-self: center;
  cursor: pointer;
  display: flex;
  z-index: 9999;
  justify-content: center;
}
span.input-group-btn-vertical {
    width: 12% !important;
    display: flex !important;
    flex-direction: column;
}
input.js-cart-line-product-quantity.form-control {
    width: 50px !important;
}
input.js-cart-line-product-quantity {
    width: 50px !important;
    height: 30px !important;
    margin: -5px;
}
.quantity-wrapper {
  display: flex;
  align-items: center;
  gap: 5px;
}
.qty-btn {
  width: 30px;
  height: 30px;
  font-size: 18px;
  cursor: pointer;
}
@keyframes bounceQty {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.2); }
  60%  { transform: scale(0.95); }
  100% { transform: scale(1); }
}
.qty-animated {
  animation: bounceQty 0.4s ease;
}

Script:

document.addEventListener('DOMContentLoaded', () => {
  console.log("🛒 Script de cantidad actualizado y funcionando");

  function animateQty(input) {
    input.classList.add("qty-animated");
    setTimeout(() => input.classList.remove("qty-animated"), 400);
  }

  const toggleBtn = document.querySelector('.tvshopping-cart-price');

  if (toggleBtn) {
    toggleBtn.addEventListener('click', () => {
    openCart();
      const cartList = document.querySelector('.tvcart-product-list');
      if (cartList) {
        cartList.style.display = 'block';
        window.cartClosedManually = false;
      }
    });
  }

  // 🔒 Previene que se agregue el event listener más de una vez
  if (!window.qtyBtnHandlerAdded) {
    document.body.addEventListener("click", (e) => {
      const btn = e.target.closest(".qty-btn");
      if (!btn) return;

      const wrapper = btn.closest(".quantity-wrapper");
      const input = wrapper.querySelector(".js-cart-line-product-quantity");
      if (!input) return;

      const min = parseInt(input.getAttribute("min")) || 1;
      const downUrl = input.dataset.downUrl;
      const upUrl = input.dataset.upUrl;
      let currentQty = parseInt(input.value) || min;
      let newQty = currentQty;

      let targetUrl = "";

      if (btn.classList.contains("up")) {
        newQty = currentQty + 1;
        targetUrl = upUrl;
      } else if (btn.classList.contains("down")) {
        newQty = currentQty - 1;

        if (newQty < 1) {
          // Buscar y usar la URL de eliminar producto
          const removeBtn = wrapper.closest('.tvcart-product-content')?.querySelector('.remove-from-cart');
          if (removeBtn && removeBtn.href) {
            fetch(removeBtn.href, {
              method: "POST",
              credentials: "same-origin"
            })
            .then(res => res.json())
            .then(data => {
              prestashop.emit("updateCart", data);
              delayedReloadCart();
            })
            .catch(err => console.error(" Error al eliminar producto:", err));
          }
          return;
        }

        targetUrl = downUrl;
      }

      input.value = newQty;
      animateQty(input);

document.addEventListener('DOMContentLoaded', () => {
  // 1) Animación rápida de pulso
  function animatePulse(el) {
    el.classList.add('qty-animated');
    setTimeout(() => el.classList.remove('qty-animated'), 400);
  }

  // 2) Debounce para agrupar requests
  const debounce = (fn, wait = 500) => {
    let timer;
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => fn(...args), wait);
    };
  };

  // 3) Envío “silencioso” al servidor
  const remoteUpdate = debounce((url) => {
    fetch(url, { method: 'POST', credentials: 'same-origin' })
      .then(r => r.json().catch(() => null))
      .then(data => {
        if (data && data.cart) prestashop.emit('updateCart', data);
      })
      .catch(console.error);
  }, 500);

  // 4) Handler global de "+" y "−"
  document.body.addEventListener('click', e => {
    const btn = e.target.closest('.qty-btn');
    if (!btn) return;
    const line = btn.closest('.cart-line');
    const input = line.querySelector('.js-cart-line-product-quantity');
    const totalEl= line.querySelector('.line-total');
    let qty = parseInt(input.value, 10);
    const min = parseInt(input.min, 10) || 1;

    // Determinar nueva cantidad y URL
    let url;
    if (btn.classList.contains('up')) {
      qty++;
      url = input.dataset.upUrl;
    } else {
      qty = Math.max(min, qty - 1);
      url = input.dataset.downUrl;
    }

    // 5) Actualizo **inmediato** en el DOM
    input.value = qty;
    const unit = parseFloat(input.dataset.unitPrice);
    const fmt  = input.dataset.priceFormat;
    const total= (unit * qty).toFixed(0);
    const formatted = fmt.replace('%s', total.replace(/\B(?=(\d{3})+(?!\d))/g, '.'));
    totalEl.textContent = formatted;
    animatePulse(totalEl);

    // 6) Disparo la petición en segundo plano
    remoteUpdate(url);
  });
});


      fetch(targetUrl, {
        method: "POST",
        credentials: "same-origin"
      })
      .then(res => {
        const contentType = res.headers.get("Content-Type");
        if (contentType && contentType.includes("application/json")) {
          return res.json();
        } else {
          return res.text();
        }
      })
      .then(data => {
        if (data && typeof data === "object" && data.cart) {
          prestashop.emit("updateCart", data);
        } else {
          console.log("ℹ️ Cambio aplicado sin emitir updateCart (respuesta no JSON)");
        }

        delayedReloadCart();
      })
      .catch(err => console.error(" Error al actualizar cantidad:", err));
    });

    window.qtyBtnHandlerAdded = true; // Marcamos que ya fue agregado
  }
});

// 🔁 Recarga y actualiza el contenido del carrito
function delayedReloadCart() {
  const cartWrapper = document.querySelector('.tvcart-product-list');
  if (cartWrapper) {
    cartWrapper.style.display = 'block';
  }

  fetch(window.location.href, {
    method: "GET",
    headers: {
      "X-Requested-With": "XMLHttpRequest"
    }
  })
  .then(res => res.text())
  .then(html => {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');

    // 🔁 Reemplazo del listado de productos del carrito
    const newCartList = doc.querySelector('.tvcart-product-list');
    const currentCartList = document.querySelector('.tvcart-product-list');
    if (newCartList && currentCartList) {
      currentCartList.innerHTML = newCartList.innerHTML;
    }

    // 🔁 Reemplazo del resumen del carrito
    const newSummary = doc.querySelector('.tvcart-summary');
    const currentSummary = document.querySelector('.tvcart-summary');
    if (newSummary && currentSummary) {
      currentSummary.innerHTML = newSummary.innerHTML;
    }

    // 🔁 Reemplazo del minicart
    const newMiniCart = doc.querySelector('.blockcart');
    const currentMiniCart = document.querySelector('.blockcart');
    if (newMiniCart && currentMiniCart) {
      currentMiniCart.innerHTML = newMiniCart.innerHTML;
    }

    // 🔁 Reemplazo de la cantidad de productos en el checkout
    const newSummaryProducts = doc.querySelector('.cart-summary-products');
    const currentSummaryProducts = document.querySelector('.cart-summary-products');
    if (newSummaryProducts && currentSummaryProducts) {
      currentSummaryProducts.innerHTML = newSummaryProducts.innerHTML;
    }

    console.log(" Carrito, totales, minicart y resumen de checkout actualizados visualmente.");
  })
  .catch(err => console.error(" Error al recargar el carrito:", err));
}

// 🔔 Siempre mantener el carrito visible al recibir updateCart
prestashop.on('updateCart', () => {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList && !window.cartClosedManually) {
    cartList.style.display = 'block';
  }
});

// Bandera global para saber si el usuario cerró el carrito
window.cartClosedManually = false;

// Función para abrir el carrito
function openCart() {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList) {
    cartList.style.display = 'block';
  }
  window.cartClosedManually = false;
}

// Función para cerrar el carrito manualmente (con la X)
function closeCart() {
  const cartList = document.querySelector('.tvcart-product-list');
  if (cartList) {
    cartList.style.display = 'none';
  }
  window.cartClosedManually = true;
}

// Reaplicar comportamiento luego de que el carrito se actualice con AJAX
prestashop.on('updateCart', () => {
  setTimeout(() => {
    const cartList = document.querySelector('.tvcart-product-list');
    const closeButton = document.querySelector('.close-cart-btn');

    // Solo mostrar el carrito si el usuario no lo cerró manualmente
    if (cartList && !window.cartClosedManually) {
      cartList.style.display = 'block';
    }

    // Reasignar el listener al botón de cerrar
    if (closeButton) {
      closeButton.removeEventListener('click', closeCart); // Evitar duplicados
      closeButton.addEventListener('click', closeCart);
    }
  }); // Espera para asegurar que el DOM esté actualizado
});

// 🔄 Forzar visibilidad del carrito mientras se detectan cambios
(function forceCartVisibleDuringUpdates() {
  const cartList = document.querySelector('.tvcart-product-list');

  if (!cartList) return;

  // Marcar como "actualizando carrito"
  const setCartUpdating = (updating) => {
    cartList.setAttribute('data-updating', updating ? 'true' : 'false');

    if (updating && !window.cartClosedManually) {
      cartList.style.display = 'block';
    }
  };

  // Hook de PrestaShop para detectar eventos relacionados al carrito
  prestashop.on('updateCart', () => {
    setCartUpdating(true);

    // Esperamos un momento para "desmarcar" la actualización
    setTimeout(() => {
      setCartUpdating(false);

      if (!window.cartClosedManually) {
        cartList.style.display = 'block'; // Nos aseguramos de que siga visible
      }
    }); // puedes ajustar este delay según rendimiento del sitio
  });

  // Observa si hay cambios visibles en el carrito
  const observer = new MutationObserver(() => {
    if (!window.cartClosedManually) {
      cartList.style.display = 'block';
    }
  });

  observer.observe(cartList, { childList: true, subtree: true });
})();

 

×
×
  • Create New...